summaryrefslogtreecommitdiffstats
path: root/modules/hints
diff options
context:
space:
mode:
Diffstat (limited to 'modules/hints')
-rw-r--r--modules/hints/.packaging/test.config4
-rw-r--r--modules/hints/README.rst145
-rw-r--r--modules/hints/hints.c681
-rw-r--r--modules/hints/meson.build25
-rw-r--r--modules/hints/tests/hints.test.hosts1
-rw-r--r--modules/hints/tests/hints.test.lua64
-rw-r--r--modules/hints/tests/hints_test.zone2
7 files changed, 922 insertions, 0 deletions
diff --git a/modules/hints/.packaging/test.config b/modules/hints/.packaging/test.config
new file mode 100644
index 0000000..d89c7f0
--- /dev/null
+++ b/modules/hints/.packaging/test.config
@@ -0,0 +1,4 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+modules.load('hints')
+assert(hints)
+quit()
diff --git a/modules/hints/README.rst b/modules/hints/README.rst
new file mode 100644
index 0000000..c2deb5a
--- /dev/null
+++ b/modules/hints/README.rst
@@ -0,0 +1,145 @@
+.. SPDX-License-Identifier: GPL-3.0-or-later
+
+.. _mod-hints:
+
+Static hints
+============
+
+This is a module providing static hints for forward records (A/AAAA) and reverse records (PTR).
+The records can be loaded from ``/etc/hosts``-like files and/or added directly.
+
+You can also use the module to change fallback addresses for the root servers.
+
+.. tip::
+
+ For blocking large lists of domains please use :func:`policy.rpz`
+ instead of creating huge list of domains with IP address *0.0.0.0*.
+
+Examples
+--------
+
+.. code-block:: lua
+
+ -- Load hints after iterator (so hints take precedence before caches)
+ modules = { 'hints > iterate' }
+ -- Add a custom hosts file
+ hints.add_hosts('hosts.custom')
+ -- Override the root hints
+ hints.root({
+ ['j.root-servers.net.'] = { '2001:503:c27::2:30', '192.58.128.30' }
+ })
+ -- Add a custom hint
+ hints['foo.bar'] = '127.0.0.1'
+
+.. note::
+ The :ref:`policy <mod-policy>` module applies before hints,
+ so your hints might get surprisingly shadowed by even default policies.
+
+ That most often happens for :rfc:`6761#section-6` names, e.g.
+ ``localhost`` and ``test`` or with ``PTR`` records in private address ranges.
+ To unblock the required names, you may use an explicit :any:`policy.PASS` action.
+
+ .. code-block:: lua
+
+ policy.add(policy.suffix(policy.PASS, {todname('1.168.192.in-addr.arpa')}))
+
+ This ``.PASS`` workaround isn't ideal. To improve some cases,
+ we recommend to move these ``.PASS`` lines to the end of your rule list.
+ The point is that applying any :ref:`non-chain action <mod-policy-actions>`
+ (e.g. :ref:`forwarding actions <forwarding>` or ``.PASS`` itself)
+ stops processing *any* later policy rules for that request (including the default block-rules).
+ You probably don't want this ``.PASS`` to shadow any other rules you might have;
+ and on the other hand, if any other non-chain rule triggers,
+ additional ``.PASS`` would not change anything even if it were somehow force-executed.
+
+Properties
+----------
+
+.. function:: hints.config([path])
+
+ :param string path: path to hosts-like file, default: no file
+ :return: ``{ result: bool }``
+
+ Clear any configured hints, and optionally load a hosts-like file as in ``hints.add_hosts(path)``.
+ (Root hints are not touched.)
+
+.. function:: hints.add_hosts([path])
+
+ :param string path: path to hosts-like file, default: ``/etc/hosts``
+
+ Add hints from a host-like file.
+
+.. function:: hints.get(hostname)
+
+ :param string hostname: i.e. ``"localhost"``
+ :return: ``{ result: [address1, address2, ...] }``
+
+ Return list of address record matching given name.
+ If no hostname is specified, all hints are returned in the table format used by ``hints.root()``.
+
+.. function:: hints.set(pair)
+
+ :param string pair: ``hostname address`` i.e. ``"localhost 127.0.0.1"``
+ :return: ``{ result: bool }``
+
+ Add a hostname--address pair hint.
+
+ .. note::
+
+ If multiple addresses have been added for a name (in separate ``hints.set()`` commands),
+ all are returned in a forward query.
+ If multiple names have been added to an address, the last one defined is returned
+ in a corresponding PTR query.
+
+.. function:: hints.del(pair)
+
+ :param string pair: ``hostname address`` i.e. ``"localhost 127.0.0.1"``, or just ``hostname``
+ :return: ``{ result: bool }``
+
+ Remove a hostname - address pair hint. If address is omitted, all addresses for the given name are deleted.
+
+.. function:: hints.root_file(path)
+
+ Replace current root hints from a zonefile. If the path is omitted, the compiled-in path is used, i.e. the root hints are reset to the default.
+
+.. function:: hints.root(root_hints)
+
+ :param table root_hints: new set of root hints i.e. ``{['name'] = 'addr', ...}``
+ :return: ``{ ['a.root-servers.net.'] = { '1.2.3.4', '5.6.7.8', ...}, ... }``
+
+ Replace current root hints and return the current table of root hints.
+
+ These root hints are only used as fallback when addresses of ``NS .`` aren't available,
+ e.g. when cache is completely clear.
+
+ .. tip:: If no parameters are passed, it only returns current root hints set without changing anything.
+
+ Example:
+
+ .. code-block:: lua
+
+ > hints.root({
+ ['l.root-servers.net.'] = '199.7.83.42',
+ ['m.root-servers.net.'] = '202.12.27.33'
+ })
+ [l.root-servers.net.] => {
+ [1] => 199.7.83.42
+ }
+ [m.root-servers.net.] => {
+ [1] => 202.12.27.33
+ }
+
+.. function:: hints.use_nodata(toggle)
+
+ :param bool toggle: true if enabling NODATA synthesis, false if disabling
+ :return: ``{ result: bool }``
+
+ If set to true (the default), NODATA will be synthesised for matching hint name, but mismatching type (e.g. AAAA query when only A hint exists).
+
+.. function:: hints.ttl([new_ttl])
+
+ :param int new_ttl: new TTL to set (optional)
+ :return: the TTL setting
+
+ This function allows to read and write the TTL value used for records generated by the hints module.
+
diff --git a/modules/hints/hints.c b/modules/hints/hints.c
new file mode 100644
index 0000000..a3f2f30
--- /dev/null
+++ b/modules/hints/hints.c
@@ -0,0 +1,681 @@
+/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * @file hints.c
+ * @brief Constructed zone cut from the hosts-like file, see @zonecut.h
+ *
+ * The module provides an override for queried address records.
+ */
+
+#include <libknot/packet/pkt.h>
+#include <libknot/descriptor.h>
+#include <ccan/json/json.h>
+#include <ucw/mempool.h>
+#include <contrib/cleanup.h>
+#include <lauxlib.h>
+
+#include "daemon/engine.h"
+#include "lib/zonecut.h"
+#include "lib/module.h"
+#include "lib/layer.h"
+
+#include <inttypes.h>
+#include <math.h>
+
+/* Defaults */
+#define VERBOSE_MSG(qry, ...) kr_log_q(qry, HINT, __VA_ARGS__)
+#define ERR_MSG(...) kr_log_error(HINT, "[ ]" __VA_ARGS__)
+
+struct hints_data {
+ struct kr_zonecut hints;
+ struct kr_zonecut reverse_hints;
+ bool use_nodata; /**< See hint_use_nodata() description, exposed via lua. */
+ uint32_t ttl; /**< TTL used for the hints, exposed via lua. */
+};
+static const uint32_t HINTS_TTL_DEFAULT = 5;
+
+/** Useful for returning from module properties. */
+static char * bool2jsonstr(bool val)
+{
+ char *result = NULL;
+ if (-1 == asprintf(&result, "{ \"result\": %s }", val ? "true" : "false"))
+ result = NULL;
+ return result;
+}
+
+static int put_answer(knot_pkt_t *pkt, struct kr_query *qry, knot_rrset_t *rr, bool use_nodata)
+{
+ int ret = 0;
+ if (!knot_rrset_empty(rr) || use_nodata) {
+ /* Update packet question */
+ if (!knot_dname_is_equal(knot_pkt_qname(pkt), rr->owner)) {
+ kr_pkt_recycle(pkt);
+ knot_pkt_put_question(pkt, qry->sname, qry->sclass, qry->stype);
+ }
+ if (!knot_rrset_empty(rr)) {
+ /* Append to packet */
+ ret = knot_pkt_put_rotate(pkt, KNOT_COMPR_HINT_QNAME, rr,
+ qry->reorder, KNOT_PF_FREE);
+ } else {
+ /* Return empty answer if name exists, but type doesn't match */
+ knot_wire_set_aa(pkt->wire);
+ }
+ } else {
+ ret = kr_error(ENOENT);
+ }
+ /* Clear RR if failed */
+ if (ret != 0) {
+ knot_rrset_clear(rr, &pkt->mm);
+ }
+ return ret;
+}
+
+static int satisfy_reverse(/*const*/ struct hints_data *data,
+ knot_pkt_t *pkt, struct kr_query *qry)
+{
+ /* Find a matching name */
+ pack_t *addr_set = kr_zonecut_find(&data->reverse_hints, qry->sname);
+ if (!addr_set || addr_set->len == 0) {
+ return kr_error(ENOENT);
+ }
+ knot_dname_t *qname = knot_dname_copy(qry->sname, &pkt->mm);
+ knot_rrset_t rr;
+ knot_rrset_init(&rr, qname, KNOT_RRTYPE_PTR, KNOT_CLASS_IN, data->ttl);
+
+ /* Append address records from hints */
+ uint8_t *addr = pack_last(*addr_set);
+ if (addr != NULL) {
+ size_t len = pack_obj_len(addr);
+ void *addr_val = pack_obj_val(addr);
+ knot_rrset_add_rdata(&rr, addr_val, len, &pkt->mm);
+ }
+
+ return put_answer(pkt, qry, &rr, data->use_nodata);
+}
+
+static int satisfy_forward(/*const*/ struct hints_data *data,
+ knot_pkt_t *pkt, struct kr_query *qry)
+{
+ /* Find a matching name */
+ pack_t *addr_set = kr_zonecut_find(&data->hints, qry->sname);
+ if (!addr_set || addr_set->len == 0) {
+ return kr_error(ENOENT);
+ }
+ knot_dname_t *qname = knot_dname_copy(qry->sname, &pkt->mm);
+ knot_rrset_t rr;
+ knot_rrset_init(&rr, qname, qry->stype, qry->sclass, data->ttl);
+
+ size_t family_len;
+ switch (rr.type) {
+ case KNOT_RRTYPE_A:
+ family_len = sizeof(struct in_addr);
+ break;
+ case KNOT_RRTYPE_AAAA:
+ family_len = sizeof(struct in6_addr);
+ break;
+ default:
+ goto finish;
+ };
+
+ /* Append address records from hints */
+ uint8_t *addr = pack_head(*addr_set);
+ while (addr != pack_tail(*addr_set)) {
+ size_t len = pack_obj_len(addr);
+ void *addr_val = pack_obj_val(addr);
+ if (len == family_len) {
+ knot_rrset_add_rdata(&rr, addr_val, len, &pkt->mm);
+ }
+ addr = pack_obj_next(addr);
+ }
+finish:
+ return put_answer(pkt, qry, &rr, data->use_nodata);
+}
+
+static int query(kr_layer_t *ctx, knot_pkt_t *pkt)
+{
+ struct kr_query *qry = ctx->req->current_query;
+ if (!qry || (ctx->state & KR_STATE_FAIL)) {
+ return ctx->state;
+ }
+
+ struct kr_module *module = ctx->api->data;
+ struct hints_data *data = module->data;
+ if (!data) { /* No valid file. */
+ return ctx->state;
+ }
+ /* We can optimize for early return like this: */
+ if (!data->use_nodata && qry->stype != KNOT_RRTYPE_A
+ && qry->stype != KNOT_RRTYPE_AAAA && qry->stype != KNOT_RRTYPE_PTR) {
+ return ctx->state;
+ }
+ /* FIXME: putting directly into packet breaks ordering in case the hint
+ * is applied after a CNAME jump. */
+ const bool is_rev =
+ knot_dname_in_bailiwick(qry->sname, (const uint8_t *)"\4arpa\0") > 0 &&
+ (knot_dname_in_bailiwick(qry->sname, (const uint8_t *)"\7in-addr\4arpa\0") > 0
+ || knot_dname_in_bailiwick(qry->sname, (const uint8_t *)"\3ip6\4arpa\0") > 0);
+ if (is_rev) {
+ if (satisfy_reverse(data, pkt, qry) != 0)
+ return ctx->state;
+ } else {
+ if (satisfy_forward(data, pkt, qry) != 0)
+ return ctx->state;
+ }
+
+ VERBOSE_MSG(qry, "<= answered from hints\n");
+ qry->flags.DNSSEC_WANT = false; /* Never authenticated */
+ qry->flags.CACHED = true;
+ qry->flags.NO_MINIMIZE = true;
+ pkt->parsed = pkt->size;
+ knot_wire_set_qr(pkt->wire);
+ return KR_STATE_DONE;
+}
+
+static int parse_addr_str(union kr_sockaddr *sa, const char *addr)
+{
+ int family = strchr(addr, ':') ? AF_INET6 : AF_INET;
+ memset(sa, 0, sizeof(*sa));
+ sa->ip.sa_family = family;
+ char *addr_bytes = (/*const*/char *)kr_inaddr(&sa->ip);
+ if (inet_pton(family, addr, addr_bytes) != 1) {
+ return kr_error(EILSEQ);
+ }
+ return 0;
+}
+
+/** @warning _NOT_ thread-safe; returns a pointer to static data! */
+static const knot_dname_t * raw_addr2reverse(const uint8_t *raw_addr, int family)
+{
+ #define REV_MAXLEN (4*16 + 16 /* the suffix, terminator, etc. */)
+ char reverse_addr[REV_MAXLEN];
+ static knot_dname_t dname[REV_MAXLEN];
+ #undef REV_MAXLEN
+
+ if (family == AF_INET) {
+ snprintf(reverse_addr, sizeof(reverse_addr),
+ "%d.%d.%d.%d.in-addr.arpa.",
+ raw_addr[3], raw_addr[2], raw_addr[1], raw_addr[0]);
+ } else if (family == AF_INET6) {
+ char *ra_it = reverse_addr;
+ for (int i = 15; i >= 0; --i) {
+ ssize_t free_space = reverse_addr + sizeof(reverse_addr) - ra_it;
+ int written = snprintf(ra_it, free_space, "%x.%x.",
+ raw_addr[i] & 0x0f, raw_addr[i] >> 4);
+ if (kr_fails_assert(written < free_space))
+ return NULL;
+ ra_it += written;
+ }
+ ssize_t free_space = reverse_addr + sizeof(reverse_addr) - ra_it;
+ if (snprintf(ra_it, free_space, "ip6.arpa.") >= free_space) {
+ return NULL;
+ }
+ } else {
+ return NULL;
+ }
+
+ if (!knot_dname_from_str(dname, reverse_addr, sizeof(dname))) {
+ return NULL;
+ }
+ return dname;
+}
+
+static const knot_dname_t * addr2reverse(const char *addr)
+{
+ /* Parse address string */
+ union kr_sockaddr ia;
+ if (parse_addr_str(&ia, addr) != 0) {
+ return NULL;
+ }
+ return raw_addr2reverse((const /*sign*/uint8_t *)kr_inaddr(&ia.ip),
+ kr_inaddr_family(&ia.ip));
+}
+
+static int add_pair(struct kr_zonecut *hints, const char *name, const char *addr)
+{
+ /* Build key */
+ knot_dname_t key[KNOT_DNAME_MAXLEN];
+ if (!knot_dname_from_str(key, name, sizeof(key))) {
+ return kr_error(EINVAL);
+ }
+ knot_dname_to_lower(key);
+
+ union kr_sockaddr ia;
+ if (parse_addr_str(&ia, addr) != 0) {
+ return kr_error(EINVAL);
+ }
+
+ return kr_zonecut_add(hints, key, kr_inaddr(&ia.ip), kr_inaddr_len(&ia.ip));
+}
+
+static int add_reverse_pair(struct kr_zonecut *hints, const char *name, const char *addr)
+{
+ const knot_dname_t *key = addr2reverse(addr);
+
+ if (key == NULL) {
+ return kr_error(EINVAL);
+ }
+
+ knot_dname_t ptr_name[KNOT_DNAME_MAXLEN];
+ if (!knot_dname_from_str(ptr_name, name, sizeof(ptr_name))) {
+ return kr_error(EINVAL);
+ }
+
+ return kr_zonecut_add(hints, key, ptr_name, knot_dname_size(ptr_name));
+}
+
+/** For a given name, remove either one address or all of them (if == NULL).
+ *
+ * Also remove the corresponding reverse records.
+ */
+static int del_pair(struct hints_data *data, const char *name, const char *addr)
+{
+ /* Build key */
+ knot_dname_t key[KNOT_DNAME_MAXLEN];
+ if (!knot_dname_from_str(key, name, sizeof(key))) {
+ return kr_error(EINVAL);
+ }
+ int key_len = knot_dname_size(key);
+
+ if (addr) {
+ /* Remove the pair. */
+ union kr_sockaddr ia;
+ if (parse_addr_str(&ia, addr) != 0) {
+ return kr_error(EINVAL);
+ }
+
+ const knot_dname_t *reverse_key = addr2reverse(addr);
+ kr_zonecut_del(&data->reverse_hints, reverse_key, key, key_len);
+ return kr_zonecut_del(&data->hints, key,
+ kr_inaddr(&ia.ip), kr_inaddr_len(&ia.ip));
+ }
+ /* We're removing everything for the name;
+ * first find the name's pack */
+ pack_t *addr_set = kr_zonecut_find(&data->hints, key);
+ if (!addr_set || addr_set->len == 0) {
+ return kr_error(ENOENT);
+ }
+
+ /* Remove address records in hints from reverse_hints. */
+
+ for (uint8_t *a = pack_head(*addr_set); a != pack_tail(*addr_set);
+ a = pack_obj_next(a)) {
+ void *addr_val = pack_obj_val(a);
+ int family = pack_obj_len(a) == kr_family_len(AF_INET)
+ ? AF_INET : AF_INET6;
+ const knot_dname_t *reverse_key = raw_addr2reverse(addr_val, family);
+ if (reverse_key != NULL) {
+ kr_zonecut_del(&data->reverse_hints, reverse_key, key, key_len);
+ }
+ }
+
+ /* Remove the whole name. */
+ return kr_zonecut_del_all(&data->hints, key);
+}
+
+static int load_file(struct kr_module *module, const char *path)
+{
+ auto_fclose FILE *fp = fopen(path, "r");
+ if (fp == NULL) {
+ ERR_MSG("reading '%s' failed: %s\n", path, strerror(errno));
+ return kr_error(errno);
+ } else {
+ VERBOSE_MSG(NULL, "reading '%s'\n", path);
+ }
+
+ /* Load file to map */
+ struct hints_data *data = module->data;
+ size_t line_len_unused = 0;
+ size_t count = 0;
+ size_t line_count = 0;
+ auto_free char *line = NULL;
+ int ret = kr_ok();
+
+ while (getline(&line, &line_len_unused, fp) > 0) {
+ ++line_count;
+ /* Ingore #comments as described in man hosts.5 */
+ char *comm = strchr(line, '#');
+ if (comm) {
+ *comm = '\0';
+ }
+
+ char *saveptr = NULL;
+ const char *addr = strtok_r(line, " \t\n", &saveptr);
+ if (addr == NULL || strlen(addr) == 0) {
+ continue;
+ }
+ const char *canonical_name = strtok_r(NULL, " \t\n", &saveptr);
+ if (canonical_name == NULL) {
+ ret = -1;
+ goto error;
+ }
+ /* Since the last added PTR records takes preference,
+ * we add canonical name as the last one. */
+ const char *name_tok;
+ while ((name_tok = strtok_r(NULL, " \t\n", &saveptr)) != NULL) {
+ ret = add_pair(&data->hints, name_tok, addr);
+ if (!ret) {
+ ret = add_reverse_pair(&data->reverse_hints, name_tok, addr);
+ }
+ if (ret) {
+ ret = -1;
+ goto error;
+ }
+ count += 1;
+ }
+ ret = add_pair(&data->hints, canonical_name, addr);
+ if (!ret) {
+ ret = add_reverse_pair(&data->reverse_hints, canonical_name, addr);
+ }
+ if (ret) {
+ ret = -1;
+ goto error;
+ }
+ count += 1;
+ }
+error:
+ if (ret) {
+ ret = kr_error(ret);
+ ERR_MSG("%s:%zu: invalid syntax\n", path, line_count);
+ }
+ VERBOSE_MSG(NULL, "loaded %zu hints\n", count);
+ return ret;
+}
+
+static char* hint_add_hosts(void *env, struct kr_module *module, const char *args)
+{
+ if (!args)
+ args = "/etc/hosts";
+ int err = load_file(module, args);
+ return bool2jsonstr(err == kr_ok());
+}
+
+/**
+ * Set name => address hint.
+ *
+ * Input: { name, address }
+ * Output: { result: bool }
+ *
+ */
+static char* hint_set(void *env, struct kr_module *module, const char *args)
+{
+ struct hints_data *data = module->data;
+ if (!args)
+ return NULL;
+ auto_free char *args_copy = strdup(args);
+ if (!args_copy)
+ return NULL;
+
+ int ret = -1;
+ char *addr = strchr(args_copy, ' ');
+ if (addr) {
+ *addr = '\0';
+ ++addr;
+ ret = add_reverse_pair(&data->reverse_hints, args_copy, addr);
+ if (ret) {
+ del_pair(data, args_copy, addr);
+ } else {
+ ret = add_pair(&data->hints, args_copy, addr);
+ }
+ }
+
+ return bool2jsonstr(ret == 0);
+}
+
+static char* hint_del(void *env, struct kr_module *module, const char *args)
+{
+ struct hints_data *data = module->data;
+ if (!args)
+ return NULL;
+ auto_free char *args_copy = strdup(args);
+ if (!args_copy)
+ return NULL;
+
+ int ret = -1;
+ char *addr = strchr(args_copy, ' ');
+ if (addr) {
+ *addr = '\0';
+ ++addr;
+ }
+ ret = del_pair(data, args_copy, addr);
+
+ return bool2jsonstr(ret == 0);
+}
+
+/** @internal Pack address list into JSON array. */
+static JsonNode *pack_addrs(pack_t *pack)
+{
+ char buf[INET6_ADDRSTRLEN];
+ JsonNode *root = json_mkarray();
+ uint8_t *addr = pack_head(*pack);
+ while (addr != pack_tail(*pack)) {
+ size_t len = pack_obj_len(addr);
+ int family = len == sizeof(struct in_addr) ? AF_INET : AF_INET6;
+ if (!inet_ntop(family, pack_obj_val(addr), buf, sizeof(buf))) {
+ break;
+ }
+ json_append_element(root, json_mkstring(buf));
+ addr = pack_obj_next(addr);
+ }
+ return root;
+}
+
+static char* pack_hints(struct kr_zonecut *hints);
+/**
+ * Retrieve address hints, either for given name or for all names.
+ *
+ * Input: name
+ * Output: NULL or "{ address1, address2, ... }"
+ */
+static char* hint_get(void *env, struct kr_module *module, const char *args)
+{
+ struct kr_zonecut *hints = &((struct hints_data *) module->data)->hints;
+ if (kr_fails_assert(hints))
+ return NULL;
+
+ if (!args) {
+ return pack_hints(hints);
+ }
+
+ knot_dname_t key[KNOT_DNAME_MAXLEN];
+ pack_t *pack = NULL;
+ if (knot_dname_from_str(key, args, sizeof(key))) {
+ pack = kr_zonecut_find(hints, key);
+ }
+ if (!pack || pack->len == 0) {
+ return NULL;
+ }
+
+ char *result = NULL;
+ JsonNode *root = pack_addrs(pack);
+ if (root) {
+ result = json_encode(root);
+ json_delete(root);
+ }
+ return result;
+}
+
+/** @internal Pack all hints into serialized JSON. */
+static char* pack_hints(struct kr_zonecut *hints) {
+ char *result = NULL;
+ JsonNode *root_node = json_mkobject();
+ trie_it_t *it;
+ for (it = trie_it_begin(hints->nsset); !trie_it_finished(it); trie_it_next(it)) {
+ KR_DNAME_GET_STR(nsname_str, (const knot_dname_t *)trie_it_key(it, NULL));
+ JsonNode *addr_list = pack_addrs((pack_t *)*trie_it_val(it));
+ if (!addr_list) goto error;
+ json_append_member(root_node, nsname_str, addr_list);
+ }
+ result = json_encode(root_node);
+error:
+ trie_it_free(it);
+ json_delete(root_node);
+ return result;
+}
+
+static void unpack_hint(struct kr_zonecut *root_hints, JsonNode *table, const char *name)
+{
+ JsonNode *node = NULL;
+ json_foreach(node, table) {
+ switch(node->tag) {
+ case JSON_STRING: add_pair(root_hints, name ? name : node->key, node->string_); break;
+ case JSON_ARRAY: unpack_hint(root_hints, node, name ? name : node->key); break;
+ default: continue;
+ }
+ }
+}
+
+/**
+ * Get/set root hints set.
+ *
+ * Input: { name: [addr_list], ... }
+ * Output: current list
+ *
+ */
+static char* hint_root(void *env, struct kr_module *module, const char *args)
+{
+ struct engine *engine = env;
+ struct kr_context *ctx = &engine->resolver;
+ struct kr_zonecut *root_hints = &ctx->root_hints;
+ /* Replace root hints if parameter is set */
+ if (args && args[0] != '\0') {
+ JsonNode *root_node = json_decode(args);
+ kr_zonecut_set(root_hints, (const uint8_t *)"");
+ unpack_hint(root_hints, root_node, NULL);
+ json_delete(root_node);
+ }
+ /* Return current root hints */
+ return pack_hints(root_hints);
+}
+
+static char* hint_root_file(void *env, struct kr_module *module, const char *args)
+{
+ struct engine *engine = env;
+ struct kr_context *ctx = &engine->resolver;
+ const char *err_msg = engine_hint_root_file(ctx, args);
+ if (err_msg) {
+ luaL_error(engine->L, "error when opening '%s': %s", args, err_msg);
+ }
+ return strdup(err_msg ? err_msg : "");
+}
+
+static char* hint_use_nodata(void *env, struct kr_module *module, const char *args)
+{
+ struct hints_data *data = module->data;
+ if (!args) {
+ return NULL;
+ }
+
+ JsonNode *root_node = json_decode(args);
+ if (!root_node || root_node->tag != JSON_BOOL) {
+ json_delete(root_node);
+ return bool2jsonstr(false);
+ }
+
+ data->use_nodata = root_node->bool_;
+ json_delete(root_node);
+ return bool2jsonstr(true);
+}
+
+static char* hint_ttl(void *env, struct kr_module *module, const char *args)
+{
+ struct hints_data *data = module->data;
+
+ /* Do no change on nonsense TTL values (incl. suspicious floats). */
+ JsonNode *root_node = args ? json_decode(args) : NULL;
+ if (root_node && root_node->tag == JSON_NUMBER) {
+ double ttl_d = root_node->number_;
+ uint32_t ttl = (uint32_t)round(ttl_d);
+ if (ttl_d >= 0 && fabs(ttl_d - ttl) * 64 < 1) {
+ data->ttl = ttl;
+ }
+ }
+ json_delete(root_node);
+
+ /* Always return the current TTL setting. Plain number is valid JSON. */
+ char *result = NULL;
+ if (-1 == asprintf(&result, "%"PRIu32, data->ttl)) {
+ result = NULL;
+ }
+ return result;
+}
+
+/** Basic initialization: get a memory pool, etc. */
+KR_EXPORT
+int hints_init(struct kr_module *module)
+{
+ static kr_layer_api_t layer = {
+ .produce = &query,
+ };
+ /* Store module reference */
+ layer.data = module;
+ module->layer = &layer;
+
+ static const struct kr_prop props[] = {
+ { &hint_set, "set", "Set {name, address} hint.", },
+ { &hint_del, "del", "Delete one {name, address} hint or all addresses for the name.", },
+ { &hint_get, "get", "Retrieve hint for given name.", },
+ { &hint_ttl, "ttl", "Set/get TTL used for the hints.", },
+ { &hint_add_hosts, "add_hosts", "Load a file with hosts-like formatting and add contents into hints.", },
+ { &hint_root, "root", "Replace root hints set (empty value to return current list).", },
+ { &hint_root_file, "root_file", "Replace root hints set from a zonefile.", },
+ { &hint_use_nodata, "use_nodata", "Synthesise NODATA if name matches, but type doesn't. True by default.", },
+ { NULL, NULL, NULL }
+ };
+ module->props = props;
+
+ knot_mm_t *pool = mm_ctx_mempool2(MM_DEFAULT_BLKSIZE);
+ if (!pool) {
+ return kr_error(ENOMEM);
+ }
+ struct hints_data *data = mm_alloc(pool, sizeof(struct hints_data));
+ if (!data) {
+ mp_delete(pool->ctx);
+ return kr_error(ENOMEM);
+ }
+ kr_zonecut_init(&data->hints, (const uint8_t *)(""), pool);
+ kr_zonecut_init(&data->reverse_hints, (const uint8_t *)(""), pool);
+ data->use_nodata = true;
+ data->ttl = HINTS_TTL_DEFAULT;
+ module->data = data;
+
+ return kr_ok();
+}
+
+/** Release all resources. */
+KR_EXPORT
+int hints_deinit(struct kr_module *module)
+{
+ struct hints_data *data = module->data;
+ if (data) {
+ kr_zonecut_deinit(&data->hints);
+ kr_zonecut_deinit(&data->reverse_hints);
+ mp_delete(data->hints.pool->ctx);
+ module->data = NULL;
+ }
+ return kr_ok();
+}
+
+/** Drop all hints, and load a hosts file if any was specified.
+ *
+ * It seems slightly strange to drop all, but keep doing that for now.
+ */
+KR_EXPORT
+int hints_config(struct kr_module *module, const char *conf)
+{
+ hints_deinit(module);
+ int err = hints_init(module);
+ if (err != kr_ok()) {
+ return err;
+ }
+
+ if (conf && conf[0]) {
+ return load_file(module, conf);
+ }
+ return kr_ok();
+}
+
+KR_MODULE_EXPORT(hints)
+
+#undef VERBOSE_MSG
diff --git a/modules/hints/meson.build b/modules/hints/meson.build
new file mode 100644
index 0000000..b837918
--- /dev/null
+++ b/modules/hints/meson.build
@@ -0,0 +1,25 @@
+# C module: hints
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+hints_src = files([
+ 'hints.c',
+])
+c_src_lint += hints_src
+
+hints_mod = shared_module(
+ 'hints',
+ hints_src,
+ dependencies: [
+ libknot,
+ luajit,
+ ],
+ include_directories: mod_inc_dir,
+ name_prefix: '',
+ install: true,
+ install_dir: modules_dir,
+ link_with: kresd,
+)
+
+config_tests += [
+ ['hints', files('tests/hints.test.lua'), ['skip_asan']],
+]
diff --git a/modules/hints/tests/hints.test.hosts b/modules/hints/tests/hints.test.hosts
new file mode 100644
index 0000000..0111507
--- /dev/null
+++ b/modules/hints/tests/hints.test.hosts
@@ -0,0 +1 @@
+192.0.2.1 myname.lan # badname.lan and the rest of the comment
diff --git a/modules/hints/tests/hints.test.lua b/modules/hints/tests/hints.test.lua
new file mode 100644
index 0000000..b62e502
--- /dev/null
+++ b/modules/hints/tests/hints.test.lua
@@ -0,0 +1,64 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+local utils = require('test_utils')
+
+-- setup resolver
+modules = { 'hints > iterate' }
+
+-- test for default configuration
+local function test_default()
+ -- get loaded root hints and change names to lowercase
+ local hints_data = utils.table_keys_to_lower(hints.root())
+
+ -- root hints loaded from default location
+ -- check correct ip address of a.root-server.net
+ utils.contains(hints_data['a.root-servers.net.'], '198.41.0.4', 'has IP address for a.root-servers.net.')
+end
+
+-- test loading from config file
+local function test_custom()
+ -- load custom root hints file with fake ip address for a.root-server.net
+ local err_msg = hints.root_file('hints_test.zone')
+ same(err_msg, '', 'load root hints from file')
+
+ -- get loaded root hints and change names to lowercase
+ local hints_data = utils.table_keys_to_lower(hints.root())
+ isnt(hints_data['a.root-servers.net.'], nil, 'can retrieve root hints')
+
+ -- check loaded ip address of a.root-server.net
+ utils.not_contains(hints_data['a.root-servers.net.'], '198.41.0.4',
+ 'real IP address for a.root-servers.net. is replaced')
+ utils.contains(hints_data['a.root-servers.net.'], '10.0.0.1',
+ 'real IP address for a.root-servers.net. is correct')
+end
+
+-- test that setting an address hint works (TODO: and NXDOMAIN)
+local function test_nxdomain()
+ hints.config() -- clean start
+ hints.use_nodata(false)
+ hints.add_hosts('hints.test.hosts')
+ -- TODO: prefilling or some other way of getting NXDOMAIN (instead of SERVFAIL)
+ utils.check_answer('bad name gives NXDOMAIN',
+ 'badname.lan', kres.type.A, kres.rcode.SERVFAIL)
+ utils.check_answer('another type gives NXDOMAIN',
+ 'myname.lan', kres.type.AAAA, kres.rcode.SERVFAIL)
+ utils.check_answer('record itself is OK',
+ 'myname.lan', kres.type.A, kres.rcode.NOERROR)
+end
+
+-- test that NODATA is correctly generated
+local function test_nodata()
+ hints.config() -- clean start
+ hints.use_nodata(true) -- default ATM but let's not depend on that
+ hints['myname.lan'] = '2001:db8::1'
+ utils.check_answer('another type gives NODATA',
+ 'myname.lan', kres.type.MX, utils.NODATA)
+ utils.check_answer('record itself is OK',
+ 'myname.lan', kres.type.AAAA, kres.rcode.NOERROR)
+end
+
+return {
+ test_default,
+ test_custom,
+ test_nxdomain,
+ test_nodata,
+}
diff --git a/modules/hints/tests/hints_test.zone b/modules/hints/tests/hints_test.zone
new file mode 100644
index 0000000..c3252f8
--- /dev/null
+++ b/modules/hints/tests/hints_test.zone
@@ -0,0 +1,2 @@
+; SPDX-License-Identifier: GPL-3.0-or-later
+A.ROOT-SERVERS.NET. 3600000 A 10.0.0.1