Knot Resolver Conditional DNS Override

Knot Resolver Conditional DNS Override

· 234 Wörter · 2 Minuten Lesezeit

Overview

This configuration implements a conditional split-DNS policy in Knot Resolver (kresd):

  • Localhost queries (127.0.0.0/8, ::1, ::ffff:127.x.x.x, or internal): Resolved normally via upstream (policy.PASS) with an informational log entry.
  • External/Remote client queries: Intercepted and answered directly with a statically configured IP address (policy.ANSWER).

Technical Context & Data Structures

Knot Resolver's Lua engine exposes C data structures via LuaJIT FFI:

  1. struct kr_request: Represents the complete client request lifecycle.
    • The originating client sockaddr is stored at req.qsource.addr.
  2. struct kr_query: Represents an individual query within the resolution plan.
    • query.sname: Wire-format domain name (converted to text via kres.dname2str()).
    • query.request: Pointer back to the parent kr_request.
  3. kr_straddr(addr): Converts const struct sockaddr * to a presentation string (IPv4/IPv6 text).
  4. log_info(grp, msg): Emits log entries requiring a valid log group enum (ffi.C.LOG_GRP_POLICY) as its first argument.

Configuration

add include_config the resolver config like so:

config resolver 'kresd'
        [..]
        option include_config '/etc/kresd/custom.conf'

Then add an /etc/kresd/custom.config :

local ffi = require('ffi')
pcall(ffi.cdef, 'char *kr_straddr(const struct sockaddr *addr);')

log_level('info')

-- Direkte Zuordnung nach Record-Typ (kres.type.A / kres.type.AAAA)
local records = {
    ['lomotron.de'] = {
        [kres.type.A]   = { 'LOCAL IPV4 ADRESS' },
        [kres.type.AAAA] = {
            'LOCAL IPV6 ADDRESS'   --ula
        }
    }
}

policy.add(function (_, query)
    if not query or not query.sname then return nil end

    local domain = kres.dname2str(query.sname):lower():gsub('%.$', '')
    local entry = records[domain]

    if entry and entry[query.stype] then
        local addr = query.request and query.request.qsource.addr
        local ip = addr ~= nil and ffi.string(ffi.C.kr_straddr(addr))
        local client_ip = ip and ip:match('^(.-)#') or ip

        if not client_ip or client_ip:find('^127%.') or client_ip == '::1' or client_ip:find('^::ffff:127%.') then
            log_info(ffi.C.LOG_GRP_POLICY, string.format('%s from localhost (%s) -> PASS', domain, ip or 'internal'))
            return policy.PASS
        end

        local rdata = {}
        for _, address in ipairs(entry[query.stype]) do
            table.insert(rdata, kres.str2ip(address))
            log_info(ffi.C.LOG_GRP_POLICY, string.format('%s from %s -> ANSWER record: %s', domain, ip, address))
        end

        return policy.ANSWER({
            [query.stype] = { rdata = rdata, ttl = 300 }
        })
    end
end)

Key Features

  • Extensibility: Add additional domain redirects simply by adding key-value entries to the fixed_ips table:
local records = {
    ['lomotron.de'] = {
        [kres.type.A]   = { 'LOCAL IPV4 ADRESS' },
        [kres.type.AAAA] = {
            'LOCAL IPV6 ADDRESS'   --ula
        }
    }
}
  • Type Safety: Directly uses query.request.qsource.addr and safe FFI bindings for address conversion.
  • Minimal Overhead: Evaluates queries directly via table lookup and returns built-in policy actions.

Refrences