Knot DNS Resolver modules

Static hints

This is a module providing static hints for forward records (A/AAAA) and reverse records (PTR). The records can be loaded from /etc/hosts-like files and/or added directly.

You can also use the module to change root hints. They are used as a safety belt or if the root NS drops out of cache.


-- Load hints after iterator
modules = { 'hints > iterate' }
-- Load hints before rrcache, custom hosts file
modules = { ['hints < rrcache'] = 'hosts.custom' }
-- Add root hints
  [''] = { '2001:503:c27::2:30', '' }
-- Add a custom hint
hints['localhost'] = ''


  • path (string) – path to hosts-like file, default: no file

{ result: bool }

Clear any configured hints, and optionally load a hosts-like file as in hints.add_hosts(path). (Root hints are not touched.)

  • path (string) – path to hosts-like file, default: /etc/hosts

Add hints from a host-like file.

  • hostname (string) – i.e. "localhost"

{ result: [address1, address2, ...] }

Return list of address record matching given name. If no hostname is specified, all hints are returned in the table format used by hints.root().

  • pair (string) – hostname address i.e. "localhost"

{ result: bool }

Add a hostname - address pair hint.


If multiple addresses have been added for a name, all are returned in a forward query. If multiple names have been added to an address, the last one defined is returned in a corresponding PTR query.

  • pair (string) – hostname address i.e. "localhost", or just hostname

{ result: bool }

Remove a hostname - address pair hint. If address is omitted, all addresses for the given name are deleted.

Returns:{ [''] = { '', '', ...}, ... }


If no parameters are passed, returns current root hints set.

  • root_hints (table) – new set of root hints i.e. {['name'] = 'addr', ...}

{ [''] = { '', '', ...}, ... }

Replace current root hints and return the current table of root hints.


> hints.root({
  [''] = '',
  [''] = ''
[] => {
  [1] =>
[] => {
  [1] =>


A good rule of thumb is to select only a few fastest root hints. The server learns RTT and NS quality over time, and thus tries all servers available. You can help it by preselecting the candidates.

Statistics collector

This modules gathers various counters from the query resolution and server internals, and offers them as a key-value storage. Any module may update the metrics or simply hook in new ones.

-- Enumerate metrics
> stats.list()
[answer.cached] => 486178
[iterator.tcp] => 490
[answer.noerror] => 507367
[] => 618631
[iterator.udp] => 102408
[query.concurrent] => 149

-- Query metrics by prefix
> stats.list('iter')
[iterator.udp] => 105104
[iterator.tcp] => 490

-- Set custom metrics from modules
> stats['filter.match'] = 5
> stats['filter.match']

-- Fetch most common queries
> stats.frequent()
[1] => {
        [type] => 2
        [count] => 4
        [name] => cz.

-- Fetch most common queries (sorted by frequency)
> table.sort(stats.frequent(), function (a, b) return a.count > b.count end)

-- Show recently contacted authoritative servers
> stats.upstreams()
[2a01:618:404::1] => {
    [1] => 26 -- RTT
[] => {
    [1] => 31 - RTT


  • key (string) – i.e. ""


Return nominal value of given metric.

stats.set(key, val)
  • key (string) – i.e. ""
  • val (number) – i.e. 5

Set nominal value of given metric.

  • prefix (string) – optional metric prefix, i.e. "answer" shows only metrics beginning with “answer”

Outputs collected metrics as a JSON dictionary.


Outputs a list of recent upstreams and their RTT. It is sorted by time and stored in a ring buffer of a fixed size. This means it’s not aggregated and readable by multiple consumers, but also that you may lose entries if you don’t read quickly enough. The default ring size is 512 entries, and may be overriden on compile time by -DUPSTREAMS_COUNT=X.


Outputs list of most frequent iterative queries as a JSON array. The queries are sampled probabilistically, and include subrequests. The list maximum size is 5000 entries, make diffs if you want to track it over time.


Clear the list of most frequent iterative queries.


Outputs list of soon-to-expire records as a JSON array. The list maximum size is 5000 entries, make diffs if you want to track it over time.


Clear the list of soon expiring records.

Built-in statistics

  • - total number of answered queries
  • answer.cached - number of queries answered from cache
  • answer.noerror - number of NOERROR answers
  • answer.nodata - number of NOERROR, but empty answers
  • answer.nxdomain - number of NXDOMAIN answers
  • answer.servfail - number of SERVFAIL answers
  • answer.1ms - number of answers completed in 1ms
  • answer.10ms - number of answers completed in 10ms
  • answer.50ms - number of answers completed in 50ms
  • answer.100ms - number of answers completed in 100ms
  • answer.250ms - number of answers completed in 250ms
  • answer.500ms - number of answers completed in 500ms
  • answer.1000ms - number of answers completed in 1000ms
  • answer.1500ms - number of answers completed in 1500ms
  • answer.slow - number of answers that took more than 1500ms
  • query.edns - number of queries with EDNS
  • query.dnssec - number of queries with DNSSEC DO=1

Query policies

This module can block, rewrite, or alter inbound queries based on user-defined policies. By default, it blocks queries to reverse lookups in private subnets as per RFC 1918, RFC 5735 and RFC 5737. You can however extend it to deflect Slow drip DNS attacks for example, or gray-list resolution of misbehaving zones.

There are several policies implemented:

  • pattern - applies action if QNAME matches regular expression
  • suffix - applies action if QNAME suffix matches given list of suffixes (useful for “is domain in zone” rules), uses Aho-Corasick string matching algorithm implemented by @jgrahamc (CloudFlare, Inc.) (BSD 3-clause)
  • rpz - implementes a subset of the RPZ format. Currently it can be used with a zonefile, a binary database support is on the way. Binary database can be updated by an external process on the fly.
  • custom filter function

There are several defined actions:

  • PASS - let the query pass through
  • DENY - return NXDOMAIN answer
  • DROP - terminate query resolution, returns SERVFAIL to requestor
  • TC - set TC=1 if the request came through UDP, forcing client to retry with TCP
  • FORWARD(ip) - solve a query via forwarding to an IP while validating and caching locally; the parameter can be a single IP (string) or a lua list of up to four IPs.
  • STUB(ip) - similar to FORWARD(ip) but without attempting DNSSEC validation. Each request may be either answered from cache or simply sent to one of the IPs with proxying back the answer.
  • MIRROR(ip) - mirror query to given IP and continue solving it (useful for partial snooping)
  • REROUTE({{subnet,target}, ...}) - reroute addresses in response matching given subnet to given target, e.g. {'', ''} will rewrite ‘’ to ‘’, see renumber module for more information.
  • QTRACE - pretty-print DNS response packets into the log (useful for debugging weird DNS servers).
  • FLAGS(set, clear) - set and/or clear some flags for the query. There can be multiple flags to set/clear, combined by bit.bor from kres.query.* values.


The policy module currently only looks at whole DNS requests. The rules won’t be re-applied e.g. when following CNAMEs.


The module (and kres) expects domain names in wire format, not textual representation. So each label in name is prefixed with its length, e.g. “” equals to "\7example\3com". You can use convenience function todname('') for automatic conversion.

Example configuration

      -- Load default policies
      modules = { 'policy' }
      -- Whitelist 'www[0-9]'
      policy.add(policy.pattern(policy.PASS, '\4www[0-9]\6badboy\2cz'))
      -- Block all names below
      policy.add(policy.suffix(policy.DENY, {todname('')}))
      -- Custom rule
      policy.add(function (req, query)
              if query:qname():find('%d.%d.%d.224\7in-addr\4arpa') then
                      return policy.DENY
      -- Disallow ANY queries
      policy.add(function (req, query)
              if query.stype == kres.type.ANY then
                      return policy.DROP
      -- Enforce local RPZ
      policy.add(policy.rpz(policy.DENY, 'blacklist.rpz'))
      -- Forward all queries below '' to given resolver
      policy.add(policy.suffix(policy.FORWARD(''), {todname('')}))
      -- Forward all queries matching pattern
      policy.add(policy.pattern(policy.FORWARD('2001:DB8::1'), '\4bad[0-9]\2cz'))
      -- Forward all queries (complete stub mode)
      -- Print all responses with matching suffix
      policy.add(policy.suffix(policy.QTRACE, {todname('')}))
      -- Print all responses
-- Mirror all queries and retrieve information
local rule = policy.add(policy.all(policy.MIRROR('')))
-- Print information about the rule
print(string.format('id: %d, matched queries: %d',, rule.count)
-- Reroute all addresses found in answer from to 127.0.0.x
-- this policy is enforced on answers, therefore 'postrule'
local rule = policy.add(policy.REROUTE({'', ''}), true)
-- Delete rule that we just created



Pass-through all queries matching the rule.


Respond with NXDOMAIN to all queries matching the rule.


Drop all queries matching the rule.


Respond with empty answer with TC bit set (if the query came through UDP).

policy.FORWARD (address)

Forward query to given IP address.

policy.MIRROR (address)

Forward query to given IP address.

policy.REROUTE({{subnet,target}, ...})

Reroute addresses in response matching given subnet to given target, e.g. {'', ''} will rewrite ‘’ to ‘’.


Print pretty-formate (dig-like) DNS answers for current query and all its subqueries that Knot Resolver receive from upstream (authoritative) DNS servers. Very useful when dealing with non-compliant DNS servers that violate DNS protocol.

policy.add(rule, postrule)
  • rule – added rule, i.e. policy.pattern(policy.DENY, '[0-9]+\2cz')
  • postrule – boolean, if true the rule will be evaluated on answer instead of query

rule description

Add a new policy rule that is executed either or queries or answers, depending on the postrule parameter. You can then use the returned rule description to get information and unique identifier for the rule, as well as match count.

  • id – identifier of a given rule


Remove a rule from policy list.

  • action – executed action for all queries

Perform action for all queries (no filtering).

policy.pattern(action, pattern)
  • action – action if the pattern matches QNAME
  • pattern – regular expression

Policy to block queries based on the QNAME regex matching.

policy.suffix(action, suffix_table)
  • action – action if the pattern matches QNAME
  • suffix_table – table of valid suffixes

Policy to block queries based on the QNAME suffix match.

policy.suffix_common(action, suffix_table[, common_suffix])
  • action – action if the pattern matches QNAME
  • suffix_table – table of valid suffixes
  • common_suffix – common suffix of entries in suffix_table

Like suffix match, but you can also provide a common suffix of all matches for faster processing (nil otherwise). This function is faster for small suffix tables (in the order of “hundreds”).

policy.rpz(action, path[, format])
  • action – the default action for match in the zone (e.g. RH-value .)
  • path – path to zone file | database

Enforce RPZ rules. This can be used in conjunction with published blocklist feeds. The RPZ operation is well described in this Jan-Piet Mens’s post, or the Pro DNS and BIND book. Here’s compatibility table:

Policy Action RH Value Support
NODATA *. partial, implemented as NXDOMAIN
Unchanged rpz-passthru. yes
Nothing rpz-drop. yes
Truncated rpz-tcp-only. yes
Modified anything no
Policy Trigger Support
CLIENT-IP partial, may be done with views
IP no
NS-IP no
policy.todnames({name, ...})
Param:names table of domain names in textual format

Returns table of domain names in wire format converted from strings.

-- Convert single name
assert(todname('') == '\7example\3com\0')
-- Convert table of names
policy.todnames({'', ''})
{ '\7example\3com\0', '\2me\2cz\0' }

Views and ACLs

The policy module implements policies for global query matching, e.g. solves “how to react to certain query”. This module combines it with query source matching, e.g. “who asked the query”. This allows you to create personalized blacklists, filters and ACLs, sort of like ISC BIND views.

There are two identification mechanisms:

  • subnet - identifies the client based on his subnet
  • tsig - identifies the client based on a TSIG key

You can combine this information with policy rules.

view:addr('', policy.suffix(policy.TC, {'\7example\3com'}))

This fill force given client subnet to TCP for names in You can combine view selectors with RPZ to create personalized filters for example.

Example configuration

-- Load modules
modules = { 'policy', 'view' }
-- Whitelist queries identified by TSIG key
view:tsig('\5mykey', function (req, qry) return policy.PASS end)
-- Block local clients (ACL like)
view:addr('', function (req, qry) return policy.DENY end))
-- Drop queries with suffix match for remote client
view:addr('', policy.suffix(policy.DROP, {'\3xxx'}))
-- RPZ for subset of clients
view:addr('', policy.rpz(policy.PASS, 'whitelist.rpz'))
-- Forward all queries from given subnet to proxy
view:addr('', policy.all(policy.FORWARD('2001:DB8::1')))
-- Drop everything that hasn't matched
view:addr('', function (req, qry) return policy.DROP end)


view:addr(subnet, rule)
  • subnet – client subnet, i.e.
  • rule – added rule, i.e. policy.pattern(policy.DENY, '[0-9]+\2cz')

Apply rule to clients in given subnet.

view:tsig(key, rule)
  • key – client TSIG key domain name, i.e. \5mykey
  • rule – added rule, i.e. policy.pattern(policy.DENY, '[0-9]+\2cz')

Apply rule to clients with given TSIG key.


This just selects rule based on the key name, it doesn’t verify the key or signature yet.

Prefetching records

The module refreshes records that are about to expire when they’re used (having less than 1% of original TTL). This improves latency for frequently used records, as they are fetched in advance.

It is also able to learn usage patterns and repetitive queries that the server makes. For example, if it makes a query every day at 18:00, the resolver expects that it is needed by that time and prefetches it ahead of time. This is helpful to minimize the perceived latency and keeps the cache hot.


The tracking window and period length determine memory requirements. If you have a server with relatively fast query turnover, keep the period low (hour for start) and shorter tracking window (5 minutes). For personal slower resolver, keep the tracking window longer (i.e. 30 minutes) and period longer (a day), as the habitual queries occur daily. Experiment to get the best results.

Example configuration


This module requires ‘stats’ module to be present and loaded.

modules = {
        predict = {
                window = 15, -- 15 minutes sampling window
                period = 6*(60/15) -- track last 6 hours

Defaults are 15 minutes window, 6 hours period.


Use period 0 to turn off prediction and just do prefetching of expiring records. That works even without the ‘stats’ module.

Exported metrics

To visualize the efficiency of the predictions, the module exports following statistics.

  • predict.epoch - current prediction epoch (based on time of day and sampling window)
  • predict.queue - number of queued queries in current window
  • predict.learned - number of learned queries in current window


predict.config({ window = 15, period = 24})

Reconfigure the predictor to given tracking window and period length. Both parameters are optional. Window length is in minutes, period is a number of windows that can be kept in memory. e.g. if a window is 15 minutes, a period of “24” means 6 hours.

HTTP/2 services

This is a module that does the heavy lifting to provide an HTTP/2 enabled server that supports TLS by default and provides endpoint for other modules in order to enable them to export restful APIs and websocket streams. One example is statistics module that can stream live metrics on the website, or publish metrics on request for Prometheus scraper.

The server allows other modules to either use default endpoint that provides built-in webpage, restful APIs and websocket streams, or create new endpoints.

Example configuration

By default, the web interface starts HTTPS/2 on port 8053 using an ephemeral certificate that is valid for 90 days and is automatically renewed. It is of course self-signed, so you should use your own judgement before exposing it to the outside world. Why not use something like Let’s Encrypt for starters?

-- Load HTTP module with defaults
modules = {
        http = {
                host = 'localhost',
                port = 8053,
                geoip = 'GeoLite2-City.mmdb' -- Optional

Now you can reach the web services and APIs, done!

$ curl -k https://localhost:8053
$ curl -k https://localhost:8053/stats

It is possible to disable HTTPS altogether by passing cert = false option. While it’s not recommended, it could be fine for localhost tests as, for example, Safari doesn’t allow WebSockets over HTTPS with a self-signed certificate. Major drawback is that current browsers won’t do HTTP/2 over insecure connection.

http = {
        host = 'localhost',
        port = 8053,
        cert = false,

If you want to provide your own certificate and key, you’re welcome to do so:

http = {
        host = 'localhost',
        port = 8053,
        cert = 'mycert.crt',
        key  = 'mykey.key',

The format of both certificate and key is expected to be PEM, e.g. equivallent to the outputs of following:

openssl ecparam -genkey -name prime256v1 -out mykey.key
openssl req -new -key mykey.key -out csr.pem
openssl req -x509 -days 90 -key mykey.key -in csr.pem -out mycert.crt

Built-in services

The HTTP module has several built-in services to use.

Endpoint Service Description
/stats Statistics/metrics Exported metrics in JSON.
/metrics Prometheus metrics Exported metrics for Prometheus
/feed Most frequent queries List of most frequent queries in JSON.

Enabling Prometheus metrics endpoint

The module exposes /metrics endpoint that serves internal metrics in Prometheus text format. You can use it out of the box:

$ curl -k https://localhost:8053/metrics | tail
# TYPE latency histogram
latency_bucket{le=10} 2.000000
latency_bucket{le=50} 2.000000
latency_bucket{le=100} 2.000000
latency_bucket{le=250} 2.000000
latency_bucket{le=500} 2.000000
latency_bucket{le=1000} 2.000000
latency_bucket{le=1500} 2.000000
latency_bucket{le=+Inf} 2.000000
latency_count 2.000000
latency_sum 11.000000

How to expose services over HTTP

The module provides a table endpoints of already existing endpoints, it is free for reading and writing. It contains tables describing a triplet - {mime, on_serve, on_websocket}. In order to register a new service, simply add it to the table:

http.endpoints['/health'] = {'application/json',
function (h, stream)
        -- API call, return a JSON table
        return {state = 'up', uptime = 0}
function (h, ws)
        -- Stream current status every second
        local ok = true
        while ok do
                local push = tojson('up')
                ok = ws:send(tojson({'up'}))
        -- Finalize the WebSocket

Then you can query the API endpoint, or tail the WebSocket using curl.

$ curl -k http://localhost:8053/health
$ curl -k -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Host: localhost:8053/health"  -H "Sec-Websocket-Key: nope" -H "Sec-Websocket-Version: 13" https://localhost:8053/health
HTTP/1.1 101 Switching Protocols
upgrade: websocket
sec-websocket-accept: eg18mwU7CDRGUF1Q+EJwPM335eM=
connection: upgrade


Since the stream handlers are effectively coroutines, you are free to keep state and yield using cqueues. This is especially useful for WebSockets, as you can stream content in a simple loop instead of chains of callbacks.

Last thing you can publish from modules are “snippets”. Snippets are plain pieces of HTML code that are rendered at the end of the built-in webpage. The snippets can be extended with JS code to talk to already exported restful APIs and subscribe to WebSockets.

http.snippets['/health'] = {'Health service', '<p>UP!</p>'}

How to expose RESTful services

A RESTful service is likely to respond differently to different type of methods and requests, there are three things that you can do in a service handler to send back results. First is to just send whatever you want to send back, it has to respect MIME type that the service declared in the endpoint definition. The response code would then be 200 OK, any non-string responses will be packed to JSON. Alternatively, you can respond with a number corresponding to the HTTP response code or send headers and body yourself.

-- Our upvalue
local value = 42

-- Expose the service
http.endpoints['/service'] = {'application/json',
function (h, stream)
        -- Get request method and deal with it properly
        local m = h:get(':method')
        local path = h:get(':path')
        log('[service] method %s path %s', m, path)
        -- Return table, response code will be '200 OK'
        if m == 'GET' then
                return {key = path, value = value}
        -- Save body, perform check and either respond with 505 or 200 OK
        elseif m == 'POST' then
                local data = stream:get_body_as_string()
                if not tonumber(data) then
                        return 500, 'Not a good request'
                value = tonumber(data)
        -- Unsupported method, return 405 Method not allowed
                return 405, 'Cannot do that'

In some cases you might need to send back your own headers instead of default provided by HTTP handler, you can do this, but then you have to return false to notify handler that it shouldn’t try to generate a response.

local headers = require('http.headers')
function (h, stream)
        -- Send back headers
        local hsend =
        hsend:append(':status', '200')
        hsend:append('content-type', 'binary/octet-stream')
        assert(stream:write_headers(hsend, false))
        -- Send back data
        local data = 'binary-data'
        assert(stream:write_chunk(data, true))
        -- Disable default handler action
        return false

How to expose more interfaces

Services exposed in the previous part share the same external interface. This means that it’s either accessible to the outside world or internally, but not one or another. This is not always desired, i.e. you might want to offer DNS/HTTPS to everyone, but allow application firewall configuration only on localhost. http module allows you to create additional interfaces with custom endpoints for this purpose.

http.interface('', 8080, {
        ['/conf'] = {'application/json', function (h, stream) print('configuration API') end},
        ['/private'] = {'text/html', static_page},

This way you can have different internal-facing and external-facing services at the same time.


  • lua-http (>= 0.1) available in LuaRocks

    If you’re installing via Homebrew on OS X, you need OpenSSL too.

    $ brew update
    $ brew install openssl
    $ brew link openssl --force # Override system OpenSSL

    Any other system can install from LuaRocks directly:

    $ luarocks install http
  • mmdblua available in LuaRocks

    $ luarocks install --server= mmdblua
    $ curl -O
    $ gzip -d GeoLite2-City.mmdb.gz

DNS Application Firewall

This module is a high-level interface for other powerful filtering modules and DNS views. It provides an easy interface to apply and monitor DNS filtering rules and a persistent memory for them. It also provides a restful service interface and an HTTP interface.

Example configuration

Firewall rules are declarative and consist of filters and actions. Filters have field operator operand notation (e.g. qname =, and may be chained using AND/OR keywords. Actions may or may not have parameters after the action name.

-- Let's write some daft rules!
modules = { 'daf' }

-- Block all queries with QNAME =
daf.add 'qname = deny'

-- Filters can be combined using AND/OR...
-- Block all queries with QNAME match regex and coming from given subnet
daf.add 'qname ~ AND src = deny'

-- We also can reroute addresses in response to alternate target
-- This reroutes to localhost
daf.add 'src = reroute'

-- Subnets work too, this reroutes a whole subnet
-- e.g. to
daf.add 'src = reroute'

-- This rewrites all A answers for '' from
-- whatever the original address was to
daf.add 'src = rewrite A'

-- Mirror queries matching given name to DNS logger
daf.add 'qname ~ mirror'
daf.add 'qname ~ mirror'

-- Forward queries from subnet
daf.add 'src = forward'
-- Forward to multiple targets
daf.add 'src = forward,'

-- Truncate queries based on destination IPs
daf.add 'dst = truncate'

-- Disable a rule
daf.disable 2
-- Enable a rule
daf.enable 2
-- Delete a rule
daf.del 2

If you’re not sure what firewall rules are in effect, see daf.rules:

-- Show active rules
> daf.rules
[1] => {
    [rule] => {
        [count] => 42
        [id] => 1
        [cb] => function: 0x1a3eda38
    [info] => qname = AND src = deny
    [policy] => function: 0x1a3eda38
[2] => {
    [rule] => {
        [suspended] => true
        [count] => 123522
        [id] => 2
        [cb] => function: 0x1a3ede88
    [info] => qname ~ AND src = deny...
    [policy] => function: 0x1a3ede88

Web interface

If you have HTTP/2 loaded, the firewall automatically loads as a snippet. You can create, track, suspend and remove firewall rules from the web interface. If you load both modules, you have to load daf after http.

RESTful interface

The module also exports a RESTful API for operations over rule chains.

URL HTTP Verb Action
/daf GET Return JSON list of active rules.
/daf POST Insert new rule, rule string is expected in body. Returns rule information in JSON.
/daf/<id> GET Retrieve a rule matching given ID.
/daf/<id> DELETE Delete a rule matching given ID.
/daf/<id>/<prop>/<val> PATCH Modify given rule, for example /daf/3/active/false suspends rule 3.

This interface is used by the web interface for all operations, but you can also use it directly for testing.

# Get current rule set
$ curl -s -X GET http://localhost:8053/daf | jq .

# Create new rule
$ curl -s -X POST -d "src = pass" http://localhost:8053/daf | jq .
  "count": 0,
  "active": true,
  "info": "src = pass",
  "id": 1

# Disable rule
$ curl -s -X PATCH http://localhost:8053/daf/1/active/false | jq .

# Retrieve a rule information
$ curl -s -X GET http://localhost:8053/daf/1 | jq .
  "count": 4,
  "active": true,
  "info": "src = pass",
  "id": 1

# Delete a rule
$ curl -s -X DELETE http://localhost:8053/daf/1 | jq .

Graphite module

The module sends statistics over the Graphite protocol to either Graphite, Metronome, InfluxDB or any compatible storage. This allows powerful visualization over metrics collected by Knot DNS Resolver.


The Graphite server is challenging to get up and running, InfluxDB combined with Grafana are much easier, and provide richer set of options and available front-ends. Metronome by PowerDNS alternatively provides a mini-graphite server for much simpler setups.

Example configuration

Only the host parameter is mandatory.

By default the module uses UDP so it doesn’t guarantee the delivery, set tcp = true to enable Graphite over TCP. If the TCP consumer goes down or the connection with Graphite is lost, resolver will periodically attempt to reconnect with it.

modules = {
        graphite = {
                prefix = hostname(), -- optional metric prefix
                host = '',  -- graphite server address
                port = 2003,         -- graphite server port
                interval = 5 * sec,  -- publish interval
                tcp = false          -- set to true if want TCP mode

The module supports sending data to multiple servers at once.

modules = {
        graphite = {
                host = { '', '', '::1' },


  • luasocket available in LuaRocks

    $ luarocks install luasocket

Memcached cache storage

Module providing a cache storage backend for memcached, which makes a good fit for making a shared cache between resolvers.

After loading you can see the storage backend registered and useable.

> modules.load 'kmemcached'
> cache.backends()
[memcached://] => true

And you can use it right away, see the libmemcached configuration reference for configuration string options, the most essential ones are –SERVER or –SOCKET. Here’s an example for connecting to UNIX socket.

> = 'memcached://--SOCKET="/var/sock/memcached"'


The memcached instance MUST support binary protocol, in order to make it work with binary keys. You can pass other options to the configuration string for performance tuning.


The memcached server is responsible for evicting entries out of cache, the pruning function is not implemented, and neither is aborting write transactions.

Build resolver shared cache

The memcached takes care of the data replication and fail over, you can add multiple servers at once.

> = 'memcached://--SOCKET="/var/sock/memcached" --SERVER= --SERVER=cache2.domain'


Depends on the libmemcached library.

Redis cache storage

This modules provides Redis backend for cache storage. Redis is a BSD-license key-value cache and storage server. Like memcached backend, Redis provides master-server replication, but also weak-consistency clustering.

After loading you can see the storage backend registered and useable.

> modules.load 'redis'
> cache.backends()
[redis://] => true

Redis client support TCP or UNIX sockets.

> = 'redis://'
> = 'redis://'
> = 'redis:///tmp/redis.sock'

It also supports indexed databases if you prefix the configuration string with DBID@.

> = 'redis://9@'


The Redis client doesn’t really support transactions nor pruning. Cache eviction policy shoud be left upon Redis server, see the Using Redis as an LRU cache.

Build distributed cache

See Redis Cluster tutorial.


Depends on the hiredis library, which is usually in the packages / ports or you can install it from sources.

Etcd module

The module connects to Etcd peers and watches for configuration change. By default, the module looks for the subtree under /kresd directory, but you can change this in the configuration.

The subtree structure corresponds to the configuration variables in the declarative style.

$ etcdctl set /kresd/net/ 53
$ etcdctl set /kresd/cache/size 10000000

Configures all listening nodes to following configuration:

net = { '' }
cache.size = 10000000

Example configuration

modules = {
        ketcd = {
                prefix = '/kresd',
                peer = ''


Work in progress!


  • lua-etcd available in LuaRocks

    $ luarocks install etcd --from=


The module for RFC 6147 DNS64 AAAA-from-A record synthesis, it is used to enable client-server communication between an IPv6-only client and an IPv4-only server. See the well written introduction in the PowerDNS documentation.


The module currently won’t work well with policy.STUB.


The A record sub-requests will be DNSSEC secured, but the synthetic AAAA records can’t be. Make sure the last mile between stub and resolver is secure to avoid spoofing.

Example configuration

-- Load the module with a NAT64 address
modules = { dns64 = 'fe80::21b:77ff:0:0' }
-- Reconfigure later


The module renumbers addresses in answers to different address space. e.g. you can redirect malicious addresses to a blackhole, or use private address ranges in local zones, that will be remapped to real addresses by the resolver.


While requests are still validated using DNSSEC, the signatures are stripped from final answer. The reason is that the address synthesis breaks signatures. You can see whether an answer was valid or not based on the AD flag.

Example configuration

modules = {
        renumber = {
                -- Source subnet, destination subnet
                {'', ''},
                -- Remap /16 block to localhost address range
                {'', ''}

DNS Cookies

The module performs most of the RFC 7873 DNS cookies functionality. Its main purpose is to check the cookies of inbound queries and responses. It is also used to alter the behaviour of the cookie functionality.

Example Configuration

-- Load the module before the 'iterate' layer.
modules = {
        'cookies < iterate'

-- Configure the client part of the resolver. Set 8 bytes of the client
-- secret and choose the hashing algorithm to be used.
-- Use a string composed of hexadecimal digits to set the secret.
cookies.config { client_secret = '0123456789ABCDEF',
                 client_cookie_alg = 'FNV-64' }

-- Configure the server part of the resolver.
cookies.config { server_secret = 'FEDCBA9876543210',
                  server_cookie_alg = 'FNV-64' }

-- Enable client cookie functionality. (Add cookies into outbound
-- queries.)
cookies.config { client_enabled = true }

-- Enable server cookie functionality. (Handle cookies in inbound
-- requests.)
cookies.config { server_enabled = true }


If you want to change several parameters regarding the client or server configuration then do it within a single cookies.config() invocation.


The module must be loaded before any other module that has direct influence on query processing and response generation. The module must be able to intercept an incoming query before the processing of the actual query starts. It must also be able to check the cookies of inbound responses and eventually discard them before they are handled by other functional units.


  • configuration (table) – part of cookie configuration to be changed, may be called without parameter

JSON dictionary containing current configuration

The function may be called without any parameter. In such case it only returns current configuration. The returned JSON also contains available algorithm choices.


  • Nettle required for HMAC-SHA256


Module checks for new version and CVE, and issues warning messages.


-- configure period of check (defaults to 1*day)




A simple module that alters resolver behavior on specific broken sub-domains. Currently it mainly disables case randomization on them.


modules = { 'workarounds < iterate' }


Dnstap module currently supports logging dns responses to a unix socket in dnstap format using fstrm framing library. The unix socket and the socket reader should be present before starting kresd.



  • socket_path: the the unix socket file where dnstap messages will be sent
  • log_responses: if true responses in wire format will be logged
modules = {
    dnstap = {
        socket_path = "/tmp/dnstap.sock",
        log_responses = true