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

Examples

-- Load hints after iterator (so hints take precedence before caches)
modules = { 'hints > iterate' }
-- Add a custom hosts file
hints.add_hosts('hosts.custom')
-- Override the root hints
hints.root({
  ['j.root-servers.net.'] = { '2001:503:c27::2:30', '192.58.128.30' }
})
-- Add a custom hint
hints['foo.bar'] = '127.0.0.1'

Note

The policy module applies before hints, meaning e.g. that hints for special names (RFC 6761#section-6) like localhost or test will get shadowed by policy rules by default. That can be worked around e.g. by explicit policy.PASS action.

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.

Note

If multiple addresses have been added for a name (in separate hints.set() commands), 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.

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_file(path)

Replace current root hints from a zonefile. If the path is omitted, the compiled-in path is used, i.e. the root hints are reset to the default.

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.

Tip

If no parameters are passed, it only returns current root hints set without changing anything.

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.

hints.use_nodata(toggle)
Parameters:
  • toggle (bool) – true if enabling NODATA synthesis, false if disabling
Returns:

{ result: bool }

If set to true (the default), NODATA will be synthesised for matching hint name, but mismatching type (e.g. AAAA query when only A hint exists).

hints.ttl([new_ttl])
Parameters:
  • new_ttl (int) – new TTL to set (optional)
Returns:

the TTL setting

This function allows to read and write the TTL value used for records generated by the hints module.

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.

Built-in statistics

Built-in counters keep track of number of queries and answers matching specific criteria.

Global request counters
request.total total number of DNS requests from clients (including internal client requests)
request.internal internal requests generated by Knot Resolver (e.g. DNSSEC trust anchor updates)
request.udp external requests received over plain UDP (RFC 1035)
request.tcp external requests received over plain TCP (RFC 1035)
request.dot external requests received over DNS-over-TLS (RFC 7858)
request.doh external requests received over DNS-over-HTTP (RFC 8484)
Global answer counters
answer.total total number of answered queries
answer.cached queries answered from cache
Answers categorized by RCODE
answer.noerror NOERROR answers
answer.nodata NOERROR, but empty answers
answer.nxdomain NXDOMAIN answers
answer.servfail SERVFAIL answers
Answer latency
answer.1ms completed in 1ms
answer.10ms completed in 10ms
answer.50ms completed in 50ms
answer.100ms completed in 100ms
answer.250ms completed in 250ms
answer.500ms completed in 500ms
answer.1000ms completed in 1000ms
answer.1500ms completed in 1500ms
answer.slow completed in more than 1500ms
Answer flags
answer.aa authoritative answer
answer.tc truncated answer
answer.ra recursion available
answer.rd recursion desired (in answer!)
answer.ad authentic data (DNSSEC)
answer.cd checking disabled (DNSSEC)
answer.do DNSSEC answer OK
answer.edns0 EDNS0 present
Query flags
query.edns queries with EDNS present
query.dnssec queries with DNSSEC DO=1

Query policies

This module can block, rewrite, or alter inbound queries based on user-defined policies.

Each policy rule has two parts: a filter and an action. A filter selects which queries will be affected by the policy, and action which modifies queries matching the associated filter.

Typically a rule is defined as follows: filter(action(action parameters), filter parameters). For example, a filter can be suffix which matches queries whose suffix part is in specified set, and one of possible actions is DENY, which denies resolution. These are combined together into policy.suffix(policy.DENY, {todname('badguy.example.')}). The rule is effective when it is added into rule table using policy.add(), please see Policy examples.

This module is enabled by default because it implements mandatory RFC 6761 logic. When no rule applies to a query, built-in rules for special-use and locally-served domain names are applied. These rules can be overriden by action PASS, see Policy examples below. For debugging purposes you can also add modules.unload('policy') to your config to unload the module.

Filters

A filter selects which queries will be affected by specified action. There are several policy filters available in the policy. table:

  • all(action) - always applies the action
  • pattern(action, pattern) - applies the action if QNAME matches a regular expression
  • suffix(action, table) - applies the action if QNAME suffix matches one of suffixes in the table (useful for “is domain in zone” rules), uses Aho-Corasick string matching algorithm from CloudFlare (BSD 3-clause)
  • policy.suffix_common
  • rpz(default_action, path) - implements a subset of RPZ in zonefile format. See below for details: policy.rpz.
  • slice(slice_func, action, action, ...) - splits the entire domain space into multiple slices, uses the slicing function to determine to which slice does the query belong, and perfroms the corresponding action. For details, see policy.slice.
  • custom filter function

Actions

An action is function which modifies DNS query, and is either of type chain or non-chain. So-called chain actions modify the query and allow other rules to evaluate and modify the same query. Non-chain actions have opposite behavior, i.e. modify the query and stop rule processing.

Resolver comes with several actions available in the policy. table:

Non-chain actions

Following actions stop the policy matching on the query, i.e. other rules are not evaluated once rule with following actions matches:

  • PASS - let the query pass through; it’s useful to make exceptions before wider rules
  • DENY - reply NXDOMAIN authoritatively
  • DENY_MSG(msg) - reply NXDOMAIN authoritatively and add explanatory message to additional section
  • DROP - terminate query resolution and return SERVFAIL to the requestor
  • REFUSE - terminate query resolution and return REFUSED to the requestor
  • TC - set TC=1 if the request came through UDP, forcing client to retry with TCP
  • FORWARD(ip) - resolve a query via forwarding to an IP while validating and caching locally
  • TLS_FORWARD({{ip, authentication}}) - resolve a query via TLS connection forwarding to an IP while validating and caching locally
  • 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.
  • 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.

FORWARD, TLS_FORWARD and STUB support up to four IP addresses “in a single call”.

Chain actions

Following actions allow to keep trying to match other rules, until a non-chain action is triggered:

  • MIRROR(ip) - mirror query to given IP and continue solving it (useful for partial snooping).
  • QTRACE - pretty-print DNS response packets into the log for the query and its sub-queries. It’s 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. You can just pass a single flag name (string) or a set of names.

Also, it is possible to write your own action (i.e. Lua function). It is possible to implement complex heuristics, e.g. to deflect Slow drip DNS attacks or gray-list resolution of misbehaving zones.

Warning

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

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.

Forwarding over TLS protocol (DNS-over-TLS)

Policy TLS_FORWARD allows you to forward queries using Transport Layer Security protocol, which hides the content of your queries from an attacker observing the network traffic. Further details about this protocol can be found in RFC 7858 and IETF draft dprive-dtls-and-tls-profiles.

Queries affected by TLS_FORWARD policy will always be resolved over TLS connection. Knot Resolver does not implement fallback to non-TLS connection, so if TLS connection cannot be established or authenticated according to the configuration, the resolution will fail.

To test this feature you need to either configure Knot Resolver as DNS-over-TLS server, or pick some public DNS-over-TLS server. Please see DNS Privacy Project homepage for list of public servers.

Note

Some public DNS-over-TLS providers may apply rate-limiting which makes their service incompatible with Knot Resolver’s TLS forwarding. Notably, Google Public DNS doesn’t work as of 2019-07-10.

When multiple servers are specified, the one with the lowest round-trip time is used.

CA+hostname authentication

Traditional PKI authentication requires server to present certificate with specified hostname, which is issued by one of trusted CAs. Example policy is:

policy.TLS_FORWARD({
        {'2001:DB8::d0c', hostname='res.example.com'}})
  • hostname must be a valid domain name matching server’s certificate. It will also be sent to the server as SNI.
  • ca_file optionally contains a path to a CA certificate (or certificate bundle) in PEM format. If you omit that, the system CA certificate store will be used instead (usually sufficient). A list of paths is also accepted, but all of them must be valid PEMs.

Key-pinned authentication

Instead of CAs, you can specify hashes of accepted certificates in pin_sha256. They are in the usual format – base64 from sha256. You may still specify hostname if you want SNI to be sent.

TLS Examples

modules = { 'policy' }
-- forward all queries over TLS to the specified server
policy.add(policy.all(policy.TLS_FORWARD({{'192.0.2.1', pin_sha256='YQ=='}})))
-- for brevity, other TLS examples omit policy.add(policy.all())
-- single server authenticated using its certificate pin_sha256
  policy.TLS_FORWARD({{'192.0.2.1', pin_sha256='YQ=='}})  -- pin_sha256 is base64-encoded
-- single server authenticated using hostname and system-wide CA certificates
  policy.TLS_FORWARD({{'192.0.2.1', hostname='res.example.com'}})
-- single server using non-standard port
  policy.TLS_FORWARD({{'192.0.2.1@443', pin_sha256='YQ=='}})  -- use @ or # to specify port
-- single server with multiple valid pins (e.g. anycast)
  policy.TLS_FORWARD({{'192.0.2.1', pin_sha256={'YQ==', 'Wg=='}})
-- multiple servers, each with own authenticator
  policy.TLS_FORWARD({ -- please note that { here starts list of servers
        {'192.0.2.1', pin_sha256='Wg=='},
        -- server must present certificate issued by specified CA and hostname must match
        {'2001:DB8::d0c', hostname='res.example.com', ca_file='/etc/knot-resolver/tlsca.crt'}
})

Forwarding to multiple targets

With the use of policy.slice function, it is possible to split the entire DNS namespace into distinct slices. When used in conjuction with policy.TLS_FORWARD, it’s possible to forward different queries to different targets.

policy.add(policy.slice(
    policy.slice_randomize_psl(),
    policy.TLS_FORWARD({{'192.0.2.1', hostname='res.example.com'}}),
    policy.TLS_FORWARD({
        -- multiple servers can be specified for a single slice
        -- the one with lowest round-trip time will be used
        {'193.17.47.1', hostname='odvr.nic.cz'},
        {'185.43.135.1', hostname='odvr.nic.cz'},
    })
))

Note

The privacy implications of using this feature aren’t clear. Since websites often make requests to multiple domains, these might be forwarded to different targets. This could result in decreased privacy (e.g. when the remote targets are both logging or otherwise processing your DNS traffic). The intended use-case is to use this feature with semi-trusted resolvers which claim to do no logging (such as those listed on dnsprivacy.org), to decrease the potential exposure of your DNS data to a malicious resolver operator.

Policy examples

-- 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
local ffi = require('ffi')
local function genRR (state, req)
        local answer = req.answer
        local qry = req:current()
        if qry.stype ~= kres.type.A then
                return state
        end
        ffi.C.kr_pkt_make_auth_header(answer)
        answer:rcode(kres.rcode.NOERROR)
        answer:begin(kres.section.ANSWER)
        answer:put(qry.sname, 900, answer:qclass(), kres.type.A, '\192\168\1\3')
        return kres.DONE
end
policy.add(policy.suffix(genRR, { todname('my.example.cz.') }))

-- 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;
-- beware: typically this won't work due to DNSSEC - see "Replacing part..." below
policy.add(policy.suffix(policy.FORWARD('192.168.1.1'), {todname('company.se')}))
-- Forward reverse queries about the 192.168.1.1/24 space to .1 port 5353
-- and do it directly without attempts to validate DNSSEC etc.
policy.add(policy.suffix(policy.STUB('192.168.1.1@5353'), {todname('1.168.192.in-addr.arpa')}))
-- Forward all queries matching pattern
policy.add(policy.pattern(policy.FORWARD('2001:DB8::1'), '\4bad[0-9]\2cz'))
-- Forward all queries (to public resolvers https://www.nic.cz/odvr)
policy.add(policy.all(policy.FORWARD({'2001:678:1::206', '193.29.206.206'})))
-- 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)

Replacing part of the DNS tree

You may want to resolve most of the DNS namespace by usual means while letting some other resolver solve specific subtrees. Such data would typically be rejected by DNSSEC validation starting from the ICANN root keys. Therefore, if you trust the resolver and your link to it, you can simply use the STUB action instead of FORWARD to avoid validation only for those subtrees.

Another issue is caused by caching, because Knot Resolver only keeps a single cache for everything. For example, if you add an alternative top-level domain while using the ICANN root zone for the rest, at some point the cache may obtain records proving that your top-level domain does not exist, and those records could then be used when the positive records fall out of cache. The easiest work-around is to disable reading from cache for those subtrees; the other resolver is often very close anyway.

Example configuration: graft DNS sub-trees faketldtest, sld.example, and internal.example.com into existing namespace
extraTrees = policy.todnames({'faketldtest', 'sld.example', 'internal.example.com'})
-- Beware: the rule order is important, as STUB is not a chain action.
policy.add(policy.suffix(policy.FLAGS({'NO_CACHE'}),   extraTrees))
policy.add(policy.suffix(policy.STUB({'2001:db8::1'}), extraTrees))

Additional properties

Most properties (actions, filters) are described above.

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.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, watch)
Parameters:
  • action – the default action for match in the zone; typically you want policy.DENY
  • path – path to zone file | database
  • watch – boolean, if not false, the file will be reparsed and the ruleset reloaded on file change

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
action is used . yes, if action is DENY
action is used *. partial [1]
policy.PASS rpz-passthru. yes
policy.DROP rpz-drop. yes
policy.TC rpz-tcp-only. yes
Modified anything no
[1]The specification for *. wants a NODATA answer. For now, policy.DENY action doing NXDOMAIN is typically used instead.
Policy Trigger Support
QNAME yes
CLIENT-IP partial, may be done with views
IP no
NSDNAME no
NS-IP no
policy.slice(slice_func, action[, action[, ...])
Parameters:
  • slice_func – slicing function that returns index based on query
  • action – action to be performed for the slice

This function splits the entire domain space into multiple slices (determined by the number of provided action``s) A ``slice_func is called to determine which slice a query belongs to. The corresponding action is then executed.

policy.slice_randomize_psl(seed = os.time() / (3600 * 24 * 7))
Parameters:
  • seed – seed for random assignment

The function initializes and returns a slicing function, which deterministically assigns query to a slice based on the QNAME.

It utilizes the Public Suffix List to ensure domains under the same registrable domain end up in a single slice. (see example below)

seed can be used to re-shuffle the slicing algorhitm when the slicing function is initialized. By default, the assigment is re-shuffled after one week (when resolver restart / reloads config). To force a stable distribution, pass a fixed value. To re-shuffle on every resolver restart, use os.time().

The following example demonstrates a distribution among 3 slices:

slice 1/3:
example.com
a.example.com
b.example.com
x.b.example.com
example3.com

slice 2/3:
example2.co.uk

slice 3/3:
example.co.uk
a.example.co.uk
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.

There are two identification mechanisms:

  • addr - identifies the client based on his subnet
  • tsig - identifies the client based on a TSIG key name (only for testing purposes, TSIG signature is not verified!)

View module allows you to combine query source information with policy rules.

view:addr('10.0.0.1', policy.suffix(policy.TC, policy.todnames({'example.com'})))

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

Warning

Beware that cache is shared by all requests. For example, it is safe to refuse answer based on who asks the resolver, but trying to serve different data to different clients will result in unexpected behavior. Setups like split-horizon which depend on isolated DNS caches are explicitly not supported.

Example configuration

-- Load modules
modules = { 'view' }
-- Whitelist queries identified by TSIG key
view:tsig('\5mykey', policy.all(policy.PASS))
-- Block local IPv4 clients (ACL like)
view:addr('127.0.0.1', policy.all(policy.DENY))
-- Block local IPv6 clients (ACL like)
view:addr('::1', policy.all(policy.DENY))
-- Drop queries with suffix match for remote client
view:addr('10.0.0.0/8', policy.suffix(policy.DROP, policy.todnames({'xxx'})))
-- RPZ for subset of clients
view:addr('192.168.1.0/24', policy.rpz(policy.PASS, 'whitelist.rpz'))
-- Do not try this - it will pollute cache and surprise you!
-- view:addr('10.0.0.0/8', policy.all(policy.FORWARD('2001:DB8::1')))
-- Drop everything that hasn't matched
view:addr('0.0.0.0/0', policy.all(policy.DROP))

Note

When using systemd socket activation, it’s possible to bind to IPv6 socket that also handles IPv4 connections via v4-mapped-on-v6 addresses. With this setup, using IPv4 syntax in view:addr() is currently not supported. Instead, you can use the v4-mapped-on-v6 syntax, e.g. ::ffff:127.0.0.0/104 instead of 127.0.0.0/8.

Rule order

The current implementation is best understood as three separate rule chains: vanilla policy.add, view:tsig and view:addr. For each request the rules in these chains get tried one by one until a non-chain policy action gets executed.

By default policy module acts before view module due to policy being loaded by default. If you want to intermingle universal rules with view:addr, you may simply wrap the universal policy rules in view closure like this:

view:addr('0.0.0.0/0', policy.<rule>) -- and
view:addr('::0/0',     policy.<rule>)

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

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

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. That works even without the stats module.

Note

Otherwise this module requires stats module and loads it if not present.

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 module does the heavy lifting to provide an HTTP/2 enabled server which provides few built-in services and also allows other modules 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.

By default this module provides two kinds of endpoints, and unlimited number of “used-defined kinds” can be added in configuration.

Endpoint Explanation
doh DNS-over-HTTP (DoH)
webmgmt built-in web management APIs (includes DoH)

Each network address and port combination can be configured to expose one kind of endpoint. This is done using the same mechanisms as network configuration for plain DNS and DNS-over-TLS, see chapter Network configuration for more details.

Warning

Management endpoint (webmgmt) must not be directly exposed to untrusted parties. Use reverse-proxy like Apache or Nginx if you need to authenticate API clients for the management API.

By default all endpoints share the same configuration for TLS certificates etc. This can be changed using http.config() configuration call explained below.

Example configuration

This section shows how to configure HTTP module itself. For information how to configure HTTP server’s IP addresses and ports please see chapter Network configuration.

-- load HTTP module with defaults (self-signed TLS cert)
modules.load('http')
-- optionally load geoIP database for server map
http.config({
        geoip = 'GeoLite2-City.mmdb',
        -- e.g. https://dev.maxmind.com/geoip/geoip2/geolite2/
        -- and install mmdblua library
})

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

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

Configuring TLS

By default, the web interface starts HTTPS/2 on specified port using an ephemeral TLS certificate that is valid for 90 days and is automatically renewed. It is of course self-signed. Why not use something like Let’s Encrypt?

Warning

If you use package luaossl < 20181207, intermediate certificate is not sent to clients, which may cause problems with validating the connection in some cases.

You can disable unecrypted HTTP and enforce HTTPS by passing tls = true option for all HTTP endpoints:

http.config({
        tls = true,
})

It is also possible to provide different configuration for each kind of endpoint, e.g. to enforce TLS and use custom certificate only for DoH:

http.config({
        tls = true,
        cert = '/etc/knot-resolver/mycert.crt',
        key  = '/etc/knot-resolver/mykey.key',
}, 'doh')

The format of both certificate and key is expected to be PEM, e.g. equivalent 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

It is also possible to disable HTTPS altogether by passing tls = false option. Plain HTTP gets handy if you want to use reverse-proxy like Apache or Nginx for authentication to API etc. (Unencrypted HTTP 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.)

Warning

If you use multiple Knot Resolver instances with these automatically maintained ephemeral certificates, they currently won’t be shared. It’s assumed that you don’t want a self-signed certificate for serious deployments anyway.

Built-in services

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

Endpoint Service Description
/stats Statistics/metrics Exported metrics from Statistics collector in JSON format.
/metrics Prometheus metrics Exported metrics for Prometheus.
/trace/:name/:type Tracking Trace resolution of a DNS query and return the verbose logs.
/doh DNS-over-HTTP RFC 8484 endpoint, see DNS-over-HTTP (DoH).

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:8453/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

You can namespace the metrics in configuration, using http.prometheus.namespace attribute:

modules.load('http')
-- Set Prometheus namespace
http.prometheus.namespace = 'resolver_'

You can also add custom metrics or rewrite existing metrics before they are returned to Prometheus client.

modules.load('http')
-- Add an arbitrary metric to Prometheus
http.prometheus.finalize = function (metrics)
        table.insert(metrics, 'build_info{version="1.2.3"} 1')
end

Tracing requests

With the /trace endpoint you can trace various aspects of the request execution. The basic mode allows you to resolve a query and trace verbose logs (and messages received):

$ curl https://localhost:8453/trace/e.root-servers.net
[ 8138] [iter] 'e.root-servers.net.' type 'A' created outbound query, parent id 0
[ 8138] [ rc ] => rank: 020, lowest 020, e.root-servers.net. A
[ 8138] [ rc ] => satisfied from cache
[ 8138] [iter] <= answer received:
;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 8138
;; Flags: qr aa  QUERY: 1; ANSWER: 0; AUTHORITY: 0; ADDITIONAL: 0

;; QUESTION SECTION
e.root-servers.net.          A

;; ANSWER SECTION
e.root-servers.net.  3556353 A       192.203.230.10

[ 8138] [iter] <= rcode: NOERROR
[ 8138] [resl] finished: 4, queries: 1, mempool: 81952 B

How to expose custom services over HTTP

Each kind of endpoint provides table of HTTP endpoints, and the default table can be replaced using http.config() configuration call which allows your to provide your own HTTP endpoints.

It contains tables describing a triplet - {mime, on_serve, on_websocket}. In order to register a new webmgmt HTTP endpoint add the new endpoint description to respective table:

-- custom function to handle HTTP /health requests
local on_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}

modules.load('http')
-- copy all existing webmgmt endpoints
my_mgmt_endpoints = http.configs._builtin.webmgmt.endpoints
-- add custom endpoint to the copy
my_mgmt_endpoints['/health'] = on_health
-- use custom HTTP configuration for webmgmt
http.config({
        endpoints = my_mgmt_endpoints
}, 'webmgmt')

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

$ curl -k https://localhost:8453/health
{"state":"up","uptime":0}
$ curl -k -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Host: localhost:8453/health"  -H "Sec-Websocket-Key: nope" -H "Sec-Websocket-Version: 13" https://localhost:8453/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 library.

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 custom 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
local 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}
modules.load('http')
http.config({
        endpoints = { ['/service'] = service }
}, 'myservice')
-- do not forget to create socket of new kind using
-- net.listen(..., { kind = 'myservice' })
-- or configure systemd socket kresd-myservice.socket

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

Dependencies

  • lua-http (>= 0.3) 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-over-HTTP (DoH)

Warning

  • DoH support was added in version 4.0.0 and is subject to change.
  • DoH implementation in Knot Resolver is intended for experimentation only as there is insufficient experience with the module and the DoH protocol in general.
  • For the time being it is recommended to run DoH endpoint on a separate machine which is not handling normal DNS operations.
  • More information about controversies around the DoH can be found in blog posts DNS Privacy at IETF 104 and More DOH by Geoff Huston.
  • Knot Resolver developers do not endorse use of the DoH protocol.

Following section compares several options for running a DoH capable server. Make sure you read through this chapter before exposing the DoH service to users.

DoH support in Knot Resolver

The HTTP module in Knot Resolver also provides support for binary DNS-over-HTTP protocol standardized in RFC 8484.

This integrated DoH server has following properties:

Scenario:

HTTP module in Knot Resolver configured to provide /doh endpoint (as shown below).

Advantages:
  • Integrated solution provides management and monitoring in one place.
  • Supports ACLs for DNS traffic based on client’s IP address.
Disadvantages:
  • Exposes Knot Resolver instance to attacks over HTTP.
  • Does not offer fine grained authorization and logging at HTTP level.
  • Let’s Encrypt integration is not automated.

Example configuration is part of examples for generic HTTP module. After configuring your endpoint you can reach the DoH endpoint using URL https://your.resolver.hostname.example/doh, done!

# query for www.knot-resolver.cz AAAA
$ curl -k https://your.resolver.hostname.example/doh?dns=l1sBAAABAAAAAAAAA3d3dw1rbm90LXJlc29sdmVyAmN6AAAcAAE

Please see section Configuring TLS for further details about TLS configuration.

Alternative configurations use HTTP proxies between clients and a Knot Resolver instance:

Normal HTTP proxy

Scenario:

A standard HTTP-compliant proxy is configured to proxy GET and POST requests to HTTP endpoint /doh to a machine running Knot Resolver.

Advantages:
  • Protects Knot Resolver instance from some types of attacks at HTTP level.
  • Allows fine-grained filtering and logging at HTTP level.
  • Let’s Encrypt integration is readily available.
  • Is based on mature software.
Disadvantages:
  • Fine-grained ACLs for DNS traffic are not available because proxy hides IP address of client sending DNS query.
  • More complicated setup with two components (proxy + Knot Resolver).

HTTP proxy with DoH support

Scenario:

HTTP proxy extended with a special module for DNS-over-HTTP. The module transforms HTTP requests to standard DNS queries which are then processed by Knot Resolver. DNS replies from Knot Resolver are then transformed back to HTTP encoding by the proxy.

Advantages:
  • Protects Knot Resolver instance from all attacks at HTTP level.
  • Allows fine-grained filtering and logging at HTTP level.
  • Let’s Encrypt integration is readily available if proxy is based on a standard HTTP software.
Disadvantages:
  • Fine-grained ACLs for DNS traffic are not available because proxy hides IP address of client sending DNS query. (Unless proxy and resolver are using non-standard packet extensions like DNS X-Proxied-For.)
  • More complicated setup with three components (proxy + special module + Knot Resolver).

Client configuration

Most common client today is web browser Firefox. Relevant configuration is described e.g. in following article. To use your own DoH server just change network.trr.uri configuration option to match URL of your DoH endpoint.

More detailed description of configuration options in Firefox can be found in article Inside Firefox’s DOH engine by Daniel Stenberg.

Warning

Please note that Knot Resolver developers are not as enthusiastic about DoH technology as author of the article linked above, make sure you read warnings at beginning of this section.

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:8453/daf | jq .
{}

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

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

# Retrieve a rule information
$ curl -s -X GET http://localhost:8453/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:8453/daf/1 | jq .
true

Rebinding protection

This module provides protection from DNS Rebinding attack by blocking answers which cointain IPv4 or IPv6 addresses for private use (or some other special-use addresses).

To enable this module insert following line into your configuration file:

modules.load('rebinding < iterate')

Please note that this module does not offer stable configuration interface yet. For this reason it is suitable mainly for public resolver operators who do not need to whitelist certain subnets.

Warning

DNS Blacklists (RFC 5782) often use 127.0.0.0/8 to blacklist a domain. Using the rebinding module prevents DNSBL from functioning properly.

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

Etcd module

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

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

$ etcdctl set /knot-resolvevr/net/127.0.0.1 53
$ etcdctl set /knot-resolver/cache/size 10000000

Configures all listening nodes to following configuration:

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

Example configuration

modules = {
        etcd = {
                prefix = '/knot-resolver',
                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. If no address is passed (i.e. nil), the well-known prefix 64:ff9b:: is used.

Warning

The module currently won’t work well with policy.STUB. Also, the IPv6 passed in configuration is assumed to be /96, and PTR synthesis and “exclusion prefixes” aren’t implemented.

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

DNSSEC validation failure logging

This module adds error message for each DNSSEC validation failure. It is meant to provide hint to operators which queries should be investigated using diagnostic tools like DNSViz.

Add following line to your configuration file to enable it:

modules.load('bogus_log')

Example of error message logged by this module:

DNSSEC validation failure dnssec-failed.org. DNSKEY

List of most frequent queries which fail as DNSSEC bogus can be obtained at run-time:

> bogus_log.frequent()
[1] => {
    [type] => DNSKEY
    [count] => 1
    [name] => dnssec-failed.org.
}
[2] => {
    [type] => DNSKEY
    [count] => 13
    [name] => rhybar.cz.
}

Please note that in future this module might be replaced with some other way to log this information.

Name Server Identifier (NSID)

This module provides server-side support for RFC 5001 and is not enabled by default.

DNS clients can request resolver to send back its NSID along with the reply to a DNS request. This is useful for identification of resolver instances in larger services (using anycast or load balancers).

This is useful tool for debugging larger services, as it reveals which particular resolver instance sent the reply.

NSID value can be configured in the resolver’s configuration file:

modules.load('nsid')
nsid.name('instance 1')

You can also obtain configured NSID value:

nsid.name()
instance 1

The module can be disabled at run-time:

modules.unload('nsid')

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

Signaling Trust Anchor Knowledge in DNSSEC

The module for Signaling Trust Anchor Knowledge in DNSSEC Using Key Tag Query, implemented according to RFC 8145#section-5.

This feature allows validating resolvers to signal to authoritative servers which keys are referenced in their chain of trust. The data from such signaling allow zone administrators to monitor the progress of rollovers in a DNSSEC-signed zone.

This mechanism serve to measure the acceptance and use of new DNSSEC trust anchors and key signing keys (KSKs). This signaling data can be used by zone administrators as a gauge to measure the successful deployment of new keys. This is of particular interest for the DNS root zone in the event of key and/or algorithm rollovers that rely on RFC 5011 to automatically update a validating DNS resolver’s trust anchor.

Attention

Experience from root zone KSK rollover in 2018 shows that this mechanism by itself is not sufficient to reliably measure acceptance of the new key. Nevertheless, some DNS researchers found it is useful in combination with other data so we left it enabled for now. This default might change once more information is available.

This module is enabled by default. You may use modules.unload('ta_signal_query') in your configuration.

Sentinel for Detecting Trusted Root Keys

The module implementing A Root Key Trust Anchor Sentinel for DNSSEC according to draft-ietf-dnsop-kskroll-sentinel-12.

This feature allows users of validating resolver to detect which root keys are configured in their chain of trust. The data from such signaling are necessary to monitor the progress of the DNSSEC root key rollover.

This module is enabled by default and we urge users not to disable it. If it is absolutely necessary you may add modules.unload('ta_sentinel') to your configuration to disable it.

Priming module

The module for Initializing a DNS Resolver with Priming Queries implemented according to RFC 8109. Purpose of the module is to keep up-to-date list of root DNS servers and associated IP addresses.

Result of successful priming query replaces root hints distributed with the resolver software. Unlike other DNS resolvers, Knot Resolver caches result of priming query on disk and keeps the data between restarts until TTL expires.

This module is enabled by default and it is not recommended to disable it. For debugging purposes you may disable the module by appending modules.unload('priming') to your configuration.

System time skew detector

This module compares local system time with inception and expiration time bounds in DNSSEC signatures for . NS records. If the local system time is outside of these bounds, it is likely a misconfiguration which will cause all DNSSEC validation (and resolution) to fail.

In case of mismatch, a warning message will be logged to help with further diagnostics.

Warning

Information printed by this module can be forged by a network attacker! System administrator MUST verify values printed by this module and fix local system time using a trusted source.

This module is useful for debugging purposes. It runs only once during resolver start does not anything after that. It is enabled by default. You may disable the module by appending modules.unload('detect_time_skew') to your configuration.

Detect discontinuous jumps in the system time

This module detect discontinuous jumps in the system time when resolver is running. It clears cache when a significant backward time jumps occurs.

Time jumps are usually created by NTP time change or by admin intervention. These change can affect cache records as they store timestamp and TTL in real time.

If you want to preserve cache during time travel you should disable this module by modules.unload('detect_time_jump').

Due to the way monotonic system time works on typical systems, suspend-resume cycles will be perceived as forward time jumps, but this direction of shift does not have the risk of using records beyond their intended TTL, so forward jumps do not cause erasing the cache.

Root on loopback (RFC 7706)

Knot Resolver developers decided that pure implementation of RFC 7706 is a bad idea so it is not implemented in the form envisioned by the RFC. You can get the very similar effect without its downsides by combining prefill and serve_stale modules with Aggressive Use of DNSSEC-Validated Cache (RFC 8198) behavior which is enabled automatically together with DNSSEC validation.

Cache prefilling

This module provides ability to periodically prefill DNS cache by importing root zone data obtained over HTTPS.

Intended users of this module are big resolver operators which will benefit from decreased latencies and smaller amount of traffic towards DNS root servets.

Example configuration is:

modules.load('prefill')
prefill.config({
      ['.'] = {
              url = 'https://www.internic.net/domain/root.zone',
              ca_file = '/etc/pki/tls/certs/ca-bundle.crt',
              interval = 86400  -- seconds
      }
})

This configuration downloads zone file from URL https://www.internic.net/domain/root.zone and imports it into cache every 86400 seconds (1 day). The HTTPS connection is authenticated using CA certificate from file /etc/pki/tls/certs/ca-bundle.crt and signed zone content is validated using DNSSEC.

Root zone to import must be signed using DNSSEC and the resolver must have valid DNSSEC configuration.

Parameter Description
ca_file path to CA certificate bundle used to authenticate the HTTPS connection
interval number of seconds between zone data refresh attempts
url URL of a file in RFC 1035 zone file format

Only root zone import is supported at the moment.

Dependencies

Depends on the luasec and luafilesystem libraries.

Serve stale

Demo module that allows using timed-out records in case kresd is unable to contact upstream servers.

By default it allows stale-ness by up to one day, after roughly four seconds trying to contact the servers. It’s quite configurable/flexible; see the beginning of the module source for details. See also the RFC draft (not fully followed) and cache.ns_tout.

Running

modules = { 'serve_stale < cache' }

EDNS keepalive

The edns_keepalive module implements RFC 7828 for clients connecting to Knot Resolver via TCP and TLS. Note that client connections are timed-out the same way regardless of them sending the EDNS option; the module just allows clients to discover the timeout.

When connecting to servers, Knot Resolver does not send this EDNS option. It still attempts to reuse established connections intelligently.

Experimental DNS-over-TLS Auto-discovery

This experimental module provides automatic discovery of authoritative servers’ supporting DNS-over-TLS. The module uses magic NS names to detect SPKI fingerprint which is very similar to dnscurve mechanism.

Warning

This protocol and module is experimental and can be changed or removed at any time. Use at own risk, security properties were not analyzed!

How it works

The module will look for NS target names formatted as: dot-{base32(sha256(SPKI))}....

For instance, Knot Resolver will detect NS names formatted like this

example.com NS dot-tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a.example.com

and automatically discover that example.com NS supports DoT with the base64-encoded SPKI digest of m+12GgMFIiheEhKvUcOynjbn3WYQUp5tVGDh7Snwj/Q= and will associate it with the IPs of dot-tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a.example.com.

In that example, the base32 encoded (no padding) version of the sha256 PIN is tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a, which when converted to base64 translates to m+12GgMFIiheEhKvUcOynjbn3WYQUp5tVGDh7Snwj/Q=.

Generating NS target names

To generate the NS target name, use the following command to generate the base32 encoded string of the SPKI fingerprint:

openssl x509 -in /path/to/cert.pem  -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | \
base32 | tr -d '=' | tr '[:upper:]' '[:lower:]'
tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a

Then add a target to your NS with: dot-${b32}.a.example.com

Finally, map dot-${b32}.a.example.com to the right set of IPs.

...
...
;; QUESTION SECTION:
;example.com.      IN      NS

;; AUTHORITY SECTION:
example.com. 3600  IN      NS      dot-tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a.a.example.com.
example.com. 3600  IN      NS      dot-tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a.b.example.com.

;; ADDITIONAL SECTION:
dot-tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a.a.example.com. 3600 IN A 192.0.2.1
dot-tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a.b.example.com. 3600 IN AAAA 2001:DB8::1
...
...

Example configuration

To enable the module, add this snippet to your config:

-- Start an experiment, use with caution
modules.load('experimental_dot_auth')

This module requires standard basexx Lua library which is typically provided by lua-basexx package.

Caveats

The module relies on seeing the reply of the NS query and as such will not work if Knot Resolver uses data from its cache. You may need to delete the cache before starting kresd to work around this.

The module also assumes that the NS query answer will return both the NS targets in the Authority section as well as the glue records in the Additional section.

Dependencies

Refuse queries without RD bit

This module ensures all queries without RD (recursion desired) bit set in query are answered with REFUSED. This prevents snooping on the resolver’s cache content.

The module is loaded by default. If you’d like to disable this behavior, you can unload it:

modules.unload('refuse_nord')