# Knot DNS Resolver modules

- Static hints
- Statistics collector
- Query policies
- Views and ACLs
- Prefetching records
- HTTP/2 services
- DNS Application Firewall
- Graphite module
- Etcd module
- DNS64
- Renumber
- DNS Cookies
- Version
- Workarounds
- Dnstap
- Signaling Trust Anchor Knowledge in DNSSEC
- Sentinel for Detecting Trusted Keys
- Priming module
- Serve stale
- System time skew detector
- Detect discontinuous jumps in the system time

## 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, 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`()

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

  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.

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

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.

By default, if no rule applies to a query, built-in rules for special-use and locally-served domain names are applied. These built-in rules can be overriden using action `PASS`, see Policy examples below.

### 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` - implements a subset of RPZ in zonefile format. See below for details: `policy.rpz`.
- custom filter function

### Actions

An action is function which modifies DNS query. There are several actions available in the `policy.` table:

- `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
- `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; the parameter can be a single IP (string) or a lua list of up to four IPs.
- `STUB(ip)` - similar to `FORWARD(ip)` but without attempting DNSSEC validation. Each request may be either answered from cache or simply sent to one of the IPs with proxying back the answer.
- `MIRROR(ip)` - mirror query to given IP and continue solving it (useful for partial snooping); it’s a chain action
- `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 for the query and its sub-queries. It’s useful for debugging weird DNS servers. It’s a chain action.
- `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. It’s a chain action.

Most actions stop the policy matching on the query, but “chain actions” allow to keep trying to match other rules, until a non-chain action is triggered.

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.

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

#### 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 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.', ca_file='/etc/knot-resolver/tlsca.crt'}
    })

### 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
    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 (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)

### 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[, 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' }

This module is enabled by default because it implements mandatory RFC 6761 logic. For debugging purposes you can add `modules.unload('policy')` to your config to unload the module.

## 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')))
    -- Drop everything that hasn't matched
    view:addr('0.0.0.0/0', function (req, qry) return policy.DROP end)

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

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

### Exported metrics

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

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

### 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, see
                    -- 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: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.
`/trace/:name/:type` | Tracking | Trace resolution of the query and return the verbose logs.

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

### 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 http://localhost:8053/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 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`

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

Warning

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

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

- Nettle required for HMAC-SHA256

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

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

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

## Sentinel for Detecting Trusted Keys

The module implementing Sentinel for Detecting Trusted Keys in DNSSEC according to draft-ietf-dnsop-kskroll-sentinel-00.

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.

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

### Running

    modules = { 'serve_stale < cache' }

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