knot-resolver

Posted on Jul 6, 2022

knot-resolver (kresd, to distinguish it from the authoritative DNS server knot!) is pretty neat, but not as popular as other recursive resolvers.

kresd has DNSSEC enabled by default, can run DoT/DoH, can serve the same as well, supports RPZ, an DNS application firewall, policies, views, persistent caches, has an prometheus endpoint, can use XDP (!) and many other features. Platform support is decent as well – I’ve run in on OpenBSD, FreeBSD and Linux.

I run kresd on my router/firewall in a overly complicated configuration, but I’ll describe a working configuration below in hopes of making someone else interested in this software.

First of all, kresd is configured in Lua. That might put some people off. One great perk of this is that all modules are also written in Lua, so they are easy to inspect and it’s (probably) easy to write your own as well.

-- Specify where kresd should listen for requests:
net.listen('127.0.0.1', 53, { kind = 'dns', freebind = true })
net.listen('192.168.0.1', 53, { kind = 'dns', freebind = true })

net.listen('127.0.0.1', 8453, { kind = 'webmgmt' })

-- Query cache size
cache.size = 100 * MB

-- Each of these enable a lot of functionality, and I urge you to read the
-- documentation on each.
modules = {
  -- The policy module implements policies for global query matching, e.g.
  -- solves “how to react to certain query”.
  "policy",

  -- Serve static hints from /etc/hosts
  "hints",

  -- Serve stale records (up to 24 hours) if we timeout to our upstream NS
  "serve_stale < cache",

  -- Give us useful statistics, please
  "stats",

  -- Keep commonly used records hot in cache
  "predict",

  -- Enable web module (for webmgmt)
  "http"
}

-- This is not recommended to run in production, but for a home lab it's
-- probably fine. Here we set a 30 minute window, and the period to 24 hours.
predict.config({ window = 30, period = 24*(60/30) })

-- I'm too lazy to setup a TLS certificate for this, allow the webmgmt interface
-- to be reached over HTTP. I use this as a prometheus endpoint.
http.config({
  tls = false,
})

-- Sets the prefix for the prometheus metrics
http.prometheus.namespace = 'kresd_'

-- I use a local authoritative DNS server (knot, ironically) for my internal
-- zones. I just threw in example.com here due to reasons.
internal_domains = policy.todnames({
  'home.arpa.',
  'example.com.'
})

-- The authoritative server runs on 127.0.0.1, port 2153
policy.add(policy.suffix(policy.STUB({'127.0.0.1@2153'}), internal_domains))

-- Finally, recurse all other domains over TLS to these public resolvers.
policy.add(
  policy.all(
    policy.TLS_FORWARD({
      {'8.8.8.8', hostname='dns.google' },
      {'8.8.4.4', hostname='dns.google' },
      {'1.1.1.1', hostname='cloudflare-dns.com' },
      {'1.0.0.1', hostname='cloudflare-dns.com' },
      {'9.9.9.9', hostname='dns.quad9.net' }
    })
))
-- You can also use policy.slice() to forward different parts of queries to
-- different servers.

The documentation can be read here.