summaryrefslogtreecommitdiffstats
path: root/daemon/lua/kres.lua
diff options
context:
space:
mode:
Diffstat (limited to 'daemon/lua/kres.lua')
-rw-r--r--daemon/lua/kres.lua1143
1 files changed, 1143 insertions, 0 deletions
diff --git a/daemon/lua/kres.lua b/daemon/lua/kres.lua
new file mode 100644
index 0000000..4dc2b40
--- /dev/null
+++ b/daemon/lua/kres.lua
@@ -0,0 +1,1143 @@
+-- LuaJIT ffi bindings for libkres, a DNS resolver library.
+-- SPDX-License-Identifier: GPL-3.0-or-later
+--
+-- @note Since it's statically compiled, it expects to find the symbols in the C namespace.
+
+local kres -- the module
+
+local kluautil = require('kluautil')
+local ffi = require('ffi')
+local bit = require('bit')
+local bor = bit.bor
+local band = bit.band
+local C = ffi.C
+local knot = ffi.load(libknot_SONAME)
+
+-- Inverse table
+local function itable(t, tolower)
+ local it = {}
+ for k,v in pairs(t) do it[v] = tolower and string.lower(k) or k end
+ return it
+end
+
+-- Byte order conversions
+local function htonl(x) return x end
+local htons = htonl
+if ffi.abi('le') then
+ htonl = bit.bswap
+ function htons(x) return bit.rshift(htonl(x), 16) end
+end
+
+-- Basic types
+local u16_p = ffi.typeof('uint16_t *')
+
+-- Various declarations that are very stable.
+ffi.cdef[[
+/*
+ * Data structures
+ */
+
+struct sockaddr {
+ uint16_t sa_family;
+ uint8_t _stub[]; /* Do not touch */
+};
+
+struct knot_error {
+ int code;
+};
+
+/*
+ * libc APIs
+ */
+void * malloc(size_t size);
+void free(void *ptr);
+int inet_pton(int af, const char *src, void *dst);
+int gettimeofday(struct timeval *tv, struct timezone *tz);
+]]
+
+require('kres-gen')
+
+-- Error code representation
+local knot_error_t = ffi.typeof('struct knot_error')
+ffi.metatype(knot_error_t, {
+ -- Convert libknot error strings
+ __tostring = function(self)
+ return ffi.string(knot.knot_strerror(self.code))
+ end,
+});
+
+-- Constant tables
+local const_class = {
+ IN = 1,
+ CH = 3,
+ NONE = 254,
+ ANY = 255,
+}
+local const_type = {
+ A = 1,
+ NS = 2,
+ MD = 3,
+ MF = 4,
+ CNAME = 5,
+ SOA = 6,
+ MB = 7,
+ MG = 8,
+ MR = 9,
+ NULL = 10,
+ WKS = 11,
+ PTR = 12,
+ HINFO = 13,
+ MINFO = 14,
+ MX = 15,
+ TXT = 16,
+ RP = 17,
+ AFSDB = 18,
+ X25 = 19,
+ ISDN = 20,
+ RT = 21,
+ NSAP = 22,
+ ['NSAP-PTR'] = 23,
+ SIG = 24,
+ KEY = 25,
+ PX = 26,
+ GPOS = 27,
+ AAAA = 28,
+ LOC = 29,
+ NXT = 30,
+ EID = 31,
+ NIMLOC = 32,
+ SRV = 33,
+ ATMA = 34,
+ NAPTR = 35,
+ KX = 36,
+ CERT = 37,
+ A6 = 38,
+ DNAME = 39,
+ SINK = 40,
+ OPT = 41,
+ APL = 42,
+ DS = 43,
+ SSHFP = 44,
+ IPSECKEY = 45,
+ RRSIG = 46,
+ NSEC = 47,
+ DNSKEY = 48,
+ DHCID = 49,
+ NSEC3 = 50,
+ NSEC3PARAM = 51,
+ TLSA = 52,
+ SMIMEA = 53,
+ HIP = 55,
+ NINFO = 56,
+ RKEY = 57,
+ TALINK = 58,
+ CDS = 59,
+ CDNSKEY = 60,
+ OPENPGPKEY = 61,
+ CSYNC = 62,
+ ZONEMD = 63,
+ SVCB = 64,
+ HTTPS = 65,
+
+ SPF = 99,
+ UINFO = 100,
+ UID = 101,
+ GID = 102,
+ UNSPEC = 103,
+ NID = 104,
+ L32 = 105,
+ L64 = 106,
+ LP = 107,
+ EUI48 = 108,
+ EUI64 = 109,
+ TKEY = 249,
+ TSIG = 250,
+ IXFR = 251,
+ AXFR = 252,
+ MAILB = 253,
+ MAILA = 254,
+ ANY = 255,
+ URI = 256,
+ CAA = 257,
+ AVC = 258,
+ DOA = 259,
+ TA = 32768,
+ DLV = 32769,
+}
+local const_section = {
+ ANSWER = 0,
+ AUTHORITY = 1,
+ ADDITIONAL = 2,
+}
+local const_opcode = {
+ QUERY = 0,
+ IQUERY = 1,
+ STATUS = 2,
+ NOTIFY = 4,
+ UPDATE = 5,
+}
+local const_rcode = {
+ NOERROR = 0,
+ FORMERR = 1,
+ SERVFAIL = 2,
+ NXDOMAIN = 3,
+ NOTIMPL = 4,
+ REFUSED = 5,
+ YXDOMAIN = 6,
+ YXRRSET = 7,
+ NXRRSET = 8,
+ NOTAUTH = 9,
+ NOTZONE = 10,
+ BADVERS = 16,
+ BADCOOKIE = 23,
+}
+-- This corresponds to `enum kr_rank`, it's not possible to do this without introspection unfortunately
+local const_rank = {
+ INITIAL = 0,
+ OMIT = 1,
+ TRY = 2,
+ INDET = 4,
+ BOGUS = 5,
+ MISMATCH = 6,
+ MISSING = 7,
+ INSECURE = 8,
+ AUTH = 16,
+ SECURE = 32
+}
+local const_extended_error = {
+ NONE = -1,
+ OTHER = 0,
+ DNSKEY_ALG = 1,
+ DS_DIGEST = 2,
+ STALE = 3,
+ FORGED = 4,
+ INDETERMINATE = 5,
+ BOGUS = 6,
+ SIG_EXPIRED = 7,
+ SIG_NOTYET = 8,
+ DNSKEY_MISS = 9,
+ RRSIG_MISS = 10,
+ DNSKEY_BIT = 11,
+ NSEC_MISS = 12,
+ CACHED_ERR = 13,
+ NOT_READY = 14,
+ BLOCKED = 15,
+ CENSORED = 16,
+ FILTERED = 17,
+ PROHIBITED = 18,
+ STALE_NXD = 19,
+ NOTAUTH = 20,
+ NOTSUP = 21,
+ NREACH_AUTH = 22,
+ NETWORK = 23,
+ INV_DATA = 24,
+}
+
+-- Constant tables
+local const_class_str = itable(const_class)
+local const_type_str = itable(const_type)
+local const_rcode_str = itable(const_rcode)
+local const_opcode_str = itable(const_opcode)
+local const_section_str = itable(const_section)
+local const_rank_str = itable(const_rank)
+local const_extended_error_str = itable(const_extended_error)
+
+-- Metatype for RR types to allow anonymous types
+setmetatable(const_type, {
+ __index = function (t, k)
+ local v = rawget(t, k)
+ if v then return v end
+ -- Allow TYPE%d notation
+ if string.find(k, 'TYPE', 1, true) then
+ return tonumber(k:sub(5))
+ end
+ -- Unknown type
+ return
+ end
+})
+
+-- Metatype for RR types to allow anonymous string types
+setmetatable(const_type_str, {
+ __index = function (t, k)
+ local v = rawget(t, k)
+ if v then return v end
+ return string.format('TYPE%d', k)
+ end
+})
+
+-- Metatype for timeval
+local timeval_t = ffi.typeof('struct timeval')
+
+-- Metatype for sockaddr
+local addr_buf = ffi.new('char[16]')
+local str_addr_buf = ffi.new('char[46 + 1 + 6 + 1]') -- INET6_ADDRSTRLEN + #port + \0
+local str_addr_buf_len = ffi.sizeof(str_addr_buf)
+local sockaddr_t = ffi.typeof('struct sockaddr')
+ffi.metatype( sockaddr_t, {
+ __index = {
+ len = function(sa) return C.kr_inaddr_len(sa) end,
+ ip = function (sa) return C.kr_inaddr(sa) end,
+ family = function (sa) return C.kr_inaddr_family(sa) end,
+ port = function (sa) return C.kr_inaddr_port(sa) end,
+ },
+ __tostring = function(sa)
+ assert(ffi.istype(sockaddr_t, sa))
+ local len = ffi.new('size_t[1]', str_addr_buf_len)
+ local ret = C.kr_inaddr_str(sa, str_addr_buf, len)
+ if ret ~= 0 then
+ error('kr_inaddr_str failed: ' .. tostring(ret))
+ end
+ return ffi.string(str_addr_buf)
+ end,
+
+})
+
+-- Parametrized LRU table
+local typed_lru_t = 'struct { $ value_type[1]; struct lru * lru; }'
+
+-- Metatype for LRU
+local lru_metatype = {
+ -- Create a new LRU with given value type
+ -- By default the LRU will have a capacity of 65536 elements
+ -- Note: At the point the parametrized type must be finalized
+ __new = function (ct, max_slots, alignment)
+ -- {0} will make sure that the value is coercible to a number
+ local o = ffi.new(ct, {0}, C.lru_create_impl(max_slots or 65536, alignment or 1, nil, nil))
+ if o.lru == nil then
+ return
+ end
+ return o
+ end,
+ -- Destructor to clean allocated memory
+ __gc = function (self)
+ assert(self.lru ~= nil)
+ C.lru_free_items_impl(self.lru)
+ C.free(self.lru)
+ self.lru = nil
+ end,
+ __index = {
+ -- Look up key and return reference to current
+ -- Note: The key will be inserted if it doesn't exist
+ get_ref = function (self, key, key_len, allow_insert)
+ local insert = allow_insert and true or false
+ local ptr = C.lru_get_impl(self.lru, key, key_len or #key, ffi.sizeof(self.value_type[0]), insert, nil)
+ if ptr ~= nil then
+ return ffi.cast(self.value_type, ptr)
+ end
+ end,
+ -- Look up key and return current value
+ get = function (self, key, key_len)
+ local ref = self:get_ref(key, key_len, false)
+ if ref then
+ return ref[0]
+ end
+ end,
+ -- Set value for key to given value
+ set = function (self, key, value, key_len)
+ local ref = self:get_ref(key, key_len, true)
+ if ref then
+ ref[0] = value
+ return true
+ end
+ end,
+ },
+}
+
+-- Pretty print for domain name
+local function dname2str(dname)
+ if dname == nil then return end
+ local text_name = ffi.gc(C.knot_dname_to_str(nil, dname, 0), C.free)
+ if text_name ~= nil then
+ return ffi.string(text_name)
+ end
+end
+
+-- Convert dname pointer to wireformat string
+local function dname2wire(name)
+ if name == nil then return nil end
+ return ffi.string(name, knot.knot_dname_size(name))
+end
+
+-- Parse RDATA, from presentation to wire-format.
+-- in: a table of strings, each a line describing RRTYPE+RDATA
+-- out: a table of RDATA strings in wire-format
+local function parse_rdata(strs, nothing)
+ local zonefile = require('zonefile')
+ if type(strs) ~= 'table' or nothing ~= nil then -- accidents like forgetting braces
+ error('a table of string(s) is expected', 2)
+ end
+ local res = {}
+ for _, line in ipairs(strs) do
+ if type(line) ~= 'string' then
+ error('table must contain strings', 2)
+ end
+ local rrs = zonefile.string('. ' .. line)
+ if #rrs == 0 then error('failed to parse line: ' .. line, 2) end
+ for _, rr in ipairs(rrs) do
+ table.insert(res, rr.rdata)
+ end
+ end
+ return res
+end
+
+-- RR sets created in Lua must have a destructor to release allocated memory
+local function rrset_free(rr)
+ if rr._owner ~= nil then ffi.C.free(rr._owner) end
+ if rr:rdcount() > 0 then ffi.C.free(rr.rrs.rdata) end
+end
+
+-- Metatype for RR set. Beware, the indexing is 0-based (rdata, get, tostring).
+local rrset_buflen = (64 + 1) * 1024
+local rrset_buf = ffi.new('char[?]', rrset_buflen)
+local knot_rrset_pt = ffi.typeof('knot_rrset_t *')
+local knot_rrset_t = ffi.typeof('knot_rrset_t')
+ffi.metatype( knot_rrset_t, {
+ -- Create a new empty RR set object with an allocated owner and a destructor
+ __new = function (ct, owner, rrtype, rrclass, ttl)
+ local rr = ffi.new(ct)
+ C.kr_rrset_init(rr,
+ owner and knot.knot_dname_copy(owner, nil),
+ rrtype or 0,
+ rrclass or const_class.IN,
+ ttl or 0)
+ return ffi.gc(rr, rrset_free)
+ end,
+ -- BEWARE: `owner` and `rdata` are typed as a plain lua strings
+ -- and not the real types they represent.
+ __tostring = function(rr)
+ assert(ffi.istype(knot_rrset_t, rr))
+ return rr:txt_dump()
+ end,
+ __index = {
+ owner = function(rr)
+ assert(ffi.istype(knot_rrset_t, rr))
+ return dname2wire(rr._owner)
+ end,
+ ttl = function(rr)
+ assert(ffi.istype(knot_rrset_t, rr))
+ return tonumber(rr._ttl)
+ end,
+ class = function(rr, val)
+ assert(ffi.istype(knot_rrset_t, rr))
+ if val then
+ rr.rclass = val
+ end
+ return tonumber(rr.rclass)
+ end,
+ rdata_pt = function(rr, i)
+ assert(ffi.istype(knot_rrset_t, rr) and i >= 0 and i < rr:rdcount())
+ return knot.knot_rdataset_at(rr.rrs, i)
+ end,
+ rdata = function(rr, i)
+ assert(ffi.istype(knot_rrset_t, rr))
+ local rd = rr:rdata_pt(i)
+ return ffi.string(rd.data, rd.len)
+ end,
+ get = function(rr, i)
+ assert(ffi.istype(knot_rrset_t, rr) and i >= 0 and i < rr:rdcount())
+ return {owner = rr:owner(),
+ ttl = rr:ttl(),
+ class = tonumber(rr.rclass),
+ type = tonumber(rr.type),
+ rdata = rr:rdata(i)}
+ end,
+ tostring = function(rr, i)
+ assert(ffi.istype(knot_rrset_t, rr)
+ and (i == nil or (i >= 0 and i < rr:rdcount())) )
+ if rr:rdcount() > 0 then
+ local ret
+ if i ~= nil then
+ ret = knot.knot_rrset_txt_dump_data(rr, i, rrset_buf, rrset_buflen, C.KR_DUMP_STYLE_DEFAULT)
+ else
+ ret = -1
+ end
+ return ret >= 0 and ffi.string(rrset_buf)
+ end
+ end,
+
+ -- Dump the rrset in presentation format (dig-like).
+ txt_dump = function(rr, style)
+ assert(ffi.istype(knot_rrset_t, rr))
+ local bufsize = 1024
+ local dump = ffi.new('char *[1]', C.malloc(bufsize))
+ -- ^ one pointer to a string
+ local size = ffi.new('size_t[1]', { bufsize }) -- one size_t = bufsize
+
+ local ret = knot.knot_rrset_txt_dump(rr, dump, size,
+ style or C.KR_DUMP_STYLE_DEFAULT)
+ local result = nil
+ if ret >= 0 then
+ result = ffi.string(dump[0], ret)
+ end
+ C.free(dump[0])
+ return result
+ end,
+ txt_fields = function(rr, i)
+ assert(ffi.istype(knot_rrset_t, rr))
+ assert(i >= 0 and i < rr:rdcount())
+ local bufsize = 1024
+ local dump = ffi.new('char *', C.malloc(bufsize))
+ ffi.gc(dump, C.free)
+
+ local ret = knot.knot_rrset_txt_dump_data(rr, i, dump, 1024,
+ C.KR_DUMP_STYLE_DEFAULT)
+ if ret >= 0 then
+ local out = {}
+ out.owner = dname2str(rr:owner())
+ out.ttl = rr:ttl()
+ out.class = kres.tostring.class[rr:class()]
+ out.type = kres.tostring.type[rr.type]
+ out.rdata = ffi.string(dump, ret)
+ return out
+ else
+ panic('knot_rrset_txt_dump_data failure ' .. tostring(ret))
+ end
+ end,
+ -- Return RDATA count for this RR set
+ rdcount = function(rr)
+ assert(ffi.istype(knot_rrset_t, rr))
+ return tonumber(rr.rrs.count)
+ end,
+ -- Add binary RDATA to the RR set
+ add_rdata = function (rr, rdata, rdlen, no_ttl)
+ assert(ffi.istype(knot_rrset_t, rr))
+ assert(no_ttl == nil, 'add_rdata() can not accept TTL anymore')
+ local ret = knot.knot_rrset_add_rdata(rr, rdata, tonumber(rdlen), nil)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ -- Merge data from another RR set into the current one
+ merge_rdata = function (rr, source)
+ assert(ffi.istype(knot_rrset_t, rr))
+ assert(ffi.istype(knot_rrset_t, source))
+ local ret = knot.knot_rdataset_merge(rr.rrs, source.rrs, nil)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ -- Return type covered by this RRSIG
+ type_covered = function(rr, i)
+ i = i or 0
+ assert(ffi.istype(knot_rrset_t, rr) and i >= 0 and i < rr:rdcount())
+ if rr.type ~= const_type.RRSIG then return end
+ return tonumber(C.kr_rrsig_type_covered(knot.knot_rdataset_at(rr.rrs, i)))
+ end,
+ -- Check whether a RRSIG is covering current RR set
+ is_covered_by = function(rr, rrsig)
+ assert(ffi.istype(knot_rrset_t, rr))
+ assert(ffi.istype(knot_rrset_t, rrsig))
+ assert(rrsig.type == const_type.RRSIG)
+ return (rr.type == rrsig:type_covered() and rr:owner() == rrsig:owner())
+ end,
+ -- Return RR set wire size
+ wire_size = function(rr)
+ assert(ffi.istype(knot_rrset_t, rr))
+ return tonumber(knot.knot_rrset_size(rr))
+ end,
+ },
+})
+
+-- Destructor for packet accepts pointer to pointer
+local knot_pkt_t = ffi.typeof('knot_pkt_t')
+
+-- Helpers for reading/writing 16-bit numbers from packet wire
+local function pkt_u16(pkt, off, val)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ptr = ffi.cast(u16_p, pkt.wire + off)
+ if val ~= nil then ptr[0] = htons(val) end
+ return (htons(ptr[0]))
+end
+
+-- Helpers for reading/writing message header flags
+local function pkt_bit(pkt, byteoff, bitmask, val)
+ -- If the value argument is passed, set/clear the desired bit
+ if val ~= nil then
+ if val then pkt.wire[byteoff] = bit.bor(pkt.wire[byteoff], bitmask)
+ else pkt.wire[byteoff] = bit.band(pkt.wire[byteoff], bit.bnot(bitmask)) end
+ return true
+ end
+ return (bit.band(pkt.wire[byteoff], bitmask) ~= 0)
+end
+
+local function knot_pkt_rr(section, i)
+ assert(section and ffi.istype('knot_pktsection_t', section)
+ and i >= 0 and i < section.count)
+ local ret = section.pkt.rr + section.pos + i
+ assert(ffi.istype(knot_rrset_pt, ret))
+ return ret
+end
+
+-- Metatype for packet
+ffi.metatype( knot_pkt_t, {
+ __new = function (_, size, wire)
+ if size < 12 or size > 65535 then
+ error('packet size must be <12, 65535>')
+ end
+
+ local pkt = knot.knot_pkt_new(nil, size, nil)
+ if pkt == nil then
+ error(string.format('failed to allocate a packet of size %d', size))
+ end
+ if wire == nil then
+ C.kr_rnd_buffered(pkt.wire, 2) -- randomize the query ID
+ else
+ assert(size <= #wire)
+ ffi.copy(pkt.wire, wire, size)
+ pkt.size = size
+ pkt.parsed = 0
+ end
+
+ return ffi.gc(pkt[0], knot.knot_pkt_free)
+ end,
+ __tostring = function(pkt)
+ return pkt:tostring()
+ end,
+ __len = function(pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ return tonumber(pkt.size)
+ end,
+ __ipairs = function(self)
+ return ipairs(self:section(const_section.ANSWER))
+ end,
+ __index = {
+ -- Header
+ id = function(pkt, val) return pkt_u16(pkt, 0, val) end,
+ qdcount = function(pkt, val) return pkt_u16(pkt, 4, val) end,
+ ancount = function(pkt, val) return pkt_u16(pkt, 6, val) end,
+ nscount = function(pkt, val) return pkt_u16(pkt, 8, val) end,
+ arcount = function(pkt, val) return pkt_u16(pkt, 10, val) end,
+ opcode = function (pkt, val)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ pkt.wire[2] = (val) and bit.bor(bit.band(pkt.wire[2], 0x78), 8 * val) or pkt.wire[2]
+ return (bit.band(pkt.wire[2], 0x78) / 8)
+ end,
+ rcode = function (pkt, val)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ pkt.wire[3] = (val) and bor(band(pkt.wire[3], 0xf0), val) or pkt.wire[3]
+ return band(pkt.wire[3], 0x0f)
+ end,
+ rd = function (pkt, val) return pkt_bit(pkt, 2, 0x01, val) end,
+ tc = function (pkt, val) return pkt_bit(pkt, 2, 0x02, val) end,
+ aa = function (pkt, val) return pkt_bit(pkt, 2, 0x04, val) end,
+ qr = function (pkt, val) return pkt_bit(pkt, 2, 0x80, val) end,
+ cd = function (pkt, val) return pkt_bit(pkt, 3, 0x10, val) end,
+ ad = function (pkt, val) return pkt_bit(pkt, 3, 0x20, val) end,
+ ra = function (pkt, val) return pkt_bit(pkt, 3, 0x80, val) end,
+ -- "do" is a reserved word in Lua; only getter
+ dobit = function(pkt, val)
+ assert(val == nil, 'dobit is getter only')
+ assert(ffi.istype(knot_pkt_t, pkt))
+ return C.kr_pkt_has_dnssec(pkt)
+ end,
+ -- Question
+ qname = function(pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ -- inlined knot_pkt_qname(), basically but not lower-cased
+ if pkt == nil or pkt.qname_size == 0 then return nil end
+ return ffi.string(pkt.wire + 12, pkt.qname_size)
+ end,
+ qclass = function(pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ return C.kr_pkt_qclass(pkt)
+ end,
+ qtype = function(pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ return C.kr_pkt_qtype(pkt)
+ end,
+ rrsets = function (pkt, section_id)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local records = {}
+ local section = pkt.sections + section_id
+ for i = 1, section.count do
+ local rrset = knot_pkt_rr(section, i - 1)
+ table.insert(records, rrset)
+ end
+ return records
+ end,
+ section = function (pkt, section_id)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local records = {}
+ local section = pkt.sections + section_id
+ for i = 1, section.count do
+ local rrset = knot_pkt_rr(section, i - 1)
+ for k = 1, rrset:rdcount() do
+ table.insert(records, rrset:get(k - 1))
+ end
+ end
+ return records
+ end,
+ begin = function (pkt, section)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ assert(section >= pkt.current, 'cannot rewind to already written section')
+ assert(const_section_str[section], string.format('invalid section: %s', section))
+ local ret = knot.knot_pkt_begin(pkt, section)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ put = function (pkt, owner, ttl, rclass, rtype, rdata)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ret = C.kr_pkt_put(pkt, owner, ttl, rclass, rtype, rdata, #rdata)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ -- Put an RR set in the packet
+ -- Note: the packet doesn't take ownership of the RR set
+ put_rr = function (pkt, rr, rotate, flags)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ assert(ffi.istype(knot_rrset_t, rr))
+ local ret = C.knot_pkt_put_rotate(pkt, 0, rr, rotate or 0, flags or 0)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ -- Checks whether the packet has a wire, i.e. the .size is not
+ -- equal to KR_PKT_SIZE_NOWIRE
+ has_wire = function (pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ return C.kr_pkt_has_wire(pkt)
+ end,
+ recycle = function (pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ret = C.kr_pkt_recycle(pkt)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ clear_payload = function (pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ret = C.kr_pkt_clear_payload(pkt)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ question = function(pkt, qname, qclass, qtype)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ assert(qclass ~= nil, string.format('invalid class: %s', qclass))
+ assert(qtype ~= nil, string.format('invalid type: %s', qtype))
+ local ret = C.knot_pkt_put_question(pkt, qname, qclass, qtype)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ towire = function (pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ return ffi.string(pkt.wire, pkt.size)
+ end,
+ tostring = function(pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ return ffi.string(ffi.gc(C.kr_pkt_text(pkt), C.free))
+ end,
+ -- Return number of remaining empty bytes in the packet
+ -- This is generally useful to check if there's enough space
+ remaining_bytes = function (pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local occupied = pkt.size + pkt.reserved
+ assert(pkt.max_size >= occupied)
+ return tonumber(pkt.max_size - occupied)
+ end,
+ -- Packet manipulation
+ parse = function (pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ret = knot.knot_pkt_parse(pkt, 0)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ -- Resize packet wire to a new size
+ resize = function (pkt, new_size)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ptr = C.mm_realloc(pkt.mm, pkt.wire, new_size, pkt.max_size)
+ if ptr == nil then return end
+ pkt.wire = ptr
+ pkt.max_size = new_size
+ return true
+ end,
+ },
+})
+-- Metatype for query
+local kr_query_t = ffi.typeof('struct kr_query')
+ffi.metatype( kr_query_t, {
+ __index = {
+ -- Return query domain name
+ name = function(qry)
+ assert(ffi.istype(kr_query_t, qry))
+ return dname2wire(qry.sname)
+ end,
+ -- Write this query into packet
+ write = function(qry, pkt)
+ assert(ffi.istype(kr_query_t, qry))
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ret = C.kr_make_query(qry, pkt)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ },
+})
+
+-- helper for trace_chain_callbacks
+-- ignores return values from successful calls but logs tracebacks for throws
+local function void_xpcall_log_tb(func, req, msg)
+ local ok, err = xpcall(func, debug.traceback, req, msg)
+ if not ok then
+ log_error(ffi.C.LOG_GRP_SYSTEM, 'callback %s req %s msg %s stack traceback:\n%s', func, req, msg, err)
+ end
+end
+
+local function void_xpcall_finish_tb(func, req)
+ local ok, err = xpcall(func, debug.traceback, req)
+ if not ok then
+ log_error(ffi.C.LOG_GRP_SYSTEM, 'callback %s req %s stack traceback:\n%s', func, req, err)
+ end
+end
+
+
+-- Metatype for request
+local kr_request_t = ffi.typeof('struct kr_request')
+ffi.metatype( kr_request_t, {
+ __index = {
+ -- makes sense only when request is finished
+ all_from_cache = function(req)
+ assert(ffi.istype(kr_request_t, req))
+ local rplan = ffi.C.kr_resolve_plan(req)
+ if tonumber(rplan.pending.len) > 0 then
+ -- an unresolved query,
+ -- i.e. something is missing from the cache
+ return false
+ end
+ for idx=0, tonumber(rplan.resolved.len) - 1 do
+ if not rplan.resolved.at[idx].flags.CACHED then
+ return false
+ end
+ end
+ return true
+ end,
+ current = function(req)
+ assert(ffi.istype(kr_request_t, req))
+ if req.current_query == nil then return nil end
+ return req.current_query
+ end,
+ -- returns the initial query that started the request
+ initial = function(req)
+ assert(ffi.istype(kr_request_t, req))
+ local rplan = C.kr_resolve_plan(req)
+ if rplan.initial == nil then return nil end
+ return rplan.initial
+ end,
+ -- Return last query on the resolution plan
+ last = function(req)
+ assert(ffi.istype(kr_request_t, req))
+ local query = C.kr_rplan_last(C.kr_resolve_plan(req))
+ if query == nil then return end
+ return query
+ end,
+ resolved = function(req)
+ assert(ffi.istype(kr_request_t, req))
+ local qry = C.kr_rplan_resolved(C.kr_resolve_plan(req))
+ if qry == nil then return nil end
+ return qry
+ end,
+ -- returns first resolved sub query for a request
+ first_resolved = function(req)
+ assert(ffi.istype(kr_request_t, req))
+ local rplan = C.kr_resolve_plan(req)
+ if not rplan or rplan.resolved.len < 1 then return nil end
+ return rplan.resolved.at[0]
+ end,
+ push = function(req, qname, qtype, qclass, flags, parent)
+ assert(ffi.istype(kr_request_t, req))
+ flags = kres.mk_qflags(flags) -- compatibility
+ local rplan = C.kr_resolve_plan(req)
+ local qry = C.kr_rplan_push(rplan, parent, qname, qclass, qtype)
+ if qry ~= nil and flags ~= nil then
+ C.kr_qflags_set(qry.flags, flags)
+ end
+ return qry
+ end,
+ pop = function(req, qry)
+ assert(ffi.istype(kr_request_t, req))
+ return C.kr_rplan_pop(C.kr_resolve_plan(req), qry)
+ end,
+ selected_tostring = function(req)
+ assert(ffi.istype(kr_request_t, req))
+ local buf = {}
+ if #req.answ_selected ~= 0 then
+ table.insert(buf, ';; selected from ANSWER sections:\n')
+ table.insert(buf, tostring(req.answ_selected))
+ end
+ if #req.auth_selected ~= 0 then
+ table.insert(buf, ';; selected from AUTHORITY sections:\n')
+ table.insert(buf, tostring(req.auth_selected))
+ end
+ if #req.add_selected ~= 0 then
+ table.insert(buf, ';; selected from ADDITIONAL sections:\n')
+ table.insert(buf, tostring(req.add_selected))
+ end
+ return table.concat(buf, '')
+ end,
+ set_extended_error = function(req, code, msg)
+ assert(ffi.istype(kr_request_t, req))
+ msg = kluautil.kr_string2c(msg, req.pool)
+ ffi.C.kr_request_set_extended_error(req, code, msg)
+ end,
+
+ -- chain new callbacks after the old ones
+ -- creates new wrapper functions as necessary
+ -- note: callbacks are FFI cdata pointers so tests must
+ -- use explicit "cb == nil", just "if cb" does not work
+ --
+ trace_chain_callbacks = function (req, new_log, new_finish)
+ local log_wrapper
+ if req.trace_log == nil then
+ req.trace_log = new_log
+ else
+ local old_log = req.trace_log
+ log_wrapper = ffi.cast('trace_log_f',
+ function(cbreq, msg)
+ jit.off(true, true) -- JIT for (C -> lua)^2 nesting isn't allowed
+ void_xpcall_log_tb(old_log, cbreq, msg)
+ void_xpcall_log_tb(new_log, cbreq, msg)
+ end)
+ req.trace_log = log_wrapper
+ end
+ local old_finish = req.trace_finish
+ if not (log_wrapper ~= nil or old_finish ~= nil) then
+ req.trace_finish = new_finish
+ else
+ local fin_wrapper
+ fin_wrapper = ffi.cast('trace_callback_f',
+ function(cbreq)
+ jit.off(true, true) -- JIT for (C -> lua)^2 nesting isn't allowed
+ if old_finish ~= nil then
+ void_xpcall_finish_tb(old_finish, cbreq)
+ end
+ if new_finish ~= nil then
+ void_xpcall_finish_tb(new_finish, cbreq)
+ end
+ -- beware: finish callbacks can call log callback
+ if log_wrapper ~= nil then
+ log_wrapper:free()
+ end
+ fin_wrapper:free()
+ end)
+ req.trace_finish = fin_wrapper
+ end
+ end,
+
+ -- Return per-request variable table
+ -- The request can store anything in this Lua table and it will be freed
+ -- when the request is closed, it doesn't have to worry about contents.
+ vars = function (req)
+ assert(ffi.istype(kr_request_t, req))
+ -- Return variable if it's already stored
+ local var = worker.vars[req.vars_ref]
+ if var then
+ return var
+ end
+ -- Either take a slot number from freelist
+ -- or find a first free slot (expand the table)
+ local ref = worker.vars[0]
+ if ref then
+ worker.vars[0] = worker.vars[ref]
+ else
+ ref = #worker.vars + 1
+ end
+ -- Create new variables table
+ var = {}
+ worker.vars[ref] = var
+ -- Save reference in the request
+ req.vars_ref = ref
+ return var
+ end,
+ -- Ensure that answer has EDNS if needed; can't fail.
+ ensure_edns = function (req)
+ assert(ffi.istype(kr_request_t, req))
+ return C.kr_request_ensure_edns(req)
+ end,
+ -- Ensure that answer exists and return it; can't fail.
+ ensure_answer = function (req)
+ assert(ffi.istype(kr_request_t, req))
+ return C.kr_request_ensure_answer(req)
+ end,
+ },
+})
+
+-- C array iterator
+local function c_array_iter(t, i)
+ i = i + 1
+ if i >= t.len then return end
+ return i, t.at[i][0]
+end
+
+-- Metatype for a single ranked record array entry (one RRset)
+local function rank_tostring(rank)
+ local names = {}
+ for name, value in pairs(const_rank) do
+ if ffi.C.kr_rank_test(rank, value) then
+ table.insert(names, string.lower(name))
+ end
+ end
+ table.sort(names) -- pairs() above doesn't give a stable ordering
+ return string.format('0%.2o (%s)', rank, table.concat(names, ' '))
+end
+
+local ranked_rr_array_entry_t = ffi.typeof('ranked_rr_array_entry_t')
+ffi.metatype(ranked_rr_array_entry_t, {
+ __tostring = function(self)
+ return string.format('; ranked rrset to_wire %s, rank %s, cached %s, qry_uid %s, revalidations %s\n%s',
+ self.to_wire, rank_tostring(self.rank), self.cached, self.qry_uid,
+ self.revalidation_cnt, string.format('%s', self.rr))
+ end
+})
+
+-- Metatype for ranked record array (array of RRsets)
+local ranked_rr_array_t = ffi.typeof('ranked_rr_array_t')
+ffi.metatype(ranked_rr_array_t, {
+ __len = function(self)
+ return tonumber(self.len)
+ end,
+ __ipairs = function (self)
+ return c_array_iter, self, -1
+ end,
+ __index = {
+ get = function (self, i)
+ if i < 0 or i > self.len then return nil end
+ return self.at[i][0]
+ end,
+ },
+ __tostring = function(self)
+ local buf = {}
+ for _, rrset in ipairs(self) do
+ table.insert(buf, tostring(rrset))
+ end
+ return table.concat(buf, '')
+ end
+})
+
+-- Cache metatype
+local kr_cache_t = ffi.typeof('struct kr_cache')
+ffi.metatype( kr_cache_t, {
+ __index = {
+ insert = function (self, rr, rrsig, rank, timestamp)
+ assert(ffi.istype(kr_cache_t, self))
+ assert(ffi.istype(knot_rrset_t, rr), 'RR must be a rrset type')
+ assert(not rrsig or ffi.istype(knot_rrset_t, rrsig), 'RRSIG must be nil or of the rrset type')
+ -- Get current timestamp
+ if not timestamp then
+ local now = timeval_t()
+ C.gettimeofday(now, nil)
+ timestamp = tonumber(now.tv_sec)
+ end
+ -- Insert record into cache
+ local ret = C.kr_cache_insert_rr(self, rr, rrsig, tonumber(rank or 0),
+ timestamp, true)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ commit = function (self)
+ assert(ffi.istype(kr_cache_t, self))
+ local ret = C.kr_cache_commit(self)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ },
+})
+
+-- Pretty-print a single RR (which is a table with .owner .ttl .type .rdata)
+-- Extension: append .comment if exists.
+local function rr2str(rr, style)
+ -- Construct a single-RR temporary set while minimizing copying.
+ local ret
+ do
+ local rrs = knot_rrset_t(rr.owner, rr.type, kres.class.IN, rr.ttl)
+ rrs:add_rdata(rr.rdata, #rr.rdata)
+ ret = rrs:txt_dump(style)
+ end
+
+ -- Trim the newline and append comment (optionally).
+ if ret then
+ if ret:byte(-1) == string.byte('\n', -1) then
+ ret = ret:sub(1, -2)
+ end
+ if rr.comment then
+ ret = ret .. ' ;' .. rr.comment
+ end
+ end
+ return ret
+end
+
+-- Module API
+kres = {
+ -- Constants
+ class = const_class,
+ type = const_type,
+ section = const_section,
+ rcode = const_rcode,
+ opcode = const_opcode,
+ rank = const_rank,
+ extended_error = const_extended_error,
+
+ -- Constants to strings
+ tostring = {
+ class = const_class_str,
+ type = const_type_str,
+ section = const_section_str,
+ rcode = const_rcode_str,
+ opcode = const_opcode_str,
+ rank = const_rank_str,
+ extended_eror = const_extended_error_str,
+ },
+
+ -- Create a struct kr_qflags from a single flag name or a list of names.
+ mk_qflags = function (names)
+ local kr_qflags = ffi.typeof('struct kr_qflags')
+ if names == 0 or names == nil then -- compatibility: nil is common in lua
+ names = {}
+ elseif type(names) == 'string' then
+ names = {names}
+ elseif ffi.istype(kr_qflags, names) then
+ return names
+ end
+
+ local fs = ffi.new(kr_qflags)
+ for _, name in pairs(names) do
+ fs[name] = true
+ end
+ return fs
+ end,
+
+ CONSUME = 1, PRODUCE = 2, DONE = 4, FAIL = 8, YIELD = 16,
+
+ -- Export types
+ rrset = knot_rrset_t,
+ packet = knot_pkt_t,
+ lru = function (max_size, value_type)
+ value_type = value_type or ffi.typeof('uint64_t')
+ local ct = ffi.typeof(typed_lru_t, value_type)
+ return ffi.metatype(ct, lru_metatype)(max_size, ffi.alignof(value_type))
+ end,
+
+ -- Metatypes. Beware that any pointer will be cast silently...
+ pkt_t = function (udata) return ffi.cast('knot_pkt_t *', udata) end,
+ request_t = function (udata) return ffi.cast('struct kr_request *', udata) end,
+ sockaddr_t = function (udata) return ffi.cast('struct sockaddr *', udata) end,
+
+ -- Global API functions
+ -- Convert a lua string to a lower-case wire format (inside GC-ed ffi.string).
+ str2dname = function(name)
+ if type(name) ~= 'string' then return end
+ local dname = ffi.gc(C.knot_dname_from_str(nil, name, 0), C.free)
+ if dname == nil then return nil end
+ ffi.C.knot_dname_to_lower(dname);
+ return dname2wire(dname)
+ end,
+ dname2str = dname2str,
+ dname2wire = dname2wire,
+ parse_rdata = parse_rdata,
+
+ rr2str = rr2str,
+ str2ip = function (ip)
+ local family = C.kr_straddr_family(ip)
+ local ret = C.inet_pton(family, ip, addr_buf)
+ if ret ~= 1 then return nil end
+ return ffi.string(addr_buf, C.kr_family_len(family))
+ end,
+ context = function () return ffi.C.the_worker.engine.resolver end,
+
+ knot_pkt_rr = knot_pkt_rr,
+}
+
+return kres