summaryrefslogtreecommitdiffstats
path: root/daemon/lua
diff options
context:
space:
mode:
Diffstat (limited to 'daemon/lua')
-rw-r--r--daemon/lua/controlsock.test.lua169
-rw-r--r--daemon/lua/distro-preconfig.lua.in19
-rw-r--r--daemon/lua/kluautil.lua94
-rw-r--r--daemon/lua/kres-gen-30.lua638
-rw-r--r--daemon/lua/kres-gen-31.lua647
-rw-r--r--daemon/lua/kres-gen-32.lua648
-rwxr-xr-xdaemon/lua/kres-gen.sh353
-rw-r--r--daemon/lua/kres.lua1143
-rw-r--r--daemon/lua/krprint.lua340
-rw-r--r--daemon/lua/krprint.test.lua292
-rw-r--r--daemon/lua/log.test.lua42
-rw-r--r--daemon/lua/map.test.integr/deckard.yaml38
-rw-r--r--daemon/lua/map.test.integr/kresd_config.j2193
-rw-r--r--daemon/lua/map.test.integr/query-while-map-is-running.rpl312
-rw-r--r--daemon/lua/meson.build118
-rw-r--r--daemon/lua/postconfig.lua70
-rw-r--r--daemon/lua/sandbox.lua.in833
-rw-r--r--daemon/lua/trust_anchors.lua.in532
-rw-r--r--daemon/lua/trust_anchors.rst123
-rw-r--r--daemon/lua/trust_anchors.test/bootstrap.test.lua112
-rw-r--r--daemon/lua/trust_anchors.test/err_attr_extra_attr.xml16
-rw-r--r--daemon/lua/trust_anchors.test/err_attr_validfrom_invalid.xml16
-rw-r--r--daemon/lua/trust_anchors.test/err_attr_validfrom_missing.xml16
-rw-r--r--daemon/lua/trust_anchors.test/err_elem_extra.xml17
-rw-r--r--daemon/lua/trust_anchors.test/err_elem_missing.xml16
-rw-r--r--daemon/lua/trust_anchors.test/err_multi_ta.xml19
-rw-r--r--daemon/lua/trust_anchors.test/ok0_badtimes.xml16
-rw-r--r--daemon/lua/trust_anchors.test/ok1.xml10
-rw-r--r--daemon/lua/trust_anchors.test/ok1_expired1.xml16
-rw-r--r--daemon/lua/trust_anchors.test/ok1_notyet1.xml16
-rw-r--r--daemon/lua/trust_anchors.test/ok2.xml16
-rwxr-xr-xdaemon/lua/trust_anchors.test/regen.sh3
-rw-r--r--daemon/lua/trust_anchors.test/root.keys1
-rw-r--r--daemon/lua/trust_anchors.test/ta.test.lua85
-rw-r--r--daemon/lua/trust_anchors.test/unsupp_nonroot.xml10
-rw-r--r--daemon/lua/trust_anchors.test/unsupp_xml_v11.xml10
-rw-r--r--daemon/lua/trust_anchors.test/webserv.lua236
-rw-r--r--daemon/lua/trust_anchors.test/x509/ca-key.pem182
-rw-r--r--daemon/lua/trust_anchors.test/x509/ca.pem24
-rw-r--r--daemon/lua/trust_anchors.test/x509/ca.tmpl4
-rwxr-xr-xdaemon/lua/trust_anchors.test/x509/gen.sh13
-rw-r--r--daemon/lua/trust_anchors.test/x509/server-key.pem182
-rw-r--r--daemon/lua/trust_anchors.test/x509/server.pem27
-rw-r--r--daemon/lua/trust_anchors.test/x509/server.tmpl7
-rw-r--r--daemon/lua/trust_anchors.test/x509/wrongca-key.pem182
-rw-r--r--daemon/lua/trust_anchors.test/x509/wrongca.pem24
-rw-r--r--daemon/lua/trust_anchors.test/x509/wrongca.tmpl4
-rw-r--r--daemon/lua/zonefile.lua93
48 files changed, 7977 insertions, 0 deletions
diff --git a/daemon/lua/controlsock.test.lua b/daemon/lua/controlsock.test.lua
new file mode 100644
index 0000000..0cce03d
--- /dev/null
+++ b/daemon/lua/controlsock.test.lua
@@ -0,0 +1,169 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+local cqsocket = require('cqueues.socket')
+local strerror = require('cqueues.errno').strerror
+local timeout = 5 -- seconds, per socket operation
+
+-- TODO: we get memory leaks from cqueues, but CI runs this without leak detection anyway
+
+local ctrl_sock_txt, ctrl_sock_bin, ctrl_sock_txt_longcmd, ctrl_sock_bin_longcmd
+local ctrl_sock_txt_partcmd, ctrl_sock_bin_partcmd
+
+local function onerr_fail(_, method, errno, stacklevel)
+ local errmsg = string.format('socket error: method %s error %d (%s)',
+ method, errno, strerror(errno))
+ fail(debug.traceback(errmsg, stacklevel))
+end
+
+
+local function switch_to_binary_mode(sock)
+ data = sock:xread(2, nil, timeout)
+ sock:xwrite('__binary\n', nil, timeout)
+ same(data, '> ', 'probably successful switch to binary mode')
+end
+
+local function socket_connect(path)
+ sock = cqsocket.connect({ path = path, nonblock = true })
+ sock:onerror(onerr_fail)
+ sock:setmode('bn', 'bn')
+
+ return sock
+end
+
+local function socket_fixture()
+ local path = worker.cwd..'/control/'..worker.pid
+ same(true, net.listen(path, nil, {kind = 'control'}), 'new control sockets were created')
+
+ ctrl_sock_txt = socket_connect(path)
+ ctrl_sock_txt_longcmd = socket_connect(path)
+ ctrl_sock_txt_partcmd = socket_connect(path)
+
+ ctrl_sock_bin = socket_connect(path)
+ switch_to_binary_mode(ctrl_sock_bin)
+ ctrl_sock_bin_longcmd = socket_connect(path)
+ switch_to_binary_mode(ctrl_sock_bin_longcmd)
+ ctrl_sock_bin_partcmd = socket_connect(path)
+ switch_to_binary_mode(ctrl_sock_bin_partcmd)
+end
+
+local function test_text_prompt()
+ data = ctrl_sock_txt:xread(2, nil, timeout)
+ same(data, '> ', 'text prompt looks like expected')
+end
+
+local function test_text_single_command()
+ local string = "this is test"
+ local input = string.format("'%s'\n", string)
+ local expect = input
+ ctrl_sock_txt:xwrite(input, nil, timeout)
+ data = ctrl_sock_txt:xread(#expect, nil, timeout)
+ same(data, expect,
+ 'text mode returns output in expected format')
+end
+
+local function binary_xread_len(sock)
+ data = sock:xread(4, nil, timeout)
+ local len = tonumber(data:byte(1))
+ for i=2,4 do
+ len = bit.bor(bit.lshift(len, 8), tonumber(data:byte(i)))
+ end
+
+ return len
+end
+
+local function test_binary_more_syscalls()
+ local len
+
+ ctrl_sock_bin:xwrite('worker.p', nil, timeout)
+ worker.sleep(0.01)
+ ctrl_sock_bin:xwrite('id\n', nil, timeout)
+ len = binary_xread_len(ctrl_sock_bin)
+ data = ctrl_sock_bin:xread(len, nil, timeout)
+ same(data, tostring(worker.pid),
+ 'binary mode returns number in expected format')
+
+ ctrl_sock_bin:xwrite('worker.p', nil, timeout)
+ worker.sleep(0.01)
+ ctrl_sock_bin:xwrite('id\nworker.id\n', nil, timeout)
+ len = binary_xread_len(ctrl_sock_bin)
+ data = ctrl_sock_bin:xread(len, nil, timeout)
+ same(data, tostring(worker.pid),
+ 'binary mode returns number in expected format')
+ len = binary_xread_len(ctrl_sock_bin)
+ data = ctrl_sock_bin:xread(len, nil, timeout)
+ same(data, string.format("'%s'", worker.id),
+ 'binary mode returns string in expected format')
+
+ ctrl_sock_bin:xwrite('worker.pid', nil, timeout)
+ worker.sleep(0.01)
+ ctrl_sock_bin:xwrite('\n', nil, timeout)
+ len = binary_xread_len(ctrl_sock_bin)
+ data = ctrl_sock_bin:xread(len, nil, timeout)
+ same(data, tostring(worker.pid),
+ 'binary mode returns output in expected format')
+
+ ctrl_sock_bin:xwrite('worker.pid', nil, timeout)
+ worker.sleep(0.01)
+ ctrl_sock_bin:xwrite('\nworker.id', nil, timeout)
+ worker.sleep(0.01)
+ ctrl_sock_bin:xwrite('\n', nil, timeout)
+ len = binary_xread_len(ctrl_sock_bin)
+ data = ctrl_sock_bin:xread(len, nil, timeout)
+ same(data, tostring(worker.pid),
+ 'binary mode returns number in expected format')
+ len = binary_xread_len(ctrl_sock_bin)
+ data = ctrl_sock_bin:xread(len, nil, timeout)
+ same(data, string.format("'%s'", worker.id),
+ 'binary mode returns string in expected format')
+
+ ctrl_sock_bin:xwrite('worker.pid\nworker.pid\nworker.pid\nworker.pid\n', nil, timeout)
+ len = binary_xread_len(ctrl_sock_bin)
+ data = ctrl_sock_bin:xread(len, nil, timeout)
+ same(data, tostring(worker.pid),
+ 'binary mode returns number in expected format')
+ len = binary_xread_len(ctrl_sock_bin)
+ data = ctrl_sock_bin:xread(len, nil, timeout)
+ same(data, tostring(worker.pid),
+ 'binary mode returns number in expected format')
+ len = binary_xread_len(ctrl_sock_bin)
+ data = ctrl_sock_bin:xread(len, nil, timeout)
+ same(data, tostring(worker.pid),
+ 'binary mode returns number in expected format')
+ len = binary_xread_len(ctrl_sock_bin)
+ data = ctrl_sock_bin:xread(len, nil, timeout)
+ same(data, tostring(worker.pid),
+ 'binary mode returns number in expected format')
+end
+
+local function test_close_incomplete_cmd()
+ ctrl_sock_txt_partcmd:xwrite('worker.p', nil, timeout)
+ ctrl_sock_txt_partcmd:close()
+ pass('close text socket with short incomplete command')
+
+ ctrl_sock_bin_partcmd:xwrite('worker.p', nil, timeout)
+ ctrl_sock_bin_partcmd:close()
+ pass('close binary socket with short incomplete command')
+end
+
+
+local function test_close_during_transfer()
+ ctrl_sock_txt_longcmd:xwrite(string.rep('a', 1024*1024*10), nil, timeout)
+ ctrl_sock_txt_longcmd:close()
+ pass('close text socket with long incomplete command')
+
+ ctrl_sock_bin_longcmd:xwrite(string.rep('a', 1024*1024*10), nil, timeout)
+ ctrl_sock_bin_longcmd:close()
+ pass('close binary socket with long incomplete command')
+end
+
+local tests = {
+ socket_fixture,
+ test_text_prompt, -- prompt after connect
+ test_text_single_command,
+ test_text_prompt, -- new prompt when command is finished
+ test_close_incomplete_cmd,
+ test_close_during_transfer,
+ test_binary_more_syscalls,
+ test_text_single_command, -- command in text mode after execute commands in binary mode
+ test_text_prompt, -- new prompt when command is finished
+}
+return tests
diff --git a/daemon/lua/distro-preconfig.lua.in b/daemon/lua/distro-preconfig.lua.in
new file mode 100644
index 0000000..df155c2
--- /dev/null
+++ b/daemon/lua/distro-preconfig.lua.in
@@ -0,0 +1,19 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+log_target('syslog') -- assume running as OS service
+
+local ffi = require('ffi')
+local id = os.getenv('SYSTEMD_INSTANCE')
+if not id then
+ log_warn(ffi.C.LOG_GRP_SYSTEM, 'environment variable $SYSTEMD_INSTANCE not set')
+else
+ -- Bind to control socket in run_dir
+ worker.control_path = '@run_dir@/control/'
+ local path = worker.control_path..id
+ local ok, err = pcall(net.listen, path, nil, { kind = 'control' })
+ if not ok then
+ log_warn(ffi.C.LOG_GRP_NETWORK, 'bind to '..path..' failed '..err)
+ end
+end
+
+-- Set cache location
+rawset(cache, 'current_storage', 'lmdb://@systemd_cache_dir@')
diff --git a/daemon/lua/kluautil.lua b/daemon/lua/kluautil.lua
new file mode 100644
index 0000000..d8569b9
--- /dev/null
+++ b/daemon/lua/kluautil.lua
@@ -0,0 +1,94 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+local ffi = require('ffi')
+local kluautil = {}
+
+-- Get length of table
+function kluautil.kr_table_len(t)
+ if type(t) ~= 'table' then
+ return nil
+ end
+
+ local len = 0
+ for _ in pairs(t) do
+ len = len + 1
+ end
+ return len
+end
+
+-- pack varargs including nil arguments into a table
+function kluautil.kr_table_pack(...)
+ local tab = {...}
+ tab.n = select('#', ...)
+ return tab
+end
+
+-- unpack table produced by kr_table_pack and including nil values
+function kluautil.kr_table_unpack(tab)
+ return unpack(tab, 1, tab.n)
+end
+
+-- Fetch over HTTPS
+function kluautil.kr_https_fetch(url, out_file, ca_file)
+ local http_ok, http_request = pcall(require, 'http.request')
+ local httptls_ok, http_tls = pcall(require, 'http.tls')
+ local openssl_ok, openssl_ctx = pcall(require, 'openssl.ssl.context')
+
+ if not http_ok or not httptls_ok or not openssl_ok then
+ return nil, 'error: lua-http and luaossl libraries are missing (but required)'
+ end
+ local cqerrno = require('cqueues.errno')
+
+ assert(string.match(url, '^https://'))
+
+ local req = http_request.new_from_uri(url)
+ req.tls = true
+ if ca_file then
+ req.ctx = openssl_ctx.new()
+ local store = req.ctx:getStore()
+ local load_ok, errmsg = pcall(store.add, store, ca_file)
+ if not load_ok then
+ return nil, errmsg
+ end
+ else -- use defaults
+ req.ctx = http_tls.new_client_context()
+ end
+
+ req.ctx:setVerify(openssl_ctx.VERIFY_PEER)
+
+ local headers, stream, errmsg = req:go()
+ if not headers then
+ errmsg = errmsg or 'unknown error'
+ if type(errmsg) == 'number' then
+ errmsg = cqerrno.strerror(errmsg) ..
+ ' (' .. tostring(errmsg) .. ')'
+ end
+ return nil, 'HTTP client library error: ' .. errmsg
+ end
+ if headers:get(':status') ~= "200" then
+ return nil, 'HTTP status != 200, got ' .. headers:get(':status')
+ end
+
+ local err
+ err, errmsg = stream:save_body_to_file(out_file)
+ if err == nil then
+ return nil, errmsg
+ end
+
+ out_file:seek('set', 0)
+
+ return true
+end
+
+-- Copy a lua string to C (to knot_mm_t or nil=malloc, zero-terminated).
+function kluautil.kr_string2c(str, mempool)
+ if str == nil then return nil end
+ local result = ffi.C.mm_realloc(mempool, nil, #str + 1, 0)
+ if result == nil then panic("not enough memory") end
+ ffi.copy(result, str)
+ return ffi.cast('const char *', result)
+end
+
+kluautil.list_dir = kluautil_list_dir
+
+return kluautil
diff --git a/daemon/lua/kres-gen-30.lua b/daemon/lua/kres-gen-30.lua
new file mode 100644
index 0000000..4353c5c
--- /dev/null
+++ b/daemon/lua/kres-gen-30.lua
@@ -0,0 +1,638 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+local ffi = require('ffi')
+--[[ This file is generated by ./kres-gen.sh ]] ffi.cdef[[
+typedef long time_t;
+typedef long __time_t;
+typedef long __suseconds_t;
+struct timeval {
+ __time_t tv_sec;
+ __suseconds_t tv_usec;
+};
+
+typedef struct knot_dump_style knot_dump_style_t;
+extern const knot_dump_style_t KR_DUMP_STYLE_DEFAULT;
+struct kr_cdb_api {};
+struct lru {};
+typedef enum {KNOT_ANSWER, KNOT_AUTHORITY, KNOT_ADDITIONAL} knot_section_t;
+typedef struct {
+ uint16_t pos;
+ uint16_t flags;
+ uint16_t compress_ptr[16];
+} knot_rrinfo_t;
+typedef unsigned char knot_dname_t;
+typedef struct {
+ uint16_t len;
+ uint8_t data[];
+} knot_rdata_t;
+typedef struct {
+ uint16_t count;
+ uint32_t size;
+ knot_rdata_t *rdata;
+} knot_rdataset_t;
+
+typedef struct knot_mm {
+ void *ctx, *alloc, *free;
+} knot_mm_t;
+
+typedef void *(*map_alloc_f)(void *, size_t);
+typedef void (*map_free_f)(void *baton, void *ptr);
+typedef void (*trace_log_f) (const struct kr_request *, const char *);
+typedef void (*trace_callback_f)(struct kr_request *);
+typedef uint8_t * (*alloc_wire_f)(struct kr_request *req, uint16_t *maxlen);
+typedef bool (*addr_info_f)(struct sockaddr*);
+typedef void (*zi_callback)(int state, void *param);
+typedef struct {
+ knot_dname_t *_owner;
+ uint32_t _ttl;
+ uint16_t type;
+ uint16_t rclass;
+ knot_rdataset_t rrs;
+ void *additional;
+} knot_rrset_t;
+
+struct kr_module;
+typedef char *(kr_prop_cb)(void *, struct kr_module *, const char *);
+typedef unsigned char knot_dname_storage_t[255];
+typedef struct knot_pkt knot_pkt_t;
+typedef struct {
+ uint8_t *ptr[15];
+} knot_edns_options_t;
+typedef struct {
+ knot_pkt_t *pkt;
+ uint16_t pos;
+ uint16_t count;
+} knot_pktsection_t;
+typedef struct knot_compr {
+ uint8_t *wire;
+ knot_rrinfo_t *rrinfo;
+ struct {
+ uint16_t pos;
+ uint8_t labels;
+ } suffix;
+} knot_compr_t;
+struct knot_pkt {
+ uint8_t *wire;
+ size_t size;
+ size_t max_size;
+ size_t parsed;
+ uint16_t reserved;
+ uint16_t qname_size;
+ uint16_t rrset_count;
+ uint16_t flags;
+ knot_rrset_t *opt_rr;
+ knot_rrset_t *tsig_rr;
+ knot_edns_options_t *edns_opts;
+ struct {
+ uint8_t *pos;
+ size_t len;
+ } tsig_wire;
+ knot_section_t current;
+ knot_pktsection_t sections[3];
+ size_t rrset_allocd;
+ knot_rrinfo_t *rr_info;
+ knot_rrset_t *rr;
+ knot_mm_t mm;
+ knot_compr_t compr;
+};
+typedef struct trie trie_t;
+struct kr_qflags {
+ _Bool NO_MINIMIZE : 1;
+ _Bool NO_IPV6 : 1;
+ _Bool NO_IPV4 : 1;
+ _Bool TCP : 1;
+ _Bool NO_ANSWER : 1;
+ _Bool RESOLVED : 1;
+ _Bool AWAIT_IPV4 : 1;
+ _Bool AWAIT_IPV6 : 1;
+ _Bool AWAIT_CUT : 1;
+ _Bool NO_EDNS : 1;
+ _Bool CACHED : 1;
+ _Bool NO_CACHE : 1;
+ _Bool EXPIRING : 1;
+ _Bool ALLOW_LOCAL : 1;
+ _Bool DNSSEC_WANT : 1;
+ _Bool DNSSEC_BOGUS : 1;
+ _Bool DNSSEC_INSECURE : 1;
+ _Bool DNSSEC_CD : 1;
+ _Bool STUB : 1;
+ _Bool ALWAYS_CUT : 1;
+ _Bool DNSSEC_WEXPAND : 1;
+ _Bool PERMISSIVE : 1;
+ _Bool STRICT : 1;
+ _Bool BADCOOKIE_AGAIN : 1;
+ _Bool CNAME : 1;
+ _Bool REORDER_RR : 1;
+ _Bool TRACE : 1;
+ _Bool NO_0X20 : 1;
+ _Bool DNSSEC_NODS : 1;
+ _Bool DNSSEC_OPTOUT : 1;
+ _Bool NONAUTH : 1;
+ _Bool FORWARD : 1;
+ _Bool DNS64_MARK : 1;
+ _Bool CACHE_TRIED : 1;
+ _Bool NO_NS_FOUND : 1;
+ _Bool PKT_IS_SANE : 1;
+ _Bool DNS64_DISABLE : 1;
+};
+typedef struct ranked_rr_array_entry {
+ uint32_t qry_uid;
+ uint8_t rank;
+ uint8_t revalidation_cnt;
+ _Bool cached : 1;
+ _Bool yielded : 1;
+ _Bool to_wire : 1;
+ _Bool expiring : 1;
+ _Bool in_progress : 1;
+ _Bool dont_cache : 1;
+ knot_rrset_t *rr;
+} ranked_rr_array_entry_t;
+typedef struct {
+ ranked_rr_array_entry_t **at;
+ size_t len;
+ size_t cap;
+} ranked_rr_array_t;
+typedef struct kr_http_header_array_entry {
+ char *name;
+ char *value;
+} kr_http_header_array_entry_t;
+typedef struct {
+ kr_http_header_array_entry_t *at;
+ size_t len;
+ size_t cap;
+} kr_http_header_array_t;
+typedef struct {
+ union kr_sockaddr *at;
+ size_t len;
+ size_t cap;
+} kr_sockaddr_array_t;
+struct kr_zonecut {
+ knot_dname_t *name;
+ knot_rrset_t *key;
+ knot_rrset_t *trust_anchor;
+ struct kr_zonecut *parent;
+ trie_t *nsset;
+ knot_mm_t *pool;
+};
+typedef struct {
+ struct kr_query **at;
+ size_t len;
+ size_t cap;
+} kr_qarray_t;
+struct kr_rplan {
+ kr_qarray_t pending;
+ kr_qarray_t resolved;
+ struct kr_query *initial;
+ struct kr_request *request;
+ knot_mm_t *pool;
+ uint32_t next_uid;
+};
+struct kr_request_qsource_flags {
+ _Bool tcp : 1;
+ _Bool tls : 1;
+ _Bool http : 1;
+ _Bool xdp : 1;
+};
+struct kr_extended_error {
+ int32_t info_code;
+ const char *extra_text;
+};
+struct kr_request {
+ struct kr_context *ctx;
+ knot_pkt_t *answer;
+ struct kr_query *current_query;
+ struct {
+ const struct sockaddr *addr;
+ const struct sockaddr *comm_addr;
+ const struct sockaddr *dst_addr;
+ const knot_pkt_t *packet;
+ struct kr_request_qsource_flags flags;
+ struct kr_request_qsource_flags comm_flags;
+ size_t size;
+ int32_t stream_id;
+ kr_http_header_array_t headers;
+ } qsource;
+ struct {
+ unsigned int rtt;
+ const struct kr_transport *transport;
+ } upstream;
+ struct kr_qflags options;
+ int state;
+ ranked_rr_array_t answ_selected;
+ ranked_rr_array_t auth_selected;
+ ranked_rr_array_t add_selected;
+ _Bool answ_validated;
+ _Bool auth_validated;
+ uint8_t rank;
+ struct kr_rplan rplan;
+ trace_log_f trace_log;
+ trace_callback_f trace_finish;
+ int vars_ref;
+ knot_mm_t pool;
+ unsigned int uid;
+ struct {
+ addr_info_f is_tls_capable;
+ addr_info_f is_tcp_connected;
+ addr_info_f is_tcp_waiting;
+ kr_sockaddr_array_t forwarding_targets;
+ } selection_context;
+ unsigned int count_no_nsaddr;
+ unsigned int count_fail_row;
+ alloc_wire_f alloc_wire_cb;
+ struct kr_extended_error extended_error;
+};
+enum kr_rank {KR_RANK_INITIAL, KR_RANK_OMIT, KR_RANK_TRY, KR_RANK_INDET = 4, KR_RANK_BOGUS, KR_RANK_MISMATCH, KR_RANK_MISSING, KR_RANK_INSECURE, KR_RANK_AUTH = 16, KR_RANK_SECURE = 32};
+typedef struct kr_cdb * kr_cdb_pt;
+struct kr_cdb_stats {
+ uint64_t open;
+ uint64_t close;
+ uint64_t count;
+ uint64_t count_entries;
+ uint64_t clear;
+ uint64_t commit;
+ uint64_t read;
+ uint64_t read_miss;
+ uint64_t write;
+ uint64_t remove;
+ uint64_t remove_miss;
+ uint64_t match;
+ uint64_t match_miss;
+ uint64_t read_leq;
+ uint64_t read_leq_miss;
+ double usage_percent;
+};
+typedef struct uv_timer_s uv_timer_t;
+struct kr_cache {
+ kr_cdb_pt db;
+ const struct kr_cdb_api *api;
+ struct kr_cdb_stats stats;
+ uint32_t ttl_min;
+ uint32_t ttl_max;
+ struct timeval checkpoint_walltime;
+ uint64_t checkpoint_monotime;
+ uv_timer_t *health_timer;
+};
+typedef struct kr_layer {
+ int state;
+ struct kr_request *req;
+ const struct kr_layer_api *api;
+ knot_pkt_t *pkt;
+ struct sockaddr *dst;
+ _Bool is_stream;
+} kr_layer_t;
+typedef struct kr_layer_api {
+ int (*begin)(kr_layer_t *);
+ int (*reset)(kr_layer_t *);
+ int (*finish)(kr_layer_t *);
+ int (*consume)(kr_layer_t *, knot_pkt_t *);
+ int (*produce)(kr_layer_t *, knot_pkt_t *);
+ int (*checkout)(kr_layer_t *, knot_pkt_t *, struct sockaddr *, int);
+ int (*answer_finalize)(kr_layer_t *);
+ void *data;
+ int cb_slots[];
+} kr_layer_api_t;
+struct kr_prop {
+ kr_prop_cb *cb;
+ const char *name;
+ const char *info;
+};
+struct kr_module {
+ char *name;
+ int (*init)(struct kr_module *);
+ int (*deinit)(struct kr_module *);
+ int (*config)(struct kr_module *, const char *);
+ const kr_layer_api_t *layer;
+ const struct kr_prop *props;
+ void *lib;
+ void *data;
+};
+struct kr_server_selection {
+ _Bool initialized;
+ void (*choose_transport)(struct kr_query *, struct kr_transport **);
+ void (*update_rtt)(struct kr_query *, const struct kr_transport *, unsigned int);
+ void (*error)(struct kr_query *, const struct kr_transport *, enum kr_selection_error);
+ struct local_state *local_state;
+};
+typedef int kr_log_level_t;
+enum kr_log_group {LOG_GRP_UNKNOWN = -1, LOG_GRP_SYSTEM = 1, LOG_GRP_CACHE, LOG_GRP_IO, LOG_GRP_NETWORK, LOG_GRP_TA, LOG_GRP_TLS, LOG_GRP_GNUTLS, LOG_GRP_TLSCLIENT, LOG_GRP_XDP, LOG_GRP_DOH, LOG_GRP_DNSSEC, LOG_GRP_HINT, LOG_GRP_PLAN, LOG_GRP_ITERATOR, LOG_GRP_VALIDATOR, LOG_GRP_RESOLVER, LOG_GRP_SELECTION, LOG_GRP_ZCUT, LOG_GRP_COOKIES, LOG_GRP_STATISTICS, LOG_GRP_REBIND, LOG_GRP_WORKER, LOG_GRP_POLICY, LOG_GRP_TASENTINEL, LOG_GRP_TASIGNALING, LOG_GRP_TAUPDATE, LOG_GRP_DAF, LOG_GRP_DETECTTIMEJUMP, LOG_GRP_DETECTTIMESKEW, LOG_GRP_GRAPHITE, LOG_GRP_PREFILL, LOG_GRP_PRIMING, LOG_GRP_SRVSTALE, LOG_GRP_WATCHDOG, LOG_GRP_NSID, LOG_GRP_DNSTAP, LOG_GRP_TESTS, LOG_GRP_DOTAUTH, LOG_GRP_HTTP, LOG_GRP_CONTROL, LOG_GRP_MODULE, LOG_GRP_DEVEL, LOG_GRP_RENUMBER, LOG_GRP_EDE, LOG_GRP_REQDBG};
+
+kr_layer_t kr_layer_t_static;
+_Bool kr_dbg_assertion_abort;
+int kr_dbg_assertion_fork;
+
+typedef int32_t (*kr_stale_cb)(int32_t ttl, const knot_dname_t *owner, uint16_t type,
+ const struct kr_query *qry);
+
+void kr_rrset_init(knot_rrset_t *rrset, knot_dname_t *owner,
+ uint16_t type, uint16_t rclass, uint32_t ttl);
+struct kr_query {
+ struct kr_query *parent;
+ knot_dname_t *sname;
+ uint16_t stype;
+ uint16_t sclass;
+ uint16_t id;
+ uint16_t reorder;
+ struct kr_qflags flags;
+ struct kr_qflags forward_flags;
+ uint32_t secret;
+ uint32_t uid;
+ uint64_t creation_time_mono;
+ uint64_t timestamp_mono;
+ struct timeval timestamp;
+ struct kr_zonecut zone_cut;
+ struct kr_layer_pickle *deferred;
+ int8_t cname_depth;
+ struct kr_query *cname_parent;
+ struct kr_request *request;
+ kr_stale_cb stale_cb;
+ struct kr_server_selection server_selection;
+};
+struct kr_context {
+ struct kr_qflags options;
+ knot_rrset_t *downstream_opt_rr;
+ knot_rrset_t *upstream_opt_rr;
+ trie_t *trust_anchors;
+ trie_t *negative_anchors;
+ struct kr_zonecut root_hints;
+ struct kr_cache cache;
+ unsigned int cache_rtt_tout_retry_interval;
+ char _stub[];
+};
+struct kr_transport {
+ knot_dname_t *ns_name;
+ /* beware: hidden stub, to avoid hardcoding sockaddr lengths */
+};
+const char *knot_strerror(int);
+knot_dname_t *knot_dname_copy(const knot_dname_t *, knot_mm_t *);
+knot_dname_t *knot_dname_from_str(uint8_t *, const char *, size_t);
+int knot_dname_in_bailiwick(const knot_dname_t *, const knot_dname_t *);
+_Bool knot_dname_is_equal(const knot_dname_t *, const knot_dname_t *);
+size_t knot_dname_labels(const uint8_t *, const uint8_t *);
+size_t knot_dname_size(const knot_dname_t *);
+void knot_dname_to_lower(knot_dname_t *);
+char *knot_dname_to_str(char *, const knot_dname_t *, size_t);
+knot_rdata_t *knot_rdataset_at(const knot_rdataset_t *, uint16_t);
+int knot_rdataset_merge(knot_rdataset_t *, const knot_rdataset_t *, knot_mm_t *);
+int knot_rrset_add_rdata(knot_rrset_t *, const uint8_t *, uint16_t, knot_mm_t *);
+void knot_rrset_free(knot_rrset_t *, knot_mm_t *);
+int knot_rrset_txt_dump(const knot_rrset_t *, char **, size_t *, const knot_dump_style_t *);
+int knot_rrset_txt_dump_data(const knot_rrset_t *, const size_t, char *, const size_t, const knot_dump_style_t *);
+size_t knot_rrset_size(const knot_rrset_t *);
+int knot_pkt_begin(knot_pkt_t *, knot_section_t);
+int knot_pkt_put_question(knot_pkt_t *, const knot_dname_t *, uint16_t, uint16_t);
+int knot_pkt_put_rotate(knot_pkt_t *, uint16_t, const knot_rrset_t *, uint16_t, uint16_t);
+knot_pkt_t *knot_pkt_new(void *, uint16_t, knot_mm_t *);
+void knot_pkt_free(knot_pkt_t *);
+int knot_pkt_parse(knot_pkt_t *, unsigned int);
+knot_rrset_t *kr_request_ensure_edns(struct kr_request *);
+knot_pkt_t *kr_request_ensure_answer(struct kr_request *);
+int kr_request_set_extended_error(struct kr_request *, int, const char *);
+struct kr_rplan *kr_resolve_plan(struct kr_request *);
+knot_mm_t *kr_resolve_pool(struct kr_request *);
+struct kr_query *kr_rplan_push(struct kr_rplan *, struct kr_query *, const knot_dname_t *, uint16_t, uint16_t);
+int kr_rplan_pop(struct kr_rplan *, struct kr_query *);
+struct kr_query *kr_rplan_resolved(struct kr_rplan *);
+struct kr_query *kr_rplan_last(struct kr_rplan *);
+int kr_forward_add_target(struct kr_request *, const struct sockaddr *);
+_Bool kr_log_is_debug_fun(enum kr_log_group, const struct kr_request *);
+void kr_log_req1(const struct kr_request * const, uint32_t, const unsigned int, enum kr_log_group, const char *, const char *, ...);
+void kr_log_q1(const struct kr_query * const, enum kr_log_group, const char *, const char *, ...);
+const char *kr_log_grp2name(enum kr_log_group);
+void kr_log_fmt(enum kr_log_group, kr_log_level_t, const char *, const char *, const char *, const char *, ...);
+int kr_make_query(struct kr_query *, knot_pkt_t *);
+void kr_pkt_make_auth_header(knot_pkt_t *);
+int kr_pkt_put(knot_pkt_t *, const knot_dname_t *, uint32_t, uint16_t, uint16_t, const uint8_t *, uint16_t);
+int kr_pkt_recycle(knot_pkt_t *);
+int kr_pkt_clear_payload(knot_pkt_t *);
+_Bool kr_pkt_has_wire(const knot_pkt_t *);
+_Bool kr_pkt_has_dnssec(const knot_pkt_t *);
+uint16_t kr_pkt_qclass(const knot_pkt_t *);
+uint16_t kr_pkt_qtype(const knot_pkt_t *);
+char *kr_pkt_text(const knot_pkt_t *);
+void kr_rnd_buffered(void *, unsigned int);
+uint32_t kr_rrsig_sig_inception(const knot_rdata_t *);
+uint32_t kr_rrsig_sig_expiration(const knot_rdata_t *);
+uint16_t kr_rrsig_type_covered(const knot_rdata_t *);
+const char *kr_inaddr(const struct sockaddr *);
+int kr_inaddr_family(const struct sockaddr *);
+int kr_inaddr_len(const struct sockaddr *);
+int kr_inaddr_str(const struct sockaddr *, char *, size_t *);
+int kr_sockaddr_cmp(const struct sockaddr *, const struct sockaddr *);
+int kr_sockaddr_len(const struct sockaddr *);
+uint16_t kr_inaddr_port(const struct sockaddr *);
+int kr_straddr_family(const char *);
+int kr_straddr_subnet(void *, const char *);
+int kr_bitcmp(const char *, const char *, int);
+int kr_family_len(int);
+struct sockaddr *kr_straddr_socket(const char *, int, knot_mm_t *);
+int kr_straddr_split(const char *, char * restrict, uint16_t *);
+_Bool kr_rank_test(uint8_t, uint8_t);
+int kr_ranked_rrarray_add(ranked_rr_array_t *, const knot_rrset_t *, uint8_t, _Bool, uint32_t, knot_mm_t *);
+int kr_ranked_rrarray_finalize(ranked_rr_array_t *, uint32_t, knot_mm_t *);
+void kr_qflags_set(struct kr_qflags *, struct kr_qflags);
+void kr_qflags_clear(struct kr_qflags *, struct kr_qflags);
+int kr_zonecut_add(struct kr_zonecut *, const knot_dname_t *, const void *, int);
+_Bool kr_zonecut_is_empty(struct kr_zonecut *);
+void kr_zonecut_set(struct kr_zonecut *, const knot_dname_t *);
+uint64_t kr_now();
+const char *kr_strptime_diff(const char *, const char *, const char *, double *);
+time_t kr_file_mtime(const char *);
+long long kr_fssize(const char *);
+const char *kr_dirent_name(const struct dirent *);
+void lru_free_items_impl(struct lru *);
+struct lru *lru_create_impl(unsigned int, unsigned int, knot_mm_t *, knot_mm_t *);
+void *lru_get_impl(struct lru *, const char *, unsigned int, unsigned int, _Bool, _Bool *);
+void *mm_realloc(knot_mm_t *, void *, size_t, size_t);
+knot_rrset_t *kr_ta_get(trie_t *, const knot_dname_t *);
+int kr_ta_add(trie_t *, const knot_dname_t *, uint16_t, uint32_t, const uint8_t *, uint16_t);
+int kr_ta_del(trie_t *, const knot_dname_t *);
+void kr_ta_clear(trie_t *);
+_Bool kr_dnssec_key_ksk(const uint8_t *);
+_Bool kr_dnssec_key_revoked(const uint8_t *);
+int kr_dnssec_key_tag(uint16_t, const uint8_t *, size_t);
+int kr_dnssec_key_match(const uint8_t *, size_t, const uint8_t *, size_t);
+int kr_cache_closest_apex(struct kr_cache *, const knot_dname_t *, _Bool, knot_dname_t **);
+int kr_cache_insert_rr(struct kr_cache *, const knot_rrset_t *, const knot_rrset_t *, uint8_t, uint32_t, _Bool);
+int kr_cache_remove(struct kr_cache *, const knot_dname_t *, uint16_t);
+int kr_cache_remove_subtree(struct kr_cache *, const knot_dname_t *, _Bool, int);
+int kr_cache_commit(struct kr_cache *);
+uint32_t packet_ttl(const knot_pkt_t *);
+typedef struct {
+ int sock_type;
+ _Bool tls;
+ _Bool http;
+ _Bool xdp;
+ _Bool freebind;
+ const char *kind;
+} endpoint_flags_t;
+typedef struct {
+ char **at;
+ size_t len;
+ size_t cap;
+} addr_array_t;
+typedef struct {
+ int fd;
+ endpoint_flags_t flags;
+} flagged_fd_t;
+typedef struct {
+ flagged_fd_t *at;
+ size_t len;
+ size_t cap;
+} flagged_fd_array_t;
+typedef struct {
+ const char **at;
+ size_t len;
+ size_t cap;
+} config_array_t;
+struct args {
+ addr_array_t addrs;
+ addr_array_t addrs_tls;
+ flagged_fd_array_t fds;
+ int control_fd;
+ int forks;
+ config_array_t config;
+ const char *rundir;
+ _Bool interactive;
+ _Bool quiet;
+ _Bool tty_binary_output;
+};
+typedef struct {
+ const char *zone_file;
+ const char *origin;
+ uint32_t ttl;
+ enum {ZI_STAMP_NOW, ZI_STAMP_MTIM} time_src;
+ _Bool downgrade;
+ _Bool zonemd;
+ const knot_rrset_t *ds;
+ zi_callback cb;
+ void *cb_param;
+} zi_config_t;
+struct args *the_args;
+struct endpoint {
+ void *handle;
+ int fd;
+ int family;
+ uint16_t port;
+ int16_t nic_queue;
+ _Bool engaged;
+ endpoint_flags_t flags;
+};
+struct request_ctx {
+ struct kr_request req;
+ struct worker_ctx *worker;
+ struct qr_task *task;
+ /* beware: hidden stub, to avoid hardcoding sockaddr lengths */
+};
+struct qr_task {
+ struct request_ctx *ctx;
+ /* beware: hidden stub, to avoid qr_tasklist_t */
+};
+int worker_resolve_exec(struct qr_task *, knot_pkt_t *);
+knot_pkt_t *worker_resolve_mk_pkt(const char *, uint16_t, uint16_t, const struct kr_qflags *);
+struct qr_task *worker_resolve_start(knot_pkt_t *, struct kr_qflags);
+int zi_zone_import(const zi_config_t);
+struct engine {
+ struct kr_context resolver;
+ char _stub[];
+};
+struct worker_ctx {
+ struct engine *engine;
+ char _stub[];
+};
+struct worker_ctx *the_worker;
+typedef struct {
+ uint8_t bitmap[32];
+ uint8_t length;
+} zs_win_t;
+typedef struct {
+ uint8_t excl_flag;
+ uint16_t addr_family;
+ uint8_t prefix_length;
+} zs_apl_t;
+typedef struct {
+ uint32_t d1;
+ uint32_t d2;
+ uint32_t m1;
+ uint32_t m2;
+ uint32_t s1;
+ uint32_t s2;
+ uint32_t alt;
+ uint64_t siz;
+ uint64_t hp;
+ uint64_t vp;
+ int8_t lat_sign;
+ int8_t long_sign;
+ int8_t alt_sign;
+} zs_loc_t;
+typedef enum {ZS_STATE_NONE, ZS_STATE_DATA, ZS_STATE_ERROR, ZS_STATE_INCLUDE, ZS_STATE_EOF, ZS_STATE_STOP} zs_state_t;
+typedef struct zs_scanner zs_scanner_t;
+typedef struct zs_scanner {
+ int cs;
+ int top;
+ int stack[16];
+ _Bool multiline;
+ uint64_t number64;
+ uint64_t number64_tmp;
+ uint32_t decimals;
+ uint32_t decimal_counter;
+ uint32_t item_length;
+ uint32_t item_length_position;
+ uint8_t *item_length_location;
+ uint32_t buffer_length;
+ uint8_t buffer[65535];
+ char include_filename[65535];
+ char *path;
+ zs_win_t windows[256];
+ int16_t last_window;
+ zs_apl_t apl;
+ zs_loc_t loc;
+ uint8_t addr[16];
+ _Bool long_string;
+ uint8_t *dname;
+ uint32_t *dname_length;
+ uint32_t dname_tmp_length;
+ uint32_t r_data_tail;
+ uint32_t zone_origin_length;
+ uint8_t zone_origin[318];
+ uint16_t default_class;
+ uint32_t default_ttl;
+ zs_state_t state;
+ struct {
+ _Bool automatic;
+ void (*record)(zs_scanner_t *);
+ void (*error)(zs_scanner_t *);
+ void (*comment)(zs_scanner_t *);
+ void *data;
+ } process;
+ struct {
+ const char *start;
+ const char *current;
+ const char *end;
+ _Bool eof;
+ _Bool mmaped;
+ } input;
+ struct {
+ char *name;
+ int descriptor;
+ } file;
+ struct {
+ int code;
+ uint64_t counter;
+ _Bool fatal;
+ } error;
+ uint64_t line_counter;
+ uint32_t r_owner_length;
+ uint8_t r_owner[318];
+ uint16_t r_class;
+ uint32_t r_ttl;
+ uint16_t r_type;
+ uint32_t r_data_length;
+ uint8_t r_data[65535];
+} zs_scanner_t;
+void zs_deinit(zs_scanner_t *);
+int zs_init(zs_scanner_t *, const char *, const uint16_t, const uint32_t);
+int zs_parse_record(zs_scanner_t *);
+int zs_set_input_file(zs_scanner_t *, const char *);
+int zs_set_input_string(zs_scanner_t *, const char *, size_t);
+const char *zs_strerror(const int);
+]]
diff --git a/daemon/lua/kres-gen-31.lua b/daemon/lua/kres-gen-31.lua
new file mode 100644
index 0000000..a68dd65
--- /dev/null
+++ b/daemon/lua/kres-gen-31.lua
@@ -0,0 +1,647 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+local ffi = require('ffi')
+--[[ This file is generated by ./kres-gen.sh ]] ffi.cdef[[
+typedef long time_t;
+typedef long __time_t;
+typedef long __suseconds_t;
+struct timeval {
+ __time_t tv_sec;
+ __suseconds_t tv_usec;
+};
+
+typedef struct knot_dump_style knot_dump_style_t;
+extern const knot_dump_style_t KR_DUMP_STYLE_DEFAULT;
+struct kr_cdb_api {};
+struct lru {};
+typedef enum {KNOT_ANSWER, KNOT_AUTHORITY, KNOT_ADDITIONAL} knot_section_t;
+typedef struct {
+ uint16_t pos;
+ uint16_t flags;
+ uint16_t compress_ptr[16];
+} knot_rrinfo_t;
+typedef unsigned char knot_dname_t;
+typedef struct {
+ uint16_t len;
+ uint8_t data[];
+} knot_rdata_t;
+typedef struct {
+ uint16_t count;
+ uint32_t size;
+ knot_rdata_t *rdata;
+} knot_rdataset_t;
+
+typedef struct knot_mm {
+ void *ctx, *alloc, *free;
+} knot_mm_t;
+
+typedef void *(*map_alloc_f)(void *, size_t);
+typedef void (*map_free_f)(void *baton, void *ptr);
+typedef void (*trace_log_f) (const struct kr_request *, const char *);
+typedef void (*trace_callback_f)(struct kr_request *);
+typedef uint8_t * (*alloc_wire_f)(struct kr_request *req, uint16_t *maxlen);
+typedef bool (*addr_info_f)(struct sockaddr*);
+typedef void (*zi_callback)(int state, void *param);
+typedef struct {
+ knot_dname_t *_owner;
+ uint32_t _ttl;
+ uint16_t type;
+ uint16_t rclass;
+ knot_rdataset_t rrs;
+ void *additional;
+} knot_rrset_t;
+
+struct kr_module;
+typedef char *(kr_prop_cb)(void *, struct kr_module *, const char *);
+typedef unsigned char knot_dname_storage_t[255];
+typedef struct knot_pkt knot_pkt_t;
+typedef struct {
+ uint8_t *ptr[18];
+} knot_edns_options_t;
+typedef struct {
+ knot_pkt_t *pkt;
+ uint16_t pos;
+ uint16_t count;
+} knot_pktsection_t;
+typedef struct knot_compr {
+ uint8_t *wire;
+ knot_rrinfo_t *rrinfo;
+ struct {
+ uint16_t pos;
+ uint8_t labels;
+ } suffix;
+} knot_compr_t;
+struct knot_pkt {
+ uint8_t *wire;
+ size_t size;
+ size_t max_size;
+ size_t parsed;
+ uint16_t reserved;
+ uint16_t qname_size;
+ uint16_t rrset_count;
+ uint16_t flags;
+ knot_rrset_t *opt_rr;
+ knot_rrset_t *tsig_rr;
+ knot_edns_options_t *edns_opts;
+ struct {
+ uint8_t *pos;
+ size_t len;
+ } tsig_wire;
+ knot_section_t current;
+ knot_pktsection_t sections[3];
+ size_t rrset_allocd;
+ knot_rrinfo_t *rr_info;
+ knot_rrset_t *rr;
+ knot_mm_t mm;
+ knot_compr_t compr;
+};
+typedef struct trie trie_t;
+struct kr_qflags {
+ _Bool NO_MINIMIZE : 1;
+ _Bool NO_IPV6 : 1;
+ _Bool NO_IPV4 : 1;
+ _Bool TCP : 1;
+ _Bool NO_ANSWER : 1;
+ _Bool RESOLVED : 1;
+ _Bool AWAIT_IPV4 : 1;
+ _Bool AWAIT_IPV6 : 1;
+ _Bool AWAIT_CUT : 1;
+ _Bool NO_EDNS : 1;
+ _Bool CACHED : 1;
+ _Bool NO_CACHE : 1;
+ _Bool EXPIRING : 1;
+ _Bool ALLOW_LOCAL : 1;
+ _Bool DNSSEC_WANT : 1;
+ _Bool DNSSEC_BOGUS : 1;
+ _Bool DNSSEC_INSECURE : 1;
+ _Bool DNSSEC_CD : 1;
+ _Bool STUB : 1;
+ _Bool ALWAYS_CUT : 1;
+ _Bool DNSSEC_WEXPAND : 1;
+ _Bool PERMISSIVE : 1;
+ _Bool STRICT : 1;
+ _Bool BADCOOKIE_AGAIN : 1;
+ _Bool CNAME : 1;
+ _Bool REORDER_RR : 1;
+ _Bool TRACE : 1;
+ _Bool NO_0X20 : 1;
+ _Bool DNSSEC_NODS : 1;
+ _Bool DNSSEC_OPTOUT : 1;
+ _Bool NONAUTH : 1;
+ _Bool FORWARD : 1;
+ _Bool DNS64_MARK : 1;
+ _Bool CACHE_TRIED : 1;
+ _Bool NO_NS_FOUND : 1;
+ _Bool PKT_IS_SANE : 1;
+ _Bool DNS64_DISABLE : 1;
+};
+typedef struct ranked_rr_array_entry {
+ uint32_t qry_uid;
+ uint8_t rank;
+ uint8_t revalidation_cnt;
+ _Bool cached : 1;
+ _Bool yielded : 1;
+ _Bool to_wire : 1;
+ _Bool expiring : 1;
+ _Bool in_progress : 1;
+ _Bool dont_cache : 1;
+ knot_rrset_t *rr;
+} ranked_rr_array_entry_t;
+typedef struct {
+ ranked_rr_array_entry_t **at;
+ size_t len;
+ size_t cap;
+} ranked_rr_array_t;
+typedef struct kr_http_header_array_entry {
+ char *name;
+ char *value;
+} kr_http_header_array_entry_t;
+typedef struct {
+ kr_http_header_array_entry_t *at;
+ size_t len;
+ size_t cap;
+} kr_http_header_array_t;
+typedef struct {
+ union kr_sockaddr *at;
+ size_t len;
+ size_t cap;
+} kr_sockaddr_array_t;
+struct kr_zonecut {
+ knot_dname_t *name;
+ knot_rrset_t *key;
+ knot_rrset_t *trust_anchor;
+ struct kr_zonecut *parent;
+ trie_t *nsset;
+ knot_mm_t *pool;
+};
+typedef struct {
+ struct kr_query **at;
+ size_t len;
+ size_t cap;
+} kr_qarray_t;
+struct kr_rplan {
+ kr_qarray_t pending;
+ kr_qarray_t resolved;
+ struct kr_query *initial;
+ struct kr_request *request;
+ knot_mm_t *pool;
+ uint32_t next_uid;
+};
+struct kr_request_qsource_flags {
+ _Bool tcp : 1;
+ _Bool tls : 1;
+ _Bool http : 1;
+ _Bool xdp : 1;
+};
+struct kr_extended_error {
+ int32_t info_code;
+ const char *extra_text;
+};
+struct kr_request {
+ struct kr_context *ctx;
+ knot_pkt_t *answer;
+ struct kr_query *current_query;
+ struct {
+ const struct sockaddr *addr;
+ const struct sockaddr *comm_addr;
+ const struct sockaddr *dst_addr;
+ const knot_pkt_t *packet;
+ struct kr_request_qsource_flags flags;
+ struct kr_request_qsource_flags comm_flags;
+ size_t size;
+ int32_t stream_id;
+ kr_http_header_array_t headers;
+ } qsource;
+ struct {
+ unsigned int rtt;
+ const struct kr_transport *transport;
+ } upstream;
+ struct kr_qflags options;
+ int state;
+ ranked_rr_array_t answ_selected;
+ ranked_rr_array_t auth_selected;
+ ranked_rr_array_t add_selected;
+ _Bool answ_validated;
+ _Bool auth_validated;
+ uint8_t rank;
+ struct kr_rplan rplan;
+ trace_log_f trace_log;
+ trace_callback_f trace_finish;
+ int vars_ref;
+ knot_mm_t pool;
+ unsigned int uid;
+ struct {
+ addr_info_f is_tls_capable;
+ addr_info_f is_tcp_connected;
+ addr_info_f is_tcp_waiting;
+ kr_sockaddr_array_t forwarding_targets;
+ } selection_context;
+ unsigned int count_no_nsaddr;
+ unsigned int count_fail_row;
+ alloc_wire_f alloc_wire_cb;
+ struct kr_extended_error extended_error;
+};
+enum kr_rank {KR_RANK_INITIAL, KR_RANK_OMIT, KR_RANK_TRY, KR_RANK_INDET = 4, KR_RANK_BOGUS, KR_RANK_MISMATCH, KR_RANK_MISSING, KR_RANK_INSECURE, KR_RANK_AUTH = 16, KR_RANK_SECURE = 32};
+typedef struct kr_cdb * kr_cdb_pt;
+struct kr_cdb_stats {
+ uint64_t open;
+ uint64_t close;
+ uint64_t count;
+ uint64_t count_entries;
+ uint64_t clear;
+ uint64_t commit;
+ uint64_t read;
+ uint64_t read_miss;
+ uint64_t write;
+ uint64_t remove;
+ uint64_t remove_miss;
+ uint64_t match;
+ uint64_t match_miss;
+ uint64_t read_leq;
+ uint64_t read_leq_miss;
+ double usage_percent;
+};
+typedef struct uv_timer_s uv_timer_t;
+struct kr_cache {
+ kr_cdb_pt db;
+ const struct kr_cdb_api *api;
+ struct kr_cdb_stats stats;
+ uint32_t ttl_min;
+ uint32_t ttl_max;
+ struct timeval checkpoint_walltime;
+ uint64_t checkpoint_monotime;
+ uv_timer_t *health_timer;
+};
+typedef struct kr_layer {
+ int state;
+ struct kr_request *req;
+ const struct kr_layer_api *api;
+ knot_pkt_t *pkt;
+ struct sockaddr *dst;
+ _Bool is_stream;
+} kr_layer_t;
+typedef struct kr_layer_api {
+ int (*begin)(kr_layer_t *);
+ int (*reset)(kr_layer_t *);
+ int (*finish)(kr_layer_t *);
+ int (*consume)(kr_layer_t *, knot_pkt_t *);
+ int (*produce)(kr_layer_t *, knot_pkt_t *);
+ int (*checkout)(kr_layer_t *, knot_pkt_t *, struct sockaddr *, int);
+ int (*answer_finalize)(kr_layer_t *);
+ void *data;
+ int cb_slots[];
+} kr_layer_api_t;
+struct kr_prop {
+ kr_prop_cb *cb;
+ const char *name;
+ const char *info;
+};
+struct kr_module {
+ char *name;
+ int (*init)(struct kr_module *);
+ int (*deinit)(struct kr_module *);
+ int (*config)(struct kr_module *, const char *);
+ const kr_layer_api_t *layer;
+ const struct kr_prop *props;
+ void *lib;
+ void *data;
+};
+struct kr_server_selection {
+ _Bool initialized;
+ void (*choose_transport)(struct kr_query *, struct kr_transport **);
+ void (*update_rtt)(struct kr_query *, const struct kr_transport *, unsigned int);
+ void (*error)(struct kr_query *, const struct kr_transport *, enum kr_selection_error);
+ struct local_state *local_state;
+};
+typedef int kr_log_level_t;
+enum kr_log_group {LOG_GRP_UNKNOWN = -1, LOG_GRP_SYSTEM = 1, LOG_GRP_CACHE, LOG_GRP_IO, LOG_GRP_NETWORK, LOG_GRP_TA, LOG_GRP_TLS, LOG_GRP_GNUTLS, LOG_GRP_TLSCLIENT, LOG_GRP_XDP, LOG_GRP_DOH, LOG_GRP_DNSSEC, LOG_GRP_HINT, LOG_GRP_PLAN, LOG_GRP_ITERATOR, LOG_GRP_VALIDATOR, LOG_GRP_RESOLVER, LOG_GRP_SELECTION, LOG_GRP_ZCUT, LOG_GRP_COOKIES, LOG_GRP_STATISTICS, LOG_GRP_REBIND, LOG_GRP_WORKER, LOG_GRP_POLICY, LOG_GRP_TASENTINEL, LOG_GRP_TASIGNALING, LOG_GRP_TAUPDATE, LOG_GRP_DAF, LOG_GRP_DETECTTIMEJUMP, LOG_GRP_DETECTTIMESKEW, LOG_GRP_GRAPHITE, LOG_GRP_PREFILL, LOG_GRP_PRIMING, LOG_GRP_SRVSTALE, LOG_GRP_WATCHDOG, LOG_GRP_NSID, LOG_GRP_DNSTAP, LOG_GRP_TESTS, LOG_GRP_DOTAUTH, LOG_GRP_HTTP, LOG_GRP_CONTROL, LOG_GRP_MODULE, LOG_GRP_DEVEL, LOG_GRP_RENUMBER, LOG_GRP_EDE, LOG_GRP_REQDBG};
+
+kr_layer_t kr_layer_t_static;
+_Bool kr_dbg_assertion_abort;
+int kr_dbg_assertion_fork;
+
+typedef int32_t (*kr_stale_cb)(int32_t ttl, const knot_dname_t *owner, uint16_t type,
+ const struct kr_query *qry);
+
+void kr_rrset_init(knot_rrset_t *rrset, knot_dname_t *owner,
+ uint16_t type, uint16_t rclass, uint32_t ttl);
+struct kr_query {
+ struct kr_query *parent;
+ knot_dname_t *sname;
+ uint16_t stype;
+ uint16_t sclass;
+ uint16_t id;
+ uint16_t reorder;
+ struct kr_qflags flags;
+ struct kr_qflags forward_flags;
+ uint32_t secret;
+ uint32_t uid;
+ uint64_t creation_time_mono;
+ uint64_t timestamp_mono;
+ struct timeval timestamp;
+ struct kr_zonecut zone_cut;
+ struct kr_layer_pickle *deferred;
+ int8_t cname_depth;
+ struct kr_query *cname_parent;
+ struct kr_request *request;
+ kr_stale_cb stale_cb;
+ struct kr_server_selection server_selection;
+};
+struct kr_context {
+ struct kr_qflags options;
+ knot_rrset_t *downstream_opt_rr;
+ knot_rrset_t *upstream_opt_rr;
+ trie_t *trust_anchors;
+ trie_t *negative_anchors;
+ struct kr_zonecut root_hints;
+ struct kr_cache cache;
+ unsigned int cache_rtt_tout_retry_interval;
+ char _stub[];
+};
+struct kr_transport {
+ knot_dname_t *ns_name;
+ /* beware: hidden stub, to avoid hardcoding sockaddr lengths */
+};
+const char *knot_strerror(int);
+knot_dname_t *knot_dname_copy(const knot_dname_t *, knot_mm_t *);
+knot_dname_t *knot_dname_from_str(uint8_t *, const char *, size_t);
+int knot_dname_in_bailiwick(const knot_dname_t *, const knot_dname_t *);
+_Bool knot_dname_is_equal(const knot_dname_t *, const knot_dname_t *);
+size_t knot_dname_labels(const uint8_t *, const uint8_t *);
+size_t knot_dname_size(const knot_dname_t *);
+void knot_dname_to_lower(knot_dname_t *);
+char *knot_dname_to_str(char *, const knot_dname_t *, size_t);
+knot_rdata_t *knot_rdataset_at(const knot_rdataset_t *, uint16_t);
+int knot_rdataset_merge(knot_rdataset_t *, const knot_rdataset_t *, knot_mm_t *);
+int knot_rrset_add_rdata(knot_rrset_t *, const uint8_t *, uint16_t, knot_mm_t *);
+void knot_rrset_free(knot_rrset_t *, knot_mm_t *);
+int knot_rrset_txt_dump(const knot_rrset_t *, char **, size_t *, const knot_dump_style_t *);
+int knot_rrset_txt_dump_data(const knot_rrset_t *, const size_t, char *, const size_t, const knot_dump_style_t *);
+size_t knot_rrset_size(const knot_rrset_t *);
+int knot_pkt_begin(knot_pkt_t *, knot_section_t);
+int knot_pkt_put_question(knot_pkt_t *, const knot_dname_t *, uint16_t, uint16_t);
+int knot_pkt_put_rotate(knot_pkt_t *, uint16_t, const knot_rrset_t *, uint16_t, uint16_t);
+knot_pkt_t *knot_pkt_new(void *, uint16_t, knot_mm_t *);
+void knot_pkt_free(knot_pkt_t *);
+int knot_pkt_parse(knot_pkt_t *, unsigned int);
+knot_rrset_t *kr_request_ensure_edns(struct kr_request *);
+knot_pkt_t *kr_request_ensure_answer(struct kr_request *);
+int kr_request_set_extended_error(struct kr_request *, int, const char *);
+struct kr_rplan *kr_resolve_plan(struct kr_request *);
+knot_mm_t *kr_resolve_pool(struct kr_request *);
+struct kr_query *kr_rplan_push(struct kr_rplan *, struct kr_query *, const knot_dname_t *, uint16_t, uint16_t);
+int kr_rplan_pop(struct kr_rplan *, struct kr_query *);
+struct kr_query *kr_rplan_resolved(struct kr_rplan *);
+struct kr_query *kr_rplan_last(struct kr_rplan *);
+int kr_forward_add_target(struct kr_request *, const struct sockaddr *);
+_Bool kr_log_is_debug_fun(enum kr_log_group, const struct kr_request *);
+void kr_log_req1(const struct kr_request * const, uint32_t, const unsigned int, enum kr_log_group, const char *, const char *, ...);
+void kr_log_q1(const struct kr_query * const, enum kr_log_group, const char *, const char *, ...);
+const char *kr_log_grp2name(enum kr_log_group);
+void kr_log_fmt(enum kr_log_group, kr_log_level_t, const char *, const char *, const char *, const char *, ...);
+int kr_make_query(struct kr_query *, knot_pkt_t *);
+void kr_pkt_make_auth_header(knot_pkt_t *);
+int kr_pkt_put(knot_pkt_t *, const knot_dname_t *, uint32_t, uint16_t, uint16_t, const uint8_t *, uint16_t);
+int kr_pkt_recycle(knot_pkt_t *);
+int kr_pkt_clear_payload(knot_pkt_t *);
+_Bool kr_pkt_has_wire(const knot_pkt_t *);
+_Bool kr_pkt_has_dnssec(const knot_pkt_t *);
+uint16_t kr_pkt_qclass(const knot_pkt_t *);
+uint16_t kr_pkt_qtype(const knot_pkt_t *);
+char *kr_pkt_text(const knot_pkt_t *);
+void kr_rnd_buffered(void *, unsigned int);
+uint32_t kr_rrsig_sig_inception(const knot_rdata_t *);
+uint32_t kr_rrsig_sig_expiration(const knot_rdata_t *);
+uint16_t kr_rrsig_type_covered(const knot_rdata_t *);
+const char *kr_inaddr(const struct sockaddr *);
+int kr_inaddr_family(const struct sockaddr *);
+int kr_inaddr_len(const struct sockaddr *);
+int kr_inaddr_str(const struct sockaddr *, char *, size_t *);
+int kr_sockaddr_cmp(const struct sockaddr *, const struct sockaddr *);
+int kr_sockaddr_len(const struct sockaddr *);
+uint16_t kr_inaddr_port(const struct sockaddr *);
+int kr_straddr_family(const char *);
+int kr_straddr_subnet(void *, const char *);
+int kr_bitcmp(const char *, const char *, int);
+int kr_family_len(int);
+struct sockaddr *kr_straddr_socket(const char *, int, knot_mm_t *);
+int kr_straddr_split(const char *, char * restrict, uint16_t *);
+_Bool kr_rank_test(uint8_t, uint8_t);
+int kr_ranked_rrarray_add(ranked_rr_array_t *, const knot_rrset_t *, uint8_t, _Bool, uint32_t, knot_mm_t *);
+int kr_ranked_rrarray_finalize(ranked_rr_array_t *, uint32_t, knot_mm_t *);
+void kr_qflags_set(struct kr_qflags *, struct kr_qflags);
+void kr_qflags_clear(struct kr_qflags *, struct kr_qflags);
+int kr_zonecut_add(struct kr_zonecut *, const knot_dname_t *, const void *, int);
+_Bool kr_zonecut_is_empty(struct kr_zonecut *);
+void kr_zonecut_set(struct kr_zonecut *, const knot_dname_t *);
+uint64_t kr_now();
+const char *kr_strptime_diff(const char *, const char *, const char *, double *);
+time_t kr_file_mtime(const char *);
+long long kr_fssize(const char *);
+const char *kr_dirent_name(const struct dirent *);
+void lru_free_items_impl(struct lru *);
+struct lru *lru_create_impl(unsigned int, unsigned int, knot_mm_t *, knot_mm_t *);
+void *lru_get_impl(struct lru *, const char *, unsigned int, unsigned int, _Bool, _Bool *);
+void *mm_realloc(knot_mm_t *, void *, size_t, size_t);
+knot_rrset_t *kr_ta_get(trie_t *, const knot_dname_t *);
+int kr_ta_add(trie_t *, const knot_dname_t *, uint16_t, uint32_t, const uint8_t *, uint16_t);
+int kr_ta_del(trie_t *, const knot_dname_t *);
+void kr_ta_clear(trie_t *);
+_Bool kr_dnssec_key_ksk(const uint8_t *);
+_Bool kr_dnssec_key_revoked(const uint8_t *);
+int kr_dnssec_key_tag(uint16_t, const uint8_t *, size_t);
+int kr_dnssec_key_match(const uint8_t *, size_t, const uint8_t *, size_t);
+int kr_cache_closest_apex(struct kr_cache *, const knot_dname_t *, _Bool, knot_dname_t **);
+int kr_cache_insert_rr(struct kr_cache *, const knot_rrset_t *, const knot_rrset_t *, uint8_t, uint32_t, _Bool);
+int kr_cache_remove(struct kr_cache *, const knot_dname_t *, uint16_t);
+int kr_cache_remove_subtree(struct kr_cache *, const knot_dname_t *, _Bool, int);
+int kr_cache_commit(struct kr_cache *);
+uint32_t packet_ttl(const knot_pkt_t *);
+typedef struct {
+ int sock_type;
+ _Bool tls;
+ _Bool http;
+ _Bool xdp;
+ _Bool freebind;
+ const char *kind;
+} endpoint_flags_t;
+typedef struct {
+ char **at;
+ size_t len;
+ size_t cap;
+} addr_array_t;
+typedef struct {
+ int fd;
+ endpoint_flags_t flags;
+} flagged_fd_t;
+typedef struct {
+ flagged_fd_t *at;
+ size_t len;
+ size_t cap;
+} flagged_fd_array_t;
+typedef struct {
+ const char **at;
+ size_t len;
+ size_t cap;
+} config_array_t;
+struct args {
+ addr_array_t addrs;
+ addr_array_t addrs_tls;
+ flagged_fd_array_t fds;
+ int control_fd;
+ int forks;
+ config_array_t config;
+ const char *rundir;
+ _Bool interactive;
+ _Bool quiet;
+ _Bool tty_binary_output;
+};
+typedef struct {
+ const char *zone_file;
+ const char *origin;
+ uint32_t ttl;
+ enum {ZI_STAMP_NOW, ZI_STAMP_MTIM} time_src;
+ _Bool downgrade;
+ _Bool zonemd;
+ const knot_rrset_t *ds;
+ zi_callback cb;
+ void *cb_param;
+} zi_config_t;
+struct args *the_args;
+struct endpoint {
+ void *handle;
+ int fd;
+ int family;
+ uint16_t port;
+ int16_t nic_queue;
+ _Bool engaged;
+ endpoint_flags_t flags;
+};
+struct request_ctx {
+ struct kr_request req;
+ struct worker_ctx *worker;
+ struct qr_task *task;
+ /* beware: hidden stub, to avoid hardcoding sockaddr lengths */
+};
+struct qr_task {
+ struct request_ctx *ctx;
+ /* beware: hidden stub, to avoid qr_tasklist_t */
+};
+int worker_resolve_exec(struct qr_task *, knot_pkt_t *);
+knot_pkt_t *worker_resolve_mk_pkt(const char *, uint16_t, uint16_t, const struct kr_qflags *);
+struct qr_task *worker_resolve_start(knot_pkt_t *, struct kr_qflags);
+int zi_zone_import(const zi_config_t);
+struct engine {
+ struct kr_context resolver;
+ char _stub[];
+};
+struct worker_ctx {
+ struct engine *engine;
+ char _stub[];
+};
+struct worker_ctx *the_worker;
+typedef struct {
+ uint8_t *params_position;
+ uint8_t *mandatory_position;
+ uint8_t *param_position;
+ int32_t last_key;
+} zs_svcb_t;
+typedef struct {
+ uint8_t bitmap[32];
+ uint8_t length;
+} zs_win_t;
+typedef struct {
+ uint8_t excl_flag;
+ uint16_t addr_family;
+ uint8_t prefix_length;
+} zs_apl_t;
+typedef struct {
+ uint32_t d1;
+ uint32_t d2;
+ uint32_t m1;
+ uint32_t m2;
+ uint32_t s1;
+ uint32_t s2;
+ uint32_t alt;
+ uint64_t siz;
+ uint64_t hp;
+ uint64_t vp;
+ int8_t lat_sign;
+ int8_t long_sign;
+ int8_t alt_sign;
+} zs_loc_t;
+typedef enum {ZS_STATE_NONE, ZS_STATE_DATA, ZS_STATE_ERROR, ZS_STATE_INCLUDE, ZS_STATE_EOF, ZS_STATE_STOP} zs_state_t;
+typedef struct zs_scanner zs_scanner_t;
+typedef struct zs_scanner {
+ int cs;
+ int top;
+ int stack[16];
+ _Bool multiline;
+ uint64_t number64;
+ uint64_t number64_tmp;
+ uint32_t decimals;
+ uint32_t decimal_counter;
+ uint32_t item_length;
+ uint32_t item_length_position;
+ uint8_t *item_length_location;
+ uint8_t *item_length2_location;
+ uint32_t buffer_length;
+ uint8_t buffer[65535];
+ char include_filename[65535];
+ char *path;
+ zs_win_t windows[256];
+ int16_t last_window;
+ zs_apl_t apl;
+ zs_loc_t loc;
+ zs_svcb_t svcb;
+ uint8_t addr[16];
+ _Bool long_string;
+ _Bool comma_list;
+ uint8_t *dname;
+ uint32_t *dname_length;
+ uint32_t dname_tmp_length;
+ uint32_t r_data_tail;
+ uint32_t zone_origin_length;
+ uint8_t zone_origin[318];
+ uint16_t default_class;
+ uint32_t default_ttl;
+ zs_state_t state;
+ struct {
+ _Bool automatic;
+ void (*record)(zs_scanner_t *);
+ void (*error)(zs_scanner_t *);
+ void (*comment)(zs_scanner_t *);
+ void *data;
+ } process;
+ struct {
+ const char *start;
+ const char *current;
+ const char *end;
+ _Bool eof;
+ _Bool mmaped;
+ } input;
+ struct {
+ char *name;
+ int descriptor;
+ } file;
+ struct {
+ int code;
+ uint64_t counter;
+ _Bool fatal;
+ } error;
+ uint64_t line_counter;
+ uint32_t r_owner_length;
+ uint8_t r_owner[318];
+ uint16_t r_class;
+ uint32_t r_ttl;
+ uint16_t r_type;
+ uint32_t r_data_length;
+ uint8_t r_data[65535];
+} zs_scanner_t;
+void zs_deinit(zs_scanner_t *);
+int zs_init(zs_scanner_t *, const char *, const uint16_t, const uint32_t);
+int zs_parse_record(zs_scanner_t *);
+int zs_set_input_file(zs_scanner_t *, const char *);
+int zs_set_input_string(zs_scanner_t *, const char *, size_t);
+const char *zs_strerror(const int);
+]]
diff --git a/daemon/lua/kres-gen-32.lua b/daemon/lua/kres-gen-32.lua
new file mode 100644
index 0000000..222891e
--- /dev/null
+++ b/daemon/lua/kres-gen-32.lua
@@ -0,0 +1,648 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+local ffi = require('ffi')
+--[[ This file is generated by ./kres-gen.sh ]] ffi.cdef[[
+typedef long time_t;
+typedef long __time_t;
+typedef long __suseconds_t;
+struct timeval {
+ __time_t tv_sec;
+ __suseconds_t tv_usec;
+};
+
+typedef struct knot_dump_style knot_dump_style_t;
+extern const knot_dump_style_t KR_DUMP_STYLE_DEFAULT;
+struct kr_cdb_api {};
+struct lru {};
+typedef enum {KNOT_ANSWER, KNOT_AUTHORITY, KNOT_ADDITIONAL} knot_section_t;
+typedef struct {
+ uint16_t pos;
+ uint16_t flags;
+ uint16_t compress_ptr[16];
+} knot_rrinfo_t;
+typedef unsigned char knot_dname_t;
+typedef struct {
+ uint16_t len;
+ uint8_t data[];
+} knot_rdata_t;
+typedef struct {
+ uint16_t count;
+ uint32_t size;
+ knot_rdata_t *rdata;
+} knot_rdataset_t;
+
+typedef struct knot_mm {
+ void *ctx, *alloc, *free;
+} knot_mm_t;
+
+typedef void *(*map_alloc_f)(void *, size_t);
+typedef void (*map_free_f)(void *baton, void *ptr);
+typedef void (*trace_log_f) (const struct kr_request *, const char *);
+typedef void (*trace_callback_f)(struct kr_request *);
+typedef uint8_t * (*alloc_wire_f)(struct kr_request *req, uint16_t *maxlen);
+typedef bool (*addr_info_f)(struct sockaddr*);
+typedef void (*zi_callback)(int state, void *param);
+typedef struct {
+ knot_dname_t *_owner;
+ uint32_t _ttl;
+ uint16_t type;
+ uint16_t rclass;
+ knot_rdataset_t rrs;
+ void *additional;
+} knot_rrset_t;
+
+struct kr_module;
+typedef char *(kr_prop_cb)(void *, struct kr_module *, const char *);
+typedef unsigned char knot_dname_storage_t[255];
+typedef struct knot_pkt knot_pkt_t;
+typedef struct {
+ uint8_t *ptr[18];
+} knot_edns_options_t;
+typedef struct {
+ knot_pkt_t *pkt;
+ uint16_t pos;
+ uint16_t count;
+} knot_pktsection_t;
+typedef struct knot_compr {
+ uint8_t *wire;
+ knot_rrinfo_t *rrinfo;
+ struct {
+ uint16_t pos;
+ uint8_t labels;
+ } suffix;
+} knot_compr_t;
+struct knot_pkt {
+ uint8_t *wire;
+ size_t size;
+ size_t max_size;
+ size_t parsed;
+ uint16_t reserved;
+ uint16_t qname_size;
+ uint16_t rrset_count;
+ uint16_t flags;
+ knot_rrset_t *opt_rr;
+ knot_rrset_t *tsig_rr;
+ knot_edns_options_t *edns_opts;
+ struct {
+ uint8_t *pos;
+ size_t len;
+ } tsig_wire;
+ knot_section_t current;
+ knot_pktsection_t sections[3];
+ size_t rrset_allocd;
+ knot_rrinfo_t *rr_info;
+ knot_rrset_t *rr;
+ knot_mm_t mm;
+ knot_compr_t compr;
+ knot_dname_storage_t lower_qname;
+};
+typedef struct trie trie_t;
+struct kr_qflags {
+ _Bool NO_MINIMIZE : 1;
+ _Bool NO_IPV6 : 1;
+ _Bool NO_IPV4 : 1;
+ _Bool TCP : 1;
+ _Bool NO_ANSWER : 1;
+ _Bool RESOLVED : 1;
+ _Bool AWAIT_IPV4 : 1;
+ _Bool AWAIT_IPV6 : 1;
+ _Bool AWAIT_CUT : 1;
+ _Bool NO_EDNS : 1;
+ _Bool CACHED : 1;
+ _Bool NO_CACHE : 1;
+ _Bool EXPIRING : 1;
+ _Bool ALLOW_LOCAL : 1;
+ _Bool DNSSEC_WANT : 1;
+ _Bool DNSSEC_BOGUS : 1;
+ _Bool DNSSEC_INSECURE : 1;
+ _Bool DNSSEC_CD : 1;
+ _Bool STUB : 1;
+ _Bool ALWAYS_CUT : 1;
+ _Bool DNSSEC_WEXPAND : 1;
+ _Bool PERMISSIVE : 1;
+ _Bool STRICT : 1;
+ _Bool BADCOOKIE_AGAIN : 1;
+ _Bool CNAME : 1;
+ _Bool REORDER_RR : 1;
+ _Bool TRACE : 1;
+ _Bool NO_0X20 : 1;
+ _Bool DNSSEC_NODS : 1;
+ _Bool DNSSEC_OPTOUT : 1;
+ _Bool NONAUTH : 1;
+ _Bool FORWARD : 1;
+ _Bool DNS64_MARK : 1;
+ _Bool CACHE_TRIED : 1;
+ _Bool NO_NS_FOUND : 1;
+ _Bool PKT_IS_SANE : 1;
+ _Bool DNS64_DISABLE : 1;
+};
+typedef struct ranked_rr_array_entry {
+ uint32_t qry_uid;
+ uint8_t rank;
+ uint8_t revalidation_cnt;
+ _Bool cached : 1;
+ _Bool yielded : 1;
+ _Bool to_wire : 1;
+ _Bool expiring : 1;
+ _Bool in_progress : 1;
+ _Bool dont_cache : 1;
+ knot_rrset_t *rr;
+} ranked_rr_array_entry_t;
+typedef struct {
+ ranked_rr_array_entry_t **at;
+ size_t len;
+ size_t cap;
+} ranked_rr_array_t;
+typedef struct kr_http_header_array_entry {
+ char *name;
+ char *value;
+} kr_http_header_array_entry_t;
+typedef struct {
+ kr_http_header_array_entry_t *at;
+ size_t len;
+ size_t cap;
+} kr_http_header_array_t;
+typedef struct {
+ union kr_sockaddr *at;
+ size_t len;
+ size_t cap;
+} kr_sockaddr_array_t;
+struct kr_zonecut {
+ knot_dname_t *name;
+ knot_rrset_t *key;
+ knot_rrset_t *trust_anchor;
+ struct kr_zonecut *parent;
+ trie_t *nsset;
+ knot_mm_t *pool;
+};
+typedef struct {
+ struct kr_query **at;
+ size_t len;
+ size_t cap;
+} kr_qarray_t;
+struct kr_rplan {
+ kr_qarray_t pending;
+ kr_qarray_t resolved;
+ struct kr_query *initial;
+ struct kr_request *request;
+ knot_mm_t *pool;
+ uint32_t next_uid;
+};
+struct kr_request_qsource_flags {
+ _Bool tcp : 1;
+ _Bool tls : 1;
+ _Bool http : 1;
+ _Bool xdp : 1;
+};
+struct kr_extended_error {
+ int32_t info_code;
+ const char *extra_text;
+};
+struct kr_request {
+ struct kr_context *ctx;
+ knot_pkt_t *answer;
+ struct kr_query *current_query;
+ struct {
+ const struct sockaddr *addr;
+ const struct sockaddr *comm_addr;
+ const struct sockaddr *dst_addr;
+ const knot_pkt_t *packet;
+ struct kr_request_qsource_flags flags;
+ struct kr_request_qsource_flags comm_flags;
+ size_t size;
+ int32_t stream_id;
+ kr_http_header_array_t headers;
+ } qsource;
+ struct {
+ unsigned int rtt;
+ const struct kr_transport *transport;
+ } upstream;
+ struct kr_qflags options;
+ int state;
+ ranked_rr_array_t answ_selected;
+ ranked_rr_array_t auth_selected;
+ ranked_rr_array_t add_selected;
+ _Bool answ_validated;
+ _Bool auth_validated;
+ uint8_t rank;
+ struct kr_rplan rplan;
+ trace_log_f trace_log;
+ trace_callback_f trace_finish;
+ int vars_ref;
+ knot_mm_t pool;
+ unsigned int uid;
+ struct {
+ addr_info_f is_tls_capable;
+ addr_info_f is_tcp_connected;
+ addr_info_f is_tcp_waiting;
+ kr_sockaddr_array_t forwarding_targets;
+ } selection_context;
+ unsigned int count_no_nsaddr;
+ unsigned int count_fail_row;
+ alloc_wire_f alloc_wire_cb;
+ struct kr_extended_error extended_error;
+};
+enum kr_rank {KR_RANK_INITIAL, KR_RANK_OMIT, KR_RANK_TRY, KR_RANK_INDET = 4, KR_RANK_BOGUS, KR_RANK_MISMATCH, KR_RANK_MISSING, KR_RANK_INSECURE, KR_RANK_AUTH = 16, KR_RANK_SECURE = 32};
+typedef struct kr_cdb * kr_cdb_pt;
+struct kr_cdb_stats {
+ uint64_t open;
+ uint64_t close;
+ uint64_t count;
+ uint64_t count_entries;
+ uint64_t clear;
+ uint64_t commit;
+ uint64_t read;
+ uint64_t read_miss;
+ uint64_t write;
+ uint64_t remove;
+ uint64_t remove_miss;
+ uint64_t match;
+ uint64_t match_miss;
+ uint64_t read_leq;
+ uint64_t read_leq_miss;
+ double usage_percent;
+};
+typedef struct uv_timer_s uv_timer_t;
+struct kr_cache {
+ kr_cdb_pt db;
+ const struct kr_cdb_api *api;
+ struct kr_cdb_stats stats;
+ uint32_t ttl_min;
+ uint32_t ttl_max;
+ struct timeval checkpoint_walltime;
+ uint64_t checkpoint_monotime;
+ uv_timer_t *health_timer;
+};
+typedef struct kr_layer {
+ int state;
+ struct kr_request *req;
+ const struct kr_layer_api *api;
+ knot_pkt_t *pkt;
+ struct sockaddr *dst;
+ _Bool is_stream;
+} kr_layer_t;
+typedef struct kr_layer_api {
+ int (*begin)(kr_layer_t *);
+ int (*reset)(kr_layer_t *);
+ int (*finish)(kr_layer_t *);
+ int (*consume)(kr_layer_t *, knot_pkt_t *);
+ int (*produce)(kr_layer_t *, knot_pkt_t *);
+ int (*checkout)(kr_layer_t *, knot_pkt_t *, struct sockaddr *, int);
+ int (*answer_finalize)(kr_layer_t *);
+ void *data;
+ int cb_slots[];
+} kr_layer_api_t;
+struct kr_prop {
+ kr_prop_cb *cb;
+ const char *name;
+ const char *info;
+};
+struct kr_module {
+ char *name;
+ int (*init)(struct kr_module *);
+ int (*deinit)(struct kr_module *);
+ int (*config)(struct kr_module *, const char *);
+ const kr_layer_api_t *layer;
+ const struct kr_prop *props;
+ void *lib;
+ void *data;
+};
+struct kr_server_selection {
+ _Bool initialized;
+ void (*choose_transport)(struct kr_query *, struct kr_transport **);
+ void (*update_rtt)(struct kr_query *, const struct kr_transport *, unsigned int);
+ void (*error)(struct kr_query *, const struct kr_transport *, enum kr_selection_error);
+ struct local_state *local_state;
+};
+typedef int kr_log_level_t;
+enum kr_log_group {LOG_GRP_UNKNOWN = -1, LOG_GRP_SYSTEM = 1, LOG_GRP_CACHE, LOG_GRP_IO, LOG_GRP_NETWORK, LOG_GRP_TA, LOG_GRP_TLS, LOG_GRP_GNUTLS, LOG_GRP_TLSCLIENT, LOG_GRP_XDP, LOG_GRP_DOH, LOG_GRP_DNSSEC, LOG_GRP_HINT, LOG_GRP_PLAN, LOG_GRP_ITERATOR, LOG_GRP_VALIDATOR, LOG_GRP_RESOLVER, LOG_GRP_SELECTION, LOG_GRP_ZCUT, LOG_GRP_COOKIES, LOG_GRP_STATISTICS, LOG_GRP_REBIND, LOG_GRP_WORKER, LOG_GRP_POLICY, LOG_GRP_TASENTINEL, LOG_GRP_TASIGNALING, LOG_GRP_TAUPDATE, LOG_GRP_DAF, LOG_GRP_DETECTTIMEJUMP, LOG_GRP_DETECTTIMESKEW, LOG_GRP_GRAPHITE, LOG_GRP_PREFILL, LOG_GRP_PRIMING, LOG_GRP_SRVSTALE, LOG_GRP_WATCHDOG, LOG_GRP_NSID, LOG_GRP_DNSTAP, LOG_GRP_TESTS, LOG_GRP_DOTAUTH, LOG_GRP_HTTP, LOG_GRP_CONTROL, LOG_GRP_MODULE, LOG_GRP_DEVEL, LOG_GRP_RENUMBER, LOG_GRP_EDE, LOG_GRP_REQDBG};
+
+kr_layer_t kr_layer_t_static;
+_Bool kr_dbg_assertion_abort;
+int kr_dbg_assertion_fork;
+
+typedef int32_t (*kr_stale_cb)(int32_t ttl, const knot_dname_t *owner, uint16_t type,
+ const struct kr_query *qry);
+
+void kr_rrset_init(knot_rrset_t *rrset, knot_dname_t *owner,
+ uint16_t type, uint16_t rclass, uint32_t ttl);
+struct kr_query {
+ struct kr_query *parent;
+ knot_dname_t *sname;
+ uint16_t stype;
+ uint16_t sclass;
+ uint16_t id;
+ uint16_t reorder;
+ struct kr_qflags flags;
+ struct kr_qflags forward_flags;
+ uint32_t secret;
+ uint32_t uid;
+ uint64_t creation_time_mono;
+ uint64_t timestamp_mono;
+ struct timeval timestamp;
+ struct kr_zonecut zone_cut;
+ struct kr_layer_pickle *deferred;
+ int8_t cname_depth;
+ struct kr_query *cname_parent;
+ struct kr_request *request;
+ kr_stale_cb stale_cb;
+ struct kr_server_selection server_selection;
+};
+struct kr_context {
+ struct kr_qflags options;
+ knot_rrset_t *downstream_opt_rr;
+ knot_rrset_t *upstream_opt_rr;
+ trie_t *trust_anchors;
+ trie_t *negative_anchors;
+ struct kr_zonecut root_hints;
+ struct kr_cache cache;
+ unsigned int cache_rtt_tout_retry_interval;
+ char _stub[];
+};
+struct kr_transport {
+ knot_dname_t *ns_name;
+ /* beware: hidden stub, to avoid hardcoding sockaddr lengths */
+};
+const char *knot_strerror(int);
+knot_dname_t *knot_dname_copy(const knot_dname_t *, knot_mm_t *);
+knot_dname_t *knot_dname_from_str(uint8_t *, const char *, size_t);
+int knot_dname_in_bailiwick(const knot_dname_t *, const knot_dname_t *);
+_Bool knot_dname_is_equal(const knot_dname_t *, const knot_dname_t *);
+size_t knot_dname_labels(const uint8_t *, const uint8_t *);
+size_t knot_dname_size(const knot_dname_t *);
+void knot_dname_to_lower(knot_dname_t *);
+char *knot_dname_to_str(char *, const knot_dname_t *, size_t);
+knot_rdata_t *knot_rdataset_at(const knot_rdataset_t *, uint16_t);
+int knot_rdataset_merge(knot_rdataset_t *, const knot_rdataset_t *, knot_mm_t *);
+int knot_rrset_add_rdata(knot_rrset_t *, const uint8_t *, uint16_t, knot_mm_t *);
+void knot_rrset_free(knot_rrset_t *, knot_mm_t *);
+int knot_rrset_txt_dump(const knot_rrset_t *, char **, size_t *, const knot_dump_style_t *);
+int knot_rrset_txt_dump_data(const knot_rrset_t *, const size_t, char *, const size_t, const knot_dump_style_t *);
+size_t knot_rrset_size(const knot_rrset_t *);
+int knot_pkt_begin(knot_pkt_t *, knot_section_t);
+int knot_pkt_put_question(knot_pkt_t *, const knot_dname_t *, uint16_t, uint16_t);
+int knot_pkt_put_rotate(knot_pkt_t *, uint16_t, const knot_rrset_t *, uint16_t, uint16_t);
+knot_pkt_t *knot_pkt_new(void *, uint16_t, knot_mm_t *);
+void knot_pkt_free(knot_pkt_t *);
+int knot_pkt_parse(knot_pkt_t *, unsigned int);
+knot_rrset_t *kr_request_ensure_edns(struct kr_request *);
+knot_pkt_t *kr_request_ensure_answer(struct kr_request *);
+int kr_request_set_extended_error(struct kr_request *, int, const char *);
+struct kr_rplan *kr_resolve_plan(struct kr_request *);
+knot_mm_t *kr_resolve_pool(struct kr_request *);
+struct kr_query *kr_rplan_push(struct kr_rplan *, struct kr_query *, const knot_dname_t *, uint16_t, uint16_t);
+int kr_rplan_pop(struct kr_rplan *, struct kr_query *);
+struct kr_query *kr_rplan_resolved(struct kr_rplan *);
+struct kr_query *kr_rplan_last(struct kr_rplan *);
+int kr_forward_add_target(struct kr_request *, const struct sockaddr *);
+_Bool kr_log_is_debug_fun(enum kr_log_group, const struct kr_request *);
+void kr_log_req1(const struct kr_request * const, uint32_t, const unsigned int, enum kr_log_group, const char *, const char *, ...);
+void kr_log_q1(const struct kr_query * const, enum kr_log_group, const char *, const char *, ...);
+const char *kr_log_grp2name(enum kr_log_group);
+void kr_log_fmt(enum kr_log_group, kr_log_level_t, const char *, const char *, const char *, const char *, ...);
+int kr_make_query(struct kr_query *, knot_pkt_t *);
+void kr_pkt_make_auth_header(knot_pkt_t *);
+int kr_pkt_put(knot_pkt_t *, const knot_dname_t *, uint32_t, uint16_t, uint16_t, const uint8_t *, uint16_t);
+int kr_pkt_recycle(knot_pkt_t *);
+int kr_pkt_clear_payload(knot_pkt_t *);
+_Bool kr_pkt_has_wire(const knot_pkt_t *);
+_Bool kr_pkt_has_dnssec(const knot_pkt_t *);
+uint16_t kr_pkt_qclass(const knot_pkt_t *);
+uint16_t kr_pkt_qtype(const knot_pkt_t *);
+char *kr_pkt_text(const knot_pkt_t *);
+void kr_rnd_buffered(void *, unsigned int);
+uint32_t kr_rrsig_sig_inception(const knot_rdata_t *);
+uint32_t kr_rrsig_sig_expiration(const knot_rdata_t *);
+uint16_t kr_rrsig_type_covered(const knot_rdata_t *);
+const char *kr_inaddr(const struct sockaddr *);
+int kr_inaddr_family(const struct sockaddr *);
+int kr_inaddr_len(const struct sockaddr *);
+int kr_inaddr_str(const struct sockaddr *, char *, size_t *);
+int kr_sockaddr_cmp(const struct sockaddr *, const struct sockaddr *);
+int kr_sockaddr_len(const struct sockaddr *);
+uint16_t kr_inaddr_port(const struct sockaddr *);
+int kr_straddr_family(const char *);
+int kr_straddr_subnet(void *, const char *);
+int kr_bitcmp(const char *, const char *, int);
+int kr_family_len(int);
+struct sockaddr *kr_straddr_socket(const char *, int, knot_mm_t *);
+int kr_straddr_split(const char *, char * restrict, uint16_t *);
+_Bool kr_rank_test(uint8_t, uint8_t);
+int kr_ranked_rrarray_add(ranked_rr_array_t *, const knot_rrset_t *, uint8_t, _Bool, uint32_t, knot_mm_t *);
+int kr_ranked_rrarray_finalize(ranked_rr_array_t *, uint32_t, knot_mm_t *);
+void kr_qflags_set(struct kr_qflags *, struct kr_qflags);
+void kr_qflags_clear(struct kr_qflags *, struct kr_qflags);
+int kr_zonecut_add(struct kr_zonecut *, const knot_dname_t *, const void *, int);
+_Bool kr_zonecut_is_empty(struct kr_zonecut *);
+void kr_zonecut_set(struct kr_zonecut *, const knot_dname_t *);
+uint64_t kr_now();
+const char *kr_strptime_diff(const char *, const char *, const char *, double *);
+time_t kr_file_mtime(const char *);
+long long kr_fssize(const char *);
+const char *kr_dirent_name(const struct dirent *);
+void lru_free_items_impl(struct lru *);
+struct lru *lru_create_impl(unsigned int, unsigned int, knot_mm_t *, knot_mm_t *);
+void *lru_get_impl(struct lru *, const char *, unsigned int, unsigned int, _Bool, _Bool *);
+void *mm_realloc(knot_mm_t *, void *, size_t, size_t);
+knot_rrset_t *kr_ta_get(trie_t *, const knot_dname_t *);
+int kr_ta_add(trie_t *, const knot_dname_t *, uint16_t, uint32_t, const uint8_t *, uint16_t);
+int kr_ta_del(trie_t *, const knot_dname_t *);
+void kr_ta_clear(trie_t *);
+_Bool kr_dnssec_key_ksk(const uint8_t *);
+_Bool kr_dnssec_key_revoked(const uint8_t *);
+int kr_dnssec_key_tag(uint16_t, const uint8_t *, size_t);
+int kr_dnssec_key_match(const uint8_t *, size_t, const uint8_t *, size_t);
+int kr_cache_closest_apex(struct kr_cache *, const knot_dname_t *, _Bool, knot_dname_t **);
+int kr_cache_insert_rr(struct kr_cache *, const knot_rrset_t *, const knot_rrset_t *, uint8_t, uint32_t, _Bool);
+int kr_cache_remove(struct kr_cache *, const knot_dname_t *, uint16_t);
+int kr_cache_remove_subtree(struct kr_cache *, const knot_dname_t *, _Bool, int);
+int kr_cache_commit(struct kr_cache *);
+uint32_t packet_ttl(const knot_pkt_t *);
+typedef struct {
+ int sock_type;
+ _Bool tls;
+ _Bool http;
+ _Bool xdp;
+ _Bool freebind;
+ const char *kind;
+} endpoint_flags_t;
+typedef struct {
+ char **at;
+ size_t len;
+ size_t cap;
+} addr_array_t;
+typedef struct {
+ int fd;
+ endpoint_flags_t flags;
+} flagged_fd_t;
+typedef struct {
+ flagged_fd_t *at;
+ size_t len;
+ size_t cap;
+} flagged_fd_array_t;
+typedef struct {
+ const char **at;
+ size_t len;
+ size_t cap;
+} config_array_t;
+struct args {
+ addr_array_t addrs;
+ addr_array_t addrs_tls;
+ flagged_fd_array_t fds;
+ int control_fd;
+ int forks;
+ config_array_t config;
+ const char *rundir;
+ _Bool interactive;
+ _Bool quiet;
+ _Bool tty_binary_output;
+};
+typedef struct {
+ const char *zone_file;
+ const char *origin;
+ uint32_t ttl;
+ enum {ZI_STAMP_NOW, ZI_STAMP_MTIM} time_src;
+ _Bool downgrade;
+ _Bool zonemd;
+ const knot_rrset_t *ds;
+ zi_callback cb;
+ void *cb_param;
+} zi_config_t;
+struct args *the_args;
+struct endpoint {
+ void *handle;
+ int fd;
+ int family;
+ uint16_t port;
+ int16_t nic_queue;
+ _Bool engaged;
+ endpoint_flags_t flags;
+};
+struct request_ctx {
+ struct kr_request req;
+ struct worker_ctx *worker;
+ struct qr_task *task;
+ /* beware: hidden stub, to avoid hardcoding sockaddr lengths */
+};
+struct qr_task {
+ struct request_ctx *ctx;
+ /* beware: hidden stub, to avoid qr_tasklist_t */
+};
+int worker_resolve_exec(struct qr_task *, knot_pkt_t *);
+knot_pkt_t *worker_resolve_mk_pkt(const char *, uint16_t, uint16_t, const struct kr_qflags *);
+struct qr_task *worker_resolve_start(knot_pkt_t *, struct kr_qflags);
+int zi_zone_import(const zi_config_t);
+struct engine {
+ struct kr_context resolver;
+ char _stub[];
+};
+struct worker_ctx {
+ struct engine *engine;
+ char _stub[];
+};
+struct worker_ctx *the_worker;
+typedef struct {
+ uint8_t *params_position;
+ uint8_t *mandatory_position;
+ uint8_t *param_position;
+ int32_t last_key;
+} zs_svcb_t;
+typedef struct {
+ uint8_t bitmap[32];
+ uint8_t length;
+} zs_win_t;
+typedef struct {
+ uint8_t excl_flag;
+ uint16_t addr_family;
+ uint8_t prefix_length;
+} zs_apl_t;
+typedef struct {
+ uint32_t d1;
+ uint32_t d2;
+ uint32_t m1;
+ uint32_t m2;
+ uint32_t s1;
+ uint32_t s2;
+ uint32_t alt;
+ uint64_t siz;
+ uint64_t hp;
+ uint64_t vp;
+ int8_t lat_sign;
+ int8_t long_sign;
+ int8_t alt_sign;
+} zs_loc_t;
+typedef enum {ZS_STATE_NONE, ZS_STATE_DATA, ZS_STATE_ERROR, ZS_STATE_INCLUDE, ZS_STATE_EOF, ZS_STATE_STOP} zs_state_t;
+typedef struct zs_scanner zs_scanner_t;
+typedef struct zs_scanner {
+ int cs;
+ int top;
+ int stack[16];
+ _Bool multiline;
+ uint64_t number64;
+ uint64_t number64_tmp;
+ uint32_t decimals;
+ uint32_t decimal_counter;
+ uint32_t item_length;
+ uint32_t item_length_position;
+ uint8_t *item_length_location;
+ uint8_t *item_length2_location;
+ uint32_t buffer_length;
+ uint8_t buffer[65535];
+ char include_filename[65535];
+ char *path;
+ zs_win_t windows[256];
+ int16_t last_window;
+ zs_apl_t apl;
+ zs_loc_t loc;
+ zs_svcb_t svcb;
+ uint8_t addr[16];
+ _Bool long_string;
+ _Bool comma_list;
+ uint8_t *dname;
+ uint32_t *dname_length;
+ uint32_t dname_tmp_length;
+ uint32_t r_data_tail;
+ uint32_t zone_origin_length;
+ uint8_t zone_origin[318];
+ uint16_t default_class;
+ uint32_t default_ttl;
+ zs_state_t state;
+ struct {
+ _Bool automatic;
+ void (*record)(zs_scanner_t *);
+ void (*error)(zs_scanner_t *);
+ void (*comment)(zs_scanner_t *);
+ void *data;
+ } process;
+ struct {
+ const char *start;
+ const char *current;
+ const char *end;
+ _Bool eof;
+ _Bool mmaped;
+ } input;
+ struct {
+ char *name;
+ int descriptor;
+ } file;
+ struct {
+ int code;
+ uint64_t counter;
+ _Bool fatal;
+ } error;
+ uint64_t line_counter;
+ uint32_t r_owner_length;
+ uint8_t r_owner[318];
+ uint16_t r_class;
+ uint32_t r_ttl;
+ uint16_t r_type;
+ uint32_t r_data_length;
+ uint8_t r_data[65535];
+} zs_scanner_t;
+void zs_deinit(zs_scanner_t *);
+int zs_init(zs_scanner_t *, const char *, const uint16_t, const uint32_t);
+int zs_parse_record(zs_scanner_t *);
+int zs_set_input_file(zs_scanner_t *, const char *);
+int zs_set_input_string(zs_scanner_t *, const char *, size_t);
+const char *zs_strerror(const int);
+]]
diff --git a/daemon/lua/kres-gen.sh b/daemon/lua/kres-gen.sh
new file mode 100755
index 0000000..70afb40
--- /dev/null
+++ b/daemon/lua/kres-gen.sh
@@ -0,0 +1,353 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Run with "ninja kres-gen" to re-generate $1
+set -o pipefail -o errexit -o nounset
+
+cd "$(dirname ${0})"
+OUTNAME="$1"
+CDEFS="../../scripts/gen-cdefs.sh"
+LIBKRES="${MESON_BUILD_ROOT}/lib/libkres.so"
+KRESD="${MESON_BUILD_ROOT}/daemon/kresd"
+if [ ! -e "$LIBKRES" ]; then
+ # We probably use static libkres.
+ LIBKRES="$KRESD"
+fi
+
+for REQFILE in "$CDEFS" "$LIBKRES" "$KRESD"
+do
+ test '!' -s "$REQFILE" -a -r "$REQFILE" \
+ && echo "Required file $REQFILE cannot be read, did you build binaries and shared libraries?" \
+ && exit 1
+done
+
+# Write to "$OUTNAME" instead of stdout
+mv "$OUTNAME"{,.bak} ||:
+exec 5<&1- # move stdout into FD 5
+exec 1<>"$OUTNAME" # replace stdout with file
+
+restore() {
+ exec 1>&- # close stdout redirected into "$OUTNAME"
+ exec 1<&5- # restore original stdout
+ mv -v "$OUTNAME"{,.fail} ||:
+ mv -v "$OUTNAME"{.bak,} ||:
+ (>&2 echo "Failed to re-generate $OUTNAME! Missing debugsymbols? Missing shared library?")
+}
+trap restore ERR INT TERM
+
+### Dev's guide
+#
+# C declarations for lua are (mostly) generated to simplify maintenance.
+# (Avoid typos, accidental mismatches, etc.)
+#
+# To regenerate the C definitions for lua:
+# - you need to have debugging symbols for knot-dns and knot-resolver;
+# you get those by compiling with -g; for knot-dns it might be enough
+# to just install it with debugging symbols included (in your distro way)
+# - run ninja kres-gen
+# - the knot-dns libraries are found via pkg-config
+# - you also need gdb on $PATH
+
+printf -- "-- SPDX-License-Identifier: GPL-3.0-or-later\n\n"
+printf -- "local ffi = require('ffi')\n"
+printf -- "--[[ This file is generated by ./kres-gen.sh ]] ffi.cdef[[\n"
+
+# Some system dependencies. TODO: this generated part isn't perfectly portable.
+${CDEFS} ${LIBKRES} types <<-EOF
+ typedef time_t
+ __time_t
+ __suseconds_t
+ struct timeval
+EOF
+
+## Various types (mainly), from libknot and libkres
+
+printf "
+typedef struct knot_dump_style knot_dump_style_t;
+extern const knot_dump_style_t KR_DUMP_STYLE_DEFAULT;
+struct kr_cdb_api {};
+struct lru {};
+"
+
+${CDEFS} ${LIBKRES} types <<-EOF
+ knot_section_t
+ knot_rrinfo_t
+ knot_dname_t
+ knot_rdata_t
+ knot_rdataset_t
+EOF
+
+# The generator doesn't work well with typedefs of functions.
+printf "
+typedef struct knot_mm {
+ void *ctx, *alloc, *free;
+} knot_mm_t;
+
+typedef void *(*map_alloc_f)(void *, size_t);
+typedef void (*map_free_f)(void *baton, void *ptr);
+typedef void (*trace_log_f) (const struct kr_request *, const char *);
+typedef void (*trace_callback_f)(struct kr_request *);
+typedef uint8_t * (*alloc_wire_f)(struct kr_request *req, uint16_t *maxlen);
+typedef bool (*addr_info_f)(struct sockaddr*);
+typedef void (*zi_callback)(int state, void *param);
+"
+
+genResType() {
+ echo "$1" | ${CDEFS} ${LIBKRES} types
+}
+
+# No simple way to fixup this rename in ./kres.lua AFAIK.
+genResType "knot_rrset_t" | sed 's/\<owner\>/_owner/; s/\<ttl\>/_ttl/'
+
+printf "
+struct kr_module;
+typedef char *(kr_prop_cb)(void *, struct kr_module *, const char *);
+typedef unsigned char knot_dname_storage_t[255];
+"
+
+${CDEFS} ${LIBKRES} types <<-EOF
+ #knot_pkt_t contains indirect recursion
+ typedef knot_pkt_t
+ knot_edns_options_t
+ knot_pktsection_t
+ knot_compr_t
+ struct knot_pkt
+ #trie_t inside is private to libknot
+ typedef trie_t
+ # libkres
+ struct kr_qflags
+ ranked_rr_array_entry_t
+ ranked_rr_array_t
+ kr_http_header_array_entry_t
+ kr_http_header_array_t
+ kr_sockaddr_array_t
+ struct kr_zonecut
+ kr_qarray_t
+ struct kr_rplan
+ struct kr_request_qsource_flags
+ struct kr_extended_error
+ struct kr_request
+ enum kr_rank
+ typedef kr_cdb_pt
+ struct kr_cdb_stats
+ typedef uv_timer_t
+ struct kr_cache
+ # lib/layer.h
+ kr_layer_t
+ kr_layer_api_t
+ # lib/module.h
+ struct kr_prop
+ struct kr_module
+ struct kr_server_selection
+ kr_log_level_t
+ enum kr_log_group
+EOF
+
+# static variables; these lines might not be simple to generate
+printf "
+kr_layer_t kr_layer_t_static;
+_Bool kr_dbg_assertion_abort;
+int kr_dbg_assertion_fork;
+"
+
+printf "
+typedef int32_t (*kr_stale_cb)(int32_t ttl, const knot_dname_t *owner, uint16_t type,
+ const struct kr_query *qry);
+
+void kr_rrset_init(knot_rrset_t *rrset, knot_dname_t *owner,
+ uint16_t type, uint16_t rclass, uint32_t ttl);
+"
+
+## Some definitions would need too many deps, so shorten them.
+
+genResType "struct kr_query"
+
+genResType "struct kr_context" | sed '/module_array_t/,$ d'
+printf "\tchar _stub[];\n};\n"
+
+
+echo "struct kr_transport" | ${CDEFS} ${KRESD} types | sed '/union /,$ d'
+printf "\t/* beware: hidden stub, to avoid hardcoding sockaddr lengths */\n};\n"
+
+## libknot API
+${CDEFS} libknot functions <<-EOF
+# Utils
+ knot_strerror
+# Domain names
+ knot_dname_copy
+ knot_dname_from_str
+ knot_dname_in_bailiwick
+ knot_dname_is_equal
+ knot_dname_labels
+ knot_dname_size
+ knot_dname_to_lower
+ knot_dname_to_str
+# Resource records
+ knot_rdataset_at
+ knot_rdataset_merge
+ knot_rrset_add_rdata
+ knot_rrset_free
+ knot_rrset_txt_dump
+ knot_rrset_txt_dump_data
+ knot_rrset_size
+# Packet
+ knot_pkt_begin
+ knot_pkt_put_question
+ knot_pkt_put_rotate
+ knot_pkt_new
+ knot_pkt_free
+ knot_pkt_parse
+EOF
+
+## libkres API
+${CDEFS} ${LIBKRES} functions <<-EOF
+# Resolution request
+ kr_request_ensure_edns
+ kr_request_ensure_answer
+ kr_request_set_extended_error
+ kr_resolve_plan
+ kr_resolve_pool
+# Resolution plan
+ kr_rplan_push
+ kr_rplan_pop
+ kr_rplan_resolved
+ kr_rplan_last
+# Forwarding
+ kr_forward_add_target
+# Utils
+ kr_log_is_debug_fun
+ kr_log_req1
+ kr_log_q1
+ kr_log_grp2name
+ kr_log_fmt
+ kr_make_query
+ kr_pkt_make_auth_header
+ kr_pkt_put
+ kr_pkt_recycle
+ kr_pkt_clear_payload
+ kr_pkt_has_wire
+ kr_pkt_has_dnssec
+ kr_pkt_qclass
+ kr_pkt_qtype
+ kr_pkt_text
+ kr_rnd_buffered
+ kr_rrsig_sig_inception
+ kr_rrsig_sig_expiration
+ kr_rrsig_type_covered
+ kr_inaddr
+ kr_inaddr_family
+ kr_inaddr_len
+ kr_inaddr_str
+ kr_sockaddr_cmp
+ kr_sockaddr_len
+ kr_inaddr_port
+ kr_straddr_family
+ kr_straddr_subnet
+ kr_bitcmp
+ kr_family_len
+ kr_straddr_socket
+ kr_straddr_split
+ kr_rank_test
+ kr_ranked_rrarray_add
+ kr_ranked_rrarray_finalize
+ kr_qflags_set
+ kr_qflags_clear
+ kr_zonecut_add
+ kr_zonecut_is_empty
+ kr_zonecut_set
+ kr_now
+ kr_strptime_diff
+ kr_file_mtime
+ kr_fssize
+ kr_dirent_name
+ lru_free_items_impl
+ lru_create_impl
+ lru_get_impl
+ mm_realloc
+# Trust anchors
+ kr_ta_get
+ kr_ta_add
+ kr_ta_del
+ kr_ta_clear
+# DNSSEC
+ kr_dnssec_key_ksk
+ kr_dnssec_key_revoked
+ kr_dnssec_key_tag
+ kr_dnssec_key_match
+# Cache
+ kr_cache_closest_apex
+ kr_cache_insert_rr
+ kr_cache_remove
+ kr_cache_remove_subtree
+ kr_cache_commit
+ # FIXME: perhaps rename this exported symbol
+ packet_ttl
+EOF
+
+
+## kresd itself: worker stuff
+
+${CDEFS} ${KRESD} types <<-EOF
+ endpoint_flags_t
+ # struct args is a bit complex
+ addr_array_t
+ flagged_fd_t
+ flagged_fd_array_t
+ config_array_t
+ struct args
+ zi_config_t
+EOF
+echo "struct args *the_args;"
+
+echo "struct endpoint" | ${CDEFS} ${KRESD} types | sed 's/uv_handle_t \*/void */'
+echo "struct request_ctx" | ${CDEFS} ${KRESD} types | sed '/struct {/,$ d'
+printf "\t/* beware: hidden stub, to avoid hardcoding sockaddr lengths */\n};\n"
+
+echo "struct qr_task" | ${CDEFS} ${KRESD} types | sed '/pktbuf/,$ d'
+printf "\t/* beware: hidden stub, to avoid qr_tasklist_t */\n};\n"
+
+
+${CDEFS} ${KRESD} functions <<-EOF
+ worker_resolve_exec
+ worker_resolve_mk_pkt
+ worker_resolve_start
+ zi_zone_import
+EOF
+
+echo "struct engine" | ${CDEFS} ${KRESD} types | sed '/struct network/,$ d'
+printf "\tchar _stub[];\n};\n"
+
+echo "struct worker_ctx" | ${CDEFS} ${KRESD} types | sed '/uv_loop_t/,$ d'
+printf "\tchar _stub[];\n};\n"
+
+echo "struct worker_ctx *the_worker;"
+
+
+## libzscanner API for ./zonefile.lua
+if pkg-config libknot --atleast-version=3.1; then
+ echo "zs_svcb_t" | ${CDEFS} libzscanner types
+fi
+${CDEFS} libzscanner types <<-EOF
+ zs_win_t
+ zs_apl_t
+ zs_loc_t
+ zs_state_t
+ #zs_scanner_t contains recursion
+ typedef zs_scanner_t
+ zs_scanner_t
+EOF
+${CDEFS} libzscanner functions <<-EOF
+ zs_deinit
+ zs_init
+ zs_parse_record
+ zs_set_input_file
+ zs_set_input_string
+ zs_strerror
+EOF
+
+printf "]]\n"
+
+rm "$OUTNAME".bak ||:
+(>&2 echo "Successfully re-generated ${PWD}/$OUTNAME")
+
+exit 0
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
diff --git a/daemon/lua/krprint.lua b/daemon/lua/krprint.lua
new file mode 100644
index 0000000..dd25a9b
--- /dev/null
+++ b/daemon/lua/krprint.lua
@@ -0,0 +1,340 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+local base_class = {
+ cur_indent = 0,
+}
+
+-- shared constructor: use as serializer_class:new()
+function base_class.new(class, on_unrepresentable)
+ on_unrepresentable = on_unrepresentable or 'comment'
+ if on_unrepresentable ~= 'comment'
+ and on_unrepresentable ~= 'error' then
+ error('unsupported val2expr on_unrepresentable option '
+ .. tostring(on_unrepresentable))
+ end
+ local inst = {}
+ inst.on_unrepresentable = on_unrepresentable
+ inst.done = {}
+ inst.tab_key_path = {}
+ setmetatable(inst, class.__inst_mt)
+ return inst
+end
+
+-- format comment with leading/ending whitespace if needed
+function base_class.format_note(_, note, ws_prefix, ws_suffix)
+ if note == nil then
+ return ''
+ else
+ return string.format('%s--[[ %s ]]%s',
+ ws_prefix or '', note, ws_suffix or '')
+ end
+end
+
+function base_class.indent_head(self)
+ return string.rep(' ', self.cur_indent)
+end
+
+function base_class.indent_inc(self)
+ self.cur_indent = self.cur_indent + self.indent_step
+end
+
+function base_class.indent_dec(self)
+ self.cur_indent = self.cur_indent - self.indent_step
+end
+
+function base_class._fallback(self, val)
+ if self.on_unrepresentable == 'comment' then
+ return 'nil', string.format('missing %s', val)
+ elseif self.on_unrepresentable == 'error' then
+ local key_path_msg
+ if #self.tab_key_path > 0 then
+ local str_key_path = {}
+ for _, key in ipairs(self.tab_key_path) do
+ table.insert(str_key_path,
+ string.format('%s %s', type(key), self:string(tostring(key))))
+ end
+ local key_path = '[' .. table.concat(str_key_path, '][') .. ']'
+ key_path_msg = string.format(' (found at [%s])', key_path)
+ else
+ key_path_msg = ''
+ end
+ error(string.format('cannot serialize type %s%s', type(val), key_path_msg), 2)
+ end
+end
+
+function base_class.val2expr(self, val)
+ local val_type = type(val)
+ local val_repr = self[val_type]
+ if val_repr then
+ return val_repr(self, val)
+ else
+ return self:_fallback(val)
+ end
+end
+
+-- "nil" is a Lua keyword so assignment below is workaround to create
+-- function base_class.nil(self, val)
+base_class['nil'] = function(_, val)
+ assert(type(val) == 'nil')
+ return 'nil'
+end
+
+function base_class.number(_, val)
+ assert(type(val) == 'number')
+ if val == math.huge then
+ return 'math.huge'
+ elseif val == -math.huge then
+ return '-math.huge'
+ elseif tostring(val) == 'nan' then
+ return 'tonumber(\'nan\')'
+ else
+ return string.format("%.60f", val)
+ end
+end
+
+function base_class.char_is_printable(_, c)
+ -- ASCII (from space to ~) and not ' or \
+ return (c >= 0x20 and c < 0x7f)
+ and c ~= 0x27 and c ~= 0x5C
+end
+
+function base_class.string(self, val)
+ assert(type(val) == 'string')
+ local chars = {'\''}
+ for i = 1, #val do
+ local c = string.byte(val, i)
+ if self:char_is_printable(c) then
+ table.insert(chars, string.char(c))
+ else
+ table.insert(chars, string.format('\\%03d', c))
+ end
+ end
+ table.insert(chars, '\'')
+ return table.concat(chars)
+end
+
+function base_class.boolean(_, val)
+ assert(type(val) == 'boolean')
+ return tostring(val)
+end
+
+local function ordered_iter(unordered_tt)
+ local keys = {}
+ for k in pairs(unordered_tt) do
+ table.insert(keys, k)
+ end
+ table.sort(keys,
+ function (a, b)
+ if type(a) ~= type(b) then
+ return type(a) < type(b)
+ end
+ if type(a) == 'number' then
+ return a < b
+ else
+ return tostring(a) < tostring(b)
+ end
+ end)
+ local i = 0
+ return function()
+ i = i + 1
+ if keys[i] ~= nil then
+ return keys[i], unordered_tt[keys[i]]
+ end
+ end
+end
+
+function base_class.table(self, tab)
+ assert(type(tab) == 'table')
+ if self.done[tab] then
+ error('cyclic reference', 0)
+ end
+ self.done[tab] = true
+
+ local items = {'{'}
+ local previdx = 0
+ self:indent_inc()
+ for idx, val in ordered_iter(tab) do
+ local errors, valok, valexpr, valnote, idxok, idxexpr, idxnote
+ errors = {}
+ -- push current index onto key path stack to make it available to sub-printers
+ table.insert(self.tab_key_path, idx)
+
+ valok, valexpr, valnote = pcall(self.val2expr, self, val)
+ if not valok then
+ table.insert(errors, string.format('value: %s', valexpr))
+ end
+
+ local addidx
+ if previdx and type(idx) == 'number' and idx - 1 == previdx then
+ -- monotonic sequence, do not print key
+ previdx = idx
+ addidx = false
+ else
+ -- end of monotonic sequence
+ -- from now on print keys as well
+ previdx = nil
+ addidx = true
+ end
+
+ if addidx then
+ idxok, idxexpr, idxnote = pcall(self.val2expr, self, idx)
+ if not idxok or idxexpr == 'nil' then
+ table.insert(errors, string.format('key: not serializable', idxexpr))
+ end
+ end
+
+ local item = ''
+ if #errors == 0 then
+ -- finally serialize one [key=]?value expression
+ local indent = self:indent_head()
+ local note
+ if addidx then
+ note = self:format_note(idxnote, nil, self.key_val_sep)
+ item = string.format('%s%s[%s]%s=%s',
+ indent, note,
+ idxexpr, self.key_val_sep, self.key_val_sep)
+ indent = ''
+ end
+ note = self:format_note(valnote, nil, self.item_sep)
+ item = item .. string.format('%s%s%s,', indent, note, valexpr)
+ else
+ local errmsg = string.format('cannot print %s = %s (%s)',
+ self:string(tostring(idx)),
+ self:string(tostring(val)),
+ table.concat(errors, ', '))
+ if self.on_unrepresentable == 'error' then
+ error(errmsg, 0)
+ else
+ errmsg = string.format('--[[ missing %s ]]', errmsg)
+ item = errmsg
+ end
+ end
+ table.insert(items, item)
+ table.remove(self.tab_key_path) -- pop current index from key path stack
+ end -- one key+value
+ self:indent_dec()
+ table.insert(items, self:indent_head() .. '}')
+ return table.concat(items, self.item_sep), string.format('%s follows', tab)
+end
+
+-- machine readable variant, cannot represent all types and repeated references to a table
+local serializer_class = {
+ indent_step = 0,
+ item_sep = ' ',
+ key_val_sep = ' ',
+ __inst_mt = {}
+}
+-- inheritance form base class (for :new())
+setmetatable(serializer_class, { __index = base_class })
+-- class instances with following metatable inherit all class members
+serializer_class.__inst_mt.__index = serializer_class
+
+local function static_serializer(val, on_unrepresentable)
+ local inst = serializer_class:new(on_unrepresentable)
+ local expr, note = inst:val2expr(val)
+ return string.format('%s%s', inst:format_note(note, nil, inst.item_sep), expr)
+ end
+
+-- human friendly variant, not stable and not intended for machine consumption
+local pprinter_class = {
+ indent_step = 4,
+ item_sep = '\n',
+ key_val_sep = ' ',
+ __inst_mt = {},
+}
+
+-- should be always empty because pretty-printer has fallback for all types
+function pprinter_class.format_note()
+ return ''
+end
+
+function pprinter_class._fallback(self, val)
+ if self.on_unrepresentable == 'error' then
+ base_class._fallback(self, val)
+ end
+ return tostring(val)
+end
+
+function pprinter_class.char_is_printable(_, c)
+ -- ASCII (from space to ~) + tab or newline
+ -- and not ' or \
+ return ((c >= 0x20 and c < 0x7f)
+ or c == 0x09 or c == 0x0A)
+ and c ~= 0x27 and c ~= 0x5C
+end
+
+-- "function" is a Lua keyword so assignment below is workaround to create
+-- function pprinter_class.function(self, f)
+pprinter_class['function'] = function(self, f)
+-- thanks to AnandA777 from StackOverflow! Function funcsign is adapted version of
+-- https://stackoverflow.com/questions/51095022/inspect-function-signature-in-lua-5-1
+ assert(type(f) == 'function', "bad argument #1 to 'funcsign' (function expected)")
+ local debuginfo = debug.getinfo(f)
+ local func_args = {}
+ local args_str
+ if debuginfo.what == 'C' then -- names N/A
+ args_str = '(?)'
+ goto add_name
+ end
+
+ pcall(function()
+ local oldhook
+ local delay = 2
+ local function hook()
+ delay = delay - 1
+ if delay == 0 then -- call this only for the introspected function
+ -- stack depth 2 is the introspected function
+ for i = 1, debuginfo.nparams do
+ local k = debug.getlocal(2, i)
+ table.insert(func_args, k)
+ end
+ if debuginfo.isvararg then
+ table.insert(func_args, "...")
+ end
+ debug.sethook(oldhook)
+ error('aborting the call to introspected function')
+ end
+ end
+ oldhook = debug.sethook(hook, "c") -- invoke hook() on function call
+ f(unpack({})) -- huh?
+ end)
+ args_str = "(" .. table.concat(func_args, ", ") .. ")"
+ ::add_name::
+ local name
+ if #self.tab_key_path > 0 then
+ name = string.format('function %s', self.tab_key_path[#self.tab_key_path])
+ else
+ name = 'function '
+ end
+ return string.format('%s%s: %s', name, args_str, string.sub(tostring(f), 11))
+end
+
+-- default tostring method is better suited for human-intended output
+function pprinter_class.number(_, number)
+ return tostring(number)
+end
+
+local function deserialize_lua(serial)
+ assert(type(serial) == 'string')
+ local deserial_func = loadstring('return ' .. serial)
+ if type(deserial_func) ~= 'function' then
+ panic('input is not a valid Lua expression')
+ end
+ return deserial_func()
+end
+
+setmetatable(pprinter_class, { __index = base_class })
+pprinter_class.__inst_mt.__index = pprinter_class
+
+local function static_pprint(val, on_unrepresentable)
+ local inst = pprinter_class:new(on_unrepresentable)
+ local expr, note = inst:val2expr(val)
+ return string.format('%s%s', inst:format_note(note, nil, inst.item_sep), expr)
+end
+
+local M = {
+ serialize_lua = static_serializer,
+ deserialize_lua = deserialize_lua,
+ pprint = static_pprint
+}
+
+return M
diff --git a/daemon/lua/krprint.test.lua b/daemon/lua/krprint.test.lua
new file mode 100644
index 0000000..9218052
--- /dev/null
+++ b/daemon/lua/krprint.test.lua
@@ -0,0 +1,292 @@
+local serialize_lua = require('krprint').serialize_lua
+local deserialize_lua = require('krprint').deserialize_lua
+
+local function gen_string(maxlen)
+ maxlen = maxlen or 100
+ local len = math.random(0, maxlen)
+ local buf = {}
+ for _=1,len do
+ table.insert(buf, string.char(math.random(0, 255)))
+ end
+ return table.concat(buf)
+end
+
+local function test_de_serialization(orig_val, desc)
+ local serial = serialize_lua(orig_val)
+ ok(type(serial) == 'string' and #serial > 0,
+ 'serialization returns non-empty string: ' .. desc)
+ local deserial_val = deserialize_lua(serial)
+ same(type(orig_val), type(deserial_val),
+ 'deserialized value has the same type: ' .. desc)
+ if type(orig_val) == 'number' then
+ -- nan cannot be compared using == operator
+ if tostring(orig_val) == 'nan' and tostring(deserial_val) == 'nan' then
+ pass('nan value serialized and deserialized')
+ elseif orig_val ~= math.huge and orig_val ~= -math.huge then
+ -- tolerance measured experimentally on x86_64 LuaJIT 2.1.0-beta3
+ local tolerance = 1e-14
+ ok(math.abs(orig_val - deserial_val) <= tolerance,
+ 'deserialized number is within tolerance ' .. tolerance)
+ else
+ same(orig_val, deserial_val, 'deserialization returns the same infinity:' .. desc)
+ end
+ else
+ same(orig_val, deserial_val,
+ 'deserialization returns the same value: ' .. desc)
+ end
+end
+
+local function test_de_serialization_autodesc(orig_val)
+ test_de_serialization(orig_val, tostring(orig_val))
+end
+
+local function test_bool()
+ test_de_serialization_autodesc(true)
+ same('true', table_print(true), 'table_print handles true')
+ test_de_serialization_autodesc(false)
+ same('false', table_print(false), 'table_print handles false')
+end
+
+local function test_nil()
+ test_de_serialization_autodesc(nil)
+ same('nil', table_print(nil), 'table_print handles nil')
+end
+
+local function gen_number_int()
+ local number
+ -- make "small" numbers more likely so they actually happen
+ if math.random() < 0.5 then
+ number = math.random(-2^32, 2^32)
+ else
+ number = math.random(-2^48, 2^48)
+ end
+ return number
+end
+
+local function gen_number_float()
+ return math.random()
+end
+
+local function test_number()
+ test_de_serialization_autodesc(0)
+ same('0', table_print(0), 'table_print handles 0')
+ test_de_serialization_autodesc(-math.huge)
+ same('-inf', table_print(-math.huge), 'table_print handles -infinity')
+ test_de_serialization_autodesc(math.huge)
+ same('inf', table_print(math.huge), 'table_print handles +infinity')
+ test_de_serialization_autodesc(tonumber('nan'))
+ same('nan', table_print(tonumber('nan')), 'table_print handles nan')
+ for _=1,20 do -- integers
+ test_de_serialization_autodesc(gen_number_int())
+ -- bigger numbers might end up with non-exact representation
+ local smallnumber = math.random(-2^32, 2^32)
+ same(tostring(smallnumber), table_print(smallnumber),
+ 'table_print handles small numbers')
+ end
+ for _=1,20 do -- floats
+ local float = math.random()
+ same(tostring(float), table_print(float),
+ 'table_print handles floats')
+ test_de_serialization_autodesc(gen_number_float())
+ end
+end
+
+local function test_string()
+ test_de_serialization('', 'empty string')
+ for _=1,20 do
+ local str = gen_string(1024*10)
+ test_de_serialization(str, 'random string length ' .. #str)
+ end
+end
+
+local function gen_number()
+ -- pure random would not produce special cases often enough
+ local generators = {
+ function() return 0 end,
+ function() return -math.huge end,
+ function() return math.huge end,
+ gen_number_int,
+ gen_number_float,
+ }
+ return generators[math.random(1, #generators)]()
+end
+
+local function gen_boolean()
+ local options = {true, false}
+ return options[math.random(1, #options)]
+end
+
+local function gen_table_atomic()
+ -- nil keys or values are not allowed
+ -- nested tables are handled elsewhere
+ local supported_types = {
+ gen_number,
+ gen_string,
+ gen_boolean,
+ }
+ val = supported_types[math.random(1, #supported_types)]()
+ return val
+end
+
+local function gen_test_tables_supported(level)
+ level = level or 1
+ local max_level = 5
+ local max_items_per_table = 20
+ local t = {}
+ for _=1, math.random(0, max_items_per_table) do
+ local val_as_table = (level <= max_level) and math.random() < 0.1
+ local key, val
+ -- tapered.same method cannot compare keys with type table
+ key = gen_table_atomic()
+ if val_as_table then
+ val = gen_test_tables_supported(level + 1)
+ else
+ val = gen_table_atomic()
+ end
+ t[key] = val
+ end
+ return t
+end
+
+local marker = 'this string must be present somewhere in output'
+local function gen_marker()
+ return marker
+end
+
+local kluautil = require('kluautil')
+local function random_modify_table(t, always, generator)
+ assert(generator)
+ local tab_len = kluautil.kr_table_len(t)
+ local modified = false
+ -- modify some values
+ for key, val in pairs(t) do
+ if math.random(1, tab_len) == 1 then
+ if type(val) == 'table' then
+ modified = modified or random_modify_table(val, false, generator)
+ else
+ t[key] = generator()
+ modified = true
+ end
+ end
+ end
+ if always and not modified then
+ -- fallback, add an unsupported key
+ t[generator()] = true
+ modified = true
+ end
+ return modified
+end
+
+local function test_table_supported()
+ for i=1,10 do
+ local t = gen_test_tables_supported()
+ test_de_serialization(t, 'random table no. ' .. i)
+ assert(random_modify_table(t, true, gen_marker))
+ local str = table_print(t)
+ ok(string.find(str, marker, 1, true),
+ 'table_print works on complex serializable tables')
+ end
+end
+
+local ffi = require('ffi')
+local const_func = tostring
+local const_thread = coroutine.create(tostring)
+local const_userdata = ffi.C
+local const_cdata = ffi.new('int')
+
+local function gen_unsupported_atomic()
+ -- nested tables are handled elsewhere
+ local unsupported_types = {
+ const_func,
+ const_thread,
+ const_userdata,
+ const_cdata
+ }
+ val = unsupported_types[math.random(1, #unsupported_types)]
+ return val
+end
+
+local function test_unsupported(val, desc)
+ desc = desc or string.format('unsupported %s', type(val))
+ return function()
+ boom(serialize_lua, { val, 'error' }, string.format(
+ 'attempt to serialize %s in error mode '
+ .. 'causes error', desc))
+ local output = serialize_lua(val, 'comment')
+ same('string', type(output),
+ string.format('attempt to serialize %s in '
+ .. 'comment mode returned a string',
+ desc))
+ ok(string.find(output, '--', 1, true),
+ 'returned string contains a comment')
+ output = table_print(val)
+ same('string', type(output),
+ string.format('table_print can stringify %s', desc))
+ if type(val) ~= 'table' then
+ ok(string.find(output, type(val), 1, true),
+ 'exotic type is mentioned in table_print output')
+ end
+ end
+end
+
+local function gen_test_tables_unsupported()
+ local t = gen_test_tables_supported()
+ random_modify_table(t, true, gen_unsupported_atomic)
+ return t
+end
+
+local function test_unsupported_table()
+ for i=1,10 do
+ local t = gen_test_tables_unsupported()
+ test_unsupported(t, 'random unsupported table no. ' .. i)()
+ assert(random_modify_table(t, true, gen_marker))
+ local str = table_print(t)
+ ok(string.find(str, marker, 1, true),
+ 'table_print works on complex unserializable tables')
+ end
+end
+
+local function func_2vararg_5ret(arg1, arg2, ...)
+ return select('#', ...), nil, arg1 + arg2, false, nil
+end
+local function func_ret_nil() return nil end
+local function func_ret_nothing() return end
+
+local function test_pprint_func()
+ local t = { [false] = func_2vararg_5ret }
+ local output = table_print(t)
+ ok(string.find(output, 'function false(arg1, arg2, ...)', 1, true),
+ 'function parameters are pretty printed')
+end
+
+local function test_pprint_func_ret()
+ local output = table_print(func_2vararg_5ret(1, 2, 'bla'))
+ local exp = [[
+1 -- result # 1
+nil -- result # 2
+3 -- result # 3
+false -- result # 4
+nil -- result # 5]]
+ same(output, exp, 'multiple return values are pretty printed')
+
+ output = table_print(func_ret_nil())
+ same(output, 'nil', 'single return value does not have extra comments')
+
+ output = table_print(func_ret_nothing())
+ same(output, nil, 'no return values to be printed cause nil output')
+end
+
+return {
+ test_bool,
+ test_nil,
+ test_number,
+ test_string,
+ test_table_supported,
+ test_unsupported(const_func),
+ test_unsupported(const_thread),
+ test_unsupported(const_userdata),
+ test_unsupported(const_cdata),
+ test_unsupported_table,
+ test_pprint_func,
+ test_pprint_func_ret,
+}
diff --git a/daemon/lua/log.test.lua b/daemon/lua/log.test.lua
new file mode 100644
index 0000000..197aa74
--- /dev/null
+++ b/daemon/lua/log.test.lua
@@ -0,0 +1,42 @@
+local function test_log_level()
+ same(log_level(), 'notice', 'default level is notice')
+ same(verbose(), false, 'verbose is not set by default')
+ same(log_level('crit'), 'crit', '"crit" level can be set')
+ same(log_level('err'), 'err', '"err" level can be set')
+ same(log_level('warning'), 'warning', '"warning" level can be set')
+ same(log_level('notice'), 'notice', '"notice" level can be set')
+ same(log_level('info'), 'info', '"info" level can be set')
+ same(log_level('debug'), 'debug', '"debug" level can be set')
+ same(verbose(), true, 'verbose is active when debug level is set')
+ same(verbose(false), false, 'verbose can be used to turn off debug level')
+ same(log_level(), 'notice', 'verbose returns log level to notice')
+ boom(log_level, { 'xxx' }, "unknown level can't be used")
+ boom(log_level, { 7 }, "numbered levels aren't supported")
+ boom(log_level, { 1, 2 }, "level doesn't take multiple arguments")
+end
+
+local function test_log_target()
+ same(log_target(), 'stderr', 'default target is stderr')
+ same(log_target('stdout'), 'stdout', 'stdout target can be set')
+ same(log_target('syslog'), 'syslog', 'syslog target can be set')
+ same(log_target('stderr'), 'stderr', 'stderr target can be set')
+ boom(log_level, { 'xxx' }, "unknown target can't be used")
+ boom(log_level, { 'stderr', 'syslog' }, "target doesn't take multiple arguments")
+end
+
+local function test_log_groups()
+ same(log_groups(), {}, 'no groups are logged by default')
+ same(log_groups({'system'}), {'system'}, 'configure "system" group')
+ same(log_groups({'devel'}), {'devel'}, 'another call overrides previously set groups')
+ same(log_groups({'devel', 'system'}), {'system', 'devel'}, 'configure multiple groups')
+ same(log_groups({}), {}, 'clear groups with empty table')
+ boom(log_groups, { 'string' }, "group argument can't be string")
+ boom(log_groups, { {'nonexistent'} }, "nonexistent group can't be added")
+ boom(log_groups, { 1, 2 }, "group doesn't take multiple arguments")
+end
+
+return {
+ test_log_level,
+ test_log_target,
+ test_log_groups,
+}
diff --git a/daemon/lua/map.test.integr/deckard.yaml b/daemon/lua/map.test.integr/deckard.yaml
new file mode 100644
index 0000000..2fe920d
--- /dev/null
+++ b/daemon/lua/map.test.integr/deckard.yaml
@@ -0,0 +1,38 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+programs:
+- name: kresd3
+ binary: kresd
+ additional:
+ - --noninteractive
+ templates:
+ - daemon/lua/map.test.integr/kresd_config.j2
+ - tests/integration/hints_zone.j2
+ - tests/config/tapered/src/tapered.lua
+ configs:
+ - config
+ - hints
+ - tapered.lua
+- name: kresd2
+ binary: kresd
+ additional:
+ - --noninteractive
+ templates:
+ - daemon/lua/map.test.integr/kresd_config.j2
+ - tests/integration/hints_zone.j2
+ - tests/config/tapered/src/tapered.lua
+ configs:
+ - config
+ - hints
+ - tapered.lua
+- name: kresd1
+ binary: kresd
+ additional:
+ - --noninteractive
+ templates:
+ - daemon/lua/map.test.integr/kresd_config.j2
+ - tests/integration/hints_zone.j2
+ - tests/config/tapered/src/tapered.lua
+ configs:
+ - config
+ - hints
+ - tapered.lua
diff --git a/daemon/lua/map.test.integr/kresd_config.j2 b/daemon/lua/map.test.integr/kresd_config.j2
new file mode 100644
index 0000000..ae403c7
--- /dev/null
+++ b/daemon/lua/map.test.integr/kresd_config.j2
@@ -0,0 +1,193 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+local ffi = require('ffi')
+log_info(ffi.C.LOG_GRP_TESTS, 'my PID = %d', worker.pid)
+
+trust_anchors.remove('.')
+
+cache.size = 2*MB
+
+net = { '{{SELF_ADDR}}' }
+
+{% if QMIN == "false" %}
+option('NO_MINIMIZE', true)
+{% else %}
+option('NO_MINIMIZE', false)
+{% endif %}
+
+-- Self-checks on globals
+assert(help() ~= nil)
+assert(worker.id ~= nil)
+-- Self-checks on facilities
+assert(cache.count() == 0)
+assert(cache.stats() ~= nil)
+assert(cache.backends() ~= nil)
+assert(worker.stats() ~= nil)
+assert(net.interfaces() ~= nil)
+-- Self-checks on loaded stuff
+assert(#modules.list() > 0)
+-- Self-check timers
+ev = event.recurrent(1 * sec, function (ev) return 1 end)
+event.cancel(ev)
+
+local kluautil = require('kluautil')
+local tap = require('tapered')
+local checks_total = 16
+local n_instances = 3 -- must match deckard.yaml
+
+worker.control_path = worker.cwd .. '/../kresd3/control/'
+net.listen(worker.control_path .. worker.pid, nil, {kind = 'control'})
+assert(#net.list() >= 3) -- UDP, TCP, control
+
+-- debug, kept for future use
+--log_level("debug")
+log_debug(ffi.C.LOG_GRP_TESTS, '%s', worker.control_path)
+log_debug(ffi.C.LOG_GRP_TESTS, '%s', table_print(net.list()))
+
+function wait_for_sockets()
+ log_info(ffi.C.LOG_GRP_TESTS, 'waiting for control sockets')
+ local timeout = 5000 -- ms
+ local start_time = tonumber(ffi.C.kr_now())
+ local now
+ while true do
+ now = tonumber(ffi.C.kr_now())
+ if now > start_time + timeout then
+ log_info(ffi.C.LOG_GRP_TESTS, 'timeout while waiting for control sockets to appear')
+ os.exit(3)
+ end
+ local pids = kluautil.list_dir(worker.control_path)
+ if #pids == n_instances then
+ -- debug, kept for future use
+ log_debug(ffi.C.LOG_GRP_TESTS, 'got control sockets:')
+ log_debug(ffi.C.LOG_GRP_TESTS, table_print(pids))
+ break
+ else
+ worker.sleep(0.1)
+ end
+ end
+ log_info(ffi.C.LOG_GRP_TESTS, 'PIDs are visible now (waiting took %d ms)', now - start_time)
+end
+
+-- expression should throw Lua error:
+-- wrap it in a function which runs the expression on leader and follower
+-- separately so we can guarantee both cases are covered
+function boom_follower_and_leader(boom_expr, desc)
+ local variants = {leader = '~=', follower = '=='}
+ for name, operator in pairs(variants) do
+ -- beware, newline is not allowed in expr
+ local full_expr = string.format(
+ 'if (worker.pid %s %s) then return true '
+ .. 'else return %s end',
+ operator, worker.pid, boom_expr)
+ local full_desc = name .. ': '
+ if desc then
+ full_desc = full_desc .. desc .. ' (' .. boom_expr .. ')'
+ else
+ full_desc = full_desc .. boom_expr
+ end
+ tap.boom(map, {full_expr}, full_desc)
+ end
+end
+
+function tests()
+ -- add delay to each test to force scheduler to interleave tests and DNS queries
+ local test_delay = 20 / 1000 -- seconds
+ log_info(ffi.C.LOG_GRP_TESTS, 'starting map() tests now')
+
+ tap.boom(map, {'1 ++ 1'}, 'syntax error in command is detected')
+ worker.sleep(test_delay)
+
+ -- array of integers
+ local pids = map('worker.pid')
+ tap.same(pids.n, n_instances, 'all pids were obtained')
+ table.sort(pids)
+ worker.sleep(test_delay)
+
+ -- expression produces array of integers
+ local pids_plus_one = map('worker.pid + 1')
+ tap.same(pids_plus_one.n, n_instances, 'all pids were obtained')
+ table.sort(pids_plus_one)
+ for idx=1,n_instances do
+ tap.same(pids[idx] + 1, pids_plus_one[idx],
+ 'increment expression worked')
+ end
+ worker.sleep(test_delay)
+
+ -- error detection
+ boom_follower_and_leader('error("explosion")')
+ worker.sleep(test_delay)
+
+ -- unsupported number of return values
+ boom_follower_and_leader('1, 2')
+ worker.sleep(test_delay)
+ boom_follower_and_leader('unpack({})')
+ worker.sleep(test_delay)
+
+ -- unsupported return type
+ boom_follower_and_leader(
+ 'function() print("this cannot be serialized") end')
+ worker.sleep(test_delay)
+
+ tap.same({n = n_instances}, map('nil'),
+ 'nil values are counted as returned')
+ worker.sleep(test_delay)
+
+ local exp = {n = n_instances}
+ for i=1,n_instances do
+ table.insert(exp, {nil, 2, nil, n=3})
+ end
+ local got = map('require("kluautil").kr_table_pack(nil, 2, nil)')
+ tap.same(got, exp, 'kr_table_pack handles nil values')
+ worker.sleep(test_delay)
+end
+
+local started = false
+function tests_start()
+ -- just in case, duplicates should not happen
+ if started then
+ log_info(ffi.C.LOG_GRP_TESTS, 'huh? duplicate test invocation ignored, a retransmit?')
+ return
+ end
+ started = true
+ log_info(ffi.C.LOG_GRP_TESTS, 'start query triggered, scheduling tests')
+
+ -- DNS queries and map() commands must be serviced while sleep is running
+ worker.coroutine(function() worker.sleep(3600) end)
+
+ worker.coroutine(tests)
+end
+-- Deckard query will trigger tests
+policy.add(policy.suffix(tests_start, {'\5start\0'}))
+
+function tests_done()
+ print('final query triggered')
+ event.after(0, function()
+ tap.done(checks_total)
+ end)
+end
+-- Deckard query will execute tap.done() which will call os.exit()
+-- i.e. this callback has to be called only after answer to Deckard was sent
+policy.add(policy.suffix(tests_done, {'\4done\0'}), true)
+
+-- add delay to each query to force scheduler to interleave tests and DNS queries
+policy.add(policy.all(
+ function()
+ local delay = 10 -- ms
+ log_info(ffi.C.LOG_GRP_TESTS, 'packet delayed by %d ms', delay)
+ worker.sleep(delay / 1000)
+ end))
+
+wait_for_sockets()
+
+{% if DAEMON_NAME == "kresd1" %}
+
+-- forward to Deckard test server
+policy.add(policy.all(policy.FORWARD('192.0.2.1')))
+
+{% else %}
+
+-- forward to next kresd instance in chain
+{# find out IP address of kresd instance with lower number,
+ i.e. kresd2 forwards to kresd1 #}
+policy.add(policy.all(policy.FORWARD('{{ PROGRAMS[ "kresd" ~ (DAEMON_NAME[-1]|int() - 1)]["address"] }}')))
+
+{% endif %}
diff --git a/daemon/lua/map.test.integr/query-while-map-is-running.rpl b/daemon/lua/map.test.integr/query-while-map-is-running.rpl
new file mode 100644
index 0000000..8590fc8
--- /dev/null
+++ b/daemon/lua/map.test.integr/query-while-map-is-running.rpl
@@ -0,0 +1,312 @@
+; does not make any practical difference so we limit ourselves to single test run
+query-minimization: off
+CONFIG_END
+
+SCENARIO_BEGIN Empty answers to any query - forwarding without validation
+
+; forwarding target
+RANGE_BEGIN 1 1000000
+ ADDRESS 192.0.2.1
+
+; NODATA to everything
+ENTRY_BEGIN
+MATCH opcode
+ADJUST copy_id copy_query
+REPLY NOERROR QR
+SECTION QUESTION
+. IN SOA
+SECTION ANSWER
+. 86400 IN SOA rootns. you.test. 2017071100 1800 900 604800 86400
+ENTRY_END
+RANGE_END
+
+STEP 10 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+start. IN TXT
+ENTRY_END
+
+STEP 11 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+start. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+
+STEP 1001 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1001. IN TXT
+ENTRY_END
+
+STEP 1002 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1001. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1003 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1003. IN TXT
+ENTRY_END
+
+STEP 1004 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1003. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1005 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1005. IN TXT
+ENTRY_END
+
+STEP 1006 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1005. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1007 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1007. IN TXT
+ENTRY_END
+
+STEP 1008 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1007. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1009 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1009. IN TXT
+ENTRY_END
+
+STEP 1010 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1009. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1011 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1011. IN TXT
+ENTRY_END
+
+STEP 1012 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1011. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1013 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1013. IN TXT
+ENTRY_END
+
+STEP 1014 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1013. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1015 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1015. IN TXT
+ENTRY_END
+
+STEP 1016 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1015. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1017 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1017. IN TXT
+ENTRY_END
+
+STEP 1018 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1017. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1019 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1019. IN TXT
+ENTRY_END
+
+STEP 1020 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1019. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1021 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1021. IN TXT
+ENTRY_END
+
+STEP 1022 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1021. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1023 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1023. IN TXT
+ENTRY_END
+
+STEP 1024 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1023. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1025 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1025. IN TXT
+ENTRY_END
+
+STEP 1026 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1025. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1027 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1027. IN TXT
+ENTRY_END
+
+STEP 1028 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1027. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1029 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1029. IN TXT
+ENTRY_END
+
+STEP 1030 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1029. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1031 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+test1031. IN TXT
+ENTRY_END
+
+STEP 1032 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+test1031. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+STEP 1033 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+done. IN TXT
+ENTRY_END
+
+STEP 1034 CHECK_ANSWER
+ENTRY_BEGIN
+REPLY NOERROR QR RD RA
+MATCH opcode rcode flags question answer
+SECTION QUESTION
+done. IN TXT
+SECTION ANSWER
+ENTRY_END
+
+SCENARIO_END
diff --git a/daemon/lua/meson.build b/daemon/lua/meson.build
new file mode 100644
index 0000000..b19777c
--- /dev/null
+++ b/daemon/lua/meson.build
@@ -0,0 +1,118 @@
+# daemon: lua modules
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+config_tests += [
+ ['controlsock', files('controlsock.test.lua')],
+ ['krprint', files('krprint.test.lua')],
+ ['log', files('log.test.lua')],
+ ['ta', files('trust_anchors.test/ta.test.lua')],
+ ['ta_bootstrap', files('trust_anchors.test/bootstrap.test.lua'), ['y2k38']],
+]
+
+integr_tests += [
+ ['map', meson.current_source_dir() / 'map.test.integr'],
+]
+
+lua_config = configuration_data()
+lua_config.set('keyfile_default', keyfile_default)
+lua_config.set('etc_dir', etc_dir)
+lua_config.set('run_dir', run_dir)
+lua_config.set('systemd_cache_dir', systemd_cache_dir)
+lua_config.set('unmanaged', managed_ta ? 'false' : 'true')
+
+trust_anchors = configure_file(
+ input: 'trust_anchors.lua.in',
+ output: 'trust_anchors.lua',
+ configuration: lua_config,
+)
+
+sandbox = configure_file(
+ input: 'sandbox.lua.in',
+ output: 'sandbox.lua',
+ configuration: lua_config,
+)
+
+distro_preconfig = configure_file(
+ input: 'distro-preconfig.lua.in',
+ output: 'distro-preconfig.lua',
+ configuration: lua_config,
+)
+
+# Unfortunately the different ABI implies different contents of 'kres-gen.lua'.
+if libknot.version().version_compare('>= 3.2')
+ kres_gen_fname = 'kres-gen-32.lua'
+elif libknot.version().version_compare('>= 3.1')
+ kres_gen_fname = 'kres-gen-31.lua'
+else
+ kres_gen_fname = 'kres-gen-30.lua'
+endif
+
+kres_gen_lua = configure_file(
+ input: kres_gen_fname,
+ output: 'kres-gen.lua',
+ copy: true,
+)
+
+run_target( # run manually to re-generate kres-gen.lua
+ 'kres-gen',
+ command: [ find_program('./kres-gen.sh'), kres_gen_fname ],
+)
+
+# A simple config test: check that sizes of some structures match
+# in C and pre-generated lua bindings.
+# The point is that regeneration is quite expensive in time and dependencies,
+# but this basic sanity check could be ran always, except for cross compilation,
+# as we *run* luajit to find out the real sizes.
+if get_option('kres_gen_test') and not meson.is_cross_build()
+ types_to_check = [
+ { 'tname': 'time_t', 'incl': '#include <sys/time.h>' },
+ { 'tname': 'struct timeval', 'incl' : '#include <sys/time.h>' },
+ { 'tname': 'zs_scanner_t', 'incl': '#include <libzscanner/scanner.h>', 'dep': libzscanner },
+ { 'tname': 'knot_pkt_t', 'incl' : '#include <libknot/packet/pkt.h>', 'dep': libknot },
+ ]
+ # Construct the lua tester as a meson string.
+ kres_gen_test_luastr = '''
+ dofile('@0@')
+ local ffi = require('ffi')
+ '''.format(meson.current_source_dir() / kres_gen_fname)
+ foreach ttc: types_to_check
+ # We're careful with adding just includes; otherwise it's more fragile (e.g. linking flags).
+ if 'dep' in ttc
+ dep = ttc.get('dep').partial_dependency(includes: true, compile_args: true)
+ else
+ dep = []
+ endif
+ tsize = meson.get_compiler('c').sizeof(ttc.get('tname'), prefix: ttc.get('incl'),
+ dependencies: dep)
+ kres_gen_test_luastr += '''
+ assert(ffi.sizeof(ffi.typeof('@0@')) == @1@,
+ 'Lua binding for C type ' .. '@0@' .. ' has incorrect size: '
+ .. ffi.sizeof(ffi.typeof('@0@'))
+ )
+ '''.format(ttc.get('tname'), tsize)
+ endforeach
+ # Now feed it directly into luajit.
+ kres_gen_test = run_command(find_program('luajit'), '-e', kres_gen_test_luastr, check: false)
+ if kres_gen_test.returncode() != 0
+ error('if you use released Knot* versions, please contact us: https://www.knot-resolver.cz/contact/\n'
+ + kres_gen_test.stderr().strip())
+ endif
+endif
+
+lua_src = [
+ files('postconfig.lua'),
+ files('kres.lua'),
+ kres_gen_lua,
+ sandbox,
+ trust_anchors,
+ files('zonefile.lua'),
+ files('kluautil.lua'),
+ files('krprint.lua'),
+ distro_preconfig,
+]
+
+# install daemon lua sources
+install_data(
+ lua_src,
+ install_dir: lib_dir,
+)
diff --git a/daemon/lua/postconfig.lua b/daemon/lua/postconfig.lua
new file mode 100644
index 0000000..ac71660
--- /dev/null
+++ b/daemon/lua/postconfig.lua
@@ -0,0 +1,70 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+local ffi = require('ffi')
+local C = ffi.C
+
+local function count_sockets()
+ local dns_socks = 0
+ local control_socks = 0
+ for _, socket in ipairs(net.list()) do
+ if socket.kind == 'control' then
+ control_socks = control_socks + 1
+ elseif (socket.kind == 'dns' or
+ socket.kind == 'xdp' or
+ socket.kind == 'tls' or
+ socket.kind == 'doh_legacy' or
+ socket.kind == 'doh2') then
+ dns_socks = dns_socks + 1
+ end
+ end
+ return dns_socks, control_socks
+end
+
+local n_dns_socks, n_control_socks = count_sockets()
+
+-- Check and set control sockets path
+worker.control_path = worker.control_path or (worker.cwd .. '/control/')
+
+-- Bind to control socket by default
+if n_control_socks == 0 and not env.KRESD_NO_LISTEN then
+ local path = worker.control_path..worker.pid
+ local ok, err = pcall(net.listen, path, nil, { kind = 'control' })
+ if not ok then
+ log_warn(C.LOG_GRP_NETWORK, 'bind to '..path..' failed '..err)
+ end
+end
+
+-- Listen on localhost
+if n_dns_socks == 0 and not env.KRESD_NO_LISTEN then
+ local ok, err = pcall(net.listen, '127.0.0.1')
+ if not ok then
+ error('bind to 127.0.0.1@53 '..err)
+ end
+ -- Binding to other ifaces may fail
+ ok, err = pcall(net.listen, '127.0.0.1', 853)
+ if not ok then
+ log_info(ffi.C.LOG_GRP_NETWORK, 'bind to 127.0.0.1@853 '..err)
+ end
+ ok, err = pcall(net.listen, '::1')
+ if not ok then
+ log_info(ffi.C.LOG_GRP_NETWORK, 'bind to ::1@53 '..err)
+ end
+ ok, err = pcall(net.listen, '::1', 853)
+ if not ok then
+ log_info(ffi.C.LOG_GRP_NETWORK, 'bind to ::1@853 '..err)
+ end
+ -- Exit when kresd isn't listening on any interfaces
+ n_dns_socks, _ = count_sockets()
+ if n_dns_socks == 0 then
+ panic('not listening on any interface, exiting...')
+ end
+end
+-- Open cache if not set/disabled
+if not cache.current_size then
+ cache.size = 100 * MB
+end
+
+-- If no addresses for root servers are set, load them from the default file
+if C.kr_zonecut_is_empty(kres.context().root_hints) then
+ _hint_root_file()
+end
diff --git a/daemon/lua/sandbox.lua.in b/daemon/lua/sandbox.lua.in
new file mode 100644
index 0000000..7c6a818
--- /dev/null
+++ b/daemon/lua/sandbox.lua.in
@@ -0,0 +1,833 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+local debug = require('debug')
+local ffi = require('ffi')
+local kluautil = require('kluautil')
+local krprint = require("krprint")
+
+-- Units
+kB = 1024
+MB = 1024*kB
+GB = 1024*MB
+-- Time
+sec = 1000
+second = sec
+minute = 60 * sec
+min = minute
+hour = 60 * minute
+day = 24 * hour
+
+-- Logging
+
+-- from syslog.h
+LOG_CRIT = 2
+LOG_ERR = 3
+LOG_WARNING = 4
+LOG_NOTICE = 5
+LOG_INFO = 6
+LOG_DEBUG = 7
+
+local function curr_file() return debug.getinfo(4,'S').source end
+local function curr_line() return debug.getinfo(4,'l').currentline end
+
+local function log_fmt(grp, level, fmt, ...)
+ ffi.C.kr_log_fmt(grp, level,
+ 'CODE_FILE='..curr_file(), 'CODE_LINE='..curr_line(), 'CODE_FUNC=',
+ '[%-6s] %s\n', ffi.C.kr_log_grp2name(grp), string.format(fmt, ...))
+end
+
+function log_req(req, qry_uid, indent, grp, fmt, ...)
+ ffi.C.kr_log_req1(req, qry_uid, indent, grp, ffi.C.kr_log_grp2name(grp),
+ '%s\n', string.format(fmt, ...))
+end
+
+function log_qry(qry, grp, fmt, ...)
+ ffi.C.kr_log_q1(qry, grp, ffi.C.kr_log_grp2name(grp),
+ '%s\n', string.format(fmt, ...))
+end
+
+function panic(fmt, ...)
+ print(debug.traceback('error occurred here (config filename:lineno is '
+ .. 'at the bottom, if config is involved):', 2))
+ error(string.format('ERROR: '.. fmt, ...), 0)
+end
+
+function log_error(grp, fmt, ...)
+ log_fmt(grp, LOG_ERR, fmt, ...)
+end
+
+function log_warn(grp, fmt, ...)
+ log_fmt(grp, LOG_WARNING, fmt, ...)
+end
+
+function log_notice(grp, fmt, ...)
+ log_fmt(grp, LOG_NOTICE, fmt, ...)
+end
+
+function log_info(grp, fmt, ...)
+ log_fmt(grp, LOG_INFO, fmt, ...)
+end
+
+function log_debug(grp, fmt, ...)
+ log_fmt(grp, LOG_DEBUG, fmt, ...)
+end
+
+function log(fmt, ...)
+ log_notice(ffi.C.LOG_GRP_MODULE, fmt, ...)
+end
+
+-- Resolver bindings
+kres = require('kres')
+if rawget(kres, 'str2dname') ~= nil then
+ todname = kres.str2dname
+end
+
+worker.resolve_pkt = function (pkt, options, finish, init)
+ options = kres.mk_qflags(options)
+ local task = ffi.C.worker_resolve_start(pkt, options)
+
+ -- Deal with finish and init callbacks
+ if finish ~= nil then
+ local finish_cb
+ finish_cb = ffi.cast('trace_callback_f',
+ function (req)
+ jit.off(true, true) -- JIT for (C -> lua)^2 nesting isn't allowed
+ finish(req.answer, req)
+ finish_cb:free()
+ end)
+ task.ctx.req.trace_finish = finish_cb
+ end
+ if init ~= nil then
+ init(task.ctx.req)
+ end
+
+ return ffi.C.worker_resolve_exec(task, pkt) == 0
+end
+
+worker.resolve = function (qname, qtype, qclass, options, finish, init)
+ -- Alternatively use named arguments
+ if type(qname) == 'table' then
+ local t = qname
+ qname = t.name
+ qtype = t.type
+ qclass = t.class
+ options = t.options
+ finish = t.finish
+ init = t.init
+ end
+ qtype = qtype or kres.type.A
+ qclass = qclass or kres.class.IN
+ options = kres.mk_qflags(options)
+ -- LATER: nicer errors for rubbish in qname, qtype, qclass?
+ local pkt = ffi.C.worker_resolve_mk_pkt(qname, qtype, qclass, options)
+ if pkt == nil then
+ panic('failure in worker.resolve(); probably invalid qname "%s"', qname)
+ end
+ local ret = worker.resolve_pkt(pkt, options, finish, init)
+ ffi.C.knot_pkt_free(pkt);
+ return ret
+end
+resolve = worker.resolve
+
+-- Shorthand for aggregated per-worker information
+worker.info = function ()
+ local t = worker.stats()
+ t.pid = worker.pid
+ return t
+end
+
+-- Resolver mode of operation
+local current_mode = 'normal'
+local mode_table = { normal=0, strict=1, permissive=2 }
+function mode(m)
+ if not m then return current_mode end
+ if not mode_table[m] then error('unsupported mode: '..m) end
+ -- Update current operation mode
+ current_mode = m
+ option('STRICT', current_mode == 'strict')
+ option('PERMISSIVE', current_mode == 'permissive')
+ return true
+end
+
+-- Trivial option alias
+function reorder_RR(val)
+ return option('REORDER_RR', val)
+end
+
+-- Get/set resolver options via name (string)
+function option(name, val)
+ local flags = kres.context().options;
+ -- Note: no way to test existence of flags[name] but we want error anyway.
+ name = string.upper(name) -- convenience
+ if val ~= nil then
+ if (val ~= true) and (val ~= false) then
+ panic('invalid option value: ' .. tostring(val))
+ end
+ flags[name] = val;
+ end
+ return flags[name];
+end
+
+-- Function aliases
+-- `env.VAR returns os.getenv(VAR)`
+env = {}
+setmetatable(env, {
+ __index = function (_, k) return os.getenv(k) end
+})
+
+debugging = {}
+setmetatable(debugging, {
+ __index = function(_, k)
+ if k == 'assertion_abort' then return ffi.C.kr_dbg_assertion_abort
+ elseif k == 'assertion_fork' then return ffi.C.kr_dbg_assertion_fork
+ else panic('invalid debugging option: ' .. tostring(k))
+ end
+ end,
+ __newindex = function(_, k, v)
+ if k == 'assertion_abort' then ffi.C.kr_dbg_assertion_abort = v
+ elseif k == 'assertion_fork' then ffi.C.kr_dbg_assertion_fork = v
+ else panic('invalid debugging option: ' .. tostring(k))
+ end
+ end
+})
+
+-- Quick access to interfaces
+-- `net.<iface>` => `net.interfaces()[iface]`
+-- `net = {addr1, ..}` => `net.listen(name, addr1)`
+-- `net.ipv{4,6} = {true, false}` => enable/disable IPv{4,6}
+setmetatable(net, {
+ __index = function (t, k)
+ local v = rawget(t, k)
+ if v then return v
+ elseif k == 'ipv6' then return not option('NO_IPV6')
+ elseif k == 'ipv4' then return not option('NO_IPV4')
+ else return net.interfaces()[k]
+ end
+ end,
+ __newindex = function (t,k,v)
+ if k == 'ipv6' then return option('NO_IPV6', not v)
+ elseif k == 'ipv4' then return option('NO_IPV4', not v)
+ else
+ local iname = rawget(net.interfaces(), v)
+ if iname then t.listen(iname)
+ else t.listen(v)
+ end
+ end
+ end
+})
+
+-- Syntactic sugar for module loading
+-- `modules.<name> = <config>`
+setmetatable(modules, {
+ __newindex = function (_, k, v)
+ if type(k) == 'number' then
+ k, v = v, nil
+ end
+ if not rawget(_G, k) then
+ modules.load(k)
+ k = string.match(k, '[%w_]+')
+ local mod = _G[k]
+ local config = mod and rawget(mod, 'config')
+ if mod ~= nil and config ~= nil then
+ if k ~= v then config(v)
+ else config()
+ end
+ end
+ end
+ end
+})
+
+-- Set up lua table for a C module. (Internal function.)
+function modules_create_table_for_c(kr_module_ud)
+ local kr_module = ffi.cast('struct kr_module **', kr_module_ud)[0]
+ --- Set up the global table named according to the module.
+ if kr_module.config == nil and kr_module.props == nil then
+ return
+ end
+ local module = {}
+ local module_name = ffi.string(kr_module.name)
+ _G[module_name] = module
+
+ --- Construct lua functions for properties.
+ if kr_module.props ~= nil then
+ local i = 0
+ while true do
+ local prop = kr_module.props[i]
+ local cb = prop.cb
+ if cb == nil then break; end
+ module[ffi.string(prop.name)] =
+ function (arg) -- lua wrapper around kr_prop_cb function typedef
+ local arg_conv
+ if type(arg) == 'table' or type(arg) == 'boolean' then
+ arg_conv = tojson(arg)
+ elseif arg ~= nil then
+ arg_conv = tostring(arg)
+ end
+ local ret_cstr = cb(ffi.C.the_worker.engine, kr_module, arg_conv)
+ if ret_cstr == nil then
+ return nil
+ end
+ -- LATER(optim.): superfluous copying
+ local ret_str = ffi.string(ret_cstr)
+ -- This is a bit ugly, but the API is that invalid JSON
+ -- should be just returned as string :-(
+ local status, ret = pcall(fromjson, ret_str)
+ if not status then ret = ret_str end
+ ffi.C.free(ret_cstr)
+ return ret
+ end
+ i = i + 1
+ end
+ end
+
+ --- Construct lua function for config().
+ if kr_module.config ~= nil then
+ module.config =
+ function (arg)
+ local arg_conv
+ if type(arg) == 'table' or type(arg) == 'boolean' then
+ arg_conv = tojson(arg)
+ elseif arg ~= nil then
+ arg_conv = tostring(arg)
+ end
+ return kr_module.config(kr_module, arg_conv)
+ end
+ end
+
+ --- Add syntactic sugar for get() and set() properties.
+ --- That also "catches" any commands like `moduleName.foo = bar`.
+ local m_index, m_newindex
+ local get_f = rawget(module, 'get')
+ if get_f ~= nil then
+ m_index = function (_, key)
+ return get_f(key)
+ end
+ else
+ m_index = function ()
+ error('module ' .. module_name .. ' does not support indexing syntax sugar')
+ end
+ end
+ local set_f = rawget(module, 'set')
+ if set_f ~= nil then
+ m_newindex = function (_, key, value)
+ -- This will produce a nasty error on some non-string parameters.
+ -- Still, we already use it with integer values, e.g. in predict module :-/
+ return set_f(key .. ' ' .. value)
+ end
+ else
+ m_newindex = function ()
+ error('module ' .. module_name .. ' does not support assignment syntax sugar')
+ end
+ end
+ setmetatable(module, {
+ -- note: the two functions only get called for *missing* indices
+ __index = m_index,
+ __newindex = m_newindex,
+ })
+end
+
+local layer_ctx = ffi.C.kr_layer_t_static
+-- Utilities internal for lua layer glue; see ../ffimodule.c
+modules_ffi_layer_wrap1 = function (layer_cb)
+ return layer_cb(layer_ctx.state, layer_ctx.req)
+end
+modules_ffi_layer_wrap2 = function (layer_cb)
+ return layer_cb(layer_ctx.state, layer_ctx.req, layer_ctx.pkt)
+end
+modules_ffi_layer_wrap_checkout = function (layer_cb)
+ return layer_cb(layer_ctx.state, layer_ctx.req, layer_ctx.pkt,
+ layer_ctx.dst, layer_ctx.is_stream)
+end
+modules_ffi_wrap_modcb = function (cb, kr_module_ud) -- this one isn't for layer
+ local kr_module = ffi.cast('struct kr_module **', kr_module_ud)[0]
+ return cb(kr_module)
+end
+
+-- Return filesystem size where the cache resides.
+cache.fssize = function ()
+ local path = cache.current_storage or '.'
+ -- As it is now, `path` may or may not include the lmdb:// prefix.
+ if string.sub(path, 1, 7) == 'lmdb://' then
+ path = string.sub(path, 8)
+ end
+ if #path == 0 then
+ path = '.'
+ end
+ local size = tonumber(ffi.C.kr_fssize(path))
+ if size < 0 then
+ panic('cache.fssize(): %s', ffi.string(ffi.C.knot_strerror(size)))
+ else
+ return size
+ end
+end
+
+cache.clear = function (name, exact_name, rr_type, chunk_size, callback, prev_state)
+ if name == nil or (name == '.' and not exact_name) then
+ -- keep same output format as for 'standard' clear
+ local total_count = cache.count()
+ if not cache.clear_everything() then
+ error('unable to clear everything')
+ end
+ return {count = total_count}
+ end
+ -- Check parameters, in order, and set defaults if missing.
+ local dname = kres.str2dname(name)
+ if not dname then error('cache.clear(): incorrect name passed') end
+ if exact_name == nil then exact_name = false end
+ if type(exact_name) ~= 'boolean'
+ then error('cache.clear(): incorrect exact_name passed') end
+
+ local cach = kres.context().cache;
+ local rettable = {}
+ -- Apex warning. If the caller passes a custom callback,
+ -- we assume they are advanced enough not to need the check.
+ -- The point is to avoid repeating the check in each callback iteration.
+ if callback == nil then
+ local apex_array = ffi.new('knot_dname_t *[1]') -- C: dname **apex_array
+ local ret = ffi.C.kr_cache_closest_apex(cach, dname, false, apex_array)
+ if ret < 0 then
+ error(ffi.string(ffi.C.knot_strerror(ret))) end
+ if not ffi.C.knot_dname_is_equal(apex_array[0], dname) then
+ local apex_str = kres.dname2str(apex_array[0])
+ rettable.not_apex = 'to clear proofs of non-existence call '
+ .. 'cache.clear(\'' .. tostring(apex_str) ..'\')'
+ rettable.subtree = apex_str
+ end
+ ffi.C.free(apex_array[0])
+ end
+
+ if rr_type ~= nil then
+ -- Special case, without any subtree searching.
+ if not exact_name
+ then error('cache.clear(): specifying rr_type only supported with exact_name') end
+ if chunk_size or callback
+ then error('cache.clear(): chunk_size and callback parameters not supported with rr_type') end
+ local ret = ffi.C.kr_cache_remove(cach, dname, rr_type)
+ if ret < 0 then error(ffi.string(ffi.C.knot_strerror(ret))) end
+ return {count = 1}
+ end
+
+ if chunk_size == nil then chunk_size = 100 end
+ if type(chunk_size) ~= 'number' or chunk_size <= 0
+ then error('cache.clear(): chunk_size has to be a positive integer') end
+
+ -- Do the C call, and add chunk_size warning.
+ rettable.count = ffi.C.kr_cache_remove_subtree(cach, dname, exact_name, chunk_size)
+ if rettable.count == chunk_size then
+ local msg_extra = ''
+ if callback == nil then
+ msg_extra = '; the default callback will continue asynchronously'
+ end
+ rettable.chunk_limit = 'chunk size limit reached' .. msg_extra
+ end
+
+ -- Default callback function: repeat after 1ms
+ if callback == nil then callback =
+ function (cbname, cbexact_name, cbrr_type, cbchunk_size, cbself, cbprev_state, cbrettable)
+ if cbrettable.count < 0 then error(ffi.string(ffi.C.knot_strerror(cbrettable.count))) end
+ if cbprev_state == nil then cbprev_state = { round = 0 } end
+ if type(cbprev_state) ~= 'table'
+ then error('cache.clear() callback: incorrect prev_state passed') end
+ cbrettable.round = cbprev_state.round + 1
+ if (cbrettable.count == cbchunk_size) then
+ event.after(1, function ()
+ cache.clear(cbname, cbexact_name, cbrr_type, cbchunk_size, cbself, cbrettable)
+ end)
+ elseif cbrettable.round > 1 then
+ log_info(ffi.C.LOG_GRP_CACHE, 'asynchronous cache.clear(\'' .. cbname .. '\', '
+ .. tostring(cbexact_name) .. ') finished')
+ end
+ return cbrettable
+ end
+ end
+ return callback(name, exact_name, rr_type, chunk_size, callback, prev_state, rettable)
+end
+-- Syntactic sugar for cache
+-- `cache[x] -> cache.get(x)`
+-- `cache.{size|storage} = value`
+setmetatable(cache, {
+ __index = function (t, k)
+ local res = rawget(t, k)
+ if not res and not rawget(t, 'current_size') then return res end
+ -- Beware: t.get returns empty table on failure to find.
+ -- That would be confusing here (breaking kresc), so return nil instead.
+ res = t.get(k)
+ if res and next(res) ~= nil then return res else return nil end
+ end,
+ __newindex = function (t,k,v)
+ -- Defaults
+ local storage = rawget(t, 'current_storage')
+ if not storage then storage = 'lmdb://' end
+ local size = rawget(t, 'current_size')
+ if not size then size = 10*MB end
+ -- Declarative interface for cache
+ if k == 'size' then t.open(v, storage)
+ elseif k == 'storage' then t.open(size, v) end
+ end
+})
+
+-- Make sandboxed environment
+local function make_sandbox(defined)
+ local __protected = {
+ worker = true, env = true, debugging = true, modules = true,
+ cache = true, net = true, trust_anchors = true
+ }
+
+ -- Compute and export the list of top-level names (hidden otherwise)
+ local nl = ""
+ for n in pairs(defined) do
+ nl = nl .. n .. "\n"
+ end
+
+ return setmetatable({ __orig_name_list = nl }, {
+ __index = defined,
+ __newindex = function (_, k, v)
+ if __protected[k] then
+ for k2,v2 in pairs(v) do
+ defined[k][k2] = v2
+ end
+ else
+ defined[k] = v
+ end
+ end
+ })
+end
+
+-- Compatibility sandbox
+_G = make_sandbox(getfenv(0))
+setfenv(0, _G)
+
+-- Load default modules
+trust_anchors = require('trust_anchors')
+modules.load('ta_update')
+modules.load('ta_signal_query')
+modules.load('policy')
+modules.load('priming')
+modules.load('detect_time_skew')
+modules.load('detect_time_jump')
+modules.load('ta_sentinel')
+modules.load('edns_keepalive')
+modules.load('refuse_nord')
+modules.load('watchdog')
+modules.load('extended_error')
+
+-- Load keyfile_default
+trust_anchors.add_file('@keyfile_default@', @unmanaged@)
+
+local function eval_cmd_compile(line, raw)
+ -- Compatibility sandbox code loading
+ local function load_code(code)
+ if getfenv then -- Lua 5.1
+ return loadstring(code)
+ else -- Lua 5.2+
+ return load(code, nil, 't', _ENV)
+ end
+ end
+ local err, chunk
+ chunk, err = load_code(raw and 'return '..line or 'return table_print('..line..')')
+ if err then
+ chunk, err = load_code(line)
+ end
+ return chunk, err
+end
+
+-- Interactive command evaluation
+function eval_cmd(line, raw)
+ local chunk, err = eval_cmd_compile(line, raw)
+ if not err then
+ return chunk()
+ else
+ error(err)
+ end
+end
+
+-- Pretty printing
+local pprint = require('krprint').pprint
+function table_print(...)
+ local strs = {}
+ local nargs = select('#', ...)
+ if nargs == 0 then
+ return nil
+ end
+ for n=1,nargs do
+ local arg = select(n, ...)
+ local arg_str = pprint(arg)
+ if nargs > 1 then
+ table.insert(strs, string.format("%s\t-- result # %d", arg_str, n))
+ else
+ table.insert(strs, arg_str)
+ end
+ end
+ return table.concat(strs, '\n')
+end
+
+-- This extends the worker module to allow asynchronous execution of functions and nonblocking I/O.
+-- The current implementation combines cqueues for Lua interface, and event.socket() in order to not
+-- block resolver engine while waiting for I/O or timers.
+--
+local has_cqueues, cqueues = pcall(require, 'cqueues')
+if has_cqueues then
+
+ -- Export the asynchronous sleep function
+ worker.sleep = cqueues.sleep
+
+ -- Create metatable for workers to define the API
+ -- It can schedule multiple cqueues and yield execution when there's a wait for blocking I/O or timer
+ local asynchronous_worker_mt = {
+ work = function (self)
+ local ok, err, _, co = self.cq:step(0)
+ if not ok then
+ log_warn(ffi.C.LOG_GRP_SYSTEM, '%s error: %s %s', self.name or 'worker', err, debug.traceback(co))
+ end
+ -- Reschedule timeout or create new one
+ local timeout = self.cq:timeout()
+ if timeout then
+ -- Throttle timeouts to avoid too frequent wakeups
+ if timeout == 0 then timeout = 0.00001 end
+ -- Convert from seconds to duration
+ timeout = timeout * sec
+ if not self.next_timeout then
+ self.next_timeout = event.after(timeout, self.on_step)
+ else
+ event.reschedule(self.next_timeout, timeout)
+ end
+ else -- Cancel running timeout when there is no next deadline
+ if self.next_timeout then
+ event.cancel(self.next_timeout)
+ self.next_timeout = nil
+ end
+ end
+ end,
+ wrap = function (self, f)
+ self.cq:wrap(f)
+ end,
+ loop = function (self)
+ self.on_step = function () self:work() end
+ self.event_fd = event.socket(self.cq:pollfd(), self.on_step)
+ end,
+ close = function (self)
+ if self.event_fd then
+ event.cancel(self.event_fd)
+ self.event_fd = nil
+ end
+ end,
+ }
+
+ -- Implement the coroutine worker with cqueues
+ local function worker_new (name)
+ return setmetatable({name = name, cq = cqueues.new()}, { __index = asynchronous_worker_mt })
+ end
+
+ -- Create a default background worker
+ worker.bg_worker = worker_new('worker.background')
+ worker.bg_worker:loop()
+
+ -- Wrap a function for asynchronous execution
+ function worker.coroutine (f)
+ worker.bg_worker:wrap(f)
+ end
+else
+ -- Disable asynchronous execution
+ local function disabled ()
+ error('Lua library cqueues is required for asynchronous execution (luaJIT requires library for Lua 5.1)')
+ end
+ worker.sleep = disabled
+ worker.map = disabled
+ worker.coroutine = disabled
+ worker.bg_worker = setmetatable({}, { __index = disabled })
+end
+
+-- Global commands for map()
+
+-- must be public because it is called from eval_cmd()
+-- when map() commands are read from control socket
+function _map_luaobj_call_wrapper(cmd)
+ local func = eval_cmd_compile(cmd, true)
+ local ret = kluautil.kr_table_pack(xpcall(func, debug.traceback))
+ local ok, serial = pcall(krprint.serialize_lua, ret, 'error')
+ if not ok then
+ log_error(ffi.C.LOG_GRP_SYSTEM, 'failed to serialize map() response %s (%s)',
+ table_print(ret), serial)
+ return krprint.serialize_lua(
+ kluautil.kr_table_pack(false, "returned values cannot be serialized: "
+ .. serial))
+ else
+ return serial
+ end
+end
+
+local function _sock_errmsg(path, desc)
+ return string.format(
+ 'map() error while communicating with %s: %s',
+ path, desc)
+end
+
+local function _sock_check(sock, call, params, path, desc)
+ local errprefix = _sock_errmsg(path, desc) .. ': '
+ local retvals = kluautil.kr_table_pack(pcall(call, unpack(params)))
+ local ok = retvals[1]
+ if not ok then
+ error(errprefix .. tostring(retvals[2]))
+ end
+ local rerr, werr = sock:error()
+ if rerr or werr then
+ error(string.format('%sread error %s; write error %s', errprefix, rerr, werr))
+ end
+ if retvals[2] == nil then
+ error(errprefix .. 'unexpected nil result')
+ end
+ return unpack(retvals, 2, retvals.n)
+end
+
+local function _sock_assert(condition, path, desc)
+ if not condition then
+ error(_sock_errmsg(path, desc))
+ end
+end
+
+local function map_send_recv(cmd, path)
+ local bit = require('bit')
+ local socket = require('cqueues.socket')
+ local s = socket.connect({ path = path })
+ s:setmaxerrs(0)
+ s:setmode('bn', 'bn')
+ local status, err = pcall(s.connect, s)
+ if not status then
+ log_error(ffi.C.LOG_GRP_NETWORK, 'map() error while connecting to control socket %s: '
+ .. '%s (ignoring this socket)', path, err)
+ return nil
+ end
+ local ret = _sock_check(s, s.write, {s, '__binary\n'}, path,
+ 'write __binary')
+ _sock_assert(ret, path,
+ 'write __binary result')
+ local recv = _sock_check(s, s.read, {s, 2}, path,
+ 'read reply to __binary')
+ _sock_assert(recv and recv == '> ', path,
+ 'unexpected reply to __binary')
+ _sock_check(s, s.write, {s, cmd..'\n'}, path,
+ 'command write')
+ recv = _sock_check(s, s.read, {s, 4}, path,
+ 'response length read')
+ _sock_assert(recv and #recv == 4, path,
+ 'length of response length preamble does not match')
+ local len = tonumber(recv:byte(1))
+ for i=2,4 do
+ len = bit.bor(bit.lshift(len, 8), tonumber(recv:byte(i)))
+ end
+ ret = _sock_check(s, s.read, {s, len}, path,
+ 'read response')
+ _sock_assert(ret and #ret == len, path,
+ 'actual response length does not match length in preamble')
+ s:close()
+ return ret
+end
+
+-- internal use only
+-- Call cmd on each instance via control sockets.
+-- @param format - "luaobj" if individual results should be Lua objects
+-- - "strings" for eval_cmd output for each instance
+-- @returns table with results, one item per instance + key n=number of instances
+-- (order of return values is undefined)
+-- @throws Lua error if:
+-- - communication failed in the middle of transaction
+-- - a result is not serializable
+-- - individual call throws an error
+-- - number of return values != 1 per instance per call
+-- - cmd execution state is undefined after an error
+-- Connection errors at the beginning are ignored to paper over leftover dead sockets.
+function map(cmd, format)
+ local local_sockets = {}
+ local results = {}
+
+ if (type(cmd) ~= 'string') then
+ panic('map() command must be a string') end
+ if string.find(cmd, '\n', 1, true) then
+ panic('map() command cannot contain literal \\n, escape it with \\010') end
+ if (#cmd <= 0) then
+ panic('map() command must be non-empty') end
+ -- syntax check on input command to detect typos early
+ local chunk, err = eval_cmd_compile(cmd, false)
+ if not chunk then
+ panic('failure when compiling map() command: %s', err)
+ end
+
+ format = format or 'luaobj'
+ if (format ~= 'luaobj' and format ~= 'strings') then
+ panic('map() output format must be luaobj or strings') end
+ if format == 'luaobj' then
+ cmd = '_map_luaobj_call_wrapper([=====[' .. cmd .. ']=====])'
+ end
+
+ -- find out control socket paths
+ for _,v in pairs(net.list()) do
+ if (v['kind'] == 'control') and (v['transport']['family'] == 'unix') then
+ table.insert(local_sockets, string.match(v['transport']['path'], '^.*/([^/]+)$'))
+ end
+ end
+ local filetab = kluautil.list_dir(worker.control_path)
+ if next(filetab) == nil then
+ panic('no control sockets found in directory %s',
+ worker.control_path)
+ end
+
+ local result_count = 0
+ -- finally execute it on all instances
+ for _, file in ipairs(filetab) do
+ local local_exec = false
+ for _, lsoc in ipairs(local_sockets) do
+ if file == lsoc then
+ local_exec = true
+ end
+ end
+ local path = worker.control_path..file
+ local path_name = (local_exec and 'this instance') or path
+ log_info(ffi.C.LOG_GRP_SYSTEM, 'executing map() on %s: command %s', path_name, cmd)
+ local ret
+ if local_exec then
+ ret = eval_cmd(cmd)
+ else
+ ret = map_send_recv(cmd, path)
+ -- skip dead sockets (leftovers from dead instances)
+ if ret == nil then
+ goto continue
+ end
+ end
+ result_count = result_count + 1
+ -- return value is output from eval_cmd
+ -- i.e. string including "quotes" and Lua escaping in between
+ assert(type(ret) == 'string', 'map() protocol error, '
+ .. 'string not retured by follower')
+ assert(#ret >= 2 and
+ string.sub(ret, 1, 1) == "'"
+ and string.sub(ret, -1, -1) == "'",
+ 'map() protocol error, value returned by follower does '
+ .. 'not look like a string')
+ -- deserialize string: remove "quotes" and de-escape bytes
+ ret = krprint.deserialize_lua(ret)
+ if format == 'luaobj' then
+ -- ret should be table with xpcall results serialized into string
+ ret = krprint.deserialize_lua(ret)
+ assert(type(ret) == 'table', 'map() protocol error, '
+ .. 'table with results not retured by follower')
+ if (ret.n ~= 2) then
+ log_error(ffi.C.LOG_GRP_SYSTEM, 'got unsupported map() response: %s', table_print(ret))
+ panic('unexpected number of return values in map() response: '
+ .. 'only single return value is allowed, '
+ .. 'use kluautil.kr_table_pack() helper')
+ end
+ local ok, retval = ret[1], ret[2]
+ if ok == false then
+ panic('error when executing map() command on control socket %s: '
+ .. '%s. command execution state is now undefined!',
+ path, retval)
+ end
+ -- drop wrapper table and return only the actual return value
+ ret = retval
+ end
+ results[result_count] = ret
+ ::continue::
+ end
+ results.n = result_count
+ return results
+end
diff --git a/daemon/lua/trust_anchors.lua.in b/daemon/lua/trust_anchors.lua.in
new file mode 100644
index 0000000..56a7f95
--- /dev/null
+++ b/daemon/lua/trust_anchors.lua.in
@@ -0,0 +1,532 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+-- Load the module
+local ffi = require 'ffi'
+local kres = require('kres')
+local C = ffi.C
+
+local trust_anchors -- the public pseudo-module, exported as global variable
+
+-- RFC5011 state table
+local key_state = {
+ Start = 'Start', AddPend = 'AddPend', Valid = 'Valid',
+ Missing = 'Missing', Revoked = 'Revoked', Removed = 'Removed'
+}
+
+local function upgrade_required(msg)
+ if msg then
+ msg = msg .. '\n'
+ else
+ msg = ''
+ end
+ panic('Configuration upgrade required: ' .. msg .. 'Please refer to ' ..
+ 'https://knot-resolver.readthedocs.io/en/stable/upgrading.html')
+end
+
+-- TODO: Move bootstrap to a separate module or even its own binary
+-- remove UTC timezone specification if present or throw error
+local function time2utc(orig_timespec)
+ local patterns = {'[+-]00:00$', 'Z$'}
+ for _, pattern in ipairs(patterns) do
+ local timespec, removals = string.gsub(orig_timespec, pattern, '')
+ if removals == 1 then
+ return timespec
+ end
+ end
+ error(string.format('unsupported time specification: %s', orig_timespec))
+end
+
+local function keydigest_is_valid(valid_from, valid_until)
+ local format = '%Y-%m-%dT%H:%M:%S'
+ local time_now = os.date('!%Y-%m-%dT%H:%M:%S') -- ! forces UTC
+ local time_diff = ffi.new('double[1]')
+ local err = ffi.C.kr_strptime_diff(
+ format, time_now, time2utc(valid_from), time_diff)
+ if (err ~= nil) then
+ error(string.format('failed to process "validFrom" constraint: %s',
+ ffi.string(err)))
+ end
+ local from_ok = time_diff[0] > 0
+
+ -- optional attribute
+ local until_ok = true
+ if valid_until then
+ err = ffi.C.kr_strptime_diff(
+ format, time_now, time2utc(valid_until), time_diff)
+ if (err ~= nil) then
+ error(string.format('failed to process "validUntil" constraint: %s',
+ ffi.string(err)))
+ end
+ until_ok = time_diff[0] < 0
+ end
+ return from_ok and until_ok
+end
+
+local function parse_xml_keydigest(attrs, inside, output)
+ local fields = {}
+ local _, n = string.gsub(attrs, "([%w]+)=\"([^\"]*)\"", function (k, v) fields[k] = v end)
+ assert(n >= 1,
+ string.format('cannot parse XML attributes from "%s"', attrs))
+ assert(fields['validFrom'],
+ string.format('mandatory KeyDigest XML attribute validFrom ' ..
+ 'not found in "%s"', attrs))
+ local valid_attrs = {id = true, validFrom = true, validUntil = true}
+ for key, _ in pairs(fields) do
+ assert(valid_attrs[key],
+ string.format('unsupported KeyDigest attribute "%s" found in "%s"',
+ key, attrs))
+ end
+
+ _, n = string.gsub(inside, "<([%w]+).->([^<]+)</[%w]+>", function (k, v) fields[k] = v end)
+ assert(n >= 1,
+ string.format('error parsing KeyDigest XML elements from "%s"',
+ inside))
+ local mandatory_elements = {'KeyTag', 'Algorithm', 'DigestType', 'Digest'}
+ for _, key in ipairs(mandatory_elements) do
+ assert(fields[key],
+ string.format('mandatory element %s is missing in "%s"',
+ key, inside))
+ end
+ assert(n == 4, string.format('found %d elements but expected 4 in %s', n, inside))
+ table.insert(output, fields) -- append to list of parsed keydigests
+end
+
+local function generate_ds(keydigests)
+ local rrset = ''
+ for _, fields in ipairs(keydigests) do
+ local rr = string.format(
+ '. 0 IN DS %s %s %s %s',
+ fields.KeyTag, fields.Algorithm, fields.DigestType, fields.Digest)
+ if keydigest_is_valid(fields['validFrom'], fields['validUntil']) then
+ rrset = rrset .. '\n' .. rr
+ else
+ log_info(ffi.C.LOG_GRP_TA, 'skipping trust anchor "%s" ' ..
+ 'because it is outside of validity range', rr)
+ end
+ end
+ return rrset
+end
+
+local function assert_str_match(str, pattern, expected)
+ local count = 0
+ for _ in string.gmatch(str, pattern) do
+ count = count + 1
+ end
+ assert(count == expected,
+ string.format('expected %d occurences of "%s" but got %d in "%s"',
+ expected, pattern, count, str))
+end
+
+-- Fetch root anchors in XML over HTTPS, returning a zone-file-style string
+-- or false in case of error, and a message.
+local function bootstrap(url, ca)
+ local kluautil = require('kluautil')
+ local file = io.tmpfile()
+ -- RFC 7958, sec. 2, but we don't do precise XML parsing.
+ -- @todo ICANN certificate is verified against current CA
+ -- this is not ideal, as it should rather verify .xml signature which
+ -- is signed by ICANN long-lived cert, but luasec has no PKCS7
+ local rcode, errmsg = kluautil.kr_https_fetch(url, file, ca)
+ if rcode == nil then
+ file:close()
+ return false, string.format('[ ta ] fetch of "%s" failed: %s', url, errmsg)
+ end
+
+ local xml = file:read("*a")
+ file:close()
+
+ -- we support only minimal subset of https://tools.ietf.org/html/rfc7958
+ assert_str_match(xml, '<?xml version="1%.0" encoding="UTF%-8"%?>', 1)
+ assert_str_match(xml, '<TrustAnchor ', 1)
+ assert_str_match(xml, '<Zone>.</Zone>', 1)
+ assert_str_match(xml, '</TrustAnchor>', 1)
+
+ -- Parse root trust anchor, one digest at a time, converting to a zone-file-style string.
+ local keydigests = {}
+ string.gsub(xml, "<KeyDigest([^>]*)>(.-)</KeyDigest>", function(attrs, inside)
+ parse_xml_keydigest(attrs, inside, keydigests)
+ end)
+ local rrset = generate_ds(keydigests)
+ if rrset == '' then
+ return false, string.format('[ ta ] no valid trust anchors found at "%s"', url)
+ end
+ local msg = '[ ta ] Root trust anchors bootstrapped over https with pinned certificate.\n'
+ .. ' You SHOULD verify them manually against original source:\n'
+ .. ' https://www.iana.org/dnssec/files\n'
+ .. '[ ta ] Bootstrapped root trust anchors are:'
+ .. rrset
+ return rrset, msg
+end
+
+local function bootstrap_write(rrstr, filename)
+ local fname_tmp = filename .. '.lock.' .. tostring(worker.pid);
+ local file = assert(io.open(fname_tmp, 'w'))
+ file:write(rrstr)
+ file:close()
+ assert(os.rename(fname_tmp, filename))
+end
+-- Bootstrap end
+
+-- Update ta.comment and return decorated line representing the RR
+-- This is meant to be in zone-file format.
+local function ta_rr_str(ta)
+ ta.comment = ' ' .. ta.state .. ':' .. (ta.timer or '')
+ .. ' ; KeyTag:' .. ta.key_tag -- the tag is just for humans
+ local rr_str = kres.rr2str(ta) .. '\n'
+ if ta.state ~= key_state.Valid and ta.state ~= key_state.Missing then
+ rr_str = '; '..rr_str -- Invalidate key string (for older kresd versions)
+ end
+ return rr_str
+end
+
+-- Write keyset to a file. States and timers are stored in comments.
+local function keyset_write(keyset)
+ if not keyset.managed then -- not to be persistent, this is an error!
+ panic('internal error: keyset_write called for an unmanaged TA')
+ end
+ local fname_tmp = keyset.filename .. '.lock.' .. tostring(worker.pid);
+ local file = assert(io.open(fname_tmp, 'w'))
+ for i = 1, #keyset do
+ file:write(ta_rr_str(keyset[i]))
+ end
+ file:close()
+ assert(os.rename(fname_tmp, keyset.filename))
+end
+
+-- Search the values of a table and return the corresponding key (or nil).
+local function table_search(t, val)
+ for k, v in pairs(t) do
+ if v == val then
+ return k
+ end
+ end
+ return nil
+end
+
+-- For each RR, parse .state and .timer from .comment.
+local function keyset_parse_comments(tas, default_state)
+ for _, ta in pairs(tas) do
+ ta.state = default_state
+ if ta.comment then
+ string.gsub(ta.comment, '^%s*(%a+):(%d*)', function (state, time)
+ if table_search(key_state, state) then
+ ta.state = state
+ end
+ ta.timer = tonumber(time) -- nil on failure
+ end)
+ ta.comment = nil
+ end
+ end
+ return tas
+end
+
+-- Read keyset from a file xor a string. (This includes the key states and timers.)
+local function keyset_read(path, str)
+ if (path == nil) == (str == nil) then -- exactly one of them must be nil
+ return nil, "internal ERROR: incorrect call to TA's keyset_read"
+ end
+ -- First load the regular entries, trusting them.
+ local zonefile = require('zonefile')
+ local tas, err
+ if path ~= nil then
+ tas, err = zonefile.file(path)
+ else
+ tas, err = zonefile.string(str)
+ end
+ if not tas then
+ return tas, err
+ end
+ keyset_parse_comments(tas, key_state.Valid)
+
+ -- The untrusted keys are commented out but important to load.
+ local line_iter
+ if path ~= nil then
+ line_iter = io.lines(path)
+ else
+ line_iter = string.gmatch(str, "[^\n]+")
+ end
+ for line in line_iter do
+ if line:sub(1, 2) == '; ' then
+ -- Ignore the line if it fails to parse including recognized .state.
+ local l_set = zonefile.string(line:sub(3))
+ if l_set and l_set[1] then
+ keyset_parse_comments(l_set)
+ if l_set[1].state then
+ table.insert(tas, l_set[1])
+ end
+ end
+ end
+ end
+
+ -- Fill tas[*].key_tag
+ for _, ta in pairs(tas) do
+ local ta_keytag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata)
+ if not (ta_keytag >= 0 and ta_keytag <= 65535) then
+ return nil, string.format('invalid key: "%s": %s',
+ kres.rr2str(ta), ffi.string(C.knot_strerror(ta_keytag)))
+ end
+ ta.key_tag = ta_keytag
+ end
+
+ -- Fill tas.owner
+ if not tas[1] then
+ return nil, "empty TA set"
+ end
+ local owner = tas[1].owner
+ for _, ta in ipairs(tas) do
+ if ta.owner ~= owner then
+ return nil, string.format("do not mix %s and %s TAs in single file/string",
+ kres.dname2str(ta.owner), kres.dname2str(owner))
+ end
+ end
+ tas.owner = owner
+
+ return tas
+end
+
+-- Replace current TAs for given owner by the "trusted" ones from passed keyset.
+-- Return true iff no TA errored out and at least one is in VALID state.
+local function keyset_publish(keyset)
+ local store = kres.context().trust_anchors
+ local count = 0
+ local has_error = false
+ C.kr_ta_del(store, keyset.owner)
+ for _, ta in ipairs(keyset) do
+ -- Key MAY be used as a TA only in these two states (RFC5011, 4.2)
+ if ta.state == key_state.Valid or ta.state == key_state.Missing then
+ if C.kr_ta_add(store, ta.owner, ta.type, ta.ttl, ta.rdata, #ta.rdata) == 0 then
+ count = count + 1
+ else
+ ta.state = 'ERROR'
+ has_error = true
+ end
+ end
+ end
+ if count == 0 then
+ log_error(ffi.C.LOG_GRP_TA, 'ERROR: no anchors are trusted for ' ..
+ kres.dname2str(keyset.owner) .. ' !')
+ end
+ return count > 0 and not has_error
+end
+
+local function add_file(path, unmanaged)
+ local managed = not unmanaged
+ if not ta_update then
+ modules.load('ta_update')
+ end
+ if managed then
+ if not io.open(path .. '.lock', 'w') then
+ error("[ ta ] ERROR: write access needed to keyfile dir '"..path.."'")
+ end
+ os.remove(path .. ".lock")
+ end
+
+ -- Bootstrap TA for root zone if keyfile doesn't exist
+ if managed and not io.open(path, 'r') then
+ if trust_anchors.keysets['\0'] then
+ error(string.format(
+ "[ ta ] keyfile '%s' doesn't exist and root key is already installed, "
+ .. "cannot bootstrap; provide a path to valid file with keys", path))
+ end
+ log_info(ffi.C.LOG_GRP_TA, "keyfile '%s': doesn't exist, bootstrapping", path);
+ local rrstr, msg = bootstrap(trust_anchors.bootstrap_url, trust_anchors.bootstrap_ca)
+ if not rrstr then
+ msg = msg .. '\n'
+ .. '[ ta ] Failed to bootstrap root trust anchors!'
+ error(msg)
+ end
+ print(msg)
+ bootstrap_write(rrstr, path)
+ -- continue as if the keyfile was there
+ end
+
+ -- Parse the file and check its sanity
+ local keyset, err = keyset_read(path)
+ if not keyset then
+ panic("[ ta ] ERROR: failed to read anchors from '%s' (%s)", path, err)
+ end
+ keyset.filename = path
+ keyset.managed = managed
+
+ local owner = keyset.owner
+ local owner_str = kres.dname2str(owner)
+ local keyset_orig = trust_anchors.keysets[owner]
+ if keyset_orig then
+ log_warn(ffi.C.LOG_GRP_TA, 'warning: overriding previously set trust anchors for ' .. owner_str)
+ if keyset_orig.managed and ta_update then
+ ta_update.stop(owner)
+ end
+ end
+ trust_anchors.keysets[owner] = keyset
+
+ -- Replace the TA store used for validation
+ if keyset_publish(keyset) then
+ log_info(ffi.C.LOG_GRP_TA, 'installed trust anchors for domain ' .. owner_str .. ' are:\n'
+ .. trust_anchors.summary(owner))
+ end
+ -- TODO: if failed and for root, try to rebootstrap?
+
+ ta_update.start(owner, managed)
+end
+
+local function remove(zname)
+ local owner = kres.str2dname(zname)
+ if not trust_anchors.keysets[owner] then
+ return false
+ end
+
+ if ta_update then
+ ta_update.stop(owner)
+ end
+ trust_anchors.keysets[owner] = nil
+ local store = kres.context().trust_anchors
+ C.kr_ta_del(store, owner)
+ return true
+end
+
+local function ta_str(owner)
+ local owner_str = kres.dname2str(owner) .. ' '
+ local msg = ''
+ for _, nta in pairs(trust_anchors.insecure) do
+ if owner == kres.str2dname(nta) then
+ msg = owner_str .. 'is negative trust anchor\n'
+ end
+ end
+ if not trust_anchors.keysets[owner] then
+ if #msg > 0 then -- it is normal that NTA does not have explicit TA
+ return msg
+ else
+ return owner_str .. 'has no explicit trust anchors\n'
+ end
+ end
+ if #msg > 0 then
+ msg = msg .. 'WARNING! negative trust anchor also has an explicit TA\n'
+ end
+ for _, ta in ipairs(trust_anchors.keysets[owner]) do
+ msg = msg .. ta_rr_str(ta)
+ end
+ return msg
+end
+
+-- TA store management, for user docs see ../README.rst
+trust_anchors = {
+ -- [internal] table indexed by dname;
+ -- each item is a list of RRs and additionally contains:
+ -- - owner - that dname (for simplicity)
+ -- - [optional] filename in which to persist the state,
+ -- implying unmanaged TA if nil
+ -- The RR tables also contain some additional TA-specific fields.
+ keysets = {},
+
+ -- Documented properties:
+ insecure = {},
+
+ bootstrap_url = 'https://data.iana.org/root-anchors/root-anchors.xml',
+ bootstrap_ca = '@etc_dir@/icann-ca.pem',
+
+ -- Load keys from a file, 5011-managed by default.
+ -- If managed and the file doesn't exist, try bootstrapping the root into it.
+ add_file = add_file,
+ config = function() upgrade_required('trust_anchors.config was removed, use trust_anchors.add_file()') end,
+ remove = remove,
+
+ keyset_publish = keyset_publish,
+ keyset_write = keyset_write,
+ key_state = key_state,
+
+ -- Add DS/DNSKEY record(s) (unmanaged)
+ add = function (keystr)
+ local keyset, err = keyset_read(nil, keystr)
+ if keyset ~= nil then
+ local owner = keyset.owner
+ local owner_str = kres.dname2str(owner)
+ local keyset_orig = trust_anchors.keysets[owner]
+ -- Set up trust_anchors.keysets[owner]
+ if keyset_orig then
+ if keyset_orig.managed then
+ panic('[ ta ] it is impossible to add an unmanaged TA for zone '
+ .. owner_str .. ' which already has a managed TA')
+ end
+ log_warn(ffi.C.LOG_GRP_TA, 'warning: extending previously set trust anchors for '
+ .. owner_str)
+ for _, ta in ipairs(keyset) do
+ table.insert(keyset_orig, ta)
+ end
+ end
+ -- Replace the TA store used for validation
+ if not keyset_publish(keyset) then
+ err = "when publishing the TA set"
+ -- trust_anchors.keysets[owner] was already updated to the
+ -- (partially) failing state, but I'm not sure how much to improve this
+ end
+ keyset.managed = false
+ trust_anchors.keysets[owner] = keyset
+
+ end
+ log_info(ffi.C.LOG_GRP_TA, 'New TA state:\n' .. trust_anchors.summary())
+ if err then
+ panic('[ ta ] .add() failed: ' .. err)
+ end
+ end,
+
+ -- Negative TA management
+ set_insecure = function (list)
+ assert(type(list) == 'table', 'parameter must be list of domain names (e.g. {"a.test", "b.example"})')
+ local store = kres.context().negative_anchors
+ for i = 1, #list do
+ local dname = kres.str2dname(list[i])
+ if trust_anchors.keysets[dname] then
+ error('cannot add NTA '..list[i]..' because it is TA. Use trust_anchors.remove() instead')
+ end
+ end
+
+ C.kr_ta_clear(store)
+ for i = 1, #list do
+ local dname = kres.str2dname(list[i])
+ C.kr_ta_add(store, dname, kres.type.DS, 0, nil, 0)
+ end
+ trust_anchors.insecure = list
+ end,
+ -- Return textual representation of all TAs (incl. negative)
+ -- It's meant for human consumption.
+ summary = function (single_owner)
+ if single_owner then -- single domain
+ return ta_str(single_owner)
+ end
+
+ -- all domains
+ local msg = ''
+ local ta_count = 0
+ local seen = {}
+ for _, nta_str in pairs(trust_anchors.insecure) do
+ local owner = kres.str2dname(nta_str)
+ seen[owner] = true
+ msg = msg .. ta_str(owner)
+ end
+ for owner, _ in pairs(trust_anchors.keysets) do
+ if not seen[owner] then
+ ta_count = ta_count + 1
+ msg = msg .. ta_str(owner)
+ end
+ end
+ if ta_count == 0 then
+ msg = msg .. 'No valid trust anchors, DNSSEC validation is disabled\n'
+ end
+ return msg
+ end,
+}
+
+-- Syntactic sugar for TA store
+setmetatable(trust_anchors, {
+ __newindex = function (t,k,v)
+ if k == 'file' then
+ upgrade_required('trust_anchors.file was removed, use trust_anchors.add_file()')
+ elseif k == 'negative' then
+ upgrade_required('trust_anchors.negative was removed, use trust_anchors.set_insecure()')
+ elseif k == 'keyfile_default' then
+ upgrade_required('trust_anchors.keyfile_default is now compiled in, see trust_anchors.remove()')
+ else rawset(t, k, v) end
+ end,
+})
+
+return trust_anchors
diff --git a/daemon/lua/trust_anchors.rst b/daemon/lua/trust_anchors.rst
new file mode 100644
index 0000000..40f79b6
--- /dev/null
+++ b/daemon/lua/trust_anchors.rst
@@ -0,0 +1,123 @@
+.. SPDX-License-Identifier: GPL-3.0-or-later
+
+.. warning:: Options in this section are intended only for expert users and
+ normally should not be needed.
+
+Since version 4.0, **DNSSEC validation is enabled by default**.
+If you really need to turn DNSSEC off and are okay with lowering security of your
+system by doing so, add the following snippet to your configuration file.
+
+.. code-block:: lua
+
+ -- turns off DNSSEC validation
+ trust_anchors.remove('.')
+
+The resolver supports DNSSEC including :rfc:`5011` automated DNSSEC TA updates
+and :rfc:`7646` negative trust anchors. Depending on your distribution, DNSSEC
+trust anchors should be either maintained in accordance with the distro-wide
+policy, or automatically maintained by the resolver itself.
+
+In practice this means that you can forget about it and your favorite Linux
+distribution will take care of it for you.
+
+Following functions allow to modify DNSSEC configuration *if you really have to*:
+
+
+.. function:: trust_anchors.add_file(keyfile[, readonly = false])
+
+ :param string keyfile: path to the file.
+ :param readonly: if true, do not attempt to update the file.
+
+ The format is standard zone file, though additional information may be persisted in comments.
+ Either DS or DNSKEY records can be used for TAs.
+ If the file does not exist, bootstrapping of *root* TA will be attempted.
+ If you want to use bootstrapping, install `lua-http`_ library.
+
+ Each file can only contain records for a single domain.
+ The TAs will be updated according to :rfc:`5011` and persisted in the file (if allowed).
+
+ Example output:
+
+ .. code-block:: lua
+
+ > trust_anchors.add_file('root.key')
+ [ ta ] new state of trust anchors for a domain:
+ . 165488 DS 19036 8 2 49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5
+ nil
+
+ [ ta ] key: 19036 state: Valid
+
+.. function:: trust_anchors.remove(zonename)
+
+ Remove specified trust anchor from trusted key set. Removing trust anchor for the root zone effectively disables DNSSEC validation (unless you configured another trust anchor).
+
+ .. code-block:: lua
+
+ > trust_anchors.remove('.')
+ true
+
+ If you want to disable DNSSEC validation for a particular domain but keep it enabled for the rest of DNS tree, use :func:`trust_anchors.set_insecure`.
+
+.. envvar:: trust_anchors.hold_down_time = 30 * day
+
+ :return: int (default: 30 * day)
+
+ Modify RFC5011 hold-down timer to given value. Intended only for testing purposes. Example: ``30 * sec``
+
+.. envvar:: trust_anchors.refresh_time = nil
+
+ :return: int (default: nil)
+
+ Modify RFC5011 refresh timer to given value (not set by default), this will force trust anchors
+ to be updated every N seconds periodically instead of relying on RFC5011 logic and TTLs.
+ Intended only for testing purposes.
+ Example: ``10 * sec``
+
+.. envvar:: trust_anchors.keep_removed = 0
+
+ :return: int (default: 0)
+
+ How many ``Removed`` keys should be held in history (and key file) before being purged.
+ Note: all ``Removed`` keys will be purged from key file after restarting the process.
+
+
+.. function:: trust_anchors.set_insecure(nta_set)
+
+ :param table nta_list: List of domain names (text format) representing NTAs.
+
+ When you use a domain name as an *negative trust anchor* (NTA), DNSSEC validation will be turned off at/below these names.
+ Each function call replaces the previous NTA set. You can find the current active set in ``trust_anchors.insecure`` variable.
+ If you want to disable DNSSEC validation completely use :func:`trust_anchors.remove` function instead.
+
+ Example output:
+
+ .. code-block:: lua
+
+ > trust_anchors.set_insecure({ 'bad.boy', 'example.com' })
+ > trust_anchors.insecure
+ [1] => bad.boy
+ [2] => example.com
+
+ .. warning:: If you set NTA on a name that is not a zone cut,
+ it may not always affect names not separated from the NTA by a zone cut.
+
+.. function:: trust_anchors.add(rr_string)
+
+ :param string rr_string: DS/DNSKEY records in presentation format (e.g. ``. 3600 IN DS 19036 8 2 49AAC11...``)
+
+ Inserts DS/DNSKEY record(s) into current keyset. These will not be managed or updated, use it only for testing
+ or if you have a specific use case for not using a keyfile.
+
+ .. note:: Static keys are very error-prone and should not be used in production. Use :func:`trust_anchors.add_file` instead.
+
+ Example output:
+
+ .. code-block:: lua
+
+ > trust_anchors.add('. 3600 IN DS 19036 8 2 49AAC11...')
+
+.. function:: trust_anchors.summary()
+
+ Return string with summary of configured DNSSEC trust anchors, including negative TAs.
+
+.. _lua-http: https://luarocks.org/modules/daurnimator/http
diff --git a/daemon/lua/trust_anchors.test/bootstrap.test.lua b/daemon/lua/trust_anchors.test/bootstrap.test.lua
new file mode 100644
index 0000000..7dd248b
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/bootstrap.test.lua
@@ -0,0 +1,112 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+modules.load('ta_update')
+
+-- check prerequisites
+local has_http = pcall(require, 'kres_modules.http') and pcall(require, 'http.request')
+if not has_http then
+ -- skipping bootstrap tests because http module is not not installed
+ os.exit(77)
+end
+
+local cqueues = require("cqueues")
+local socket = require("cqueues.socket")
+
+-- unload modules which are not related to this test
+if ta_signal_query then
+ modules.unload('ta_signal_query')
+end
+if priming then
+ modules.unload('priming')
+end
+if detect_time_skew then
+ modules.unload('detect_time_skew')
+end
+
+-- Self-checks on globals
+assert(help() ~= nil)
+assert(worker.id ~= nil)
+-- Self-checks on facilities
+assert(worker.stats() ~= nil)
+assert(net.interfaces() ~= nil)
+-- Self-checks on loaded stuff
+assert(#modules.list() > 0)
+-- Self-check timers
+ev = event.recurrent(1 * sec, function () return 1 end)
+event.cancel(ev)
+ev = event.after(0, function () return 1 end)
+
+
+-- do not attempt to contact outside world using DNS, operate only on cache
+net.ipv4 = false
+net.ipv6 = false
+-- do not listen, test is driven by config code
+env.KRESD_NO_LISTEN = true
+
+-- start test webserver
+local function start_webserver()
+ -- srvout = io.popen('luajit webserv.lua')
+ -- TODO
+ os.execute('luajit webserv.lua >/dev/null 2>&1 &')
+ -- assert(srvout, 'failed to start webserver')
+end
+
+local function wait_for_webserver()
+ local starttime = os.time()
+ local connected = false
+ while not connected and os.difftime(os.time(), starttime) < 10 do
+ local con = socket.connect("localhost", 8080)
+ connected, msg = pcall(con.connect, con, 3)
+ cqueues.sleep (0.3)
+ end
+ assert(connected, string.format('unable to connect to web server: %s', msg))
+end
+
+local host = 'https://localhost:8080/'
+-- avoid interference with configured keyfile_default
+trust_anchors.remove('.')
+
+local function test_err_cert()
+ trust_anchors.bootstrap_ca = 'x509/wrongca.pem'
+ trust_anchors.bootstrap_url = host .. 'ok1.xml'
+ boom(trust_anchors.add_file, {'ok1.keys'},
+ 'fake server certificate is detected')
+end
+
+local function test_err_xml(testname, testdesc)
+ return function()
+ trust_anchors.bootstrap_ca = 'x509/ca.pem'
+ trust_anchors.bootstrap_url = host .. testname .. '.xml'
+ boom(trust_anchors.add_file, {testname .. '.keys'}, testdesc)
+ end
+end
+
+-- dumb test, right now it cannot check content of keys because
+-- it does not get written until refresh fetches DNSKEY from network
+-- (and bypassing network using policy bypasses also validation
+-- so it does not test anything)
+local function test_ok_xml(testname, testdesc)
+ return function()
+ trust_anchors.bootstrap_url = host .. testname .. '.xml'
+ trust_anchors.remove('.')
+ same(trust_anchors.add_file(testname .. '.keys'), nil, testdesc)
+ end
+end
+
+return {
+ start_webserver,
+ wait_for_webserver,
+ test_err_cert,
+ test_err_xml('err_attr_extra_attr', 'bogus TA XML with an extra attribute'),
+ test_err_xml('err_attr_validfrom_invalid', 'bogus TA XML with invalid validFrom value'),
+ test_err_xml('err_attr_validfrom_missing', 'bogus TA XML without mandatory validFrom attribute'),
+ test_err_xml('err_elem_extra', 'bogus TA XML with an extra element'),
+ test_err_xml('err_elem_missing', 'bogus TA XML without mandatory element'),
+ test_err_xml('err_multi_ta', 'bogus TA XML with multiple TAs'),
+ test_err_xml('unsupp_nonroot', 'unsupported TA XML for non-root zone'),
+ test_err_xml('unsupp_xml_v11', 'unsupported TA XML with XML v1.1'),
+ test_err_xml('ok0_badtimes', 'TA XML with no valid keys'),
+ test_ok_xml('ok1_expired1', 'TA XML with 1 valid and 1 expired key'),
+ test_ok_xml('ok1_notyet1', 'TA XML with 1 valid and 1 not yet valid key'),
+ test_ok_xml('ok1', 'TA XML with 1 valid key'),
+ test_ok_xml('ok2', 'TA XML with 2 valid keys'),
+}
diff --git a/daemon/lua/trust_anchors.test/err_attr_extra_attr.xml b/daemon/lua/trust_anchors.test/err_attr_extra_attr.xml
new file mode 100644
index 0000000..2a87957
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/err_attr_extra_attr.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TrustAnchor id="FC4A93EC-9F4E-4597-A766-AD6723E4A56E" source="https://localhost/err_attr_extra_attr.xml">
+<Zone>.</Zone>
+<KeyDigest unknownattr="test" id="Kjqmt7v" validFrom="2010-07-15T00:00:00+00:00" validUntil="2019-01-11T00:00:00+00:00">
+<KeyTag>19036</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5</Digest>
+</KeyDigest>
+<KeyDigest id="Klajeyz" validFrom="2017-02-02T00:00:00+00:00">
+<KeyTag>20326</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</Digest>
+</KeyDigest>
+</TrustAnchor>
diff --git a/daemon/lua/trust_anchors.test/err_attr_validfrom_invalid.xml b/daemon/lua/trust_anchors.test/err_attr_validfrom_invalid.xml
new file mode 100644
index 0000000..5a4c68c
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/err_attr_validfrom_invalid.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TrustAnchor id="ABD668AB-52DF-4A59-80E3-16CE6341BC55" source="https://localhost/err_attr_validfrom_invalid.xml">
+<Zone>.</Zone>
+<KeyDigest id="Kjqmt7v" validFrom="2010-07-32T00:00:00+00:00" validUntil="2019-01-11T00:00:00+00:00">
+<KeyTag>19036</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5</Digest>
+</KeyDigest>
+<KeyDigest id="Klajeyz" validFrom="2017-02-02T00:00:00+00:00">
+<KeyTag>20326</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</Digest>
+</KeyDigest>
+</TrustAnchor>
diff --git a/daemon/lua/trust_anchors.test/err_attr_validfrom_missing.xml b/daemon/lua/trust_anchors.test/err_attr_validfrom_missing.xml
new file mode 100644
index 0000000..1261b09
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/err_attr_validfrom_missing.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TrustAnchor id="3513058C-4041-40CC-AF0A-D3CCD70F962B" source="https://localhost/err_attr_validfrom_missing.xml">
+<Zone>.</Zone>
+<KeyDigest id="Kjqmt7v" validUntil="2019-01-11T00:00:00+00:00">
+<KeyTag>19036</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5</Digest>
+</KeyDigest>
+<KeyDigest id="Klajeyz" validFrom="2017-02-02T00:00:00+00:00">
+<KeyTag>20326</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</Digest>
+</KeyDigest>
+</TrustAnchor>
diff --git a/daemon/lua/trust_anchors.test/err_elem_extra.xml b/daemon/lua/trust_anchors.test/err_elem_extra.xml
new file mode 100644
index 0000000..150a3b1
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/err_elem_extra.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TrustAnchor id="B1854D58-1867-4FA7-872F-0099D394114D" source="https://localhost/err_elem_extra.xml">
+<Zone>.</Zone>
+<KeyDigest id="Kjqmt7v" validFrom="2010-07-15T00:00:00+00:00" validUntil="2019-01-11T00:00:00+00:00">
+<KeyTag>19036</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5</Digest>
+</KeyDigest>
+<KeyDigest id="Klajeyz" validFrom="2017-02-02T00:00:00+00:00">
+<KeyTag>20326</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</Digest>
+<UnknownElement>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</UnknownElement>
+</KeyDigest>
+</TrustAnchor>
diff --git a/daemon/lua/trust_anchors.test/err_elem_missing.xml b/daemon/lua/trust_anchors.test/err_elem_missing.xml
new file mode 100644
index 0000000..899e1d0
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/err_elem_missing.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TrustAnchor id="BB074095-3A42-4B13-9CC1-CFFF644D4D54" source="https://localhost/err_elem_missing.xml">
+<Zone>.</Zone>
+<KeyDigest id="Kjqmt7v" validFrom="2010-07-15T00:00:00+00:00" validUntil="2019-01-11T00:00:00+00:00">
+<KeyTag>19036</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5</Digest>
+</KeyDigest>
+<KeyDigest id="Klajeyz" validFrom="2017-02-02T00:00:00+00:00">
+<KeyTag>20326</KeyTag>
+<Algorithm>8</Algorithm>
+<!-- this element is missing: DigestType>2</DigestType-->
+<Digest>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</Digest>
+</KeyDigest>
+</TrustAnchor>
diff --git a/daemon/lua/trust_anchors.test/err_multi_ta.xml b/daemon/lua/trust_anchors.test/err_multi_ta.xml
new file mode 100644
index 0000000..20cd73f
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/err_multi_ta.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TrustAnchor id="9DCE46E1-FC78-48E1-81B5-94E328790BB5" source="https://localhost/err_multi_ta.xml">
+<Zone>.</Zone>
+<KeyDigest id="1" validFrom="2000-02-02T00:00:00+00:00">
+<KeyTag>2</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
+</KeyDigest>
+</TrustAnchor>
+<TrustAnchor id="9DCE46E1-FC78-48E1-81B5-94E328790BB5" source="https://localhost/err_multi_ta.xml">
+<Zone>test.</Zone>
+<KeyDigest id="2" validFrom="2000-02-02T00:00:00+00:00">
+<KeyTag>2</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
+</KeyDigest>
+</TrustAnchor>
diff --git a/daemon/lua/trust_anchors.test/ok0_badtimes.xml b/daemon/lua/trust_anchors.test/ok0_badtimes.xml
new file mode 100644
index 0000000..4535a41
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/ok0_badtimes.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TrustAnchor id="EDEDAA08-D2A0-421E-81DC-AF11F5A0CDCD" source="https://localhost/ok0_badtimes.xml">
+<Zone>.</Zone>
+<KeyDigest id="E" validFrom="2000-01-01T00:00:00+00:00" validUntil="2000-01-01T00:00:00+00:00">
+<KeyTag>1</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE</Digest>
+</KeyDigest>
+<KeyDigest id="F" validFrom="2001-01-01T00:00:00+00:00" validUntil="2001-01-01T00:00:00+00:00">
+<KeyTag>2</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF</Digest>
+</KeyDigest>
+</TrustAnchor>
diff --git a/daemon/lua/trust_anchors.test/ok1.xml b/daemon/lua/trust_anchors.test/ok1.xml
new file mode 100644
index 0000000..117495c
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/ok1.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TrustAnchor id="82E6CB77-12DF-4E61-BF49-367FB95A8BAA" source="https://localhost/ok1.xml">
+<Zone>.</Zone>
+<KeyDigest id="2" validFrom="2000-02-02T00:00:00+00:00">
+<KeyTag>2</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
+</KeyDigest>
+</TrustAnchor>
diff --git a/daemon/lua/trust_anchors.test/ok1_expired1.xml b/daemon/lua/trust_anchors.test/ok1_expired1.xml
new file mode 100644
index 0000000..f1269da
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/ok1_expired1.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TrustAnchor id="68463155-A857-4C7E-BCA6-2F6CC2EAC1BE" source="https://localhost/ok1_expired1.xml">
+<Zone>.</Zone>
+<KeyDigest id="F" validFrom="1990-01-01T00:00:00+00:00" validUntil="2000-01-01T00:00:00+00:00">
+<KeyTag>1</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF</Digest>
+</KeyDigest>
+<KeyDigest id="1" validFrom="2000-01-01T00:00:00+00:00">
+<KeyTag>2</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
+</KeyDigest>
+</TrustAnchor>
diff --git a/daemon/lua/trust_anchors.test/ok1_notyet1.xml b/daemon/lua/trust_anchors.test/ok1_notyet1.xml
new file mode 100644
index 0000000..7b5881b
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/ok1_notyet1.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TrustAnchor id="507B39D5-049E-467C-9E9A-F5BE597C9DDA" source="https://localhost/ok1_notyet1.xml">
+<Zone>.</Zone>
+<KeyDigest id="1" validFrom="2010-07-15T00:00:00+00:00">
+<KeyTag>1</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
+</KeyDigest>
+<KeyDigest id="2" validFrom="2050-12-31T23:59:59+00:00">
+<KeyTag>2</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF</Digest>
+</KeyDigest>
+</TrustAnchor>
diff --git a/daemon/lua/trust_anchors.test/ok2.xml b/daemon/lua/trust_anchors.test/ok2.xml
new file mode 100644
index 0000000..149f6b5
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/ok2.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TrustAnchor id="1DECEB91-0591-44A1-95CF-1788337514B8" source="https://localhost/ok2.xml">
+<Zone>.</Zone>
+<KeyDigest id="K1" validFrom="2010-07-15T00:00:00+00:00">
+<KeyTag>1</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
+</KeyDigest>
+<KeyDigest id="K2" validFrom="2011-02-02T00:00:00+00:00">
+<KeyTag>2</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>2222222222222222222222222222222222222222222222222222222222222222</Digest>
+</KeyDigest>
+</TrustAnchor>
diff --git a/daemon/lua/trust_anchors.test/regen.sh b/daemon/lua/trust_anchors.test/regen.sh
new file mode 100755
index 0000000..9e7dac1
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/regen.sh
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+for F in *.xml; do sed -i "s/TrustAnchor id=\"[^\"]*\"/TrustAnchor id=\"$(uuidgen | tr '[[:lower:]]' '[[:upper:]]')\"/" $F; done
+for F in *.xml; do sed -i "s#source=\"[^\"]*\"#source=\"https://localhost/$F\"#" $F; done
diff --git a/daemon/lua/trust_anchors.test/root.keys b/daemon/lua/trust_anchors.test/root.keys
new file mode 100644
index 0000000..e292b5a
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/root.keys
@@ -0,0 +1 @@
+. IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D
diff --git a/daemon/lua/trust_anchors.test/ta.test.lua b/daemon/lua/trust_anchors.test/ta.test.lua
new file mode 100644
index 0000000..b977bc9
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/ta.test.lua
@@ -0,0 +1,85 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+trust_anchors.remove('.')
+
+local ffi = require('ffi')
+
+-- count warning messages
+warn_msg = {}
+overriding_msg="warning: overriding previously set trust anchors for ."
+warn_msg[overriding_msg] = 0
+function log_warn(grp, fmt, ...) --luacheck: no unused args
+ msg = string.format(fmt, ...)
+ if warn_msg[msg] ~= nil then
+ warn_msg[msg] = warn_msg[msg] + 1
+ end
+end
+
+-- Test that adding a revoked DNSKEY is refused.
+local function test_revoked_key()
+ local ta_c = kres.context().trust_anchors
+ same(ffi.C.kr_ta_del(ta_c, '\0'), 0, 'remove root TAs if any')
+ -- same() doesn't consider nil and typed NULL pointer equal, so we work around:
+ same(ffi.C.kr_ta_get(ta_c, '\0') == nil, true, 'no TA for root is used')
+ local key_crypto = 'AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjFFV'
+ .. 'QUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoXbfDaUeVPQuYEhg37'
+ .. 'NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaDX6RS6CXpoY68LsvPVjR0ZSwzz1apAz'
+ .. 'vN9dlzEheX7ICJBBtuA6G3LQpzW5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7O'
+ .. 'yQdXfZ57relSQageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulqQxA+Uk1ihz0='
+ boom(trust_anchors.add, { '. 3600 DNSKEY 385 3 8 ' .. key_crypto }, 'refuse revoked key')
+ same(ffi.C.kr_ta_get(ta_c, '\0') == nil, true, 'no TA for root is used')
+ -- Test that we don't have another problem in the key
+ trust_anchors.add('. 3600 DNSKEY 257 3 8 ' .. key_crypto)
+ local root_ta = ffi.C.kr_ta_get(ta_c, '\0')
+ same(root_ta == nil, false, 'we got non-NULL TA RRset')
+ same(root_ta.rrs.count, 1, 'the root TA set contains one RR')
+end
+
+local function test_remove()
+ -- uses root key from the previous test
+ assert(trust_anchors.keysets['\0'], 'root key must be there from previous test')
+ local ta_c = kres.context().trust_anchors
+ local root_ta = ffi.C.kr_ta_get(ta_c, '\0')
+ assert(root_ta ~= nil, 'we got non-NULL TA RRset')
+ assert(root_ta.rrs.count, 1, 'we have a root TA set to be deleted')
+
+ trust_anchors.remove('.')
+
+ same(trust_anchors.keysets['\0'], nil, 'Lua interface does not have the removed key')
+ root_ta = ffi.C.kr_ta_get(ta_c, '\0')
+ same(root_ta == nil, true, 'C interface does not have the removed key')
+end
+
+local function test_add_file()
+ boom(trust_anchors.add_file, {'nonwriteable/root.keys', false},
+ "Managed trust anchor in non-writeable directory")
+
+ boom(trust_anchors.add_file, {'nonexistent.keys', true},
+ "Nonexistent unmanaged trust anchor file")
+
+ is(warn_msg[overriding_msg], 0, "No override warning messages at start of test")
+ trust_anchors.add_file('root.keys', true)
+ trust_anchors.add_file('root.keys', true)
+ is(warn_msg[overriding_msg], 1, "Warning message when override trust anchors")
+
+ is(trust_anchors.keysets['\0'][1].key_tag, 20326,
+ "Loaded KeyTag from root.keys")
+end
+
+local function test_nta()
+ assert(trust_anchors.keysets['\0'], 'root key must be there from previous tests')
+
+ trust_anchors.set_insecure({'example.com'})
+ is(trust_anchors.insecure[1], 'example.com', 'Add example.com to NTA list')
+ boom(trust_anchors.set_insecure, {{'.'}}, 'Got error when adding TA . to NTA list')
+ is(#trust_anchors.insecure, 1, 'Check one item in NTA list')
+ is(trust_anchors.insecure[1], 'example.com', 'Check previous NTA list')
+end
+
+return {
+ test_revoked_key,
+ test_remove,
+ test_add_file,
+ test_nta,
+}
+
diff --git a/daemon/lua/trust_anchors.test/unsupp_nonroot.xml b/daemon/lua/trust_anchors.test/unsupp_nonroot.xml
new file mode 100644
index 0000000..51b3c0a
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/unsupp_nonroot.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TrustAnchor id="8449BFB8-FD6C-4082-B0FE-1A3E3399203B" source="https://localhost/unsupp_nonroot.xml">
+<Zone>test.</Zone>
+<KeyDigest id="2" validFrom="2000-02-02T00:00:00+00:00">
+<KeyTag>2</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
+</KeyDigest>
+</TrustAnchor>
diff --git a/daemon/lua/trust_anchors.test/unsupp_xml_v11.xml b/daemon/lua/trust_anchors.test/unsupp_xml_v11.xml
new file mode 100644
index 0000000..87a4b57
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/unsupp_xml_v11.xml
@@ -0,0 +1,10 @@
+<?xml version="1.1" encoding="UTF-8"?>
+<TrustAnchor id="3612AE1C-E8F3-4FD8-B8CD-96C7FDACC7A5" source="https://localhost/unsupp_xml_v11.xml">
+<Zone>.</Zone>
+<KeyDigest id="2" validFrom="2000-02-02T00:00:00+00:00">
+<KeyTag>2</KeyTag>
+<Algorithm>8</Algorithm>
+<DigestType>2</DigestType>
+<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
+</KeyDigest>
+</TrustAnchor>
diff --git a/daemon/lua/trust_anchors.test/webserv.lua b/daemon/lua/trust_anchors.test/webserv.lua
new file mode 100644
index 0000000..c108bba
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/webserv.lua
@@ -0,0 +1,236 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+-- This is a module that does the heavy lifting to provide an HTTP/2 enabled
+-- server that supports TLS by default and provides endpoint for other modules
+-- in order to enable them to export restful APIs and websocket streams.
+-- One example is statistics module that can stream live metrics on the website,
+-- or publish metrics on request for Prometheus scraper.
+local http_server = require('http.server')
+local http_headers = require('http.headers')
+local http_websocket = require('http.websocket')
+local http_util = require "http.util"
+local x509, pkey = require('openssl.x509'), require('openssl.pkey')
+
+-- Module declaration
+local M = {}
+
+-- Export HTTP service endpoints
+M.endpoints = {
+ ['/'] = {'text/html', 'test'},
+}
+
+-- Serve known requests, for methods other than GET
+-- the endpoint must be a closure and not a preloaded string
+local function serve(endpoints, h, stream)
+ local hsend = http_headers.new()
+ local path = h:get(':path')
+ local entry = endpoints[path]
+ if not entry then -- Accept top-level path match
+ entry = endpoints[path:match '^/[^/?]*']
+ end
+ -- Unpack MIME and data
+ local data, mime, ttl, err
+ if entry then
+ mime = entry[1]
+ data = entry[2]
+ ttl = entry[4]
+ end
+ -- Get string data out of service endpoint
+ if type(data) == 'function' then
+ local set_mime, set_ttl
+ data, err, set_mime, set_ttl = data(h, stream)
+ -- Override default endpoint mime/TTL
+ if set_mime then mime = set_mime end
+ if set_ttl then ttl = set_ttl end
+ -- Handler doesn't provide any data
+ if data == false then return end
+ if type(data) == 'number' then return tostring(data), err end
+ -- Methods other than GET require handler to be closure
+ elseif h:get(':method') ~= 'GET' then
+ return '501', ''
+ end
+ if not mime or type(data) ~= 'string' then
+ return '404', ''
+ else
+ -- Serve content type appropriately
+ hsend:append(':status', '200')
+ hsend:append('content-type', mime)
+ hsend:append('content-length', tostring(#data))
+ if ttl then
+ hsend:append('cache-control', string.format('max-age=%d', ttl))
+ end
+ assert(stream:write_headers(hsend, false))
+ assert(stream:write_chunk(data, true))
+ end
+end
+
+-- Web server service closure
+local function route(endpoints)
+ return function (_, stream)
+ -- HTTP/2: We're only permitted to send in open/half-closed (remote)
+ local connection = stream.connection
+ if connection.version >= 2 then
+ if stream.state ~= 'open' and stream.state ~= 'half closed (remote)' then
+ return
+ end
+ end
+ -- Start reading headers
+ local h = assert(stream:get_headers())
+ local m = h:get(':method')
+ local path = h:get(':path')
+ -- Upgrade connection to WebSocket
+ local ws = http_websocket.new_from_stream(stream, h)
+ if ws then
+ assert(ws:accept { protocols = {'json'} })
+ -- Continue streaming results to client
+ local ep = endpoints[path]
+ local cb = ep[3]
+ if cb then
+ cb(h, ws)
+ end
+ ws:close()
+ return
+ else
+ local ok, err, reason = http_util.yieldable_pcall(serve, endpoints, h, stream)
+ if not ok or err then
+ print(string.format('%s err %s %s: %s (%s)', os.date(), m, path, err or '500', reason))
+ -- Method is not supported
+ local hsend = http_headers.new()
+ hsend:append(':status', err or '500')
+ if reason then
+ assert(stream:write_headers(hsend, false))
+ assert(stream:write_chunk(reason, true))
+ else
+ assert(stream:write_headers(hsend, true))
+ end
+ else
+ print(string.format('%s ok %s %s', os.date(), m, path))
+ end
+ end
+ end
+end
+
+-- @function Prefer HTTP/2 or HTTP/1.1
+local function alpnselect(_, protos)
+ for _, proto in ipairs(protos) do
+ if proto == 'h2' or proto == 'http/1.1' then
+ return proto
+ end
+ end
+ return nil
+end
+
+-- @function Create TLS context
+local function tlscontext(crt, key)
+ local http_tls = require('http.tls')
+ local ctx = http_tls.new_server_context()
+ if ctx.setAlpnSelect then
+ ctx:setAlpnSelect(alpnselect)
+ end
+ assert(ctx:setPrivateKey(key))
+ assert(ctx:setCertificate(crt))
+ return ctx
+end
+
+-- @function Listen on given HTTP(s) host
+function M.add_interface(conf)
+ local crt, key
+ if conf.tls ~= false then
+ assert(conf.cert, 'cert missing')
+ assert(conf.key, 'private key missing')
+ -- Check if a cert file was specified
+ -- Read x509 certificate
+ local f = io.open(conf.cert, 'r')
+ if f then
+ crt = assert(x509.new(f:read('*all')))
+ f:close()
+ -- Continue reading key file
+ if crt then
+ f = io.open(conf.key, 'r')
+ key = assert(pkey.new(f:read('*all')))
+ f:close()
+ end
+ end
+ -- Check loaded certificate
+ assert(crt and key,
+ string.format('failed to load certificate "%s"', conf.cert))
+ end
+ -- Compose server handler
+ local routes = route(conf.endpoints or M.endpoints)
+ -- Check if UNIX socket path is used
+ local addr_str
+ if not conf.path then
+ conf.host = conf.host or 'localhost'
+ conf.port = conf.port or 8453
+ addr_str = string.format('%s@%d', conf.host, conf.port)
+ else
+ if conf.host or conf.port then
+ error('either "path", or "host" and "port" must be provided')
+ end
+ addr_str = conf.path
+ end
+ -- Create TLS context and start listening
+ local s, err = http_server.listen {
+ -- cq = worker.bg_worker.cq,
+ host = conf.host,
+ port = conf.port,
+ path = conf.path,
+ v6only = conf.v6only,
+ unlink = conf.unlink,
+ reuseaddr = conf.reuseaddr,
+ reuseport = conf.reuseport,
+ client_timeout = conf.client_timeout or 5,
+ ctx = crt and tlscontext(crt, key),
+ tls = conf.tls,
+ onstream = routes,
+ -- Log errors, but do not throw
+ onerror = function(myserver, context, op, err, errno) -- luacheck: ignore 212
+ local msg = '[http] ' .. op .. ' on ' .. tostring(context) .. ' failed'
+ if err then
+ msg = msg .. ': ' .. tostring(err)
+ end
+ print(msg)
+ end,
+ }
+ -- Manually call :listen() so that we are bound before calling :localname()
+ if s then
+ err = select(2, s:listen())
+ end
+ assert(not err, string.format('failed to listen on %s: %s', addr_str, err))
+ return s
+end
+
+-- init
+local files = {
+ 'ok0_badtimes.xml',
+ 'ok1.xml',
+ 'ok1_expired1.xml',
+ 'ok1_notyet1.xml',
+ 'ok2.xml',
+ 'err_attr_validfrom_missing.xml',
+ 'err_attr_validfrom_invalid.xml',
+ 'err_attr_extra_attr.xml',
+ 'err_elem_missing.xml',
+ 'err_elem_extra.xml',
+ 'err_multi_ta.xml',
+ 'unsupp_nonroot.xml',
+ 'unsupp_xml_v11.xml'
+}
+
+-- Export static pages specified at command line
+for _, name in ipairs(files) do
+ local fd = io.open(name)
+ assert(fd, string.format('unable to open file "%s"', name))
+ M.endpoints['/' .. name] = { 'text/xml', fd:read('*a') }
+ fd:close()
+end
+
+local server = M.add_interface({
+ host = 'localhost',
+ port = 8080,
+ tls = true,
+ cert = 'x509/server.pem',
+ key = 'x509/server-key.pem'
+ })
+
+server:loop()
diff --git a/daemon/lua/trust_anchors.test/x509/ca-key.pem b/daemon/lua/trust_anchors.test/x509/ca-key.pem
new file mode 100644
index 0000000..2e95b23
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/ca-key.pem
@@ -0,0 +1,182 @@
+Public Key Info:
+ Public Key Algorithm: RSA
+ Key Security Level: High (3072 bits)
+
+modulus:
+ 00:9e:ee:f2:d8:41:ae:2c:93:8a:01:1f:88:5b:d6:85
+ 29:2f:91:9d:37:fc:35:88:7f:53:71:87:fc:17:71:e7
+ 15:57:06:2d:54:fb:19:98:80:82:ec:1a:99:2d:57:cb
+ 5f:dd:28:26:d8:95:fb:65:b2:be:e1:11:86:69:14:7e
+ 32:5c:c0:02:0b:5d:11:78:69:50:20:25:3e:15:fb:8a
+ 46:d1:83:f9:3b:84:46:9c:69:21:44:d5:09:1d:7a:04
+ cc:f3:6a:ea:4c:1b:da:7c:40:dd:1c:6f:f6:85:b4:ea
+ 75:98:34:79:11:fb:cf:d3:18:70:64:25:33:8a:31:b6
+ 93:67:d4:32:67:61:1c:d0:7b:85:61:54:c6:fb:51:b6
+ 87:1d:d4:b8:58:40:a9:c5:32:ce:e0:b9:90:37:0d:58
+ e4:33:70:c5:c5:91:f2:18:f5:e0:08:ad:17:8b:cf:72
+ f1:26:6c:9c:88:d2:9e:06:4c:02:5d:4e:7c:93:af:8d
+ 72:93:75:1d:60:0c:f7:34:09:a8:e6:f2:80:4a:14:81
+ 24:40:4b:45:19:85:2e:ad:8e:97:4c:ff:ec:d0:9f:e6
+ a0:b7:c0:a9:a0:ad:d2:02:2d:13:55:f3:df:f8:f9:f1
+ f3:3e:35:e9:08:2b:db:11:93:57:13:55:c6:ba:c6:d7
+ ff:7d:e1:fa:8c:47:5d:da:bf:31:56:80:aa:34:97:43
+ bb:9e:ff:d3:e6:13:a9:c2:99:49:c2:1e:da:f2:c7:d2
+ d6:f7:5f:70:36:91:2f:ea:36:e9:88:44:08:a3:1a:0a
+ c0:e0:4b:48:82:9a:c9:72:29:9c:09:24:63:b3:c2:9f
+ 2a:f6:e8:3a:c4:46:03:8d:70:ae:14:bb:3a:d6:c6:62
+ 93:24:7f:bc:0a:c8:a2:20:53:3c:9f:5c:15:45:05:3d
+ 1b:38:17:d4:fe:6b:6a:c2:16:f3:14:73:c2:c3:c7:36
+ e3:f1:f8:e5:28:84:4e:37:d4:68:e8:82:70:20:53:fc
+ 01:
+
+public exponent:
+ 01:00:01:
+
+private exponent:
+ 44:4a:68:0e:84:2a:52:fd:12:4f:69:3d:2e:38:fe:fe
+ b3:71:de:1c:30:42:d0:63:e5:76:e2:f7:6f:1b:82:2f
+ a9:34:fe:45:85:9f:79:e7:be:59:b5:14:1c:67:9c:fb
+ 94:0b:ac:a5:63:cc:a1:e6:2c:1e:89:69:37:bd:96:7c
+ 0d:5e:73:82:6e:7b:13:42:2d:2b:a2:d5:0a:9d:0a:cd
+ 63:39:51:de:40:f8:16:3d:16:0e:7d:7b:6d:2f:00:e1
+ 0f:b6:e0:f5:d3:02:0e:61:d0:a0:67:7b:85:f8:36:c6
+ 50:a0:3b:65:7e:cd:cd:e6:b2:64:55:97:cf:c9:8c:a9
+ c9:f3:63:b5:08:05:59:8f:b9:c0:18:ad:67:4f:b5:1e
+ 59:b3:0d:82:de:46:14:75:c0:6e:cf:4d:28:5a:93:d7
+ 7a:42:fa:b7:e9:fe:1c:bb:89:88:30:d7:ed:3b:36:28
+ 68:5a:42:e8:87:97:5f:1d:49:e6:cd:d2:b9:a2:b5:23
+ d8:df:5d:cf:c6:98:9a:e0:99:7a:33:52:75:22:ce:ca
+ 85:eb:d9:92:6a:d5:49:c0:cb:df:b1:a2:98:b5:6c:37
+ 85:c2:e1:6a:13:48:22:72:02:a7:e2:e0:f3:f3:0c:ed
+ 42:f6:83:ba:71:f0:ef:8f:ce:6a:59:30:be:9d:5f:23
+ 06:c3:0e:49:5c:8f:6a:8d:c2:c3:c5:07:45:55:78:f0
+ bd:29:01:cb:ac:ec:b1:40:7d:78:cc:4d:cb:f9:60:a4
+ a2:f5:aa:21:0b:3e:da:1b:d9:f0:99:19:44:57:21:09
+ ba:0f:f9:05:8e:ee:59:4f:59:08:b1:67:51:02:80:4e
+ 34:c7:5d:25:79:8c:84:f7:be:15:02:28:9c:f9:b9:ca
+ fc:6a:ed:d0:5f:df:be:ce:c4:96:63:23:2e:db:e1:85
+ 1e:45:16:2c:24:b0:5e:7a:62:bf:36:00:8b:c8:90:61
+ c2:68:4b:95:b0:ce:41:77:a3:a0:5d:09:72:01:a0:01
+
+
+prime1:
+ 00:ca:fe:eb:14:07:13:a9:ef:b6:d5:6c:52:02:39:b6
+ 6e:55:b6:dd:70:fc:c6:04:7c:07:81:9b:98:a4:da:db
+ f1:66:b8:33:91:fd:00:15:6e:72:0d:ab:0b:6f:be:34
+ c8:d9:82:58:7e:09:7f:e4:6f:c0:70:99:53:68:c7:53
+ d2:8f:97:22:f5:e8:e4:be:5f:e1:29:0f:27:a7:66:74
+ b2:cc:96:a1:d2:ca:2a:40:4b:70:cc:7a:16:4f:c9:4f
+ 49:16:11:d5:f8:da:f6:92:06:1c:45:c3:f8:17:c4:1d
+ 65:9a:2a:3f:33:be:33:f8:84:03:26:49:d8:52:25:f8
+ 19:ce:31:00:c0:b6:55:71:74:03:53:e8:0c:ef:85:64
+ 54:d4:8f:68:08:87:da:cb:9b:55:6a:2e:2b:c2:95:36
+ c4:dd:09:62:c0:6f:9e:e9:cc:ea:96:4e:e6:2d:6c:72
+ c1:54:92:11:29:91:af:4d:cd:08:7c:f8:6f:28:9d:ca
+ c5:
+
+prime2:
+ 00:c8:6e:b7:af:c3:c6:b9:df:49:ad:ea:b8:62:b7:43
+ e6:04:d9:5a:df:c3:f9:a3:0d:b5:e6:2d:9d:f7:c4:ff
+ 38:c0:cb:03:9c:c1:d1:6f:b4:fb:cf:81:c4:9e:94:2a
+ d5:e2:a1:77:a8:7b:8f:d1:34:7a:c2:f5:38:ec:0d:35
+ a0:5e:3c:af:e9:2d:f4:f9:32:ae:da:c1:1e:62:74:e5
+ ab:3d:3b:3d:d0:88:fc:53:59:0f:21:30:ed:24:ac:a7
+ 5a:a5:b7:f4:cc:5a:96:ad:79:a3:41:74:56:ad:39:14
+ 0a:27:a4:10:18:19:33:f2:1a:aa:b4:36:9d:fa:3f:fc
+ 71:42:1e:a0:96:8e:0f:de:46:87:ba:6c:38:17:d0:7e
+ c8:4a:cb:4a:29:1c:44:b9:88:29:c7:6f:b7:4f:3f:00
+ cd:0c:6b:0f:77:a1:5a:f1:80:21:91:b3:68:ca:0d:b5
+ c9:6d:04:f4:98:94:9f:09:f9:a8:58:ea:34:9c:d8:f0
+ 0d:
+
+coefficient:
+ 00:bf:7b:93:68:64:ea:5e:b5:f4:b6:8c:91:49:aa:2b
+ b0:a5:74:40:73:45:23:b6:74:ae:7a:55:ae:9d:8a:bb
+ 3b:6d:3a:7d:c4:7a:c0:82:7f:0e:ef:57:1c:86:e2:56
+ 30:5b:0c:d9:d1:52:cf:df:10:4f:c8:4a:75:b1:b8:b1
+ 59:9b:01:02:a2:4d:29:aa:63:e5:11:0a:17:ae:1e:79
+ ed:5d:10:fc:f0:8a:8d:f6:77:f8:78:17:1e:07:ee:d1
+ de:59:ed:d7:fb:94:bf:c9:7c:f7:f3:a0:8d:66:d3:94
+ 9a:7f:d1:7a:89:87:71:17:96:90:4e:be:7b:54:5e:51
+ 03:c6:35:af:5f:ea:5d:cc:31:ab:56:4b:75:6a:14:b6
+ c0:1a:bf:fb:e9:54:ba:ad:c3:52:e9:85:03:db:b2:e8
+ 0b:18:60:37:19:f9:07:87:e7:b2:d8:3a:0d:c9:d5:f0
+ f0:73:60:fc:9e:e0:9e:b1:ea:52:71:c7:fd:27:0a:22
+ 42:
+
+exp1:
+ 04:a5:69:04:00:55:76:e2:41:b1:08:d5:a7:af:62:79
+ 8c:04:af:74:d2:94:45:ae:01:0d:fa:5d:b8:08:3a:58
+ 80:1d:5f:30:cc:35:a0:47:f2:dc:55:39:e1:c8:dc:b4
+ 6c:26:0b:98:76:e7:32:77:4c:54:47:6e:1d:4b:d2:a3
+ 53:1e:06:72:d2:6d:c9:dd:af:ed:9c:a7:2f:b1:ac:a2
+ 1b:04:a7:97:87:81:08:0f:b1:f9:3c:22:1e:99:60:f3
+ 2f:4a:21:37:9c:eb:5f:d4:3b:f9:6c:ce:d4:dc:6c:3f
+ d3:13:7c:76:d6:b7:a1:cc:83:b5:f0:a2:be:de:97:9b
+ 1f:99:07:87:61:a4:fa:ff:c8:c3:b6:df:f4:eb:7c:ac
+ 64:61:13:e4:7e:17:87:e9:7a:3e:ff:e7:88:80:99:cc
+ 4f:b2:d3:4b:cc:42:3a:df:b1:ce:d6:e7:75:ca:b1:a5
+ b3:25:d4:b6:ba:da:e4:50:f9:0c:c3:32:e8:1f:14:71
+
+
+exp2:
+ 00:9d:9d:bb:a3:63:b0:96:20:8a:5f:52:f2:b6:e6:69
+ 65:ac:30:84:ee:ec:bd:16:45:44:e3:02:c2:73:c2:9d
+ f5:b2:5e:b3:b3:85:13:3e:e6:33:13:66:78:09:40:79
+ 43:03:5a:78:af:ac:a3:57:20:0e:dd:db:5b:6c:fd:a2
+ 4b:3f:70:37:e1:85:fb:5c:30:48:22:cc:b5:29:35:c6
+ 1a:58:27:8b:1f:bf:69:b5:dd:96:31:42:b9:6a:1a:bf
+ ec:5f:df:7e:89:69:3f:8e:a1:d6:09:36:04:a7:69:f8
+ 61:57:f5:03:96:ff:d2:c2:b3:c7:c3:ba:23:97:54:d4
+ 1e:f8:a0:ff:26:06:07:62:83:52:5e:fe:95:49:dc:f2
+ a6:6d:72:da:19:e4:1a:03:50:99:92:35:3f:10:f9:79
+ 96:c6:0a:36:fa:9b:8f:d9:d4:2a:11:da:e5:2f:e7:82
+ 2a:29:2a:39:72:f7:84:ed:a2:3f:89:d4:7b:95:50:5f
+ cd:
+
+
+Public Key PIN:
+ pin-sha256:u7TPTyh/innOijbJFG3Y4pWghApErLvhCQUZNXBlVFU=
+Public Key ID:
+ sha256:bbb4cf4f287f8a79ce8a36c9146dd8e295a0840a44acbbe10905193570655455
+ sha1:92b7d0c4d107e2a73f827b87866aef9ff4379cc8
+
+-----BEGIN RSA PRIVATE KEY-----
+MIIG5AIBAAKCAYEAnu7y2EGuLJOKAR+IW9aFKS+RnTf8NYh/U3GH/Bdx5xVXBi1U
++xmYgILsGpktV8tf3Sgm2JX7ZbK+4RGGaRR+MlzAAgtdEXhpUCAlPhX7ikbRg/k7
+hEacaSFE1QkdegTM82rqTBvafEDdHG/2hbTqdZg0eRH7z9MYcGQlM4oxtpNn1DJn
+YRzQe4VhVMb7UbaHHdS4WECpxTLO4LmQNw1Y5DNwxcWR8hj14AitF4vPcvEmbJyI
+0p4GTAJdTnyTr41yk3UdYAz3NAmo5vKAShSBJEBLRRmFLq2Ol0z/7NCf5qC3wKmg
+rdICLRNV89/4+fHzPjXpCCvbEZNXE1XGusbX/33h+oxHXdq/MVaAqjSXQ7ue/9Pm
+E6nCmUnCHtryx9LW919wNpEv6jbpiEQIoxoKwOBLSIKayXIpnAkkY7PCnyr26DrE
+RgONcK4UuzrWxmKTJH+8CsiiIFM8n1wVRQU9GzgX1P5rasIW8xRzwsPHNuPx+OUo
+hE431GjognAgU/wBAgMBAAECggGAREpoDoQqUv0ST2k9Ljj+/rNx3hwwQtBj5Xbi
+928bgi+pNP5FhZ95575ZtRQcZ5z7lAuspWPMoeYsHolpN72WfA1ec4JuexNCLSui
+1QqdCs1jOVHeQPgWPRYOfXttLwDhD7bg9dMCDmHQoGd7hfg2xlCgO2V+zc3msmRV
+l8/JjKnJ82O1CAVZj7nAGK1nT7UeWbMNgt5GFHXAbs9NKFqT13pC+rfp/hy7iYgw
+1+07NihoWkLoh5dfHUnmzdK5orUj2N9dz8aYmuCZejNSdSLOyoXr2ZJq1UnAy9+x
+opi1bDeFwuFqE0gicgKn4uDz8wztQvaDunHw74/Oalkwvp1fIwbDDklcj2qNwsPF
+B0VVePC9KQHLrOyxQH14zE3L+WCkovWqIQs+2hvZ8JkZRFchCboP+QWO7llPWQix
+Z1ECgE40x10leYyE974VAiic+bnK/Grt0F/fvs7ElmMjLtvhhR5FFiwksF56Yr82
+AIvIkGHCaEuVsM5Bd6OgXQlyAaABAoHBAMr+6xQHE6nvttVsUgI5tm5Vtt1w/MYE
+fAeBm5ik2tvxZrgzkf0AFW5yDasLb740yNmCWH4Jf+RvwHCZU2jHU9KPlyL16OS+
+X+EpDyenZnSyzJah0soqQEtwzHoWT8lPSRYR1fja9pIGHEXD+BfEHWWaKj8zvjP4
+hAMmSdhSJfgZzjEAwLZVcXQDU+gM74VkVNSPaAiH2subVWouK8KVNsTdCWLAb57p
+zOqWTuYtbHLBVJIRKZGvTc0IfPhvKJ3KxQKBwQDIbrevw8a530mt6rhit0PmBNla
+38P5ow215i2d98T/OMDLA5zB0W+0+8+BxJ6UKtXioXeoe4/RNHrC9TjsDTWgXjyv
+6S30+TKu2sEeYnTlqz07PdCI/FNZDyEw7SSsp1qlt/TMWpateaNBdFatORQKJ6QQ
+GBkz8hqqtDad+j/8cUIeoJaOD95Gh7psOBfQfshKy0opHES5iCnHb7dPPwDNDGsP
+d6Fa8YAhkbNoyg21yW0E9JiUnwn5qFjqNJzY8A0CgcAEpWkEAFV24kGxCNWnr2J5
+jASvdNKURa4BDfpduAg6WIAdXzDMNaBH8txVOeHI3LRsJguYducyd0xUR24dS9Kj
+Ux4GctJtyd2v7ZynL7GsohsEp5eHgQgPsfk8Ih6ZYPMvSiE3nOtf1Dv5bM7U3Gw/
+0xN8dta3ocyDtfCivt6Xmx+ZB4dhpPr/yMO23/TrfKxkYRPkfheH6Xo+/+eIgJnM
+T7LTS8xCOt+xztbndcqxpbMl1La62uRQ+QzDMugfFHECgcEAnZ27o2OwliCKX1Ly
+tuZpZawwhO7svRZFROMCwnPCnfWyXrOzhRM+5jMTZngJQHlDA1p4r6yjVyAO3dtb
+bP2iSz9wN+GF+1wwSCLMtSk1xhpYJ4sfv2m13ZYxQrlqGr/sX99+iWk/jqHWCTYE
+p2n4YVf1A5b/0sKzx8O6I5dU1B74oP8mBgdig1Je/pVJ3PKmbXLaGeQaA1CZkjU/
+EPl5lsYKNvqbj9nUKhHa5S/ngiopKjly94Ttoj+J1HuVUF/NAoHBAL97k2hk6l61
+9LaMkUmqK7CldEBzRSO2dK56Va6dirs7bTp9xHrAgn8O71cchuJWMFsM2dFSz98Q
+T8hKdbG4sVmbAQKiTSmqY+URCheuHnntXRD88IqN9nf4eBceB+7R3lnt1/uUv8l8
+9/OgjWbTlJp/0XqJh3EXlpBOvntUXlEDxjWvX+pdzDGrVkt1ahS2wBq/++lUuq3D
+UumFA9uy6AsYYDcZ+QeH57LYOg3J1fDwc2D8nuCesepSccf9JwoiQg==
+-----END RSA PRIVATE KEY-----
diff --git a/daemon/lua/trust_anchors.test/x509/ca.pem b/daemon/lua/trust_anchors.test/x509/ca.pem
new file mode 100644
index 0000000..e3c3ca2
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/ca.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEGTCCAoGgAwIBAgIUXWsAXOOaZw+h37N9gUc/XLw3KHwwDQYJKoZIhvcNAQEL
+BQAwIzEhMB8GA1UEAxMYS25vdCBSZXNvbHZlciB0ZXN0aW5nIENBMCAXDTIwMDEw
+NzA5MzQwOVoYDzk5OTkxMjMxMjM1OTU5WjAjMSEwHwYDVQQDExhLbm90IFJlc29s
+dmVyIHRlc3RpbmcgQ0EwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCe
+7vLYQa4sk4oBH4hb1oUpL5GdN/w1iH9TcYf8F3HnFVcGLVT7GZiAguwamS1Xy1/d
+KCbYlftlsr7hEYZpFH4yXMACC10ReGlQICU+FfuKRtGD+TuERpxpIUTVCR16BMzz
+aupMG9p8QN0cb/aFtOp1mDR5EfvP0xhwZCUzijG2k2fUMmdhHNB7hWFUxvtRtocd
+1LhYQKnFMs7guZA3DVjkM3DFxZHyGPXgCK0Xi89y8SZsnIjSngZMAl1OfJOvjXKT
+dR1gDPc0Cajm8oBKFIEkQEtFGYUurY6XTP/s0J/moLfAqaCt0gItE1Xz3/j58fM+
+NekIK9sRk1cTVca6xtf/feH6jEdd2r8xVoCqNJdDu57/0+YTqcKZScIe2vLH0tb3
+X3A2kS/qNumIRAijGgrA4EtIgprJcimcCSRjs8KfKvboOsRGA41wrhS7OtbGYpMk
+f7wKyKIgUzyfXBVFBT0bOBfU/mtqwhbzFHPCw8c24/H45SiETjfUaOiCcCBT/AEC
+AwEAAaNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwQAMB0GA1Ud
+DgQWBBSSt9DE0Qfipz+Ce4eGau+f9DecyDANBgkqhkiG9w0BAQsFAAOCAYEAA45p
+Ak7ebzk2ss5FHhJDvHrhoTZG2/esNEQhtv70nuRPnm3j8UGDxYyydwjf4+W9DT1v
+53QNfvbOIPcOUsGArAItmI7K6ltkBSS6ymO8T1ZY4vYw+77jJZ1EeYS6kjdan7dK
+f2Zz23CVbuq8BOc/Ob6ChepEq/MIb3g/Y6FowuWqeC85s61GW9MKr5GeG0oSyYAO
+UZdFnwa8QLCZ2IzQcwnolkAw2A/5TDxovINy9Lb5U3kyphC9vhjPqr8PJ5q/KVuK
+vcHvEsrsSNPvW/WcxkziV1oJTnjvr/69mwAme8+xjjF90GhrNaQF1YOoijuZuQaS
+Q+0qmwZbsMtcqAABKQALHfLGsGAA5MKip49khIQWuIAS8P2vb+hzbqQRLjq1uW7B
+dEGvBHF0QebDZOXJeXEYK/b7btWa9kNedD2FvBx5c9QNiWwh7jZENkICKnhI7E+n
+d5/gsKVa1glKwbMagZBSJgFtjZe/eo/LcoK82m4VuOUCJSe0Kd0McrSZ7XZX
+-----END CERTIFICATE-----
diff --git a/daemon/lua/trust_anchors.test/x509/ca.tmpl b/daemon/lua/trust_anchors.test/x509/ca.tmpl
new file mode 100644
index 0000000..ed801af
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/ca.tmpl
@@ -0,0 +1,4 @@
+cn = Knot Resolver testing CA
+ca
+cert_signing_key
+expiration_days = -1
diff --git a/daemon/lua/trust_anchors.test/x509/gen.sh b/daemon/lua/trust_anchors.test/x509/gen.sh
new file mode 100755
index 0000000..7251f12
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/gen.sh
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# CA
+certtool --generate-privkey > ca-key.pem
+certtool --generate-self-signed --load-privkey ca-key.pem --template ca.tmpl --outfile ca.pem
+
+# server cert signed by CA above
+certtool --generate-privkey > server-key.pem
+certtool --generate-certificate --load-privkey server-key.pem --load-ca-certificate ca.pem --load-ca-privkey ca-key.pem --template server.tmpl --outfile server.pem
+
+# wrong CA - unrelated to others
+certtool --generate-privkey > wrongca-key.pem
+certtool --generate-self-signed --load-privkey wrongca-key.pem --template wrongca.tmpl --outfile wrongca.pem
diff --git a/daemon/lua/trust_anchors.test/x509/server-key.pem b/daemon/lua/trust_anchors.test/x509/server-key.pem
new file mode 100644
index 0000000..9eaef8a
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/server-key.pem
@@ -0,0 +1,182 @@
+Public Key Info:
+ Public Key Algorithm: RSA
+ Key Security Level: High (3072 bits)
+
+modulus:
+ 00:c3:46:2a:27:c8:39:e4:de:fa:24:45:6c:00:26:80
+ 61:ca:dd:a1:24:34:1b:93:1c:13:c8:5a:cf:af:6a:ef
+ 34:b9:89:83:02:76:51:ad:67:bf:ed:39:ee:0a:15:57
+ 91:6e:fa:68:60:78:22:62:fa:0a:55:12:03:b3:0c:8e
+ b4:ca:cd:2b:9d:a2:43:b5:5a:48:a0:3d:4a:1f:77:a4
+ a6:d4:87:eb:79:99:df:d4:b4:a3:cf:91:03:a0:c5:82
+ 39:f5:75:20:4b:90:b9:3b:72:65:a7:75:39:a6:62:58
+ 65:b0:9c:40:5c:c7:c4:4c:d3:1e:cc:74:18:74:15:23
+ 44:fd:51:59:b2:b7:70:95:6b:a0:be:d5:e4:72:59:2b
+ df:a5:a2:06:c8:e1:bd:17:80:25:b3:cf:8e:e8:ad:b2
+ f7:04:b1:9e:b8:72:0b:c5:dc:cd:a5:b6:f5:c9:1a:eb
+ 63:78:75:9c:5d:c5:03:a9:4b:7b:d6:cd:5c:5f:8d:2e
+ d0:b4:0d:96:55:c7:1e:c7:ac:13:46:b8:ec:9c:36:b9
+ 6a:1d:f0:7c:41:00:c6:bd:1f:81:7e:1d:48:1d:59:bc
+ e2:61:a6:d1:2d:52:10:3f:63:93:a9:14:d8:03:27:21
+ b0:d4:07:24:78:04:2b:86:c8:2b:0f:eb:a7:b3:3e:e2
+ 81:62:2a:4a:07:d9:fd:f6:77:7f:50:88:ee:bb:7d:31
+ 53:a8:97:bf:30:07:37:41:e9:52:16:15:74:a8:64:ed
+ 93:46:38:56:b4:89:d9:0c:62:4b:64:a9:64:ff:fc:9a
+ d6:19:a7:84:98:28:04:b4:95:76:ac:4a:42:6a:fb:67
+ 5b:b4:37:e6:e6:e2:52:d3:e9:38:8b:76:10:55:f1:e6
+ 8e:d8:73:eb:17:d1:54:41:d4:5b:76:2d:70:7f:f5:0d
+ 7d:d2:d6:f8:05:33:18:ab:dd:10:8a:5b:21:ee:3d:78
+ 9d:cd:c9:c0:c6:98:4e:a6:0a:41:f0:97:91:83:c2:c8
+ 4b:
+
+public exponent:
+ 01:00:01:
+
+private exponent:
+ 23:88:1f:e1:8f:40:61:91:e5:28:36:6d:99:75:68:04
+ e3:5a:02:99:48:d5:ff:a5:ab:3f:d8:ae:53:b7:fc:80
+ b6:85:fc:0d:b5:a3:d5:0e:bc:d0:98:aa:e4:b0:cf:77
+ 4a:1f:4c:60:c9:5b:50:71:38:f2:13:ce:12:85:65:6e
+ 26:3c:c1:03:f4:e3:a7:1b:1f:7d:f0:c9:0d:02:c3:36
+ 0c:14:13:57:d4:14:f3:6a:4f:28:54:b5:b9:4a:57:10
+ de:c6:0a:33:55:c9:2e:b4:f9:24:48:63:4e:10:35:0f
+ 83:dc:5a:a5:c7:3f:c3:ce:e2:9a:c7:41:2f:d6:2c:cd
+ bf:de:4e:99:03:61:fb:fe:52:88:86:f9:03:89:90:3f
+ 28:af:5b:d6:af:a4:ad:a3:06:b9:3a:3a:41:c2:61:7f
+ 2b:1e:7a:c8:0b:10:73:57:63:20:15:33:91:fd:50:f9
+ 8f:90:ae:fc:2c:fe:26:8e:f2:a0:ba:4b:65:a3:95:f1
+ d8:30:d4:fa:8d:12:1b:8b:58:1d:66:10:cd:41:22:1e
+ b0:7a:f4:e6:0f:76:3f:0a:0f:9c:44:e2:19:cf:c6:4d
+ de:3a:f4:96:70:c7:e6:2d:98:27:0e:ac:3b:32:41:37
+ 4e:05:b2:22:af:7b:38:92:16:40:fb:5c:96:b0:86:da
+ 96:c6:77:c3:66:78:07:80:5c:2a:46:dc:9a:bf:fc:0c
+ 2f:ee:f7:a1:b3:77:b4:50:75:a2:b7:36:9d:28:73:ee
+ 7a:ab:a6:0c:f6:92:18:8f:ff:16:28:90:7f:16:4f:f1
+ 6d:77:99:dd:a6:46:95:6a:6c:7a:15:48:53:b3:17:0b
+ 30:aa:0d:c8:68:33:2b:4d:40:da:74:cf:9b:73:1b:cd
+ 5d:f0:a9:d1:00:6f:db:de:55:ec:d4:24:96:bb:da:50
+ b4:d5:e1:87:35:5c:d4:50:c3:03:d5:d5:ee:03:65:4b
+ 68:9c:07:5c:59:28:78:bd:d1:4b:cb:8d:85:8b:5b:c1
+
+
+prime1:
+ 00:cc:8d:55:38:2d:57:cb:d2:4b:57:5b:3f:a2:6d:91
+ 4b:9c:54:29:98:9d:1d:bb:36:a6:e8:ba:e9:50:db:83
+ cf:c1:45:24:16:70:e5:51:40:eb:23:6b:fa:be:d5:d5
+ 00:27:ed:99:c7:7c:6b:16:79:77:0b:f3:ff:58:35:4e
+ 6c:58:68:51:d3:20:3c:57:b7:7d:bc:6b:fd:a7:c3:38
+ 9a:f2:7a:8a:b6:71:a1:6e:5e:64:7e:a8:c5:7c:58:70
+ fb:8d:63:b3:27:cc:1a:97:1e:04:da:d5:34:b5:d1:aa
+ f1:96:39:89:5e:cb:e7:75:ab:7e:ac:8c:fe:62:3e:cc
+ 93:66:88:d7:cd:c6:2d:db:9e:2f:f7:d1:6e:96:99:d2
+ 32:61:f4:9b:f5:48:fe:e7:90:b7:a2:ab:89:90:c1:ae
+ 67:5d:18:7a:c1:a3:84:97:09:47:13:df:d2:85:46:46
+ c7:77:3b:9f:b5:74:5c:f6:ec:a0:a7:66:0e:d1:d7:a4
+ e1:
+
+prime2:
+ 00:f4:63:70:fa:dd:7e:3d:1a:2b:5b:47:79:56:e9:c7
+ 7f:6b:50:41:60:45:af:59:e0:77:b2:76:4e:40:ff:f8
+ 55:9d:77:3b:c1:00:6a:c6:84:6a:09:a5:45:e6:fc:e6
+ e6:92:72:32:fc:93:8f:93:d9:db:fc:8c:43:d2:7a:ea
+ 4b:0e:ee:1d:dc:e0:27:08:83:16:aa:de:37:59:39:c0
+ 21:26:b5:34:49:f2:1f:7b:0c:d4:3c:0f:e5:06:ac:23
+ 7b:85:b1:39:35:44:ec:70:48:c5:10:86:02:ea:36:4a
+ f1:20:a2:b2:c9:8d:d3:f6:5a:86:72:4b:8b:28:07:04
+ 39:8d:01:fa:75:3a:35:40:c2:21:c3:ac:50:da:2f:3e
+ 30:ee:ab:f7:7d:81:a3:77:5e:b7:03:be:52:fb:a4:70
+ 92:5d:fd:09:ae:52:33:b8:7b:9c:e2:2a:77:f7:23:4d
+ c5:4b:82:f1:fb:0a:09:62:e6:5f:32:1e:7b:c7:c6:66
+ ab:
+
+coefficient:
+ 00:c3:2d:d8:18:32:30:a1:fa:2c:23:d0:ea:b4:60:0f
+ 29:67:50:4a:5a:61:aa:6d:15:0e:4b:66:43:35:ee:39
+ 4c:e7:8d:31:73:b0:bb:04:4d:e5:bc:28:ea:dc:77:81
+ 35:bb:f7:80:13:96:04:4c:45:9c:43:6f:64:e0:a3:51
+ 4b:7e:6c:b6:7d:c2:a6:e0:94:e6:6c:34:4e:62:71:ea
+ c0:c0:ab:30:30:c1:3a:39:0e:cd:f0:cc:0e:31:b5:fc
+ 61:64:1e:29:1c:cd:fc:69:c0:02:7c:2a:fe:86:d5:e2
+ 7b:8d:fe:ae:3d:3a:6c:1b:b3:b4:0d:b1:1b:d1:4d:37
+ 36:ea:d7:15:f3:6e:02:b1:86:98:51:02:fc:62:df:30
+ f2:de:9f:03:6d:27:45:d7:c7:a8:04:ba:76:18:01:09
+ 34:d2:57:f9:10:50:ea:ae:0e:ae:c6:a4:cd:f9:fd:b1
+ 25:b2:45:20:bc:50:2d:9b:80:c1:39:08:97:d2:75:9b
+ f7:
+
+exp1:
+ 00:8a:b8:6a:8b:cf:8c:54:08:c8:d9:74:63:82:67:25
+ fb:0e:08:b1:b0:f3:14:7f:ab:3f:a4:63:65:e9:55:05
+ 5e:36:a7:0a:23:41:ea:f2:a0:c1:16:63:9b:48:22:41
+ f2:7a:21:93:81:8a:ea:20:f2:bc:fe:59:39:d8:fb:45
+ b5:0a:7b:ac:ca:2e:79:5d:cf:6d:b1:03:d7:a1:17:2e
+ e3:3e:00:46:e4:15:c9:b1:cc:c8:00:71:ba:84:6a:82
+ 2c:c6:a5:4f:91:74:c4:af:a9:47:07:95:41:ca:f0:67
+ 2a:b1:83:51:9a:fd:53:7a:24:94:a2:b6:77:a9:ef:06
+ d4:0b:dc:4f:e6:18:39:6f:50:27:1d:bc:65:70:32:df
+ 2f:15:e9:4a:7f:1d:42:e9:8d:e6:4b:a6:63:83:cd:25
+ d6:a9:76:f9:81:2a:c0:b7:a1:2e:17:d7:59:b0:d2:89
+ 1b:aa:cb:bf:b2:d2:38:5d:a8:fa:06:ac:9a:ee:4a:7d
+ 61:
+
+exp2:
+ 00:92:66:af:db:d8:ac:33:36:66:1a:bc:6a:78:22:7c
+ 1c:5c:d1:2b:18:dd:25:fa:95:79:9f:33:38:15:c0:41
+ a8:28:38:b1:57:21:44:d5:bf:a5:36:3a:07:f2:24:36
+ be:91:a4:4f:de:f7:16:df:df:76:e5:87:b1:69:79:b0
+ b9:5e:2c:4f:3f:6e:18:74:04:f3:a3:50:93:9f:a3:f4
+ f0:e7:1b:4e:43:ae:04:25:d6:bd:9d:6d:78:29:d3:1d
+ 3e:76:0c:80:d4:e4:81:2f:92:a8:5b:09:ac:dd:59:c0
+ f3:4a:35:ad:1d:09:15:9d:53:05:8f:9a:a9:b6:44:dd
+ c7:0c:2d:cf:38:42:b2:7c:24:cf:cd:44:80:fa:f3:aa
+ 31:ee:08:9e:ae:54:e6:f4:2f:8d:3b:74:dc:89:5b:2d
+ 04:c1:c1:3f:f7:69:cf:0a:09:23:26:69:82:8c:4e:5d
+ dc:7f:2b:e6:82:18:b5:1e:c6:1a:e9:0f:51:df:8f:7f
+ 19:
+
+
+Public Key PIN:
+ pin-sha256:pFSHHHovr50DJ04K3wEJcyxth+nszZdClOet/CRN9cU=
+Public Key ID:
+ sha256:a454871c7a2faf9d03274e0adf0109732c6d87e9eccd974294e7adfc244df5c5
+ sha1:5137ef343399ccf38d6566803ddce123da640553
+
+-----BEGIN RSA PRIVATE KEY-----
+MIIG5QIBAAKCAYEAw0YqJ8g55N76JEVsACaAYcrdoSQ0G5McE8haz69q7zS5iYMC
+dlGtZ7/tOe4KFVeRbvpoYHgiYvoKVRIDswyOtMrNK52iQ7VaSKA9Sh93pKbUh+t5
+md/UtKPPkQOgxYI59XUgS5C5O3Jlp3U5pmJYZbCcQFzHxEzTHsx0GHQVI0T9UVmy
+t3CVa6C+1eRyWSvfpaIGyOG9F4Als8+O6K2y9wSxnrhyC8XczaW29cka62N4dZxd
+xQOpS3vWzVxfjS7QtA2WVccex6wTRrjsnDa5ah3wfEEAxr0fgX4dSB1ZvOJhptEt
+UhA/Y5OpFNgDJyGw1AckeAQrhsgrD+unsz7igWIqSgfZ/fZ3f1CI7rt9MVOol78w
+BzdB6VIWFXSoZO2TRjhWtInZDGJLZKlk//ya1hmnhJgoBLSVdqxKQmr7Z1u0N+bm
+4lLT6TiLdhBV8eaO2HPrF9FUQdRbdi1wf/UNfdLW+AUzGKvdEIpbIe49eJ3NycDG
+mE6mCkHwl5GDwshLAgMBAAECggGAI4gf4Y9AYZHlKDZtmXVoBONaAplI1f+lqz/Y
+rlO3/IC2hfwNtaPVDrzQmKrksM93Sh9MYMlbUHE48hPOEoVlbiY8wQP046cbH33w
+yQ0CwzYMFBNX1BTzak8oVLW5SlcQ3sYKM1XJLrT5JEhjThA1D4PcWqXHP8PO4prH
+QS/WLM2/3k6ZA2H7/lKIhvkDiZA/KK9b1q+kraMGuTo6QcJhfyseesgLEHNXYyAV
+M5H9UPmPkK78LP4mjvKguktlo5Xx2DDU+o0SG4tYHWYQzUEiHrB69OYPdj8KD5xE
+4hnPxk3eOvSWcMfmLZgnDqw7MkE3TgWyIq97OJIWQPtclrCG2pbGd8NmeAeAXCpG
+3Jq//Awv7vehs3e0UHWitzadKHPuequmDPaSGI//FiiQfxZP8W13md2mRpVqbHoV
+SFOzFwswqg3IaDMrTUDadM+bcxvNXfCp0QBv295V7NQklrvaULTV4Yc1XNRQwwPV
+1e4DZUtonAdcWSh4vdFLy42Fi1vBAoHBAMyNVTgtV8vSS1dbP6JtkUucVCmYnR27
+NqbouulQ24PPwUUkFnDlUUDrI2v6vtXVACftmcd8axZ5dwvz/1g1TmxYaFHTIDxX
+t328a/2nwzia8nqKtnGhbl5kfqjFfFhw+41jsyfMGpceBNrVNLXRqvGWOYley+d1
+q36sjP5iPsyTZojXzcYt254v99FulpnSMmH0m/VI/ueQt6KriZDBrmddGHrBo4SX
+CUcT39KFRkbHdzuftXRc9uygp2YO0dek4QKBwQD0Y3D63X49GitbR3lW6cd/a1BB
+YEWvWeB3snZOQP/4VZ13O8EAasaEagmlReb85uaScjL8k4+T2dv8jEPSeupLDu4d
+3OAnCIMWqt43WTnAISa1NEnyH3sM1DwP5QasI3uFsTk1ROxwSMUQhgLqNkrxIKKy
+yY3T9lqGckuLKAcEOY0B+nU6NUDCIcOsUNovPjDuq/d9gaN3XrcDvlL7pHCSXf0J
+rlIzuHuc4ip39yNNxUuC8fsKCWLmXzIee8fGZqsCgcEAirhqi8+MVAjI2XRjgmcl
++w4IsbDzFH+rP6RjZelVBV42pwojQeryoMEWY5tIIkHyeiGTgYrqIPK8/lk52PtF
+tQp7rMoueV3PbbED16EXLuM+AEbkFcmxzMgAcbqEaoIsxqVPkXTEr6lHB5VByvBn
+KrGDUZr9U3oklKK2d6nvBtQL3E/mGDlvUCcdvGVwMt8vFelKfx1C6Y3mS6Zjg80l
+1ql2+YEqwLehLhfXWbDSiRuqy7+y0jhdqPoGrJruSn1hAoHBAJJmr9vYrDM2Zhq8
+angifBxc0SsY3SX6lXmfMzgVwEGoKDixVyFE1b+lNjoH8iQ2vpGkT973Ft/fduWH
+sWl5sLleLE8/bhh0BPOjUJOfo/Tw5xtOQ64EJda9nW14KdMdPnYMgNTkgS+SqFsJ
+rN1ZwPNKNa0dCRWdUwWPmqm2RN3HDC3POEKyfCTPzUSA+vOqMe4Inq5U5vQvjTt0
+3IlbLQTBwT/3ac8KCSMmaYKMTl3cfyvmghi1HsYa6Q9R349/GQKBwQDDLdgYMjCh
++iwj0Oq0YA8pZ1BKWmGqbRUOS2ZDNe45TOeNMXOwuwRN5bwo6tx3gTW794ATlgRM
+RZxDb2Tgo1FLfmy2fcKm4JTmbDROYnHqwMCrMDDBOjkOzfDMDjG1/GFkHikczfxp
+wAJ8Kv6G1eJ7jf6uPTpsG7O0DbEb0U03NurXFfNuArGGmFEC/GLfMPLenwNtJ0XX
+x6gEunYYAQk00lf5EFDqrg6uxqTN+f2xJbJFILxQLZuAwTkIl9J1m/c=
+-----END RSA PRIVATE KEY-----
diff --git a/daemon/lua/trust_anchors.test/x509/server.pem b/daemon/lua/trust_anchors.test/x509/server.pem
new file mode 100644
index 0000000..b42f07e
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/server.pem
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEfTCCAuWgAwIBAgIUIREQSLx52Sc9PFWI6Nwe3YzRp3MwDQYJKoZIhvcNAQEL
+BQAwIzEhMB8GA1UEAxMYS25vdCBSZXNvbHZlciB0ZXN0aW5nIENBMCAXDTIwMDEw
+NzA5MzQwOVoYDzk5OTkxMjMxMjM1OTU5WjA8MRIwEAYDVQQDEwlsb2NhbGhvc3Qx
+JjAkBgNVBAoTHUZha2UgRE5TIHJvb3Qgb3JnIHRlc3Qgc2VydmVyMIIBojANBgkq
+hkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAw0YqJ8g55N76JEVsACaAYcrdoSQ0G5Mc
+E8haz69q7zS5iYMCdlGtZ7/tOe4KFVeRbvpoYHgiYvoKVRIDswyOtMrNK52iQ7Va
+SKA9Sh93pKbUh+t5md/UtKPPkQOgxYI59XUgS5C5O3Jlp3U5pmJYZbCcQFzHxEzT
+Hsx0GHQVI0T9UVmyt3CVa6C+1eRyWSvfpaIGyOG9F4Als8+O6K2y9wSxnrhyC8Xc
+zaW29cka62N4dZxdxQOpS3vWzVxfjS7QtA2WVccex6wTRrjsnDa5ah3wfEEAxr0f
+gX4dSB1ZvOJhptEtUhA/Y5OpFNgDJyGw1AckeAQrhsgrD+unsz7igWIqSgfZ/fZ3
+f1CI7rt9MVOol78wBzdB6VIWFXSoZO2TRjhWtInZDGJLZKlk//ya1hmnhJgoBLSV
+dqxKQmr7Z1u0N+bm4lLT6TiLdhBV8eaO2HPrF9FUQdRbdi1wf/UNfdLW+AUzGKvd
+EIpbIe49eJ3NycDGmE6mCkHwl5GDwshLAgMBAAGjgY0wgYowDAYDVR0TAQH/BAIw
+ADAUBgNVHREEDTALgglsb2NhbGhvc3QwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYD
+VR0PAQH/BAUDAwegADAdBgNVHQ4EFgQUUTfvNDOZzPONZWaAPdzhI9pkBVMwHwYD
+VR0jBBgwFoAUkrfQxNEH4qc/gnuHhmrvn/Q3nMgwDQYJKoZIhvcNAQELBQADggGB
+AFfFgv5J1eb8h33tnvDJ/dLBSA7Soz1NXK8iha18CH1uxW2lo+6iJl7g191WD/3W
+m/LdpRU3h5ewbD6An3FSA0I25cYQD1vlH7vdI+xu3hIuFhQVnkxGbwISzlM5vat8
+1Ry7z/RpHmQA4V4z4R/PuYcQHQG5tINMPySmbfHBK/Ju+nnmSTJ/p3Z7sVaSCfNN
+l37me0w197QU3ovNtA61xHa77VUSJeaAC+zOOXUBZ8Rc5PqhYOf6AJbIBk7tPNei
+XH5Yyg3UT0i7V09vUViXK8EXbMX1VWsw59Et4Ro1YouS6TN34i2w8FtKg1+amQLr
+UXmQW1lkzx23FdGG4T0fFPtWuJCL6ioc0J6vS7xt0xkbrri9U2thC7gvrKLGCJ6J
+hWTGoKwjcBHpoLsT62XogHlctagkyXfjJ1Piik7k2JjqvmyteFlDDkOToLQmaCuI
+LIBmOnO9mEig3y7T72cnL8QM+nb+c70cssfCW3LBTHb893J4QAOt5RN6LQUlFc49
+sQ==
+-----END CERTIFICATE-----
diff --git a/daemon/lua/trust_anchors.test/x509/server.tmpl b/daemon/lua/trust_anchors.test/x509/server.tmpl
new file mode 100644
index 0000000..8021616
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/server.tmpl
@@ -0,0 +1,7 @@
+organization = Fake DNS root org test server
+cn = localhost
+tls_www_server
+encryption_key
+signing_key
+dns_name = localhost
+expiration_days = -1
diff --git a/daemon/lua/trust_anchors.test/x509/wrongca-key.pem b/daemon/lua/trust_anchors.test/x509/wrongca-key.pem
new file mode 100644
index 0000000..1ddc1ad
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/wrongca-key.pem
@@ -0,0 +1,182 @@
+Public Key Info:
+ Public Key Algorithm: RSA
+ Key Security Level: High (3072 bits)
+
+modulus:
+ 00:bb:d7:47:1f:55:ed:c0:08:af:1d:32:d2:69:ef:77
+ d2:f3:f6:86:7e:f3:97:e2:35:72:d4:0a:87:1e:75:76
+ bf:59:29:be:cd:e6:ad:6d:7d:62:47:19:fb:ed:24:94
+ 7f:2b:d6:0c:68:cf:cd:ee:f3:5e:b2:db:11:44:4b:7f
+ 30:ce:d2:a7:75:a7:37:83:c0:41:d6:a1:87:22:48:fa
+ ef:d1:15:ed:c9:d2:73:ab:e1:7c:94:4d:b2:96:80:cf
+ 5a:5c:7e:96:f6:02:fa:a4:8b:b1:05:b0:27:f5:d7:38
+ bd:20:37:ed:12:c0:22:07:a9:a6:5e:47:bd:1d:33:27
+ a2:cd:4c:0c:70:ba:6e:d9:13:6f:7b:a1:72:e8:f4:be
+ e3:86:1b:a2:b3:a1:07:cf:93:e8:3a:26:51:3e:af:bc
+ da:80:b1:92:56:8b:21:e7:1d:d9:f9:0c:a9:68:b7:04
+ d8:6d:1f:6f:98:90:fb:fb:35:18:71:3c:50:73:b1:45
+ b1:e7:ee:7b:84:5d:57:95:33:37:b0:0f:eb:85:8f:8d
+ b0:7f:10:17:80:03:99:1b:62:0c:1d:72:6f:e5:77:38
+ c8:75:96:61:36:4b:28:ae:17:a4:f9:81:90:4d:4b:85
+ 61:39:be:6c:ca:c0:a9:cd:4e:45:27:47:84:82:3d:7f
+ c6:a7:00:d7:90:64:7c:a5:e9:f8:f6:92:d2:72:54:a7
+ 95:5f:fc:93:1d:c9:1a:78:6e:3a:1a:1f:8f:a2:41:d2
+ 04:5c:19:32:54:16:f2:97:6f:7c:f9:24:d7:a6:e2:07
+ cf:9f:9e:64:27:81:5f:5a:77:65:4f:7b:b2:81:78:3f
+ a3:22:17:d3:ba:06:71:d5:09:6a:c2:85:ba:35:f7:71
+ 01:b4:63:c7:70:62:98:58:80:a2:40:27:c0:e2:d5:fd
+ 60:e0:5a:7a:9c:bf:7b:e6:34:78:f1:16:e8:28:d9:92
+ dc:e6:2e:b6:d7:1a:83:4b:86:92:d6:81:ce:8e:50:0a
+ d5:
+
+public exponent:
+ 01:00:01:
+
+private exponent:
+ 7a:27:5e:66:1f:60:54:60:91:58:80:a3:5b:26:d2:9a
+ 89:f2:88:b6:68:3d:1e:6b:39:b8:70:fc:3b:af:91:c0
+ 90:00:58:c7:d7:ba:72:98:76:5f:dc:a2:fb:2d:ad:b0
+ 21:d6:ba:0d:33:0e:2d:d5:70:81:09:7b:6a:19:5a:a6
+ 67:e9:8f:e3:30:12:27:08:d1:07:fd:d5:3e:53:8d:74
+ 85:59:28:60:f6:0e:28:f9:a3:25:62:7d:bf:e8:16:70
+ 21:f4:64:c1:a9:60:4b:bf:58:28:65:cd:26:cf:86:63
+ 5f:5f:5f:39:b1:5e:af:f3:00:71:11:60:07:6c:2b:db
+ 70:7c:83:1e:8f:ee:e4:16:02:8a:b8:8c:5c:b8:44:a6
+ fb:a0:5f:27:47:92:27:c8:7c:dd:cb:eb:4b:c3:c7:21
+ a5:4d:54:e8:18:e4:bc:42:aa:6c:8e:72:60:d9:9c:3a
+ 0e:84:c1:f2:ca:5e:43:97:dc:c4:4e:bf:d6:ec:b2:70
+ 08:41:13:01:48:bc:36:a2:eb:5e:67:b6:6a:a4:b6:4a
+ 24:fa:fd:6d:ef:5b:77:bc:0c:7d:95:9a:84:ec:3f:97
+ aa:7c:07:76:80:f5:3a:49:f4:99:ee:cf:17:12:83:e8
+ db:ef:22:60:67:62:f8:3e:f9:bc:18:2b:84:fc:a9:82
+ 95:8d:91:27:8e:ba:87:15:65:1e:9f:b3:95:5f:dc:40
+ 2f:15:eb:7e:0a:d7:69:80:7b:8a:e2:29:89:3a:2e:eb
+ a9:05:c1:1e:5d:23:0d:a0:d7:c4:95:4d:09:85:8c:af
+ 90:23:36:04:66:a9:16:d7:d4:e2:aa:5a:6d:44:5a:6c
+ c8:e8:a0:08:fa:de:19:20:5f:e3:06:17:e5:65:c6:55
+ ef:0f:0d:ff:3e:1c:c5:98:ee:34:d3:07:81:11:fe:e9
+ 15:87:e6:9a:76:44:bd:cb:a0:38:63:9a:af:d1:7c:a7
+ db:26:e2:cd:4a:a2:8a:7f:b8:dc:7a:55:00:4c:20:c1
+
+
+prime1:
+ 00:c9:f5:14:59:49:3b:95:1f:15:b0:0c:83:cb:f4:6a
+ 48:60:2a:af:8b:d5:83:16:aa:71:5a:af:11:63:c6:c1
+ 0a:91:af:5b:bd:6e:9c:cb:d7:eb:bf:c7:31:9f:22:46
+ 01:cf:3b:3c:cb:ba:7d:ad:e5:bb:d8:7c:d2:5d:52:20
+ 14:ea:70:08:9e:29:98:31:20:78:9e:b6:3e:90:e8:ef
+ c8:2f:45:d4:35:04:71:a1:84:18:50:a9:a5:12:b7:14
+ 4e:42:3e:93:50:9d:2f:c1:bd:45:f3:4e:86:61:0b:bc
+ 3b:ed:78:c7:2b:ba:4b:a0:ef:e6:0e:a9:9a:f4:aa:73
+ 23:b8:51:c7:d3:dd:fd:a7:1c:c1:69:32:ea:26:32:6d
+ 40:b0:0a:cd:0d:fa:b4:f4:56:ed:e8:d4:96:08:80:fd
+ 43:44:8c:fb:bb:af:81:d7:bb:71:c6:7c:3a:d2:a7:83
+ e6:28:2d:2f:00:05:82:d7:cc:59:db:d9:e5:4f:a4:67
+ 05:
+
+prime2:
+ 00:ee:1b:2a:48:37:fa:7c:94:35:36:ac:83:5f:2c:98
+ e3:07:43:d1:2c:80:0e:a2:b8:7a:eb:e2:70:f6:49:77
+ b3:42:05:fe:06:cf:3f:ca:0f:0d:44:1c:74:0a:77:f7
+ 31:9f:30:fb:d9:44:71:11:e6:4a:ff:ef:ae:77:98:3e
+ 73:a0:77:21:a6:e0:66:9a:cf:5f:eb:3b:39:62:0b:ba
+ 1b:9b:1a:a5:58:4c:7e:17:fc:64:61:93:89:f0:c0:0f
+ ce:55:18:7e:d4:33:87:32:0e:53:51:5f:03:b4:05:4a
+ 5c:e7:5b:10:e5:b7:88:e5:04:b2:53:45:98:2f:9d:fb
+ 32:f5:2f:d9:59:54:ce:91:83:4c:37:ee:ab:5a:05:40
+ 85:05:03:ae:b4:3d:96:c2:67:6b:28:25:91:87:ed:d1
+ 3a:0f:4b:38:a5:81:b3:5b:6f:3e:33:27:1e:9a:4a:e6
+ 3c:7c:be:9f:45:72:5b:eb:e3:dd:6c:73:ae:0d:07:bd
+ 91:
+
+coefficient:
+ 45:53:87:ab:71:9c:14:af:6c:00:44:bb:de:d5:72:ed
+ e9:21:f2:19:e5:4d:30:92:8e:9b:b7:f6:db:9e:ea:71
+ b3:c2:89:01:4a:49:1f:2e:f8:34:57:e0:36:9a:20:84
+ a8:b0:8a:0b:2a:d6:da:36:22:c2:ac:a2:85:99:f7:5d
+ 3f:2e:71:ab:e5:f7:bd:b2:8c:6f:44:33:aa:2d:cf:38
+ 8c:d6:77:c7:d5:68:88:f1:f9:80:c2:e2:b8:58:26:bd
+ de:d6:8d:d5:c9:43:dc:e2:af:2e:d3:c5:19:4e:d5:14
+ 33:bc:15:58:6f:05:eb:8d:0d:fa:40:a3:b7:77:24:4b
+ 30:a7:c2:8b:89:08:24:4d:fb:2e:3c:ad:ff:e3:d7:8b
+ 9c:f2:07:0d:79:3c:5e:f5:83:94:32:e2:16:dc:a9:22
+ b4:f4:09:6a:f6:af:7d:9c:41:dc:be:23:7e:c4:6d:d6
+ f9:e6:8e:3c:2d:00:fa:ac:d2:c8:6e:c5:6d:52:74:cd
+
+
+exp1:
+ 4d:20:f9:2d:84:47:6a:13:1e:10:47:27:4a:8c:44:ce
+ f1:53:3c:09:d6:78:22:fe:e3:1d:b4:00:9b:2f:7b:e8
+ 12:6d:7b:46:e4:68:a3:7d:09:ff:0b:0f:0b:6c:66:7a
+ 28:6f:c2:2f:38:40:e9:59:f4:9c:a0:47:22:f6:cb:63
+ d1:89:09:f1:85:87:27:33:f4:7d:00:b2:f2:5a:d3:c0
+ 8b:35:4a:ef:18:8c:61:17:f6:c5:4f:94:c8:89:fd:0a
+ 4a:48:65:b0:82:e7:8b:41:42:e6:c2:15:96:18:8a:42
+ 04:d6:7c:92:59:aa:aa:83:14:44:83:47:b7:ab:25:1f
+ fe:33:d5:72:37:b4:b8:ce:c5:9a:ec:a3:fa:04:86:2f
+ 0f:4c:80:b5:97:0a:e6:ca:10:40:3c:78:34:35:37:04
+ 2a:b9:01:26:d3:c7:6d:e1:9b:79:27:56:bb:be:d8:23
+ dd:32:2c:62:00:b8:d0:bb:ad:91:c6:2c:ca:76:ca:15
+
+
+exp2:
+ 30:d8:19:c0:5e:db:5f:9a:f7:9f:93:9c:0f:76:12:96
+ df:f2:a5:82:3f:72:c1:26:9e:f0:ac:af:07:96:e2:9b
+ 3f:3c:03:74:5a:27:77:c7:c6:ac:e6:39:57:bc:6c:55
+ 1d:96:ea:d3:13:1b:2e:d4:d3:25:d5:81:30:bf:66:70
+ 49:c6:a6:7c:99:23:f3:35:ff:33:3e:1e:f3:61:fc:77
+ 95:45:ce:0d:63:03:aa:df:f7:a7:9c:a0:7b:66:aa:d7
+ 64:d5:75:8f:0a:52:fd:8d:ba:c1:c2:7f:fb:f9:e9:db
+ 4d:0a:7d:58:e2:61:8e:b9:7b:eb:61:27:6a:fd:39:7e
+ a6:95:7e:3c:b9:0c:f7:04:bc:29:ed:27:f1:7b:8a:54
+ bf:46:96:1c:1b:56:45:e2:f9:34:6f:20:7f:85:e5:99
+ c7:71:62:d9:70:d5:de:37:df:c6:96:8b:cc:92:f8:d0
+ 07:b7:02:ed:38:1c:6b:33:7f:44:b4:26:4c:3d:fe:41
+
+
+
+Public Key PIN:
+ pin-sha256:UOonm3sEw21t/nC/tr24q9sX/HPV9mo0/M3Ya8rAwLs=
+Public Key ID:
+ sha256:50ea279b7b04c36d6dfe70bfb6bdb8abdb17fc73d5f66a34fccdd86bcac0c0bb
+ sha1:b963cfb8eb202ccad2bb988dfa9e00cc52c1a4ba
+
+-----BEGIN RSA PRIVATE KEY-----
+MIIG4gIBAAKCAYEAu9dHH1XtwAivHTLSae930vP2hn7zl+I1ctQKhx51dr9ZKb7N
+5q1tfWJHGfvtJJR/K9YMaM/N7vNestsRREt/MM7Sp3WnN4PAQdahhyJI+u/RFe3J
+0nOr4XyUTbKWgM9aXH6W9gL6pIuxBbAn9dc4vSA37RLAIgeppl5HvR0zJ6LNTAxw
+um7ZE297oXLo9L7jhhuis6EHz5PoOiZRPq+82oCxklaLIecd2fkMqWi3BNhtH2+Y
+kPv7NRhxPFBzsUWx5+57hF1XlTM3sA/rhY+NsH8QF4ADmRtiDB1yb+V3OMh1lmE2
+SyiuF6T5gZBNS4VhOb5sysCpzU5FJ0eEgj1/xqcA15BkfKXp+PaS0nJUp5Vf/JMd
+yRp4bjoaH4+iQdIEXBkyVBbyl298+STXpuIHz5+eZCeBX1p3ZU97soF4P6MiF9O6
+BnHVCWrChbo193EBtGPHcGKYWICiQCfA4tX9YOBaepy/e+Y0ePEW6CjZktzmLrbX
+GoNLhpLWgc6OUArVAgMBAAECggGAeideZh9gVGCRWICjWybSmonyiLZoPR5rObhw
+/DuvkcCQAFjH17pymHZf3KL7La2wIda6DTMOLdVwgQl7ahlapmfpj+MwEicI0Qf9
+1T5TjXSFWShg9g4o+aMlYn2/6BZwIfRkwalgS79YKGXNJs+GY19fXzmxXq/zAHER
+YAdsK9twfIMej+7kFgKKuIxcuESm+6BfJ0eSJ8h83cvrS8PHIaVNVOgY5LxCqmyO
+cmDZnDoOhMHyyl5Dl9zETr/W7LJwCEETAUi8NqLrXme2aqS2SiT6/W3vW3e8DH2V
+moTsP5eqfAd2gPU6SfSZ7s8XEoPo2+8iYGdi+D75vBgrhPypgpWNkSeOuocVZR6f
+s5Vf3EAvFet+CtdpgHuK4imJOi7rqQXBHl0jDaDXxJVNCYWMr5AjNgRmqRbX1OKq
+Wm1EWmzI6KAI+t4ZIF/jBhflZcZV7w8N/z4cxZjuNNMHgRH+6RWH5pp2RL3LoDhj
+mq/RfKfbJuLNSqKKf7jcelUATCDBAoHBAMn1FFlJO5UfFbAMg8v0akhgKq+L1YMW
+qnFarxFjxsEKka9bvW6cy9frv8cxnyJGAc87PMu6fa3lu9h80l1SIBTqcAieKZgx
+IHietj6Q6O/IL0XUNQRxoYQYUKmlErcUTkI+k1CdL8G9RfNOhmELvDvteMcrukug
+7+YOqZr0qnMjuFHH0939pxzBaTLqJjJtQLAKzQ36tPRW7ejUlgiA/UNEjPu7r4HX
+u3HGfDrSp4PmKC0vAAWC18xZ29nlT6RnBQKBwQDuGypIN/p8lDU2rINfLJjjB0PR
+LIAOorh66+Jw9kl3s0IF/gbPP8oPDUQcdAp39zGfMPvZRHER5kr/7653mD5zoHch
+puBmms9f6zs5Ygu6G5sapVhMfhf8ZGGTifDAD85VGH7UM4cyDlNRXwO0BUpc51sQ
+5beI5QSyU0WYL537MvUv2VlUzpGDTDfuq1oFQIUFA660PZbCZ2soJZGH7dE6D0s4
+pYGzW28+MycemkrmPHy+n0VyW+vj3Wxzrg0HvZECgcBNIPkthEdqEx4QRydKjETO
+8VM8CdZ4Iv7jHbQAmy976BJte0bkaKN9Cf8LDwtsZnoob8IvOEDpWfScoEci9stj
+0YkJ8YWHJzP0fQCy8lrTwIs1Su8YjGEX9sVPlMiJ/QpKSGWwgueLQULmwhWWGIpC
+BNZ8klmqqoMURINHt6slH/4z1XI3tLjOxZrso/oEhi8PTIC1lwrmyhBAPHg0NTcE
+KrkBJtPHbeGbeSdWu77YI90yLGIAuNC7rZHGLMp2yhUCgcAw2BnAXttfmvefk5wP
+dhKW3/Klgj9ywSae8KyvB5bimz88A3RaJ3fHxqzmOVe8bFUdlurTExsu1NMl1YEw
+v2ZwScamfJkj8zX/Mz4e82H8d5VFzg1jA6rf96ecoHtmqtdk1XWPClL9jbrBwn/7
++enbTQp9WOJhjrl762Enav05fqaVfjy5DPcEvCntJ/F7ilS/RpYcG1ZF4vk0byB/
+heWZx3Fi2XDV3jffxpaLzJL40Ae3Au04HGszf0S0Jkw9/kECgcBFU4ercZwUr2wA
+RLve1XLt6SHyGeVNMJKOm7f2257qcbPCiQFKSR8u+DRX4DaaIISosIoLKtbaNiLC
+rKKFmfddPy5xq+X3vbKMb0Qzqi3POIzWd8fVaIjx+YDC4rhYJr3e1o3VyUPc4q8u
+08UZTtUUM7wVWG8F640N+kCjt3ckSzCnwouJCCRN+y48rf/j14uc8gcNeTxe9YOU
+MuIW3KkitPQJavavfZxB3L4jfsRt1vnmjjwtAPqs0shuxW1SdM0=
+-----END RSA PRIVATE KEY-----
diff --git a/daemon/lua/trust_anchors.test/x509/wrongca.pem b/daemon/lua/trust_anchors.test/x509/wrongca.pem
new file mode 100644
index 0000000..fc3e43f
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/wrongca.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEETCCAnmgAwIBAgIUNVTN+if8IQU0I1n4qyVF9qqhuo0wDQYJKoZIhvcNAQEL
+BQAwHzEdMBsGA1UEAxMUQW5vdGhlciB1bnJlbGF0ZWQgQ0EwIBcNMjAwMTA3MDkz
+NDA5WhgPOTk5OTEyMzEyMzU5NTlaMB8xHTAbBgNVBAMTFEFub3RoZXIgdW5yZWxh
+dGVkIENBMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAu9dHH1XtwAiv
+HTLSae930vP2hn7zl+I1ctQKhx51dr9ZKb7N5q1tfWJHGfvtJJR/K9YMaM/N7vNe
+stsRREt/MM7Sp3WnN4PAQdahhyJI+u/RFe3J0nOr4XyUTbKWgM9aXH6W9gL6pIux
+BbAn9dc4vSA37RLAIgeppl5HvR0zJ6LNTAxwum7ZE297oXLo9L7jhhuis6EHz5Po
+OiZRPq+82oCxklaLIecd2fkMqWi3BNhtH2+YkPv7NRhxPFBzsUWx5+57hF1XlTM3
+sA/rhY+NsH8QF4ADmRtiDB1yb+V3OMh1lmE2SyiuF6T5gZBNS4VhOb5sysCpzU5F
+J0eEgj1/xqcA15BkfKXp+PaS0nJUp5Vf/JMdyRp4bjoaH4+iQdIEXBkyVBbyl298
++STXpuIHz5+eZCeBX1p3ZU97soF4P6MiF9O6BnHVCWrChbo193EBtGPHcGKYWICi
+QCfA4tX9YOBaepy/e+Y0ePEW6CjZktzmLrbXGoNLhpLWgc6OUArVAgMBAAGjQzBB
+MA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcEADAdBgNVHQ4EFgQUuWPP
+uOsgLMrSu5iN+p4AzFLBpLowDQYJKoZIhvcNAQELBQADggGBAEobHXRbd6wfUmyf
+P5v6qdJQMqtNGlU8eYzizEyuyovSlL+g9wgQ/91RAYK26FXzOuRz9Cg/ZWYVHqiG
+rRWWwcfzY7qHo3HGkpDSIjD53TAoK46ICD4+EreG+JBvy1P3Ij/VX7M07swIg8Ff
+6O4CnJpKAFaSr9wT8Ac3oCu+vymgLajMocNYV/UFVND+TLi6sx0zcMfCgW2vhSWk
+PRulxL76xq97vjWoveqDiFS41cPOAghd4hUmzRFByX6XPBx6YZddSUF+QZt92K4Z
+YEU4UbKqhbiBoZMGaQ8DzM2T44WPISrRZ0QpeS+pXwVjbDfoUbBWYAjFA8EHhPOi
+oewIIYnarItI3z3iccErOeKPPVQh5QW3/CwO4XSnvTEBkhf2EjG25UAHZ8LZy0t8
+Sw1raGJPYJV/qNVeIzLKd3tYmNpcmddYqS+ei2yBOoO5UPdbYaH1gTAZ4BbOhOml
+BJKJWcekpJrZAVTBNRectxsMXB8fHYL65Wa+w3cRqsZRjTbTEg==
+-----END CERTIFICATE-----
diff --git a/daemon/lua/trust_anchors.test/x509/wrongca.tmpl b/daemon/lua/trust_anchors.test/x509/wrongca.tmpl
new file mode 100644
index 0000000..0e8491b
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/wrongca.tmpl
@@ -0,0 +1,4 @@
+cn = Another unrelated CA
+ca
+cert_signing_key
+expiration_days = -1
diff --git a/daemon/lua/zonefile.lua b/daemon/lua/zonefile.lua
new file mode 100644
index 0000000..8ea3a08
--- /dev/null
+++ b/daemon/lua/zonefile.lua
@@ -0,0 +1,93 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+-- LuaJIT ffi bindings for zscanner, a DNS zone parser.
+-- Author: Marek Vavrusa <marek.vavrusa@nic.cz>
+
+local ffi = require('ffi')
+local libzscanner = ffi.load(libzscanner_SONAME)
+
+-- Wrap scanner context
+local zs_scanner_t = ffi.typeof('zs_scanner_t')
+ffi.metatype( zs_scanner_t, {
+ __gc = function(zs) return libzscanner.zs_deinit(zs) end,
+ __new = function(ct, origin, class, ttl)
+ if not class then class = 1 end
+ if not ttl then ttl = 3600 end
+ local parser = ffi.new(ct)
+ libzscanner.zs_init(parser, origin, class, ttl)
+ return parser
+ end,
+ __index = {
+ open = function (zs, file)
+ assert(ffi.istype(zs, zs_scanner_t))
+ local ret = libzscanner.zs_set_input_file(zs, file)
+ if ret ~= 0 then return false, zs:strerr() end
+ return true
+ end,
+ parse = function(zs, input)
+ assert(ffi.istype(zs, zs_scanner_t))
+ if input ~= nil then libzscanner.zs_set_input_string(zs, input, #input) end
+ local ret = libzscanner.zs_parse_record(zs)
+ -- Return current state only when parsed correctly, otherwise return error
+ if ret == 0 and zs.state ~= "ZS_STATE_ERROR" then
+ return zs.state == "ZS_STATE_DATA"
+ else
+ return false, zs:strerr()
+ end
+ end,
+ current_rr = function(zs)
+ assert(ffi.istype(zs, zs_scanner_t))
+ return {
+ owner = ffi.string(zs.r_owner, zs.r_owner_length),
+ ttl = tonumber(zs.r_ttl),
+ class = tonumber(zs.r_class),
+ type = tonumber(zs.r_type),
+ rdata = ffi.string(zs.r_data, zs.r_data_length),
+ comment = zs:current_comment(),
+ }
+ end,
+ strerr = function(zs)
+ assert(ffi.istype(zs, zs_scanner_t))
+ return ffi.string(libzscanner.zs_strerror(zs.error.code))
+ end,
+ current_comment = function(zs)
+ if zs.buffer_length > 0 then
+ return ffi.string(zs.buffer, zs.buffer_length - 1)
+ else
+ return nil
+ end
+ end
+ },
+})
+
+-- Module API
+local rrparser = {
+ new = zs_scanner_t,
+
+ -- Parse a file into a list of RRs
+ file = function (path)
+ local zs = zs_scanner_t()
+ local ok, err = zs:open(path)
+ if not ok then
+ return ok, err
+ end
+ local results = {}
+ while zs:parse() do
+ table.insert(results, zs:current_rr())
+ end
+ return results
+ end,
+
+ -- Parse a string into a list of RRs.
+ string = function (input)
+ local zs = zs_scanner_t()
+ local results = {}
+ local ok = zs:parse(input)
+ while ok do
+ table.insert(results, zs:current_rr())
+ ok = zs:parse()
+ end
+ return results
+ end,
+}
+return rrparser