1143 lines
32 KiB
Lua
1143 lines
32 KiB
Lua
-- 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
|