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 that are used as a safety belt, or if the root NS drops out of cache.

Examples

-- Load hints after iterator
modules = { 'hints > iterate' }
-- Load hints before rrcache, custom hosts file
modules = { ['hints < rrcache'] = 'hosts.custom' }
-- Add root hints
hints.root({
  ['j.root-servers.net.'] = { '2001:503:c27::2:30', '192.58.128.30' }
})
-- Set custom hint
hints['localhost'] = '127.0.0.1'

Properties

hints.config([path])
Parameters:
  • path (string) – path to hosts-like file, default: no file
Returns:

{ result: bool }

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

hints.add_hosts([path])
Parameters:
  • path (string) – path to hosts-like file, default: /etc/hosts

Add hints from a host-like file.

hints.get(hostname)
Parameters:
  • hostname (string) – i.e. "localhost"
Returns:

{ 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().

hints.set(pair)
Parameters:
  • pair (string) – hostname address i.e. "localhost 127.0.0.1"
Returns:

{ result: bool }

Add a hostname - address pair hint.

hints.del(pair)
Parameters:
  • pair (string) – hostname address i.e. "localhost 127.0.0.1", or just hostname
Returns:

{ result: bool }

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

hints.root()
Returns:{ ['a.root-servers.net.'] = { '1.2.3.4', '5.6.7.8', ...}, ... }

Tip

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

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

{ ['a.root-servers.net.'] = { '1.2.3.4', '5.6.7.8', ...}, ... }

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

Example:

> hints.root({
  ['l.root-servers.net.'] = '199.7.83.42',
  ['m.root-servers.net.'] = '202.12.27.33'
})
[l.root-servers.net.] => {
  [1] => 199.7.83.42
}
[m.root-servers.net.] => {
  [1] => 202.12.27.33
}

Tip

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
[answer.total] => 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']
5

-- 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
}
[128.241.220.33] => {
    [1] => 31 - RTT
}

Properties

stats.get(key)
Parameters:
  • key (string) – i.e. "answer.total"
Returns:

number

Return nominal value of given metric.

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

Set nominal value of given metric.

stats.list([prefix])
Parameters:
  • prefix (string) – optional metric prefix, i.e. "answer" shows only metrics beginning with “answer”

Outputs collected metrics as a JSON dictionary.

stats.upstreams()

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.

stats.frequent()

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.

stats.clear_frequent()

Clear the list of most frequent iterative queries.

stats.expiring()

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.

stats.clear_expiring()

Clear the list of soon expiring records.

Built-in statistics

  • answer.total - 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) - forward query to given IP and proxy back response (stub mode); it can be a single IP (string) or a list of up to four IPs.
  • 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. {'192.0.2.0/24', '127.0.0.0'} will rewrite ‘192.0.2.55’ to ‘127.0.0.55’, 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.

Warning

The policy module only looks at the inbound DNS queries. Thus the FORWARD(ip) policy does only forward inbound query to the specified IP address(es) and it doesn’t and it can’t do DNSSEC validation. If you need DNSSEC validation, you either need to disable FORWARD(ip) policy or use an upstream DNSSEC-validating resolver.

Note

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. “example.com” equals to "\7example\3com". You can use convenience function todname('example.com') for automatic conversion.

Example configuration

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

Properties

policy.PASS

Pass-through all queries matching the rule.

policy.DENY

Respond with NXDOMAIN to all queries matching the rule.

policy.DROP

Drop all queries matching the rule.

policy.TC

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. {'192.0.2.0/24', '127.0.0.0'} will rewrite ‘192.0.2.55’ to ‘127.0.0.55’.

policy.QTRACE

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)
Parameters:
  • 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
Returns:

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.

policy.del(id)
Parameters:
  • id – identifier of a given rule
Returns:

boolean

Remove a rule from policy list.

policy.all(action)
Parameters:
  • action – executed action for all queries

Perform action for all queries (no filtering).

policy.pattern(action, pattern)
Parameters:
  • 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)
Parameters:
  • 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])
Parameters:
  • 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])
Parameters:
  • 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
NXDOMAIN . yes
NODATA *. partial, implemented as NXDOMAIN
Unchanged rpz-passthru. yes
Nothing rpz-drop. yes
Truncated rpz-tcp-only. yes
Modified anything no
Policy Trigger Support
QNAME yes
CLIENT-IP partial, may be done with views
IP no
NSDNAME 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('example.com') == '\7example\3com\0')
-- Convert table of names
policy.todnames({'example.com', 'me.cz'})
{ '\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('10.0.0.1', policy.suffix(policy.TC, {'\7example\3com'}))

This fill force given client subnet to TCP for names in example.com. 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('127.0.0.1', function (req, qry) return policy.DENY end))
-- Drop queries with suffix match for remote client
view:addr('10.0.0.0/8', policy.suffix(policy.DROP, {'\3xxx'}))
-- RPZ for subset of clients
view:addr('192.168.1.0/24', policy.rpz(policy.PASS, 'whitelist.rpz'))
-- Forward all queries from given subnet to proxy
view:addr('10.0.0.0/8', policy.all(policy.FORWARD('2001:DB8::1')))

Properties

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

Apply rule to clients in given subnet.

view:tsig(key, rule)
Parameters:
  • 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.

Warning

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

Prefetching records

The module tracks expiring records (having less than 5% of original TTL) and batches them for predict. 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.

Tip

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

Warning

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.

Tip

Use period 0 to turn off prediction and just do prefetching of expiring records.

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

Properties

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}
end,
function (h, ws)
        -- Stream current status every second
        local ok = true
        while ok do
                local push = tojson('up')
                ok = ws:send(tojson({'up'}))
                require('cqueues').sleep(1)
        end
        -- Finalize the WebSocket
        ws:close()
end}

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

$ curl -k http://localhost:8053/health
{"state":"up","uptime":0}
$ 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

?["up"]?["up"]?["up"]

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'
                end
                value = tonumber(data)
        -- Unsupported method, return 405 Method not allowed
        else
                return 405, 'Cannot do that'
        end
end}

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 = headers.new()
        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
end

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('127.0.0.1', 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.

Dependencies

  • 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=https://luarocks.org/dev mmdblua
    $ curl -O https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz
    $ 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 = example.com), 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 = example.com
daf.add 'qname = example.com deny'

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

-- We also can reroute addresses in response to alternate target
-- This reroutes 1.2.3.4 to localhost
daf.add 'src = 127.0.0.0/8 reroute 192.0.2.1-127.0.0.1'

-- Subnets work too, this reroutes a whole subnet
-- e.g. 192.0.2.55 to 127.0.0.55
daf.add 'src = 127.0.0.0/8 reroute 192.0.2.0/24-127.0.0.0'

-- This rewrites all A answers for 'example.com' from
-- whatever the original address was to 127.0.0.2
daf.add 'src = 127.0.0.0/8 rewrite example.com A 127.0.0.2'

-- Mirror queries matching given name to DNS logger
daf.add 'qname ~ %w+.example.com mirror 127.0.0.2'
daf.add 'qname ~ example-%d.com mirror 127.0.0.3@5353'

-- Forward queries from subnet
daf.add 'src = 127.0.0.1/8 forward 127.0.0.1@5353'
-- Forward to multiple targets
daf.add 'src = 127.0.0.1/8 forward 127.0.0.1@5353,127.0.0.2@5353'

-- Truncate queries based on destination IPs
daf.add 'dst = 192.0.2.51 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 = example.com AND src = 127.0.0.1/8 deny
    [policy] => function: 0x1a3eda38
}
[2] => {
    [rule] => {
        [suspended] => true
        [count] => 123522
        [id] => 2
        [cb] => function: 0x1a3ede88
    }
    [info] => qname ~ %w+.facebook.com AND src = 127.0.0.1/8 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 = 127.0.0.1 pass" http://localhost:8053/daf | jq .
{
  "count": 0,
  "active": true,
  "info": "src = 127.0.0.1 pass",
  "id": 1
}

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

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

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

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.

Tip

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 = '127.0.0.1',  -- 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 = { '127.0.0.1', '1.2.3.4', '::1' },
        }
}

Dependencies

  • 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.

> cache.storage = 'memcached://--SOCKET="/var/sock/memcached"'

Note

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.

Warning

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.

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

Dependencies

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.

> cache.storage = 'redis://127.0.0.1'
> cache.storage = 'redis://127.0.0.1:6398'
> cache.storage = 'redis:///tmp/redis.sock'

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

> cache.storage = 'redis://9@127.0.0.1'

Warning

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.

Dependencies

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/127.0.0.1 53
$ etcdctl set /kresd/cache/size 10000000

Configures all listening nodes to following configuration:

net = { '127.0.0.1' }
cache.size = 10000000

Example configuration

modules = {
        ketcd = {
                prefix = '/kresd',
                peer = 'http://127.0.0.1:7001'
        }
}

Warning

Work in progress!

Dependencies

  • lua-etcd available in LuaRocks

    $ luarocks install etcd --from=https://mah0x211.github.io/rocks/

DNS64

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.

Tip

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
dns64.config('fe80::21b:aabb:0:0')

Renumber

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.

Warning

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
                {'10.10.10.0/24', '192.168.1.0'},
                -- Remap /16 block to localhost address range
                {'166.66.0.0/16', '127.0.0.0'}
        }
}

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 }

Tip

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

Warning

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.

Properties

cookies.config(configuration)
Parameters:
  • configuration (table) – part of cookie configuration to be changed, may be called without parameter
Returns:

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.

Dependencies

Version

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

Configuration

    version.config(2*day)
-- configure period of check (defaults to 1*day)

Running

modules.load("version")

Workarounds

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

Running

modules = { 'workarounds < iterate' }

Dnstap

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.

Configuration

Tunables:

  • 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
    }
}