summaryrefslogtreecommitdiffstats
path: root/utils
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--utils/cache_gc/.gitignore2
-rw-r--r--utils/cache_gc/README.rst20
-rw-r--r--utils/cache_gc/categories.c56
-rw-r--r--utils/cache_gc/categories.h10
-rw-r--r--utils/cache_gc/db.c280
-rw-r--r--utils/cache_gc/db.h37
-rw-r--r--utils/cache_gc/kr_cache_gc.c326
-rw-r--r--utils/cache_gc/kr_cache_gc.h41
-rw-r--r--utils/cache_gc/main.c163
-rw-r--r--utils/cache_gc/meson.build30
-rw-r--r--utils/cache_gc/test.integr/deckard.yaml37
-rw-r--r--utils/cache_gc/test.integr/val_rrsig.rpl737
-rw-r--r--utils/client/kresc.c458
-rw-r--r--utils/client/meson.build37
-rw-r--r--utils/meson.build8
-rw-r--r--utils/upgrade/meson.build13
-rw-r--r--utils/upgrade/upgrade-4-to-5.lua.in87
17 files changed, 2342 insertions, 0 deletions
diff --git a/utils/cache_gc/.gitignore b/utils/cache_gc/.gitignore
new file mode 100644
index 0000000..86ce5b1
--- /dev/null
+++ b/utils/cache_gc/.gitignore
@@ -0,0 +1,2 @@
+kres_cache_gc
+
diff --git a/utils/cache_gc/README.rst b/utils/cache_gc/README.rst
new file mode 100644
index 0000000..8d991b2
--- /dev/null
+++ b/utils/cache_gc/README.rst
@@ -0,0 +1,20 @@
+.. SPDX-License-Identifier: GPL-3.0-or-later
+
+.. _garbage-collector:
+
+Garbage Collector
+-----------------
+
+.. note:: When using systemd, ``kres-cache-gc.service`` is enabled by default
+ and does not need any manual configuration.
+
+Knot Resolver employs a separate garbage collector daemon which periodically
+trims the cache to keep its size below size limit configured using
+:envvar:`cache.size`.
+
+To execute the daemon manually, you can use the following command to run it
+every second:
+
+.. code-block:: bash
+
+ $ kres-cache-gc -c /var/cache/knot-resolver -d 1000
diff --git a/utils/cache_gc/categories.c b/utils/cache_gc/categories.c
new file mode 100644
index 0000000..19dec45
--- /dev/null
+++ b/utils/cache_gc/categories.c
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+#include "categories.h"
+
+#include <libknot/libknot.h>
+#include "lib/utils.h"
+
+static bool rrtype_is_infrastructure(uint16_t r)
+{
+ switch (r) {
+ case KNOT_RRTYPE_NS:
+ case KNOT_RRTYPE_DS:
+ case KNOT_RRTYPE_DNSKEY:
+ case KNOT_RRTYPE_A:
+ case KNOT_RRTYPE_AAAA:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static int get_random(int to)
+{
+ // We don't need these to be really unpredictable,
+ // but this should be cheap enough not to be noticeable.
+ return kr_rand_bytes(1) % to;
+}
+
+// TODO this is just an example, make this more clever
+category_t kr_gc_categorize(gc_record_info_t * info)
+{
+ category_t res;
+
+ if (!info->valid)
+ return CATEGORIES - 1;
+
+ switch (info->no_labels) {
+ case 0: /* root zone */
+ res = 5;
+ break;
+ case 1: /* TLD */
+ res = 10;
+ break;
+ default: /* SLD and below */
+ res = (rrtype_is_infrastructure(info->rrtype) ? 15 : 20);
+ if (info->entry_size > 300)
+ /* Penalty for big answers */
+ res += 30;
+ break;
+ }
+
+ if (info->expires_in <= 0) {
+ res += 40;
+ }
+
+ return res + get_random(5);
+}
diff --git a/utils/cache_gc/categories.h b/utils/cache_gc/categories.h
new file mode 100644
index 0000000..388fbe7
--- /dev/null
+++ b/utils/cache_gc/categories.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+#pragma once
+
+#include "kr_cache_gc.h"
+
+typedef uint8_t category_t;
+
+#define CATEGORIES 100 // number of categories
+
+category_t kr_gc_categorize(gc_record_info_t * info);
diff --git a/utils/cache_gc/db.c b/utils/cache_gc/db.c
new file mode 100644
index 0000000..fc4a2fd
--- /dev/null
+++ b/utils/cache_gc/db.c
@@ -0,0 +1,280 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+
+#include "db.h"
+
+#include "lib/cache/cdb_lmdb.h"
+#include "lib/cache/impl.h"
+
+#include <ctype.h>
+#include <time.h>
+#include <sys/stat.h>
+
+int kr_gc_cache_open(const char *cache_path, struct kr_cache *kres_db,
+ knot_db_t ** libknot_db)
+{
+ char cache_data[strlen(cache_path) + 10];
+ snprintf(cache_data, sizeof(cache_data), "%s/data.mdb", cache_path);
+
+ struct stat st = { 0 };
+ if (stat(cache_path, &st) || !(st.st_mode & S_IFDIR)
+ || stat(cache_data, &st)) {
+ printf("Error: %s does not exist or is not a LMDB.\n", cache_path);
+ return -ENOENT;
+ }
+
+ struct kr_cdb_opts opts = { .path = cache_path, .maxsize = 0/*don't resize*/ };
+
+ int ret = kr_cache_open(kres_db, NULL, &opts, NULL);
+ if (ret || kres_db->db == NULL) {
+ printf("Error opening Resolver cache (%s).\n", kr_strerror(ret));
+ return -EINVAL;
+ }
+
+ *libknot_db = kr_cdb_pt2knot_db_t(kres_db->db);
+ if (*libknot_db == NULL) {
+ printf("Out of memory.\n");
+ return -ENOMEM;
+ }
+
+ return 0;
+}
+
+int kr_gc_cache_check_health(struct kr_cache *kres_db, knot_db_t ** libknot_db)
+{
+ int ret = kr_cache_check_health(kres_db, 0);
+ if (ret == 0) {
+ return 0;
+ } else if (ret != 1) {
+ kr_gc_cache_close(kres_db, *libknot_db);
+ return ret;
+ }
+ /* Cache was reopen. */
+ free(*libknot_db);
+ *libknot_db = kr_cdb_pt2knot_db_t(kres_db->db);
+ if (*libknot_db == NULL) {
+ printf("Out of memory.\n");
+ return -ENOMEM;
+ }
+ return 0;
+}
+
+void kr_gc_cache_close(struct kr_cache *kres_db, knot_db_t * knot_db)
+{
+ free(knot_db);
+ kr_cache_close(kres_db);
+}
+
+int kr_gc_key_consistent(knot_db_val_t key)
+{
+ const uint8_t *kd = key.data;
+ ssize_t i;
+ /* CACHE_KEY_DEF */
+ if (key.len >= 2 && kd[0] == '\0') {
+ /* Beware: root zone is special and starts with
+ * a single \0 followed by type sign */
+ i = 1;
+ } else {
+ /* find the first double zero in the key */
+ for (i = 2; kd[i - 1] || kd[i - 2]; ++i) {
+ if (kr_fails_assert(i < key.len))
+ return kr_error(EINVAL);
+ }
+ }
+ // the next character can be used for classification
+ switch (kd[i]) {
+ case 'E':
+ (void)0; // C can't have a variable definition following a label
+ uint16_t type;
+ if (kr_fails_assert(i + 1 + sizeof(type) <= key.len))
+ return kr_error(EINVAL);
+ memcpy(&type, kd + i + 1, sizeof(type));
+ return type;
+ case '1':
+ return KNOT_RRTYPE_NSEC;
+ case '3':
+ return KNOT_RRTYPE_NSEC3;
+ case 'S': // the rtt_state entries are considered inconsistent, at least for now
+ return -1;
+ default:
+ kr_assert(!EINVAL);
+ return kr_error(EINVAL);
+ }
+}
+
+/// expects that key is consistent! CACHE_KEY_DEF
+static uint8_t entry_labels(knot_db_val_t * key, uint16_t rrtype)
+{
+ uint8_t lab = 0, *p = key->data;
+ while (*p != 0) {
+ while (*p++ != 0) {
+ if (p - (uint8_t *) key->data >= key->len) {
+ return 0;
+ }
+ }
+ lab++;
+ }
+ if (rrtype == KNOT_RRTYPE_NSEC3) {
+ // We don't know the number of labels so easily,
+ // but let's classify everything as directly
+ // below the zone apex (that's most common).
+ ++lab;
+ }
+ return lab;
+}
+
+void debug_printbin(const char *str, unsigned int len)
+{
+ putchar('"');
+ for (int idx = 0; idx < len; idx++) {
+ char c = str[idx];
+ if (isprint(c))
+ putchar(c);
+ else
+ printf("`%02hhx`", c);
+ }
+ putchar('"');
+}
+
+/** Return one entry_h reference from a cache DB value. NULL if not consistent/suitable. */
+static const struct entry_h *val2entry(const knot_db_val_t val, uint16_t ktype)
+{
+ if (ktype != KNOT_RRTYPE_NS)
+ return entry_h_consistent(val, ktype);
+ /* Otherwise we have a multi-purpose entry.
+ * Well, for now we simply choose the most suitable entry;
+ * the only realistic collision is DNAME in apex where we'll prefer NS. */
+ entry_list_t el;
+ if (entry_list_parse(val, el))
+ return NULL;
+ for (int i = ENTRY_APEX_NSECS_CNT; i < EL_LENGTH; ++i) {
+ if (el[i].len)
+ return entry_h_consistent(el[i], EL2RRTYPE(i));
+ }
+ /* Only NSEC* meta-data inside. */
+ return NULL;
+}
+
+int kr_gc_cache_iter(knot_db_t * knot_db, const kr_cache_gc_cfg_t *cfg,
+ kr_gc_iter_callback callback, void *ctx)
+{
+ unsigned int counter_iter = 0;
+ unsigned int counter_gc_consistent = 0;
+ unsigned int counter_kr_consistent = 0;
+
+ knot_db_txn_t txn = { 0 };
+ knot_db_iter_t *it = NULL;
+ const knot_db_api_t *api = knot_db_lmdb_api();
+ gc_record_info_t info = { 0 };
+ int64_t now = time(NULL);
+
+ int ret = api->txn_begin(knot_db, &txn, KNOT_DB_RDONLY);
+ if (ret != KNOT_EOK) {
+ printf("Error starting DB transaction (%s).\n", knot_strerror(ret));
+ return ret;
+ }
+
+ it = api->iter_begin(&txn, KNOT_DB_NOOP); // _FIRST is split for easier debugging
+ if (it == NULL) {
+ printf("Error: failed to create an iterator.\n");
+ api->txn_abort(&txn);
+ return KNOT_ERROR;
+ }
+ it = api->iter_seek(it, NULL, KNOT_DB_FIRST);
+ if (it == NULL)
+ printf("Suspicious: completely empty LMDB at this moment?\n");
+
+ int txn_steps = 0;
+ while (it != NULL) {
+ knot_db_val_t key = { 0 }, val = { 0 };
+ ret = api->iter_key(it, &key);
+ if (ret == KNOT_EOK && key.len == 4 && memcmp("VERS", key.data, 4) == 0) {
+ /* skip DB metadata */
+ goto skip;
+ }
+ if (ret == KNOT_EOK) {
+ ret = api->iter_val(it, &val);
+ }
+ if (ret != KNOT_EOK) {
+ goto error;
+ }
+
+ info.entry_size = key.len + val.len;
+ info.valid = false;
+ const int entry_type = kr_gc_key_consistent(key);
+ const struct entry_h *entry = NULL;
+ if (entry_type >= 0) {
+ counter_gc_consistent++;
+ entry = val2entry(val, entry_type);
+ }
+ /* TODO: perhaps improve some details around here:
+ * - rtt_state entries are considered gc_inconsistent;
+ * therefore they'll be the first to get freed (see kr_gc_categorize())
+ * - xNAME have .rrtype NS
+ * - DNAME hidden on NS name will not be considered here
+ * - if zone has NSEC* meta-data but no NS, it will be seen
+ * here as kr_inconsistent */
+ if (entry != NULL) {
+ info.valid = true;
+ info.rrtype = entry_type;
+ info.expires_in = entry->time + entry->ttl - now;
+ info.no_labels = entry_labels(&key, entry_type);
+ }
+ counter_iter++;
+ counter_kr_consistent += info.valid;
+ if (VERBOSE_STATUS) {
+ if (!entry_type || !entry) { // don't log fully consistent entries
+ printf
+ ("GC %sconsistent, KR %sconsistent, size %zu, key len %zu: ",
+ entry_type ? "" : "in", entry ? "" : "IN",
+ (key.len + val.len), key.len);
+ debug_printbin(key.data, key.len);
+ printf("\n");
+ }
+ }
+ ret = callback(&key, &info, ctx);
+
+ if (ret != KNOT_EOK) {
+ error:
+ printf("Error iterating database (%s).\n",
+ knot_strerror(ret));
+ api->iter_finish(it);
+ api->txn_abort(&txn);
+ return ret;
+ }
+
+ skip: // Advance to the next GC item.
+ if (++txn_steps < cfg->ro_txn_items || !cfg->ro_txn_items/*unlimited*/) {
+ it = api->iter_next(it);
+ } else {
+ /* The transaction has been too long; let's reopen it. */
+ txn_steps = 0;
+ uint8_t key_storage[key.len];
+ memcpy(key_storage, key.data, key.len);
+ key.data = key_storage;
+
+ api->iter_finish(it);
+ api->txn_abort(&txn);
+
+ ret = api->txn_begin(knot_db, &txn, KNOT_DB_RDONLY);
+ if (ret != KNOT_EOK) {
+ printf("Error restarting DB transaction (%s).\n",
+ knot_strerror(ret));
+ return ret;
+ }
+ it = api->iter_begin(&txn, KNOT_DB_NOOP);
+ if (it == NULL) {
+ printf("Error: failed to create an iterator.\n");
+ api->txn_abort(&txn);
+ return KNOT_ERROR;
+ }
+ it = api->iter_seek(it, &key, KNOT_DB_GEQ);
+ // NULL here means we'we reached the end
+ }
+ }
+
+ api->txn_abort(&txn);
+
+ kr_log_debug(CACHE, "iterated %u items, gc consistent %u, kr consistent %u\n",
+ counter_iter, counter_gc_consistent, counter_kr_consistent);
+ return KNOT_EOK;
+}
diff --git a/utils/cache_gc/db.h b/utils/cache_gc/db.h
new file mode 100644
index 0000000..7865471
--- /dev/null
+++ b/utils/cache_gc/db.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+#pragma once
+
+#include <lib/cache/api.h>
+#include <libknot/libknot.h>
+
+#include "kr_cache_gc.h"
+
+int kr_gc_cache_open(const char *cache_path, struct kr_cache *kres_db,
+ knot_db_t ** libknot_db);
+
+/** A wrapper around kr_cdb_api::check_health that keeps libknot_db up to date.
+ * \return zero or negative error code. */
+int kr_gc_cache_check_health(struct kr_cache *kres_db, knot_db_t ** libknot_db);
+
+void kr_gc_cache_close(struct kr_cache *kres_db, knot_db_t * knot_db);
+
+typedef int (*kr_gc_iter_callback)(const knot_db_val_t * key,
+ gc_record_info_t * info, void *ctx);
+
+int kr_gc_cache_iter(knot_db_t * knot_db, const kr_cache_gc_cfg_t *cfg,
+ kr_gc_iter_callback callback, void *ctx);
+
+/** Return RR type corresponding to the key or negative error code.
+ *
+ * Error is returned on unexpected values (those also trigger assertion)
+ * and on other kinds of data in cache (e.g. struct rtt_state).
+ */
+int kr_gc_key_consistent(knot_db_val_t key);
+
+/** Printf a *binary* string in a human-readable way. */
+void debug_printbin(const char *str, unsigned int len);
+
+/** Block run in --verbose mode; optimized when not run. */
+#define VERBOSE_STATUS __builtin_expect(KR_LOG_LEVEL_IS(LOG_DEBUG), false)
+/* TODO: replace when solving GC logging properly */
+
diff --git a/utils/cache_gc/kr_cache_gc.c b/utils/cache_gc/kr_cache_gc.c
new file mode 100644
index 0000000..5978345
--- /dev/null
+++ b/utils/cache_gc/kr_cache_gc.c
@@ -0,0 +1,326 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+// standard includes
+#include <inttypes.h>
+#include <limits.h>
+#include <stdio.h>
+#include <time.h>
+
+// libknot includes
+#include <libknot/libknot.h>
+
+// dynarray is inside libknot since 3.1, but it's differently named
+#ifdef knot_dynarray_declare
+ #define dynarray_declare knot_dynarray_declare
+ #define dynarray_define knot_dynarray_define
+ #define dynarray_foreach knot_dynarray_foreach
+#else
+ #include <contrib/dynarray.h>
+#endif
+
+// resolver includes
+#include <lib/cache/api.h>
+#include <lib/cache/impl.h>
+#include <lib/defines.h>
+#include "lib/cache/cdb_lmdb.h"
+#include "lib/utils.h"
+
+#include "kr_cache_gc.h"
+
+#include "categories.h"
+#include "db.h"
+
+// section: dbval_copy
+
+static knot_db_val_t *dbval_copy(const knot_db_val_t * from)
+{
+ knot_db_val_t *to = malloc(sizeof(knot_db_val_t) + from->len);
+ if (to != NULL) {
+ memcpy(to, from, sizeof(knot_db_val_t));
+ to->data = to + 1; // == ((uit8_t *)to) + sizeof(knot_db_val_t)
+ memcpy(to->data, from->data, from->len);
+ }
+ return to;
+}
+
+// section: rrtype list
+
+dynarray_declare(rrtype, uint16_t, DYNARRAY_VISIBILITY_STATIC, 64)
+ dynarray_define(rrtype, uint16_t, DYNARRAY_VISIBILITY_STATIC)
+static void rrtypelist_add(rrtype_dynarray_t * arr, uint16_t add_type)
+{
+ bool already_present = false;
+ dynarray_foreach(rrtype, uint16_t, i, *arr) {
+ if (*i == add_type) {
+ already_present = true;
+ break;
+ }
+ }
+ if (!already_present) {
+ rrtype_dynarray_add(arr, &add_type);
+ }
+}
+
+static void rrtypelist_print(rrtype_dynarray_t * arr)
+{
+ char type_s[32] = { 0 };
+ dynarray_foreach(rrtype, uint16_t, i, *arr) {
+ knot_rrtype_to_string(*i, type_s, sizeof(type_s));
+ printf(" %s", type_s);
+ }
+ printf("\n");
+}
+
+dynarray_declare(entry, knot_db_val_t *, DYNARRAY_VISIBILITY_STATIC, 256)
+ dynarray_define(entry, knot_db_val_t *, DYNARRAY_VISIBILITY_STATIC)
+static void entry_dynarray_deep_free(entry_dynarray_t * d)
+{
+ dynarray_foreach(entry, knot_db_val_t *, i, *d) {
+ free(*i);
+ }
+ entry_dynarray_free(d);
+}
+
+typedef struct {
+ size_t categories_sizes[CATEGORIES];
+ size_t records;
+} ctx_compute_categories_t;
+
+int cb_compute_categories(const knot_db_val_t * key, gc_record_info_t * info,
+ void *vctx)
+{
+ ctx_compute_categories_t *ctx = vctx;
+ category_t cat = kr_gc_categorize(info);
+ (void)key;
+ ctx->categories_sizes[cat] += info->entry_size;
+ ctx->records++;
+ return KNOT_EOK;
+}
+
+typedef struct {
+ category_t limit_category;
+ entry_dynarray_t to_delete;
+ size_t cfg_temp_keys_space;
+ size_t used_space;
+ size_t oversize_records;
+} ctx_delete_categories_t;
+
+int cb_delete_categories(const knot_db_val_t * key, gc_record_info_t * info,
+ void *vctx)
+{
+ ctx_delete_categories_t *ctx = vctx;
+ category_t cat = kr_gc_categorize(info);
+ if (cat >= ctx->limit_category) {
+ knot_db_val_t *todelete = dbval_copy(key);
+ size_t used = ctx->used_space + key->len + sizeof(*key);
+ if ((ctx->cfg_temp_keys_space > 0 &&
+ used > ctx->cfg_temp_keys_space) || todelete == NULL) {
+ ctx->oversize_records++;
+ free(todelete);
+ } else {
+ entry_dynarray_add(&ctx->to_delete, &todelete);
+ ctx->used_space = used;
+ }
+ }
+ return KNOT_EOK;
+}
+
+struct kr_cache_gc_state {
+ struct kr_cache kres_db;
+ knot_db_t *db;
+};
+
+void kr_cache_gc_free_state(kr_cache_gc_state_t **state)
+{
+ if (kr_fails_assert(state))
+ return;
+ if (!*state) { // not open
+ return;
+ }
+ kr_gc_cache_close(&(*state)->kres_db, (*state)->db);
+ free(*state);
+ *state = NULL;
+}
+
+int kr_cache_gc(kr_cache_gc_cfg_t *cfg, kr_cache_gc_state_t **state)
+{
+ // The whole function works in four "big phases":
+ //// 1. find out whether we should even do analysis and deletion.
+ if (kr_fails_assert(cfg && state))
+ return KNOT_EINVAL;
+ int ret;
+ // Ensure that we have open and "healthy" cache.
+ if (!*state) {
+ *state = calloc(1, sizeof(**state));
+ if (!*state) {
+ return KNOT_ENOMEM;
+ }
+ ret = kr_gc_cache_open(cfg->cache_path, &(*state)->kres_db,
+ &(*state)->db);
+ } else { // To be sure, we guard against the file getting replaced.
+ ret = kr_gc_cache_check_health(&(*state)->kres_db, &(*state)->db);
+ // In particular, missing data.mdb gives us kr_error(ENOENT) == KNOT_ENOENT
+ }
+ if (ret) {
+ free(*state);
+ *state = NULL;
+ return ret;
+ }
+ knot_db_t *const db = (*state)->db; // frequently used shortcut
+
+ const double db_usage = kr_cdb_lmdb()->usage_percent((*state)->kres_db.db);
+ const bool large_usage = db_usage >= cfg->cache_max_usage;
+ if (cfg->dry_run || large_usage || VERBOSE_STATUS) { // don't print this on every size check
+ printf("Usage: %.2lf%%\n", db_usage);
+ }
+ if (cfg->dry_run || !large_usage) {
+ return KNOT_EOK;
+ }
+
+ //// 2. classify all cache items into categories
+ // and compute which categories to delete.
+ kr_timer_t timer_analyze = { 0 }, timer_choose = { 0 }, timer_delete =
+ { 0 }, timer_rw_txn = { 0 };
+
+ kr_timer_start(&timer_analyze);
+ ctx_compute_categories_t cats = { { 0 }
+ };
+ ret = kr_gc_cache_iter(db, cfg, cb_compute_categories, &cats);
+ if (ret != KNOT_EOK) {
+ kr_cache_gc_free_state(state);
+ return ret;
+ }
+
+ //ssize_t amount_tofree = knot_db_lmdb_get_mapsize(db) * cfg->cache_to_be_freed / 100;
+ // Mixing ^^ page usage and entry sizes (key+value lengths) didn't work
+ // too well, probably due to internal fragmentation after some GC cycles.
+ // Therefore let's scale this by the ratio of these two sums.
+ ssize_t cats_sumsize = 0;
+ for (int i = 0; i < CATEGORIES; ++i) {
+ cats_sumsize += cats.categories_sizes[i];
+ }
+ /* use less precise variant to avoid 32-bit overflow */
+ ssize_t amount_tofree = cats_sumsize / 100 * cfg->cache_to_be_freed;
+
+ kr_log_debug(CACHE, "tofree: %zd / %zd\n", amount_tofree, cats_sumsize);
+ if (VERBOSE_STATUS) {
+ for (int i = 0; i < CATEGORIES; i++) {
+ if (cats.categories_sizes[i] > 0) {
+ printf("category %.2d size %zu\n", i,
+ cats.categories_sizes[i]);
+ }
+ }
+ }
+
+ category_t limit_category = CATEGORIES;
+ while (limit_category > 0 && amount_tofree > 0) {
+ amount_tofree -= cats.categories_sizes[--limit_category];
+ }
+
+ printf("Cache analyzed in %.0lf msecs, %zu records, limit category is %d.\n",
+ kr_timer_elapsed(&timer_analyze) * 1000, cats.records, limit_category);
+
+ //// 3. pass whole cache again to collect a list of keys that should be deleted.
+ kr_timer_start(&timer_choose);
+ ctx_delete_categories_t to_del = { 0 };
+ to_del.cfg_temp_keys_space = cfg->temp_keys_space;
+ to_del.limit_category = limit_category;
+ ret = kr_gc_cache_iter(db, cfg, cb_delete_categories, &to_del);
+ if (ret != KNOT_EOK) {
+ entry_dynarray_deep_free(&to_del.to_delete);
+ kr_cache_gc_free_state(state);
+ return ret;
+ }
+ printf
+ ("%zu records to be deleted using %.2lf MBytes of temporary memory, %zu records skipped due to memory limit.\n",
+ to_del.to_delete.size, ((double)to_del.used_space / 1048576.0),
+ to_del.oversize_records);
+
+ //// 4. execute the planned deletions.
+ const knot_db_api_t *api = knot_db_lmdb_api();
+ knot_db_txn_t txn = { 0 };
+ size_t deleted_records = 0, already_gone = 0, rw_txn_count = 0;
+
+ kr_timer_start(&timer_delete);
+ kr_timer_start(&timer_rw_txn);
+ rrtype_dynarray_t deleted_rrtypes = { 0 };
+
+ ret = api->txn_begin(db, &txn, 0);
+ if (ret != KNOT_EOK) {
+ printf("Error starting R/W DB transaction (%s).\n",
+ knot_strerror(ret));
+ entry_dynarray_deep_free(&to_del.to_delete);
+ kr_cache_gc_free_state(state);
+ return ret;
+ }
+
+ dynarray_foreach(entry, knot_db_val_t *, i, to_del.to_delete) {
+ ret = api->del(&txn, *i);
+ switch (ret) {
+ case KNOT_EOK:
+ deleted_records++;
+ const int entry_type = kr_gc_key_consistent(**i);
+ if (entry_type >= 0) // some "inconsistent" entries are OK
+ rrtypelist_add(&deleted_rrtypes, entry_type);
+ break;
+ case KNOT_ENOENT:
+ already_gone++;
+ if (VERBOSE_STATUS) {
+ // kresd normally only inserts (or overwrites),
+ // so it's generally suspicious when a key goes missing.
+ printf("Record already gone (key len %zu): ", (*i)->len);
+ debug_printbin((*i)->data, (*i)->len);
+ printf("\n");
+ }
+ break;
+ case KNOT_ESPACE:
+ printf("Warning: out of space, bailing out to retry later.\n");
+ api->txn_abort(&txn);
+ goto finish;
+ default:
+ printf("Warning: skipping deletion because of error (%s)\n",
+ knot_strerror(ret));
+ api->txn_abort(&txn);
+ ret = api->txn_begin(db, &txn, 0);
+ if (ret != KNOT_EOK) {
+ printf
+ ("Error: can't begin txn because of error (%s)\n",
+ knot_strerror(ret));
+ goto finish;
+ }
+ continue;
+ }
+ if ((cfg->rw_txn_items > 0 &&
+ (deleted_records + already_gone) % cfg->rw_txn_items == 0) ||
+ (cfg->rw_txn_duration > 0 &&
+ kr_timer_elapsed_us(&timer_rw_txn) > cfg->rw_txn_duration)) {
+ ret = api->txn_commit(&txn);
+ if (ret == KNOT_EOK) {
+ rw_txn_count++;
+ usleep(cfg->rw_txn_delay);
+ kr_timer_start(&timer_rw_txn);
+ ret = api->txn_begin(db, &txn, 0);
+ }
+ if (ret != KNOT_EOK) {
+ printf("Error: transaction failed (%s)\n",
+ knot_strerror(ret));
+ goto finish;
+ }
+ }
+ }
+ ret = api->txn_commit(&txn);
+
+finish:
+ printf("Deleted %zu records (%zu already gone) types", deleted_records,
+ already_gone);
+ rrtypelist_print(&deleted_rrtypes);
+ printf("It took %.0lf msecs, %zu transactions (%s)\n\n",
+ kr_timer_elapsed(&timer_delete) * 1000, rw_txn_count, knot_strerror(ret));
+
+ rrtype_dynarray_free(&deleted_rrtypes);
+ entry_dynarray_deep_free(&to_del.to_delete);
+
+ // OK, let's close it in this case.
+ kr_cache_gc_free_state(state);
+
+ return ret;
+}
diff --git a/utils/cache_gc/kr_cache_gc.h b/utils/cache_gc/kr_cache_gc.h
new file mode 100644
index 0000000..c64e99e
--- /dev/null
+++ b/utils/cache_gc/kr_cache_gc.h
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+typedef struct {
+ size_t entry_size; // amount of bytes occupied in cache by this record
+ bool valid; // fields further down are valid (ignore them if false)
+ int64_t expires_in; // < 0 => already expired
+ uint16_t rrtype;
+ uint8_t no_labels; // 0 == ., 1 == root zone member, 2 == TLD member ...
+ uint8_t rank;
+} gc_record_info_t;
+
+typedef struct {
+ const char *cache_path; // path to the LMDB with resolver cache
+ unsigned long gc_interval; // waiting time between two whole garbage collections in usecs (0 = just one-time cleanup)
+
+ size_t temp_keys_space; // maximum amount of temporary memory for copied keys in bytes (0 = unlimited)
+
+ size_t rw_txn_items; // maximum number of deleted records per RW transaction (0 = unlimited)
+ size_t ro_txn_items; // maximum number of iterated records (RO transactions, 0 = unlimited)
+ unsigned long rw_txn_duration; // maximum duration of RW transaction in usecs (0 = unlimited)
+ unsigned long rw_txn_delay; // waiting time between two RW transactions in usecs
+
+ uint8_t cache_max_usage; // maximum cache usage before triggering GC (percent)
+ uint8_t cache_to_be_freed; // percent of current cache usage to be freed during GC
+
+ bool dry_run;
+} kr_cache_gc_cfg_t;
+
+/** State persisting across kr_cache_gc() invocations (opaque).
+ * NULL pointer represents a clean state. */
+typedef struct kr_cache_gc_state kr_cache_gc_state_t;
+
+/** Do one iteration of cache-size check and (if necessary) GC pass. */
+int kr_cache_gc(kr_cache_gc_cfg_t *cfg, kr_cache_gc_state_t **state);
+void kr_cache_gc_free_state(kr_cache_gc_state_t **state);
+
diff --git a/utils/cache_gc/main.c b/utils/cache_gc/main.c
new file mode 100644
index 0000000..5adf19f
--- /dev/null
+++ b/utils/cache_gc/main.c
@@ -0,0 +1,163 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "lib/defines.h"
+#include "lib/utils.h"
+#include <libknot/libknot.h>
+#include <lmdb.h>
+
+#include "kresconfig.h"
+#include "kr_cache_gc.h"
+
+static volatile int killed = 0;
+
+static void got_killed(int signum)
+{
+ (void)signum;
+ switch (++killed) {
+ case 1:
+ break;
+ case 2:
+ exit(5);
+ break;
+ case 3:
+ abort();
+ default:
+ kr_assert(false);
+ }
+}
+
+static void print_help(void)
+{
+ printf("Usage: kr_cache_gc -c <resolver_cache> [ optional params... ]\n");
+ printf("Optional params:\n");
+ printf(" -d <garbage_interval(millis)>\n");
+ printf(" -l <deletes_per_txn>\n");
+ printf(" -L <reads_per_txn>\n");
+ printf(" -m <rw_txn_duration(usecs)>\n");
+ printf(" -u <cache_max_usage(percent)>\n");
+ printf(" -f <cache_to_be_freed(percent-of-current-usage)>\n");
+ printf(" -w <wait_next_rw_txn(usecs)>\n");
+ printf(" -t <temporary_memory(MBytes)>\n");
+ printf(" -n (= dry run)\n");
+}
+
+static long get_nonneg_optarg(void)
+{
+ char *end;
+ const long result = strtol(optarg, &end, 10);
+ if (result >= 0 && end && *end == '\0')
+ return result;
+ // not OK
+ print_help();
+ exit(2);
+}
+
+int main(int argc, char *argv[])
+{
+ printf("Knot Resolver Cache Garbage Collector, version %s\n", PACKAGE_VERSION);
+ if (setvbuf(stdout, NULL, _IONBF, 0) || setvbuf(stderr, NULL, _IONBF, 0)) {
+ fprintf(stderr, "Failed to to set output buffering (ignored): %s\n",
+ strerror(errno));
+ fflush(stderr);
+ }
+
+ signal(SIGTERM, got_killed);
+ signal(SIGKILL, got_killed);
+ signal(SIGPIPE, got_killed);
+ signal(SIGCHLD, got_killed);
+ signal(SIGINT, got_killed);
+
+ kr_cache_gc_cfg_t cfg = {
+ .ro_txn_items = 200,
+ .rw_txn_items = 100,
+ .cache_max_usage = 80,
+ .cache_to_be_freed = 10
+ };
+
+ int o;
+ while ((o = getopt(argc, argv, "hnvc:d:l:L:m:u:f:w:t:")) != -1) {
+ switch (o) {
+ case 'c':
+ cfg.cache_path = optarg;
+ break;
+ case 'd':
+ cfg.gc_interval = get_nonneg_optarg();
+ cfg.gc_interval *= 1000;
+ break;
+ case 'l':
+ cfg.rw_txn_items = get_nonneg_optarg();
+ break;
+ case 'L':
+ cfg.ro_txn_items = get_nonneg_optarg();
+ break;
+ case 'm':
+ cfg.rw_txn_duration = get_nonneg_optarg();
+ break;
+ case 'u':
+ cfg.cache_max_usage = get_nonneg_optarg();
+ break;
+ case 'f':
+ cfg.cache_to_be_freed = get_nonneg_optarg();
+ break;
+ case 'w':
+ cfg.rw_txn_delay = get_nonneg_optarg();
+ break;
+ case 't':
+ cfg.temp_keys_space = get_nonneg_optarg();
+ cfg.temp_keys_space *= 1048576;
+ break;
+ case 'n':
+ cfg.dry_run = true;
+ break;
+ case 'v':
+ kr_log_level_set(LOG_DEBUG);
+ break;
+ case ':':
+ case '?':
+ case 'h':
+ print_help();
+ return 1;
+ default:
+ kr_assert(false);
+ }
+ }
+
+ if (cfg.cache_path == NULL) {
+ print_help();
+ return 1;
+ }
+
+ int exit_code = 0;
+ kr_cache_gc_state_t *gc_state = NULL;
+ bool last_espace = false;
+ do {
+ int ret = kr_cache_gc(&cfg, &gc_state);
+
+ /* Let's tolerate ESPACE unless twice in a row. */
+ if (ret == KNOT_ESPACE) {
+ if (!last_espace)
+ ret = KNOT_EOK;
+ last_espace = true;
+ } else {
+ last_espace = false;
+ }
+
+ // ENOENT: kresd may not be started yet or cleared the cache now
+ // MDB_MAP_RESIZED: GC bailed out but on next iteration it should be OK
+ if (ret && ret != KNOT_ENOENT && ret != kr_error(MDB_MAP_RESIZED)) {
+ printf("Error (%s)\n", knot_strerror(ret));
+ exit_code = 10;
+ break;
+ }
+
+ usleep(cfg.gc_interval);
+ } while (cfg.gc_interval > 0 && !killed);
+
+ kr_cache_gc_free_state(&gc_state);
+
+ return exit_code;
+}
diff --git a/utils/cache_gc/meson.build b/utils/cache_gc/meson.build
new file mode 100644
index 0000000..40e127d
--- /dev/null
+++ b/utils/cache_gc/meson.build
@@ -0,0 +1,30 @@
+## utils/cache_gc
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+cache_gc_src = files([
+ 'categories.c',
+ 'db.c',
+ 'kr_cache_gc.c',
+ 'main.c',
+])
+c_src_lint += cache_gc_src
+
+if build_utils
+ cache_gc = executable(
+ 'kres-cache-gc',
+ cache_gc_src,
+ dependencies: [
+ kresconfig_dep,
+ contrib_dep,
+ libkres_dep,
+ libknot,
+ ],
+ install: true,
+ install_dir: get_option('sbindir'),
+ )
+
+integr_tests += [
+ ['gc_cache_overflow', meson.current_source_dir() / 'test.integr'],
+]
+
+endif
diff --git a/utils/cache_gc/test.integr/deckard.yaml b/utils/cache_gc/test.integr/deckard.yaml
new file mode 100644
index 0000000..b1a8b15
--- /dev/null
+++ b/utils/cache_gc/test.integr/deckard.yaml
@@ -0,0 +1,37 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+programs:
+- name: kresd1
+ binary: kresd
+ additional:
+ - -n
+ - ../kresd1/
+ templates:
+ - lib/cache/overflow.test.integr/kresd_config.j2
+ - tests/config/test_dns_generators.lua
+ configs:
+ - config
+ - dns_gen.lua
+- name: kresd2
+ binary: kresd
+ additional:
+ - -n
+ - ../kresd1/
+ templates:
+ - lib/cache/overflow.test.integr/kresd_config.j2
+ - tests/config/test_dns_generators.lua
+ configs:
+ - config
+ - dns_gen.lua
+- name: gc
+ binary: kres-cache-gc
+ additional:
+ # small cache needs shorter RW transactions and larger "percentages"
+ - -l8
+ - -u50
+ - -f20
+ - -d1
+ - -c
+ - ../kresd1/
+ conncheck: False
+ templates: []
+ configs: []
diff --git a/utils/cache_gc/test.integr/val_rrsig.rpl b/utils/cache_gc/test.integr/val_rrsig.rpl
new file mode 100644
index 0000000..22002b7
--- /dev/null
+++ b/utils/cache_gc/test.integr/val_rrsig.rpl
@@ -0,0 +1,737 @@
+query-minimization: off
+CONFIG_END
+
+SCENARIO_BEGIN Test validator with qtype RRSIG response
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 2 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 3 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 4 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 5 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 6 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 7 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 8 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 9 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 10 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 11 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 12 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 13 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 14 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 15 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 16 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 17 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 18 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 19 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 20 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 21 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 22 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 23 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 24 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 25 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 26 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 27 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 28 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 29 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 30 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 31 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 32 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 33 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 34 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 35 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 36 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 37 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 38 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 39 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 40 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 41 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 42 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 43 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 44 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 45 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 46 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 47 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 48 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 49 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 50 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 51 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 52 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 53 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 54 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 55 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 56 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 57 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 58 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 59 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 60 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 61 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 62 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 63 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 64 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 65 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 66 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 67 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 68 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 69 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 70 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 71 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 72 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 73 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 74 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 75 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 76 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 77 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 78 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 79 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 80 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 81 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 82 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 83 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 84 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+STEP 85 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 86 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode rcode flags question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 192.0.2.1
+ENTRY_END
+
+SCENARIO_END
diff --git a/utils/client/kresc.c b/utils/client/kresc.c
new file mode 100644
index 0000000..e083470
--- /dev/null
+++ b/utils/client/kresc.c
@@ -0,0 +1,458 @@
+/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+#include <arpa/inet.h>
+#include <contrib/ccan/asprintf/asprintf.h>
+#include <editline/readline.h>
+#include <errno.h>
+#include <histedit.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#define HISTORY_FILE "kresc_history"
+#define PROGRAM_NAME "kresc"
+
+FILE *g_tty = NULL; //!< connection to the daemon
+
+static char *run_cmd(const char *cmd, size_t * out_len);
+
+const char *prompt(EditLine * e)
+{
+ return PROGRAM_NAME "> ";
+}
+
+bool starts_with(const char *a, const char *b)
+{
+ if (strncmp(a, b, strlen(b)) == 0)
+ return 1;
+ return 0;
+}
+
+//! Returns Lua name of type of value, NULL on error. Puts length of type in name_len;
+char *get_type_name(const char *value)
+{
+ if (value == NULL) {
+ return NULL;
+ }
+
+ for (int i = 0; value[i]; i++) {
+ if (value[i] == ')') {
+ //Return NULL to prevent unexpected function call
+ return NULL;
+ }
+ }
+
+ char *cmd = afmt("type(%s)", value);
+ if (!cmd) {
+ perror("While tab-completing.");
+ return NULL;
+ }
+
+ size_t name_len;
+ char *type = run_cmd(cmd, &name_len);
+ if (!type) {
+ return NULL;
+ }
+ free(cmd);
+
+ if (starts_with(type, "[")) {
+ //Return "nil" on non-valid name.
+ free(type);
+ return strdup("nil");
+ } else {
+ type[strlen(type) - 1] = '\0';
+ return type;
+ }
+}
+
+static void complete_function(EditLine * el)
+{
+ //Add left parenthesis to function name.
+ el_insertstr(el, "(");
+}
+
+static void complete_members(EditLine * el, const char *str,
+ const char *str_type, int str_len, char *dot)
+{
+ char *table = strdup(str);
+ if (!table) {
+ perror("While tab-completing");
+ return;
+ }
+ //Get only the table name (without partial member name).
+ if (dot) {
+ *(table + (dot - str)) = '\0';
+ }
+ //Insert a dot after the table name.
+ if (!strncmp(str_type, "table", 5)) {
+ el_insertstr(el, ".");
+ str_len++;
+ }
+ //Check if the substring before dot is a valid table name.
+ const char *t_type = get_type_name(table);
+ if (t_type && !strncmp("table", t_type, 5)) {
+ //Get string of members of the table.
+ char *cmd =
+ afmt
+ ("do local s=\"\"; for i in pairs(%s) do s=s..i..\"\\n\" end return(s) end",
+ table);
+ if (!cmd) {
+ perror("While tab-completing.");
+ goto complete_members_exit;
+ }
+ size_t members_len;
+ char *members = run_cmd(cmd, &members_len);
+ free(cmd);
+ if (!members) {
+ perror("While communication with daemon");
+ goto complete_members_exit;
+ }
+ //Split members by newline.
+ char *members_tok = strdup(members);
+ free(members);
+ if (!members_tok) {
+ goto complete_members_exit;
+ }
+ char *token = strtok(members_tok, "\n");
+ int matches = 0;
+ char *lastmatch = NULL;
+ if (!dot || dot - str + 1 == strlen(str)) {
+ //Prints all members.
+ while (token) {
+ char *member = afmt("%s.%s", table, token);
+ const char *member_type = get_type_name(member);
+ if (member && member_type) {
+ printf("\n%s (%s)", member, member_type);
+ free(member);
+ free((void *)member_type);
+ } else if (member) {
+ printf("\n%s", member);
+ free(member);
+ }
+ token = strtok(NULL, "\n");
+ matches++;
+ }
+ } else {
+ //Print members matching the current line.
+ while (token) {
+ if (starts_with(token, dot + 1)) {
+ const char *member_type =
+ get_type_name(afmt
+ ("%s.%s", table,
+ token));
+ if (member_type) {
+ printf("\n%s.%s (%s)", table,
+ token, member_type);
+ free((void *)member_type);
+ } else {
+ printf("\n%s.%s", table, token);
+ }
+ lastmatch = token;
+ matches++;
+ }
+ token = strtok(NULL, "\n");
+ }
+
+ //Complete matching member.
+ if (matches == 1) {
+ el_deletestr(el, str_len);
+ el_insertstr(el, table);
+ el_insertstr(el, ".");
+ el_insertstr(el, lastmatch);
+ }
+ }
+ if (matches > 1) {
+ printf("\n");
+ }
+ free(members_tok);
+ }
+
+complete_members_exit:
+ free(table);
+ if(t_type) {
+ free((void*)t_type);
+ }
+}
+
+static void complete_globals(EditLine * el, const char *str, int str_len)
+{
+ //Parse Lua globals.
+ size_t globals_len;
+ char *globals = run_cmd("_G.__orig_name_list", &globals_len);
+ if (!globals) {
+ perror("While tab-completing");
+ return;
+ }
+ //Show possible globals.
+ char *globals_tok = strdup(globals);
+ free(globals);
+ if (!globals_tok) {
+ return;
+ }
+ char *token = strtok(globals_tok, "\n");
+ int matches = 0;
+ char *lastmatch = NULL;
+ while (token) {
+ if (str && starts_with(token, str)) {
+ char *name = get_type_name(token);
+ printf("\n%s (%s)", token, name);
+ free(name);
+ lastmatch = token;
+ matches++;
+ }
+ token = strtok(NULL, "\n");
+ }
+ if (matches > 1) {
+ printf("\n");
+ }
+ //Complete matching global.
+ if (matches == 1) {
+ el_deletestr(el, str_len);
+ el_insertstr(el, lastmatch);
+ }
+ free(globals_tok);
+}
+
+static unsigned char complete(EditLine * el, int ch)
+{
+ int argc, pos;
+ const char **argv;
+ const LineInfo *li = el_line(el);
+ Tokenizer *tok = tok_init(NULL);
+
+ //Tokenize current line.
+ int ret = tok_line(tok, li, &argc, &argv, NULL, &pos);
+
+ if (ret != 0) {
+ perror("While tab-completing.");
+ goto complete_exit;
+ }
+ //Show help.
+ if (argc == 0) {
+ size_t help_len;
+ char *help = run_cmd("help()", &help_len);
+ if (help) {
+ printf("\n%s", help);
+ free(help);
+ } else {
+ perror("While communication with daemon");
+ }
+ goto complete_exit;
+ }
+
+ if (argc > 1) {
+ goto complete_exit;
+ }
+ //Get name of type of current line.
+ const char *type = get_type_name(argv[0]);
+
+ if (!type) {
+ goto complete_exit;
+ }
+ //Get position of last dot in current line (useful for parsing table).
+ char *dot = strrchr(argv[0], '.');
+
+ if (strncmp(type, "table", 5) != 0 && !dot) {
+ //Line is not a name of some table and there is no dot in it.
+ complete_globals(el, argv[0], pos);
+ } else if ((dot && strncmp(type, "nil", 3) == 0)
+ || strncmp(type, "table", 5) == 0) {
+ //Current line (or part of it) is a name of some table.
+ complete_members(el, argv[0], type, pos, dot);
+ } else if (strncmp(type, "function", 8) == 0) {
+ //Current line is a function.
+ complete_function(el);
+ }
+ if (type) {
+ free((void *)type);
+ }
+
+complete_exit:
+ tok_reset(tok);
+ tok_end(tok);
+ return CC_REDISPLAY;
+}
+
+//! Initialize connection to the daemon; return 0 on success.
+static int init_tty(const char *path)
+{
+ int fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd < 0)
+ return 1;
+
+ struct sockaddr_un addr;
+ addr.sun_family = AF_UNIX;
+ size_t plen = strlen(path);
+ if (plen + 1 > sizeof(addr.sun_path)) {
+ fprintf(stderr, "Path too long\n");
+ close(fd);
+ return 1;
+ }
+ memcpy(addr.sun_path, path, plen + 1);
+ if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr))) {
+ perror("While connecting to daemon");
+ close(fd);
+ return 1;
+ }
+ g_tty = fdopen(fd, "r+");
+ if (!g_tty) {
+ perror("While opening TTY");
+ close(fd);
+ return 1;
+ }
+
+ // Switch to binary mode and consume the text "> ".
+ if (fprintf(g_tty, "__binary\n") < 0 || !fread(&addr, 2, 1, g_tty)
+ || fflush(g_tty)) {
+ perror("While initializing TTY");
+ fclose(g_tty);
+ g_tty = NULL;
+ return 1;
+ }
+
+ return 0;
+}
+
+//! Run a command on the daemon; return the answer or NULL on failure, puts answer length to out_len.
+static char *run_cmd(const char *cmd, size_t * out_len)
+{
+ if (!g_tty || !cmd)
+ return NULL;
+ if (fprintf(g_tty, "%s", cmd) < 0 || fflush(g_tty))
+ return NULL;
+ uint32_t len;
+ if (!fread(&len, sizeof(len), 1, g_tty))
+ return NULL;
+ len = ntohl(len);
+ if (!len)
+ return NULL;
+ char *msg = malloc(1 + (size_t) len);
+ if (!msg)
+ return NULL;
+ if (len && !fread(msg, len, 1, g_tty)) {
+ free(msg);
+ return NULL;
+ }
+ msg[len] = '\0';
+ *out_len = len;
+ return msg;
+}
+
+static int interact(void)
+{
+ EditLine *el;
+ History *hist;
+ int count;
+ const char *line;
+ HistEvent ev;
+ el = el_init(PROGRAM_NAME, stdin, stdout, stderr);
+ el_set(el, EL_PROMPT, prompt);
+ el_set(el, EL_EDITOR, "emacs");
+ el_set(el, EL_ADDFN, PROGRAM_NAME "-complete",
+ "Perform " PROGRAM_NAME " completion.", complete);
+ el_set(el, EL_BIND, "^I", PROGRAM_NAME "-complete", NULL);
+
+ hist = history_init();
+ if (hist == 0) {
+ perror("While initializing command history");
+ return 1;
+ }
+ history(hist, &ev, H_SETSIZE, 800);
+ el_set(el, EL_HIST, history, hist);
+
+ char *hist_file = NULL;
+
+ char *data_home = getenv("XDG_DATA_HOME");
+
+ //Check whether $XDG_DATA_HOME is set.
+ if (!data_home || *data_home == '\0') {
+ const char *home = getenv("HOME"); //This should be set on any POSIX compliant OS, even for nobody
+
+ //Create necessary folders.
+ char *dirs[3] =
+ { afmt("%s/.local", home), afmt("%s/.local/share", home),
+ afmt("%s/.local/share/knot-resolver/", home)
+ };
+ bool ok = true;
+ for (int i = 0; i < 3; i++) {
+ if (mkdir(dirs[i], 0755) && errno != EEXIST) {
+ ok = false;
+ break;
+ }
+ }
+ if (ok) {
+ hist_file =
+ afmt("%s/.local/share/knot-resolver/" HISTORY_FILE, home);
+ }
+ } else {
+ if (!mkdir(afmt("%s/knot-resolver/", data_home), 0755)
+ || errno == EEXIST) {
+ hist_file = afmt("%s/knot-resolver/" HISTORY_FILE, data_home);
+ }
+ }
+
+ //Load history file
+ if (hist_file) {
+ history(hist, &ev, H_LOAD, hist_file);
+ } else {
+ perror("While opening history file");
+ }
+
+ while (1) {
+ line = el_gets(el, &count);
+ if (count > 0) {
+ history(hist, &ev, H_ENTER, line);
+ size_t msg_len;
+ char *msg = run_cmd(line, &msg_len);
+ if (!msg) {
+ perror("While communication with daemon");
+ history_end(hist);
+ el_end(el);
+ free(msg);
+ return 1;
+ }
+ printf("%s", msg);
+ if (msg_len > 0 && msg[msg_len - 1] != '\n') {
+ printf("\n");
+ }
+ if (hist_file) {
+ history(hist, &ev, H_SAVE, hist_file);
+ }
+ free(msg);
+ }
+ }
+ history_end(hist);
+ free(hist_file);
+ el_end(el);
+ if (feof(stdin))
+ return 0;
+ perror("While reading input");
+ return 1;
+}
+
+int main(int argc, char **argv)
+{
+ fprintf(stderr, "Warning! %s is highly experimental, use at own risk.\n", argv[0]);
+ fprintf(stderr, "Please tell authors what features you expect from client utility.\n");
+ if (argc != 2) {
+ fprintf(stderr, "Usage: %s tty/xxxxx\n", argv[0]);
+ return 1;
+ }
+
+ int res = init_tty(argv[1]);
+
+ if (!res)
+ res = interact();
+
+ if (g_tty)
+ fclose(g_tty);
+ return res;
+}
diff --git a/utils/client/meson.build b/utils/client/meson.build
new file mode 100644
index 0000000..761c2cd
--- /dev/null
+++ b/utils/client/meson.build
@@ -0,0 +1,37 @@
+# client
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+kresc_src = files([
+ 'kresc.c',
+])
+c_src_lint += kresc_src
+
+build_client = false
+if get_option('client') != 'disabled'
+ message('--- client dependencies ---')
+ libedit = dependency('libedit', required: false)
+ if libedit.found()
+ build_client = true
+ else # darwin workaround: missing pkgconfig
+ libedit = meson.get_compiler('c').find_library(
+ 'edit', required: get_option('client') == 'enabled')
+ if libedit.found()
+ build_client = true
+ endif
+ endif
+ message('---------------------------')
+endif
+
+
+if build_client
+ kresc = executable(
+ 'kresc',
+ kresc_src,
+ dependencies: [
+ contrib_dep,
+ libedit,
+ ],
+ install: true,
+ install_dir: get_option('sbindir'),
+ )
+endif
diff --git a/utils/meson.build b/utils/meson.build
new file mode 100644
index 0000000..0487538
--- /dev/null
+++ b/utils/meson.build
@@ -0,0 +1,8 @@
+# utils
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+build_utils = get_option('utils') != 'disabled'
+
+subdir('client')
+subdir('cache_gc')
+subdir('upgrade')
diff --git a/utils/upgrade/meson.build b/utils/upgrade/meson.build
new file mode 100644
index 0000000..33983e8
--- /dev/null
+++ b/utils/upgrade/meson.build
@@ -0,0 +1,13 @@
+## utils/upgrade
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+upgrade_config = configuration_data()
+upgrade_config.set('etc_dir', etc_dir)
+upgrade_config.set('systemd_work_dir', systemd_work_dir)
+
+configure_file(
+ input: 'upgrade-4-to-5.lua.in',
+ output: 'upgrade-4-to-5.lua',
+ configuration: upgrade_config,
+ install_dir: lib_dir
+)
diff --git a/utils/upgrade/upgrade-4-to-5.lua.in b/utils/upgrade/upgrade-4-to-5.lua.in
new file mode 100644
index 0000000..28f800b
--- /dev/null
+++ b/utils/upgrade/upgrade-4-to-5.lua.in
@@ -0,0 +1,87 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+local upg_dir = '@systemd_work_dir@/.upgrade-4-to-5'
+local out = upg_dir..'/kresd.conf.net'
+local sockets = {
+ { file='kresd.socket', kind='dns' },
+ { file='kresd-tls.socket', kind='tls' },
+ { file='kresd-doh.socket', kind='doh2' },
+ { file='kresd-webmgmt.socket', kind='webmgmt' },
+}
+
+-- globals
+addr_port = {}
+outfile = io.open(out, 'w')
+
+if outfile == nil then
+ -- this is technically an error, but upgrade script shouldn't fail in scriptlets
+ os.exit(0) -- make no changes and exit
+end
+
+outfile:write("-- Suggested network interface configuration\n")
+outfile:write("-- See https://knot-resolver.readthedocs.io/en/stable/upgrading.html\n\n")
+outfile:write("-- Please remove any unused or undesired interfaces and add them to\n")
+outfile:write("-- @etc_dir@/kresd.conf\n\n")
+
+local function write_net_listen(addr, port, kind)
+ -- make sure (addr, port) combination is unique
+ for _, val in ipairs(addr_port) do
+ if val.addr == addr and val.port == port then
+ return
+ end
+ end
+
+ table.insert(addr_port, { addr=addr, port=port })
+ outfile:write(
+ "net.listen('"..addr.."', "..tostring(port)..
+ ", { kind = '"..kind.."', freebind = true })\n")
+end
+
+local function convert(line, kind, ipv6only)
+ local patterns = {
+ '^[^=]+=(%d+%.%d+%.%d+%.%d+):(%d+)', -- IPv4
+ '^[^=]+=%[([^%]]+)%]:(%d+)', -- IPv6
+ '^[^=]+=(/.*)', -- UNIX
+ }
+
+ -- Datagram is either implied (dns) or unsupported (tls/doh/webmgmt)
+ if not line:match('^Listen.*Stream') then
+ return
+ end
+
+ for _, pattern in ipairs(patterns) do
+ local addr, port = line:match(pattern)
+ if addr ~= nil then
+ write_net_listen(addr, port, kind)
+ if not ipv6only then
+ if addr:match('^::$') then
+ write_net_listen('0.0.0.0', port, kind)
+ end
+ if addr:match('^::1$') then
+ write_net_listen('127.0.0.1', port, kind)
+ end
+ end
+ end
+ end
+ return
+end
+
+for _, socket in pairs(sockets) do
+ local ipv6only = false
+ local ipv6only_f = io.open(upg_dir..'/'..socket.file..'.v6only', 'r')
+ if ipv6only_f ~= nil then
+ ipv6only = true
+ io.close(ipv6only_f)
+ end
+ local sockinfo = io.open(upg_dir..'/'..socket.file, 'r')
+ if sockinfo ~= nil then
+ for line in sockinfo:lines() do
+ convert(line, socket.kind, ipv6only)
+ end
+ end
+end
+
+outfile:write("\n")
+
+io.close(outfile)
+os.exit(0)