Scripting in Knot DNS Resolver
This week I was approached by a man dressed in platypus pyjamas, he asked me: “These layers and modules you talk about, they’re cool. But can it be even better?”. After the initial costume distraction wore off, I pondered a bit and said: “Sure, let me just grab a cup of coffee”. The real story is that the layers are now much more interactive, and the documentation is improved.
What are layers
Callbacks you can execute on each new/completed query, and resolver sub-requests. For example - to log all AAAA
answers from *.kiwi
, or refuse all queries matching a blocklist. Here’s an example similar to the documentation.
local mod = {}
mod.count = 0
mod.layer = {
consume = function (state, req, pkt)
pkt = kres.pkt_t(pkt)
if pkt:qtype() == kres.type.AAAA and
pkt:qname():find('\4kiwi') then
mod.count = mod.count + 1
end
return state
end
}
return mod
Save it as example.lua
and start kresd
.
$ kresd -v
> modules = { 'example' }
> resolve('abcd.kiwi', kres.type.AAAA)
...
> example.count
1
This is only passive observation of the resolver I/O. You can also inspect state of the queries. For example to answer question “which queries in this zone are answered from cache?” We can write something along the lines of:
req = kres.request_t(req)
local query = req:current()
if query.flags and kres.query.CACHED then
mod.count = mod.count + 1
end
At this point, you should skim through the Lua API reference. The examples often reference to constants like kres.type.AAAA
or accessors req:current()
, the reference explains where to find what.
Chaining queries
The previous example has shown how to observe the resolution chain. Let’s see how we can change it. For example - for every NS record, I want its SOA as well. One way how to do that, is to simply push another query. Here’s a documentation reference.
Short version is that there is a driver behind resolution which does I/O, figures out when to ask what, sets correct flags, and so on. The added value is that layers don’t have to know about DNSSEC, caching, correct ordering etc.
if pkt:qtype() == kres.type.SOA then
local next = req:push(pkt:qname(), kres.type.NS, kres.class.IN)
next.flags = kres.query.AWAIT_CUT + kres.query.DNSSEC_WANT
return kres.DONE
else
return state
end
This piece of code fetches NS
for each SOA
query, after the query completes. You can check that the record was fetched in the finish
layer afterwards. This is convenient if you want to simply chain queries.
Reordering queries
States signalize the outcome of layer pass-through, there are 3 interesting ones: DONE
, FAIL
and YIELD
. A common idiom is to copy input state if you’re not interested in this packet.
consume = function (state, req, pkt)
if state == kres.FAIL then
return state
end
return kres.DONE
end
The YIELD
pauses current layer, starts solving whatever queries you pushed, and then resumes the paused layer. One example is DNSSEC, where you need a DNSKEY to verify signatures. Or to answer only NS
records, for which a SOA
exists.
if state == kres.YIELD then
local last = req:resolved()
if bit.band(last.flags, kres.query.RESOLVED) then
return kres.DONE -- Fetched NS
else
return kres.FAIL -- Failed to fetch NS
end
else
if pkt:qtype() == kres.type.SOA then
local next = req:push(pkt:qname(), kres.type.NS,
kres.class.IN)
next.flags = kres.query.AWAIT_CUT + kres.query.DNSSEC_WANT
next.parent = qry
return kres.YIELD -- Resolve NS and resume
end
return state
end
The first branch is executed when the paused layer resumes, there we can check whether the NS
query finished successfuly. The second branch pushes the NS
query, and then pauses.
Rewriting queries
I’m a bit reluctant to write this, as there is not any good use case that comes to my mind. But let’s say you want to sinkhole several domain names, e.g. if the QNAME
matches a pattern, we rewrite it to something else. This can be done before driver starts issuing queries, in the begin layer.
begin = function(state, req)
req = kres.request_t(req)
local query = req:current()
if query:name():find('\4kiwi') then
req:pop(query) -- Pop old query, and replace with new one
req:push('\7blocked', query.type, query.class,
query.flags, 0)
end
return state
end
Wrap up
That’s it. Here we are with the examples for three different layer usage patterns in a few lines of Lua code. The advantage here is that you can drop custom modules in the configuration directory, no need to patch or recompile the software. If you don’t mess up the Lua code, it’s going to run close to C code speed. When in trouble achieving that, check the Embedding LuaJIT in 30 minutes I wrote before.