summaryrefslogtreecommitdiffstats
path: root/src/utils
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 19:05:44 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 19:05:44 +0000
commitb045529c40c83601909dca7b76a53498e9a70f33 (patch)
tree88371572105933fd950676c07b3a12163a0c9de0 /src/utils
parentInitial commit. (diff)
downloadknot-b045529c40c83601909dca7b76a53498e9a70f33.tar.xz
knot-b045529c40c83601909dca7b76a53498e9a70f33.zip
Adding upstream version 3.3.4.upstream/3.3.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/utils')
-rw-r--r--src/utils/Makefile.inc188
-rw-r--r--src/utils/common/exec.c1212
-rw-r--r--src/utils/common/exec.h137
-rw-r--r--src/utils/common/hex.c82
-rw-r--r--src/utils/common/hex.h31
-rw-r--r--src/utils/common/https.c528
-rw-r--r--src/utils/common/https.h150
-rw-r--r--src/utils/common/lookup.c295
-rw-r--r--src/utils/common/lookup.h124
-rw-r--r--src/utils/common/msg.c40
-rw-r--r--src/utils/common/msg.h42
-rw-r--r--src/utils/common/netio.c925
-rw-r--r--src/utils/common/netio.h253
-rw-r--r--src/utils/common/params.c343
-rw-r--r--src/utils/common/params.h170
-rw-r--r--src/utils/common/quic.c813
-rw-r--r--src/utils/common/quic.h118
-rw-r--r--src/utils/common/resolv.c211
-rw-r--r--src/utils/common/resolv.h24
-rw-r--r--src/utils/common/sign.c109
-rw-r--r--src/utils/common/sign.h63
-rw-r--r--src/utils/common/signal.c66
-rw-r--r--src/utils/common/signal.h37
-rw-r--r--src/utils/common/tls.c736
-rw-r--r--src/utils/common/tls.h83
-rw-r--r--src/utils/common/token.c115
-rw-r--r--src/utils/common/token.h65
-rw-r--r--src/utils/common/util_conf.c139
-rw-r--r--src/utils/common/util_conf.h86
-rw-r--r--src/utils/kcatalogprint/main.c185
-rw-r--r--src/utils/kdig/kdig_exec.c1324
-rw-r--r--src/utils/kdig/kdig_exec.h21
-rw-r--r--src/utils/kdig/kdig_main.c45
-rw-r--r--src/utils/kdig/kdig_params.c2765
-rw-r--r--src/utils/kdig/kdig_params.h187
-rw-r--r--src/utils/keymgr/bind_privkey.c411
-rw-r--r--src/utils/keymgr/bind_privkey.h72
-rw-r--r--src/utils/keymgr/functions.c1161
-rw-r--r--src/utils/keymgr/functions.h62
-rw-r--r--src/utils/keymgr/main.c416
-rw-r--r--src/utils/keymgr/offline_ksk.c582
-rw-r--r--src/utils/keymgr/offline_ksk.h35
-rw-r--r--src/utils/khost/khost_main.c45
-rw-r--r--src/utils/khost/khost_params.c365
-rw-r--r--src/utils/khost/khost_params.h22
-rw-r--r--src/utils/kjournalprint/main.c491
-rw-r--r--src/utils/knotc/commands.c1377
-rw-r--r--src/utils/knotc/commands.h74
-rw-r--r--src/utils/knotc/interactive.c450
-rw-r--r--src/utils/knotc/interactive.h26
-rw-r--r--src/utils/knotc/main.c181
-rw-r--r--src/utils/knotc/process.c291
-rw-r--r--src/utils/knotc/process.h78
-rw-r--r--src/utils/knotd/main.c670
-rw-r--r--src/utils/knsec3hash/knsec3hash.c187
-rw-r--r--src/utils/knsupdate/knsupdate_exec.c1059
-rw-r--r--src/utils/knsupdate/knsupdate_exec.h25
-rw-r--r--src/utils/knsupdate/knsupdate_interactive.c177
-rw-r--r--src/utils/knsupdate/knsupdate_interactive.h26
-rw-r--r--src/utils/knsupdate/knsupdate_main.c46
-rw-r--r--src/utils/knsupdate/knsupdate_params.c304
-rw-r--r--src/utils/knsupdate/knsupdate_params.h72
-rw-r--r--src/utils/kxdpgun/ip_route.c358
-rw-r--r--src/utils/kxdpgun/ip_route.h46
-rw-r--r--src/utils/kxdpgun/load_queries.c164
-rw-r--r--src/utils/kxdpgun/load_queries.h32
-rw-r--r--src/utils/kxdpgun/main.c1502
-rw-r--r--src/utils/kzonecheck/main.c185
-rw-r--r--src/utils/kzonecheck/zone_check.c101
-rw-r--r--src/utils/kzonecheck/zone_check.h23
-rw-r--r--src/utils/kzonesign/main.c283
71 files changed, 23111 insertions, 0 deletions
diff --git a/src/utils/Makefile.inc b/src/utils/Makefile.inc
new file mode 100644
index 0000000..b39b10d
--- /dev/null
+++ b/src/utils/Makefile.inc
@@ -0,0 +1,188 @@
+bin_PROGRAMS =
+sbin_PROGRAMS =
+
+if HAVE_LIBUTILS
+noinst_LTLIBRARIES += libknotus.la
+
+libknotus_la_CPPFLAGS = $(embedded_libngtcp2_CFLAGS) \
+ $(AM_CPPFLAGS) $(CFLAG_VISIBILITY) $(gnutls_CFLAGS) \
+ $(libedit_CFLAGS) $(libidn2_CFLAGS) $(libidn_CFLAGS) \
+ $(libkqueue_CFLAGS) $(libnghttp2_CFLAGS) $(libngtcp2_CFLAGS) \
+ $(lmdb_CFLAGS)
+libknotus_la_LDFLAGS = $(AM_LDFLAGS) $(LDFLAG_EXCLUDE_LIBS)
+libknotus_la_LIBADD = $(libidn2_LIBS) $(libidn_LIBS) $(libnghttp2_LIBS) $(libngtcp2_LIBS)
+libknotus_LIBS = libknotus.la libknot.la libdnssec.la $(libcontrib_LIBS) \
+ $(gnutls_LIBS) $(libedit_LIBS)
+
+if EMBEDDED_LIBNGTCP2
+libknotus_la_LIBADD += $(libembngtcp2_LIBS)
+endif EMBEDDED_LIBNGTCP2
+
+libknotus_la_SOURCES = \
+ utils/common/exec.c \
+ utils/common/exec.h \
+ utils/common/hex.c \
+ utils/common/hex.h \
+ utils/common/https.c \
+ utils/common/https.h \
+ utils/common/lookup.c \
+ utils/common/lookup.h \
+ utils/common/msg.c \
+ utils/common/msg.h \
+ utils/common/netio.c \
+ utils/common/netio.h \
+ utils/common/params.c \
+ utils/common/params.h \
+ utils/common/quic.c \
+ utils/common/quic.h \
+ utils/common/resolv.c \
+ utils/common/resolv.h \
+ utils/common/sign.c \
+ utils/common/sign.h \
+ utils/common/signal.c \
+ utils/common/signal.h \
+ utils/common/tls.c \
+ utils/common/tls.h \
+ utils/common/token.c \
+ utils/common/token.h \
+ utils/common/util_conf.c \
+ utils/common/util_conf.h
+endif HAVE_LIBUTILS
+
+if HAVE_UTILS
+bin_PROGRAMS += kdig khost knsec3hash knsupdate
+
+kdig_SOURCES = \
+ utils/kdig/kdig_exec.c \
+ utils/kdig/kdig_exec.h \
+ utils/kdig/kdig_main.c \
+ utils/kdig/kdig_params.c \
+ utils/kdig/kdig_params.h
+
+khost_SOURCES = \
+ utils/kdig/kdig_exec.c \
+ utils/kdig/kdig_exec.h \
+ utils/kdig/kdig_params.c \
+ utils/kdig/kdig_params.h \
+ utils/khost/khost_main.c \
+ utils/khost/khost_params.c \
+ utils/khost/khost_params.h
+
+knsec3hash_SOURCES = \
+ utils/knsec3hash/knsec3hash.c
+
+knsupdate_SOURCES = \
+ utils/knsupdate/knsupdate_exec.c \
+ utils/knsupdate/knsupdate_exec.h \
+ utils/knsupdate/knsupdate_interactive.c \
+ utils/knsupdate/knsupdate_interactive.h \
+ utils/knsupdate/knsupdate_main.c \
+ utils/knsupdate/knsupdate_params.c \
+ utils/knsupdate/knsupdate_params.h
+
+kdig_CPPFLAGS = $(libknotus_la_CPPFLAGS)
+kdig_LDADD = $(libknotus_LIBS)
+khost_CPPFLAGS = $(libknotus_la_CPPFLAGS)
+khost_LDADD = $(libknotus_LIBS)
+knsec3hash_CPPFLAGS = $(libknotus_la_CPPFLAGS)
+knsec3hash_LDADD = libknot.la libdnssec.la $(libcontrib_LIBS)
+knsupdate_CPPFLAGS = $(libknotus_la_CPPFLAGS)
+knsupdate_LDADD = $(libknotus_LIBS) libzscanner.la
+
+if HAVE_DNSTAP
+kdig_CPPFLAGS += $(DNSTAP_CFLAGS)
+kdig_LDADD += $(libdnstap_LIBS)
+khost_CPPFLAGS += $(DNSTAP_CFLAGS)
+khost_LDADD += $(libdnstap_LIBS)
+endif HAVE_DNSTAP
+
+if ENABLE_XDP
+sbin_PROGRAMS += kxdpgun
+kxdpgun_SOURCES = \
+ utils/kxdpgun/ip_route.c \
+ utils/kxdpgun/ip_route.h \
+ utils/kxdpgun/load_queries.c \
+ utils/kxdpgun/load_queries.h \
+ utils/kxdpgun/main.c
+
+kxdpgun_CPPFLAGS = $(libknotus_la_CPPFLAGS) $(libmnl_CFLAGS)
+kxdpgun_LDADD = libknot.la $(libcontrib_LIBS) $(libmnl_LIBS) $(pthread_LIBS)
+if ENABLE_QUIC
+kxdpgun_CPPFLAGS += $(gnutls_CFLAGS)
+kxdpgun_LDADD += $(gnutls_LIBS)
+endif ENABLE_QUIC
+endif ENABLE_XDP
+endif HAVE_UTILS
+
+if HAVE_DAEMON
+# Create storage and run-time directories
+install-data-hook:
+ $(INSTALL) -d $(DESTDIR)/@config_dir@
+ $(INSTALL) -d $(DESTDIR)/@run_dir@
+ $(INSTALL) -d $(DESTDIR)/@storage_dir@
+
+sbin_PROGRAMS += knotc knotd
+
+knotc_SOURCES = \
+ utils/knotc/commands.c \
+ utils/knotc/commands.h \
+ utils/knotc/interactive.c \
+ utils/knotc/interactive.h \
+ utils/knotc/process.c \
+ utils/knotc/process.h \
+ utils/knotc/main.c
+
+knotd_SOURCES = \
+ utils/knotd/main.c
+
+knotc_CPPFLAGS = $(libknotus_la_CPPFLAGS)
+knotc_LDADD = $(libknotd_LIBS) $(libknotus_LIBS)
+knotc_LDFLAGS = $(AM_LDFLAGS) -rdynamic
+knotd_CPPFLAGS = $(libknotus_la_CPPFLAGS) $(liburcu_CFLAGS) $(systemd_CFLAGS)
+knotd_LDADD = $(malloc_LIBS) $(libknotd_LIBS) $(cap_ng_LIBS)
+knotd_LDFLAGS = $(AM_LDFLAGS) -rdynamic
+
+if HAVE_UTILS
+bin_PROGRAMS += kzonecheck kzonesign
+sbin_PROGRAMS += keymgr kjournalprint kcatalogprint
+
+kzonecheck_SOURCES = \
+ utils/kzonecheck/main.c \
+ utils/kzonecheck/zone_check.c \
+ utils/kzonecheck/zone_check.h
+
+kzonesign_SOURCES = \
+ utils/kzonesign/main.c
+
+keymgr_SOURCES = \
+ utils/keymgr/bind_privkey.c \
+ utils/keymgr/bind_privkey.h \
+ utils/keymgr/functions.c \
+ utils/keymgr/functions.h \
+ utils/keymgr/offline_ksk.c \
+ utils/keymgr/offline_ksk.h \
+ utils/keymgr/main.c
+
+kjournalprint_SOURCES = \
+ utils/kjournalprint/main.c
+
+kcatalogprint_SOURCES = \
+ utils/kcatalogprint/main.c
+
+kzonecheck_CPPFLAGS = $(libknotus_la_CPPFLAGS)
+kzonecheck_LDADD = $(libknotd_LIBS)
+kzonecheck_LDFLAGS = $(AM_LDFLAGS) -rdynamic
+kzonesign_CPPFLAGS = $(libknotus_la_CPPFLAGS)
+kzonesign_LDADD = $(libknotd_LIBS) $(libknotus_LIBS)
+kzonesign_LDFLAGS = $(AM_LDFLAGS) -rdynamic
+keymgr_CPPFLAGS = $(libknotus_la_CPPFLAGS)
+keymgr_LDADD = $(libknotd_LIBS) $(libknotus_LIBS)
+keymgr_LDFLAGS = $(AM_LDFLAGS) -rdynamic
+kjournalprint_CPPFLAGS = $(libknotus_la_CPPFLAGS)
+kjournalprint_LDADD = $(libknotd_LIBS) $(libknotus_LIBS)
+kjournalprint_LDFLAGS = $(AM_LDFLAGS) -rdynamic
+kcatalogprint_CPPFLAGS = $(libknotus_la_CPPFLAGS)
+kcatalogprint_LDADD = $(libknotd_LIBS) $(libknotus_LIBS)
+kcatalogprint_LDFLAGS = $(AM_LDFLAGS) -rdynamic
+endif HAVE_UTILS
+endif HAVE_DAEMON
diff --git a/src/utils/common/exec.c b/src/utils/common/exec.c
new file mode 100644
index 0000000..798ae42
--- /dev/null
+++ b/src/utils/common/exec.c
@@ -0,0 +1,1212 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <arpa/inet.h>
+#include <stdlib.h>
+#include <time.h>
+
+#include "libdnssec/random.h"
+#include "utils/common/exec.h"
+#include "utils/common/msg.h"
+#include "utils/common/netio.h"
+#include "utils/common/params.h"
+#include "libknot/libknot.h"
+#include "contrib/ctype.h"
+#include "contrib/macros.h"
+#include "contrib/sockaddr.h"
+#include "contrib/time.h"
+#include "contrib/openbsd/strlcat.h"
+#include "contrib/ucw/lists.h"
+#include "contrib/wire_ctx.h"
+
+static const char *JSON_INDENT = " ";
+
+static knot_lookup_t rtypes[] = {
+ { KNOT_RRTYPE_A, "has IPv4 address" },
+ { KNOT_RRTYPE_NS, "nameserver is" },
+ { KNOT_RRTYPE_CNAME, "is an alias for" },
+ { KNOT_RRTYPE_SOA, "start of authority is" },
+ { KNOT_RRTYPE_PTR, "points to" },
+ { KNOT_RRTYPE_MX, "mail is handled by" },
+ { KNOT_RRTYPE_TXT, "description is" },
+ { KNOT_RRTYPE_AAAA, "has IPv6 address" },
+ { KNOT_RRTYPE_LOC, "location is" },
+ { KNOT_RRTYPE_DS, "delegation signature is" },
+ { KNOT_RRTYPE_SSHFP, "SSH fingerprint is" },
+ { KNOT_RRTYPE_RRSIG, "RR set signature is" },
+ { KNOT_RRTYPE_DNSKEY, "DNSSEC key is" },
+ { KNOT_RRTYPE_TLSA, "has TLS certificate" },
+ { 0, NULL }
+};
+
+static void print_header(const knot_pkt_t *packet, const style_t *style)
+{
+ if (packet->size < KNOT_WIRE_OFFSET_QDCOUNT) {
+ return;
+ }
+
+ char flags[64] = "";
+ char unknown_rcode[64] = "";
+ char unknown_opcode[64] = "";
+
+ const char *rcode_str = NULL;
+ const char *opcode_str = NULL;
+
+ uint16_t qdcount = 0, ancount = 0, nscount = 0, arcount = 0;
+
+ uint16_t id = knot_wire_get_id(packet->wire);
+
+ // Get extended RCODE.
+ const char *code_name = knot_pkt_ext_rcode_name(packet);
+ if (code_name[0] != '\0') {
+ rcode_str = code_name;
+ } else {
+ uint16_t code = knot_pkt_ext_rcode(packet);
+ (void)snprintf(unknown_rcode, sizeof(unknown_rcode), "RCODE %d", code);
+ rcode_str = unknown_rcode;
+ }
+
+ // Get OPCODE.
+ uint8_t code = knot_wire_get_opcode(packet->wire);
+ const knot_lookup_t *opcode = knot_lookup_by_id(knot_opcode_names, code);
+ if (opcode != NULL) {
+ opcode_str = opcode->name;
+ } else {
+ (void)snprintf(unknown_opcode, sizeof(unknown_opcode), "OPCODE %d", code);
+ opcode_str = unknown_opcode;
+ }
+
+ // Get flags.
+ size_t flags_rest = sizeof(flags);
+ const size_t flag_len = 4;
+ if (knot_wire_get_qr(packet->wire) != 0 && flags_rest > flag_len) {
+ flags_rest -= strlcat(flags, " qr", flags_rest);
+ }
+ if (knot_wire_get_aa(packet->wire) != 0 && flags_rest > flag_len) {
+ flags_rest -= strlcat(flags, " aa", flags_rest);
+ }
+ if (knot_wire_get_tc(packet->wire) != 0 && flags_rest > flag_len) {
+ flags_rest -= strlcat(flags, " tc", flags_rest);
+ }
+ if (knot_wire_get_rd(packet->wire) != 0 && flags_rest > flag_len) {
+ flags_rest -= strlcat(flags, " rd", flags_rest);
+ }
+ if (knot_wire_get_ra(packet->wire) != 0 && flags_rest > flag_len) {
+ flags_rest -= strlcat(flags, " ra", flags_rest);
+ }
+ if (knot_wire_get_z(packet->wire) != 0 && flags_rest > flag_len) {
+ flags_rest -= strlcat(flags, " z", flags_rest);
+ }
+ if (knot_wire_get_ad(packet->wire) != 0 && flags_rest > flag_len) {
+ flags_rest -= strlcat(flags, " ad", flags_rest);
+ }
+ if (knot_wire_get_cd(packet->wire) != 0 && flags_rest > flag_len) {
+ strlcat(flags, " cd", flags_rest);
+ }
+
+ if (packet->size >= KNOT_WIRE_HEADER_SIZE) {
+ qdcount = knot_wire_get_qdcount(packet->wire);
+ ancount = knot_wire_get_ancount(packet->wire);
+ nscount = knot_wire_get_nscount(packet->wire);
+ arcount = knot_wire_get_arcount(packet->wire);
+
+ if (knot_pkt_has_tsig(packet)) {
+ arcount++;
+ }
+ }
+
+ // Print formatted info.
+ switch (style->format) {
+ case FORMAT_NSUPDATE:
+ printf(";; ->>HEADER<<- opcode: %s; status: %s; id: %u\n"
+ ";; Flags:%1s; "
+ "ZONE: %u; PREREQ: %u; UPDATE: %u; ADDITIONAL: %u\n",
+ opcode_str, rcode_str, id, flags, qdcount, ancount,
+ nscount, arcount);
+ break;
+ default:
+ printf(";; ->>HEADER<<- opcode: %s; status: %s; id: %u\n"
+ ";; Flags:%1s; "
+ "QUERY: %u; ANSWER: %u; AUTHORITY: %u; ADDITIONAL: %u\n",
+ opcode_str, rcode_str, id, flags, qdcount, ancount,
+ nscount, arcount);
+ break;
+ }
+}
+
+static void print_footer(const size_t total_len,
+ const size_t msg_count,
+ const size_t rr_count,
+ const net_t *net,
+ const float elapsed,
+ time_t exec_time,
+ const bool incoming)
+{
+ struct tm tm;
+ char date[64];
+
+ // Get current timestamp.
+ if (exec_time == 0) {
+ exec_time = time(NULL);
+ }
+
+ // Create formatted date-time string.
+ localtime_r(&exec_time, &tm);
+ strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S %Z", &tm);
+
+ // Print messages statistics.
+ if (incoming) {
+ printf(";; Received %zu B", total_len);
+ } else {
+ printf(";; Sent %zu B", total_len);
+ }
+
+ // If multimessage (XFR) print additional statistics.
+ if (msg_count > 0) {
+ printf(" (%zu messages, %zu records)\n", msg_count, rr_count);
+ } else {
+ printf("\n");
+ }
+ // Print date.
+ printf(";; Time %s\n", date);
+
+ // Print connection statistics.
+ if (net != NULL) {
+ if (incoming) {
+ printf(";; From %s", net->remote_str);
+ } else {
+ printf(";; To %s", net->remote_str);
+ }
+
+ if (elapsed >= 0) {
+ printf(" in %.1f ms\n", elapsed);
+ } else {
+ printf("\n");
+ }
+ }
+}
+
+static void print_hex(const uint8_t *data, uint16_t len)
+{
+ for (int i = 0; i < len; i++) {
+ printf("%02X", data[i]);
+ }
+}
+
+static void print_nsid(const uint8_t *data, uint16_t len)
+{
+ if (len == 0) {
+ return;
+ }
+
+ print_hex(data, len);
+
+ // Check if printable string.
+ for (int i = 0; i < len; i++) {
+ if (!is_print(data[i])) {
+ return;
+ }
+ }
+ printf(" \"%.*s\"", len, data);
+}
+
+static bool print_text(const uint8_t *data, uint16_t len)
+{
+ if (len == 0) {
+ return false;
+ }
+
+ // Check if printable string.
+ for (int i = 0; i < len; i++) {
+ if (!is_print(data[i])) {
+ return false;
+ }
+ }
+ printf("%.*s", len, data);
+ return true;
+}
+
+static void print_edns_client_subnet(const uint8_t *data, uint16_t len)
+{
+ knot_edns_client_subnet_t ecs = { 0 };
+ int ret = knot_edns_client_subnet_parse(&ecs, data, len);
+ if (ret != KNOT_EOK) {
+ return;
+ }
+
+ struct sockaddr_storage addr = { 0 };
+ ret = knot_edns_client_subnet_get_addr(&addr, &ecs);
+ assert(ret == KNOT_EOK);
+
+ char addr_str[SOCKADDR_STRLEN] = { 0 };
+ sockaddr_tostr(addr_str, sizeof(addr_str), &addr);
+
+ printf("%s/%u/%u", addr_str, ecs.source_len, ecs.scope_len);
+}
+
+static void print_ede(const uint8_t *data, uint16_t len)
+{
+ if (len < 2) {
+ printf("(malformed)");
+ return;
+ }
+
+
+ uint16_t errcode;
+ memcpy(&errcode, data, sizeof(errcode));
+ errcode = be16toh(errcode);
+
+ const knot_lookup_t *item = knot_lookup_by_id(knot_edns_ede_names, errcode);
+ const char *strerr = (item != NULL) ? item->name : "Unknown code";
+
+ if (len > 2) {
+ printf("%hu (%s): '%.*s'", errcode, strerr, (int)(len - 2), data + 2);
+ } else {
+ printf("%hu (%s)", errcode, strerr);
+ }
+}
+
+static void print_expire(const uint8_t *data, uint16_t len)
+{
+ if (len == 0) {
+ printf("(empty)");
+ } else if (len != sizeof(uint32_t)) {
+ printf("(malformed)");
+ } else {
+ char str[80] = "";
+ uint32_t timer = knot_wire_read_u32(data);
+ if (knot_time_print_human(timer, str, sizeof(str), false) > 0) {
+ printf("%u (%s)", timer, str);
+ } else {
+ printf("%u", timer);
+ }
+ }
+}
+
+static void print_section_opt(const knot_pkt_t *packet, const style_t *style)
+{
+ if (style->present_edns) {
+ size_t buflen = 8192;
+ char *buf = calloc(buflen, 1);
+ int ret = knot_rrset_txt_dump_edns(packet->opt_rr,
+ knot_wire_get_rcode(packet->wire),
+ buf, buflen, &style->style);
+ if (ret < 0) {
+ WARN("can't print OPT record (%s)", knot_strerror(ret));
+ } else {
+ printf(". 0 ANY EDNS\t\t\t%s\n", buf);
+ }
+ free(buf);
+ return;
+ }
+
+ char unknown_ercode[64] = "";
+ const char *ercode_str = NULL;
+
+ uint16_t ercode = knot_edns_get_ext_rcode(packet->opt_rr);
+ if (ercode > 0) {
+ ercode = knot_edns_whole_rcode(ercode,
+ knot_wire_get_rcode(packet->wire));
+ }
+
+ const knot_lookup_t *item = knot_lookup_by_id(knot_rcode_names, ercode);
+ if (item != NULL) {
+ ercode_str = item->name;
+ } else {
+ (void)snprintf(unknown_ercode, sizeof(unknown_ercode), "RCODE %d", ercode);
+ ercode_str = unknown_ercode;
+ }
+
+ printf(";; Version: %u; flags: %s; UDP size: %u B; ext-rcode: %s\n",
+ knot_edns_get_version(packet->opt_rr),
+ (knot_edns_do(packet->opt_rr) != 0) ? "do" : "",
+ knot_edns_get_payload(packet->opt_rr),
+ ercode_str);
+
+ assert(packet->opt_rr->rrs.count > 0);
+ knot_rdata_t *rdata = packet->opt_rr->rrs.rdata;
+ wire_ctx_t wire = wire_ctx_init_const(rdata->data, rdata->len);
+
+ while (wire_ctx_available(&wire) >= KNOT_EDNS_OPTION_HDRLEN) {
+ uint16_t opt_code = wire_ctx_read_u16(&wire);
+ uint16_t opt_len = wire_ctx_read_u16(&wire);
+ uint8_t *opt_data = wire.position;
+
+ if (wire.error != KNOT_EOK) {
+ WARN("invalid OPT record data");
+ return;
+ }
+
+ switch (opt_code) {
+ case KNOT_EDNS_OPTION_NSID:
+ printf(";; NSID: ");
+ print_nsid(opt_data, opt_len);
+ break;
+ case KNOT_EDNS_OPTION_CLIENT_SUBNET:
+ printf(";; CLIENT-SUBNET: ");
+ print_edns_client_subnet(opt_data, opt_len);
+ break;
+ case KNOT_EDNS_OPTION_PADDING:
+ printf(";; PADDING: %u B", opt_len);
+ break;
+ case KNOT_EDNS_OPTION_COOKIE:
+ printf(";; COOKIE: ");
+ print_hex(opt_data, opt_len);
+ break;
+ case KNOT_EDNS_OPTION_EDE:
+ printf(";; EDE: ");
+ print_ede(opt_data, opt_len);
+ break;
+ case KNOT_EDNS_OPTION_EXPIRE:
+ printf(";; EXPIRE: ");
+ print_expire(opt_data, opt_len);
+ break;
+ default:
+ printf(";; Option (%u): ", opt_code);
+ if (style->show_edns_opt_text) {
+ if (!print_text(opt_data, opt_len)) {
+ print_hex(opt_data, opt_len);
+ }
+ } else {
+ print_hex(opt_data, opt_len);
+ }
+ }
+ printf("\n");
+
+ wire_ctx_skip(&wire, opt_len);
+ }
+
+ if (wire_ctx_available(&wire) > 0) {
+ WARN("invalid OPT record data");
+ }
+}
+
+static void print_section_question(const knot_dname_t *owner,
+ const uint16_t qclass,
+ const uint16_t qtype,
+ const style_t *style)
+{
+ size_t buflen = 8192;
+ char *buf = calloc(buflen, 1);
+
+ // Don't print zero TTL.
+ knot_dump_style_t qstyle = style->style;
+ qstyle.empty_ttl = true;
+
+ knot_rrset_t *question = knot_rrset_new(owner, qtype, qclass, 0, NULL);
+
+ if (knot_rrset_txt_dump_header(question, 0, buf, buflen, &qstyle) < 0) {
+ WARN("can't print whole question section");
+ }
+
+ printf("%s\n", buf);
+
+ knot_rrset_free(question, NULL);
+ free(buf);
+}
+
+static void print_section_full(const knot_rrset_t *rrsets,
+ const uint16_t count,
+ const style_t *style,
+ const bool no_tsig)
+{
+ size_t buflen = 8192;
+ char *buf = calloc(buflen, 1);
+
+ for (size_t i = 0; i < count; i++) {
+ // Ignore OPT records.
+ if (rrsets[i].type == KNOT_RRTYPE_OPT) {
+ continue;
+ }
+
+ // Exclude TSIG record.
+ if (no_tsig && rrsets[i].type == KNOT_RRTYPE_TSIG) {
+ continue;
+ }
+
+ if (knot_rrset_txt_dump(&rrsets[i], &buf, &buflen,
+ &(style->style)) < 0) {
+ WARN("can't print whole section");
+ break;
+ }
+ printf("%s", buf);
+ }
+
+ free(buf);
+}
+
+static void print_section_dig(const knot_rrset_t *rrsets,
+ const uint16_t count,
+ const style_t *style)
+{
+ size_t buflen = 8192;
+ char *buf = calloc(buflen, 1);
+
+ for (size_t i = 0; i < count; i++) {
+ const knot_rrset_t *rrset = &rrsets[i];
+ uint16_t rrset_rdata_count = rrset->rrs.count;
+ for (uint16_t j = 0; j < rrset_rdata_count; j++) {
+ while (knot_rrset_txt_dump_data(rrset, j, buf, buflen,
+ &(style->style)) < 0) {
+ buflen += 4096;
+ // Oversize protection.
+ if (buflen > 100000) {
+ WARN("can't print whole section");
+ break;
+ }
+
+ char *newbuf = realloc(buf, buflen);
+ if (newbuf == NULL) {
+ WARN("can't print whole section");
+ break;
+ }
+ buf = newbuf;
+ }
+ printf("%s\n", buf);
+ }
+ }
+
+ free(buf);
+}
+
+static void print_section_host(const knot_rrset_t *rrsets,
+ const uint16_t count,
+ const style_t *style)
+{
+ size_t buflen = 8192;
+ char *buf = calloc(buflen, 1);
+
+ for (size_t i = 0; i < count; i++) {
+ const knot_rrset_t *rrset = &rrsets[i];
+ const knot_lookup_t *descr;
+ char type[32] = "NULL";
+ char *owner;
+
+ owner = knot_dname_to_str_alloc(rrset->owner);
+ if (style->style.ascii_to_idn != NULL) {
+ style->style.ascii_to_idn(&owner);
+ }
+ descr = knot_lookup_by_id(rtypes, rrset->type);
+
+ uint16_t rrset_rdata_count = rrset->rrs.count;
+ for (uint16_t j = 0; j < rrset_rdata_count; j++) {
+ if (rrset->type == KNOT_RRTYPE_CNAME &&
+ style->hide_cname) {
+ continue;
+ }
+
+ while (knot_rrset_txt_dump_data(rrset, j, buf, buflen,
+ &(style->style)) < 0) {
+ buflen += 4096;
+ // Oversize protection.
+ if (buflen > 100000) {
+ WARN("can't print whole section");
+ break;
+ }
+
+ char *newbuf = realloc(buf, buflen);
+ if (newbuf == NULL) {
+ WARN("can't print whole section");
+ break;
+ }
+ buf = newbuf;
+ }
+
+ if (descr != NULL) {
+ printf("%s %s %s\n", owner, descr->name, buf);
+ } else {
+ knot_rrtype_to_string(rrset->type, type, sizeof(type));
+ printf("%s has %s record %s\n", owner, type, buf);
+ }
+ }
+
+ free(owner);
+ }
+
+ free(buf);
+}
+
+static void print_error_host(const knot_pkt_t *packet, const style_t *style)
+{
+ char type[32] = "Unknown";
+ const char *rcode_str = "Unknown";
+
+ knot_rrtype_to_string(knot_pkt_qtype(packet), type, sizeof(type));
+
+ // Get extended RCODE.
+ const char *code_name = knot_pkt_ext_rcode_name(packet);
+ if (code_name[0] != '\0') {
+ rcode_str = code_name;
+ }
+
+ // Get record owner.
+ char *owner = knot_dname_to_str_alloc(knot_pkt_qname(packet));
+ if (style->style.ascii_to_idn != NULL) {
+ style->style.ascii_to_idn(&owner);
+ }
+
+ if (knot_pkt_ext_rcode(packet) == KNOT_RCODE_NOERROR) {
+ printf("Host %s has no %s record\n", owner, type);
+ } else {
+ printf("Host %s type %s error: %s\n", owner, type, rcode_str);
+ }
+
+ free(owner);
+}
+
+static void json_dname(jsonw_t *w, const char *key, const knot_dname_t *dname)
+{
+ knot_dname_txt_storage_t name;
+ if (knot_dname_to_str(name, dname, sizeof(name)) != NULL) {
+ jsonw_str(w, key, name);
+ }
+}
+
+static void json_rdata(jsonw_t *w, const knot_rrset_t *rrset)
+{
+ char type[16];
+ if (knot_rrtype_to_string(rrset->type, type, sizeof(type)) <= 0 ||
+ strncmp(type, "TYPE", 4) == 0) { // Unknown/hex format.
+ return;
+ }
+
+ char key[32] = "rdata";
+ strlcat(key, type, sizeof(key));
+
+ char data[16384];
+ const knot_dump_style_t *style = &KNOT_DUMP_STYLE_DEFAULT;
+ if (knot_rrset_txt_dump_data(rrset, 0, data, sizeof(data), style) > 0) {
+ jsonw_str(w, key, data);
+ }
+}
+
+static void json_print_section(jsonw_t *w, const char *name,
+ const knot_pktsection_t *section)
+{
+ if (section->count == 0 ||
+ (section->count == 1 && knot_pkt_rr(section, 0)->type == KNOT_RRTYPE_OPT)) {
+ return;
+ }
+
+ char str[16];
+
+ jsonw_list(w, name);
+
+ bool first_opt = true;
+ for (int i = 0; i < section->count; i++) {
+ const knot_rrset_t *rr = knot_pkt_rr(section, i);
+ if (rr->type == KNOT_RRTYPE_OPT && first_opt) {
+ first_opt = false;
+ continue;
+ }
+ jsonw_object(w, NULL);
+ json_dname(w, "NAME", rr->owner);
+ jsonw_int(w, "TYPE", rr->type);
+ if (knot_rrtype_to_string(rr->type, str, sizeof(str)) > 0) {
+ jsonw_str(w, "TYPEname", str);
+ }
+ jsonw_int(w, "CLASS", rr->rclass);
+ if (rr->type != KNOT_RRTYPE_OPT && // OPT class meaning is different.
+ knot_rrclass_to_string(rr->rclass, str, sizeof(str)) > 0) {
+ jsonw_str(w, "CLASSname", str);
+ }
+ jsonw_int(w, "TTL", rr->ttl);
+ if (rr->type != KNOT_RRTYPE_OPT) { // OPT with HEX rdata.
+ json_rdata(w, rr);
+ }
+ jsonw_int(w, "RDLENGTH", rr->rrs.rdata->len);
+ if (rr->rrs.rdata->len > 0 ) {
+ jsonw_hex(w, "RDATAHEX", rr->rrs.rdata->data, rr->rrs.rdata->len);
+ }
+ jsonw_end(w);
+ }
+
+ jsonw_end(w);
+}
+
+static void json_print_edns_generic(jsonw_t *w, const knot_rrset_t *rr)
+{
+ jsonw_object(w, "EDNS");
+ json_dname(w, "NAME", rr->owner);
+ jsonw_int(w, "CLASS", rr->rclass);
+ jsonw_int(w, "TTL", rr->ttl);
+ if (rr->rrs.count > 0) {
+ jsonw_int(w, "RDLENGTH", rr->rrs.rdata->len);
+ jsonw_hex(w, "RDATAHEX", rr->rrs.rdata->data, rr->rrs.rdata->len);
+ }
+ jsonw_end(w);
+}
+
+static void json_edns_unknown(jsonw_t *w, uint8_t *optdata, uint16_t optype, uint16_t optlen)
+{
+ char name[9] = { 0 };
+ (void)snprintf(name, sizeof(name), "OPT%hu", optype);
+ jsonw_hex(w, name, optdata, optlen);
+}
+
+static bool all_zero(const uint8_t * const str, const size_t len)
+{
+ for (const uint8_t *p = str; p != str + len; p++) {
+ if (*p != 0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool all_print(const uint8_t * const str, const size_t len)
+{
+ for (const uint8_t *p = str; p != str + len; p++) {
+ if (!is_print(*p)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static void json_edns_ecs(jsonw_t *w, uint8_t *optdata, uint16_t optlen,
+ char *tmps, size_t tmps_size)
+{
+ knot_edns_client_subnet_t ecs = { 0 };
+ struct sockaddr_storage addr = { 0 };
+
+ int ret = knot_edns_client_subnet_parse(&ecs, optdata, optlen);
+ if (ret == KNOT_EOK) {
+ ret = knot_edns_client_subnet_get_addr(&addr, &ecs);
+ }
+ if (ret == KNOT_EOK) {
+ ret = sockaddr_tostr(tmps, tmps_size, &addr);
+ assert(ret > 0);
+
+ (void)snprintf(tmps + ret, tmps_size - ret,
+ "/%d/%d", ecs.source_len, ecs.scope_len);
+
+ jsonw_str(w, "ECS", tmps);
+ } else {
+ jsonw_hex(w, "ECS", optdata, optlen);
+ }
+}
+
+static void json_edns_opt(jsonw_t *w, uint8_t *optdata, uint16_t optype, uint16_t optlen)
+{
+ char tmps[KNOT_DNAME_TXT_MAXLEN] = { 0 };
+ uint32_t tmpu = 0;
+ uint16_t tmphu = 0;
+
+ switch (optype) {
+ case KNOT_EDNS_OPTION_NSID:
+ jsonw_object(w, "NSID");
+ jsonw_hex(w, "HEX", optdata, optlen);
+ if (all_print(optdata, optlen)) {
+ jsonw_str_len(w, "TEXT", optdata, optlen, true);
+ }
+ jsonw_end(w);
+ break;
+ case KNOT_EDNS_OPTION_CLIENT_SUBNET:
+ json_edns_ecs(w, optdata, optlen, tmps, sizeof(tmps));
+ break;
+ case KNOT_EDNS_OPTION_EXPIRE:
+ if (optlen == 0) {
+ jsonw_str(w, "EXPIRE", "NONE");
+ } else if (optlen == sizeof(tmpu)) {
+ tmpu = knot_wire_read_u32(optdata);
+ (void)snprintf(tmps, sizeof(tmps), "%u", tmpu);
+ jsonw_str(w, "EXPIRE", tmps);
+ } else {
+ json_edns_unknown(w, optdata, optype, optlen);
+ }
+ break;
+ case KNOT_EDNS_OPTION_COOKIE:
+ jsonw_list(w, "COOKIE");
+ tmphu = MIN(optlen, KNOT_EDNS_COOKIE_CLNT_SIZE);
+ jsonw_hex(w, NULL, optdata, tmphu);
+ if (optlen > tmphu) {
+ jsonw_hex(w, NULL, optdata + tmphu, optlen - tmphu);
+ }
+ jsonw_end(w);
+ break;
+ case KNOT_EDNS_OPTION_TCP_KEEPALIVE:
+ if (optlen == sizeof(tmphu)) {
+ tmphu = knot_wire_read_u16(optdata);
+ jsonw_int(w, "KEEPALIVE", tmphu);
+ } else {
+ json_edns_unknown(w, optdata, optype, optlen);
+ }
+ break;
+ case KNOT_EDNS_OPTION_PADDING:
+ jsonw_object(w, "PADDING");
+ jsonw_int(w, "LENGTH", optlen);
+ if (!all_zero(optdata, optlen)) {
+ jsonw_hex(w, "HEX", optdata, optlen);
+ }
+ jsonw_end(w);
+ break;
+ case KNOT_EDNS_OPTION_CHAIN:
+ if (knot_dname_wire_check(optdata, optdata + optlen, NULL) > 0 &&
+ knot_dname_to_str(tmps, optdata, sizeof(tmps)) != NULL) {
+ jsonw_str(w, "CHAIN", tmps);
+ } else {
+ json_edns_unknown(w, optdata, optype, optlen);
+ }
+ break;
+ case KNOT_EDNS_OPTION_EDE:
+ if (optlen < sizeof(uint16_t)) {
+ json_edns_unknown(w, optdata, optype, optlen);
+ } else {
+ tmphu = knot_wire_read_u16(optdata);
+ jsonw_object(w, "EDE");
+ jsonw_int(w, "CODE", tmphu);
+ const knot_lookup_t *item = knot_lookup_by_id(knot_edns_ede_names, tmphu);
+ if (item != NULL) {
+ jsonw_str(w, "Purpose", item->name);
+ }
+ if (optlen > 2) {
+ jsonw_str_len(w, "TEXT", optdata + 2, optlen - 2, true);
+ }
+ jsonw_end(w);
+ }
+ break;
+ default:
+ json_edns_unknown(w, optdata, optype, optlen);
+ break;
+ }
+}
+
+static void json_print_edns(jsonw_t *w, const knot_pkt_t *pkt)
+{
+ assert(pkt != NULL && pkt->opt_rr != NULL);
+
+ if (pkt->opt_rr->owner[0] != '\0' || pkt->opt_rr->rrs.count != 1) {
+ json_print_edns_generic(w, pkt->opt_rr);
+ return;
+ }
+
+ char tmp[11] = { 0 };
+
+ jsonw_object(w, "EDNS");
+ uint16_t version = (pkt->opt_rr->ttl & 0x00ff0000) >> 16;
+ uint16_t flags = pkt->opt_rr->ttl & 0xffff, mask = (1 << 15);
+ jsonw_int(w, "Version", version);
+ jsonw_list(w, "FLAGS");
+ for (int i = 0; i < 16; i++) {
+ if ((flags & mask)) {
+ if ((mask & KNOT_EDNS_DO_MASK)) {
+ jsonw_str(w, NULL, "DO");
+ } else {
+ (void)snprintf(tmp, sizeof(tmp), "BIT%d", i);
+ jsonw_str(w, NULL, tmp);
+ }
+ }
+ mask >>= 1;
+ }
+ jsonw_end(w);
+
+ const knot_lookup_t *item = knot_lookup_by_id(knot_rcode_names, knot_pkt_ext_rcode(pkt));
+ (void)snprintf(tmp, sizeof(tmp), "RCODE%hu", knot_pkt_ext_rcode(pkt));
+ jsonw_str(w, "RCODE", item == NULL ? tmp : item->name);
+ jsonw_int(w, "UDPSIZE", knot_edns_get_payload(pkt->opt_rr));
+
+ assert(pkt->opt_rr->rrs.count == 1);
+ wire_ctx_t opts = wire_ctx_init(pkt->opt_rr->rrs.rdata->data, pkt->opt_rr->rrs.rdata->len);
+ while (wire_ctx_available(&opts) > 0 && opts.error == KNOT_EOK) {
+ uint16_t optype = wire_ctx_read_u16(&opts);
+ uint16_t optlen = wire_ctx_read_u16(&opts);
+ if (wire_ctx_can_read(&opts, optlen) == KNOT_EOK) {
+ json_edns_opt(w, opts.position, optype, optlen);
+ wire_ctx_skip(&opts, optlen);
+ }
+ }
+ jsonw_end(w);
+}
+
+static void print_packet_json(jsonw_t *w, const knot_pkt_t *pkt, time_t time)
+{
+ if (pkt == NULL) {
+ return;
+ }
+
+ char str[16];
+
+ struct tm tm;
+ char date[64];
+ localtime_r(&time, &tm);
+ strftime(date, sizeof(date), "%Y-%m-%dT%H:%M:%S%z", &tm);
+ jsonw_str(w, "dateString", date);
+ jsonw_ulong(w, "dateSeconds", time);
+
+ jsonw_int(w, "msgLength", pkt->size);
+
+ if (pkt->parsed >= KNOT_WIRE_HEADER_SIZE) {
+ jsonw_int(w, "ID", knot_wire_get_id(pkt->wire));
+ jsonw_int(w, "QR", (bool)knot_wire_get_qr(pkt->wire));
+ jsonw_int(w, "Opcode", knot_wire_get_opcode(pkt->wire));
+ jsonw_int(w, "AA", (bool)knot_wire_get_aa(pkt->wire));
+ jsonw_int(w, "TC", (bool)knot_wire_get_tc(pkt->wire));
+ jsonw_int(w, "RD", (bool)knot_wire_get_rd(pkt->wire));
+ jsonw_int(w, "RA", (bool)knot_wire_get_ra(pkt->wire));
+ jsonw_int(w, "AD", (bool)knot_wire_get_ad(pkt->wire));
+ jsonw_int(w, "CD", (bool)knot_wire_get_cd(pkt->wire));
+ jsonw_int(w, "RCODE", knot_wire_get_rcode(pkt->wire));
+ jsonw_int(w, "QDCOUNT", knot_wire_get_qdcount(pkt->wire));
+ jsonw_int(w, "ANCOUNT", knot_wire_get_ancount(pkt->wire));
+ jsonw_int(w, "NSCOUNT", knot_wire_get_nscount(pkt->wire));
+ jsonw_int(w, "ARCOUNT", knot_wire_get_arcount(pkt->wire));
+ }
+ if (knot_wire_get_qdcount(pkt->wire) == 1) {
+ json_dname(w, "QNAME", knot_pkt_qname(pkt));
+ jsonw_int(w, "QTYPE", knot_pkt_qtype(pkt));
+ if (knot_rrtype_to_string(knot_pkt_qtype(pkt), str, sizeof(str)) > 0) {
+ jsonw_str(w, "QTYPEname", str);
+ }
+ jsonw_int(w, "QCLASS", knot_pkt_qclass(pkt));
+ if (knot_rrclass_to_string(knot_pkt_qclass(pkt), str, sizeof(str)) > 0) {
+ jsonw_str(w, "QCLASSname", str);
+ }
+ }
+ if (pkt->rrset_count) {
+ json_print_section(w, "answerRRs", knot_pkt_section(pkt, KNOT_ANSWER));
+ json_print_section(w, "authorityRRs", knot_pkt_section(pkt, KNOT_AUTHORITY));
+ json_print_section(w, "additionalRRs", knot_pkt_section(pkt, KNOT_ADDITIONAL));
+ }
+ if (knot_pkt_has_edns(pkt)) {
+ json_print_edns(w, pkt);
+ }
+ if (pkt->parsed < pkt->size) {
+ jsonw_hex(w, "messageOctetsHEX", pkt->wire, pkt->size);
+ }
+}
+
+knot_pkt_t *create_empty_packet(const uint16_t max_size)
+{
+ // Create packet skeleton.
+ knot_pkt_t *packet = knot_pkt_new(NULL, max_size, NULL);
+ if (packet == NULL) {
+ DBG_NULL;
+ return NULL;
+ }
+
+ // Set random sequence id.
+ knot_wire_set_id(packet->wire, dnssec_random_uint16_t());
+
+ return packet;
+}
+
+jsonw_t *print_header_xfr_json(const knot_pkt_t *query,
+ const time_t exec_time,
+ const style_t *style)
+{
+ if (style == NULL) {
+ DBG_NULL;
+ return NULL;
+ }
+
+ jsonw_t *w = jsonw_new(stdout, JSON_INDENT);
+ if (w == NULL) {
+ return NULL;
+ }
+
+ if (style->show_query) {
+ jsonw_object(w, NULL);
+ jsonw_object(w, "queryMessage");
+ print_packet_json(w, query, exec_time);
+ jsonw_end(w);
+ jsonw_list(w, "responseMessage");
+ } else {
+ jsonw_list(w, NULL);
+ }
+
+ return w;
+}
+
+void print_data_xfr_json(jsonw_t *w,
+ const knot_pkt_t *reply,
+ const time_t exec_time)
+{
+ if (w == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ jsonw_object(w, NULL);
+ print_packet_json(w, reply, exec_time);
+ jsonw_end(w);
+}
+
+void print_footer_xfr_json(jsonw_t **w,
+ const style_t *style)
+{
+ if (w == NULL || style == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ jsonw_end(*w); // list (responseMessage)
+ if (style->show_query) {
+ jsonw_end(*w); // object
+ }
+
+ jsonw_free(w);
+ *w = NULL;
+}
+
+void print_header_xfr(const knot_pkt_t *packet, const style_t *style)
+{
+ if (style == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ char xfr[16] = "AXFR";
+
+ switch (knot_pkt_qtype(packet)) {
+ case KNOT_RRTYPE_AXFR:
+ break;
+ case KNOT_RRTYPE_IXFR:
+ xfr[0] = 'I';
+ break;
+ default:
+ return;
+ }
+
+ if (style->show_header) {
+ char *owner = knot_dname_to_str_alloc(knot_pkt_qname(packet));
+ if (style->style.ascii_to_idn != NULL) {
+ style->style.ascii_to_idn(&owner);
+ }
+ if (owner != NULL) {
+ printf(";; %s for %s\n", xfr, owner);
+ free(owner);
+ }
+ }
+}
+
+void print_data_xfr(const knot_pkt_t *packet,
+ const style_t *style)
+{
+ if (packet == NULL || style == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ const knot_pktsection_t *answers = knot_pkt_section(packet, KNOT_ANSWER);
+
+ switch (style->format) {
+ case FORMAT_DIG:
+ print_section_dig(knot_pkt_rr(answers, 0), answers->count, style);
+ break;
+ case FORMAT_HOST:
+ print_section_host(knot_pkt_rr(answers, 0), answers->count, style);
+ break;
+ case FORMAT_FULL:
+ print_section_full(knot_pkt_rr(answers, 0), answers->count, style, true);
+
+ // Print TSIG record.
+ if (style->show_tsig && knot_pkt_has_tsig(packet)) {
+ print_section_full(packet->tsig_rr, 1, style, false);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void print_footer_xfr(const size_t total_len,
+ const size_t msg_count,
+ const size_t rr_count,
+ const net_t *net,
+ const float elapsed,
+ const time_t exec_time,
+ const style_t *style)
+{
+ if (style == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ if (style->show_footer) {
+ print_footer(total_len, msg_count, rr_count, net, elapsed,
+ exec_time, true);
+ }
+}
+
+void print_packets_json(const knot_pkt_t *query,
+ const knot_pkt_t *reply,
+ const net_t *net,
+ const time_t exec_time,
+ const style_t *style)
+{
+ if (style == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ jsonw_t *w = jsonw_new(stdout, JSON_INDENT);
+ if (w == NULL) {
+ return;
+ }
+ jsonw_object(w, NULL);
+
+ if (style->show_query) {
+ jsonw_object(w, "queryMessage");
+ print_packet_json(w, query, exec_time);
+ jsonw_end(w);
+ jsonw_object(w, "responseMessage");
+ }
+
+ print_packet_json(w, reply, exec_time);
+
+ if (style->show_query) {
+ jsonw_end(w);
+ }
+
+ jsonw_end(w);
+ jsonw_free(&w);
+}
+
+void print_packet(const knot_pkt_t *packet,
+ const net_t *net,
+ const size_t size,
+ const float elapsed,
+ const time_t exec_time,
+ const bool incoming,
+ const style_t *style)
+{
+ if (packet == NULL || style == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ const knot_pktsection_t *answers = knot_pkt_section(packet,
+ KNOT_ANSWER);
+ const knot_pktsection_t *authority = knot_pkt_section(packet,
+ KNOT_AUTHORITY);
+ const knot_pktsection_t *additional = knot_pkt_section(packet,
+ KNOT_ADDITIONAL);
+
+ uint16_t qdcount = packet->parsed >= KNOT_WIRE_OFFSET_ANCOUNT ?
+ knot_wire_get_qdcount(packet->wire) : 0;
+ uint16_t ancount = packet->sections[KNOT_ANSWER].count;
+ uint16_t nscount = packet->sections[KNOT_AUTHORITY].count;
+ uint16_t arcount = packet->sections[KNOT_ADDITIONAL].count;
+
+ // Disable additionals printing if there are no other records.
+ // OPT record may be placed anywhere within additionals!
+ if (knot_pkt_has_edns(packet) && arcount == 1) {
+ arcount = 0;
+ }
+
+ // Print packet information header.
+ if (style->show_header) {
+ if (net != NULL) {
+#ifdef ENABLE_QUIC
+ if (net->quic.params.enable) {
+ print_quic(&net->quic);
+ } else
+#endif
+ {
+ print_tls(&net->tls);
+#ifdef LIBNGHTTP2
+ print_https(&net->https);
+#endif
+ }
+ }
+ print_header(packet, style);
+ }
+
+ // Print EDNS section.
+ if (style->show_edns && knot_pkt_has_edns(packet)) {
+ printf("%s", style->show_section ? "\n;; EDNS PSEUDOSECTION:\n" : ";;");
+ print_section_opt(packet, style);
+ }
+
+ // Print DNS sections.
+ switch (style->format) {
+ case FORMAT_DIG:
+ if (ancount > 0) {
+ print_section_dig(knot_pkt_rr(answers, 0), ancount, style);
+ }
+ break;
+ case FORMAT_HOST:
+ if (ancount > 0) {
+ print_section_host(knot_pkt_rr(answers, 0), ancount, style);
+ } else {
+ print_error_host(packet, style);
+ }
+ break;
+ case FORMAT_NSUPDATE:
+ if (style->show_question && qdcount > 0) {
+ printf("%s", style->show_section ? "\n;; ZONE SECTION:\n;; " : ";;");
+ print_section_question(knot_pkt_qname(packet),
+ knot_pkt_qclass(packet),
+ knot_pkt_qtype(packet),
+ style);
+ }
+
+ if (style->show_answer && ancount > 0) {
+ printf("%s", style->show_section ? "\n;; PREREQUISITE SECTION:\n" : "");
+ print_section_full(knot_pkt_rr(answers, 0), ancount, style, true);
+ }
+
+ if (style->show_authority && nscount > 0) {
+ printf("%s", style->show_section ? "\n;; UPDATE SECTION:\n" : "");
+ print_section_full(knot_pkt_rr(authority, 0), nscount, style, true);
+ }
+
+ if (style->show_additional && arcount > 0) {
+ printf("%s", style->show_section ? "\n;; ADDITIONAL DATA:\n" : "");
+ print_section_full(knot_pkt_rr(additional, 0), arcount, style, true);
+ }
+ break;
+ case FORMAT_FULL:
+ if (style->show_question && qdcount > 0) {
+ printf("%s", style->show_section ? "\n;; QUESTION SECTION:\n;; " : ";;");
+ print_section_question(knot_pkt_wire_qname(packet),
+ knot_pkt_qclass(packet),
+ knot_pkt_qtype(packet),
+ style);
+ }
+
+ if (style->show_answer && ancount > 0) {
+ printf("%s", style->show_section ? "\n;; ANSWER SECTION:\n" : "");
+ print_section_full(knot_pkt_rr(answers, 0), ancount, style, true);
+ }
+
+ if (style->show_authority && nscount > 0) {
+ printf("%s", style->show_section ? "\n;; AUTHORITY SECTION:\n" : "");
+ print_section_full(knot_pkt_rr(authority, 0), nscount, style, true);
+ }
+
+ if (style->show_additional && arcount > 0) {
+ printf("%s", style->show_section ? "\n;; ADDITIONAL SECTION:\n" : "");
+ print_section_full(knot_pkt_rr(additional, 0), arcount, style, true);
+ }
+ break;
+ default:
+ break;
+ }
+
+ // Print TSIG section.
+ if (style->show_tsig && knot_pkt_has_tsig(packet)) {
+ printf("%s", style->show_section ? "\n;; TSIG PSEUDOSECTION:\n" : "");
+ print_section_full(packet->tsig_rr, 1, style, false);
+ }
+
+ // Print packet statistics.
+ if (style->show_footer) {
+ printf("\n");
+ print_footer(size, 0, 0, net, elapsed, exec_time, incoming);
+ }
+}
diff --git a/src/utils/common/exec.h b/src/utils/common/exec.h
new file mode 100644
index 0000000..359926c
--- /dev/null
+++ b/src/utils/common/exec.h
@@ -0,0 +1,137 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <time.h>
+
+#include "utils/common/netio.h"
+#include "utils/common/params.h"
+#include "libknot/libknot.h"
+#include "contrib/json.h"
+
+/*!
+ * \brief Allocates empty packet and sets packet size and random id.
+ *
+ * \param max_size Maximal packet size.
+ *
+ * \retval packet if success.
+ * \retval NULL if error.
+ */
+knot_pkt_t *create_empty_packet(const uint16_t max_size);
+
+/*!
+ * \brief Prints information header for transfer.
+ *
+ * \param packet Parsed packet.
+ * \param style Style of the output.
+ */
+void print_header_xfr(const knot_pkt_t *packet, const style_t *style);
+
+/*!
+ * \brief Prints answer section for 1 transfer message.
+ *
+ * \param packet Response packet.
+ * \param style Style of the output.
+ */
+void print_data_xfr(const knot_pkt_t *packet, const style_t *style);
+
+/*!
+ * \brief Prints trailing statistics for transfer.
+ *
+ * \param total_len Total reply size (all messages).
+ * \param msg_count Number of messages.
+ * \param rr_count Total number of answer records.
+ * \param net Connection information.
+ * \param elapsed Total elapsed time.
+ * \param exec_time Time of the packet creation.
+ * \param style Style of the output.
+ */
+void print_footer_xfr(const size_t total_len,
+ const size_t msg_count,
+ const size_t rr_count,
+ const net_t *net,
+ const float elapsed,
+ const time_t exec_time,
+ const style_t *style);
+
+/*!
+ * \brief Prints initial JSON part of XFR output.
+ *
+ * \param query Query packet.
+ * \param exec_time Time of the packet creation.
+ * \param style Style of the output.
+ *
+ * \retval JSON witter if success.
+ * \retval NULL if error.
+ */
+jsonw_t *print_header_xfr_json(const knot_pkt_t *query,
+ const time_t exec_time,
+ const style_t *style);
+
+/*!
+ * \brief Prints one XFR reply packet in JSON.
+ *
+ * \param w JSON writter.
+ * \param reply Reply packet (possibly one of many).
+ * \param exec_time Time of the packet creation.
+ */
+void print_data_xfr_json(jsonw_t *w,
+ const knot_pkt_t *reply,
+ const time_t exec_time);
+
+/*!
+ * \brief Prints trailing JSON part of XFR output.
+ *
+ * \param w JSON writter.
+ * \param style Style of the output.
+ */
+void print_footer_xfr_json(jsonw_t **w,
+ const style_t *style);
+
+/*!
+ * \brief Prints one or query/reply pair of DNS packets in JSON format.
+ *
+ * \param query Query DNS packet.
+ * \param reply Reply DNS packet.
+ * \param net Connection information.
+ * \param exec_time Time of the packet creation.
+ * \param style Style of the output.
+ */
+void print_packets_json(const knot_pkt_t *query,
+ const knot_pkt_t *reply,
+ const net_t *net,
+ const time_t exec_time,
+ const style_t *style);
+
+/*!
+ * \brief Prints one DNS packet.
+ *
+ * \param packet DNS packet.
+ * \param net Connection information.
+ * \param size Original packet wire size.
+ * \param elapsed Total elapsed time.
+ * \param exec_time Time of the packet creation.
+ * \param incoming Indicates if the packet is input.
+ * \param style Style of the output.
+ */
+void print_packet(const knot_pkt_t *packet,
+ const net_t *net,
+ const size_t size,
+ const float elapsed,
+ const time_t exec_time,
+ const bool incoming,
+ const style_t *style);
diff --git a/src/utils/common/hex.c b/src/utils/common/hex.c
new file mode 100644
index 0000000..9683446
--- /dev/null
+++ b/src/utils/common/hex.c
@@ -0,0 +1,82 @@
+/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include "libknot/libknot.h"
+#include "contrib/ctype.h"
+#include "contrib/tolower.h"
+
+/*!
+ * \brief Convert HEX char to byte.
+ * \note Expects valid lowercase letters.
+ */
+static uint8_t hex_to_num(int c)
+{
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ } else {
+ return c - 'a' + 10;
+ }
+}
+
+/*!
+ * \brief Convert string encoded in hex to bytes.
+ */
+int hex_decode(const char *input, uint8_t **output, size_t *output_size)
+{
+ if (!input || input[0] == '\0' || !output || !output_size) {
+ return KNOT_EINVAL;
+ }
+
+ // input validation (length and content)
+
+ size_t input_size = strlen(input);
+ if (input_size % 2 != 0) {
+ return KNOT_EMALF;
+ }
+
+ for (size_t i = 0; i < input_size; i++) {
+ if (!is_xdigit(input[i])) {
+ return KNOT_EMALF;
+ }
+ }
+
+ // output allocation
+
+ size_t result_size = input_size / 2;
+ assert(result_size > 0);
+ uint8_t *result = malloc(result_size);
+ if (!result) {
+ return KNOT_ENOMEM;
+ }
+
+ // conversion
+
+ for (size_t i = 0; i < result_size; i++) {
+ int high_nib = knot_tolower(input[2 * i]);
+ int low_nib = knot_tolower(input[2 * i + 1]);
+
+ result[i] = hex_to_num(high_nib) << 4 | hex_to_num(low_nib);
+ }
+
+ *output = result;
+ *output_size = result_size;
+
+ return KNOT_EOK;
+}
diff --git a/src/utils/common/hex.h b/src/utils/common/hex.h
new file mode 100644
index 0000000..efe81be
--- /dev/null
+++ b/src/utils/common/hex.h
@@ -0,0 +1,31 @@
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdint.h>
+#include <stdlib.h>
+
+/*!
+ * \brief Convert string encoded in hex to bytes.
+ *
+ * \param input Hex encoded input string.
+ * \param output Decoded bytes.
+ * \param output_size Size of the output.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int hex_decode(const char *input, uint8_t **output, size_t *output_size);
diff --git a/src/utils/common/https.c b/src/utils/common/https.c
new file mode 100644
index 0000000..e673b28
--- /dev/null
+++ b/src/utils/common/https.c
@@ -0,0 +1,528 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <arpa/inet.h>
+#include <poll.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "contrib/base64url.h"
+#include "contrib/macros.h"
+#include "contrib/musl/inet_ntop.h"
+#include "contrib/openbsd/strlcat.h"
+#include "contrib/openbsd/strlcpy.h"
+#include "contrib/url-parser/url_parser.h"
+#include "libknot/errcode.h"
+#include "utils/common/https.h"
+#include "utils/common/msg.h"
+
+#define is_read(ctx) (ctx->stream == -1)
+
+int https_params_copy(https_params_t *dst, const https_params_t *src)
+{
+ if (dst == NULL || src == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ dst->enable = src->enable;
+ dst->method = src->method;
+ if (src->path != NULL) {
+ dst->path = strdup(src->path);
+ if (dst->path == NULL) {
+ return KNOT_ENOMEM;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+void https_params_clean(https_params_t *params)
+{
+ if (params == NULL) {
+ return;
+ }
+
+ params->enable = false;
+ params->method = GET;
+ free(params->path);
+ params->path = NULL;
+}
+
+#ifdef LIBNGHTTP2
+
+#define HTTP_STATUS_SUCCESS 200
+#define HTTPS_MAX_STREAMS 16
+#define HTTPS_AUTHORITY_LEN (INET6_ADDRSTRLEN + 2)
+
+#define MAKE_NV(K, KS, V, VS) \
+ { (uint8_t *)K, (uint8_t *)V, KS, VS, NGHTTP2_NV_FLAG_NONE }
+
+#define MAKE_STATIC_NV(K, V) \
+ MAKE_NV(K, sizeof(K) - 1, V, sizeof(V) - 1)
+
+static const char default_path[] = "/dns-query";
+static const char default_query[] = "?dns=";
+
+static const nghttp2_settings_entry settings[] = {
+ { NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, HTTPS_MAX_STREAMS }
+};
+
+const gnutls_datum_t doh_alpn = {
+ .data = (unsigned char *)"h2",
+ .size = 2
+};
+
+static bool https_status_is_redirect(unsigned long status)
+{
+ switch (status) {
+ case 301UL:
+ case 302UL:
+ case 307UL:
+ case 308UL:
+ return true;
+ }
+ return false;
+}
+
+static ssize_t https_send_callback(nghttp2_session *session, const uint8_t *data,
+ size_t length, int flags, void *user_data)
+{
+ assert(user_data);
+
+ gnutls_session_t tls_session = ((https_ctx_t *)user_data)->tls->session;
+ ssize_t len = 0;
+
+ gnutls_record_cork(tls_session);
+ if ((len = gnutls_record_send(tls_session, data, length)) <= 0) {
+ WARN("TLS, failed to send");
+ return KNOT_NET_ESEND;
+ }
+ return len;
+}
+
+static int https_on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame,
+ void *user_data)
+{
+ assert(user_data);
+
+ gnutls_session_t tls_session = ((https_ctx_t *)user_data)->tls->session;
+ while (gnutls_record_check_corked(tls_session) > 0) {
+ int ret = gnutls_record_uncork(tls_session, 0);
+ if (ret < 0 && gnutls_error_is_fatal(ret) != 0) {
+ WARN("TLS, failed to send (%s)", gnutls_strerror(ret));
+ return KNOT_NET_ESEND;
+ }
+ }
+ return KNOT_EOK;
+}
+
+static ssize_t https_recv_callback(nghttp2_session *session, uint8_t *data, size_t length,
+ int flags, void *user_data)
+{
+ assert(user_data);
+
+ https_ctx_t *ctx = (https_ctx_t *)user_data;
+ struct pollfd pfd = {
+ .fd = ctx->tls->sockfd,
+ .events = POLLIN,
+ .revents = 0,
+ };
+
+ ssize_t ret = 0;
+ while ((ret = gnutls_record_recv(ctx->tls->session, data, length)) <= 0) {
+ if (is_read(ctx)) { //Unblock `nghttp2_session_recv(nghttp2_session)`
+ return NGHTTP2_ERR_WOULDBLOCK;
+ }
+ if (ret == 0) {
+ WARN("TLS, peer has closed the connection");
+ return KNOT_NET_ERECV;
+ } else if (gnutls_error_is_fatal(ret)) {
+ WARN("TLS, failed to receive reply (%s)",
+ gnutls_strerror(ret));
+ return KNOT_NET_ERECV;
+ } else if (poll(&pfd, 1, 1000 * ctx->tls->wait) != 1) {
+ WARN("TLS, peer took too long to respond");
+ return KNOT_ETIMEOUT;
+ }
+ }
+
+ return ret;
+}
+
+static int https_on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags, int32_t stream_id,
+ const uint8_t *data, size_t len, void *user_data)
+{
+ assert(user_data);
+
+ https_ctx_t *ctx = (https_ctx_t *)user_data;
+ if (ctx->stream == stream_id) {
+ int cpy_len = MIN(len, ctx->recv_buflen);
+ memcpy(ctx->recv_buf, data, cpy_len);
+ ctx->recv_buf += cpy_len;
+ ctx->recv_buflen -= cpy_len;
+ }
+ return KNOT_EOK;
+}
+
+static int https_on_stream_close_callback(nghttp2_session *session, int32_t stream_id, uint32_t error_code, void *user_data)
+{
+ assert(user_data);
+
+ https_ctx_t *ctx = (https_ctx_t *)user_data;
+ if (ctx->stream == stream_id) {
+ ctx->stream = -1;
+ }
+ return KNOT_EOK;
+}
+
+static int https_on_header_callback(nghttp2_session *session, const nghttp2_frame *frame,
+ const uint8_t *name, size_t namelen,
+ const uint8_t *value, size_t valuelen,
+ uint8_t flags, void *user_data)
+{
+ assert(user_data);
+ https_ctx_t *ctx = (https_ctx_t *)user_data;
+
+ if (!strncasecmp(":status", (const char *)name, namelen)) {
+ char *end;
+ long status;
+ status = strtoul((const char *)value, &end, 10);
+ if (value != (const uint8_t *)end) {
+ ctx->status = status;
+ }
+ }
+ else if (!strncasecmp("location", (const char *)name, namelen) &&
+ https_status_is_redirect(ctx->status)) {
+ struct http_parser_url redirect_url;
+ http_parser_parse_url((const char *)value, valuelen, 0, &redirect_url);
+
+ bool r_auth = redirect_url.field_set & (1 << UF_HOST);
+ bool r_path = redirect_url.field_set & (1 << UF_PATH);
+ char *old_auth = ctx->authority, *old_path = ctx->path;
+
+ if (r_auth) {
+ ctx->authority = strndup((const char *)(value + redirect_url.field_data[UF_HOST].off),
+ redirect_url.field_data[UF_HOST].len);
+ }
+ if (r_path) {
+ ctx->path = strndup((const char *)(value + redirect_url.field_data[UF_PATH].off),
+ redirect_url.field_data[UF_PATH].len);
+ }
+ WARN("HTTP redirect (%s%s)->(%s%s)", old_auth, old_path, ctx->authority, ctx->path);
+ if (r_auth) {
+ free(old_auth);
+ }
+ if (r_path) {
+ free(old_path);
+ }
+ return https_send_dns_query(ctx, ctx->send_buf, ctx->send_buflen);
+ }
+ return KNOT_EOK;
+}
+
+int https_ctx_init(https_ctx_t *ctx, tls_ctx_t *tls_ctx, const https_params_t *params)
+{
+ if (ctx == NULL || tls_ctx == NULL || params == NULL) {
+ return KNOT_EINVAL;
+ }
+ if (ctx->session != NULL) { // Already initialized before
+ return KNOT_EINVAL;
+ }
+ if (!params->enable) {
+ return KNOT_EINVAL;
+ }
+
+ nghttp2_session_callbacks *callbacks;
+ nghttp2_session_callbacks_new(&callbacks);
+ nghttp2_session_callbacks_set_send_callback(callbacks, https_send_callback);
+ nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, https_on_frame_send_callback);
+ nghttp2_session_callbacks_set_recv_callback(callbacks, https_recv_callback);
+ nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, https_on_data_chunk_recv_callback);
+ nghttp2_session_callbacks_set_on_header_callback(callbacks, https_on_header_callback);
+ nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, https_on_stream_close_callback);
+
+ int ret = nghttp2_session_client_new(&(ctx->session), callbacks, ctx);
+ if (ret != 0) {
+ return KNOT_EINVAL;
+ }
+
+ nghttp2_session_callbacks_del(callbacks);
+
+ if (pthread_mutex_init(&ctx->recv_mx, NULL) != 0) {
+ return KNOT_EINVAL;
+ }
+
+ ctx->tls = tls_ctx;
+ ctx->params = *params;
+ ctx->authority = (tls_ctx->params->hostname) ? strdup(tls_ctx->params->hostname) : NULL;
+ ctx->path = strdup((ctx->params.path) ? ctx->params.path : (char *)default_path);
+ ctx->stream = -1;
+
+ return KNOT_EOK;
+}
+
+static int sockaddr_to_authority(char *buf, const size_t buf_len, const struct sockaddr_storage *ss)
+{
+ if (buf == NULL || ss == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ const char *out = NULL;
+
+ /* Convert IPv6 network address string. */
+ if (ss->ss_family == AF_INET6) {
+ if (buf_len < HTTPS_AUTHORITY_LEN) {
+ return KNOT_EINVAL;
+ }
+
+ const struct sockaddr_in6 *s = (const struct sockaddr_in6 *)ss;
+ buf[0] = '[';
+
+ out = knot_inet_ntop(ss->ss_family, &s->sin6_addr, buf + 1, buf_len - 1);
+ if (out == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ buf += strlen(buf);
+ buf[0] = ']';
+ buf[1] = '\0';
+ /* Convert IPv4 network address string. */
+ } else if (ss->ss_family == AF_INET) {
+ if (buf_len < INET_ADDRSTRLEN) {
+ return KNOT_EINVAL;
+ }
+
+ const struct sockaddr_in *s = (const struct sockaddr_in *)ss;
+
+ out = knot_inet_ntop(ss->ss_family, &s->sin_addr, buf, buf_len);
+ if (out == NULL) {
+ return KNOT_EINVAL;
+ }
+ /* Unknown network address family. */
+ } else {
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+int https_ctx_connect(https_ctx_t *ctx, int sockfd, bool fastopen,
+ struct sockaddr_storage *addr)
+{
+ if (ctx == NULL || addr == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ // Create TLS connection
+ int ret = tls_ctx_connect(ctx->tls, sockfd, fastopen, addr);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // Perform HTTP handshake
+ ret = nghttp2_submit_settings(ctx->session, NGHTTP2_FLAG_NONE, settings,
+ sizeof(settings) / sizeof(*settings));
+ if (ret != 0) {
+ return KNOT_NET_ESOCKET;
+ }
+ ret = nghttp2_session_send(ctx->session);
+ if (ret != 0) {
+ return KNOT_NET_ESOCKET;
+ }
+
+ // Save authority server
+ if (ctx->authority == NULL) {
+ ctx->authority = calloc(HTTPS_AUTHORITY_LEN, 1);
+ ret = sockaddr_to_authority(ctx->authority, HTTPS_AUTHORITY_LEN, addr);
+ if (ret != KNOT_EOK) {
+ free(ctx->authority);
+ ctx->authority = NULL;
+ return KNOT_EINVAL;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+static int https_send_dns_query_common(https_ctx_t *ctx, nghttp2_nv *hdrs, size_t hdrs_len, nghttp2_data_provider *data_provider)
+{
+ assert(hdrs != NULL && hdrs_len > 0);
+
+ ctx->stream = nghttp2_submit_request(ctx->session, NULL, hdrs, hdrs_len,
+ data_provider, NULL);
+ if (ctx->stream < 0) {
+ return KNOT_NET_ESEND;
+ }
+ int ret = nghttp2_session_send(ctx->session);
+ if (ret != 0) {
+ return KNOT_NET_ESEND;
+ }
+
+ return KNOT_EOK;
+}
+
+static int https_send_dns_query_get(https_ctx_t *ctx)
+{
+ const size_t dns_query_len = strlen(ctx->path) +
+ sizeof(default_query) +
+ (ctx->send_buflen * 4) / 3 + 3;
+ char dns_query[dns_query_len];
+ strlcpy(dns_query, ctx->path, dns_query_len);
+ strlcat(dns_query, default_query, dns_query_len);
+
+ size_t tmp_strlen = strlen(dns_query);
+ int32_t ret = knot_base64url_encode(ctx->send_buf, ctx->send_buflen,
+ (uint8_t *)(dns_query + tmp_strlen), dns_query_len - tmp_strlen - 1);
+ if (ret < 0) {
+ return KNOT_EINVAL;
+ }
+
+ nghttp2_nv hdrs[] = {
+ MAKE_STATIC_NV(":method", "GET"),
+ MAKE_STATIC_NV(":scheme", "https"),
+ MAKE_NV(":authority", 10, ctx->authority, strlen(ctx->authority)),
+ MAKE_NV(":path", 5, dns_query, tmp_strlen + ret),
+ MAKE_STATIC_NV("accept", "application/dns-message"),
+ };
+
+ return https_send_dns_query_common(ctx, hdrs, sizeof(hdrs) / sizeof(*hdrs),
+ NULL);
+}
+
+static ssize_t https_send_data_callback(nghttp2_session *session, int32_t stream_id,
+ uint8_t *buf, size_t length, uint32_t *data_flags,
+ nghttp2_data_source *source, void *user_data)
+{
+ https_data_provider_t *buffer = source->ptr;
+ ssize_t sent = (length < buffer->buf_len) ? length : buffer->buf_len;
+
+ memcpy(buf, buffer->buf, sent);
+ buffer->buf += sent;
+ buffer->buf_len -= sent;
+ if (!buffer->buf_len) {
+ *data_flags |= NGHTTP2_DATA_FLAG_EOF;
+ }
+
+ return sent;
+}
+
+static int https_send_dns_query_post(https_ctx_t *ctx)
+{
+ // limit for x->inf: log10(2^(8*sizeof(x))-1)/sizeof(x) = 2,408239965 -> 3
+ size_t capacity = sizeof(size_t) * 3 + 1;
+ // size of number in text form (base 10)
+ char content_length[capacity];
+ int content_length_len = snprintf(content_length, capacity, "%zu", ctx->send_buflen);
+ assert(content_length_len > 0 && content_length_len < capacity);
+
+ nghttp2_nv hdrs[] = {
+ MAKE_STATIC_NV(":method", "POST"),
+ MAKE_STATIC_NV(":scheme", "https"),
+ MAKE_NV(":authority", 10, ctx->authority, strlen(ctx->authority)),
+ MAKE_NV(":path", 5, ctx->path, strlen(ctx->path)),
+ MAKE_STATIC_NV("accept", "application/dns-message"),
+ MAKE_STATIC_NV("content-type", "application/dns-message"),
+ MAKE_NV("content-length", 14, content_length, content_length_len)
+ };
+
+ https_data_provider_t data = {
+ .buf = ctx->send_buf,
+ .buf_len = ctx->send_buflen
+ };
+
+ nghttp2_data_provider data_provider = {
+ .source.ptr = &data,
+ .read_callback = https_send_data_callback
+ };
+
+ return https_send_dns_query_common(ctx, hdrs, sizeof(hdrs) / sizeof(*hdrs),
+ &data_provider);
+}
+
+int https_send_dns_query(https_ctx_t *ctx, const uint8_t *buf, const size_t buf_len)
+{
+ if (ctx == NULL || buf == NULL || buf_len == 0) {
+ return KNOT_EINVAL;
+ }
+
+ ctx->send_buf = buf;
+ ctx->send_buflen = buf_len;
+
+ assert(ctx->params.method == POST || ctx->params.method == GET);
+
+ if (ctx->params.method == POST) {
+ return https_send_dns_query_post(ctx);
+ } else {
+ return https_send_dns_query_get(ctx);
+ }
+}
+
+int https_recv_dns_response(https_ctx_t *ctx, uint8_t *buf, const size_t buf_len)
+{
+ if (ctx == NULL || buf == NULL || buf_len == 0) {
+ return KNOT_EINVAL;
+ }
+
+ pthread_mutex_lock(&ctx->recv_mx);
+ ctx->recv_buf = buf;
+ ctx->recv_buflen = buf_len;
+
+ int ret = nghttp2_session_recv(ctx->session);
+ if (ret != 0) {
+ pthread_mutex_unlock(&ctx->recv_mx);
+ return KNOT_NET_ERECV;
+ }
+ ctx->recv_buf = NULL;
+
+ pthread_mutex_unlock(&ctx->recv_mx);
+
+ if (ctx->status != HTTP_STATUS_SUCCESS) {
+ print_https(ctx);
+ return KNOT_NET_ERECV;
+ }
+
+ assert(buf_len >= ctx->recv_buflen);
+ return buf_len - ctx->recv_buflen;
+}
+
+void https_ctx_deinit(https_ctx_t *ctx)
+{
+ if (ctx == NULL) {
+ return;
+ }
+
+ nghttp2_session_del(ctx->session);
+ ctx->session = NULL;
+ pthread_mutex_destroy(&ctx->recv_mx);
+ free(ctx->path);
+ ctx->path = NULL;
+ free(ctx->authority);
+ ctx->authority = NULL;
+}
+
+void print_https(const https_ctx_t *ctx)
+{
+ if (!ctx || !ctx->params.enable || !ctx->authority || !ctx->path) {
+ return;
+ }
+
+ printf(";; HTTP session (HTTP/2-%s)-(%s%s)-(status: %lu)\n",
+ ctx->params.method == POST ? "POST" : "GET", ctx->authority,
+ ctx->path, ctx->status);
+}
+
+#endif //LIBNGHTTP2
diff --git a/src/utils/common/https.h b/src/utils/common/https.h
new file mode 100644
index 0000000..aed1cd5
--- /dev/null
+++ b/src/utils/common/https.h
@@ -0,0 +1,150 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+/*! \brief HTTP method to transfer query. */
+typedef enum {
+ POST,
+ GET
+} https_method_t;
+
+/*! \brief HTTPS parameters. */
+typedef struct {
+ /*! Use HTTPS indicator. */
+ bool enable;
+ /*! HTTP method to transfer query. */
+ https_method_t method;
+ /*! Path */
+ char *path;
+} https_params_t;
+
+int https_params_copy(https_params_t *dst, const https_params_t *src);
+void https_params_clean(https_params_t *params);
+
+#ifdef LIBNGHTTP2
+
+#include <netinet/in.h>
+#include <pthread.h>
+#include <sys/socket.h>
+#include <nghttp2/nghttp2.h>
+
+#include "utils/common/tls.h"
+
+extern const gnutls_datum_t doh_alpn;
+
+/*! \brief Structure that stores data source for DATA frames. */
+typedef struct {
+ const uint8_t *buf;
+ size_t buf_len;
+} https_data_provider_t;
+
+/*! \brief HTTPS context. */
+typedef struct {
+ // Parameters
+ https_params_t params;
+
+ // Contexts
+ nghttp2_session *session;
+ tls_ctx_t *tls;
+ char *authority;
+ char *path;
+
+ // Send destination
+ const uint8_t *send_buf;
+ size_t send_buflen;
+
+ // Recv destination
+ uint8_t *recv_buf;
+ size_t recv_buflen;
+ unsigned long status;
+
+ // Recv locks
+ pthread_mutex_t recv_mx;
+ int32_t stream;
+} https_ctx_t;
+
+/*!
+ * \brief Initialize HTTPS context.
+ *
+ * \param ctx HTTPS context.
+ * \param tls_ctx TLS context.
+ * \param params Parameter table.
+ *
+ * \retval KNOT_EOK When initialized.
+ * \retval KNOT_EINVAL When parameters are invalid.
+ */
+int https_ctx_init(https_ctx_t *ctx, tls_ctx_t *tls_ctx, const https_params_t *params);
+
+/*!
+ * \brief Create TLS connection and perform HTTPS handshake.
+ *
+ * \param ctx HTTPS context.
+ * \param sockfd Socket descriptor.
+ * \param fastopen Use TCP Fast Open indication.
+ * \param addr Socket address storage with address to server side.
+ *
+ * \retval KNOT_EOK When successfully connected.
+ * \retval KNOT_EINVAL When parameters are invalid.
+ * \retval KNOT_NET_ESOCKET When socket is no accessible.
+ * \retval KNOT_NET_ETIMEOUT When server respond takes too long.
+ * \retval KNOT_NET_ECONNECT When unnable to connect to the server.
+ */
+int https_ctx_connect(https_ctx_t *ctx, int sockfd, bool fastopen,
+ struct sockaddr_storage *addr);
+
+/*!
+ * \brief Send buffer as DNS message over HTTPS.
+ *
+ * \param ctx HTTPS context.
+ * \param buf Buffer with DNS message in wire format.
+ * \param buf_len Length of buffer.
+ *
+ * \retval KNOT_EOK When successfully sent.
+ * \retval KNOT_EINVAL When parameters are invalid.
+ * \retval KNOT_NET_ESEND When error occurs while sending a data.
+ */
+int https_send_dns_query(https_ctx_t *ctx, const uint8_t *buf, const size_t buf_len);
+
+/*!
+ * \brief Receive DATA frame as HTTPS packet, and store it into buffer.
+ *
+ * \param ctx HTTPS context.
+ * \param buf Buffer where will be DNS response stored.
+ * \param buf_len Length of buffer.
+ *
+ * \retval >=0 Number of bytes received in DATA frame.
+ * \retval KNOT_NET_ERECV When error while receive.
+ */
+int https_recv_dns_response(https_ctx_t *ctx, uint8_t *buf, const size_t buf_len);
+
+/*!
+ * \brief Deinitialize HTTPS context.
+ *
+ * \param ctx HTTPS context.
+ */
+void https_ctx_deinit(https_ctx_t *ctx);
+
+/*!
+ * \brief Prints information about HTTPS context.
+ *
+ * \param ctx HTTPS context.
+ */
+void print_https(const https_ctx_t *ctx);
+
+#endif //LIBNGHTTP2
diff --git a/src/utils/common/lookup.c b/src/utils/common/lookup.c
new file mode 100644
index 0000000..e7f6084
--- /dev/null
+++ b/src/utils/common/lookup.c
@@ -0,0 +1,295 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+
+#include "utils/common/lookup.h"
+#include "contrib/mempattern.h"
+#include "contrib/ucw/mempool.h"
+#include "libknot/error.h"
+
+int lookup_init(lookup_t *lookup)
+{
+ if (lookup == NULL) {
+ return KNOT_EINVAL;
+ }
+ memset(lookup, 0, sizeof(*lookup));
+
+ mm_ctx_mempool(&lookup->mm, MM_DEFAULT_BLKSIZE);
+ lookup->trie = trie_create(&lookup->mm);
+ if (lookup->trie == NULL) {
+ mp_delete(lookup->mm.ctx);
+ return KNOT_ENOMEM;
+ }
+
+ return KNOT_EOK;
+}
+
+static void reset_output(lookup_t *lookup)
+{
+ if (lookup == NULL) {
+ return;
+ }
+
+ mm_free(&lookup->mm, lookup->found.key);
+ lookup->found.key = NULL;
+ lookup->found.data = NULL;
+
+ lookup->iter.count = 0;
+
+ mm_free(&lookup->mm, lookup->iter.first_key);
+ lookup->iter.first_key = NULL;
+
+ trie_it_free(lookup->iter.it);
+ lookup->iter.it = NULL;
+}
+
+void lookup_deinit(lookup_t *lookup)
+{
+ if (lookup == NULL) {
+ return;
+ }
+
+ reset_output(lookup);
+
+ trie_free(lookup->trie);
+ mp_delete(lookup->mm.ctx);
+}
+
+int lookup_insert(lookup_t *lookup, const char *str, void *data)
+{
+ if (lookup == NULL || str == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ size_t str_len = strlen(str);
+ if (str_len == 0) {
+ return KNOT_EINVAL;
+ }
+
+ trie_val_t *val = trie_get_ins(lookup->trie, (const trie_key_t *)str, str_len);
+ if (val == NULL) {
+ return KNOT_ENOMEM;
+ }
+ *val = data;
+
+ return KNOT_EOK;
+}
+
+int lookup_remove(lookup_t *lookup, const char *str)
+{
+ if (lookup == NULL || str == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ size_t str_len = strlen(str);
+ if (str_len > 0) {
+ (void)trie_del(lookup->trie, (const trie_key_t *)str, str_len, NULL);
+ }
+
+ return KNOT_EOK;
+}
+
+static int set_key(lookup_t *lookup, char **dst, const char *key, size_t key_len)
+{
+ if (*dst != NULL) {
+ mm_free(&lookup->mm, *dst);
+ }
+ *dst = mm_alloc(&lookup->mm, key_len + 1);
+ if (*dst == NULL) {
+ return KNOT_ENOMEM;
+ }
+ memcpy(*dst, key, key_len);
+ (*dst)[key_len] = '\0';
+
+ return KNOT_EOK;
+}
+
+int lookup_search(lookup_t *lookup, const char *str, size_t str_len)
+{
+ if (lookup == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ // Change NULL string to the empty one.
+ if (str == NULL) {
+ str = "";
+ }
+
+ reset_output(lookup);
+
+ size_t new_len = 0;
+ trie_it_t *it = trie_it_begin(lookup->trie);
+ for (; !trie_it_finished(it); trie_it_next(it)) {
+ size_t len;
+ const char *key = (const char *)trie_it_key(it, &len);
+
+ // Compare with a shorter key.
+ if (len < str_len) {
+ int ret = memcmp(str, key, len);
+ if (ret >= 0) {
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ // Compare with an equal length or longer key.
+ int ret = memcmp(str, key, str_len);
+ if (ret == 0) {
+ lookup->iter.count++;
+
+ // First candidate.
+ if (lookup->iter.count == 1) {
+ ret = set_key(lookup, &lookup->found.key, key, len);
+ if (ret != KNOT_EOK) {
+ break;
+ }
+ lookup->found.data = *trie_it_val(it);
+ new_len = len;
+ // Another candidate.
+ } else if (new_len > str_len) {
+ if (new_len > len) {
+ new_len = len;
+ }
+ while (memcmp(lookup->found.key, key, new_len) != 0) {
+ new_len--;
+ }
+ }
+ // Stop if greater than the key, and also than all the following keys.
+ } else if (ret < 0) {
+ break;
+ }
+ }
+ trie_it_free(it);
+
+ switch (lookup->iter.count) {
+ case 0:
+ return KNOT_ENOENT;
+ case 1:
+ return KNOT_EOK;
+ default:
+ // Store full name of the first candidate.
+ if (set_key(lookup, &lookup->iter.first_key, lookup->found.key,
+ strlen(lookup->found.key)) != KNOT_EOK) {
+ return KNOT_ENOMEM;
+ }
+ lookup->found.key[new_len] = '\0';
+ lookup->found.data = NULL;
+
+ return KNOT_EFEWDATA;
+ }
+}
+
+void lookup_list(lookup_t *lookup)
+{
+ if (lookup == NULL || lookup->iter.first_key == NULL) {
+ return;
+ }
+
+ if (lookup->iter.it != NULL) {
+ if (trie_it_finished(lookup->iter.it)) {
+ trie_it_free(lookup->iter.it);
+ lookup->iter.it = NULL;
+ return;
+ }
+
+ trie_it_next(lookup->iter.it);
+
+ size_t len;
+ const char *key = (const char *)trie_it_key(lookup->iter.it, &len);
+
+ int ret = set_key(lookup, &lookup->found.key, key, len);
+ if (ret == KNOT_EOK) {
+ lookup->found.data = *trie_it_val(lookup->iter.it);
+ }
+ return;
+ }
+
+ lookup->iter.it = trie_it_begin(lookup->trie);
+ while (!trie_it_finished(lookup->iter.it)) {
+ size_t len;
+ const char *key = (const char *)trie_it_key(lookup->iter.it, &len);
+
+ if (strncmp(key, lookup->iter.first_key, len) == 0) {
+ int ret = set_key(lookup, &lookup->found.key, key, len);
+ if (ret == KNOT_EOK) {
+ lookup->found.data = *trie_it_val(lookup->iter.it);
+ }
+ break;
+ }
+ trie_it_next(lookup->iter.it);
+ }
+}
+
+static void print_options(lookup_t *lookup, EditLine *el)
+{
+ // Get terminal lines.
+ unsigned lines = 0;
+ if (el_get(el, EL_GETTC, "li", &lines) != 0 || lines < 3) {
+ return;
+ }
+
+ for (size_t i = 1; i <= lookup->iter.count; i++) {
+ lookup_list(lookup);
+ printf("\n%s", lookup->found.key);
+
+ if (i > 1 && i % (lines - 1) == 0 && i < lookup->iter.count) {
+ printf("\n Display next from %zu possibilities? (y or n)",
+ lookup->iter.count);
+ char next;
+ el_getc(el, &next);
+ if (next != 'y') {
+ break;
+ }
+ }
+ }
+
+ printf("\n");
+ fflush(stdout);
+}
+
+int lookup_complete(lookup_t *lookup, const char *str, size_t str_len,
+ EditLine *el, bool add_space)
+{
+ if (lookup == NULL || el == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ // Try to complete the command name.
+ int ret = lookup_search(lookup, str, str_len);
+ switch (ret) {
+ case KNOT_EOK:
+ el_deletestr(el, str_len);
+ el_insertstr(el, lookup->found.key);
+ if (add_space) {
+ el_insertstr(el, " ");
+ }
+ break;
+ case KNOT_EFEWDATA:
+ if (strlen(lookup->found.key) > str_len) {
+ el_deletestr(el, str_len);
+ el_insertstr(el, lookup->found.key);
+ } else {
+ print_options(lookup, el);
+ }
+ break;
+ default:
+ break;
+ }
+
+ return ret;
+}
diff --git a/src/utils/common/lookup.h b/src/utils/common/lookup.h
new file mode 100644
index 0000000..b6dc8ee
--- /dev/null
+++ b/src/utils/common/lookup.h
@@ -0,0 +1,124 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <histedit.h>
+
+#include "libknot/mm_ctx.h"
+#include "contrib/qp-trie/trie.h"
+
+/*! Lookup context. */
+typedef struct {
+ /*! Memory pool context. */
+ knot_mm_t mm;
+ /*! Main trie storage. */
+ trie_t *trie;
+
+ /*! Current (iteration) data context. */
+ struct {
+ /*! Stored key. */
+ char *key;
+ /*! Corresponding key data. */
+ void *data;
+ } found;
+
+ /*! Iteration context. */
+ struct {
+ /*! Total number of possibilities. */
+ size_t count;
+ /*! The first possibility. */
+ char *first_key;
+ /*! Hat-trie iterator. */
+ trie_it_t *it;
+ } iter;
+} lookup_t;
+
+/*!
+ * Initializes the lookup context.
+ *
+ * \param[in] lookup Lookup context.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int lookup_init(lookup_t *lookup);
+
+/*!
+ * Deinitializes the lookup context.
+ *
+ * \param[in] lookup Lookup context.
+ */
+void lookup_deinit(lookup_t *lookup);
+
+/*!
+ * Inserts given key and data into the lookup.
+ *
+ * \param[in] lookup Lookup context.
+ * \param[in] str Textual key.
+ * \param[in] data Key textual data.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int lookup_insert(lookup_t *lookup, const char *str, void *data);
+
+/*!
+ * Removes given key from the lookup.
+ *
+ * \param[in] lookup Lookup context.
+ * \param[in] str Textual key.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int lookup_remove(lookup_t *lookup, const char *str);
+
+/*!
+ * Searches the lookup container for the given key.
+ *
+ * \note If one candidate, lookup.found contains the key/data,
+ * if more candidates, lookup.found contains the common key prefix and
+ * lookup.iter.first_key is the first candidate key.
+ *
+ * \param[in] lookup Lookup context.
+ * \param[in] str Textual key.
+ * \param[in] str_len Textual key length.
+ *
+ * \return Error code, KNOT_EOK if 1 candidate, KNOT_ENOENT if no candidate,
+ * and KNOT_EFEWDATA if more candidates are possible.
+ */
+int lookup_search(lookup_t *lookup, const char *str, size_t str_len);
+
+/*!
+ * Moves the lookup iterator to the next key candidate.
+ *
+ * \note lookup.found is updated.
+ *
+ * \param[in] lookup Lookup context.
+ */
+void lookup_list(lookup_t *lookup);
+
+/*!
+ * Completes the string based on the lookup content or prints all candidates.
+ *
+ * \param[in] lookup Lookup context.
+ * \param[in] str Textual key.
+ * \param[in] str_len Textual key length.
+ * \param[in] el Editline context.
+ * \param[in] add_space Add one space after completed string flag.
+ *
+ * \return Error code, same as lookup_search().
+ */
+int lookup_complete(lookup_t *lookup, const char *str, size_t str_len,
+ EditLine *el, bool add_space);
diff --git a/src/utils/common/msg.c b/src/utils/common/msg.c
new file mode 100644
index 0000000..c125297
--- /dev/null
+++ b/src/utils/common/msg.c
@@ -0,0 +1,40 @@
+/* Copyright (C) 2011 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "utils/common/msg.h"
+
+static volatile int MSG_DBG_STATE = 0; /* True if debugging is enabled. */
+
+int msg_enable_debug(int val)
+{
+ return MSG_DBG_STATE = val;
+}
+
+int msg_debug(const char *fmt, ...)
+{
+ int n = 0;
+ if (MSG_DBG_STATE) {
+ va_list ap;
+ va_start(ap, fmt);
+ n = vprintf(fmt, ap);
+ va_end(ap);
+ }
+ return n;
+}
diff --git a/src/utils/common/msg.h b/src/utils/common/msg.h
new file mode 100644
index 0000000..d2ed57e
--- /dev/null
+++ b/src/utils/common/msg.h
@@ -0,0 +1,42 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdio.h>
+
+#define ERROR_ ";; ERROR: "
+#define INFO_ ";; INFO: "
+#define WARNING_ ";; WARNING: "
+#define DEBUG_ ";; DEBUG: "
+
+#define ERR(msg, ...) { fprintf(stderr, ERROR_ msg "\n", ##__VA_ARGS__); fflush(stderr); }
+#define INFO(msg, ...) { fprintf(stdout, INFO_ msg "\n", ##__VA_ARGS__); fflush(stdout); }
+#define WARN(msg, ...) { fprintf(stderr, WARNING_ msg "\n", ##__VA_ARGS__); fflush(stderr); }
+#define DBG(msg, ...) { msg_debug(DEBUG_ msg "\n", ##__VA_ARGS__); fflush(stdout); }
+
+/*! \brief Enable/disable debugging. */
+int msg_enable_debug(int val);
+
+/*! \brief Print debug message. */
+int msg_debug(const char *fmt, ...);
+
+/*! \brief Debug message for null input. */
+#define DBG_NULL DBG("%s: null parameter", __func__)
+
+#define ERR2(msg, ...) { fprintf(stderr, "error: " msg "\n", ##__VA_ARGS__); fflush(stderr); }
+#define WARN2(msg, ...) { fprintf(stderr, "warning: " msg "\n", ##__VA_ARGS__); fflush(stderr); }
+#define INFO2(msg, ...) { fprintf(stdout, msg "\n", ##__VA_ARGS__); fflush(stdout); }
diff --git a/src/utils/common/netio.c b/src/utils/common/netio.c
new file mode 100644
index 0000000..eed14ee
--- /dev/null
+++ b/src/utils/common/netio.c
@@ -0,0 +1,925 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <arpa/inet.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <poll.h>
+#include <stdlib.h>
+#include <netinet/in.h>
+#include <sys/types.h> // OpenBSD
+#include <netinet/tcp.h> // TCP_FASTOPEN
+#include <sys/socket.h>
+
+#ifdef HAVE_SYS_UIO_H
+#include <sys/uio.h>
+#endif
+
+#include "utils/common/netio.h"
+#include "utils/common/msg.h"
+#include "utils/common/tls.h"
+#include "libknot/libknot.h"
+#include "contrib/net.h"
+#include "contrib/proxyv2/proxyv2.h"
+#include "contrib/sockaddr.h"
+
+static knot_probe_proto_t get_protocol(const net_t *net)
+{
+#ifdef ENABLE_QUIC
+ if (net->quic.params.enable) {
+ return KNOT_PROBE_PROTO_QUIC;
+ } else
+#endif
+#ifdef LIBNGHTTP2
+ if (net->https.params.enable) {
+ return KNOT_PROBE_PROTO_HTTPS;
+ } else
+#endif
+ if (net->tls.params != NULL && net->tls.params->enable) {
+ return KNOT_PROBE_PROTO_TLS;
+ } else if (net->socktype == PROTO_TCP) {
+ return KNOT_PROBE_PROTO_TCP;
+ } else {
+ assert(net->socktype == PROTO_UDP);
+ return KNOT_PROBE_PROTO_UDP;
+ }
+}
+
+static const char *get_protocol_str(const knot_probe_proto_t proto)
+{
+ switch (proto) {
+ case KNOT_PROBE_PROTO_UDP:
+ return "UDP";
+ case KNOT_PROBE_PROTO_QUIC:
+ return "QUIC";
+ case KNOT_PROBE_PROTO_TCP:
+ return "TCP";
+ case KNOT_PROBE_PROTO_TLS:
+ return "TLS";
+ case KNOT_PROBE_PROTO_HTTPS:
+ return "HTTPS";
+ default:
+ return "UNKNOWN";
+ }
+}
+
+srv_info_t *srv_info_create(const char *name, const char *service)
+{
+ if (name == NULL || service == NULL) {
+ DBG_NULL;
+ return NULL;
+ }
+
+ // Create output structure.
+ srv_info_t *server = calloc(1, sizeof(srv_info_t));
+
+ // Check output.
+ if (server == NULL) {
+ return NULL;
+ }
+
+ // Fill output.
+ server->name = strdup(name);
+ server->service = strdup(service);
+
+ if (server->name == NULL || server->service == NULL) {
+ srv_info_free(server);
+ return NULL;
+ }
+
+ // Return result.
+ return server;
+}
+
+void srv_info_free(srv_info_t *server)
+{
+ if (server == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ free(server->name);
+ free(server->service);
+ free(server);
+}
+
+int get_iptype(const ip_t ip, const srv_info_t *server)
+{
+ bool unix_socket = (server->name[0] == '/');
+
+ switch (ip) {
+ case IP_4:
+ return AF_INET;
+ case IP_6:
+ return AF_INET6;
+ default:
+ return unix_socket ? AF_UNIX : AF_UNSPEC;
+ }
+}
+
+int get_socktype(const protocol_t proto, const uint16_t type)
+{
+ switch (proto) {
+ case PROTO_TCP:
+ return SOCK_STREAM;
+ case PROTO_UDP:
+ return SOCK_DGRAM;
+ default:
+ if (type == KNOT_RRTYPE_AXFR || type == KNOT_RRTYPE_IXFR) {
+ return SOCK_STREAM;
+ } else {
+ return SOCK_DGRAM;
+ }
+ }
+}
+
+const char *get_sockname(const int socktype)
+{
+ switch (socktype) {
+ case SOCK_STREAM:
+ return "TCP";
+ case SOCK_DGRAM:
+ return "UDP";
+ default:
+ return "UNKNOWN";
+ }
+}
+
+static int get_addr(const srv_info_t *server,
+ const int iptype,
+ const int socktype,
+ struct addrinfo **info)
+{
+ struct addrinfo hints;
+
+ // Set connection hints.
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = iptype;
+ hints.ai_socktype = socktype;
+
+ // Get connection parameters.
+ int ret = getaddrinfo(server->name, server->service, &hints, info);
+ switch (ret) {
+ case 0:
+ return 0;
+#ifdef EAI_ADDRFAMILY /* EAI_ADDRFAMILY isn't implemented in FreeBSD/macOS anymore. */
+ case EAI_ADDRFAMILY:
+ break;
+#else /* FreeBSD, macOS, and likely others return EAI_NONAME instead. */
+ case EAI_NONAME:
+ if (iptype != AF_UNSPEC) {
+ break;
+ }
+ /* FALLTHROUGH */
+#endif /* EAI_ADDRFAMILY */
+ default:
+ ERR("%s for %s@%s", gai_strerror(ret), server->name, server->service);
+ }
+ return -1;
+}
+
+void get_addr_str(const struct sockaddr_storage *ss,
+ const knot_probe_proto_t protocol,
+ char **dst)
+{
+ char addr_str[SOCKADDR_STRLEN] = { 0 };
+ const char *proto_str = get_protocol_str(protocol);
+
+ // Get network address string and port number.
+ sockaddr_tostr(addr_str, sizeof(addr_str), ss);
+
+ // Calculate needed buffer size
+ size_t buflen = strlen(addr_str) + strlen(proto_str) + 3 /* () */;
+
+ // Free previous string if any and write result
+ free(*dst);
+ *dst = malloc(buflen);
+ if (*dst != NULL) {
+ int ret = snprintf(*dst, buflen, "%s(%s)", addr_str, proto_str);
+ if (ret <= 0 || ret >= buflen) {
+ **dst = '\0';
+ }
+ }
+}
+
+int net_init(const srv_info_t *local,
+ const srv_info_t *remote,
+ const int iptype,
+ const int socktype,
+ const int wait,
+ const net_flags_t flags,
+ const struct sockaddr *proxy_src,
+ const struct sockaddr *proxy_dst,
+ net_t *net)
+{
+ if (remote == NULL || net == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ // Clean network structure.
+ memset(net, 0, sizeof(*net));
+ net->sockfd = -1;
+
+ if (iptype == AF_UNIX) {
+ struct addrinfo *info = calloc(1, sizeof(struct addrinfo));
+ info->ai_addr = calloc(1, sizeof(struct sockaddr_storage));
+ info->ai_addrlen = sizeof(struct sockaddr_un);
+ info->ai_socktype = socktype;
+ info->ai_family = iptype;
+ int ret = sockaddr_set_raw((struct sockaddr_storage *)info->ai_addr,
+ AF_UNIX, (const uint8_t *)remote->name,
+ strlen(remote->name));
+ if (ret != KNOT_EOK) {
+ free(info->ai_addr);
+ free(info);
+ return ret;
+ }
+ net->remote_info = info;
+ } else {
+ // Get remote address list.
+ if (get_addr(remote, iptype, socktype, &net->remote_info) != 0) {
+ net_clean(net);
+ return KNOT_NET_EADDR;
+ }
+ }
+
+ // Set current remote address.
+ net->srv = net->remote_info;
+
+ // Get local address if specified.
+ if (local != NULL) {
+ if (get_addr(local, iptype, socktype, &net->local_info) != 0) {
+ net_clean(net);
+ return KNOT_NET_EADDR;
+ }
+ }
+
+ // Store network parameters.
+ net->sockfd = -1;
+ net->iptype = iptype;
+ net->socktype = socktype;
+ net->wait = wait;
+ net->local = local;
+ net->remote = remote;
+ net->flags = flags;
+ net->proxy.src = proxy_src;
+ net->proxy.dst = proxy_dst;
+
+ if ((bool)(proxy_src == NULL) != (bool)(proxy_dst == NULL) ||
+ (proxy_src != NULL && proxy_src->sa_family != proxy_dst->sa_family)) {
+ net_clean(net);
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+int net_init_crypto(net_t *net,
+ const tls_params_t *tls_params,
+ const https_params_t *https_params,
+ const quic_params_t *quic_params)
+{
+ if (net == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ if (tls_params == NULL || !tls_params->enable) {
+ return KNOT_EOK;
+ }
+
+ tls_ctx_deinit(&net->tls);
+#ifdef LIBNGHTTP2
+ // Prepare for HTTPS.
+ if (https_params != NULL && https_params->enable) {
+ int ret = tls_ctx_init(&net->tls, tls_params,
+ GNUTLS_NONBLOCK, net->wait);
+ if (ret != KNOT_EOK) {
+ net_clean(net);
+ return ret;
+ }
+ https_ctx_deinit(&net->https);
+ ret = https_ctx_init(&net->https, &net->tls, https_params);
+ if (ret != KNOT_EOK) {
+ net_clean(net);
+ return ret;
+ }
+ } else
+#endif //LIBNGHTTP2
+#ifdef ENABLE_QUIC
+ if (quic_params != NULL && quic_params->enable) {
+ int ret = tls_ctx_init(&net->tls, tls_params,
+ GNUTLS_NONBLOCK | GNUTLS_ENABLE_EARLY_DATA |
+ GNUTLS_NO_END_OF_EARLY_DATA, net->wait);
+ if (ret != KNOT_EOK) {
+ net_clean(net);
+ return ret;
+ }
+ quic_ctx_deinit(&net->quic);
+ ret = quic_ctx_init(&net->quic, &net->tls, quic_params);
+ if (ret != KNOT_EOK) {
+ net_clean(net);
+ return ret;
+ }
+ } else
+#endif //ENABLE_QUIC
+ {
+ int ret = tls_ctx_init(&net->tls, tls_params,
+ GNUTLS_NONBLOCK, net->wait);
+ if (ret != KNOT_EOK) {
+ net_clean(net);
+ return ret;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+/*!
+ * Connect with TCP Fast Open.
+ */
+static int fastopen_connect(int sockfd, const struct addrinfo *srv)
+{
+#if defined( __FreeBSD__)
+ const int enable = 1;
+ return setsockopt(sockfd, IPPROTO_TCP, TCP_FASTOPEN, &enable, sizeof(enable));
+#elif defined(__APPLE__)
+ // connection is performed lazily when first data are sent
+ struct sa_endpoints ep = {0};
+ ep.sae_dstaddr = srv->ai_addr;
+ ep.sae_dstaddrlen = srv->ai_addrlen;
+ int flags = CONNECT_DATA_IDEMPOTENT|CONNECT_RESUME_ON_READ_WRITE;
+
+ return connectx(sockfd, &ep, SAE_ASSOCID_ANY, flags, NULL, 0, NULL, NULL);
+#elif defined(__linux__)
+ // connect() will be called implicitly with sendto(), sendmsg()
+ return 0;
+#else
+ errno = ENOTSUP;
+ return -1;
+#endif
+}
+
+/*!
+ * Sends data with TCP Fast Open.
+ */
+static int fastopen_send(int sockfd, const struct msghdr *msg, int timeout)
+{
+#if defined(__FreeBSD__) || defined(__APPLE__)
+ return sendmsg(sockfd, msg, 0);
+#elif defined(__linux__)
+ int ret = sendmsg(sockfd, msg, MSG_FASTOPEN);
+ if (ret == -1 && errno == EINPROGRESS) {
+ struct pollfd pfd = {
+ .fd = sockfd,
+ .events = POLLOUT,
+ .revents = 0,
+ };
+ if (poll(&pfd, 1, 1000 * timeout) != 1) {
+ errno = ETIMEDOUT;
+ return -1;
+ }
+ ret = sendmsg(sockfd, msg, 0);
+ }
+ return ret;
+#else
+ errno = ENOTSUP;
+ return -1;
+#endif
+}
+
+static char *net_get_remote(const net_t *net)
+{
+ if (net->tls.params->sni != NULL) {
+ return net->tls.params->sni;
+ } else if (net->tls.params->hostname != NULL) {
+ return net->tls.params->hostname;
+ } else if (strchr(net->remote_str, ':') == NULL) {
+ char *at = strchr(net->remote_str, '@');
+ if (at != NULL && strncmp(net->remote->name, net->remote_str,
+ at - net->remote_str)) {
+ return net->remote->name;
+ }
+ }
+ return NULL;
+}
+
+int net_connect(net_t *net)
+{
+ if (net == NULL || net->srv == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ // Set remote information string.
+ get_addr_str((struct sockaddr_storage *)net->srv->ai_addr,
+ get_protocol(net), &net->remote_str);
+
+ // Create socket.
+ int sockfd = socket(net->srv->ai_family, net->socktype, 0);
+ if (sockfd == -1) {
+ WARN("can't create socket for %s", net->remote_str);
+ return KNOT_NET_ESOCKET;
+ }
+
+ // Initialize poll descriptor structure.
+ struct pollfd pfd = {
+ .fd = sockfd,
+ .events = POLLOUT,
+ .revents = 0,
+ };
+
+ // Set non-blocking socket.
+ if (fcntl(sockfd, F_SETFL, O_NONBLOCK) == -1) {
+ WARN("can't set non-blocking socket for %s", net->remote_str);
+ return KNOT_NET_ESOCKET;
+ }
+
+ // Bind address to socket if specified.
+ if (net->local_info != NULL) {
+ if (bind(sockfd, net->local_info->ai_addr,
+ net->local_info->ai_addrlen) == -1) {
+ WARN("can't assign address %s", net->local->name);
+ return KNOT_NET_ESOCKET;
+ }
+ } else {
+ // Ensure source port is always randomized (even for TCP).
+ struct sockaddr_storage local = { .ss_family = net->srv->ai_family };
+ (void)bind(sockfd, (struct sockaddr *)&local, sockaddr_len(&local));
+ }
+
+ int ret = 0;
+ if (net->socktype == SOCK_STREAM) {
+ int cs = 1, err;
+ socklen_t err_len = sizeof(err);
+ bool fastopen = net->flags & NET_FLAGS_FASTOPEN;
+
+#ifdef TCP_NODELAY
+ (void)setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &cs, sizeof(cs));
+#endif
+
+ // Establish a connection.
+ if (net->tls.params == NULL || !fastopen) {
+ if (fastopen) {
+ ret = fastopen_connect(sockfd, net->srv);
+ } else {
+ ret = connect(sockfd, net->srv->ai_addr, net->srv->ai_addrlen);
+ }
+ if (ret != 0 && errno != EINPROGRESS) {
+ WARN("can't connect to %s", net->remote_str);
+ net_close(net);
+ return KNOT_NET_ECONNECT;
+ }
+
+ // Check for connection timeout.
+ if (!fastopen && poll(&pfd, 1, 1000 * net->wait) != 1) {
+ WARN("connection timeout for %s", net->remote_str);
+ net_close(net);
+ return KNOT_NET_ECONNECT;
+ }
+
+ // Check if NB socket is writeable.
+ cs = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, &err_len);
+ if (cs < 0 || err != 0) {
+ WARN("can't connect to %s", net->remote_str);
+ net_close(net);
+ return KNOT_NET_ECONNECT;
+ }
+ }
+
+ if (net->tls.params != NULL) {
+#ifdef LIBNGHTTP2
+ if (net->https.params.enable) {
+ // Establish HTTPS connection.
+ char *remote = net_get_remote(net);
+ ret = tls_ctx_setup_remote_endpoint(&net->tls, &doh_alpn, 1, NULL,
+ remote);
+ if (ret != 0) {
+ net_close(net);
+ return ret;
+ }
+ if (remote && net->https.authority == NULL) {
+ net->https.authority = strdup(remote);
+ }
+ ret = https_ctx_connect(&net->https, sockfd, fastopen,
+ (struct sockaddr_storage *)net->srv->ai_addr);
+ } else
+#endif //LIBNGHTTP2
+ {
+ // Establish TLS connection.
+ ret = tls_ctx_setup_remote_endpoint(&net->tls, &dot_alpn, 1, NULL,
+ net_get_remote(net));
+ if (ret != 0) {
+ net_close(net);
+ return ret;
+ }
+ ret = tls_ctx_connect(&net->tls, sockfd, fastopen,
+ (struct sockaddr_storage *)net->srv->ai_addr);
+ }
+ if (ret != KNOT_EOK) {
+ net_close(net);
+ return ret;
+ }
+ }
+ }
+#ifdef ENABLE_QUIC
+ else if (net->socktype == SOCK_DGRAM) {
+ if (net->quic.params.enable) {
+ // Establish QUIC connection.
+ ret = net_cmsg_ecn_enable(sockfd, net->srv->ai_family);
+ if (ret != KNOT_EOK && ret != KNOT_ENOTSUP) {
+ net_close(net);
+ return ret;
+ }
+ ret = tls_ctx_setup_remote_endpoint(&net->tls,
+ &doq_alpn, 1, QUIC_PRIORITY, net_get_remote(net));
+ if (ret != 0) {
+ net_close(net);
+ return ret;
+ }
+ ret = quic_ctx_connect(&net->quic, sockfd,
+ (struct addrinfo *)net->srv);
+ if (ret != KNOT_EOK) {
+ net_close(net);
+ return ret;
+ }
+ }
+ }
+#endif
+
+ // Store socket descriptor.
+ net->sockfd = sockfd;
+
+ return KNOT_EOK;
+}
+
+int net_set_local_info(net_t *net)
+{
+ if (net == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ socklen_t local_addr_len = sizeof(struct sockaddr_storage);
+
+ struct addrinfo *new_info = calloc(1, sizeof(*new_info) + local_addr_len);
+ if (new_info == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ new_info->ai_addr = (struct sockaddr *)(new_info + 1);
+ new_info->ai_family = net->srv->ai_family;
+ new_info->ai_socktype = net->srv->ai_socktype;
+ new_info->ai_protocol = net->srv->ai_protocol;
+ new_info->ai_addrlen = local_addr_len;
+
+ if (getsockname(net->sockfd, new_info->ai_addr, &local_addr_len) == -1) {
+ WARN("can't get local address");
+ free(new_info);
+ return KNOT_NET_ESOCKET;
+ }
+
+ if (net->local_info != NULL) {
+ if (net->local == NULL) {
+ free(net->local_info);
+ } else {
+ freeaddrinfo(net->local_info);
+ }
+ }
+
+ net->local_info = new_info;
+
+ get_addr_str((struct sockaddr_storage *)net->local_info->ai_addr,
+ get_protocol(net), &net->local_str);
+
+ return KNOT_EOK;
+}
+
+int net_send(const net_t *net, const uint8_t *buf, const size_t buf_len)
+{
+ if (net == NULL || buf == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+#ifdef ENABLE_QUIC
+ // Send data over QUIC.
+ if (net->quic.params.enable) {
+ int ret = quic_send_dns_query((quic_ctx_t *)&net->quic,
+ net->sockfd, net->srv, buf, buf_len);
+ if (ret != KNOT_EOK) {
+ WARN("can't send query to %s", net->remote_str);
+ return KNOT_NET_ESEND;
+ }
+ } else
+#endif
+ // Send data over UDP.
+ if (net->socktype == SOCK_DGRAM) {
+ char proxy_buf[PROXYV2_HEADER_MAXLEN];
+ struct iovec iov[2] = {
+ { .iov_base = proxy_buf, .iov_len = 0 },
+ { .iov_base = (void *)buf, .iov_len = buf_len }
+ };
+
+ struct msghdr msg = {
+ .msg_name = net->srv->ai_addr,
+ .msg_namelen = net->srv->ai_addrlen,
+ .msg_iov = &iov[1],
+ .msg_iovlen = 1
+ };
+
+ if (net->proxy.src != NULL && net->proxy.src->sa_family != 0) {
+ int ret = proxyv2_write_header(proxy_buf, sizeof(proxy_buf),
+ SOCK_DGRAM, net->proxy.src,
+ net->proxy.dst);
+ if (ret < 0) {
+ WARN("can't send proxied query to %s", net->remote_str);
+ return KNOT_NET_ESEND;
+ }
+ iov[0].iov_len = ret;
+ msg.msg_iov--;
+ msg.msg_iovlen++;
+ }
+
+ ssize_t total = iov[0].iov_len + iov[1].iov_len;
+
+ if (sendmsg(net->sockfd, &msg, 0) != total) {
+ WARN("can't send query to %s", net->remote_str);
+ return KNOT_NET_ESEND;
+ }
+#ifdef LIBNGHTTP2
+ // Send data over HTTPS
+ } else if (net->https.params.enable) {
+ int ret = https_send_dns_query((https_ctx_t *)&net->https, buf, buf_len);
+ if (ret != KNOT_EOK) {
+ WARN("can't send query to %s", net->remote_str);
+ return KNOT_NET_ESEND;
+ }
+#endif //LIBNGHTTP2
+ // Send data over TLS.
+ } else if (net->tls.params != NULL) {
+ int ret = tls_ctx_send((tls_ctx_t *)&net->tls, buf, buf_len);
+ if (ret != KNOT_EOK) {
+ WARN("can't send query to %s", net->remote_str);
+ return KNOT_NET_ESEND;
+ }
+ // Send data over TCP.
+ } else {
+ bool fastopen = net->flags & NET_FLAGS_FASTOPEN;
+
+ char proxy_buf[PROXYV2_HEADER_MAXLEN];
+ uint16_t pktsize = htons(buf_len); // Leading packet length bytes.
+ struct iovec iov[3] = {
+ { .iov_base = proxy_buf, .iov_len = 0 },
+ { .iov_base = &pktsize, .iov_len = sizeof(pktsize) },
+ { .iov_base = (void *)buf, .iov_len = buf_len }
+ };
+
+ struct msghdr msg = {
+ .msg_name = net->srv->ai_addr,
+ .msg_namelen = net->srv->ai_addrlen,
+ .msg_iov = &iov[1],
+ .msg_iovlen = 2
+ };
+
+ if (net->srv->ai_addr->sa_family == AF_UNIX) {
+ msg.msg_name = NULL;
+ }
+
+ if (net->proxy.src != NULL && net->proxy.src->sa_family != 0) {
+ int ret = proxyv2_write_header(proxy_buf, sizeof(proxy_buf),
+ SOCK_STREAM, net->proxy.src,
+ net->proxy.dst);
+ if (ret < 0) {
+ WARN("can't send proxied query to %s", net->remote_str);
+ return KNOT_NET_ESEND;
+ }
+ iov[0].iov_len = ret;
+ msg.msg_iov--;
+ msg.msg_iovlen++;
+ }
+
+ ssize_t total = iov[0].iov_len + iov[1].iov_len + iov[2].iov_len;
+
+ int ret = 0;
+ if (fastopen) {
+ ret = fastopen_send(net->sockfd, &msg, net->wait);
+ } else {
+ ret = sendmsg(net->sockfd, &msg, 0);
+ }
+ if (ret != total) {
+ WARN("can't send query to %s", net->remote_str);
+ return KNOT_NET_ESEND;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+int net_receive(const net_t *net, uint8_t *buf, const size_t buf_len)
+{
+ if (net == NULL || buf == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ // Initialize poll descriptor structure.
+ struct pollfd pfd = {
+ .fd = net->sockfd,
+ .events = POLLIN,
+ .revents = 0,
+ };
+
+#ifdef ENABLE_QUIC
+ // Receive data over QUIC.
+ if (net->quic.params.enable) {
+ int ret = quic_recv_dns_response((quic_ctx_t *)&net->quic, buf,
+ buf_len, net->srv);
+ if (ret < 0) {
+ WARN("can't receive reply from %s", net->remote_str);
+ return KNOT_NET_ERECV;
+ }
+ return ret;
+ } else
+#endif
+ // Receive data over UDP.
+ if (net->socktype == SOCK_DGRAM) {
+ struct sockaddr_storage from;
+ memset(&from, '\0', sizeof(from));
+
+ // Receive replies unless correct reply or timeout.
+ while (true) {
+ socklen_t from_len = sizeof(from);
+
+ // Wait for datagram data.
+ if (poll(&pfd, 1, 1000 * net->wait) != 1) {
+ WARN("response timeout for %s",
+ net->remote_str);
+ return KNOT_NET_ETIMEOUT;
+ }
+
+ // Receive whole UDP datagram.
+ ssize_t ret = recvfrom(net->sockfd, buf, buf_len, 0,
+ (struct sockaddr *)&from, &from_len);
+ if (ret <= 0) {
+ WARN("can't receive reply from %s",
+ net->remote_str);
+ return KNOT_NET_ERECV;
+ }
+
+ // Compare reply address with the remote one.
+ if (from_len > sizeof(from) ||
+ memcmp(&from, net->srv->ai_addr, from_len) != 0) {
+ char *src = NULL;
+ get_addr_str(&from, get_protocol(net), &src);
+ WARN("unexpected reply source %s", src);
+ free(src);
+ continue;
+ }
+
+ return ret;
+ }
+#ifdef LIBNGHTTP2
+ // Receive data over HTTPS.
+ } else if (net->https.params.enable) {
+ int ret = https_recv_dns_response((https_ctx_t *)&net->https, buf, buf_len);
+ if (ret < 0) {
+ WARN("can't receive reply from %s", net->remote_str);
+ return KNOT_NET_ERECV;
+ }
+ return ret;
+#endif //LIBNGHTTP2
+ // Receive data over TLS.
+ } else if (net->tls.params != NULL) {
+ int ret = tls_ctx_receive((tls_ctx_t *)&net->tls, buf, buf_len);
+ if (ret < 0) {
+ WARN("can't receive reply from %s", net->remote_str);
+ return KNOT_NET_ERECV;
+ }
+ return ret;
+ // Receive data over TCP.
+ } else {
+ uint32_t total = 0;
+
+ uint16_t msg_len = 0;
+ // Receive TCP message header.
+ while (total < sizeof(msg_len)) {
+ if (poll(&pfd, 1, 1000 * net->wait) != 1) {
+ WARN("response timeout for %s",
+ net->remote_str);
+ return KNOT_NET_ETIMEOUT;
+ }
+
+ // Receive piece of message.
+ ssize_t ret = recv(net->sockfd, (uint8_t *)&msg_len + total,
+ sizeof(msg_len) - total, 0);
+ if (ret <= 0) {
+ WARN("can't receive reply from %s",
+ net->remote_str);
+ return KNOT_NET_ERECV;
+ }
+ total += ret;
+ }
+
+ // Convert number to host format.
+ msg_len = ntohs(msg_len);
+ if (msg_len > buf_len) {
+ return KNOT_ESPACE;
+ }
+
+ total = 0;
+
+ // Receive whole answer message by parts.
+ while (total < msg_len) {
+ if (poll(&pfd, 1, 1000 * net->wait) != 1) {
+ WARN("response timeout for %s",
+ net->remote_str);
+ return KNOT_NET_ETIMEOUT;
+ }
+
+ // Receive piece of message.
+ ssize_t ret = recv(net->sockfd, buf + total, msg_len - total, 0);
+ if (ret <= 0) {
+ WARN("can't receive reply from %s",
+ net->remote_str);
+ return KNOT_NET_ERECV;
+ }
+ total += ret;
+ }
+
+ return total;
+ }
+
+ return KNOT_NET_ERECV;
+}
+
+void net_close(net_t *net)
+{
+ if (net == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+#ifdef ENABLE_QUIC
+ if (net->quic.params.enable) {
+ quic_ctx_close(&net->quic);
+ }
+#endif
+ tls_ctx_close(&net->tls);
+ close(net->sockfd);
+ net->sockfd = -1;
+}
+
+void net_clean(net_t *net)
+{
+ if (net == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ free(net->local_str);
+ free(net->remote_str);
+ net->local_str = NULL;
+ net->remote_str = NULL;
+
+ if (net->local_info != NULL) {
+ if (net->local == NULL) {
+ free(net->local_info);
+ } else {
+ freeaddrinfo(net->local_info);
+ }
+ net->local_info = NULL;
+ }
+
+ if (net->remote_info != NULL) {
+ if (net->remote_info->ai_addr->sa_family == AF_UNIX) {
+ free(net->remote_info->ai_addr);
+ free(net->remote_info);
+ } else {
+ freeaddrinfo(net->remote_info);
+ }
+ net->remote_info = NULL;
+ }
+
+#ifdef LIBNGHTTP2
+ https_ctx_deinit(&net->https);
+#endif
+#ifdef ENABLE_QUIC
+ quic_ctx_deinit(&net->quic);
+#endif
+ tls_ctx_deinit(&net->tls);
+}
diff --git a/src/utils/common/netio.h b/src/utils/common/netio.h
new file mode 100644
index 0000000..772784d
--- /dev/null
+++ b/src/utils/common/netio.h
@@ -0,0 +1,253 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <netdb.h>
+#include <stdint.h>
+#include <sys/socket.h>
+
+#include "libknot/probe/data.h"
+#include "utils/common/https.h"
+#include "utils/common/params.h"
+#include "utils/common/quic.h"
+#include "utils/common/tls.h"
+
+/*! \brief Structure containing server information. */
+typedef struct {
+ /*! List node (for list container). */
+ node_t n;
+ /*! Name or address of the server. */
+ char *name;
+ /*! Name or number of the service. */
+ char *service;
+} srv_info_t;
+
+typedef enum {
+ NET_FLAGS_NONE = 0,
+ NET_FLAGS_FASTOPEN = 1 << 0,
+} net_flags_t;
+
+typedef struct {
+ /*! Socket descriptor. */
+ int sockfd;
+
+ /*! IP protocol type. */
+ int iptype;
+ /*! Socket type. */
+ int socktype;
+ /*! Timeout for all network operations. */
+ int wait;
+ /*! Connection flags. */
+ net_flags_t flags;
+
+ /*! Local interface parameters. */
+ const srv_info_t *local;
+ /*! Remote server parameters. */
+ const srv_info_t *remote;
+
+ /*! Local description string (used for logging). */
+ char *local_str;
+ /*! Remote description string (used for logging). */
+ char *remote_str;
+
+ /*! Output from getaddrinfo for remote server. If the server is
+ * specified using domain name, this structure may contain more
+ * results.
+ */
+ struct addrinfo *remote_info;
+ /*! Currently used result from remote_info. */
+ struct addrinfo *srv;
+ /*! Output from getaddrinfo for local address. Only first result is
+ * used.
+ */
+ struct addrinfo *local_info;
+
+ /*! TLS context. */
+ tls_ctx_t tls;
+#ifdef LIBNGHTTP2
+ /*! HTTPS context. */
+ https_ctx_t https;
+#endif
+#ifdef ENABLE_QUIC
+ /*! QUIC context. */
+ quic_ctx_t quic;
+#endif
+ struct {
+ const struct sockaddr *src;
+ const struct sockaddr *dst;
+ } proxy;
+} net_t;
+
+/*!
+ * \brief Creates and fills server structure.
+ *
+ * \param name Address or host name.
+ * \param service Port number or service name.
+ *
+ * \retval server if success.
+ * \retval NULL if error.
+ */
+srv_info_t *srv_info_create(const char *name, const char *service);
+
+/*!
+ * \brief Destroys server structure.
+ *
+ * \param server Server structure to destroy.
+ */
+void srv_info_free(srv_info_t *server);
+
+/*!
+ * \brief Translates enum IP version type to int version.
+ *
+ * \param ip IP version to convert.
+ * \param server Server structure.
+ *
+ * \retval AF_INET, AF_INET6, AF_UNIX, or AF_UNSPEC.
+ */
+int get_iptype(const ip_t ip, const srv_info_t *server);
+
+/*!
+ * \brief Translates enum IP protocol type to int version in context to the
+ * current DNS query type.
+ *
+ * \param proto IP protocol type to convert.
+ * \param type DNS query type number.
+ *
+ * \retval SOCK_STREAM or SOCK_DGRAM.
+ */
+int get_socktype(const protocol_t proto, const uint16_t type);
+
+/*!
+ * \brief Translates int socket type to the common string one.
+ *
+ * \param socktype Socket type (SOCK_STREAM or SOCK_DGRAM).
+ *
+ * \retval "TCP" or "UDP".
+ */
+const char *get_sockname(const int socktype);
+
+/*!
+ * \brief Translates protocol type to a common string.
+ *
+ * \param ss Socket address storage.
+ * \param socktype Protocol type.
+ * \param dst Output string.
+ */
+void get_addr_str(const struct sockaddr_storage *ss,
+ const knot_probe_proto_t socktype,
+ char **dst);
+
+/*!
+ * \brief Initializes network structure and resolves local and remote addresses.
+ *
+ * \param local Local address and service description.
+ * \param remote Remote address and service description.
+ * \param iptype IP version.
+ * \param socktype Socket type.
+ * \param wait Network timeout interval.
+ * \param flags Connection flags.
+ * \param proxy_src Proxy source address.
+ * \param proxy_dst Proxy destination address.
+ * \param net Network structure to initialize.
+ *
+ * \retval KNOT_EOK if success.
+ * \retval errcode if error.
+ */
+int net_init(const srv_info_t *local,
+ const srv_info_t *remote,
+ const int iptype,
+ const int socktype,
+ const int wait,
+ const net_flags_t flags,
+ const struct sockaddr *proxy_src,
+ const struct sockaddr *proxy_dst,
+ net_t *net);
+
+/*!
+ * \brief (Re)initializes crypto protocols in network structure.
+ *
+ * \param net Network structure to initialize.
+ * \param tls_params TLS parameters.
+ * \param https_params HTTPS parameters.
+ * \param quic_params QUIC parameters.
+ *
+ * \retval KNOT_EOK if success.
+ * \retval errcode if error.
+ */
+int net_init_crypto(net_t *net,
+ const tls_params_t *tls_params,
+ const https_params_t *https_params,
+ const quic_params_t *quic_params);
+
+/*!
+ * \brief Creates socket and connects (if TCP) to remote address specified
+ * by net->srv.
+ *
+ * \param net Connection parameters.
+ *
+ * \retval KNOT_EOK if success.
+ * \retval errcode if error.
+ */
+int net_connect(net_t *net);
+
+/*!
+ * \brief Fills in local address information.
+ *
+ * \param net Connection parameters.
+ *
+ * \retval KNOT_EOK if success.
+ * \retval errcode if error.
+ */
+int net_set_local_info(net_t *net);
+
+/*!
+ * \brief Sends data to connected remote server.
+ *
+ * \param net Connection parameters.
+ * \param buf Data to send.
+ * \param buf_len Length of the data to send.
+ *
+ * \retval KNOT_EOK if success.
+ * \retval errcode if error.
+ */
+int net_send(const net_t *net, const uint8_t *buf, const size_t buf_len);
+
+/*!
+ * \brief Receives data from connected remote server.
+ *
+ * \param net Connection parameters.
+ * \param buf Buffer for incoming data.
+ * \param buf_len Length of the buffer.
+ *
+ * \retval >=0 length of successfully received data.
+ * \retval errcode if error.
+ */
+int net_receive(const net_t *net, uint8_t *buf, const size_t buf_len);
+
+/*!
+ * \brief Closes current network connection.
+ *
+ * \param net Connection parameters.
+ */
+void net_close(net_t *net);
+
+/*!
+ * \brief Cleans up network structure.
+ *
+ * \param net Connection parameters.
+ */
+void net_clean(net_t *net);
diff --git a/src/utils/common/params.c b/src/utils/common/params.c
new file mode 100644
index 0000000..4db4b9e
--- /dev/null
+++ b/src/utils/common/params.c
@@ -0,0 +1,343 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <arpa/inet.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+
+#ifdef LIBIDN
+#include LIBIDN_HEADER
+#endif
+
+#include "utils/common/params.h"
+#include "utils/common/msg.h"
+#include "utils/common/resolv.h"
+#include "utils/common/token.h"
+#include "libknot/libknot.h"
+#include "contrib/macros.h"
+#include "contrib/mempattern.h"
+#include "contrib/openbsd/strlcpy.h"
+#include "contrib/strtonum.h"
+
+#define IPV4_REVERSE_DOMAIN "in-addr.arpa."
+#define IPV6_REVERSE_DOMAIN "ip6.arpa."
+
+char *name_from_idn(const char *idn_name) {
+#ifdef LIBIDN
+ char *name = NULL;
+
+ int rc = idna_to_ascii_lz(idn_name, &name, 0);
+ if (rc != IDNA_SUCCESS) {
+ ERR("IDNA (%s)", idna_strerror(rc));
+ return NULL;
+ }
+
+ return name;
+#endif
+ return strdup(idn_name);
+}
+
+void name_to_idn(char **name) {
+#ifdef LIBIDN
+ char *idn_name = NULL;
+
+ int rc = idna_to_unicode_8zlz(*name, &idn_name, 0);
+ if (rc != IDNA_SUCCESS) {
+ return;
+ }
+
+ free(*name);
+ *name = idn_name;
+#endif
+ return;
+}
+
+/*!
+ * \brief Checks if string is a prefix of reference string.
+ *
+ * \param pref Prefix string.
+ * \param pref_len Prefix length.
+ * \param str Reference string (must have trailing zero).
+ *
+ * \retval -1 \a pref is not a prefix of \a str.
+ * \retval 0<= number of chars after prefix \a pref in \a str.
+ */
+static int cmp_prefix(const char *pref, const size_t pref_len,
+ const char *str)
+{
+ size_t i = 0;
+ while (1) {
+ // Different characters => NOT prefix.
+ if (pref[i] != str[i]) {
+ return -1;
+ }
+
+ i++;
+
+ // Pref IS a prefix of pref.
+ if (i == pref_len) {
+ size_t rest = 0;
+ while (str[i + rest] != '\0') {
+ rest++;
+ }
+ return rest;
+ // Pref is longer then ref => NOT prefix.
+ } else if (str[i] == '\0') {
+ return -1;
+ }
+ }
+}
+
+int best_param(const char *str, const size_t str_len, const param_t *tbl,
+ bool *unique)
+{
+ if (str == NULL || str_len == 0 || tbl == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ int best_pos = -1;
+ int best_match = INT_MAX;
+ size_t matches = 0;
+ for (int i = 0; tbl[i].name != NULL; i++) {
+ int ret = cmp_prefix(str, str_len, tbl[i].name);
+ switch (ret) {
+ case -1:
+ continue;
+ case 0:
+ *unique = true;
+ return i;
+ default:
+ if (ret < best_match) {
+ best_pos = i;
+ best_match = ret;
+ }
+ matches++;
+ }
+ }
+
+ switch (matches) {
+ case 0:
+ return KNOT_ENOTSUP;
+ case 1:
+ *unique = true;
+ return best_pos;
+ default:
+ *unique = false;
+ return best_pos;
+ }
+}
+
+char *get_reverse_name(const char *name)
+{
+ struct in_addr addr4;
+ struct in6_addr addr6;
+ int ret;
+ char buf[128] = "\0";
+
+ if (name == NULL) {
+ DBG_NULL;
+ return NULL;
+ }
+
+ // Check name for IPv4 address, IPv6 address or other.
+ if (inet_pton(AF_INET, name, &addr4) == 1) {
+ uint32_t num = ntohl(addr4.s_addr);
+
+ // Create IPv4 reverse FQD name.
+ ret = snprintf(buf, sizeof(buf), "%u.%u.%u.%u.%s",
+ (num >> 0) & 0xFF, (num >> 8) & 0xFF,
+ (num >> 16) & 0xFF, (num >> 24) & 0xFF,
+ IPV4_REVERSE_DOMAIN);
+ if (ret < 0 || (size_t)ret >= sizeof(buf)) {
+ return NULL;
+ }
+
+ return strdup(buf);
+ } else if (inet_pton(AF_INET6, name, &addr6) == 1) {
+ char *pos = buf;
+ size_t len = sizeof(buf);
+ uint8_t left, right;
+
+ // Create IPv6 reverse name.
+ for (int i = 15; i >= 0; i--) {
+ left = ((addr6.s6_addr)[i] & 0xF0) >> 4;
+ right = (addr6.s6_addr)[i] & 0x0F;
+
+ ret = snprintf(pos, len, "%x.%x.", right, left);
+ if (ret < 0 || (size_t)ret >= len) {
+ return NULL;
+ }
+
+ pos += ret;
+ len -= ret;
+ }
+
+ // Add IPv6 reverse domain.
+ ret = snprintf(pos, len, "%s", IPV6_REVERSE_DOMAIN);
+ if (ret < 0 || (size_t)ret >= len) {
+ return NULL;
+ }
+
+ return strdup(buf);
+ } else {
+ return NULL;
+ }
+}
+
+char *get_fqd_name(const char *name)
+{
+ char *fqd_name = NULL;
+
+ if (name == NULL) {
+ DBG_NULL;
+ return NULL;
+ }
+
+ size_t name_len = strlen(name);
+
+ // If the name is FQDN, make a copy.
+ if (name[name_len - 1] == '.') {
+ fqd_name = strdup(name);
+ // Else make a copy and append a trailing dot.
+ } else {
+ size_t fqd_name_size = name_len + 2;
+ fqd_name = malloc(fqd_name_size);
+ if (fqd_name != NULL) {
+ strlcpy(fqd_name, name, fqd_name_size);
+ fqd_name[name_len] = '.';
+ fqd_name[name_len + 1] = 0;
+ }
+ }
+
+ return fqd_name;
+}
+
+int params_parse_class(const char *value, uint16_t *rclass)
+{
+ if (value == NULL || rclass == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ if (knot_rrclass_from_string(value, rclass) == 0) {
+ return KNOT_EOK;
+ } else {
+ return KNOT_EINVAL;
+ }
+}
+
+int params_parse_type(const char *value, uint16_t *rtype, int64_t *serial,
+ bool *notify)
+{
+ if (value == NULL || rtype == NULL || serial == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ // Find and parse type name.
+ size_t param_pos = strcspn(value, "=");
+ char *type_char = strndup(value, param_pos);
+
+ if (knot_rrtype_from_string(type_char, rtype) != 0) {
+ size_t cmp_len = MAX(strlen("NOTIFY"), param_pos);
+ if (strncasecmp(type_char, "NOTIFY", cmp_len) == 0) {
+ *rtype = KNOT_RRTYPE_SOA;
+ *notify = true;
+ } else {
+ free(type_char);
+ return KNOT_EINVAL;
+ }
+ } else {
+ *notify = false;
+ }
+
+ free(type_char);
+
+ // Parse additional parameter.
+ if (param_pos == strlen(value)) {
+ // IXFR requires serial parameter.
+ if (*rtype == KNOT_RRTYPE_IXFR) {
+ DBG("SOA serial is required for IXFR query");
+ return KNOT_EINVAL;
+ } else {
+ *serial = -1;
+ }
+ } else {
+ // Additional parameter is accepted for IXFR or NOTIFY.
+ if (*rtype == KNOT_RRTYPE_IXFR || *notify) {
+ const char *param_str = value + 1 + param_pos;
+ char *end;
+
+ // Convert string to serial.
+ unsigned long long num = strtoull(param_str, &end, 10);
+
+ // Check for bad serial string.
+ if (end == param_str || *end != '\0' || num > UINT32_MAX) {
+ DBG("bad SOA serial '%s'", param_str);
+ return KNOT_EINVAL;
+ }
+
+ *serial = num;
+ } else {
+ DBG("unsupported parameter '%s'", value);
+ return KNOT_EINVAL;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+int params_parse_server(const char *value, list_t *servers, const char *def_port)
+{
+ if (value == NULL || servers == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ // Add specified nameserver.
+ srv_info_t *server = parse_nameserver(value, def_port);
+ if (server == NULL) {
+ return KNOT_EINVAL;
+ }
+ add_tail(servers, (node_t *)server);
+
+ return KNOT_EOK;
+}
+
+int params_parse_wait(const char *value, int32_t *dst)
+{
+ if (value == NULL || dst == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ uint32_t num = 0;
+ int ret = str_to_u32(value, &num);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (num < 1 || num > INT32_MAX / 1000) {
+ num = INT32_MAX / 1000;
+ }
+
+ *dst = num;
+
+ return KNOT_EOK;
+}
diff --git a/src/utils/common/params.h b/src/utils/common/params.h
new file mode 100644
index 0000000..8b7565e
--- /dev/null
+++ b/src/utils/common/params.h
@@ -0,0 +1,170 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <limits.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "libknot/libknot.h"
+#include "contrib/ucw/lists.h"
+
+#define DEFAULT_IPV4_NAME "127.0.0.1"
+#define DEFAULT_IPV6_NAME "::1"
+#define DEFAULT_DNS_PORT "53"
+#define DEFAULT_DNS_HTTPS_PORT "443"
+#define DEFAULT_DNS_QUIC_PORT "853"
+#define DEFAULT_DNS_TLS_PORT "853"
+#define DEFAULT_UDP_SIZE 512
+#define DEFAULT_EDNS_SIZE 4096
+#define MAX_PACKET_SIZE 65535
+
+#define SEP_CHARS "\n\t "
+
+/*! \brief Variants of IP protocol. */
+typedef enum {
+ IP_ALL,
+ IP_4,
+ IP_6
+} ip_t;
+
+/*! \brief Variants of transport protocol. */
+typedef enum {
+ PROTO_ALL,
+ PROTO_TCP,
+ PROTO_UDP
+} protocol_t;
+
+/*! \brief Variants of output type. */
+typedef enum {
+ /*!< Verbose output (same for host and dig). */
+ FORMAT_FULL,
+ /*!< Short dig output. */
+ FORMAT_DIG,
+ /*!< Brief host output. */
+ FORMAT_HOST,
+ /*!< Brief nsupdate output. */
+ FORMAT_NSUPDATE,
+ /*!< Machine readable JSON format (RFC 8427). */
+ FORMAT_JSON
+} format_t;
+
+/*! \brief Text output settings. */
+typedef struct {
+ /*!< Output format. */
+ format_t format;
+
+ /*!< Style of rrset dump. */
+ knot_dump_style_t style;
+
+ /*!< Show query packet. */
+ bool show_query;
+ /*!< Show header info. */
+ bool show_header;
+ /*!< Show section name. */
+ bool show_section;
+ /*!< Show EDNS pseudosection. */
+ bool show_edns;
+ /*!< Show unknown EDNS options in printable format. */
+ bool show_edns_opt_text;
+ /*!< Show QUERY/ZONE section. */
+ bool show_question;
+ /*!< Show ANSWER/PREREQ section. */
+ bool show_answer;
+ /*!< Show UPDATE/AUTHORITY section. */
+ bool show_authority;
+ /*!< Show ADDITIONAL section. */
+ bool show_additional;
+ /*!< Show TSIG pseudosection. */
+ bool show_tsig;
+ /*!< Show footer info. */
+ bool show_footer;
+ /*!< Display EDNS in Presentation format. */
+ bool present_edns;
+
+ /*!< KHOST - Hide CNAME record in answer (duplicity reduction). */
+ bool hide_cname;
+} style_t;
+
+/*! \brief Parameter handler. */
+typedef int (*param_handle_f)(const char *arg, void *params);
+
+/*! \brief Parameter argument type. */
+typedef enum {
+ ARG_NONE,
+ ARG_REQUIRED,
+ ARG_OPTIONAL
+} arg_t;
+
+/*! \brief Parameter specification. */
+typedef struct {
+ const char *name;
+ arg_t arg;
+ param_handle_f handler;
+} param_t;
+
+inline static void print_version(const char *program_name)
+{
+ printf("%s (Knot DNS), version %s\n", program_name, PACKAGE_VERSION);
+}
+
+/*!
+ * \brief Transforms localized IDN string to ASCII punycode.
+ *
+ * \param idn_name IDN name to transform.
+ *
+ * \retval NULL if transformation fails.
+ * \retval string if ok.
+ */
+char *name_from_idn(const char *idn_name);
+
+/*!
+ * \brief Transforms ASCII punycode to localized IDN string.
+ *
+ * If an error occurs or IDN support is missing, this function does nothing.
+ *
+ * \param name ASCII name to transform and replace with IDN name.
+ */
+void name_to_idn(char **name);
+
+/*!
+ * \brief Find the best parameter match in table based on prefix equality.
+ *
+ * \param str Parameter name to look up.
+ * \param str_len Parameter name length.
+ * \param tbl Parameter table.
+ * \param unique Indication if output is unique result.
+ *
+ * \retval >=0 looked up parameter position in \a tbl.
+ * \retval err if error.
+ */
+int best_param(const char *str, const size_t str_len, const param_t *tbl,
+ bool *unique);
+
+char *get_reverse_name(const char *name);
+
+char *get_fqd_name(const char *name);
+
+int params_parse_class(const char *value, uint16_t *rclass);
+
+int params_parse_type(const char *value, uint16_t *rtype, int64_t *serial,
+ bool *notify);
+
+int params_parse_server(const char *value, list_t *servers, const char *def_port);
+
+int params_parse_wait(const char *value, int32_t *dst);
diff --git a/src/utils/common/quic.c b/src/utils/common/quic.c
new file mode 100644
index 0000000..55e5408
--- /dev/null
+++ b/src/utils/common/quic.c
@@ -0,0 +1,813 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+
+#include "contrib/net.h"
+#include "libknot/errcode.h"
+#include "utils/common/quic.h"
+#include "utils/common/msg.h"
+
+int quic_params_copy(quic_params_t *dst, const quic_params_t *src)
+{
+ if (dst == NULL || src == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ dst->enable = src->enable;
+
+ return KNOT_EOK;
+}
+
+void quic_params_clean(quic_params_t *params)
+{
+ if (params == NULL) {
+ return;
+ }
+
+ params->enable = false;
+}
+
+#ifdef ENABLE_QUIC
+
+#include <assert.h>
+#include <poll.h>
+#include <gnutls/crypto.h>
+
+#include <ngtcp2/ngtcp2_crypto.h>
+#include <ngtcp2/ngtcp2_crypto_gnutls.h>
+
+#include "contrib/macros.h"
+#include "libdnssec/error.h"
+#include "libdnssec/random.h"
+#include "libknot/xdp/tcp_iobuf.h"
+#include "utils/common/params.h"
+
+#define quic_get_encryption_level(level) ngtcp2_crypto_gnutls_from_gnutls_record_encryption_level(level)
+#define quic_send(ctx, sockfd, family) quic_send_data(ctx, sockfd, family, NULL, 0)
+#define set_application_error(ctx, error_code, reason, reason_len) \
+ ngtcp2_ccerr_set_application_error(&(ctx)->last_err, \
+ error_code, reason, reason_len)
+#define set_transport_error(ctx, error_code, reason, reason_len) \
+ ngtcp2_ccerr_set_transport_error(&(ctx)->last_err, \
+ error_code, reason, reason_len)
+
+const gnutls_datum_t doq_alpn = {
+ (unsigned char *)"doq", 3
+};
+
+static int recv_stream_data_cb(ngtcp2_conn *conn, uint32_t flags,
+ int64_t stream_id, uint64_t offset, const uint8_t *data,
+ size_t datalen, void *user_data, void *stream_user_data)
+{
+ (void)conn;
+ (void)flags;
+ (void)offset;
+ (void)stream_user_data;
+
+ quic_ctx_t *ctx = (quic_ctx_t *)user_data;
+
+ if (stream_id != ctx->stream.id) {
+ const uint8_t msg[] = "Unknown stream";
+ set_application_error(ctx, DOQ_PROTOCOL_ERROR, msg, sizeof(msg) - 1);
+ return NGTCP2_ERR_CALLBACK_FAILURE;
+ }
+
+ struct iovec in = {
+ .iov_base = (uint8_t *)data,
+ .iov_len = datalen
+ };
+
+ int ret = knot_tcp_inbufs_upd(&ctx->stream.in_buffer, in, true,
+ &ctx->stream.in_parsed,
+ &ctx->stream.in_parsed_total);
+ if (ret != KNOT_EOK) {
+ const uint8_t msg[] = "Malformed payload";
+ set_application_error(ctx, DOQ_PROTOCOL_ERROR, msg, sizeof(msg) - 1);
+ return NGTCP2_ERR_CALLBACK_FAILURE;
+ }
+
+ ctx->stream.in_parsed_it = 0;
+ return 0;
+}
+
+static int stream_open_cb(ngtcp2_conn *conn, int64_t stream_id,
+ void *user_data)
+{
+ (void)conn;
+
+ quic_ctx_t *ctx = (quic_ctx_t *)user_data;
+ set_application_error(ctx, DOQ_PROTOCOL_ERROR, NULL, 0);
+ return NGTCP2_ERR_CALLBACK_FAILURE;
+}
+
+static int acked_stream_data_offset_cb(ngtcp2_conn *conn, int64_t stream_id,
+ uint64_t offset, uint64_t datalen, void *user_data,
+ void *stream_user_data)
+{
+ (void)conn;
+ (void)offset;
+ (void)stream_user_data;
+
+ quic_ctx_t *ctx = (quic_ctx_t *)user_data;
+ if (ctx->stream.id == stream_id) {
+ ctx->stream.out_ack -= datalen;
+ }
+ return KNOT_EOK;
+}
+
+static int stream_close_cb(ngtcp2_conn *conn, uint32_t flags,
+ int64_t stream_id, uint64_t app_error_code, void *user_data,
+ void *stream_user_data)
+{
+ (void)conn;
+ (void)flags;
+ (void)app_error_code;
+ (void)stream_user_data;
+
+ quic_ctx_t *ctx = (quic_ctx_t *)user_data;
+ if (ctx && stream_id == ctx->stream.id) {
+ ctx->stream.id = -1;
+ }
+ return KNOT_EOK;
+}
+
+static int quic_open_bidi_stream(quic_ctx_t *ctx)
+{
+ if (ctx->stream.id >= 0) {
+ return KNOT_EOK;
+ }
+
+ int ret = ngtcp2_conn_open_bidi_stream(ctx->conn, &ctx->stream.id, NULL);
+ if (ret) {
+ return KNOT_ERROR;
+ }
+ return KNOT_EOK;
+}
+
+static void rand_cb(uint8_t *dest, size_t destlen,
+ const ngtcp2_rand_ctx *rand_ctx)
+{
+ (void)rand_ctx;
+
+ dnssec_random_buffer(dest, destlen);
+}
+
+static int get_new_connection_id_cb(ngtcp2_conn *conn, ngtcp2_cid *cid,
+ uint8_t *token, size_t cidlen, void *user_data)
+{
+ (void)conn;
+
+ quic_ctx_t *ctx = (quic_ctx_t *)user_data;
+
+ if (dnssec_random_buffer(cid->data, cidlen) != DNSSEC_EOK) {
+ return NGTCP2_ERR_CALLBACK_FAILURE;
+ }
+ cid->datalen = cidlen;
+
+ if (ngtcp2_crypto_generate_stateless_reset_token(token, ctx->secret,
+ sizeof(ctx->secret), cid) != 0)
+ {
+ return NGTCP2_ERR_CALLBACK_FAILURE;
+ }
+
+ return 0;
+}
+
+static int stream_reset_cb(ngtcp2_conn *conn, int64_t stream_id,
+ uint64_t final_size, uint64_t app_error_code, void *user_data,
+ void *stream_user_data)
+{
+ quic_ctx_t *ctx = (quic_ctx_t *)user_data;
+ if (ctx->stream.id == stream_id) {
+ set_transport_error(ctx, NGTCP2_PROTOCOL_VIOLATION, NULL, 0);
+ quic_ctx_close(ctx);
+ return NGTCP2_ERR_CALLBACK_FAILURE;
+ }
+
+ return 0;
+}
+
+static int handshake_confirmed_cb(ngtcp2_conn *conn, void *user_data)
+{
+ (void)conn;
+
+ quic_ctx_t *ctx = (quic_ctx_t *)user_data;
+ ctx->state = CONNECTED;
+ return 0;
+}
+
+static int recv_rx_key_cb(ngtcp2_conn *conn, ngtcp2_encryption_level level,
+ void *user_data)
+{
+ quic_ctx_t *ctx = user_data;
+ if (level == NGTCP2_ENCRYPTION_LEVEL_1RTT) {
+ ctx->state = CONNECTED;
+ }
+
+ return 0;
+}
+
+static const ngtcp2_callbacks quic_client_callbacks = {
+ ngtcp2_crypto_client_initial_cb,
+ NULL, /* recv_client_initial */
+ ngtcp2_crypto_recv_crypto_data_cb,
+ NULL, /* handshake_completed */
+ NULL, /* recv_version_negotiation */
+ ngtcp2_crypto_encrypt_cb,
+ ngtcp2_crypto_decrypt_cb,
+ ngtcp2_crypto_hp_mask_cb,
+ recv_stream_data_cb,
+ acked_stream_data_offset_cb,
+ stream_open_cb,
+ stream_close_cb,
+ NULL, /* recv_stateless_reset */
+ ngtcp2_crypto_recv_retry_cb,
+ NULL, /* extend_max_bidi_streams */
+ NULL, /* extend_max_local_streams_uni */
+ rand_cb,
+ get_new_connection_id_cb,
+ NULL, /* remove_connection_id */
+ ngtcp2_crypto_update_key_cb,
+ NULL, /* path_validation */
+ NULL, /* select_preferred_address */
+ stream_reset_cb,
+ NULL, /* extend_max_remote_streams_bidi */
+ NULL, /* extend_max_remote_streams_uni */
+ NULL, /* extend_max_stream_data */
+ NULL, /* dcid_status */
+ handshake_confirmed_cb,
+ NULL, /* recv_new_token */
+ ngtcp2_crypto_delete_crypto_aead_ctx_cb,
+ ngtcp2_crypto_delete_crypto_cipher_ctx_cb,
+ NULL, /* recv_datagram */
+ NULL, /* ack_datagram */
+ NULL, /* lost_datagram */
+ ngtcp2_crypto_get_path_challenge_data_cb,
+ NULL, /* stream_stop_sending */
+ ngtcp2_crypto_version_negotiation_cb,
+ recv_rx_key_cb,
+ NULL /* recv_tx_key */
+};
+
+static int hook_func(gnutls_session_t session, unsigned int htype,
+ unsigned when, unsigned int incoming, const gnutls_datum_t *msg)
+{
+ (void)session;
+ (void)htype;
+ (void)when;
+ (void)incoming;
+ (void)msg;
+
+ return GNUTLS_E_SUCCESS;
+}
+
+static int quic_send_data(quic_ctx_t *ctx, int sockfd, int family,
+ ngtcp2_vec *datav, size_t datavlen)
+{
+ uint8_t enc_buf[MAX_PACKET_SIZE];
+ struct iovec msg_iov = {
+ .iov_base = enc_buf,
+ .iov_len = 0
+ };
+ struct msghdr msg = {
+ .msg_iov = &msg_iov,
+ .msg_iovlen = 1
+ };
+ uint64_t ts = quic_timestamp();
+
+ uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_NONE;
+ int64_t stream_id = -1;
+ if (datavlen > 0) {
+ flags = NGTCP2_WRITE_STREAM_FLAG_FIN;
+ stream_id = ctx->stream.id;
+ }
+ ngtcp2_ssize send_datalen = 0;
+ ngtcp2_ssize nwrite = ngtcp2_conn_writev_stream(ctx->conn,
+ (ngtcp2_path *)ngtcp2_conn_get_path(ctx->conn), &ctx->pi,
+ enc_buf, sizeof(enc_buf), &send_datalen, flags, stream_id,
+ datav, datavlen, ts);
+ if (nwrite <= 0) {
+ switch(nwrite) {
+ case 0:
+ ngtcp2_conn_update_pkt_tx_time(ctx->conn, ts);
+ return KNOT_EOK;
+ case NGTCP2_ERR_WRITE_MORE:
+ assert(0);
+ return KNOT_NET_ESEND;
+ default:
+ set_transport_error(ctx,
+ ngtcp2_err_infer_quic_transport_error_code(nwrite),
+ NULL, 0);
+ if (ngtcp2_err_is_fatal(nwrite)) {
+ return KNOT_NET_ESEND;
+ } else {
+ return KNOT_EOK;
+ }
+ }
+ }
+
+ msg_iov.iov_len = (size_t)nwrite;
+
+ int ret = net_ecn_set(sockfd, family, ctx->pi.ecn);
+ if (ret != KNOT_EOK && ret != KNOT_ENOTSUP) {
+ return ret;
+ }
+
+ if (sendmsg(sockfd, &msg, 0) == -1) {
+ set_transport_error(ctx, NGTCP2_INTERNAL_ERROR, NULL, 0);
+ return KNOT_NET_ESEND;
+ }
+
+ if (send_datalen > 0) {
+ return send_datalen;
+ }
+
+ return KNOT_EOK;
+}
+
+static int quic_recv(quic_ctx_t *ctx, int sockfd)
+{
+ uint8_t enc_buf[MAX_PACKET_SIZE];
+ uint8_t msg_ctrl[CMSG_SPACE(sizeof(uint8_t))];
+ struct sockaddr_in6 from = { 0 };
+ struct iovec msg_iov = {
+ .iov_base = enc_buf,
+ .iov_len = sizeof(enc_buf)
+ };
+ struct msghdr msg = {
+ .msg_name = &from,
+ .msg_namelen = sizeof(from),
+ .msg_iov = &msg_iov,
+ .msg_iovlen = 1,
+ .msg_control = msg_ctrl,
+ .msg_controllen = sizeof(msg_ctrl),
+ .msg_flags = 0
+ };
+
+ ssize_t nwrite = recvmsg(sockfd, &msg, 0);
+ if (nwrite <= 0) {
+ return knot_map_errno();
+ }
+ ngtcp2_pkt_info *pi = &ctx->pi;
+ ctx->pi.ecn = net_cmsg_ecn(&msg);
+
+ int ret = ngtcp2_conn_read_pkt(ctx->conn,
+ ngtcp2_conn_get_path(ctx->conn),
+ pi, enc_buf, nwrite,
+ quic_timestamp());
+ if (ngtcp2_err_is_fatal(ret)) {
+ set_transport_error(ctx,
+ ngtcp2_err_infer_quic_transport_error_code(ret),
+ NULL, 0);
+ return KNOT_NET_ERECV;
+ }
+ return KNOT_EOK;
+}
+
+static int quic_respcpy(quic_ctx_t *ctx, uint8_t *buf, const size_t buf_len)
+{
+ assert(ctx && buf && buf_len > 0);
+ if (ctx->stream.in_parsed != NULL) {
+ knot_tcp_inbufs_upd_res_t *cur = ctx->stream.in_parsed;
+ struct iovec *it = &cur->inbufs[ctx->stream.in_parsed_it];
+ if (buf_len < it->iov_len) {
+ return KNOT_ENOMEM;
+ }
+ size_t len = it->iov_len;
+ memcpy(buf, it->iov_base, len);
+ if (++ctx->stream.in_parsed_it == cur->n_inbufs) {
+ ctx->stream.in_parsed_it = 0;
+ ctx->stream.in_parsed = cur->next;
+ free(cur);
+ }
+ return len;
+ }
+ return 0;
+}
+
+uint64_t quic_timestamp(void)
+{
+ struct timespec ts;
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
+ return 0;
+ }
+
+ return (uint64_t)ts.tv_sec * NGTCP2_SECONDS + (uint64_t)ts.tv_nsec;
+}
+
+int quic_generate_secret(uint8_t *buf, size_t buflen)
+{
+ assert(buf != NULL && buflen > 0 && buflen <= 32);
+ uint8_t rand[16], hash[32];
+ int ret = dnssec_random_buffer(rand, sizeof(rand));
+ if (ret != DNSSEC_EOK) {
+ return KNOT_ERROR;
+ }
+ ret = gnutls_hash_fast(GNUTLS_DIG_SHA256, rand, sizeof(rand), hash);
+ if (ret != 0) {
+ return KNOT_ERROR;
+ }
+ memcpy(buf, hash, buflen);
+ return KNOT_EOK;
+}
+
+static int verify_certificate(gnutls_session_t session)
+{
+ quic_ctx_t *ctx = gnutls_session_get_ptr(session);
+ return tls_certificate_verification(ctx->tls);
+}
+
+static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref)
+{
+ return ((quic_ctx_t *)conn_ref->user_data)->conn;
+}
+
+int quic_ctx_init(quic_ctx_t *ctx, tls_ctx_t *tls_ctx, const quic_params_t *params)
+{
+ if (ctx == NULL || tls_ctx == NULL || params == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ ctx->conn_ref = (ngtcp2_crypto_conn_ref) {
+ .get_conn = get_conn,
+ .user_data = ctx
+ };
+ ctx->params = *params;
+ ctx->tls = tls_ctx;
+ ctx->state = CLOSED;
+ ctx->stream.id = -1;
+ set_application_error(ctx, DOQ_NO_ERROR, NULL, 0);
+ if (quic_generate_secret(ctx->secret, sizeof(ctx->secret)) != KNOT_EOK) {
+ return KNOT_ENOMEM;
+ }
+
+ gnutls_certificate_set_verify_function(
+ tls_ctx->credentials,
+ verify_certificate);
+
+ return KNOT_EOK;
+}
+
+static int get_expiry(ngtcp2_conn *ctx)
+{
+ ngtcp2_tstamp now = quic_timestamp();
+ ngtcp2_tstamp expiry = ngtcp2_conn_get_expiry(ctx);
+ if (expiry == UINT64_MAX) {
+ return -1;
+ } else if (expiry < now) {
+ return 0;
+ }
+ /* ceil((expiry - now) / NGTCP2_MILLISECONDS) */
+ return (expiry - now + NGTCP2_MILLISECONDS - 1) / NGTCP2_MILLISECONDS;
+}
+
+int quic_ctx_connect(quic_ctx_t *ctx, int sockfd, struct addrinfo *dst_addr)
+{
+ if (connect(sockfd, (const struct sockaddr *)(dst_addr->ai_addr),
+ dst_addr->ai_addrlen) != 0)
+ {
+ return knot_map_errno();
+ }
+
+ ngtcp2_cid dcid, scid;
+ scid.datalen = NGTCP2_MAX_CIDLEN;
+ int ret = dnssec_random_buffer(scid.data, scid.datalen);
+ if (ret != DNSSEC_EOK) {
+ return ret;
+ }
+ dcid.datalen = 18;
+ ret = dnssec_random_buffer(dcid.data, dcid.datalen);
+ if (ret != DNSSEC_EOK) {
+ return ret;
+ }
+
+ ngtcp2_settings settings;
+ ngtcp2_settings_default(&settings);
+ settings.initial_ts = quic_timestamp();
+ settings.handshake_timeout = ctx->tls->wait * NGTCP2_SECONDS;
+
+ ngtcp2_transport_params params;
+ ngtcp2_transport_params_default(&params);
+ params.initial_max_streams_uni = 0;
+ params.initial_max_streams_bidi = 0;
+ params.initial_max_stream_data_bidi_local = NGTCP2_MAX_VARINT;
+ params.initial_max_data = NGTCP2_MAX_VARINT;
+ params.max_ack_delay = 1 * NGTCP2_SECONDS;
+ params.max_idle_timeout = ctx->tls->wait * NGTCP2_SECONDS;
+
+ struct sockaddr_in6 src_addr;
+ socklen_t src_addr_len = sizeof(src_addr);
+ ret = getsockname(sockfd, (struct sockaddr *)&src_addr, &src_addr_len);
+ if (ret < 0) {
+ return knot_map_errno();
+ }
+ ngtcp2_path path = {
+ .local = {
+ .addrlen = src_addr_len,
+ .addr = (struct sockaddr *)&src_addr
+ },
+ .remote = {
+ .addrlen = sizeof(*(dst_addr->ai_addr)),
+ .addr = (struct sockaddr *)(dst_addr->ai_addr)
+ },
+ .user_data = NULL
+ };
+
+ if (ctx->conn) {
+ ngtcp2_conn_del(ctx->conn);
+ ctx->conn = NULL;
+ }
+
+ if (ngtcp2_conn_client_new(&ctx->conn, &dcid, &scid, &path,
+ NGTCP2_PROTO_VER_V1, &quic_client_callbacks,
+ &settings, &params, NULL, ctx) != 0) {
+ return KNOT_NET_ECONNECT;
+ }
+ gnutls_handshake_set_hook_function(ctx->tls->session,
+ GNUTLS_HANDSHAKE_ANY,
+ GNUTLS_HOOK_POST, hook_func);
+ ret = ngtcp2_crypto_gnutls_configure_client_session(ctx->tls->session);
+ if (ret != KNOT_EOK) {
+ return KNOT_NET_ECONNECT;
+ }
+ gnutls_session_set_ptr(ctx->tls->session, ctx);
+ ngtcp2_conn_set_tls_native_handle(ctx->conn, ctx->tls->session);
+
+ struct pollfd pfd = {
+ .fd = sockfd,
+ .events = POLLIN,
+ .revents = 0,
+ };
+ ctx->tls->sockfd = sockfd;
+
+ while (ctx->state != CONNECTED) {
+ ret = quic_send(ctx, sockfd, dst_addr->ai_family);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ int timeout = get_expiry(ctx->conn);
+ ret = poll(&pfd, 1, timeout);
+ if (ret == 0) {
+ ret = ngtcp2_conn_handle_expiry(ctx->conn, quic_timestamp());
+ } else if (ret < 0) {
+ return knot_map_errno();
+ }
+
+ ret = quic_recv(ctx, sockfd);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+static int offset_span(ngtcp2_vec **vec, size_t *veclen, size_t sub)
+{
+ ngtcp2_vec *new_vec = *vec;
+ size_t new_veclen = *veclen;
+
+ while (sub) {
+ if (new_veclen == 0) {
+ return KNOT_EINVAL;
+ }
+ size_t part = MIN(sub, new_vec->len);
+ new_vec->base += part;
+ new_vec->len -= part;
+ sub -= part;
+ const int empty = ((new_vec->len == 0) ? 1 : 0);
+ new_vec += empty;
+ new_veclen -= empty;
+ }
+ *vec = new_vec;
+ *veclen = new_veclen;
+
+ return KNOT_EOK;
+}
+
+int quic_send_dns_query(quic_ctx_t *ctx, int sockfd, struct addrinfo *srv,
+ const uint8_t *buf, const size_t buf_len)
+{
+ if (ctx == NULL || buf == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ if (ctx->state < CONNECTED) {
+ return KNOT_ECONN;
+ }
+
+ uint16_t query_length = htons(buf_len);
+ ngtcp2_vec datav[] = {
+ {(uint8_t *)&query_length, sizeof(uint16_t)},
+ {(uint8_t *)buf, buf_len}
+ };
+ size_t datavlen = sizeof(datav) / sizeof(*datav);
+ ngtcp2_vec *pdatav = datav;
+
+ struct pollfd pfd = {
+ .fd = sockfd,
+ .events = POLLIN,
+ .revents = 0,
+ };
+
+ assert(ctx->stream.id < 0);
+ int ret = quic_open_bidi_stream(ctx);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ctx->stream.out_ack = 0;
+ for (ngtcp2_vec *it = datav; it < datav + datavlen; ++it) {
+ ctx->stream.out_ack += it->len;
+ }
+
+ while (ctx->stream.out_ack > 0) {
+ ret = quic_send_data(ctx, sockfd, srv->ai_family, pdatav, datavlen);
+ if (ret < 0) {
+ WARN("QUIC, failed to send");
+ return ret;
+ } else if (ret > 0) {
+ ret = offset_span(&pdatav, &datavlen, ret);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ int timeout = get_expiry(ctx->conn);
+ if (timeout > 0 && datavlen > 0) {
+ continue;
+ }
+ ret = poll(&pfd, 1, timeout);
+ if (ret < 0) {
+ WARN("QUIC, failed to send");
+ return knot_map_errno();
+ } else if (ret == 0) {
+ ret = ngtcp2_conn_handle_expiry(ctx->conn, quic_timestamp());
+ continue;
+ }
+ ret = quic_recv(ctx, sockfd);
+ if (ret != KNOT_EOK) {
+ WARN("QUIC, failed to send");
+ return ret;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+int quic_recv_dns_response(quic_ctx_t *ctx, uint8_t *buf, const size_t buf_len,
+ struct addrinfo *srv)
+{
+ if (ctx == NULL || ctx->tls == NULL || buf == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ int ret = quic_respcpy(ctx, buf, buf_len);
+ if (ret != 0) {
+ return ret;
+ } else if (ctx->stream.id < 0) {
+ return KNOT_NET_ERECV;
+ }
+
+ int sockfd = ctx->tls->sockfd;
+
+ struct pollfd pfd = {
+ .fd = sockfd,
+ .events = POLLIN,
+ .revents = 0,
+ };
+
+ while (1) {
+ int timeout = get_expiry(ctx->conn);
+ ret = poll(&pfd, 1, timeout);
+ if (ret < 0) {
+ WARN("QUIC, failed to receive reply (%s)",
+ knot_strerror(errno));
+ return knot_map_errno();
+ } else if (ret == 0) {
+ ret = ngtcp2_conn_handle_expiry(ctx->conn, quic_timestamp());
+ WARN("QUIC, peer took too long to respond");
+ goto send;
+ }
+
+ ret = quic_recv(ctx, sockfd);
+ if (ret != KNOT_EOK) {
+ WARN("QUIC, failed to receive reply (%s)",
+ knot_strerror(ret));
+ return ret;
+ }
+ ret = quic_respcpy(ctx, buf, buf_len);
+ if (ret != 0) {
+ if (ret < 0) {
+ WARN("QUIC, failed to receive reply (%s)",
+ knot_strerror(ret));
+ }
+ return ret;
+ } else if (ctx->stream.id < 0) {
+ return KNOT_NET_ERECV;
+ }
+
+ send:
+ ret = quic_send(ctx, sockfd, srv->ai_family);
+ if (ret != KNOT_EOK) {
+ WARN("QUIC, failed to receive reply (%s)",
+ knot_strerror(ret));
+ return ret;
+ }
+ }
+
+ WARN("QUIC, peer took too long to respond");
+ const uint8_t msg[] = "Connection timeout";
+ set_application_error(ctx, DOQ_REQUEST_CANCELLED, msg, sizeof(msg) - 1);
+
+ return KNOT_NET_ETIMEOUT;
+}
+
+#define quic_ctx_write_close(ctx, dest, dest_len, ts) \
+ ngtcp2_conn_write_connection_close((ctx)->conn, (ngtcp2_path *)ngtcp2_conn_get_path((ctx)->conn), \
+ &(ctx)->pi, dest, dest_len, &(ctx)->last_err, ts)
+
+void quic_ctx_close(quic_ctx_t *ctx)
+{
+ if (ctx == NULL || ctx->state == CLOSED) {
+ return;
+ }
+
+ uint8_t enc_buf[MAX_PACKET_SIZE];
+ struct iovec msg_iov = {
+ .iov_base = enc_buf,
+ .iov_len = 0
+ };
+ struct msghdr msg = {
+ .msg_iov = &msg_iov,
+ .msg_iovlen = 1
+ };
+
+ ngtcp2_ssize nwrite = quic_ctx_write_close(ctx, enc_buf, sizeof(enc_buf),
+ quic_timestamp());
+ if (nwrite <= 0) {
+ return;
+ }
+
+ msg_iov.iov_len = nwrite;
+
+ struct sockaddr_in6 si = { 0 };
+ socklen_t si_len = sizeof(si);
+ if (getsockname(ctx->tls->sockfd, (struct sockaddr *)&si, &si_len) == 0) {
+ (void)net_ecn_set(ctx->tls->sockfd, si.sin6_family, ctx->pi.ecn);
+ }
+
+ (void)sendmsg(ctx->tls->sockfd, &msg, 0);
+ ctx->state = CLOSED;
+}
+
+void quic_ctx_deinit(quic_ctx_t *ctx)
+{
+ if (ctx == NULL) {
+ return;
+ }
+
+ if (ctx->conn) {
+ ngtcp2_conn_del(ctx->conn);
+ ctx->conn = NULL;
+ }
+
+ if (ctx->stream.in_buffer.iov_base != NULL) {
+ free(ctx->stream.in_buffer.iov_base);
+ ctx->stream.in_buffer.iov_base = NULL;
+ }
+
+ while (ctx->stream.in_parsed != NULL) {
+ knot_tcp_inbufs_upd_res_t *tofree = ctx->stream.in_parsed;
+ ctx->stream.in_parsed = tofree->next;
+ free(tofree);
+ }
+}
+
+void print_quic(const quic_ctx_t *ctx)
+{
+ if (ctx == NULL || !ctx->params.enable || ctx->tls->session == NULL) {
+ return;
+ }
+
+ char *msg = gnutls_session_get_desc(ctx->tls->session);
+ printf(";; QUIC session (QUICv%d)-%s\n", ngtcp2_conn_get_negotiated_version(ctx->conn), msg);
+ gnutls_free(msg);
+}
+
+#endif
diff --git a/src/utils/common/quic.h b/src/utils/common/quic.h
new file mode 100644
index 0000000..fd70d27
--- /dev/null
+++ b/src/utils/common/quic.h
@@ -0,0 +1,118 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+/*! \brief QUIC parameters. */
+typedef struct {
+ /*! Use QUIC indicator. */
+ bool enable;
+} quic_params_t;
+
+int quic_params_copy(quic_params_t *dst, const quic_params_t *src);
+
+void quic_params_clean(quic_params_t *params);
+
+#ifdef ENABLE_QUIC
+
+#include <ngtcp2/ngtcp2.h>
+#include <ngtcp2/ngtcp2_crypto.h>
+
+#include "utils/common/tls.h"
+
+#define QUIC_DEFAULT_VERSION "-VERS-ALL:+VERS-TLS1.3"
+#define QUIC_DEFAULT_GROUPS "-GROUP-ALL:+GROUP-X25519:+GROUP-SECP256R1:+GROUP-SECP384R1:+GROUP-SECP521R1"
+#define QUIC_PRIORITY "%DISABLE_TLS13_COMPAT_MODE:NORMAL:"QUIC_DEFAULT_VERSION":"QUIC_DEFAULT_GROUPS
+
+typedef enum {
+ CLOSED, // Initialized
+ CONNECTED, // RTT-0
+ VERIFIED, // RTT-1
+} quic_state_t;
+
+typedef enum {
+ /*! No error. This is used when the connection or stream needs to be
+ closed, but there is no error to signal. */
+ DOQ_NO_ERROR = 0x0,
+ /*! The DoQ implementation encountered an internal error and is
+ incapable of pursuing the transaction or the connection. */
+ DOQ_INTERNAL_ERROR = 0x1,
+ /*! The DoQ implementation encountered a protocol error and is forcibly
+ aborting the connection. */
+ DOQ_PROTOCOL_ERROR = 0x2,
+ /*! A DoQ client uses this to signal that it wants to cancel an
+ outstanding transaction. */
+ DOQ_REQUEST_CANCELLED = 0x3,
+ /*! A DoQ implementation uses this to signal when closing a connection
+ due to excessive load. */
+ DOQ_EXCESSIVE_LOAD = 0x4,
+ /*! A DoQ implementation uses this in the absence of a more specific
+ error code. */
+ DOQ_UNSPECIFIED_ERROR = 0x5,
+ /*! Alternative error code used for tests. */
+ DOQ_ERROR_RESERVED = 0xd098ea5e
+} quic_doq_error_t;
+
+typedef struct {
+ ngtcp2_crypto_conn_ref conn_ref;
+ // Parameters
+ quic_params_t params;
+
+ // Context
+ ngtcp2_settings settings;
+ struct {
+ int64_t id;
+ uint64_t out_ack;
+ struct iovec in_buffer;
+ struct knot_tcp_inbufs_upd_res *in_parsed;
+ size_t in_parsed_it;
+ size_t in_parsed_total;
+ } stream;
+ ngtcp2_ccerr last_err;
+ uint8_t secret[32];
+ tls_ctx_t *tls;
+ ngtcp2_conn *conn;
+ ngtcp2_pkt_info pi;
+ quic_state_t state;
+} quic_ctx_t;
+
+extern const gnutls_datum_t doq_alpn;
+
+uint64_t quic_timestamp(void);
+
+int quic_generate_secret(uint8_t *buf, size_t buflen);
+
+uint32_t quic_get_ecn(struct msghdr *msg, const int family);
+
+int quic_ctx_init(quic_ctx_t *ctx, tls_ctx_t *tls_ctx, const quic_params_t *params);
+
+int quic_ctx_connect(quic_ctx_t *ctx, int sockfd, struct addrinfo *dst_addr);
+
+int quic_send_dns_query(quic_ctx_t *ctx, int sockfd, struct addrinfo *srv,
+ const uint8_t *buf, const size_t buf_len);
+
+int quic_recv_dns_response(quic_ctx_t *ctx, uint8_t *buf, const size_t buf_len,
+ struct addrinfo *srv);
+
+void quic_ctx_close(quic_ctx_t *ctx);
+
+void quic_ctx_deinit(quic_ctx_t *ctx);
+
+void print_quic(const quic_ctx_t *ctx);
+
+#endif //ENABLE_QUIC
diff --git a/src/utils/common/resolv.c b/src/utils/common/resolv.c
new file mode 100644
index 0000000..674a760
--- /dev/null
+++ b/src/utils/common/resolv.c
@@ -0,0 +1,211 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "utils/common/resolv.h"
+#include "utils/common/msg.h"
+#include "utils/common/params.h"
+#include "libknot/libknot.h"
+#include "contrib/ucw/lists.h"
+
+#define RESOLV_FILE "/etc/resolv.conf"
+
+srv_info_t* parse_nameserver(const char *str, const char *def_port)
+{
+ char *host = NULL, *port = NULL;
+ const char *addr = NULL, *sep = NULL;
+ size_t addr_len = 0;
+ char separator = ':';
+
+ if (str == NULL || def_port == NULL) {
+ DBG_NULL;
+ return NULL;
+ }
+
+ const size_t str_len = strlen(str);
+ const char *str_end = str + str_len;
+
+ // UNIX socket path.
+ if (*str == '/') {
+ return srv_info_create(str, "UNIX");
+ // [address]:port notation.
+ } else if (*str == '[') {
+ addr = str + 1;
+ const char *addr_end = strchr(addr, ']');
+ // Missing closing bracket -> stop processing.
+ if (addr_end == NULL) {
+ return NULL;
+ }
+ addr_len = addr_end - addr;
+ str += 1 + addr_len + 1;
+ // Address@port notation.
+ } else if ((sep = strchr(str, '@')) != NULL) {
+ addr = str;
+ addr_len = sep - addr;
+ str += addr_len;
+ separator = '@';
+ // Address#port notation.
+ } else if ((sep = strchr(str, '#')) != NULL) {
+ addr = str;
+ addr_len = sep - addr;
+ str += addr_len;
+ separator = '#';
+ // IPv4:port notation.
+ } else if ((sep = strchr(str, ':')) != NULL) {
+ addr = str;
+ // Not IPv4 address -> no port.
+ if (strchr(sep + 1, ':') != NULL) {
+ addr_len = str_len;
+ str = str_end;
+ } else {
+ addr_len = sep - addr;
+ str += addr_len;
+ }
+ // No port specified.
+ } else {
+ addr = str;
+ addr_len = str_len;
+ str = str_end;
+ }
+
+ // Process port.
+ if (str < str_end) {
+ // Check port separator.
+ if (*str != separator) {
+ return NULL;
+ }
+ str++;
+
+ // Check for missing port.
+ if (str >= str_end) {
+ return NULL;
+ }
+
+ port = strdup(str);
+ } else {
+ port = strdup(def_port);
+ }
+
+ host = strndup(addr, addr_len);
+
+ // Create server structure.
+ srv_info_t *server = srv_info_create(host, port);
+
+ free(host);
+ free(port);
+
+ return server;
+}
+
+static size_t get_resolv_nameservers(list_t *servers, const char *def_port)
+{
+ char line[512];
+
+ // Open config file.
+ FILE *f = fopen(RESOLV_FILE, "r");
+ if (f == NULL) {
+ return 0;
+ }
+
+ // Read lines from config file.
+ while (fgets(line, sizeof(line), f) != NULL) {
+ size_t len;
+ char *pos = line;
+ char *option, *value;
+
+ // Find leading white characters.
+ len = strspn(pos, SEP_CHARS);
+ pos += len;
+
+ // Start of the first token.
+ option = pos;
+
+ // Find length of the token.
+ len = strcspn(pos, SEP_CHARS);
+ pos += len;
+
+ // Check if the token is not empty.
+ if (len == 0) {
+ continue;
+ }
+
+ // Find separating white characters.
+ len = strspn(pos, SEP_CHARS);
+ pos += len;
+
+ // Check if there is a separation between tokens.
+ if (len == 0) {
+ continue;
+ }
+
+ // Copy of the second token.
+ value = strndup(pos, strcspn(pos, SEP_CHARS));
+
+ // Process value with respect to option name.
+ if (strncmp(option, "nameserver", strlen("nameserver")) == 0) {
+ srv_info_t *server;
+
+ server = parse_nameserver(value, def_port);
+
+ // If value is correct, add nameserver to the list.
+ if (server != NULL) {
+ add_tail(servers, (node_t *)server);
+ }
+ }
+
+ // Drop value string.
+ free(value);
+ }
+
+ // Close config file.
+ fclose(f);
+
+ // Return number of servers.
+ return list_size(servers);
+}
+
+void get_nameservers(list_t *servers, const char *def_port)
+{
+ if (servers == NULL || def_port == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ // Initialize list of servers.
+ init_list(servers);
+
+ // Read nameservers from resolv file or use the default ones.
+ if (get_resolv_nameservers(servers, def_port) == 0) {
+ srv_info_t *server;
+
+ // Add default ipv6 nameservers.
+ server = srv_info_create(DEFAULT_IPV6_NAME, def_port);
+
+ if (server != NULL) {
+ add_tail(servers, (node_t *)server);
+ }
+
+ // Add default ipv4 nameservers.
+ server = srv_info_create(DEFAULT_IPV4_NAME, def_port);
+
+ if (server != NULL) {
+ add_tail(servers, (node_t *)server);
+ }
+ }
+}
diff --git a/src/utils/common/resolv.h b/src/utils/common/resolv.h
new file mode 100644
index 0000000..fb751d1
--- /dev/null
+++ b/src/utils/common/resolv.h
@@ -0,0 +1,24 @@
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "utils/common/netio.h"
+#include "contrib/ucw/lists.h"
+
+srv_info_t* parse_nameserver(const char *str, const char *def_port);
+
+void get_nameservers(list_t *servers, const char *def_port);
diff --git a/src/utils/common/sign.c b/src/utils/common/sign.c
new file mode 100644
index 0000000..84284d3
--- /dev/null
+++ b/src/utils/common/sign.c
@@ -0,0 +1,109 @@
+/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+
+#include "utils/common/sign.h"
+#include "libknot/errcode.h"
+#include "libknot/tsig-op.h"
+
+int sign_context_init_tsig(sign_context_t *ctx, const knot_tsig_key_t *key)
+{
+ if (!ctx || !key) {
+ return KNOT_EINVAL;
+ }
+
+ size_t digest_size = dnssec_tsig_algorithm_size(key->algorithm);
+ if (digest_size == 0) {
+ return KNOT_EINVAL;
+ }
+
+ uint8_t *digest = calloc(1, digest_size);
+ if (!digest) {
+ return KNOT_ENOMEM;
+ }
+
+ ctx->digest_size = digest_size;
+ ctx->digest = digest;
+ ctx->tsig_key = key;
+
+ return KNOT_EOK;
+}
+
+void sign_context_deinit(sign_context_t *ctx)
+{
+ if (!ctx) {
+ return;
+ }
+
+ free(ctx->digest);
+
+ memset(ctx, 0, sizeof(*ctx));
+}
+
+int sign_packet(knot_pkt_t *pkt, sign_context_t *sign_ctx)
+{
+ if (pkt == NULL || sign_ctx == NULL || sign_ctx->digest == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ uint8_t *wire = pkt->wire;
+ size_t *wire_size = &pkt->size;
+ size_t max_size = pkt->max_size;
+
+ int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(sign_ctx->tsig_key));
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ return knot_tsig_sign(wire, wire_size, max_size, NULL, 0,
+ sign_ctx->digest, &sign_ctx->digest_size,
+ sign_ctx->tsig_key, 0, 0);
+}
+
+int verify_packet(const knot_pkt_t *pkt, const sign_context_t *sign_ctx)
+{
+ if (pkt == NULL || sign_ctx == NULL || sign_ctx->digest == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ const uint8_t *wire = pkt->wire;
+ const size_t *wire_size = &pkt->size;
+
+ if (pkt->tsig_rr == NULL) {
+ return KNOT_ENOTSIG;
+ }
+
+ int ret = knot_tsig_client_check(pkt->tsig_rr, wire, *wire_size,
+ sign_ctx->digest, sign_ctx->digest_size,
+ sign_ctx->tsig_key, 0);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ switch (knot_tsig_rdata_error(pkt->tsig_rr)) {
+ case KNOT_RCODE_BADSIG:
+ return KNOT_TSIG_EBADSIG;
+ case KNOT_RCODE_BADKEY:
+ return KNOT_TSIG_EBADKEY;
+ case KNOT_RCODE_BADTIME:
+ return KNOT_TSIG_EBADTIME;
+ case KNOT_RCODE_BADTRUNC:
+ return KNOT_TSIG_EBADTRUNC;
+ default:
+ return KNOT_EOK;
+ }
+}
diff --git a/src/utils/common/sign.h b/src/utils/common/sign.h
new file mode 100644
index 0000000..52f41ef
--- /dev/null
+++ b/src/utils/common/sign.h
@@ -0,0 +1,63 @@
+/* Copyright (C) 2015 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "libknot/packet/pkt.h"
+#include "libknot/tsig.h"
+
+/*!
+ * \brief Holds data required between signing and signature verification.
+ */
+struct sign_context {
+ size_t digest_size;
+ uint8_t *digest;
+ const knot_tsig_key_t *tsig_key;
+};
+
+typedef struct sign_context sign_context_t;
+
+/*!
+ * \brief Initialize signing context for TSIG.
+ */
+int sign_context_init_tsig(sign_context_t *ctx, const knot_tsig_key_t *key);
+
+/*!
+ * \brief Clean up signing context.
+ *
+ * \param ctx Sign context.
+ */
+void sign_context_deinit(sign_context_t *ctx);
+
+/*!
+ * \brief Signs outgoing DNS packet.
+ *
+ * \param pkt Packet to sign.
+ * \param sign_ctx Signing context.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int sign_packet(knot_pkt_t *pkt, sign_context_t *sign_ctx);
+
+/*!
+ * \brief Verifies signature for incoming DNS packet.
+ *
+ * \param pkt Packet verify sign.
+ * \param sign_ctx Signing context.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int verify_packet(const knot_pkt_t *pkt, const sign_context_t *sign_ctx);
diff --git a/src/utils/common/signal.c b/src/utils/common/signal.c
new file mode 100644
index 0000000..953fb12
--- /dev/null
+++ b/src/utils/common/signal.c
@@ -0,0 +1,66 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <signal.h>
+#include <stdio.h>
+
+#include "utils/common/signal.h"
+
+#include "contrib/color.h"
+#include "knot/conf/base.h"
+
+extern signal_ctx_t signal_ctx; // It must be defined as global in each utility.
+int SIGNAL_REPEAT = 1;
+
+static void signal_handler(int signum)
+{
+ // Allow a repeated signal during the handler run (in case
+ // the handler gets stuck).
+ sigset_t set;
+ (void)sigaddset(&set, signum);
+ (void)sigprocmask(SIG_UNBLOCK, &set, NULL);
+
+ if (--SIGNAL_REPEAT < 0) {
+ abort();
+ }
+
+ (void)printf("%s%s\n", COL_RST(signal_ctx.color),
+ signum == SIGINT ? "" : strsignal(signum));
+
+ conf_t *config = conf();
+ if (config != NULL && config->api != NULL) {
+ config->api->deinit(config->db);
+ }
+
+ if (signal_ctx.close_db != NULL) {
+ knot_lmdb_close(signal_ctx.close_db);
+ }
+
+ exit(EXIT_FAILURE);
+}
+
+void signal_init_std(void)
+{
+ struct sigaction sigact = { .sa_handler = signal_handler };
+
+ sigaction(SIGHUP, &sigact, NULL);
+ sigaction(SIGINT, &sigact, NULL);
+ sigaction(SIGPIPE, &sigact, NULL);
+ sigaction(SIGALRM, &sigact, NULL);
+ sigaction(SIGTERM, &sigact, NULL);
+ sigaction(SIGUSR1, &sigact, NULL);
+ sigaction(SIGUSR2, &sigact, NULL);
+}
diff --git a/src/utils/common/signal.h b/src/utils/common/signal.h
new file mode 100644
index 0000000..0955c9d
--- /dev/null
+++ b/src/utils/common/signal.h
@@ -0,0 +1,37 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/journal/knot_lmdb.h"
+
+/*!
+ * \brief Data passed to the signal handler.
+ */
+typedef struct {
+ knot_lmdb_db_t *close_db; // LMDB database to be closed
+ bool color; // do a terminal color reset
+} signal_ctx_t;
+
+/*!
+ * \brief Prepares a signal handler for a clean shutdown.
+ *
+ * \note Configures common break signals to initiate close of confdb
+ * and of another LMDB database defined by the global variable
+ * signal_ctx_t signal_ctx. If set to NULL, only confdb
+ * is closed.
+ */
+void signal_init_std(void);
diff --git a/src/utils/common/tls.c b/src/utils/common/tls.c
new file mode 100644
index 0000000..276ae16
--- /dev/null
+++ b/src/utils/common/tls.c
@@ -0,0 +1,736 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <arpa/inet.h>
+#include <stdbool.h>
+#include <string.h>
+#include <poll.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <gnutls/gnutls.h>
+#include <gnutls/ocsp.h>
+#include <gnutls/x509.h>
+#define GNUTLS_VERSION_FASTOPEN_READY 0x030503
+#if GNUTLS_VERSION_NUMBER >= GNUTLS_VERSION_FASTOPEN_READY
+#include <gnutls/socket.h>
+#endif
+
+#include "utils/common/tls.h"
+#include "utils/common/msg.h"
+#include "contrib/base64.h"
+#include "libknot/errcode.h"
+
+const gnutls_datum_t dot_alpn = {
+ (unsigned char *)"dot", 3
+};
+
+void tls_params_init(tls_params_t *params)
+{
+ if (params == NULL) {
+ return;
+ }
+
+ memset(params, 0, sizeof(*params));
+
+ init_list(&params->ca_files);
+ init_list(&params->pins);
+}
+
+int tls_params_copy(tls_params_t *dst, const tls_params_t *src)
+{
+ if (dst == NULL || src == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ tls_params_init(dst);
+
+ dst->enable = src->enable;
+ dst->system_ca = src->system_ca;
+ if (src->hostname != NULL) {
+ dst->hostname = strdup(src->hostname);
+ if (dst->hostname == NULL) {
+ tls_params_clean(dst);
+ return KNOT_ENOMEM;
+ }
+ }
+
+ if (src->sni != NULL) {
+ dst->sni = strdup(src->sni);
+ if (dst->sni == NULL) {
+ tls_params_clean(dst);
+ return KNOT_ENOMEM;
+ }
+ }
+
+ if (src->keyfile != NULL) {
+ dst->keyfile = strdup(src->keyfile);
+ if (dst->keyfile == NULL) {
+ tls_params_clean(dst);
+ return KNOT_ENOMEM;
+ }
+ }
+
+ if (src->certfile != NULL) {
+ dst->certfile = strdup(src->certfile);
+ if (dst->certfile == NULL) {
+ tls_params_clean(dst);
+ return KNOT_ENOMEM;
+ }
+ }
+
+ dst->ocsp_stapling = src->ocsp_stapling;
+
+ ptrnode_t *n;
+ WALK_LIST(n, src->ca_files) {
+ char *src_file = (char *)n->d;
+ char *file = strdup(src_file);
+ if (file == NULL || ptrlist_add(&dst->ca_files, file, NULL) == NULL) {
+ tls_params_clean(dst);
+ return KNOT_ENOMEM;
+ }
+ }
+ WALK_LIST(n, src->pins) {
+ uint8_t *src_pin = (uint8_t *)n->d;
+ uint8_t *pin = malloc(1 + src_pin[0]);
+ if (pin == NULL || ptrlist_add(&dst->pins, pin, NULL) == NULL) {
+ tls_params_clean(dst);
+ return KNOT_ENOMEM;
+ }
+ memcpy(pin, src_pin, 1 + src_pin[0]);
+ }
+
+ return KNOT_EOK;
+}
+
+void tls_params_clean(tls_params_t *params)
+{
+ if (params == NULL) {
+ return;
+ }
+
+ ptrnode_t *node, *nxt;
+ WALK_LIST_DELSAFE(node, nxt, params->ca_files) {
+ free(node->d);
+ }
+ ptrlist_free(&params->ca_files, NULL);
+
+ WALK_LIST_DELSAFE(node, nxt, params->pins) {
+ free(node->d);
+ }
+ ptrlist_free(&params->pins, NULL);
+
+ free(params->hostname);
+ free(params->sni);
+ free(params->keyfile);
+ free(params->certfile);
+
+ memset(params, 0, sizeof(*params));
+}
+
+static bool check_pin(const uint8_t *cert_pin, size_t cert_pin_len, const list_t *pins)
+{
+ if (EMPTY_LIST(*pins)) {
+ return false;
+ }
+
+ ptrnode_t *n;
+ WALK_LIST(n, *pins) {
+ uint8_t *pin = (uint8_t *)n->d;
+ if (pin[0] == cert_pin_len &&
+ memcmp(cert_pin, &pin[1], cert_pin_len) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static bool verify_ocsp(gnutls_session_t *session)
+{
+ bool ret = false;
+
+ gnutls_ocsp_resp_t ocsp_resp;
+ bool deinit_ocsp_resp = false;
+
+ gnutls_x509_crt_t server_cert;
+ bool deinit_server_cert = false;
+
+ gnutls_certificate_credentials_t xcred;
+ bool deinit_xcreds = false;
+
+ gnutls_x509_crt_t issuer_cert;
+ bool deinit_issuer_cert = false;
+
+ gnutls_datum_t ocsp_resp_raw;
+ if (gnutls_ocsp_status_request_get(*session, &ocsp_resp_raw) != GNUTLS_E_SUCCESS) {
+ WARN("TLS, unable to retrieve stapled OCSP data");
+ goto cleanup;
+ }
+ if (gnutls_ocsp_resp_init(&ocsp_resp) != GNUTLS_E_SUCCESS) {
+ WARN("TLS, unable to init OCSP data");
+ goto cleanup;
+ }
+ deinit_ocsp_resp = true;
+ if (gnutls_ocsp_resp_import(ocsp_resp, &ocsp_resp_raw) != GNUTLS_E_SUCCESS) {
+ WARN("TLS, unable to import OCSP response");
+ goto cleanup;
+ }
+
+ unsigned int cert_list_size = 0;
+ const gnutls_datum_t *cert_list = gnutls_certificate_get_peers(*session, &cert_list_size);
+ if (cert_list_size == 0) {
+ WARN("TLS, unable to retrieve peer certs when verifying OCSP");
+ goto cleanup;
+ }
+ if (gnutls_x509_crt_init(&server_cert) != GNUTLS_E_SUCCESS) {
+ WARN("TLS, unable to init server cert when verifying OCSP");
+ goto cleanup;
+ }
+ deinit_server_cert = true;
+ if (gnutls_x509_crt_import(server_cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) {
+ WARN("TLS, unable to import server cert when verifying OCSP");
+ goto cleanup;
+ }
+
+ if (gnutls_certificate_allocate_credentials(&xcred) != GNUTLS_E_SUCCESS) {
+ WARN("TLS, unable to allocate credentials when verifying OCSP");
+ goto cleanup;
+ }
+ deinit_xcreds = true;
+
+ if (gnutls_certificate_get_issuer(xcred, server_cert, &issuer_cert, 0) != GNUTLS_E_SUCCESS) {
+ if (cert_list_size < 2) {
+ WARN("TLS, unable to get issuer (CA) cert when verifying OCSP");
+ goto cleanup;
+ }
+ if (gnutls_x509_crt_init(&issuer_cert) != GNUTLS_E_SUCCESS) {
+ WARN("TLS, unable to init issuer cert when verifying OCSP");
+ goto cleanup;
+ }
+ deinit_issuer_cert = true;
+ if (gnutls_x509_crt_import(issuer_cert, &cert_list[1], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) {
+ WARN("TLS, unable to import issuer cert when verifying OCSP");
+ goto cleanup;
+ }
+ }
+
+ unsigned int status;
+ time_t this_upd, next_upd, now = time(0);
+ if (gnutls_ocsp_resp_check_crt(ocsp_resp, 0, server_cert) != GNUTLS_E_SUCCESS) {
+ WARN("TLS, OCSP response either empty or not for provided server cert");
+ goto cleanup;
+ }
+ if (gnutls_ocsp_resp_verify_direct(ocsp_resp, issuer_cert, &status, 0) != GNUTLS_E_SUCCESS) {
+ WARN("TLS, unable to verify OCSP response against issuer cert");
+ goto cleanup;
+ }
+ if (status != 0) {
+ WARN("TLS, got a non-zero status when verifying OCSP response against issuer cert");
+ goto cleanup;
+ }
+ if (gnutls_ocsp_resp_get_single(ocsp_resp, 0, NULL, NULL, NULL, NULL, &status,
+ &this_upd, &next_upd, NULL, NULL) != GNUTLS_E_SUCCESS) {
+ WARN("TLS, error reading OCSP response");
+ goto cleanup;
+ }
+ if (status == GNUTLS_OCSP_CERT_REVOKED) {
+ WARN("TLS, OCSP data shows that cert was revoked");
+ goto cleanup;
+ }
+ if (next_upd == -1) {
+ tls_ctx_t *ctx = gnutls_session_get_ptr(*session);
+ assert(now >= this_upd);
+ assert(ctx->params->ocsp_stapling > 0);
+ if (now - this_upd > ctx->params->ocsp_stapling) {
+ WARN("TLS, OCSP response is out of date.");
+ goto cleanup;
+ }
+ } else {
+ if (next_upd < now) {
+ WARN("TLS, a newer OCSP response is available but was not sent");
+ goto cleanup;
+ }
+ }
+
+ // Only if we get here is the ocsp result completely valid.
+ ret = true;
+
+cleanup:
+ if (deinit_issuer_cert) {
+ gnutls_x509_crt_deinit(issuer_cert);
+ }
+ if (deinit_xcreds) {
+ gnutls_certificate_free_credentials(xcred);
+ }
+ if (deinit_server_cert) {
+ gnutls_x509_crt_deinit(server_cert);
+ }
+ if (deinit_ocsp_resp) {
+ gnutls_ocsp_resp_deinit(ocsp_resp);
+ }
+
+ return ret;
+}
+
+static int check_certificates(gnutls_session_t session, const list_t *pins)
+{
+ if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) {
+ DBG("TLS, invalid certificate type");
+ return GNUTLS_E_CERTIFICATE_ERROR;
+ }
+
+ unsigned cert_list_size;
+ const gnutls_datum_t *cert_list =
+ gnutls_certificate_get_peers(session, &cert_list_size);
+ if (cert_list == NULL || cert_list_size == 0) {
+ DBG("TLS, empty certificate list");
+ return GNUTLS_E_CERTIFICATE_ERROR;
+ }
+
+ size_t matches = 0;
+
+ DBG("TLS, received certificate hierarchy:");
+ for (int i = 0; i < cert_list_size; i++) {
+ gnutls_x509_crt_t cert;
+ int ret = gnutls_x509_crt_init(&cert);
+ if (ret != GNUTLS_E_SUCCESS) {
+ return ret;
+ }
+
+ ret = gnutls_x509_crt_import(cert, &cert_list[i], GNUTLS_X509_FMT_DER);
+ if (ret != GNUTLS_E_SUCCESS) {
+ gnutls_x509_crt_deinit(cert);
+ return ret;
+ }
+
+ gnutls_datum_t cert_name = { 0 };
+ ret = gnutls_x509_crt_get_dn2(cert, &cert_name);
+ if (ret != GNUTLS_E_SUCCESS) {
+ gnutls_x509_crt_deinit(cert);
+ return ret;
+ }
+ DBG(" #%i, %s", i + 1, cert_name.data);
+ gnutls_free(cert_name.data);
+
+ uint8_t cert_pin[CERT_PIN_LEN] = { 0 };
+ size_t cert_pin_size = sizeof(cert_pin);
+ ret = gnutls_x509_crt_get_key_id(cert, GNUTLS_KEYID_USE_SHA256,
+ cert_pin, &cert_pin_size);
+ if (ret != 0) {
+ gnutls_x509_crt_deinit(cert);
+ return ret;
+ }
+
+ // Check if correspond to a specified PIN.
+ bool match = check_pin(cert_pin, sizeof(cert_pin), pins);
+ if (match) {
+ matches++;
+ }
+
+ uint8_t *txt_pin;
+ ret = knot_base64_encode_alloc(cert_pin, sizeof(cert_pin), &txt_pin);
+ if (ret < 0) {
+ gnutls_x509_crt_deinit(cert);
+ return ret;
+ }
+ DBG(" SHA-256 PIN: %.*s%s", ret, txt_pin, match ? ", MATCH" : "");
+ free(txt_pin);
+
+ gnutls_x509_crt_deinit(cert);
+ }
+
+ if (matches > 0) {
+ return GNUTLS_E_SUCCESS;
+ } else if (EMPTY_LIST(*pins)) {
+ DBG("TLS, skipping certificate PIN check");
+ return GNUTLS_E_SUCCESS;
+ } else {
+ DBG("TLS, no certificate PIN match");
+ return GNUTLS_E_CERTIFICATE_ERROR;
+ }
+}
+
+static bool do_verification(const tls_params_t *params)
+{
+ return params->hostname != NULL || params->system_ca ||
+ !EMPTY_LIST(params->ca_files) || params->ocsp_stapling > 0;
+}
+
+int tls_certificate_verification(tls_ctx_t *ctx)
+{
+ gnutls_session_t session = ctx->session;
+ // Check for pinned certificates and print certificate hierarchy.
+ int ret = check_certificates(session, &ctx->params->pins);
+ if (ret != GNUTLS_E_SUCCESS) {
+ return ret;
+ }
+
+ if (!do_verification(ctx->params)) {
+ DBG("TLS, skipping certificate verification");
+ return GNUTLS_E_SUCCESS;
+ }
+
+ if (ctx->params->ocsp_stapling > 0 && !verify_ocsp(&session)) {
+ WARN("TLS, failed to validate required OCSP data");
+ return GNUTLS_E_CERTIFICATE_ERROR;
+ }
+
+ // Set server certificate check.
+ gnutls_typed_vdata_st data[2] = {
+ { .type = GNUTLS_DT_KEY_PURPOSE_OID,
+ .data = (void *)GNUTLS_KP_TLS_WWW_SERVER },
+ { .type = GNUTLS_DT_DNS_HOSTNAME,
+ .data = (void *)ctx->params->hostname }
+ };
+ size_t data_count = (ctx->params->hostname != NULL) ? 2 : 1;
+ if (data_count == 1) {
+ WARN("TLS, no hostname provided, will not verify certificate owner")
+ }
+
+ unsigned int status;
+ ret = gnutls_certificate_verify_peers(session, data, data_count, &status);
+ if (ret != GNUTLS_E_SUCCESS) {
+ WARN("TLS, failed to verify peer certificate");
+ return GNUTLS_E_CERTIFICATE_ERROR;
+ }
+
+ gnutls_datum_t msg;
+ ret = gnutls_certificate_verification_status_print(
+ status, gnutls_certificate_type_get(session), &msg, 0);
+ if (ret == GNUTLS_E_SUCCESS) {
+ DBG("TLS, %s", msg.data);
+ }
+ gnutls_free(msg.data);
+
+ if (status != 0) {
+ return GNUTLS_E_CERTIFICATE_ERROR;
+ }
+
+ return GNUTLS_E_SUCCESS;
+}
+
+static int verify_certificate(gnutls_session_t session)
+{
+ tls_ctx_t *ctx = gnutls_session_get_ptr(session);
+ return tls_certificate_verification(ctx);
+}
+
+int tls_ctx_init(tls_ctx_t *ctx, const tls_params_t *params,
+ unsigned int flags, int wait)
+
+{
+ if (ctx == NULL || params == NULL || !params->enable) {
+ return KNOT_EINVAL;
+ }
+
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->params = params;
+ ctx->wait = wait;
+ ctx->sockfd = -1;
+
+ int ret = gnutls_certificate_allocate_credentials(&ctx->credentials);
+ if (ret != GNUTLS_E_SUCCESS) {
+ return KNOT_ENOMEM;
+ }
+
+ // Import system certificates.
+ if (ctx->params->system_ca ||
+ (ctx->params->hostname != NULL && EMPTY_LIST(ctx->params->ca_files))) {
+ ret = gnutls_certificate_set_x509_system_trust(ctx->credentials);
+ if (ret < 0) {
+ WARN("TLS, failed to import system certificates (%s)",
+ gnutls_strerror_name(ret));
+ return KNOT_ERROR;
+ } else {
+ DBG("TLS, imported %i system certificates", ret);
+ }
+ }
+
+ // Import provided certificate files.
+ ptrnode_t *n;
+ WALK_LIST(n, ctx->params->ca_files) {
+ const char *file = (char *)n->d;
+ ret = gnutls_certificate_set_x509_trust_file(ctx->credentials, file,
+ GNUTLS_X509_FMT_PEM);
+ if (ret < 0) {
+ WARN("TLS, failed to import certificate file '%s' (%s)",
+ file, gnutls_strerror_name(ret));
+ return KNOT_ERROR;
+ } else {
+ DBG("TLS, imported %i certificates from '%s'", ret, file);
+ }
+ }
+
+ gnutls_certificate_set_verify_function(ctx->credentials, verify_certificate);
+
+ // Setup client keypair if specified. Both key and cert files must be provided.
+ if (params->keyfile != NULL && params->certfile != NULL) {
+ // First, try PEM.
+ ret = gnutls_certificate_set_x509_key_file(ctx->credentials,
+ params->certfile, params->keyfile, GNUTLS_X509_FMT_PEM);
+ if (ret != GNUTLS_E_SUCCESS) {
+ // If PEM didn't work, try DER.
+ ret = gnutls_certificate_set_x509_key_file(ctx->credentials,
+ params->certfile, params->keyfile, GNUTLS_X509_FMT_DER);
+ }
+
+ if (ret != GNUTLS_E_SUCCESS) {
+ WARN("TLS, failed to add client certfile '%s' and keyfile '%s'",
+ params->certfile, params->keyfile);
+ return KNOT_ERROR;
+ } else {
+ DBG("TLS, added client certfile '%s' and keyfile '%s'",
+ params->certfile, params->keyfile);
+ }
+ } else if (params->keyfile != NULL) {
+ WARN("TLS, cannot use client keyfile without a certfile");
+ return KNOT_ERROR;
+ } else if (params->certfile != NULL) {
+ WARN("TLS, cannot use client certfile without a keyfile");
+ return KNOT_ERROR;
+ }
+
+ ret = gnutls_init(&ctx->session, GNUTLS_CLIENT | flags);
+ if (ret != GNUTLS_E_SUCCESS) {
+ return KNOT_ENOMEM;
+ }
+
+ ret = gnutls_credentials_set(ctx->session, GNUTLS_CRD_CERTIFICATE,
+ ctx->credentials);
+ if (ret != GNUTLS_E_SUCCESS) {
+ return KNOT_ERROR;
+ }
+
+ return KNOT_EOK;
+}
+
+int tls_ctx_setup_remote_endpoint(tls_ctx_t *ctx, const gnutls_datum_t *alpn,
+ size_t alpn_size, const char *priority, const char *remote)
+{
+ if (ctx == NULL || ctx->session == NULL || ctx->credentials == NULL) {
+ return KNOT_EINVAL;
+ }
+ int ret = 0;
+ if (alpn != NULL) {
+ ret = gnutls_alpn_set_protocols(ctx->session, alpn, alpn_size, 0);
+ if (ret != GNUTLS_E_SUCCESS) {
+ return KNOT_NET_ECONNECT;
+ }
+ }
+
+ if (priority != NULL) {
+ ret = gnutls_priority_set_direct(ctx->session, priority, NULL);
+ } else {
+ ret = gnutls_set_default_priority(ctx->session);
+ }
+ if (ret != GNUTLS_E_SUCCESS) {
+ return KNOT_EINVAL;
+ }
+
+ if (remote != NULL) {
+ ret = gnutls_server_name_set(ctx->session, GNUTLS_NAME_DNS, remote,
+ strlen(remote));
+ if (ret != GNUTLS_E_SUCCESS) {
+ return KNOT_EINVAL;
+ }
+ }
+ return KNOT_EOK;
+}
+
+int tls_ctx_connect(tls_ctx_t *ctx, int sockfd, bool fastopen,
+ struct sockaddr_storage *addr)
+{
+ if (ctx == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ int ret = 0;
+ gnutls_session_set_ptr(ctx->session, ctx);
+
+ if (fastopen) {
+#if GNUTLS_VERSION_NUMBER >= GNUTLS_VERSION_FASTOPEN_READY
+ gnutls_transport_set_fastopen(ctx->session, sockfd, (struct sockaddr *)addr,
+ sockaddr_len(addr), 0);
+#else
+ return KNOT_ENOTSUP;
+#endif
+ } else {
+ gnutls_transport_set_int(ctx->session, sockfd);
+ }
+
+ gnutls_handshake_set_timeout(ctx->session, 1000 * ctx->wait);
+
+ // Initialize poll descriptor structure.
+ struct pollfd pfd = {
+ .fd = sockfd,
+ .events = POLLIN,
+ .revents = 0,
+ };
+
+ // Perform the TLS handshake
+ do {
+ ret = gnutls_handshake(ctx->session);
+ if (ret != GNUTLS_E_SUCCESS && gnutls_error_is_fatal(ret) == 0) {
+ if (poll(&pfd, 1, 1000 * ctx->wait) != 1) {
+ WARN("TLS, peer took too long to respond");
+ return KNOT_NET_ETIMEOUT;
+ }
+ }
+ } while (ret != GNUTLS_E_SUCCESS && gnutls_error_is_fatal(ret) == 0);
+ if (ret != GNUTLS_E_SUCCESS) {
+ WARN("TLS, handshake failed (%s)", gnutls_strerror(ret));
+ tls_ctx_close(ctx);
+ return KNOT_NET_ESOCKET;
+ }
+
+ // Save the socket descriptor.
+ ctx->sockfd = sockfd;
+
+ return KNOT_EOK;
+}
+
+int tls_ctx_send(tls_ctx_t *ctx, const uint8_t *buf, const size_t buf_len)
+{
+ if (ctx == NULL || buf == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ uint16_t msg_len = htons(buf_len);
+
+ gnutls_record_cork(ctx->session);
+
+ if (gnutls_record_send(ctx->session, &msg_len, sizeof(msg_len)) <= 0) {
+ WARN("TLS, failed to send");
+ return KNOT_NET_ESEND;
+ }
+ if (gnutls_record_send(ctx->session, buf, buf_len) <= 0) {
+ WARN("TLS, failed to send");
+ return KNOT_NET_ESEND;
+ }
+
+ while (gnutls_record_check_corked(ctx->session) > 0) {
+ int ret = gnutls_record_uncork(ctx->session, 0);
+ if (ret < 0 && gnutls_error_is_fatal(ret) != 0) {
+ WARN("TLS, failed to send (%s)", gnutls_strerror(ret));
+ return KNOT_NET_ESEND;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+int tls_ctx_receive(tls_ctx_t *ctx, uint8_t *buf, const size_t buf_len)
+{
+ if (ctx == NULL || buf == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ // Initialize poll descriptor structure.
+ struct pollfd pfd = {
+ .fd = ctx->sockfd,
+ .events = POLLIN,
+ .revents = 0,
+ };
+
+ uint32_t total = 0;
+ uint16_t msg_len = 0;
+
+ // Receive message header.
+ while (total < sizeof(msg_len)) {
+ ssize_t ret = gnutls_record_recv(ctx->session,
+ (uint8_t *)&msg_len + total,
+ sizeof(msg_len) - total);
+ if (ret > 0) {
+ total += ret;
+ } else if (ret == 0) {
+ WARN("TLS, peer has closed the connection");
+ return KNOT_NET_ERECV;
+ } else if (gnutls_error_is_fatal(ret) != 0) {
+ WARN("TLS, failed to receive reply (%s)",
+ gnutls_strerror(ret));
+ return KNOT_NET_ERECV;
+ } else if (poll(&pfd, 1, 1000 * ctx->wait) != 1) {
+ WARN("TLS, peer took too long to respond");
+ return KNOT_NET_ETIMEOUT;
+ }
+ }
+
+ // Convert number to host format.
+ msg_len = ntohs(msg_len);
+ if (msg_len > buf_len) {
+ return KNOT_ESPACE;
+ }
+
+ total = 0;
+
+ // Receive data over TLS
+ while (total < msg_len) {
+ ssize_t ret = gnutls_record_recv(ctx->session, buf + total,
+ msg_len - total);
+ if (ret > 0) {
+ total += ret;
+ } else if (ret == 0) {
+ WARN("TLS, peer has closed the connection");
+ return KNOT_NET_ERECV;
+ } else if (gnutls_error_is_fatal(ret) != 0) {
+ WARN("TLS, failed to receive reply (%s)",
+ gnutls_strerror(ret));
+ return KNOT_NET_ERECV;
+ } else if (poll(&pfd, 1, 1000 * ctx->wait) != 1) {
+ WARN("TLS, peer took too long to respond");
+ return KNOT_NET_ETIMEOUT;
+ }
+ }
+
+ return total;
+}
+
+void tls_ctx_close(tls_ctx_t *ctx)
+{
+ if (ctx == NULL || ctx->session == NULL || ctx->sockfd < 0) {
+ return;
+ }
+
+ gnutls_bye(ctx->session, GNUTLS_SHUT_RDWR);
+}
+
+void tls_ctx_deinit(tls_ctx_t *ctx)
+{
+ if (ctx == NULL) {
+ return;
+ }
+
+ if (ctx->credentials != NULL) {
+ gnutls_certificate_free_credentials(ctx->credentials);
+ ctx->credentials = NULL;
+ }
+ if (ctx->session != NULL) {
+ gnutls_deinit(ctx->session);
+ ctx->session = NULL;
+ }
+}
+
+void print_tls(const tls_ctx_t *ctx)
+{
+ if (ctx == NULL || ctx->params == NULL || !ctx->params->enable || ctx->session == NULL) {
+ return;
+ }
+
+ char *msg = gnutls_session_get_desc(ctx->session);
+ printf(";; TLS session %s\n", msg);
+ gnutls_free(msg);
+}
diff --git a/src/utils/common/tls.h b/src/utils/common/tls.h
new file mode 100644
index 0000000..623db1d
--- /dev/null
+++ b/src/utils/common/tls.h
@@ -0,0 +1,83 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdint.h>
+#include <netdb.h>
+#include <gnutls/gnutls.h>
+
+#include "contrib/sockaddr.h"
+#include "contrib/ucw/lists.h"
+
+#define CERT_PIN_LEN 32
+
+/*! \brief TLS parameters. */
+typedef struct {
+ /*! Use TLS indicator. */
+ bool enable;
+ /*! Import system certificates indicator. */
+ bool system_ca;
+ /*! Certificate files to import. */
+ list_t ca_files;
+ /*! Pinned certificates. */
+ list_t pins;
+ /*! Required server hostname. */
+ char *hostname;
+ /*! Optional server name indicator. */
+ char *sni;
+ /*! Optional client keyfile name. */
+ char *keyfile;
+ /*! Optional client certfile name. */
+ char *certfile;
+ /*! Optional validity of stapled OCSP response for the server cert. */
+ uint32_t ocsp_stapling;
+} tls_params_t;
+
+/*! \brief TLS context. */
+typedef struct {
+ /*! TLS handshake timeout. */
+ int wait;
+ /*! Socket descriptor. */
+ int sockfd;
+ /*! TLS parameters. */
+ const tls_params_t *params;
+ /*! GnuTLS session handle. */
+ gnutls_session_t session;
+ /*! GnuTLS credentials handle. */
+ gnutls_certificate_credentials_t credentials;
+} tls_ctx_t;
+
+extern const gnutls_datum_t dot_alpn;
+
+void tls_params_init(tls_params_t *params);
+int tls_params_copy(tls_params_t *dst, const tls_params_t *src);
+void tls_params_clean(tls_params_t *params);
+
+int tls_certificate_verification(tls_ctx_t *ctx);
+
+int tls_ctx_init(tls_ctx_t *ctx, const tls_params_t *params,
+ unsigned int flags, int wait);
+int tls_ctx_setup_remote_endpoint(tls_ctx_t *ctx, const gnutls_datum_t *alpn,
+ size_t alpn_size, const char *priority, const char *remote);
+int tls_ctx_connect(tls_ctx_t *ctx, int sockfd,
+ bool fastopen, struct sockaddr_storage *addr);
+
+int tls_ctx_send(tls_ctx_t *ctx, const uint8_t *buf, const size_t buf_len);
+int tls_ctx_receive(tls_ctx_t *ctx, uint8_t *buf, const size_t buf_len);
+void tls_ctx_close(tls_ctx_t *ctx);
+void tls_ctx_deinit(tls_ctx_t *ctx);
+void print_tls(const tls_ctx_t *ctx);
diff --git a/src/utils/common/token.c b/src/utils/common/token.c
new file mode 100644
index 0000000..10e788e
--- /dev/null
+++ b/src/utils/common/token.c
@@ -0,0 +1,115 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "utils/common/token.h"
+#include "utils/common/msg.h"
+#include "libknot/libknot.h"
+#include "contrib/ctype.h"
+
+int tok_scan(const char* lp, const char **tbl, int *lpm)
+{
+ if (lp == NULL || tbl == NULL || *tbl == NULL || lpm == NULL) {
+ DBG_NULL;
+ return -1;
+ }
+
+ const char *prefix = lp; /* Ptr to line start. */
+ int i = 0, pl = 1; /* Match index, prefix length. */
+ unsigned char len = 0; /* Read length. */
+ for(;;) {
+ const char *tok = tbl[i];
+ if (*lp == '\0' || is_space(*lp)) {
+ if (tok && TOK_L(tok) == len) { /* Consumed whole w? */
+ return i; /* Identifier */
+ } else { /* Word is shorter than cmd? */
+ break;
+ }
+ }
+
+ /* Find next prefix match. */
+ ++len;
+ while (tok) {
+ if (TOK_L(tok) >= len) { /* Is prefix of current token */
+ if (*lp < tok[pl]) { /* Terminate early. */
+ tok = NULL;
+ break; /* No match could be found. */
+ }
+ if (*lp == tok[pl]) { /* Match */
+ if(lpm) *lpm = i;
+ ++pl;
+ break;
+ }
+ }
+
+ /* No early cut, no match - seek next. */
+ while ((tok = tbl[++i]) != NULL) {
+ if (TOK_L(tok) >= len &&
+ memcmp(TOK_S(tok), prefix, len) == 0) {
+ break;
+ }
+ }
+ }
+
+ if (tok == NULL) {
+ break; /* All tokens exhausted. */
+ } else {
+ ++lp; /* Next char */
+ }
+ }
+
+ return -1;
+}
+
+int tok_find(const char *lp, const char **tbl)
+{
+ if (lp == NULL || tbl == NULL || *tbl == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ int lpm = -1;
+ int bp = 0;
+ if ((bp = tok_scan(lp, tbl, &lpm)) < 0) {
+ if (lpm > -1) {
+ ERR("unexpected literal: '%s', did you mean '%s' ?",
+ lp, TOK_S(tbl[lpm]));
+ } else {
+ ERR("unexpected literal: '%s'", lp);
+ }
+
+ return KNOT_EPARSEFAIL;
+ }
+
+ return bp;
+}
+
+const char *tok_skipspace(const char *lp)
+{
+ if (lp == NULL) {
+ DBG_NULL;
+ return NULL;
+ }
+
+ while (is_space(*lp)) {
+ lp += 1;
+ }
+
+ return lp;
+}
diff --git a/src/utils/common/token.h b/src/utils/common/token.h
new file mode 100644
index 0000000..fab4ea1
--- /dev/null
+++ b/src/utils/common/token.h
@@ -0,0 +1,65 @@
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdio.h>
+
+/*!
+ * \brief Example of token table:
+ *
+ * \warning Table _must_ be lexicographically ordered.
+ *
+ * const char *tok_tbl[] = {
+ * // LEN STRING
+ * "\x4" "abcd",
+ * "\x5" "class",
+ * NULL // END
+ * }
+ */
+/*! \brief String part of the token. */
+#define TOK_S(x) ((x)+1)
+/*! \brief Len of the token. */
+#define TOK_L(x) ((unsigned char)(x)[0])
+
+/*!
+ * \brief Scan for matching token described by a match table.
+ *
+ * Table consists of strings, prefixed with 1B length.
+ *
+ * \param lp Pointer to current line.
+ * \param tbl Match description table.
+ * \param lpm Pointer to longest prefix match.
+ * \retval index to matching record.
+ * \retval -1 if no match is found, lpm may be set to longest prefix match.
+ */
+int tok_scan(const char* lp, const char **tbl, int *lpm);
+
+/*!
+ * \brief Find token from table in a line buffer.
+ * \param lp Pointer to current line.
+ * \param tbl Match description table.
+ * \retval index to matching record.
+ * \retval error code if no match is found
+ */
+int tok_find(const char *lp, const char **tbl);
+
+/*!
+ * \brief Return pointer to next non-blank character.
+ * \param lp Pointer to current line.
+ * \return ptr to next non-blank character.
+ */
+const char *tok_skipspace(const char *lp);
diff --git a/src/utils/common/util_conf.c b/src/utils/common/util_conf.c
new file mode 100644
index 0000000..a98f526
--- /dev/null
+++ b/src/utils/common/util_conf.c
@@ -0,0 +1,139 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "utils/common/util_conf.h"
+
+#include "contrib/string.h"
+#include "knot/common/log.h"
+#include "knot/conf/conf.h"
+#include "libknot/attribute.h"
+#include "utils/common/msg.h"
+
+bool util_conf_initialized(void)
+{
+ return (conf() != NULL);
+}
+
+int util_conf_init_confdb(const char *confdb)
+{
+ if (util_conf_initialized()) {
+ ERR2("configuration already initialized");
+ util_conf_deinit();
+ return KNOT_ESEMCHECK;
+ }
+
+ size_t max_conf_size = (size_t)CONF_MAPSIZE * 1024 * 1024;
+
+ conf_flag_t flags = CONF_FNOHOSTNAME | CONF_FOPTMODULES;
+ if (confdb != NULL) {
+ flags |= CONF_FREADONLY;
+ }
+
+ log_init();
+ log_levels_set(LOG_TARGET_STDOUT, LOG_SOURCE_ANY, 0);
+ log_levels_set(LOG_TARGET_STDERR, LOG_SOURCE_ANY, LOG_UPTO(LOG_WARNING));
+ log_levels_set(LOG_TARGET_SYSLOG, LOG_SOURCE_ANY, 0);
+ log_flag_set(LOG_FLAG_NOTIMESTAMP | LOG_FLAG_NOINFO);
+
+ conf_t *new_conf = NULL;
+ int ret = conf_new(&new_conf, conf_schema, confdb, max_conf_size, flags);
+ if (ret != KNOT_EOK) {
+ ERR2("failed opening configuration database '%s' (%s)",
+ (confdb == NULL ? "" : confdb), knot_strerror(ret));
+ } else {
+ conf_update(new_conf, CONF_UPD_FNONE);
+ }
+ return ret;
+}
+
+int util_conf_init_file(const char *conffile)
+{
+ int ret = util_conf_init_confdb(NULL);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = conf_import(conf(), conffile, IMPORT_FILE);
+ if (ret != KNOT_EOK) {
+ ERR2("failed opening configuration file '%s' (%s)",
+ conffile, knot_strerror(ret));
+ }
+ return ret;
+}
+
+int util_conf_init_justdb(const char *db_type, const char *db_path)
+{
+ int ret = util_conf_init_confdb(NULL);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ char *conf_str = sprintf_alloc("database:\n"
+ " storage: .\n"
+ " %s: \"%s\"\n", db_type, db_path);
+ if (conf_str == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ ret = conf_import(conf(), conf_str, 0);
+ free(conf_str);
+ if (ret != KNOT_EOK) {
+ ERR2("failed creating temporary configuration (%s)", knot_strerror(ret));
+ }
+ return ret;
+}
+
+int util_conf_init_default(bool allow_db)
+{
+ struct stat st;
+ if (util_conf_initialized()) {
+ return KNOT_EOK;
+ } else if (conf_db_exists(CONF_DEFAULT_DBDIR)) {
+ return util_conf_init_confdb(CONF_DEFAULT_DBDIR);
+ } else if (stat(CONF_DEFAULT_FILE, &st) == 0) {
+ return util_conf_init_file(CONF_DEFAULT_FILE);
+ } else {
+ ERR2("couldn't initialize configuration, please provide %s option",
+ (allow_db ? "-c, -C, or -D" : "-c or -C"));
+ return KNOT_EINVAL;
+ }
+}
+
+void util_update_privileges(void)
+{
+ int uid, gid;
+ if (conf_user(conf(), &uid, &gid) != KNOT_EOK) {
+ return;
+ }
+
+ // Just try to alter process privileges if different from configured.
+ _unused_ int unused;
+ if ((gid_t)gid != getgid()) {
+ unused = setregid(gid, gid);
+ }
+ if ((uid_t)uid != getuid()) {
+ unused = setreuid(uid, uid);
+ }
+}
+
+void util_conf_deinit(void)
+{
+ log_close();
+ conf_update(NULL, CONF_UPD_FNONE);
+}
diff --git a/src/utils/common/util_conf.h b/src/utils/common/util_conf.h
new file mode 100644
index 0000000..a71d886
--- /dev/null
+++ b/src/utils/common/util_conf.h
@@ -0,0 +1,86 @@
+/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+#include "knot/conf/conf.h"
+
+/*!
+ * General note:
+ *
+ * Those functions operate and manipulate with conf() singleton.
+ * Thus they are not threadsafe etc.
+ * It is expected to use them just inside the main() function.
+ *
+ * Those functions already log any error, while returning an errcode.
+ */
+
+/*!
+ * \brief Return true if conf() for utilities already exists.
+ */
+bool util_conf_initialized(void);
+
+/*!
+ * \brief Initialize conf() for utilities from a configuration database.
+ *
+ * \param confdb Path to configuration database.
+ *
+ * \return KNOT_E*
+ */
+int util_conf_init_confdb(const char *confdb);
+
+/*!
+ * \brief Initialize conf() for utilities from a config file.
+ *
+ * \param conffile Path to Knot configuration file.
+ *
+ * \return KNOT_E*
+ */
+int util_conf_init_file(const char *conffile);
+
+/*!
+ * \brief Initialize basic conf() for utilities just with defaults and some database path.
+ *
+ * \param db_type Type of the database to be configured.
+ * \param db_path Path to that database.
+ *
+ * \return KNOT_E*
+ */
+int util_conf_init_justdb(const char *db_type, const char *db_path);
+
+/*!
+ * \brief Initialize conf() for utilities based on existence of confDB or config
+ * file on default locations.
+ *
+ * \param allow_db Direct path to a database is allowed.
+ *
+ * \return KNOT_E*
+ */
+int util_conf_init_default(bool allow_db);
+
+/*!
+ * \brief Set UID and GID of running utility process to what is configured...
+ *
+ * ...so that e.g. opened files have correct owner.
+ */
+void util_update_privileges(void);
+
+/*!
+ * \brief Deinitialize utility conf() from util_conf_init_*().
+ */
+void util_conf_deinit(void);
diff --git a/src/utils/kcatalogprint/main.c b/src/utils/kcatalogprint/main.c
new file mode 100644
index 0000000..0172347
--- /dev/null
+++ b/src/utils/kcatalogprint/main.c
@@ -0,0 +1,185 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <getopt.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "knot/catalog/catalog_db.h"
+#include "utils/common/msg.h"
+#include "utils/common/params.h"
+#include "utils/common/signal.h"
+#include "utils/common/util_conf.h"
+
+#define PROGRAM_NAME "kcatalogprint"
+
+static knot_dname_t *filter_member = NULL;
+static knot_dname_t *filter_catalog = NULL;
+
+signal_ctx_t signal_ctx = { 0 }; // global, needed by signal handler
+
+static void print_help(void)
+{
+ printf("Usage: %s [-c | -C | -D <path>] [options]\n"
+ "\n"
+ "Config options:\n"
+ " -c, --config <file> Path to a textual configuration file.\n"
+ " (default %s)\n"
+ " -C, --confdb <dir> Path to a configuration database directory.\n"
+ " (default %s)\n"
+ " -D, --dir <path> Path to a catalog database directory, use default\n"
+ " configuration.\n"
+ "Options:\n"
+ " -a, --catalog <name> Filter the output by catalog zone name.\n"
+ " -m, --member <name> Filter the output by member zone name.\n"
+ " -h, --help Print the program help.\n"
+ " -V, --version Print the program version.\n",
+ PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR);
+}
+
+static void print_dname(const knot_dname_t *d)
+{
+ knot_dname_txt_storage_t tmp;
+ knot_dname_to_str(tmp, d, sizeof(tmp));
+ printf("%s ", tmp);
+}
+
+static int catalog_print_cb(const knot_dname_t *mem, const knot_dname_t *ow,
+ const knot_dname_t *cz, const char *group, void *ctx)
+{
+ if (filter_catalog != NULL && !knot_dname_is_equal(filter_catalog, cz)) {
+ return KNOT_EOK;
+ }
+ print_dname(mem);
+ print_dname(ow);
+ print_dname(cz);
+ printf("%s\n", group);
+ (*(ssize_t *)ctx)++;
+ return KNOT_EOK;
+}
+
+static void catalog_print(catalog_t *cat)
+{
+ ssize_t total = 0;
+
+ printf(";; <member zone> <record owner> <catalog zone> <group>\n");
+
+ if (cat != NULL) {
+ int ret = catalog_open(cat);
+ if (ret == KNOT_EOK) {
+ ret = catalog_apply(cat, filter_member, catalog_print_cb, &total, false);
+ }
+ if (ret != KNOT_EOK) {
+ ERR2("failed to print catalog (%s)", knot_strerror(ret));
+ return;
+ }
+ }
+
+ printf("Total records: %zd\n", total);
+}
+
+static void params_cleanup(void)
+{
+ free(filter_member);
+ free(filter_catalog);
+}
+
+int main(int argc, char *argv[])
+{
+ catalog_t c = { { 0 } };
+
+ struct option opts[] = {
+ { "config", required_argument, NULL, 'c' },
+ { "confdb", required_argument, NULL, 'C' },
+ { "dir", required_argument, NULL, 'D' },
+ { "catalog", required_argument, NULL, 'a' },
+ { "member", required_argument, NULL, 'm' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL }
+ };
+
+ signal_ctx.close_db = &c.db;
+ signal_init_std();
+
+ int opt = 0;
+ while ((opt = getopt_long(argc, argv, "c:C:D:a:m:hV", opts, NULL)) != -1) {
+ switch (opt) {
+ case 'c':
+ if (util_conf_init_file(optarg) != KNOT_EOK) {
+ goto failure;
+ }
+ break;
+ case 'C':
+ if (util_conf_init_confdb(optarg) != KNOT_EOK) {
+ goto failure;
+ }
+ break;
+ case 'D':
+ if (util_conf_init_justdb("catalog-db", optarg) != KNOT_EOK) {
+ goto failure;
+ }
+ break;
+ case 'a':
+ free(filter_catalog);
+ filter_catalog = knot_dname_from_str_alloc(optarg);
+ knot_dname_to_lower(filter_catalog);
+ break;
+ case 'm':
+ free(filter_member);
+ filter_member = knot_dname_from_str_alloc(optarg);
+ knot_dname_to_lower(filter_member);
+ break;
+ case 'h':
+ print_help();
+ goto success;
+ case 'V':
+ print_version(PROGRAM_NAME);
+ goto success;
+ default:
+ print_help();
+ goto failure;
+ }
+ }
+
+ // Backward compatibility.
+ if (argc - optind > 0) {
+ WARN2("obsolete parameter specified");
+ if (util_conf_init_justdb("catalog-db", argv[optind]) != KNOT_EOK) {
+ goto failure;
+ }
+ optind++;
+ }
+
+ if (util_conf_init_default(true) != KNOT_EOK) {
+ goto failure;
+ }
+
+ char *db = conf_db(conf(), C_CATALOG_DB);
+ catalog_init(&c, db, 0); // mapsize grows automatically
+ free(db);
+ catalog_print(&c);
+ catalog_deinit(&c);
+
+success:
+ params_cleanup();
+ util_conf_deinit();
+ return EXIT_SUCCESS;
+failure:
+ params_cleanup();
+ util_conf_deinit();
+ return EXIT_FAILURE;
+}
diff --git a/src/utils/kdig/kdig_exec.c b/src/utils/kdig/kdig_exec.c
new file mode 100644
index 0000000..7733254
--- /dev/null
+++ b/src/utils/kdig/kdig_exec.c
@@ -0,0 +1,1324 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+
+#include "utils/kdig/kdig_exec.h"
+#include "utils/common/exec.h"
+#include "utils/common/msg.h"
+#include "utils/common/netio.h"
+#include "utils/common/sign.h"
+#include "libknot/libknot.h"
+#include "contrib/json.h"
+#include "contrib/sockaddr.h"
+#include "contrib/time.h"
+#include "contrib/ucw/lists.h"
+
+#if USE_DNSTAP
+#include "contrib/dnstap/convert.h"
+#include "contrib/dnstap/message.h"
+#include "contrib/dnstap/writer.h"
+#include "libknot/probe/data.h"
+
+static int write_dnstap(dt_writer_t *writer,
+ const bool is_query,
+ const uint8_t *wire,
+ const size_t wire_len,
+ net_t *net,
+ const struct timespec *mtime)
+{
+ Dnstap__Message msg;
+ Dnstap__Message__Type msg_type;
+ int ret;
+ int protocol = 0;
+
+ if (writer == NULL) {
+ return KNOT_EOK;
+ }
+
+ if (net->local == NULL) {
+ net_set_local_info(net);
+ }
+
+ msg_type = is_query ? DNSTAP__MESSAGE__TYPE__TOOL_QUERY :
+ DNSTAP__MESSAGE__TYPE__TOOL_RESPONSE;
+
+ if (net->socktype == SOCK_DGRAM) {
+ protocol = IPPROTO_UDP;
+ } else if (net->socktype == SOCK_STREAM) {
+ protocol = IPPROTO_TCP;
+ }
+
+ ret = dt_message_fill(&msg, msg_type, net->local_info->ai_addr,
+ net->srv->ai_addr, protocol,
+ wire, wire_len, mtime);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ return dt_writer_write(writer, (const ProtobufCMessage *)&msg);
+}
+
+static float get_query_time(const Dnstap__Dnstap *frame)
+{
+ if (!frame->message->has_query_time_sec ||
+ !frame->message->has_query_time_nsec ||
+ !frame->message->has_response_time_sec ||
+ !frame->message->has_response_time_sec) {
+ return 0;
+ }
+
+ struct timespec from = {
+ .tv_sec = frame->message->query_time_sec,
+ .tv_nsec = frame->message->query_time_nsec
+ };
+
+ struct timespec to = {
+ .tv_sec = frame->message->response_time_sec,
+ .tv_nsec = frame->message->response_time_nsec
+ };
+
+ return time_diff_ms(&from, &to);
+}
+
+static void fill_remote_addr(net_t *net, Dnstap__Message *message, bool is_initiator)
+{
+ if (!message->has_socket_family || !message->has_socket_protocol) {
+ return;
+ }
+
+ if ((message->response_address.data == NULL && is_initiator) ||
+ message->query_address.data == NULL) {
+ return;
+ }
+
+ struct sockaddr_storage ss = { 0 };
+ int family = dt_family_decode(message->socket_family);
+ knot_probe_proto_t proto = dt_protocol_decode(message->socket_protocol);
+
+ ProtobufCBinaryData *addr = NULL;
+ uint32_t port = 0;
+ if (is_initiator) {
+ addr = &message->response_address;
+ port = message->response_port;
+ } else {
+ addr = &message->query_address;
+ port = message->query_port;
+ }
+
+ sockaddr_set_raw(&ss, family, addr->data, addr->len);
+ sockaddr_port_set(&ss, port);
+
+ get_addr_str(&ss, proto, &net->remote_str);
+}
+
+static int process_dnstap(const query_t *query)
+{
+ dt_reader_t *reader = query->dt_reader;
+
+ if (query->dt_reader == NULL) {
+ return -1;
+ }
+
+ bool first_message = true;
+
+ for (;;) {
+ Dnstap__Dnstap *frame = NULL;
+ Dnstap__Message *message = NULL;
+ ProtobufCBinaryData *wire = NULL;
+ bool is_query;
+ bool is_initiator;
+
+ // Read next message.
+ int ret = dt_reader_read(reader, &frame);
+ if (ret == KNOT_EOF) {
+ break;
+ } else if (ret != KNOT_EOK) {
+ ERR("can't read dnstap message");
+ break;
+ }
+
+ // Check for dnstap message.
+ if (frame->type == DNSTAP__DNSTAP__TYPE__MESSAGE) {
+ message = frame->message;
+ } else {
+ WARN("ignoring non-dnstap message");
+ dt_reader_free_frame(reader, &frame);
+ continue;
+ }
+
+ // Check for the type of dnstap message.
+ if (message->has_response_message) {
+ wire = &message->response_message;
+ is_query = false;
+ } else if (message->has_query_message) {
+ wire = &message->query_message;
+ is_query = true;
+ } else {
+ WARN("dnstap frame contains no message");
+ dt_reader_free_frame(reader, &frame);
+ continue;
+ }
+
+ // Ignore query message if requested.
+ if (is_query && !query->style.show_query) {
+ dt_reader_free_frame(reader, &frame);
+ continue;
+ }
+
+ // Get the message role.
+ is_initiator = dt_message_role_is_initiator(message->type);
+
+ // Create dns packet based on dnstap wire data.
+ knot_pkt_t *pkt = knot_pkt_new(wire->data, wire->len, NULL);
+ if (pkt == NULL) {
+ ERR("can't allocate packet");
+ dt_reader_free_frame(reader, &frame);
+ break;
+ }
+
+ // Parse packet and reconstruct required data.
+ ret = knot_pkt_parse(pkt, KNOT_PF_NOCANON);
+ if (ret == KNOT_EOK || ret == KNOT_ETRAIL) {
+ time_t timestamp = 0;
+ float query_time = 0.0;
+ net_t net_ctx = { 0 };
+
+ if (ret == KNOT_ETRAIL) {
+ WARN("malformed message (%s)", knot_strerror(ret));
+ }
+
+ if (is_query) {
+ if (message->has_query_time_sec) {
+ timestamp = message->query_time_sec;
+ }
+ } else {
+ if (message->has_response_time_sec) {
+ timestamp = message->response_time_sec;
+ }
+ query_time = get_query_time(frame);
+ }
+
+ // Prepare connection information string.
+ fill_remote_addr(&net_ctx, message, is_initiator);
+
+ if (first_message) {
+ first_message = false;
+ } else {
+ printf("\n");
+ }
+
+ print_packet(pkt, &net_ctx, pkt->size, query_time, timestamp,
+ is_query ^ is_initiator, &query->style);
+
+ net_clean(&net_ctx);
+ } else {
+ ERR("can't print dnstap message");
+ }
+
+ knot_pkt_free(pkt);
+ dt_reader_free_frame(reader, &frame);
+ }
+
+ return 0;
+}
+#endif // USE_DNSTAP
+
+static int add_query_edns(knot_pkt_t *packet, const query_t *query, uint16_t max_size)
+{
+ /* Initialize OPT RR. */
+ knot_rrset_t opt_rr;
+ int ret = knot_edns_init(&opt_rr, max_size, 0,
+ query->edns > -1 ? query->edns : 0, &packet->mm);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (query->flags.do_flag) {
+ knot_edns_set_do(&opt_rr);
+ }
+
+ /* Append NSID. */
+ if (query->nsid) {
+ ret = knot_edns_add_option(&opt_rr, KNOT_EDNS_OPTION_NSID,
+ 0, NULL, &packet->mm);
+ if (ret != KNOT_EOK) {
+ knot_rrset_clear(&opt_rr, &packet->mm);
+ return ret;
+ }
+ }
+
+ /* Append EDNS-client-subnet. */
+ if (query->subnet.family != AF_UNSPEC) {
+ uint16_t size = knot_edns_client_subnet_size(&query->subnet);
+ uint8_t data[size];
+
+ ret = knot_edns_client_subnet_write(data, size, &query->subnet);
+ if (ret != KNOT_EOK) {
+ knot_rrset_clear(&opt_rr, &packet->mm);
+ return ret;
+ }
+
+ ret = knot_edns_add_option(&opt_rr, KNOT_EDNS_OPTION_CLIENT_SUBNET,
+ size, data, &packet->mm);
+ if (ret != KNOT_EOK) {
+ knot_rrset_clear(&opt_rr, &packet->mm);
+ return ret;
+ }
+ }
+
+ /* Append a cookie option if present. */
+ if (query->cc.len > 0) {
+ uint16_t size = knot_edns_cookie_size(&query->cc, &query->sc);
+ uint8_t data[size];
+
+ ret = knot_edns_cookie_write(data, size, &query->cc, &query->sc);
+ if (ret != KNOT_EOK) {
+ knot_rrset_clear(&opt_rr, &packet->mm);
+ return ret;
+ }
+
+ ret = knot_edns_add_option(&opt_rr, KNOT_EDNS_OPTION_COOKIE,
+ size, data, &packet->mm);
+ if (ret != KNOT_EOK) {
+ knot_rrset_clear(&opt_rr, &packet->mm);
+ return ret;
+ }
+ }
+
+ /* Append EDNS Padding. */
+ int padding = query->padding;
+ if (padding != -3 && query->alignment > 0) {
+ padding = knot_edns_alignment_size(packet->size,
+ knot_rrset_size(&opt_rr),
+ query->alignment);
+ } else if (query->padding == -2 || (query->padding == -1 && query->tls.enable)) {
+ padding = knot_pkt_default_padding_size(packet, &opt_rr);
+ }
+ if (padding > -1) {
+ uint8_t zeros[padding];
+ memset(zeros, 0, sizeof(zeros));
+
+ ret = knot_edns_add_option(&opt_rr, KNOT_EDNS_OPTION_PADDING,
+ padding, zeros, &packet->mm);
+ if (ret != KNOT_EOK) {
+ knot_rrset_clear(&opt_rr, &packet->mm);
+ return ret;
+ }
+ }
+
+ /* Append custom EDNS options. */
+ node_t *node;
+ WALK_LIST(node, query->edns_opts) {
+ ednsopt_t *opt = (ednsopt_t *)node;
+ ret = knot_edns_add_option(&opt_rr, opt->code, opt->length,
+ opt->data, &packet->mm);
+ if (ret != KNOT_EOK) {
+ knot_rrset_clear(&opt_rr, &packet->mm);
+ return ret;
+ }
+ }
+
+ /* Add prepared OPT to packet. */
+ ret = knot_pkt_put(packet, KNOT_COMPR_HINT_NONE, &opt_rr, KNOT_PF_FREE);
+ if (ret != KNOT_EOK) {
+ knot_rrset_clear(&opt_rr, &packet->mm);
+ }
+
+ return ret;
+}
+
+static bool do_padding(const query_t *query)
+{
+ return (query->padding != -3) && // Disabled padding.
+ (query->padding > -1 || query->alignment > 0 || // Explicit padding.
+ query->padding == -2 || // Default padding.
+ (query->padding == -1 && query->tls.enable)); // TLS automatic.
+}
+
+static bool use_edns(const query_t *query)
+{
+ return query->edns > -1 || query->udp_size > -1 || query->nsid ||
+ query->flags.do_flag || query->subnet.family != AF_UNSPEC ||
+ query->cc.len > 0 || do_padding(query) ||
+ !ednsopt_list_empty(&query->edns_opts);
+}
+
+static knot_pkt_t *create_query_packet(const query_t *query)
+{
+ // Set packet buffer size.
+ uint16_t max_size;
+ if (query->udp_size < 0) {
+ if (use_edns(query)) {
+ max_size = DEFAULT_EDNS_SIZE;
+ } else {
+ max_size = DEFAULT_UDP_SIZE;
+ }
+ } else {
+ max_size = query->udp_size;
+ }
+
+ // Create packet skeleton.
+ knot_pkt_t *packet = create_empty_packet(max_size);
+ if (packet == NULL) {
+ return NULL;
+ }
+
+ // Set ID = 0 for packet send over HTTPS
+ // Due HTTP cache it is convenient to set the query ID to 0 - GET messages has same header then
+#if defined(LIBNGHTTP2) || defined(ENABLE_QUIC)
+ if (query->https.enable || query->quic.enable) {
+ knot_wire_set_id(packet->wire, 0);
+ }
+#endif
+
+ // Set flags to wireformat.
+ if (query->flags.aa_flag) {
+ knot_wire_set_aa(packet->wire);
+ }
+ if (query->flags.tc_flag) {
+ knot_wire_set_tc(packet->wire);
+ }
+ if (query->flags.rd_flag) {
+ knot_wire_set_rd(packet->wire);
+ }
+ if (query->flags.ra_flag) {
+ knot_wire_set_ra(packet->wire);
+ }
+ if (query->flags.z_flag) {
+ knot_wire_set_z(packet->wire);
+ }
+ if (query->flags.ad_flag) {
+ knot_wire_set_ad(packet->wire);
+ }
+ if (query->flags.cd_flag) {
+ knot_wire_set_cd(packet->wire);
+ }
+
+ // Set NOTIFY opcode.
+ if (query->notify) {
+ knot_wire_set_opcode(packet->wire, KNOT_OPCODE_NOTIFY);
+ }
+
+ // Set packet question if available.
+ knot_dname_t *qname = NULL;
+ if (query->owner != NULL) {
+ qname = knot_dname_from_str_alloc(query->owner);
+ if (qname == NULL) {
+ ERR("'%s' is not a valid domain name", query->owner);
+ knot_pkt_free(packet);
+ return NULL;
+ }
+
+ int ret = knot_pkt_put_question(packet, qname, query->class_num,
+ query->type_num);
+ if (ret != KNOT_EOK) {
+ knot_dname_free(qname, NULL);
+ knot_pkt_free(packet);
+ return NULL;
+ }
+ }
+
+ // For IXFR query or NOTIFY query with SOA serial, add a proper section.
+ if (query->serial >= 0) {
+ if (query->notify) {
+ knot_pkt_begin(packet, KNOT_ANSWER);
+ } else {
+ knot_pkt_begin(packet, KNOT_AUTHORITY);
+ }
+
+ // SOA rdata in wireformat.
+ uint8_t wire[22] = { 0x0 };
+
+ // Create rrset with SOA record.
+ knot_rrset_t *soa = knot_rrset_new(qname,
+ KNOT_RRTYPE_SOA,
+ query->class_num,
+ 0,
+ &packet->mm);
+ knot_dname_free(qname, NULL);
+ if (soa == NULL) {
+ knot_pkt_free(packet);
+ return NULL;
+ }
+
+ // Fill in blank SOA rdata to rrset.
+ int ret = knot_rrset_add_rdata(soa, wire, sizeof(wire), &packet->mm);
+ if (ret != KNOT_EOK) {
+ knot_rrset_free(soa, &packet->mm);
+ knot_pkt_free(packet);
+ return NULL;
+ }
+
+ // Set SOA serial.
+ knot_soa_serial_set(soa->rrs.rdata, query->serial);
+
+ ret = knot_pkt_put(packet, KNOT_COMPR_HINT_NONE, soa, KNOT_PF_FREE);
+ if (ret != KNOT_EOK) {
+ knot_rrset_free(soa, &packet->mm);
+ knot_pkt_free(packet);
+ return NULL;
+ }
+
+ free(soa);
+ } else {
+ knot_dname_free(qname, NULL);
+ }
+
+ // Begin additional section
+ knot_pkt_begin(packet, KNOT_ADDITIONAL);
+
+ // Create EDNS section if required.
+ if (use_edns(query)) {
+ int ret = add_query_edns(packet, query, max_size);
+ if (ret != KNOT_EOK) {
+ ERR("can't set up EDNS section");
+ knot_pkt_free(packet);
+ return NULL;
+ }
+ }
+
+ return packet;
+}
+
+static bool check_reply_id(const knot_pkt_t *reply,
+ const knot_pkt_t *query)
+{
+ uint16_t query_id = knot_wire_get_id(query->wire);
+ uint16_t reply_id = knot_wire_get_id(reply->wire);
+
+ if (reply_id != query_id) {
+ WARN("reply ID (%u) is different from query ID (%u)",
+ reply_id, query_id);
+ return false;
+ }
+
+ return true;
+}
+
+static void check_reply_qr(const knot_pkt_t *reply)
+{
+ if (!knot_wire_get_qr(reply->wire)) {
+ WARN("response QR bit not set");
+ }
+}
+
+static void check_reply_question(const knot_pkt_t *reply,
+ const knot_pkt_t *query)
+{
+ if (knot_wire_get_qdcount(reply->wire) < 1) {
+ WARN("response doesn't have question section");
+ return;
+ }
+
+ if (!knot_dname_is_equal(knot_pkt_wire_qname(reply), knot_pkt_wire_qname(query)) ||
+ knot_pkt_qclass(reply) != knot_pkt_qclass(query) ||
+ knot_pkt_qtype(reply) != knot_pkt_qtype(query)) {
+ WARN("query/response question sections are different");
+ return;
+ }
+}
+
+static int64_t first_serial_check(const knot_pkt_t *reply, const knot_pkt_t *query)
+{
+ const knot_pktsection_t *answer = knot_pkt_section(reply, KNOT_ANSWER);
+
+ if (answer->count <= 0) {
+ return -1;
+ }
+
+ const knot_rrset_t *first = knot_pkt_rr(answer, 0);
+
+ if (first->type != KNOT_RRTYPE_SOA) {
+ return -1;
+ } else {
+ if (!knot_dname_is_case_equal(first->owner, knot_pkt_qname(query))) {
+ WARN("leading SOA owner not matching the requested zone name");
+ }
+
+ return knot_soa_serial(first->rrs.rdata);
+ }
+}
+
+static bool finished_xfr(const uint32_t serial, const knot_pkt_t *reply,
+ const knot_pkt_t *query, const size_t msg_count, bool is_ixfr)
+{
+ const knot_pktsection_t *answer = knot_pkt_section(reply, KNOT_ANSWER);
+
+ if (answer->count <= 0) {
+ return false;
+ }
+
+ const knot_rrset_t *last = knot_pkt_rr(answer, answer->count - 1);
+
+ if (last->type != KNOT_RRTYPE_SOA) {
+ return false;
+ } else if (answer->count == 1 && msg_count == 1) {
+ return is_ixfr;
+ } else {
+ if (!knot_dname_is_case_equal(last->owner, knot_pkt_qname(query))) {
+ WARN("final SOA owner not matching the requested zone name");
+ }
+
+ return knot_soa_serial(last->rrs.rdata) == serial;
+ }
+}
+
+static int sign_query(knot_pkt_t *pkt, const query_t *query, sign_context_t *ctx)
+{
+ if (query->tsig_key.name == NULL) {
+ return KNOT_EOK;
+ }
+
+ int ret = sign_context_init_tsig(ctx, &query->tsig_key);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = sign_packet(pkt, ctx);
+ if (ret != KNOT_EOK) {
+ sign_context_deinit(ctx);
+ return ret;
+ }
+
+ return KNOT_EOK;
+}
+
+static void net_close_keepopen(net_t *net, const query_t *query)
+{
+ if (!query->keepopen) {
+ net_close(net);
+ }
+}
+
+static int process_query_packet(const knot_pkt_t *query,
+ net_t *net,
+ const query_t *query_ctx,
+ const bool ignore_tc,
+ const sign_context_t *sign_ctx,
+ const style_t *style)
+{
+ struct timespec t_start, t_query, t_end;
+ time_t timestamp;
+ knot_pkt_t *reply = NULL;
+ uint8_t in[MAX_PACKET_SIZE];
+ int in_len = 0;
+ int ret;
+
+ // Get start query time.
+ timestamp = time(NULL);
+ t_start = time_now();
+
+ // Connect to the server if not already connected.
+ if (net->sockfd < 0) {
+ ret = net_connect(net);
+ if (ret != KNOT_EOK) {
+ return -1;
+ }
+ }
+
+ // Send query packet.
+ ret = net_send(net, query->wire, query->size);
+ if (ret != KNOT_EOK) {
+ net_close(net);
+ return -1;
+ }
+
+ // Get stop query time and start reply time.
+ t_query = time_now();
+
+#if USE_DNSTAP
+ struct timespec t_query_full = time_diff(&t_start, &t_query);
+ t_query_full.tv_sec += timestamp;
+
+ // Make the dnstap copy of the query.
+ write_dnstap(query_ctx->dt_writer, true, query->wire, query->size,
+ net, &t_query_full);
+#endif // USE_DNSTAP
+
+ // Print query packet if required.
+ if (style->show_query && style->format != FORMAT_JSON) {
+ // Create copy of query packet for parsing.
+ knot_pkt_t *q = knot_pkt_new(query->wire, query->size, NULL);
+ if (q != NULL) {
+ if (knot_pkt_parse(q, KNOT_PF_NOCANON) == KNOT_EOK) {
+ print_packet(q, net, query->size,
+ time_diff_ms(&t_start, &t_query),
+ timestamp, false, style);
+ } else {
+ ERR("can't print query packet");
+ }
+ knot_pkt_free(q);
+ } else {
+ ERR("can't print query packet");
+ }
+
+ printf("\n");
+ }
+
+ // Loop over incoming messages, unless reply id is correct or timeout.
+ while (true) {
+ reply = NULL;
+
+ // Receive a reply message.
+ in_len = net_receive(net, in, sizeof(in));
+ t_end = time_now();
+ if (in_len <= 0) {
+ goto fail;
+ }
+
+#if USE_DNSTAP
+ struct timespec t_end_full = time_diff(&t_start, &t_end);
+ t_end_full.tv_sec += timestamp;
+
+ // Make the dnstap copy of the response.
+ write_dnstap(query_ctx->dt_writer, false, in, in_len, net,
+ &t_end_full);
+#endif // USE_DNSTAP
+
+ // Create reply packet structure to fill up.
+ reply = knot_pkt_new(in, in_len, NULL);
+ if (reply == NULL) {
+ ERR("internal error (%s)", knot_strerror(KNOT_ENOMEM));
+ goto fail;
+ }
+
+ // Parse reply to the packet structure.
+ ret = knot_pkt_parse(reply, KNOT_PF_NOCANON);
+ if (ret == KNOT_ETRAIL) {
+ WARN("malformed reply packet (%s)", knot_strerror(ret));
+ } else if (ret != KNOT_EOK) {
+ ERR("malformed reply packet from %s", net->remote_str);
+ goto fail;
+ }
+
+ // Compare reply header id.
+ if (check_reply_id(reply, query)) {
+ break;
+ // Check for timeout.
+ } else if (time_diff_ms(&t_query, &t_end) > 1000 * net->wait) {
+ goto fail;
+ }
+
+ knot_pkt_free(reply);
+ }
+
+ // Check for TC bit and repeat query with TCP if required.
+ if (knot_wire_get_tc(reply->wire) != 0 &&
+ ignore_tc == false && net->socktype == SOCK_DGRAM) {
+ printf("\n");
+ WARN("truncated reply from %s, retrying over TCP\n",
+ net->remote_str);
+ knot_pkt_free(reply);
+ net_close_keepopen(net, query_ctx);
+
+ net->socktype = SOCK_STREAM;
+
+ return process_query_packet(query, net, query_ctx, true,
+ sign_ctx, style);
+ }
+
+ // Check for question sections equality.
+ check_reply_question(reply, query);
+
+ // Check QR bit
+ check_reply_qr(reply);
+
+ // Print reply packet.
+ if (style->format != FORMAT_JSON) {
+ // Intentionaly start-end because of QUIC can have receive time.
+ print_packet(reply, net, in_len, time_diff_ms(&t_start, &t_end),
+ timestamp, true, style);
+ } else {
+ knot_pkt_t *q = knot_pkt_new(query->wire, query->size, NULL);
+ (void)knot_pkt_parse(q, KNOT_PF_NOCANON);
+ print_packets_json(q, reply, net, timestamp, style);
+ knot_pkt_free(q);
+ }
+
+ // Verify signature if a key was specified.
+ if (sign_ctx->digest != NULL) {
+ ret = verify_packet(reply, sign_ctx);
+ if (ret != KNOT_EOK) {
+ WARN("reply verification for %s (%s)",
+ net->remote_str, knot_strerror(ret));
+ }
+ }
+
+ // Check for BADCOOKIE RCODE and repeat query with the new cookie if required.
+ if (knot_pkt_ext_rcode(reply) == KNOT_RCODE_BADCOOKIE && query_ctx->badcookie > 0) {
+ printf("\n");
+ WARN("bad cookie from %s, retrying with the received one\n",
+ net->remote_str);
+ net_close_keepopen(net, query_ctx);
+
+ // Prepare new query context.
+ query_t new_ctx = *query_ctx;
+
+ uint8_t *opt = knot_pkt_edns_option(reply, KNOT_EDNS_OPTION_COOKIE);
+ if (opt == NULL) {
+ ERR("bad cookie, missing EDNS section");
+ goto fail;
+ }
+
+ const uint8_t *data = knot_edns_opt_get_data(opt);
+ uint16_t data_len = knot_edns_opt_get_length(opt);
+ ret = knot_edns_cookie_parse(&new_ctx.cc, &new_ctx.sc, data, data_len);
+ if (ret != KNOT_EOK) {
+ ERR("bad cookie, missing EDNS cookie option");
+ goto fail;
+ }
+ knot_pkt_free(reply);
+
+ // Restore the original client cookie.
+ new_ctx.cc = query_ctx->cc;
+
+ new_ctx.badcookie--;
+
+ knot_pkt_t *new_query = create_query_packet(&new_ctx);
+ ret = process_query_packet(new_query, net, &new_ctx, ignore_tc,
+ sign_ctx, style);
+ knot_pkt_free(new_query);
+
+ return ret;
+ }
+
+ knot_pkt_free(reply);
+ net_close_keepopen(net, query_ctx);
+
+ return 0;
+
+fail:
+ if (style->format != FORMAT_JSON) {
+ // Intentionaly start-end because of QUIC can have receive time.
+ print_packet(reply, net, in_len, time_diff_ms(&t_start, &t_end),
+ timestamp, true, style);
+ } else {
+ knot_pkt_t *q = knot_pkt_new(query->wire, query->size, NULL);
+ (void)knot_pkt_parse(q, KNOT_PF_NOCANON);
+ print_packets_json(q, reply, net, timestamp, style);
+ knot_pkt_free(q);
+ }
+
+ knot_pkt_free(reply);
+ net_close(net);
+
+ return -1;
+}
+
+static int process_query(const query_t *query, net_t *net)
+{
+ node_t *server;
+ knot_pkt_t *out_packet;
+ int ret;
+
+ // Create query packet.
+ out_packet = create_query_packet(query);
+ if (out_packet == NULL) {
+ ERR("can't create query packet");
+ return -1;
+ }
+
+ // Sign the query.
+ sign_context_t sign_ctx = { 0 };
+ ret = sign_query(out_packet, query, &sign_ctx);
+ if (ret != KNOT_EOK) {
+ ERR("can't sign the packet (%s)", knot_strerror(ret));
+ return -1;
+ }
+
+ // Reuse previous connection if available.
+ if (net->sockfd >= 0) {
+ DBG("Querying for owner(%s), class(%u), type(%u), reused connection",
+ query->owner, query->class_num, query->type_num);
+
+ ret = process_query_packet(out_packet, net, query, query->ignore_tc,
+ &sign_ctx, &query->style);
+ goto finish;
+ }
+
+ // Get connection parameters.
+ int socktype = get_socktype(query->protocol, query->type_num);
+ int flags = query->fastopen ? NET_FLAGS_FASTOPEN : NET_FLAGS_NONE;
+
+ // Loop over server list to process query.
+ WALK_LIST(server, query->servers) {
+ srv_info_t *remote = (srv_info_t *)server;
+ int iptype = get_iptype(query->ip, remote);
+
+ DBG("Querying for owner(%s), class(%u), type(%u), server(%s), "
+ "port(%s), protocol(%s)", query->owner, query->class_num,
+ query->type_num, remote->name, remote->service,
+ get_sockname(socktype));
+
+ // Loop over the number of retries.
+ for (size_t i = 0; i <= query->retries; i++) {
+ // Initialize network structure for current server.
+ ret = net_init(query->local, remote, iptype, socktype,
+ query->wait, flags,
+ (struct sockaddr *)&query->proxy.src,
+ (struct sockaddr *)&query->proxy.dst,
+ net);
+ if (ret != KNOT_EOK) {
+ if (ret == KNOT_NET_EADDR) {
+ // Requested address family not available.
+ goto next_server;
+ }
+ continue;
+ }
+
+ // Loop over all resolved addresses for remote.
+ while (net->srv != NULL) {
+ ret = net_init_crypto(net, &query->tls, &query->https,
+ &query->quic);
+ if (ret != 0) {
+ ERR("failed to initialize crypto context (%s)",
+ knot_strerror(ret));
+ break;
+ }
+
+ ret = process_query_packet(out_packet, net,
+ query,
+ query->ignore_tc,
+ &sign_ctx,
+ &query->style);
+ // If error try next resolved address.
+ if (ret != 0) {
+ net->srv = net->srv->ai_next;
+ if (net->srv != NULL && query->style.show_query) {
+ printf("\n");
+ }
+
+ continue;
+ }
+
+ break;
+ }
+
+ // Success.
+ if (ret == 0) {
+ goto finish;
+ }
+
+ if (i < query->retries) {
+ DBG("retrying server %s@%s(%s)",
+ remote->name, remote->service,
+ get_sockname(socktype));
+
+ if (query->style.show_query) {
+ printf("\n");
+ }
+ }
+
+ net_clean(net);
+ }
+
+ ERR("failed to query server %s@%s(%s)",
+ remote->name, remote->service, get_sockname(socktype));
+
+ // If not last server, print separation.
+ if (server->next->next && query->style.show_query) {
+ printf("\n");
+ }
+next_server:
+ continue;
+ }
+finish:
+ if (!query->keepopen || net->sockfd < 0) {
+ net_clean(net);
+ }
+ sign_context_deinit(&sign_ctx);
+ knot_pkt_free(out_packet);
+
+ if (ret == KNOT_NET_EADDR) {
+ WARN("no servers to query");
+ }
+
+ return ret;
+}
+
+static int process_xfr_packet(const knot_pkt_t *query,
+ net_t *net,
+ const query_t *query_ctx,
+ const sign_context_t *sign_ctx,
+ const style_t *style)
+{
+ struct timespec t_start, t_query, t_end;
+ time_t timestamp;
+ knot_pkt_t *reply = NULL;
+ uint8_t in[MAX_PACKET_SIZE];
+ int in_len;
+ int ret;
+ int64_t serial = 0;
+ size_t total_len = 0;
+ size_t msg_count = 0;
+ size_t rr_count = 0;
+ jsonw_t *w = NULL;
+
+ // Get start query time.
+ timestamp = time(NULL);
+ t_start = time_now();
+
+ // Connect to the server if not already connected.
+ if (net->sockfd < 0) {
+ ret = net_connect(net);
+ if (ret != KNOT_EOK) {
+ return -1;
+ }
+ }
+
+ // Send query packet.
+ ret = net_send(net, query->wire, query->size);
+ if (ret != KNOT_EOK) {
+ net_close(net);
+ return -1;
+ }
+
+ // Get stop query time and start reply time.
+ t_query = time_now();
+
+#if USE_DNSTAP
+ struct timespec t_query_full = time_diff(&t_start, &t_query);
+ t_query_full.tv_sec += timestamp;
+
+ // Make the dnstap copy of the query.
+ write_dnstap(query_ctx->dt_writer, true, query->wire, query->size,
+ net, &t_query_full);
+#endif // USE_DNSTAP
+
+ // Print query packet if required.
+ if (style->show_query && style->format != FORMAT_JSON) {
+ // Create copy of query packet for parsing.
+ knot_pkt_t *q = knot_pkt_new(query->wire, query->size, NULL);
+ if (q != NULL) {
+ if (knot_pkt_parse(q, KNOT_PF_NOCANON) == KNOT_EOK) {
+ print_packet(q, net, query->size,
+ time_diff_ms(&t_start, &t_query),
+ timestamp, false, style);
+ } else {
+ ERR("can't print query packet");
+ }
+ knot_pkt_free(q);
+ } else {
+ ERR("can't print query packet");
+ }
+
+ printf("\n");
+ }
+
+ // Loop over reply messages unless first and last SOA serials differ.
+ while (true) {
+ reply = NULL;
+
+ // Receive a reply message.
+ in_len = net_receive(net, in, sizeof(in));
+ t_end = time_now();
+ if (in_len <= 0) {
+ goto fail;
+ }
+
+#if USE_DNSTAP
+ struct timespec t_end_full = time_diff(&t_start, &t_end);
+ t_end_full.tv_sec += timestamp;
+
+ // Make the dnstap copy of the response.
+ write_dnstap(query_ctx->dt_writer, false, in, in_len, net,
+ &t_end_full);
+#endif // USE_DNSTAP
+
+ // Create reply packet structure to fill up.
+ reply = knot_pkt_new(in, in_len, NULL);
+ if (reply == NULL) {
+ ERR("internal error (%s)", knot_strerror(KNOT_ENOMEM));
+ goto fail;
+ }
+
+ // Parse reply to the packet structure.
+ ret = knot_pkt_parse(reply, KNOT_PF_NOCANON);
+ if (ret == KNOT_ETRAIL) {
+ WARN("malformed reply packet (%s)", knot_strerror(ret));
+ } else if (ret != KNOT_EOK) {
+ ERR("malformed reply packet from %s", net->remote_str);
+ goto fail;
+ }
+
+ // Compare reply header id.
+ if (check_reply_id(reply, query) == false) {
+ ERR("reply ID mismatch from %s", net->remote_str);
+ goto fail;
+ }
+
+ // Print leading transfer information.
+ if (msg_count == 0) {
+ if (style->format != FORMAT_JSON) {
+ print_header_xfr(query, style);
+ } else {
+ knot_pkt_t *q = knot_pkt_new(query->wire, query->size, NULL);
+ (void)knot_pkt_parse(q, KNOT_PF_NOCANON);
+ w = print_header_xfr_json(q, timestamp, style);
+ knot_pkt_free(q);
+ }
+ }
+
+ // Check for error reply.
+ if (knot_pkt_ext_rcode(reply) != KNOT_RCODE_NOERROR) {
+ ERR("server replied with error '%s'",
+ knot_pkt_ext_rcode_name(reply));
+ goto fail;
+ }
+
+ // The first message has a special treatment.
+ if (msg_count == 0) {
+ // Verify 1. signature if a key was specified.
+ if (sign_ctx->digest != NULL) {
+ ret = verify_packet(reply, sign_ctx);
+ if (ret != KNOT_EOK) {
+ style_t tsig_style = {
+ .format = style->format,
+ .style = style->style,
+ .show_tsig = true
+ };
+ if (style->format != FORMAT_JSON) {
+ print_data_xfr(reply, &tsig_style);
+ }
+
+ ERR("reply verification for %s (%s)",
+ net->remote_str, knot_strerror(ret));
+ goto fail;
+ }
+ }
+
+ // Read first SOA serial.
+ serial = first_serial_check(reply, query);
+
+ if (serial < 0) {
+ ERR("first answer record from %s isn't SOA",
+ net->remote_str);
+ goto fail;
+ }
+
+ // Check for question sections equality.
+ check_reply_question(reply, query);
+
+ // Check QR bit
+ check_reply_qr(reply);
+ }
+
+ msg_count++;
+ rr_count += knot_wire_get_ancount(reply->wire);
+ total_len += in_len;
+
+ // Print reply packet.
+ if (style->format != FORMAT_JSON) {
+ print_data_xfr(reply, style);
+ } else {
+ print_data_xfr_json(w, reply, timestamp);
+ }
+
+ // Fail to continue if TC is set.
+ if (knot_wire_get_tc(reply->wire)) {
+ ERR("truncated reply");
+ goto fail;
+ }
+
+ // Check for finished transfer.
+ if (finished_xfr(serial, reply, query, msg_count, query_ctx->serial != -1)) {
+ knot_pkt_free(reply);
+ break;
+ }
+
+ knot_pkt_free(reply);
+ reply = NULL;
+ }
+
+ // Print full transfer information.
+ t_end = time_now();
+ if (style->format != FORMAT_JSON) {
+ print_footer_xfr(total_len, msg_count, rr_count, net,
+ time_diff_ms(&t_query, &t_end), timestamp, style);
+ } else {
+ print_footer_xfr_json(&w, style);
+ }
+
+ net_close_keepopen(net, query_ctx);
+
+ return 0;
+
+fail:
+ // Print partial transfer information.
+ t_end = time_now();
+ if (style->format != FORMAT_JSON) {
+ print_data_xfr(reply, style);
+ print_footer_xfr(total_len, msg_count, rr_count, net,
+ time_diff_ms(&t_query, &t_end), timestamp, style);
+ } else {
+ print_data_xfr_json(w, reply, timestamp);
+ print_footer_xfr_json(&w, style);
+ }
+
+ knot_pkt_free(reply);
+ net_close(net);
+ free(w);
+
+ return -1;
+}
+
+static int process_xfr(const query_t *query, net_t *net)
+{
+ knot_pkt_t *out_packet;
+ int ret;
+
+ // Create query packet.
+ out_packet = create_query_packet(query);
+ if (out_packet == NULL) {
+ ERR("can't create query packet");
+ return -1;
+ }
+
+ // Sign the query.
+ sign_context_t sign_ctx = { 0 };
+ ret = sign_query(out_packet, query, &sign_ctx);
+ if (ret != KNOT_EOK) {
+ ERR("can't sign the packet (%s)", knot_strerror(ret));
+ return -1;
+ }
+
+ // Reuse previous connection if available.
+ if (net->sockfd >= 0) {
+ DBG("Querying for owner(%s), class(%u), type(%u), reused connection",
+ query->owner, query->class_num, query->type_num);
+
+ ret = process_xfr_packet(out_packet, net, query,
+ &sign_ctx, &query->style);
+ goto finish;
+ }
+
+ // Get connection parameters.
+ int socktype = get_socktype(query->protocol, query->type_num);
+ int flags = query->fastopen ? NET_FLAGS_FASTOPEN : NET_FLAGS_NONE;
+
+ // Use the first nameserver from the list.
+ srv_info_t *remote = HEAD(query->servers);
+ int iptype = get_iptype(query->ip, remote);
+
+ DBG("Querying for owner(%s), class(%u), type(%u), server(%s), "
+ "port(%s), protocol(%s)", query->owner, query->class_num,
+ query->type_num, remote->name, remote->service,
+ get_sockname(socktype));
+
+ // Initialize network structure.
+ ret = net_init(query->local, remote, iptype, socktype, query->wait, flags,
+ (struct sockaddr *)&query->proxy.src,
+ (struct sockaddr *)&query->proxy.dst,
+ net);
+ if (ret != KNOT_EOK) {
+ sign_context_deinit(&sign_ctx);
+ knot_pkt_free(out_packet);
+ return -1;
+ }
+
+ // Loop over all resolved addresses for remote.
+ while (net->srv != NULL) {
+ ret = net_init_crypto(net, &query->tls, &query->https,
+ &query->quic);
+ if (ret != 0) {
+ ERR("failed to initialize crypto context (%s)",
+ knot_strerror(ret));
+ break;
+ }
+
+ ret = process_xfr_packet(out_packet, net,
+ query,
+ &sign_ctx,
+ &query->style);
+ // If error try next resolved address.
+ if (ret != 0) {
+ net->srv = (net->srv)->ai_next;
+ continue;
+ }
+
+ break;
+ }
+
+ if (ret != 0) {
+ ERR("failed to query server %s@%s(%s)",
+ remote->name, remote->service, get_sockname(socktype));
+ }
+finish:
+ if (!query->keepopen || net->sockfd < 0) {
+ net_clean(net);
+ }
+ sign_context_deinit(&sign_ctx);
+ knot_pkt_free(out_packet);
+
+ return ret;
+}
+
+int kdig_exec(const kdig_params_t *params)
+{
+ node_t *n;
+ net_t net = { .sockfd = -1 };
+
+ if (params == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ bool success = true;
+
+ // Loop over query list.
+ WALK_LIST(n, params->queries) {
+ query_t *query = (query_t *)n;
+
+ int ret = -1;
+ switch (query->operation) {
+ case OPERATION_QUERY:
+ ret = process_query(query, &net);
+ break;
+ case OPERATION_XFR:
+ ret = process_xfr(query, &net);
+ break;
+#if USE_DNSTAP
+ case OPERATION_LIST_DNSTAP:
+ ret = process_dnstap(query);
+ break;
+#endif // USE_DNSTAP
+ default:
+ ERR("unsupported operation");
+ break;
+ }
+
+ // All operations must succeed.
+ if (ret != 0) {
+ success = false;
+ }
+
+ // If not last query, print separation.
+ if (n->next->next && params->config->style.format == FORMAT_FULL) {
+ printf("\n");
+ }
+ }
+
+ if (net.sockfd >= 0) {
+ net_close(&net);
+ net_clean(&net);
+ }
+
+ return success ? KNOT_EOK : KNOT_ERROR;
+}
diff --git a/src/utils/kdig/kdig_exec.h b/src/utils/kdig/kdig_exec.h
new file mode 100644
index 0000000..99167ce
--- /dev/null
+++ b/src/utils/kdig/kdig_exec.h
@@ -0,0 +1,21 @@
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "utils/kdig/kdig_params.h"
+
+int kdig_exec(const kdig_params_t *params);
diff --git a/src/utils/kdig/kdig_main.c b/src/utils/kdig/kdig_main.c
new file mode 100644
index 0000000..534d50e
--- /dev/null
+++ b/src/utils/kdig/kdig_main.c
@@ -0,0 +1,45 @@
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+
+#include "libdnssec/crypto.h"
+#include "utils/kdig/kdig_params.h"
+#include "utils/kdig/kdig_exec.h"
+#include "libknot/libknot.h"
+
+int main(int argc, char *argv[])
+{
+ int ret = EXIT_SUCCESS;
+
+ tzset();
+
+ kdig_params_t params;
+ if (kdig_parse(&params, argc, argv) == KNOT_EOK) {
+ if (!params.stop) {
+ dnssec_crypto_init();
+ if (kdig_exec(&params) != KNOT_EOK) {
+ ret = EXIT_FAILURE;
+ }
+ dnssec_crypto_cleanup();
+ }
+ } else {
+ ret = EXIT_FAILURE;
+ }
+
+ kdig_clean(&params);
+ return ret;
+}
diff --git a/src/utils/kdig/kdig_params.c b/src/utils/kdig/kdig_params.c
new file mode 100644
index 0000000..310e890
--- /dev/null
+++ b/src/utils/kdig/kdig_params.c
@@ -0,0 +1,2765 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <arpa/inet.h>
+#include <locale.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "utils/kdig/kdig_params.h"
+#include "utils/common/hex.h"
+#include "utils/common/msg.h"
+#include "utils/common/netio.h"
+#include "utils/common/params.h"
+#include "utils/common/resolv.h"
+#include "libknot/descriptor.h"
+#include "libknot/libknot.h"
+#include "contrib/base64.h"
+#include "contrib/sockaddr.h"
+#include "contrib/string.h"
+#include "contrib/strtonum.h"
+#include "contrib/time.h"
+#include "contrib/ucw/lists.h"
+#include "libdnssec/error.h"
+#include "libdnssec/random.h"
+
+#define PROGRAM_NAME "kdig"
+
+#define DEFAULT_RETRIES_DIG 2
+#define DEFAULT_TIMEOUT_DIG 5
+#define DEFAULT_ALIGNMENT_SIZE 128
+#define DEFAULT_TLS_OCSP_STAPLING (7 * 24 * 3600)
+
+#define BADCOOKIE_RETRY_MAX 10
+
+static const flags_t DEFAULT_FLAGS_DIG = {
+ .aa_flag = false,
+ .tc_flag = false,
+ .rd_flag = true,
+ .ra_flag = false,
+ .z_flag = false,
+ .ad_flag = true,
+ .cd_flag = false,
+ .do_flag = false
+};
+
+static const style_t DEFAULT_STYLE_DIG = {
+ .format = FORMAT_FULL,
+ .style = {
+ .wrap = false,
+ .show_class = true,
+ .show_ttl = true,
+ .verbose = false,
+ .original_ttl = false,
+ .empty_ttl = false,
+ .human_ttl = false,
+ .human_timestamp = true,
+ .generic = false,
+ .ascii_to_idn = name_to_idn
+ },
+ .show_query = false,
+ .show_header = true,
+ .show_section = true,
+ .show_edns = true,
+ .show_question = true,
+ .show_answer = true,
+ .show_authority = true,
+ .show_additional = true,
+ .show_tsig = true,
+ .show_footer = true
+};
+
+static int opt_multiline(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.style.wrap = true;
+ q->style.format = FORMAT_FULL;
+ q->style.show_header = true;
+ q->style.show_edns = true;
+ q->style.show_footer = true;
+ q->style.style.verbose = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_nomultiline(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.style.wrap = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_short(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.format = FORMAT_DIG;
+ q->style.show_header = false;
+ q->style.show_edns = false;
+ q->style.show_footer = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_noshort(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.format = FORMAT_FULL;
+
+ return KNOT_EOK;
+}
+
+static int opt_generic(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.style.generic = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_nogeneric(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.style.generic = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_aaflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.aa_flag = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noaaflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.aa_flag = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_tcflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.tc_flag = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_notcflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.tc_flag = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_rdflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.rd_flag = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_nordflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.rd_flag = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_raflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.ra_flag = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noraflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.ra_flag = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_zflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.z_flag = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_nozflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.z_flag = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_adflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.ad_flag = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noadflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.ad_flag = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_cdflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.cd_flag = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_nocdflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.cd_flag = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_doflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.do_flag = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_nodoflag(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->flags.do_flag = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_all(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_header = true;
+ q->style.show_section = true;
+ q->style.show_edns = true;
+ q->style.show_question = true;
+ q->style.show_answer = true;
+ q->style.show_authority = true;
+ q->style.show_additional = true;
+ q->style.show_tsig = true;
+ q->style.show_footer = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noall(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_header = false;
+ q->style.show_section = false;
+ q->style.show_edns = false;
+ q->style.show_query = false;
+ q->style.show_question = false;
+ q->style.show_answer = false;
+ q->style.show_authority = false;
+ q->style.show_additional = false;
+ q->style.show_tsig = false;
+ q->style.show_footer = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_qr(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_query = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noqr(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_query = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_header(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_header = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noheader(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_header = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_comments(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_section = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_nocomments(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_section = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_opt(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_edns = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noopt(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_edns = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_opttext(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_edns_opt_text = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noopttext(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_edns_opt_text = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_question(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_question = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noquestion(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_question = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_optpresent(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.present_edns = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_nooptpresent(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.present_edns = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_answer(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_answer = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noanswer(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_answer = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_authority(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_authority = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noauthority(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_authority = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_additional(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_additional = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noadditional(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_additional = false;
+ q->style.show_edns = false;
+ q->style.show_tsig = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_tsig(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_tsig = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_notsig(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_tsig = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_stats(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_footer = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_nostats(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.show_footer = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_class(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.style.show_class = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noclass(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.style.show_class = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_ttl(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.style.show_ttl = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_nottl(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.style.show_ttl = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_ignore(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->ignore_tc = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_noignore(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->ignore_tc = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_crypto(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.style.hide_crypto = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_nocrypto(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.style.hide_crypto = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_tcp(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->protocol = PROTO_TCP;
+
+ return KNOT_EOK;
+}
+
+static int opt_notcp(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->protocol = PROTO_UDP;
+ return opt_ignore(arg, query);
+}
+
+static int opt_fastopen(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->fastopen = true;
+
+ return opt_tcp(arg, query);
+}
+
+static int opt_nofastopen(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->fastopen = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_keepopen(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->keepopen = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_nokeepopen(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->keepopen = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_tls(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->tls.enable = true;
+ if (q->quic.enable) {
+ return KNOT_EOK;
+ }
+ return opt_tcp(arg, query);
+}
+
+static int opt_notls(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ tls_params_clean(&q->tls);
+ tls_params_init(&q->tls);
+
+ return KNOT_EOK;
+}
+
+static int opt_tls_ca(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ if (arg == NULL) {
+ q->tls.system_ca = true;
+ return opt_tls(arg, query);
+ } else {
+ if (ptrlist_add(&q->tls.ca_files, strdup(arg), NULL) == NULL) {
+ return KNOT_ENOMEM;
+ }
+ return opt_tls(arg, query);
+ }
+}
+
+static int opt_notls_ca(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->tls.system_ca = false;
+
+ ptrnode_t *node, *nxt;
+ WALK_LIST_DELSAFE(node, nxt, q->tls.ca_files) {
+ free(node->d);
+ }
+ ptrlist_free(&q->tls.ca_files, NULL);
+
+ return KNOT_EOK;
+}
+
+static int opt_tls_pin(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ uint8_t pin[64] = { 0 };
+
+ int ret = knot_base64_decode((const uint8_t *)arg, strlen(arg), pin, sizeof(pin));
+ if (ret < 0) {
+ ERR("invalid +tls-pin=%s", arg);
+ return ret;
+ } else if (ret != CERT_PIN_LEN) { // Check for 256-bit value.
+ ERR("invalid sha256 hash length +tls-pin=%s", arg);
+ return KNOT_EINVAL;
+ }
+
+ uint8_t *item = malloc(1 + ret); // 1 ~ leading data length.
+ if (item == NULL) {
+ return KNOT_ENOMEM;
+ }
+ item[0] = ret;
+ memcpy(&item[1], pin, ret);
+
+ if (ptrlist_add(&q->tls.pins, item, NULL) == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ return opt_tls(arg, query);
+}
+
+static int opt_notls_pin(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ ptrnode_t *node, *nxt;
+ WALK_LIST_DELSAFE(node, nxt, q->tls.pins) {
+ free(node->d);
+ }
+ ptrlist_free(&q->tls.pins, NULL);
+
+ return KNOT_EOK;
+}
+
+static int opt_tls_hostname(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ free(q->tls.hostname);
+ q->tls.hostname = strdup(arg);
+
+ return opt_tls(arg, query);
+}
+
+static int opt_notls_hostname(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ free(q->tls.hostname);
+ q->tls.hostname = NULL;
+
+ return KNOT_EOK;
+}
+
+static int opt_tls_sni(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ free(q->tls.sni);
+ q->tls.sni = strdup(arg);
+
+ return opt_tls(arg, query);
+}
+
+static int opt_notls_sni(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ free(q->tls.sni);
+ q->tls.sni = NULL;
+
+ return KNOT_EOK;
+}
+
+static int opt_tls_keyfile(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ free(q->tls.keyfile);
+ q->tls.keyfile = strdup(arg);
+
+ return opt_tls(arg, query);
+}
+
+static int opt_notls_keyfile(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ free(q->tls.keyfile);
+ q->tls.keyfile = NULL;
+
+ return KNOT_EOK;
+}
+
+static int opt_tls_certfile(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ free(q->tls.certfile);
+ q->tls.certfile = strdup(arg);
+
+ return opt_tls(arg, query);
+}
+
+static int opt_notls_certfile(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ free(q->tls.certfile);
+ q->tls.certfile = NULL;
+
+ return KNOT_EOK;
+}
+
+static int opt_tls_ocsp_stapling(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ if (arg == NULL) {
+ q->tls.ocsp_stapling = DEFAULT_TLS_OCSP_STAPLING;
+ return opt_tls(arg, query);
+ } else {
+ uint32_t num = 0;
+ if (str_to_u32(arg, &num) != KNOT_EOK || num == 0) {
+ ERR("invalid +tls-ocsp-stapling=%s", arg);
+ return KNOT_EINVAL;
+ }
+
+ q->tls.ocsp_stapling = 3600 * num;
+ return opt_tls(arg, query);
+ }
+}
+
+static int opt_notls_ocsp_stapling(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->tls.ocsp_stapling = 0;
+
+ return KNOT_EOK;
+}
+
+static int opt_https(const char *arg, void *query)
+{
+#ifdef LIBNGHTTP2
+ query_t *q = query;
+
+ q->https.enable = true;
+
+ if (arg != NULL) {
+ char *resource = strstr(arg, "://");
+ if (resource == NULL) {
+ resource = (char *)arg;
+ } else {
+ resource += 3; // strlen("://")
+ if (*resource == '\0') {
+ ERR("invalid +https=%s", arg);
+ return KNOT_EINVAL;
+ }
+ }
+
+ char *tmp_path = strchr(resource, '/');
+ if (tmp_path) {
+ free(q->https.path);
+ q->https.path = strdup(tmp_path);
+
+ if (tmp_path != resource) {
+ free(q->tls.hostname);
+ q->tls.hostname = strndup(resource, (size_t)(tmp_path - resource));
+ }
+ return opt_tls(arg, query);
+ } else {
+ return opt_tls_hostname(arg, query);
+ }
+
+ }
+
+ return opt_tls(arg, query);
+
+#else
+ return KNOT_ENOTSUP;
+#endif //LIBNGHTTP2
+}
+
+static int opt_nohttps(const char *arg, void *query)
+{
+#ifdef LIBNGHTTP2
+ query_t *q = query;
+
+ https_params_clean(&q->https);
+
+ return opt_notls(arg, query);
+#else
+ return KNOT_ENOTSUP;
+#endif //LIBNGHTTP2
+}
+
+static int opt_https_get(const char *arg, void *query)
+{
+#ifdef LIBNGHTTP2
+ query_t *q = query;
+
+ q->https.method = GET;
+
+ return opt_https(arg, query);
+#else
+ return KNOT_ENOTSUP;
+#endif //LIBNGHTTP2
+}
+
+static int opt_nohttps_get(const char *arg, void *query)
+{
+#ifdef LIBNGHTTP2
+ query_t *q = query;
+
+ q->https.method = POST;
+
+ return KNOT_EOK;
+#else
+ return KNOT_ENOTSUP;
+#endif
+}
+
+static int opt_quic(const char *arg, void *query)
+{
+#ifdef ENABLE_QUIC
+ query_t *q = query;
+
+ q->quic.enable = true;
+
+ opt_tls(arg, query);
+ opt_notcp(arg, query);
+
+ return KNOT_EOK;
+#else
+ return KNOT_ENOTSUP;
+#endif //ENABLE_QUIC
+}
+
+static int opt_noquic(const char *arg, void *query)
+{
+#ifdef ENABLE_QUIC
+ query_t *q = query;
+
+ quic_params_clean(&q->quic);
+
+ return opt_notls(arg, query);
+#else
+ return KNOT_ENOTSUP;
+#endif //ENABLE_QUIC
+}
+
+static int opt_nsid(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->nsid = true;
+
+ return KNOT_EOK;
+}
+
+static int opt_nonsid(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->nsid = false;
+
+ return KNOT_EOK;
+}
+
+static int opt_bufsize(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ uint16_t num = 0;
+ if (str_to_u16(arg, &num) != KNOT_EOK) {
+ ERR("invalid +bufsize=%s", arg);
+ return KNOT_EINVAL;
+ }
+
+ // Disable EDNS if zero bufsize.
+ if (num == 0) {
+ q->udp_size = -1;
+ } else if (num < KNOT_WIRE_HEADER_SIZE) {
+ q->udp_size = KNOT_WIRE_HEADER_SIZE;
+ } else {
+ q->udp_size = num;
+ }
+
+ return KNOT_EOK;
+}
+
+static int opt_nobufsize(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->udp_size = -1;
+
+ return KNOT_EOK;
+}
+
+static int opt_cookie(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ if (arg != NULL) {
+ uint8_t *input = NULL;
+ size_t input_len;
+
+ int ret = hex_decode(arg, &input, &input_len);
+ if (ret != KNOT_EOK) {
+ ERR("invalid +cookie=%s", arg);
+ return KNOT_EINVAL;
+ }
+
+ if (input_len < KNOT_EDNS_COOKIE_CLNT_SIZE) {
+ ERR("too short client +cookie=%s", arg);
+ free(input);
+ return KNOT_EINVAL;
+ }
+ q->cc.len = KNOT_EDNS_COOKIE_CLNT_SIZE;
+ memcpy(q->cc.data, input, q->cc.len);
+
+ input_len -= q->cc.len;
+ if (input_len > 0) {
+ if (input_len < KNOT_EDNS_COOKIE_SRVR_MIN_SIZE) {
+ ERR("too short server +cookie=%s", arg);
+ free(input);
+ return KNOT_EINVAL;
+ }
+ if (input_len > KNOT_EDNS_COOKIE_SRVR_MAX_SIZE) {
+ ERR("too long server +cookie=%s", arg);
+ free(input);
+ return KNOT_EINVAL;
+ }
+ q->sc.len = input_len;
+ memcpy(q->sc.data, input + q->cc.len, q->sc.len);
+ }
+
+ free(input);
+ } else {
+ q->cc.len = KNOT_EDNS_COOKIE_CLNT_SIZE;
+
+ int ret = dnssec_random_buffer(q->cc.data, q->cc.len);
+ if (ret != DNSSEC_EOK) {
+ return knot_error_from_libdnssec(ret);
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+static int opt_nocookie(const char *arg, void *query)
+{
+ query_t *q = query;
+ q->cc.len = 0;
+ q->sc.len = 0;
+ return KNOT_EOK;
+}
+
+static int opt_badcookie(const char *arg, void *query)
+{
+ query_t *q = query;
+ q->badcookie = BADCOOKIE_RETRY_MAX;
+ return KNOT_EOK;
+}
+
+static int opt_nobadcookie(const char *arg, void *query)
+{
+ query_t *q = query;
+ q->badcookie = 0;
+ return KNOT_EOK;
+}
+
+static int opt_padding(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ if (arg == NULL) {
+ q->padding = -2;
+ return KNOT_EOK;
+ } else {
+ uint16_t num = 0;
+ if (str_to_u16(arg, &num) != KNOT_EOK) {
+ ERR("invalid +padding=%s", arg);
+ return KNOT_EINVAL;
+ }
+
+ q->padding = num;
+ return KNOT_EOK;
+ }
+}
+
+static int opt_nopadding(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->padding = -3;
+
+ return KNOT_EOK;
+}
+
+static int opt_alignment(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ if (arg == NULL) {
+ q->alignment = DEFAULT_ALIGNMENT_SIZE;
+ return KNOT_EOK;
+ } else {
+ uint16_t num = 0;
+ if (str_to_u16(arg, &num) != KNOT_EOK || num < 2) {
+ ERR("invalid +alignment=%s", arg);
+ return KNOT_EINVAL;
+ }
+
+ q->alignment = num;
+ return KNOT_EOK;
+ }
+}
+
+static int opt_noalignment(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->alignment = 0;
+
+ return KNOT_EOK;
+}
+
+static int opt_subnet(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ char *sep = NULL;
+ const size_t arg_len = strlen(arg);
+ const char *arg_end = arg + arg_len;
+ size_t addr_len = 0;
+
+ // Separate address and network mask.
+ if ((sep = strchr(arg, '/')) != NULL) {
+ addr_len = sep - arg;
+ } else {
+ addr_len = arg_len;
+ }
+
+ // Check IP address.
+
+ struct sockaddr_storage ss = { 0 };
+ struct addrinfo hints = { .ai_flags = AI_NUMERICHOST };
+ struct addrinfo *ai = NULL;
+
+ char *addr_str = strndup(arg, addr_len);
+ if (getaddrinfo(addr_str, NULL, &hints, &ai) != 0) {
+ free(addr_str);
+ ERR("invalid address +subnet=%s", arg);
+ return KNOT_EINVAL;
+ }
+
+ memcpy(&ss, ai->ai_addr, ai->ai_addrlen);
+ freeaddrinfo(ai);
+ free(addr_str);
+
+ if (knot_edns_client_subnet_set_addr(&q->subnet, &ss) != KNOT_EOK) {
+ ERR("invalid address +subnet=%s", arg);
+ return KNOT_EINVAL;
+ }
+
+ // Parse network mask.
+ const char *mask = arg;
+ if (mask + addr_len < arg_end) {
+ mask += addr_len + 1;
+ uint8_t num = 0;
+ if (str_to_u8(mask, &num) != KNOT_EOK || num > q->subnet.source_len) {
+ ERR("invalid network mask +subnet=%s", arg);
+ return KNOT_EINVAL;
+ }
+ q->subnet.source_len = num;
+ }
+
+ return KNOT_EOK;
+}
+
+static int opt_nosubnet(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->subnet.family = AF_UNSPEC;
+
+ return KNOT_EOK;
+}
+
+static int opt_edns(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ if (arg == NULL) {
+ q->edns = 0;
+ return KNOT_EOK;
+ } else {
+ uint8_t num = 0;
+ if (str_to_u8(arg, &num) != KNOT_EOK) {
+ ERR("invalid +edns=%s", arg);
+ return KNOT_EINVAL;
+ }
+
+ q->edns = num;
+ return KNOT_EOK;
+ }
+}
+
+static int opt_noedns(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->edns = -1;
+ opt_nodoflag(arg, query);
+ opt_nonsid(arg, query);
+ opt_nobufsize(arg, query);
+ opt_nocookie(arg, query);
+ opt_nopadding(arg, query);
+ opt_noalignment(arg, query);
+ opt_nosubnet(arg, query);
+
+ return KNOT_EOK;
+}
+
+static int opt_timeout(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ if (params_parse_wait(arg, &q->wait) != KNOT_EOK) {
+ ERR("invalid +timeout=%s", arg);
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+static int opt_notimeout(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ (void)params_parse_wait("0", &q->wait);
+
+ return KNOT_EOK;
+}
+
+static int opt_retry(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ if (str_to_u32(arg, &q->retries) != KNOT_EOK) {
+ ERR("invalid +retry=%s", arg);
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+static int opt_noretry(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->retries = 0;
+
+ return KNOT_EOK;
+}
+
+static int opt_expire(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ ednsopt_t *opt = ednsopt_create(KNOT_EDNS_OPTION_EXPIRE, 0, NULL);
+ add_tail(&q->edns_opts, &opt->n);
+
+ return KNOT_EOK;
+}
+
+static int opt_noexpire(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ ednsopt_t *node, *nxt;
+ WALK_LIST_DELSAFE(node, nxt, q->edns_opts) {
+ ednsopt_t *opt = node;
+ if (opt->code == KNOT_EDNS_OPTION_EXPIRE) {
+ rem_node(&opt->n);
+ ednsopt_free(opt);
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+static int parse_ednsopt(const char *arg, ednsopt_t **opt_ptr)
+{
+ errno = 0;
+ char *end = NULL;
+ unsigned long code = strtoul(arg, &end, 10);
+ if (errno != 0 || arg == end || code > UINT16_MAX) {
+ return KNOT_EINVAL;
+ }
+
+ size_t length = 0;
+ uint8_t *data = NULL;
+ if (end[0] == ':') {
+ if (end[1] != '\0') {
+ int ret = hex_decode(end + 1, &data, &length);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ if (length > UINT16_MAX) {
+ free(data);
+ return KNOT_ERANGE;
+ }
+ }
+ } else if (end[0] != '\0') {
+ return KNOT_EINVAL;
+ }
+
+ ednsopt_t *opt = ednsopt_create(code, length, data);
+ if (opt == NULL) {
+ free(data);
+ return KNOT_ENOMEM;
+ }
+
+ *opt_ptr = opt;
+ return KNOT_EOK;
+}
+
+static int opt_ednsopt(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ ednsopt_t *opt = NULL;
+ int ret = parse_ednsopt(arg, &opt);
+ if (ret != KNOT_EOK) {
+ ERR("invalid +ednsopt=%s", arg);
+ return KNOT_EINVAL;
+ }
+
+ add_tail(&q->edns_opts, &opt->n);
+
+ return KNOT_EOK;
+}
+
+static int opt_noednsopt(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ ednsopt_list_deinit(&q->edns_opts);
+
+ return KNOT_EOK;
+}
+
+static int opt_noidn(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->idn = false;
+ q->style.style.ascii_to_idn = NULL;
+
+ return KNOT_EOK;
+}
+
+static int opt_json(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.format = FORMAT_JSON;
+
+ return KNOT_EOK;
+}
+
+static int opt_nojson(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->style.format = FORMAT_FULL;
+
+ return KNOT_EOK;
+}
+
+static int parse_addr(struct sockaddr_storage *addr, const char *arg, const char *def_port)
+{
+ srv_info_t *info = parse_nameserver(arg, def_port);
+ if (info == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ struct addrinfo *addr_info = NULL;
+ int ret = getaddrinfo(info->name, info->service, NULL, &addr_info);
+ srv_info_free(info);
+ if (ret != 0) {
+ return KNOT_EINVAL;
+ }
+
+ memcpy(addr, addr_info->ai_addr, addr_info->ai_addrlen);
+ freeaddrinfo(addr_info);
+
+ return KNOT_EOK;
+}
+
+static int opt_proxy(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ const char *sep = strchr(arg, '-');
+ if (sep == NULL || sep == arg || *(sep + 1) == '\0') {
+ ERR("invalid specification +proxy=%s", arg);
+ return KNOT_EINVAL;
+ }
+
+ char *src = strndup(arg, sep - arg);
+ int ret = parse_addr(&q->proxy.src, src, "0");
+ if (ret != KNOT_EOK) {
+ ERR("invalid proxy source address '%s'", src);
+ free(src);
+ return KNOT_EINVAL;
+ }
+
+ const char *dst = sep + 1;
+ ret = parse_addr(&q->proxy.dst, dst, "53");
+ if (ret != KNOT_EOK) {
+ ERR("invalid proxy destination address '%s'", dst);
+ free(src);
+ return KNOT_EINVAL;
+ }
+
+ if (q->proxy.src.ss_family != q->proxy.dst.ss_family) {
+ ERR("proxy address family mismatch '%s' versus '%s'", src, dst);
+ free(src);
+ return KNOT_EINVAL;
+ }
+ free(src);
+
+ return KNOT_EOK;
+}
+
+static int opt_noproxy(const char *arg, void *query)
+{
+ query_t *q = query;
+
+ q->proxy.src.ss_family = 0;
+ q->proxy.dst.ss_family = 0;
+
+ return KNOT_EOK;
+}
+
+static const param_t kdig_opts2[] = {
+ { "multiline", ARG_NONE, opt_multiline },
+ { "nomultiline", ARG_NONE, opt_nomultiline },
+
+ { "short", ARG_NONE, opt_short },
+ { "noshort", ARG_NONE, opt_noshort },
+
+ { "generic", ARG_NONE, opt_generic },
+ { "nogeneric", ARG_NONE, opt_nogeneric },
+
+ { "aaflag", ARG_NONE, opt_aaflag },
+ { "noaaflag", ARG_NONE, opt_noaaflag },
+
+ { "tcflag", ARG_NONE, opt_tcflag },
+ { "notcflag", ARG_NONE, opt_notcflag },
+
+ { "rdflag", ARG_NONE, opt_rdflag },
+ { "nordflag", ARG_NONE, opt_nordflag },
+
+ { "recurse", ARG_NONE, opt_rdflag },
+ { "norecurse", ARG_NONE, opt_nordflag },
+
+ { "raflag", ARG_NONE, opt_raflag },
+ { "noraflag", ARG_NONE, opt_noraflag },
+
+ { "zflag", ARG_NONE, opt_zflag },
+ { "nozflag", ARG_NONE, opt_nozflag },
+
+ { "adflag", ARG_NONE, opt_adflag },
+ { "noadflag", ARG_NONE, opt_noadflag },
+
+ { "cdflag", ARG_NONE, opt_cdflag },
+ { "nocdflag", ARG_NONE, opt_nocdflag },
+
+ { "dnssec", ARG_NONE, opt_doflag },
+ { "nodnssec", ARG_NONE, opt_nodoflag },
+
+ { "all", ARG_NONE, opt_all },
+ { "noall", ARG_NONE, opt_noall },
+
+ { "qr", ARG_NONE, opt_qr },
+ { "noqr", ARG_NONE, opt_noqr },
+
+ { "header", ARG_NONE, opt_header },
+ { "noheader", ARG_NONE, opt_noheader },
+
+ { "comments", ARG_NONE, opt_comments },
+ { "nocomments", ARG_NONE, opt_nocomments },
+
+ { "opt", ARG_NONE, opt_opt },
+ { "noopt", ARG_NONE, opt_noopt },
+
+ { "opttext", ARG_NONE, opt_opttext },
+ { "noopttext", ARG_NONE, opt_noopttext },
+
+ { "optpresent", ARG_NONE, opt_optpresent },
+ { "nooptpresent", ARG_NONE, opt_nooptpresent },
+
+ { "question", ARG_NONE, opt_question },
+ { "noquestion", ARG_NONE, opt_noquestion },
+
+ { "answer", ARG_NONE, opt_answer },
+ { "noanswer", ARG_NONE, opt_noanswer },
+
+ { "authority", ARG_NONE, opt_authority },
+ { "noauthority", ARG_NONE, opt_noauthority },
+
+ { "additional", ARG_NONE, opt_additional },
+ { "noadditional", ARG_NONE, opt_noadditional },
+
+ { "tsig", ARG_NONE, opt_tsig },
+ { "notsig", ARG_NONE, opt_notsig },
+
+ { "stats", ARG_NONE, opt_stats },
+ { "nostats", ARG_NONE, opt_nostats },
+
+ { "class", ARG_NONE, opt_class },
+ { "noclass", ARG_NONE, opt_noclass },
+
+ { "ttl", ARG_NONE, opt_ttl },
+ { "nottl", ARG_NONE, opt_nottl },
+
+ { "crypto", ARG_NONE, opt_crypto },
+ { "nocrypto", ARG_NONE, opt_nocrypto },
+
+ { "tcp", ARG_NONE, opt_tcp },
+ { "notcp", ARG_NONE, opt_notcp },
+
+ { "fastopen", ARG_NONE, opt_fastopen },
+ { "nofastopen", ARG_NONE, opt_nofastopen },
+
+ { "ignore", ARG_NONE, opt_ignore },
+ { "noignore", ARG_NONE, opt_noignore },
+
+ { "keepopen", ARG_NONE, opt_keepopen },
+ { "nokeepopen", ARG_NONE, opt_nokeepopen },
+
+ { "tls", ARG_NONE, opt_tls },
+ { "notls", ARG_NONE, opt_notls },
+
+ { "tls-ca", ARG_OPTIONAL, opt_tls_ca },
+ { "notls-ca", ARG_NONE, opt_notls_ca },
+
+ { "tls-pin", ARG_REQUIRED, opt_tls_pin },
+ { "notls-pin", ARG_NONE, opt_notls_pin },
+
+ { "tls-hostname", ARG_REQUIRED, opt_tls_hostname },
+ { "notls-hostname", ARG_NONE, opt_notls_hostname },
+
+ { "tls-sni", ARG_REQUIRED, opt_tls_sni },
+ { "notls-sni", ARG_NONE, opt_notls_sni },
+
+ { "tls-keyfile", ARG_REQUIRED, opt_tls_keyfile },
+ { "notls-keyfile", ARG_NONE, opt_notls_keyfile },
+
+ { "tls-certfile", ARG_REQUIRED, opt_tls_certfile },
+ { "notls-certfile", ARG_NONE, opt_notls_certfile },
+
+ { "tls-ocsp-stapling", ARG_OPTIONAL, opt_tls_ocsp_stapling },
+ { "notls-ocsp-stapling", ARG_NONE, opt_notls_ocsp_stapling },
+
+ { "https", ARG_OPTIONAL, opt_https },
+ { "nohttps", ARG_NONE, opt_nohttps },
+
+ { "https-get", ARG_NONE, opt_https_get },
+ { "nohttps-get", ARG_NONE, opt_nohttps_get },
+
+ { "quic", ARG_NONE, opt_quic },
+ { "noquic", ARG_NONE, opt_noquic },
+
+ { "nsid", ARG_NONE, opt_nsid },
+ { "nonsid", ARG_NONE, opt_nonsid },
+
+ { "bufsize", ARG_REQUIRED, opt_bufsize },
+ { "nobufsize", ARG_NONE, opt_nobufsize },
+
+ { "padding", ARG_OPTIONAL, opt_padding },
+ { "nopadding", ARG_NONE, opt_nopadding },
+
+ { "alignment", ARG_OPTIONAL, opt_alignment },
+ { "noalignment", ARG_NONE, opt_noalignment },
+
+ { "subnet", ARG_REQUIRED, opt_subnet },
+ { "nosubnet", ARG_NONE, opt_nosubnet },
+
+ { "proxy", ARG_REQUIRED, opt_proxy },
+ { "noproxy", ARG_NONE, opt_noproxy },
+
+ // Obsolete aliases.
+ { "client", ARG_REQUIRED, opt_subnet },
+ { "noclient", ARG_NONE, opt_nosubnet },
+
+ { "edns", ARG_OPTIONAL, opt_edns },
+ { "noedns", ARG_NONE, opt_noedns },
+
+ { "timeout", ARG_REQUIRED, opt_timeout },
+ { "notimeout", ARG_NONE, opt_notimeout },
+
+ { "retry", ARG_REQUIRED, opt_retry },
+ { "noretry", ARG_NONE, opt_noretry },
+
+ { "expire", ARG_NONE, opt_expire },
+ { "noexpire", ARG_NONE, opt_noexpire },
+
+ { "cookie", ARG_OPTIONAL, opt_cookie },
+ { "nocookie", ARG_NONE, opt_nocookie },
+
+ { "badcookie", ARG_NONE, opt_badcookie },
+ { "nobadcookie", ARG_NONE, opt_nobadcookie },
+
+ { "ednsopt", ARG_REQUIRED, opt_ednsopt },
+ { "noednsopt", ARG_NONE, opt_noednsopt },
+
+ { "json", ARG_NONE, opt_json },
+ { "nojson", ARG_NONE, opt_nojson },
+
+ /* "idn" doesn't work since it must be called before query creation. */
+ { "noidn", ARG_NONE, opt_noidn },
+
+ { NULL }
+};
+
+query_t *query_create(const char *owner, const query_t *conf)
+{
+ // Create output structure.
+ query_t *query = calloc(1, sizeof(query_t));
+
+ if (query == NULL) {
+ DBG_NULL;
+ return NULL;
+ }
+
+ // Initialization with defaults or with reference query.
+ if (conf == NULL) {
+ query->conf = NULL;
+ query->local = NULL;
+ query->operation = OPERATION_QUERY;
+ query->ip = IP_ALL;
+ query->protocol = PROTO_ALL;
+ query->fastopen = false;
+ query->port = strdup("");
+ query->udp_size = -1;
+ query->retries = DEFAULT_RETRIES_DIG;
+ query->wait = DEFAULT_TIMEOUT_DIG;
+ query->ignore_tc = false;
+ query->class_num = -1;
+ query->type_num = -1;
+ query->serial = -1;
+ query->notify = false;
+ query->flags = DEFAULT_FLAGS_DIG;
+ query->style = DEFAULT_STYLE_DIG;
+ query->style.style.now = knot_time();
+ query->idn = true;
+ query->nsid = false;
+ query->edns = -1;
+ query->cc.len = 0;
+ query->sc.len = 0;
+ query->badcookie = BADCOOKIE_RETRY_MAX;
+ query->padding = -1;
+ query->alignment = 0;
+ tls_params_init(&query->tls);
+ //query->tsig_key
+ query->subnet.family = AF_UNSPEC;
+ ednsopt_list_init(&query->edns_opts);
+#if USE_DNSTAP
+ query->dt_reader = NULL;
+ query->dt_writer = NULL;
+#endif // USE_DNSTAP
+ } else {
+ *query = *conf;
+ query->conf = conf;
+ if (conf->local != NULL) {
+ query->local = srv_info_create(conf->local->name,
+ conf->local->service);
+ if (query->local == NULL) {
+ query_free(query);
+ return NULL;
+ }
+ } else {
+ query->local = NULL;
+ }
+ query->port = strdup(conf->port);
+ tls_params_copy(&query->tls, &conf->tls);
+ https_params_copy(&query->https, &conf->https);
+ quic_params_copy(&query->quic, &conf->quic);
+ if (conf->tsig_key.name != NULL) {
+ int ret = knot_tsig_key_copy(&query->tsig_key,
+ &conf->tsig_key);
+ if (ret != KNOT_EOK) {
+ query_free(query);
+ return NULL;
+ }
+ }
+
+ int ret = ednsopt_list_dup(&query->edns_opts, &conf->edns_opts);
+ if (ret != KNOT_EOK) {
+ query_free(query);
+ return NULL;
+ }
+
+#if USE_DNSTAP
+ query->dt_reader = conf->dt_reader;
+ query->dt_writer = conf->dt_writer;
+#endif // USE_DNSTAP
+ }
+
+ // Initialize list of servers.
+ init_list(&query->servers);
+
+ // Set the query owner if any.
+ if (owner != NULL) {
+ if ((query->owner = strdup(owner)) == NULL) {
+ query_free(query);
+ return NULL;
+ }
+ }
+
+ // Check dynamic allocation.
+ if (query->port == NULL) {
+ query_free(query);
+ return NULL;
+ }
+
+ return query;
+}
+
+void query_free(query_t *query)
+{
+ node_t *n, *nxt;
+
+ if (query == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ // Cleanup servers.
+ WALK_LIST_DELSAFE(n, nxt, query->servers) {
+ srv_info_free((srv_info_t *)n);
+ }
+
+ // Cleanup local address.
+ if (query->local != NULL) {
+ srv_info_free(query->local);
+ }
+
+ tls_params_clean(&query->tls);
+ https_params_clean(&query->https);
+ quic_params_clean(&query->quic);
+
+ // Cleanup signing key.
+ knot_tsig_key_deinit(&query->tsig_key);
+
+ // Cleanup EDNS options.
+ ednsopt_list_deinit(&query->edns_opts);
+
+#if USE_DNSTAP
+ if (query->dt_reader != NULL) {
+ dt_reader_free(query->dt_reader);
+ }
+ if (query->dt_writer != NULL) {
+ // Global writer can be shared!
+ if (query->conf == NULL ||
+ query->conf->dt_writer != query->dt_writer) {
+ dt_writer_free(query->dt_writer);
+ }
+ }
+#endif // USE_DNSTAP
+
+ free(query->owner);
+ free(query->port);
+ free(query);
+}
+
+ednsopt_t *ednsopt_create(uint16_t code, uint16_t length, uint8_t *data)
+{
+ ednsopt_t *opt = calloc(1, sizeof(*opt));
+ if (opt == NULL) {
+ return NULL;
+ }
+
+ opt->code = code;
+ opt->length = length;
+ opt->data = data;
+
+ return opt;
+}
+
+ednsopt_t *ednsopt_dup(const ednsopt_t *opt)
+{
+ ednsopt_t *dup = calloc(1, sizeof(*opt));
+ if (dup == NULL) {
+ return NULL;
+ }
+
+ dup->code = opt->code;
+ dup->length = opt->length;
+ dup->data = memdup(opt->data, opt->length);
+ if (dup->data == NULL) {
+ free(dup);
+ return NULL;
+ }
+
+ return dup;
+}
+
+void ednsopt_free(ednsopt_t *opt)
+{
+ if (opt == NULL) {
+ return;
+ }
+
+ free(opt->data);
+ free(opt);
+}
+
+void ednsopt_list_init(list_t *list)
+{
+ init_list(list);
+}
+
+void ednsopt_list_deinit(list_t *list)
+{
+ node_t *n, *next;
+ WALK_LIST_DELSAFE(n, next, *list) {
+ ednsopt_t *opt = (ednsopt_t *)n;
+ ednsopt_free(opt);
+ }
+
+ init_list(list);
+}
+
+int ednsopt_list_dup(list_t *dest, const list_t *src)
+{
+ list_t backup = *dest;
+ init_list(dest);
+
+ node_t *n;
+ WALK_LIST(n, *src) {
+ ednsopt_t *opt = (ednsopt_t *)n;
+ ednsopt_t *dup = ednsopt_dup(opt);
+ if (dup == NULL) {
+ ednsopt_list_deinit(dest);
+ *dest = backup;
+ return KNOT_ENOMEM;
+ }
+
+ add_tail(dest, &dup->n);
+ }
+
+ return KNOT_EOK;
+}
+
+bool ednsopt_list_empty(const list_t *list)
+{
+ return EMPTY_LIST(*list);
+}
+
+int kdig_init(kdig_params_t *params)
+{
+ if (params == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ memset(params, 0, sizeof(*params));
+
+ params->stop = false;
+
+ // Initialize list of queries.
+ init_list(&params->queries);
+
+ // Create config query.
+ if ((params->config = query_create(NULL, NULL)) == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ return KNOT_EOK;
+}
+
+void kdig_clean(kdig_params_t *params)
+{
+ node_t *n, *nxt;
+
+ if (params == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ // Clean up queries.
+ WALK_LIST_DELSAFE(n, nxt, params->queries) {
+ query_free((query_t *)n);
+ }
+
+ // Clean up config.
+ query_free(params->config);
+
+ // Clean up the structure.
+ memset(params, 0, sizeof(*params));
+}
+
+static int parse_class(const char *value, query_t *query)
+{
+ uint16_t rclass;
+
+ if (params_parse_class(value, &rclass) != KNOT_EOK) {
+ return KNOT_EINVAL;
+ }
+
+ query->class_num = rclass;
+
+ return KNOT_EOK;
+}
+
+static int parse_keyfile(const char *value, query_t *query)
+{
+ knot_tsig_key_deinit(&query->tsig_key);
+
+ if (knot_tsig_key_init_file(&query->tsig_key, value) != KNOT_EOK) {
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+static int parse_local(const char *value, query_t *query)
+{
+ srv_info_t *local = parse_nameserver(value, "0");
+ if (local == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ if (query->local != NULL) {
+ srv_info_free(query->local);
+ }
+
+ query->local = local;
+
+ return KNOT_EOK;
+}
+
+static int parse_name(const char *value, list_t *queries, const query_t *conf)
+{
+ query_t *query = NULL;
+ char *ascii_name = (char *)value;
+ char *fqd_name = NULL;
+
+ if (value != NULL) {
+ if (conf->idn) {
+ ascii_name = name_from_idn(value);
+ if (ascii_name == NULL) {
+ return KNOT_EINVAL;
+ }
+ }
+
+ // If name is not FQDN, append trailing dot.
+ fqd_name = get_fqd_name(ascii_name);
+
+ if (conf->idn) {
+ free(ascii_name);
+ }
+ }
+
+ // Create new query.
+ query = query_create(fqd_name, conf);
+
+ free(fqd_name);
+
+ if (query == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // Add new query to the queries.
+ add_tail(queries, (node_t *)query);
+
+ return KNOT_EOK;
+}
+
+static int parse_port(const char *value, query_t *query)
+{
+ char **port;
+
+ // Set current server port (last or query default).
+ if (list_size(&query->servers) > 0) {
+ srv_info_t *server = TAIL(query->servers);
+ port = &(server->service);
+ } else {
+ port = &(query->port);
+ }
+
+ char *new_port = strdup(value);
+
+ if (new_port == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // Deallocate old string.
+ free(*port);
+
+ *port = new_port;
+
+ return KNOT_EOK;
+}
+
+static int parse_reverse(const char *value, list_t *queries, const query_t *conf)
+{
+ query_t *query = NULL;
+
+ // Create reverse name.
+ char *reverse = get_reverse_name(value);
+
+ if (reverse == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ // Create reverse query for given address.
+ query = query_create(reverse, conf);
+
+ free(reverse);
+
+ if (query == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // Set type for reverse query.
+ query->type_num = KNOT_RRTYPE_PTR;
+
+ // Add new query to the queries.
+ add_tail(queries, (node_t *)query);
+
+ return KNOT_EOK;
+}
+
+static int parse_server(const char *value, kdig_params_t *params)
+{
+ query_t *query;
+
+ // Set current query (last or config).
+ if (list_size(&params->queries) > 0) {
+ query = TAIL(params->queries);
+ } else {
+ query = params->config;
+ }
+
+ if (params_parse_server(value, &query->servers, query->port) != KNOT_EOK) {
+ ERR("invalid server @%s", value);
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+static int parse_tsig(const char *value, query_t *query)
+{
+ knot_tsig_key_deinit(&query->tsig_key);
+
+ if (knot_tsig_key_init_str(&query->tsig_key, value) != KNOT_EOK) {
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+static int parse_type(const char *value, query_t *query)
+{
+ uint16_t rtype;
+ int64_t serial;
+ bool notify;
+
+ if (params_parse_type(value, &rtype, &serial, &notify) != KNOT_EOK) {
+ return KNOT_EINVAL;
+ }
+
+ query->type_num = rtype;
+ query->serial = serial;
+ query->notify = notify;
+
+ // If NOTIFY, reset default RD flag.
+ if (query->notify) {
+ query->flags.rd_flag = false;
+ }
+
+ return KNOT_EOK;
+}
+
+#if USE_DNSTAP
+static int parse_dnstap_output(const char *value, query_t *query)
+{
+ if (query->dt_writer != NULL) {
+ if (query->conf == NULL ||
+ query->conf->dt_writer != query->dt_writer) {
+ dt_writer_free(query->dt_writer);
+ }
+ }
+
+ query->dt_writer = dt_writer_create(value, "kdig " PACKAGE_VERSION);
+ if (query->dt_writer == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+static int parse_dnstap_input(const char *value, query_t *query)
+{
+ // Just in case, shouldn't happen.
+ if (query->dt_reader != NULL) {
+ dt_reader_free(query->dt_reader);
+ }
+
+ query->dt_reader = dt_reader_create(value);
+ if (query->dt_reader == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+#endif // USE_DNSTAP
+
+static void complete_servers(query_t *query, const query_t *conf)
+{
+ node_t *n;
+ char *def_port;
+
+ // Decide which default port use.
+ if (strlen(query->port) > 0) {
+ def_port = query->port;
+ } else if (strlen(conf->port) > 0) {
+ def_port = conf->port;
+ } else if (query->https.enable) {
+ def_port = DEFAULT_DNS_HTTPS_PORT;
+ } else if (query->quic.enable) {
+ def_port = DEFAULT_DNS_QUIC_PORT;
+ } else if (query->tls.enable) {
+ def_port = DEFAULT_DNS_TLS_PORT;
+ } else {
+ def_port = DEFAULT_DNS_PORT;
+ }
+
+ // Complete specified nameservers if any.
+ if (list_size(&query->servers) > 0) {
+ WALK_LIST(n, query->servers) {
+ srv_info_t *s = (srv_info_t *)n;
+
+ // If the port isn't specified yet use the default one.
+ if (strlen(s->service) == 0) {
+ free(s->service);
+ s->service = strdup(def_port);
+ if (s->service == NULL) {
+ WARN("can't set port %s", def_port);
+ return;
+ }
+ }
+
+ // Use server name as hostname for TLS if necessary.
+ if (query->tls.enable && query->tls.hostname == NULL &&
+ (query->tls.system_ca || !EMPTY_LIST(query->tls.ca_files))) {
+ query->tls.hostname = strdup(s->name);
+ }
+ }
+ // Use servers from config if any.
+ } else if (list_size(&conf->servers) > 0) {
+ WALK_LIST(n, conf->servers) {
+ srv_info_t *s = (srv_info_t *)n;
+ char *port = def_port;
+
+ // If the port is already specified, use it.
+ if (strlen(s->service) > 0) {
+ port = s->service;
+ }
+
+ srv_info_t *server = srv_info_create(s->name, port);
+ if (server == NULL) {
+ WARN("can't set nameserver %s port %s",
+ s->name, s->service);
+ return;
+ }
+ add_tail(&query->servers, (node_t *)server);
+
+ // Use server name as hostname for TLS if necessary.
+ if (query->tls.enable && query->tls.hostname == NULL &&
+ (query->tls.system_ca || !EMPTY_LIST(query->tls.ca_files))) {
+ query->tls.hostname = strdup(s->name);
+ }
+ }
+ // Use system specific.
+ } else {
+ get_nameservers(&query->servers, def_port);
+ }
+}
+
+static bool compare_servers(list_t *s1, list_t *s2)
+{
+ if (list_size(s1) != list_size(s2)) {
+ return false;
+ }
+
+ node_t *n1, *n2 = HEAD(*s2);
+ WALK_LIST(n1, *s1) {
+ srv_info_t *i1 = (srv_info_t *)n1, *i2 = (srv_info_t *)n2;
+ if (strcmp(i1->service, i2->service) != 0 ||
+ strcmp(i1->name, i2->name) != 0)
+ {
+ return false;
+ }
+ n2 = n2->next;
+ }
+ return true;
+}
+
+void complete_queries(list_t *queries, const query_t *conf)
+{
+ node_t *n;
+
+ if (queries == NULL || conf == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ // If there is no query, add default query: NS to ".".
+ if (list_size(queries) == 0) {
+ query_t *q = query_create(".", conf);
+ if (q == NULL) {
+ WARN("can't create query . NS IN");
+ return;
+ }
+ q->class_num = KNOT_CLASS_IN;
+ q->type_num = KNOT_RRTYPE_NS;
+ add_tail(queries, (node_t *)q);
+ }
+
+ WALK_LIST(n, *queries) {
+ query_t *q = (query_t *)n;
+ query_t *q_prev = (HEAD(*queries) != n) ? (query_t *)n->prev : NULL;
+
+ // Fill class number if missing.
+ if (q->class_num < 0) {
+ if (conf->class_num >= 0) {
+ q->class_num = conf->class_num;
+ } else {
+ q->class_num = KNOT_CLASS_IN;
+ }
+ }
+
+ // Fill type number if missing.
+ if (q->type_num < 0) {
+ if (conf->type_num >= 0) {
+ q->type_num = conf->type_num;
+ q->serial = conf->serial;
+ } else {
+ q->type_num = KNOT_RRTYPE_A;
+ }
+ }
+
+ // Set zone transfer if any.
+ if (q->type_num == KNOT_RRTYPE_AXFR ||
+ q->type_num == KNOT_RRTYPE_IXFR) {
+ q->operation = OPERATION_XFR;
+ }
+
+ // Retries only apply to pure UDP.
+ if (q->protocol == PROTO_TCP ||
+ q->tls.enable || q->https.enable || q->quic.enable) {
+ q->retries = 0;
+ }
+
+ // Complete nameservers list.
+ complete_servers(q, conf);
+
+ // Check if using previous connection makes sense.
+ if (q_prev != NULL && q_prev->keepopen &&
+ (get_socktype(q->protocol, q->type_num) !=
+ get_socktype(q_prev->protocol, q_prev->type_num) ||
+ q->https.enable != q_prev->https.enable ||
+ q->tls.enable != q_prev->tls.enable ||
+ strcmp(q->port, q_prev->port) != 0 ||
+ !compare_servers(&q->servers, &q_prev->servers)))
+ {
+ WARN("connection parameters mismatch for query (%s), "
+ "ignoring keepopen", q->owner);
+ q_prev->keepopen = false;
+ }
+ }
+}
+
+static void print_help(void)
+{
+ printf("Usage: %s [-4] [-6] [-d] [-b address] [-c class] [-p port]\n"
+ " [-q name] [-t type] [-x address] [-k keyfile]\n"
+ " [-y [algo:]keyname:key] [-E tapfile] [-G tapfile]\n"
+ " name [type] [class] [@server]\n"
+ "\n"
+ " +[no]multiline Wrap long records to more lines.\n"
+ " +[no]short Show record data only.\n"
+ " +[no]generic Use generic representation format.\n"
+ " +[no]aaflag Set AA flag.\n"
+ " +[no]tcflag Set TC flag.\n"
+ " +[no]rdflag Set RD flag.\n"
+ " +[no]recurse Same as +[no]rdflag\n"
+ " +[no]raflag Set RA flag.\n"
+ " +[no]zflag Set zero flag bit.\n"
+ " +[no]adflag Set AD flag.\n"
+ " +[no]cdflag Set CD flag.\n"
+ " +[no]dnssec Set DO flag.\n"
+ " +[no]all Show all packet sections.\n"
+ " +[no]qr Show query packet.\n"
+ " +[no]header Show packet header.\n"
+ " +[no]comments Show commented section names.\n"
+ " +[no]opt Show EDNS pseudosection.\n"
+ " +[no]opttext Try to show unknown EDNS options as text.\n"
+ " +[no]optpresent Show EDNS in presenatation format.\n"
+ " +[no]question Show question section.\n"
+ " +[no]answer Show answer section.\n"
+ " +[no]authority Show authority section.\n"
+ " +[no]additional Show additional section.\n"
+ " +[no]tsig Show TSIG pseudosection.\n"
+ " +[no]stats Show trailing packet statistics.\n"
+ " +[no]class Show DNS class.\n"
+ " +[no]ttl Show TTL value.\n"
+ " +[no]crypto Show binary parts of RRSIGs and DNSKEYs.\n"
+ " +[no]tcp Use TCP protocol.\n"
+ " +[no]fastopen Use TCP Fast Open.\n"
+ " +[no]ignore Don't use TCP automatically if truncated.\n"
+ " +[no]keepopen Don't close the TCP connection to be reused.\n"
+ " +[no]tls Use TLS with Opportunistic privacy profile.\n"
+ " +[no]tls-ca[=FILE] Use TLS with Out-Of-Band privacy profile.\n"
+ " +[no]tls-pin=BASE64 Use TLS with pinned certificate.\n"
+ " +[no]tls-hostname=STR Use TLS with remote server hostname.\n"
+ " +[no]tls-sni=STR Use TLS with Server Name Indication.\n"
+ " +[no]tls-keyfile=FILE Use TLS with a client keyfile.\n"
+ " +[no]tls-certfile=FILE Use TLS with a client certfile.\n"
+ " +[no]tls-ocsp-stapling[=H] Use TLS with a valid stapled OCSP response for the\n"
+ " server certificate (%u or specify hours).\n"
+#ifdef LIBNGHTTP2
+ " +[no]https[=URL] Use HTTPS protocol. It's also possible to specify\n"
+ " URL as [authority][/path] where query will be sent.\n"
+ " +[no]https-get Use HTTPS protocol with GET method instead of POST.\n"
+#endif
+#ifdef ENABLE_QUIC
+ " +[no]quic Use QUIC protocol.\n"
+#endif
+ " +[no]nsid Request NSID.\n"
+ " +[no]bufsize=B Set EDNS buffer size.\n"
+ " +[no]padding[=N] Pad with EDNS(0) (default or specify size).\n"
+ " +[no]alignment[=N] Pad with EDNS(0) to blocksize (%u or specify size).\n"
+ " +[no]subnet=SUBN Set EDNS(0) client subnet addr/prefix.\n"
+ " +[no]edns[=N] Use EDNS(=version).\n"
+ " +[no]timeout=T Set wait for reply interval in seconds.\n"
+ " +[no]retry=N Set number of retries.\n"
+ " +[no]expire Set the EXPIRE EDNS option.\n"
+ " +[no]cookie[=HEX] Attach EDNS(0) cookie to the query.\n"
+ " +[no]badcookie Repeat a query with the correct cookie.\n"
+ " +[no]ednsopt=CODE[:HEX] Set custom EDNS option.\n"
+ " +[no]proxy=SADDR-DADDR Add PROXYv2 header with src and dest addresses.\n"
+ " +[no]json Use JSON for output encoding (RFC 8427).\n"
+ " +noidn Disable IDN transformation.\n"
+ "\n"
+ " -h, --help Print the program help.\n"
+ " -V, --version Print the program version.\n",
+ PROGRAM_NAME, DEFAULT_TLS_OCSP_STAPLING / 3600, DEFAULT_ALIGNMENT_SIZE);
+}
+
+static int parse_opt1(const char *opt, const char *value, kdig_params_t *params,
+ int *index)
+{
+ const char *val = value;
+ size_t len = strlen(opt);
+ int add = 1;
+ query_t *query;
+
+ // Set current query (last or config).
+ if (list_size(&params->queries) > 0) {
+ query = TAIL(params->queries);
+ } else {
+ query = params->config;
+ }
+
+ // If there is no space between option and argument.
+ if (len > 1) {
+ val = opt + 1;
+ add = 0;
+ }
+
+ switch (opt[0]) {
+ case '4':
+ if (len > 1) {
+ ERR("invalid option -%s", opt);
+ return KNOT_ENOTSUP;
+ }
+
+ query->ip = IP_4;
+ break;
+ case '6':
+ if (len > 1) {
+ ERR("invalid option -%s", opt);
+ return KNOT_ENOTSUP;
+ }
+
+ query->ip = IP_6;
+ break;
+ case 'b':
+ if (val == NULL) {
+ ERR("missing address");
+ return KNOT_EINVAL;
+ }
+
+ if (parse_local(val, query) != KNOT_EOK) {
+ ERR("bad address %s", val);
+ return KNOT_EINVAL;
+ }
+ *index += add;
+ break;
+ case 'd':
+ msg_enable_debug(1);
+ break;
+ case 'h':
+ if (len > 1) {
+ ERR("invalid option -%s", opt);
+ return KNOT_ENOTSUP;
+ }
+
+ print_help();
+ params->stop = true;
+ break;
+ case 'c':
+ if (val == NULL) {
+ ERR("missing class");
+ return KNOT_EINVAL;
+ }
+
+ if (parse_class(val, query) != KNOT_EOK) {
+ ERR("bad class %s", val);
+ return KNOT_EINVAL;
+ }
+ *index += add;
+ break;
+ case 'k':
+ if (val == NULL) {
+ ERR("missing filename");
+ return KNOT_EINVAL;
+ }
+
+ if (parse_keyfile(val, query) != KNOT_EOK) {
+ ERR("bad keyfile %s", value);
+ return KNOT_EINVAL;
+ }
+ *index += add;
+ break;
+ case 'p':
+ if (val == NULL) {
+ ERR("missing port");
+ return KNOT_EINVAL;
+ }
+
+ if (parse_port(val, query) != KNOT_EOK) {
+ ERR("bad port %s", value);
+ return KNOT_EINVAL;
+ }
+ *index += add;
+ break;
+ case 'q':
+ // Allow empty QNAME.
+ if (parse_name(val, &params->queries, params->config)
+ != KNOT_EOK) {
+ ERR("bad query name %s", val);
+ return KNOT_EINVAL;
+ }
+ *index += add;
+ break;
+ case 't':
+ if (val == NULL) {
+ ERR("missing type");
+ return KNOT_EINVAL;
+ }
+
+ if (parse_type(val, query) != KNOT_EOK) {
+ ERR("bad type %s", val);
+ return KNOT_EINVAL;
+ }
+ *index += add;
+ break;
+ case 'V':
+ if (len > 1) {
+ ERR("invalid option -%s", opt);
+ return KNOT_ENOTSUP;
+ }
+
+ print_version(PROGRAM_NAME);
+ params->stop = true;
+ break;
+ case 'x':
+ if (val == NULL) {
+ ERR("missing address");
+ return KNOT_EINVAL;
+ }
+
+ if (parse_reverse(val, &params->queries, params->config)
+ != KNOT_EOK) {
+ ERR("bad reverse name %s", val);
+ return KNOT_EINVAL;
+ }
+ *index += add;
+ break;
+ case 'y':
+ if (val == NULL) {
+ ERR("missing key");
+ return KNOT_EINVAL;
+ }
+
+ if (parse_tsig(val, query) != KNOT_EOK) {
+ ERR("bad key %s", value);
+ return KNOT_EINVAL;
+ }
+ *index += add;
+ break;
+ case 'E':
+#if USE_DNSTAP
+ if (val == NULL) {
+ ERR("missing filename");
+ return KNOT_EINVAL;
+ }
+
+ if (parse_dnstap_output(val, query) != KNOT_EOK) {
+ ERR("unable to open dnstap output file %s", val);
+ return KNOT_EINVAL;
+ }
+ *index += add;
+#else
+ ERR("no dnstap support but -E specified");
+ return KNOT_EINVAL;
+#endif // USE_DNSTAP
+ break;
+ case 'G':
+#if USE_DNSTAP
+ if (val == NULL) {
+ ERR("missing filename");
+ return KNOT_EINVAL;
+ }
+
+ query = query_create(NULL, params->config);
+ if (query == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ if (parse_dnstap_input(val, query) != KNOT_EOK) {
+ ERR("unable to open dnstap input file %s", val);
+ query_free(query);
+ return KNOT_EINVAL;
+ }
+
+ query->operation = OPERATION_LIST_DNSTAP;
+ add_tail(&params->queries, (node_t *)query);
+
+ *index += add;
+#else
+ ERR("no dnstap support but -G specified");
+ return KNOT_EINVAL;
+#endif // USE_DNSTAP
+ break;
+ case '-':
+ if (strcmp(opt, "-help") == 0) {
+ print_help();
+ params->stop = true;
+ } else if (strcmp(opt, "-version") == 0) {
+ print_version(PROGRAM_NAME);
+ params->stop = true;
+ } else {
+ ERR("invalid option: -%s", opt);
+ return KNOT_ENOTSUP;
+ }
+ break;
+ default:
+ ERR("invalid option: -%s", opt);
+ return KNOT_ENOTSUP;
+ }
+
+ return KNOT_EOK;
+}
+
+static int parse_opt2(const char *value, kdig_params_t *params)
+{
+ query_t *query;
+
+ // Set current query (last or config).
+ if (list_size(&params->queries) > 0) {
+ query = TAIL(params->queries);
+ } else {
+ query = params->config;
+ }
+
+ // Get option name.
+ const char *arg_sep = "=";
+ size_t opt_len = strcspn(value, arg_sep);
+ if (opt_len < 1) {
+ ERR("invalid option: +%s", value);
+ return KNOT_ENOTSUP;
+ }
+
+ // Get option argument if any.
+ const char *arg = NULL;
+ const char *rest = value + opt_len;
+ if (strlen(rest) > 0) {
+ arg = rest + strspn(rest, arg_sep);
+ }
+
+ // Check if the given option is supported.
+ bool unique;
+ int ret = best_param(value, opt_len, kdig_opts2, &unique);
+ if (ret < 0) {
+ ERR("invalid option: +%s", value);
+ return KNOT_ENOTSUP;
+ } else if (!unique) {
+ ERR("ambiguous option: +%s", value);
+ return KNOT_ENOTSUP;
+ }
+
+ // Check argument presence.
+ switch (kdig_opts2[ret].arg) {
+ case ARG_NONE:
+ if (arg != NULL && *arg != '\0') {
+ WARN("superfluous option argument: +%s", value);
+ }
+ break;
+ case ARG_REQUIRED:
+ if (arg == NULL) {
+ ERR("missing argument: +%s", value);
+ return KNOT_EFEWDATA;
+ }
+ // FALLTHROUGH
+ case ARG_OPTIONAL:
+ if (arg != NULL && *arg == '\0') {
+ ERR("empty argument: +%s", value);
+ return KNOT_EFEWDATA;
+ }
+ break;
+ }
+
+ // Call option handler.
+ return kdig_opts2[ret].handler(arg, query);
+}
+
+static int parse_token(const char *value, kdig_params_t *params)
+{
+ query_t *query;
+
+ // Set current query (last or config).
+ if (list_size(&params->queries) > 0) {
+ query = TAIL(params->queries);
+ } else {
+ query = params->config;
+ }
+
+ // Try to guess the meaning of the token.
+ if (strlen(value) == 0) {
+ ERR("invalid empty parameter");
+ } else if (parse_type(value, query) == KNOT_EOK) {
+ return KNOT_EOK;
+ } else if (parse_class(value, query) == KNOT_EOK) {
+ return KNOT_EOK;
+ } else if (parse_name(value, &params->queries, params->config) == KNOT_EOK) {
+ return KNOT_EOK;
+ } else {
+ ERR("invalid parameter: %s", value);
+ }
+
+ return KNOT_EINVAL;
+}
+
+int kdig_parse(kdig_params_t *params, int argc, char *argv[])
+{
+ if (params == NULL || argv == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ // Initialize parameters.
+ if (kdig_init(params) != KNOT_EOK) {
+ return KNOT_ERROR;
+ }
+
+#ifdef LIBIDN
+ // Set up localization.
+ if (setlocale(LC_CTYPE, "") == NULL) {
+ WARN("can't setlocale, disabling IDN");
+ params->config->idn = false;
+ params->config->style.style.ascii_to_idn = NULL;
+ }
+#endif
+
+ // Command line parameters processing.
+ for (int i = 1; i < argc; i++) {
+ int ret = KNOT_ERROR;
+
+ // Process parameter.
+ switch (argv[i][0]) {
+ case '@':
+ ret = parse_server(argv[i] + 1, params);
+ break;
+ case '-':
+ ret = parse_opt1(argv[i] + 1, argv[i + 1], params, &i);
+ break;
+ case '+':
+ ret = parse_opt2(argv[i] + 1, params);
+ break;
+ default:
+ ret = parse_token(argv[i], params);
+ break;
+ }
+
+ // Check return.
+ switch (ret) {
+ case KNOT_EOK:
+ if (params->stop) {
+ return KNOT_EOK;
+ }
+ break;
+ case KNOT_ENOTSUP:
+ print_help();
+ default: // Fall through.
+ return ret;
+ }
+ }
+
+ // Complete missing data in queries based on defaults.
+ complete_queries(&params->queries, params->config);
+
+ return KNOT_EOK;
+}
diff --git a/src/utils/kdig/kdig_params.h b/src/utils/kdig/kdig_params.h
new file mode 100644
index 0000000..03aafe3
--- /dev/null
+++ b/src/utils/kdig/kdig_params.h
@@ -0,0 +1,187 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+#include "utils/common/params.h"
+#include "utils/common/exec.h"
+#include "utils/common/https.h"
+#include "utils/common/quic.h"
+#include "utils/common/sign.h"
+#include "libknot/libknot.h"
+#include "contrib/sockaddr.h"
+
+#if USE_DNSTAP
+# include "contrib/dnstap/reader.h"
+# include "contrib/dnstap/writer.h"
+#endif // USE_DNSTAP
+
+/*! \brief Operation mode of kdig. */
+typedef enum {
+ /*!< Standard 1-message query/reply. */
+ OPERATION_QUERY,
+ /*!< Zone transfer (AXFR or IXFR). */
+ OPERATION_XFR,
+ /*!< Dump dnstap file. */
+ OPERATION_LIST_DNSTAP,
+} operation_t;
+
+/*! \brief DNS header and EDNS flags. */
+typedef struct {
+ /*!< Authoritative answer flag. */
+ bool aa_flag;
+ /*!< Truncated flag. */
+ bool tc_flag;
+ /*!< Recursion desired flag. */
+ bool rd_flag;
+ /*!< Recursion available flag. */
+ bool ra_flag;
+ /*!< Z flag. */
+ bool z_flag;
+ /*!< Authenticated data flag. */
+ bool ad_flag;
+ /*!< Checking disabled flag. */
+ bool cd_flag;
+ /*!< DNSSEC OK flag. */
+ bool do_flag;
+} flags_t;
+
+/*! \brief Basic parameters for DNS query. */
+typedef struct query query_t; // Forward declaration due to configuration.
+struct query {
+ /*!< List node (for list container). */
+ node_t n;
+ /*!< Reference to global config. */
+ const query_t *conf;
+ /*!< Name to query on. */
+ char *owner;
+ /*!< List of nameservers to query to. */
+ list_t servers;
+ /*!< Local interface (optional). */
+ srv_info_t *local;
+ /*!< Operation mode. */
+ operation_t operation;
+ /*!< Version of ip protocol to use. */
+ ip_t ip;
+ /*!< Protocol type (TCP, UDP) to use. */
+ protocol_t protocol;
+ /*!< Use TCP Fast Open. */
+ bool fastopen;
+ /*!< Keep TCP connection open. */
+ bool keepopen;
+ /*!< Port/service to connect to. */
+ char *port;
+ /*!< UDP buffer size (16unsigned + -1 uninitialized). */
+ int32_t udp_size;
+ /*!< Number of UDP retries. */
+ uint32_t retries;
+ /*!< Wait for network response in seconds (-1 means forever). */
+ int32_t wait;
+ /*!< Ignore truncated response. */
+ bool ignore_tc;
+ /*!< Class number (16unsigned + -1 uninitialized). */
+ int32_t class_num;
+ /*!< Type number (16unsigned + -1 uninitialized). */
+ int32_t type_num;
+ /*!< SOA serial for IXFR and NOTIFY (32unsigned + -1 uninitialized). */
+ int64_t serial;
+ /*!< NOTIFY query. */
+ bool notify;
+ /*!< Header flags. */
+ flags_t flags;
+ /*!< Output settings. */
+ style_t style;
+ /*!< IDN conversion. */
+ bool idn;
+ /*!< Query for NSID. */
+ bool nsid;
+ /*!< EDNS version (8unsigned + -1 uninitialized). */
+ int16_t edns;
+ /*!< EDNS client cookie. */
+ knot_edns_cookie_t cc;
+ /*!< EDNS server cookie. */
+ knot_edns_cookie_t sc;
+ /*!< Repeat query after BADCOOKIE. */
+ int badcookie;
+ /*!< EDNS0 padding (16unsigned + -1 ~ uninitialized, -2 ~ default, -3 ~ none). */
+ int32_t padding;
+ /*!< Query alignment with EDNS0 padding (0 ~ uninitialized). */
+ uint16_t alignment;
+ /*!< TLS parameters. */
+ tls_params_t tls;
+ /*!< HTTPS parameters. */
+ https_params_t https;
+ /*!< QUIC parameters. */
+ quic_params_t quic;
+ /*!< Transaction signature. */
+ knot_tsig_key_t tsig_key;
+ /*!< EDNS client subnet. */
+ knot_edns_client_subnet_t subnet;
+ /*!< Lits of custom EDNS options. */
+ list_t edns_opts;
+ /*!< PROXYv2 source and destination address. */
+ struct {
+ struct sockaddr_storage src;
+ struct sockaddr_storage dst;
+ } proxy;
+#if USE_DNSTAP
+ /*!< Context for dnstap reader input. */
+ dt_reader_t *dt_reader;
+ /*!< Context for dnstap writer output. */
+ dt_writer_t *dt_writer;
+#endif // USE_DNSTAP
+};
+
+/*! \brief EDNS option data. */
+typedef struct {
+ /*! List node (for list container). */
+ node_t n;
+ /*!< OPTION-CODE field. */
+ uint16_t code;
+ /*!< OPTION-LENGTH field. */
+ uint16_t length;
+ /*!< OPTION-DATA field. */
+ uint8_t *data;
+} ednsopt_t;
+
+/*! \brief Settings for kdig. */
+typedef struct {
+ /*!< Stop processing - just print help, version,... */
+ bool stop;
+ /*!< List of DNS queries to process. */
+ list_t queries;
+ /*!< Default settings for queries. */
+ query_t *config;
+} kdig_params_t;
+
+query_t *query_create(const char *owner, const query_t *config);
+void query_free(query_t *query);
+void complete_queries(list_t *queries, const query_t *conf);
+
+ednsopt_t *ednsopt_create(uint16_t code, uint16_t length, uint8_t *data);
+ednsopt_t *ednsopt_dup(const ednsopt_t *opt);
+void ednsopt_free(ednsopt_t *opt);
+
+void ednsopt_list_init(list_t *list);
+void ednsopt_list_deinit(list_t *list);
+int ednsopt_list_dup(list_t *dst, const list_t *src);
+bool ednsopt_list_empty(const list_t *list);
+
+int kdig_init(kdig_params_t *params);
+int kdig_parse(kdig_params_t *params, int argc, char *argv[]);
+void kdig_clean(kdig_params_t *params);
diff --git a/src/utils/keymgr/bind_privkey.c b/src/utils/keymgr/bind_privkey.c
new file mode 100644
index 0000000..9ab895c
--- /dev/null
+++ b/src/utils/keymgr/bind_privkey.c
@@ -0,0 +1,411 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+
+#include "contrib/ctype.h"
+#include "contrib/strtonum.h"
+#include "libdnssec/binary.h"
+#include "libdnssec/error.h"
+#include "libdnssec/pem.h"
+#include "libdnssec/shared/shared.h"
+#include "utils/keymgr/bind_privkey.h"
+
+/* -- private key params conversion ---------------------------------------- */
+
+/*!
+ * Private key attribute conversion.
+ */
+typedef struct param_t {
+ char *name;
+ size_t offset;
+ int (*parse_cb)(char *string, void *data);
+ void (*free_cb)(void *data);
+} param_t;
+
+static int parse_algorithm(char *string, void *_algorithm);
+static int parse_binary(char *string, void *_binary);
+static int parse_time(char *string, void *_time);
+
+static void binary_free(void *_binary)
+{
+ dnssec_binary_t *binary = _binary;
+ dnssec_binary_free(binary);
+}
+
+/*!
+ * Know attributes in private key file.
+ */
+const param_t PRIVKEY_CONVERSION_TABLE[] = {
+ #define o(field) offsetof(bind_privkey_t, field)
+ { "Algorithm", o(algorithm), parse_algorithm, NULL },
+ { "Modulus", o(modulus), parse_binary, binary_free },
+ { "PublicExponent", o(public_exponent), parse_binary, binary_free },
+ { "PrivateExponent", o(private_exponent), parse_binary, binary_free },
+ { "Prime1", o(prime_one), parse_binary, binary_free },
+ { "Prime2", o(prime_two), parse_binary, binary_free },
+ { "Exponent1", o(exponent_one), parse_binary, binary_free },
+ { "Exponent2", o(exponent_two), parse_binary, binary_free },
+ { "Coefficient", o(coefficient), parse_binary, binary_free },
+ { "PrivateKey", o(private_key), parse_binary, binary_free },
+ { "Created", o(time_created), parse_time, NULL },
+ { "Publish", o(time_publish), parse_time, NULL },
+ { "Activate", o(time_activate), parse_time, NULL },
+ { "Revoke", o(time_revoke), parse_time, NULL },
+ { "Inactive", o(time_inactive), parse_time, NULL },
+ { "Delete", o(time_delete), parse_time, NULL },
+ { NULL }
+ #undef o
+};
+
+/* -- attribute parsing ---------------------------------------------------- */
+
+/*!
+ * Parse key algorithm field.
+ *
+ * Example: 7 (NSEC3RSASHA1)
+ *
+ * Only the numeric value is decoded, the rest of the value is ignored.
+ */
+static int parse_algorithm(char *string, void *_algorithm)
+{
+ char *end = string;
+ while (*end != '\0' && !is_space(*end)) {
+ end += 1;
+ }
+ *end = '\0';
+
+ uint8_t *algorithm = _algorithm;
+ int r = str_to_u8(string, algorithm);
+
+ return (r == KNOT_EOK ? DNSSEC_EOK : DNSSEC_INVALID_KEY_ALGORITHM);
+}
+
+/*!
+ * Parse binary data encoded in Base64.
+ *
+ * Example: AQAB
+ */
+static int parse_binary(char *string, void *_binary)
+{
+ dnssec_binary_t base64 = {
+ .data = (uint8_t *)string,
+ .size = strlen(string)
+ };
+
+ dnssec_binary_t *binary = _binary;
+ return dnssec_binary_from_base64(&base64, binary);
+}
+
+#define LEGACY_DATE_FORMAT "%Y%m%d%H%M%S"
+
+/*!
+ * Parse timestamp in a format in \ref LEGACY_DATE_FORMAT.
+ *
+ * Example: 20140415151855
+ */
+static int parse_time(char *string, void *_time)
+{
+ struct tm tm = { 0 };
+
+ char *end = strptime(string, LEGACY_DATE_FORMAT, &tm);
+ if (end == NULL || *end != '\0') {
+ return DNSSEC_MALFORMED_DATA;
+ }
+
+ time_t *time = _time;
+ *time = timegm(&tm);
+
+ return DNSSEC_EOK;
+}
+
+/* -- key parsing ---------------------------------------------------------- */
+
+/*!
+ * Strip string value of left and right whitespaces.
+ *
+ * \param[in,out] value Start of the string.
+ * \param[in,out] length Length of the string.
+ *
+ */
+static void strip(char **value, size_t *length)
+{
+ // strip from left
+ while (*length > 0 && is_space(**value)) {
+ *value += 1;
+ *length -= 1;
+ }
+ // strip from right
+ while (*length > 0 && is_space((*value)[*length - 1])) {
+ *length -= 1;
+ }
+}
+
+/*!
+ * Parse one line of the private key file.
+ */
+static int parse_line(bind_privkey_t *params, char *line, size_t length)
+{
+ assert(params);
+ assert(line);
+
+ strip(&line, &length);
+ if (length == 0) {
+ return KNOT_EOK; // blank line
+ }
+
+ char *separator = memchr(line, ':', length);
+ if (!separator) {
+ return DNSSEC_MALFORMED_DATA;
+ }
+
+ char *key = line;
+ size_t key_length = separator - key;
+ strip(&key, &key_length);
+
+ char *value = separator + 1;
+ size_t value_length = (line + length) - value;
+ strip(&value, &value_length);
+
+ if (key_length == 0 || value_length == 0) {
+ return DNSSEC_MALFORMED_DATA;
+ }
+
+ key[key_length] = '\0';
+ value[value_length] = '\0';
+
+ for (const param_t *p = PRIVKEY_CONVERSION_TABLE; p->name != NULL; p++) {
+ size_t name_length = strlen(p->name);
+ if (name_length != key_length) {
+ continue;
+ }
+
+ if (strcasecmp(p->name, key) != 0) {
+ continue;
+ }
+
+ return p->parse_cb(value, (void *)params + p->offset);
+ }
+
+ // ignore unknown attributes
+
+ return DNSSEC_EOK;
+}
+
+int bind_privkey_parse(const char *filename, bind_privkey_t *params_ptr)
+{
+ _cleanup_fclose_ FILE *file = fopen(filename, "r");
+ if (!file) {
+ return DNSSEC_NOT_FOUND;
+ }
+
+ bind_privkey_t params = *params_ptr;
+
+ _cleanup_free_ char *line = NULL;
+ size_t size = 0;
+ ssize_t read = 0;
+ while ((read = getline(&line, &size, file)) != -1) {
+ int r = parse_line(&params, line, read);
+ if (r != DNSSEC_EOK) {
+ bind_privkey_free(&params);
+ return r;
+ }
+ }
+
+ *params_ptr = params;
+
+ return DNSSEC_EOK;
+}
+
+/* -- freeing -------------------------------------------------------------- */
+
+/*!
+ * Free private key parameters.
+ */
+void bind_privkey_free(bind_privkey_t *params)
+{
+ if (!params) {
+ return;
+ }
+
+ for (const param_t *p = PRIVKEY_CONVERSION_TABLE; p->name != NULL; p++) {
+ if (p->free_cb) {
+ p->free_cb((void *)params + p->offset);
+ }
+ }
+
+ clear_struct(params);
+}
+
+/* -- export to PEM -------------------------------------------------------- */
+
+static int rsa_params_to_pem(const bind_privkey_t *params, dnssec_binary_t *pem)
+{
+ _cleanup_x509_privkey_ gnutls_x509_privkey_t key = NULL;
+ int result = gnutls_x509_privkey_init(&key);
+ if (result != GNUTLS_E_SUCCESS) {
+ return DNSSEC_ENOMEM;
+ }
+
+ gnutls_datum_t m = binary_to_datum(&params->modulus);
+ gnutls_datum_t e = binary_to_datum(&params->public_exponent);
+ gnutls_datum_t d = binary_to_datum(&params->private_exponent);
+ gnutls_datum_t p = binary_to_datum(&params->prime_one);
+ gnutls_datum_t q = binary_to_datum(&params->prime_two);
+ gnutls_datum_t u = binary_to_datum(&params->coefficient);
+
+ result = gnutls_x509_privkey_import_rsa_raw(key, &m, &e, &d, &p, &q, &u);
+ if (result != GNUTLS_E_SUCCESS) {
+ return DNSSEC_KEY_IMPORT_ERROR;
+ }
+
+ return dnssec_pem_from_x509(key, pem);
+}
+
+/*!
+ * \see lib/key/convert.h
+ */
+static gnutls_ecc_curve_t choose_ecdsa_curve(size_t pubkey_size)
+{
+ switch (pubkey_size) {
+#ifdef HAVE_ED25519
+ case 32: return GNUTLS_ECC_CURVE_ED25519;
+#endif
+#ifdef HAVE_ED448
+ case 57: return GNUTLS_ECC_CURVE_ED448;
+#endif
+ case 64: return GNUTLS_ECC_CURVE_SECP256R1;
+ case 96: return GNUTLS_ECC_CURVE_SECP384R1;
+ default: return GNUTLS_ECC_CURVE_INVALID;
+ }
+}
+
+static void ecdsa_extract_public_params(dnssec_key_t *key, gnutls_ecc_curve_t *curve,
+ gnutls_datum_t *x, gnutls_datum_t *y)
+{
+ dnssec_binary_t pubkey = { 0 };
+ dnssec_key_get_pubkey(key, &pubkey);
+
+ *curve = choose_ecdsa_curve(pubkey.size);
+
+ size_t param_size = pubkey.size / 2;
+ x->data = pubkey.data;
+ x->size = param_size;
+ y->data = pubkey.data + param_size;
+ y->size = param_size;
+}
+
+static int ecdsa_params_to_pem(dnssec_key_t *dnskey, const bind_privkey_t *params,
+ dnssec_binary_t *pem)
+{
+ _cleanup_x509_privkey_ gnutls_x509_privkey_t key = NULL;
+ int result = gnutls_x509_privkey_init(&key);
+ if (result != GNUTLS_E_SUCCESS) {
+ return DNSSEC_ENOMEM;
+ }
+
+ gnutls_ecc_curve_t curve = 0;
+ gnutls_datum_t x = { 0 };
+ gnutls_datum_t y = { 0 };
+ ecdsa_extract_public_params(dnskey, &curve, &x, &y);
+
+ gnutls_datum_t k = binary_to_datum(&params->private_key);
+
+ result = gnutls_x509_privkey_import_ecc_raw(key, curve, &x, &y, &k);
+ if (result != DNSSEC_EOK) {
+ return DNSSEC_KEY_IMPORT_ERROR;
+ }
+
+ gnutls_x509_privkey_fix(key);
+
+ return dnssec_pem_from_x509(key, pem);
+}
+
+#if defined(HAVE_ED25519) || defined(HAVE_ED448)
+static void eddsa_extract_public_params(dnssec_key_t *key, gnutls_ecc_curve_t *curve,
+ gnutls_datum_t *x)
+{
+ dnssec_binary_t pubkey = { 0 };
+ dnssec_key_get_pubkey(key, &pubkey);
+
+ *curve = choose_ecdsa_curve(pubkey.size);
+
+ x->data = pubkey.data;
+ x->size = pubkey.size;
+}
+
+static int eddsa_params_to_pem(dnssec_key_t *dnskey, const bind_privkey_t *params,
+ dnssec_binary_t *pem)
+{
+ _cleanup_x509_privkey_ gnutls_x509_privkey_t key = NULL;
+ int result = gnutls_x509_privkey_init(&key);
+ if (result != GNUTLS_E_SUCCESS) {
+ return DNSSEC_ENOMEM;
+ }
+
+ gnutls_ecc_curve_t curve = 0;
+ gnutls_datum_t x = { 0 };
+ eddsa_extract_public_params(dnskey, &curve, &x);
+
+ gnutls_datum_t k = binary_to_datum(&params->private_key);
+
+ result = gnutls_x509_privkey_import_ecc_raw(key, curve, &x, NULL, &k);
+ if (result != DNSSEC_EOK) {
+ return DNSSEC_KEY_IMPORT_ERROR;
+ }
+
+ gnutls_x509_privkey_fix(key);
+
+ return dnssec_pem_from_x509(key, pem);
+}
+#endif
+
+int bind_privkey_to_pem(dnssec_key_t *key, bind_privkey_t *params, dnssec_binary_t *pem)
+{
+ dnssec_key_algorithm_t algorithm = dnssec_key_get_algorithm(key);
+ switch (algorithm) {
+ case DNSSEC_KEY_ALGORITHM_RSA_SHA1:
+ case DNSSEC_KEY_ALGORITHM_RSA_SHA1_NSEC3:
+ case DNSSEC_KEY_ALGORITHM_RSA_SHA256:
+ case DNSSEC_KEY_ALGORITHM_RSA_SHA512:
+ return rsa_params_to_pem(params, pem);
+ case DNSSEC_KEY_ALGORITHM_ECDSA_P256_SHA256:
+ case DNSSEC_KEY_ALGORITHM_ECDSA_P384_SHA384:
+ return ecdsa_params_to_pem(key, params, pem);
+#ifdef HAVE_ED25519
+ case DNSSEC_KEY_ALGORITHM_ED25519:
+#endif
+#ifdef HAVE_ED448
+ case DNSSEC_KEY_ALGORITHM_ED448:
+#endif
+#if defined(HAVE_ED25519) || defined(HAVE_ED448)
+ return eddsa_params_to_pem(key, params, pem);
+#endif
+ default:
+ return DNSSEC_INVALID_KEY_ALGORITHM;
+ }
+}
+
+void bind_privkey_to_timing(bind_privkey_t *params, knot_kasp_key_timing_t *timing)
+{
+ timing->created = (knot_time_t)params->time_created;
+ timing->publish = (knot_time_t)params->time_publish;
+ timing->ready = 0;
+ timing->active = (knot_time_t)params->time_activate;
+ timing->retire = (knot_time_t)params->time_inactive;
+ timing->revoke = (knot_time_t)params->time_revoke;
+ timing->remove = (knot_time_t)params->time_delete;
+}
diff --git a/src/utils/keymgr/bind_privkey.h b/src/utils/keymgr/bind_privkey.h
new file mode 100644
index 0000000..cdb4924
--- /dev/null
+++ b/src/utils/keymgr/bind_privkey.h
@@ -0,0 +1,72 @@
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdint.h>
+#include <time.h>
+
+#include "libdnssec/binary.h"
+#include "knot/dnssec/kasp/policy.h"
+
+/*!
+ * Legacy private key parameters.
+ */
+typedef struct {
+ // key information
+ uint8_t algorithm;
+
+ // RSA
+ dnssec_binary_t modulus;
+ dnssec_binary_t public_exponent;
+ dnssec_binary_t private_exponent;
+ dnssec_binary_t prime_one;
+ dnssec_binary_t prime_two;
+ dnssec_binary_t exponent_one;
+ dnssec_binary_t exponent_two;
+ dnssec_binary_t coefficient;
+
+ // ECDSA
+ dnssec_binary_t private_key;
+
+ // key lifetime
+ time_t time_created;
+ time_t time_publish;
+ time_t time_activate;
+ time_t time_revoke;
+ time_t time_inactive;
+ time_t time_delete;
+} bind_privkey_t;
+
+/*!
+ * Extract parameters from legacy private key file.
+ */
+int bind_privkey_parse(const char *filename, bind_privkey_t *params);
+
+/*!
+ * Free private key parameters.
+ */
+void bind_privkey_free(bind_privkey_t *params);
+
+/*!
+ * Generate PEM from pub&priv key.
+ */
+int bind_privkey_to_pem(dnssec_key_t *key, bind_privkey_t *params, dnssec_binary_t *pem);
+
+/*!
+ * Extract timing info.
+ */
+void bind_privkey_to_timing(bind_privkey_t *params, knot_kasp_key_timing_t *timing);
diff --git a/src/utils/keymgr/functions.c b/src/utils/keymgr/functions.c
new file mode 100644
index 0000000..bd5345e
--- /dev/null
+++ b/src/utils/keymgr/functions.c
@@ -0,0 +1,1161 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <limits.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+#include <fcntl.h>
+
+#include "utils/keymgr/functions.h"
+
+#include "utils/common/msg.h"
+#include "utils/keymgr/bind_privkey.h"
+#include "contrib/base64.h"
+#include "contrib/color.h"
+#include "contrib/ctype.h"
+#include "contrib/json.h"
+#include "contrib/string.h"
+#include "contrib/strtonum.h"
+#include "contrib/tolower.h"
+#include "contrib/wire_ctx.h"
+#include "libdnssec/error.h"
+#include "libdnssec/keyid.h"
+#include "libdnssec/shared/shared.h"
+#include "knot/dnssec/kasp/policy.h"
+#include "knot/dnssec/key-events.h"
+#include "knot/dnssec/rrset-sign.h"
+#include "knot/dnssec/zone-events.h"
+#include "knot/dnssec/zone-keys.h"
+#include "knot/dnssec/zone-sign.h"
+#include "libzscanner/scanner.h"
+
+int parse_timestamp(char *arg, knot_time_t *stamp)
+{
+ int ret = knot_time_parse("YMDhms|'now'+-#u|'t'+-#u|+-#u|'t'+-#|+-#|#",
+ arg, stamp);
+ if (ret < 0) {
+ ERR2("invalid timestamp: %s", arg);
+ return KNOT_EINVAL;
+ }
+ return KNOT_EOK;
+}
+
+static bool init_timestamps(char *arg, knot_kasp_key_timing_t *timing)
+{
+ knot_time_t *dst = NULL;
+
+ if (strncasecmp(arg, "created=", 8) == 0) {
+ dst = &timing->created;
+ } else if (strncasecmp(arg, "publish=", 8) == 0) {
+ dst = &timing->publish;
+ } else if (strncasecmp(arg, "ready=", 6) == 0) {
+ dst = &timing->ready;
+ } else if (strncasecmp(arg, "active=", 7) == 0) {
+ dst = &timing->active;
+ } else if (strncasecmp(arg, "retire=", 7) == 0) {
+ dst = &timing->retire;
+ } else if (strncasecmp(arg, "remove=", 7) == 0) {
+ dst = &timing->remove;
+ } else if (strncasecmp(arg, "pre_active=", 11) == 0) {
+ dst = &timing->pre_active;
+ } else if (strncasecmp(arg, "post_active=", 12) == 0) {
+ dst = &timing->post_active;
+ } else if (strncasecmp(arg, "retire_active=", 14) == 0) {
+ dst = &timing->retire_active;
+ } else if (strncasecmp(arg, "revoke=", 7) == 0) {
+ dst = &timing->revoke;
+ } else {
+ return false;
+ }
+
+ knot_time_t stamp;
+ int ret = parse_timestamp(strchr(arg, '=') + 1, &stamp);
+ if (ret != KNOT_EOK) {
+ return true;
+ }
+
+ *dst = stamp;
+
+ return true;
+}
+
+static bool str2bool(const char *s)
+{
+ switch (knot_tolower(s[0])) {
+ case '1':
+ case 'y':
+ case 't':
+ return true;
+ default:
+ return false;
+ }
+}
+
+static void bitmap_set(kdnssec_generate_flags_t *bitmap, int flag, bool onoff)
+{
+ if (onoff) {
+ *bitmap |= flag;
+ } else {
+ *bitmap &= ~flag;
+ }
+}
+
+static bool genkeyargs(int argc, char *argv[], bool just_timing,
+ kdnssec_generate_flags_t *flags, dnssec_key_algorithm_t *algorithm,
+ uint16_t *keysize, knot_kasp_key_timing_t *timing,
+ const char **addtopolicy)
+{
+ // generate algorithms field
+ char *algnames[256] = { 0 };
+ algnames[DNSSEC_KEY_ALGORITHM_RSA_SHA1] = "rsasha1";
+ algnames[DNSSEC_KEY_ALGORITHM_RSA_SHA1_NSEC3] = "rsasha1nsec3sha1";
+ algnames[DNSSEC_KEY_ALGORITHM_RSA_SHA256] = "rsasha256";
+ algnames[DNSSEC_KEY_ALGORITHM_RSA_SHA512] = "rsasha512";
+ algnames[DNSSEC_KEY_ALGORITHM_ECDSA_P256_SHA256] = "ecdsap256sha256";
+ algnames[DNSSEC_KEY_ALGORITHM_ECDSA_P384_SHA384] = "ecdsap384sha384";
+ algnames[DNSSEC_KEY_ALGORITHM_ED25519] = "ed25519";
+ algnames[DNSSEC_KEY_ALGORITHM_ED448] = "ed448";
+
+ // parse args
+ for (int i = 0; i < argc; i++) {
+ if (!just_timing && strncasecmp(argv[i], "algorithm=", 10) == 0) {
+ int alg = 256; // invalid value
+ (void)str_to_int(argv[i] + 10, &alg, 0, 255);
+ for (int al = 0; al < 256 && alg > 255; al++) {
+ if (algnames[al] != NULL &&
+ strcasecmp(argv[i] + 10, algnames[al]) == 0) {
+ alg = al;
+ }
+ }
+ if (alg > 255) {
+ ERR2("unknown algorithm: %s", argv[i] + 10);
+ return false;
+ }
+ *algorithm = alg;
+ } else if (strncasecmp(argv[i], "ksk=", 4) == 0) {
+ bitmap_set(flags, DNSKEY_GENERATE_KSK, str2bool(argv[i] + 4));
+ } else if (strncasecmp(argv[i], "zsk=", 4) == 0) {
+ bitmap_set(flags, DNSKEY_GENERATE_ZSK, str2bool(argv[i] + 4));
+ } else if (strncasecmp(argv[i], "sep=", 4) == 0) {
+ bitmap_set(flags, DNSKEY_GENERATE_SEP_SPEC, true);
+ bitmap_set(flags, DNSKEY_GENERATE_SEP_ON, str2bool(argv[i] + 4));
+ } else if (!just_timing && strncasecmp(argv[i], "size=", 5) == 0) {
+ if (str_to_u16(argv[i] + 5, keysize) != KNOT_EOK) {
+ ERR2("invalid size: '%s'", argv[i] + 5);
+ return false;
+ }
+ } else if (!just_timing && strncasecmp(argv[i], "addtopolicy=", 12) == 0) {
+ *addtopolicy = argv[i] + 12;
+ } else if (!init_timestamps(argv[i], timing)) {
+ ERR2("invalid parameter: %s", argv[i]);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool _check_lower(knot_time_t a, knot_time_t b,
+ const char *a_name, const char *b_name)
+{
+ if (knot_time_cmp(a, b) > 0) {
+ ERR2("timestamp '%s' must be before '%s'", a_name, b_name);
+ return false;
+ }
+ return true;
+}
+
+#define check_lower(t, a, b) if (!_check_lower(t->a, t->b, #a, #b)) return KNOT_ESEMCHECK
+
+static int check_timers(const knot_kasp_key_timing_t *t)
+{
+ if (t->pre_active != 0) {
+ check_lower(t, pre_active, publish);
+ }
+ check_lower(t, publish, active);
+ check_lower(t, active, retire_active);
+ check_lower(t, active, retire);
+ check_lower(t, active, post_active);
+ if (t->post_active == 0) {
+ check_lower(t, retire, remove);
+ }
+ return KNOT_EOK;
+}
+
+#undef check_lower
+
+// modifies ctx->policy options, so don't do anything afterwards !
+int keymgr_generate_key(kdnssec_ctx_t *ctx, int argc, char *argv[])
+{
+ knot_time_t now = knot_time(), infinity = 0;
+ knot_kasp_key_timing_t gen_timing = { now, infinity, now, infinity, now, infinity, infinity, infinity, infinity };
+ kdnssec_generate_flags_t flags = 0;
+ uint16_t keysize = 0;
+ const char *addtopolicy = NULL;
+ if (!genkeyargs(argc, argv, false, &flags, &ctx->policy->algorithm,
+ &keysize, &gen_timing, &addtopolicy)) {
+ return KNOT_EINVAL;
+ }
+
+ int ret = check_timers(&gen_timing);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if ((flags & DNSKEY_GENERATE_KSK) && gen_timing.ready == infinity) {
+ gen_timing.ready = gen_timing.active;
+ }
+
+ if (keysize == 0) {
+ keysize = dnssec_algorithm_key_size_default(ctx->policy->algorithm);
+ }
+ if ((flags & DNSKEY_GENERATE_KSK)) {
+ ctx->policy->ksk_size = keysize;
+ } else {
+ ctx->policy->zsk_size = keysize;
+ }
+
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *kasp_key = &ctx->zone->keys[i];
+ if ((kasp_key->is_ksk && (flags & DNSKEY_GENERATE_KSK)) &&
+ dnssec_key_get_algorithm(kasp_key->key) != ctx->policy->algorithm) {
+ WARN2("creating key with different algorithm than "
+ "configured in the policy");
+ break;
+ }
+ }
+
+ knot_kasp_key_t *key = NULL;
+ ret = kdnssec_generate_key(ctx, flags, &key);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ key->timing = gen_timing;
+
+ if (addtopolicy != NULL) {
+ char *last_policy_last = NULL;
+
+ knot_dname_t *unused = NULL;
+ ret = kasp_db_get_policy_last(ctx->kasp_db, addtopolicy, &unused,
+ &last_policy_last);
+ knot_dname_free(unused, NULL);
+ if (ret != KNOT_EOK && ret != KNOT_ENOENT) {
+ free(last_policy_last);
+ return ret;
+ }
+
+ ret = kasp_db_set_policy_last(ctx->kasp_db, addtopolicy, last_policy_last,
+ ctx->zone->dname, key->id);
+ free(last_policy_last);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ ret = kdnssec_ctx_commit(ctx);
+
+ if (ret == KNOT_EOK) {
+ printf("%s\n", key->id);
+ }
+
+ return ret;
+}
+
+static void parse_record(zs_scanner_t *scanner)
+{
+ dnssec_key_t *key = scanner->process.data;
+
+ if (dnssec_key_get_dname(key) != NULL ||
+ scanner->r_type != KNOT_RRTYPE_DNSKEY) {
+ scanner->state = ZS_STATE_STOP;
+ return;
+ }
+
+ dnssec_binary_t rdata = {
+ .data = scanner->r_data,
+ .size = scanner->r_data_length
+ };
+ dnssec_key_set_dname(key, scanner->dname);
+ dnssec_key_set_rdata(key, &rdata);
+}
+
+int bind_pubkey_parse(const char *filename, dnssec_key_t **key_ptr)
+{
+ dnssec_key_t *key = NULL;
+ int result = dnssec_key_new(&key);
+ if (result != DNSSEC_EOK) {
+ return KNOT_ENOMEM;
+ }
+
+ uint16_t cls = KNOT_CLASS_IN;
+ uint32_t ttl = 0;
+ zs_scanner_t *scanner = malloc(sizeof(zs_scanner_t));
+ if (scanner == NULL) {
+ dnssec_key_free(key);
+ return KNOT_ENOMEM;
+ }
+
+ if (zs_init(scanner, ".", cls, ttl) != 0 ||
+ zs_set_input_file(scanner, filename) != 0 ||
+ zs_set_processing(scanner, parse_record, NULL, key) != 0 ||
+ zs_parse_all(scanner) != 0) {
+ int ret;
+ switch (scanner->error.code) {
+ case ZS_FILE_OPEN:
+ case ZS_FILE_INVALID:
+ ret = KNOT_EFILE;
+ break;
+ case ZS_FILE_ACCESS:
+ ret = KNOT_EFACCES;
+ break;
+ default:
+ ret = KNOT_EPARSEFAIL;
+ }
+ zs_deinit(scanner);
+ free(scanner);
+ dnssec_key_free(key);
+ return ret;
+ }
+ zs_deinit(scanner);
+ free(scanner);
+
+ if (dnssec_key_get_dname(key) == NULL) {
+ dnssec_key_free(key);
+ return KNOT_INVALID_PUBLIC_KEY;
+ }
+
+ *key_ptr = key;
+ return KNOT_EOK;
+}
+
+static char *gen_keyfilename(const char *orig, const char *wantsuff, const char *altsuff)
+{
+ assert(orig && wantsuff && altsuff);
+
+ const char *dot = strrchr(orig, '.');
+
+ if (dot != NULL && strcmp(dot, wantsuff) == 0) { // Full match.
+ return strdup(orig);
+ } else if (dot != NULL && strcmp(dot, altsuff) == 0) { // Replace suffix.
+ return sprintf_alloc("%.*s%s", (int)(dot - orig), orig, wantsuff);
+ } else { // Add wanted suffix.
+ return sprintf_alloc("%s%s", orig, wantsuff);
+ }
+}
+
+int keymgr_import_bind(kdnssec_ctx_t *ctx, const char *import_file, bool pub_only)
+{
+ if (ctx == NULL || import_file == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ knot_kasp_key_timing_t timing = { 0 };
+ dnssec_key_t *key = NULL;
+ char *keyid = NULL;
+
+ char *pubname = gen_keyfilename(import_file, ".key", ".private");
+ if (pubname == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ int ret = bind_pubkey_parse(pubname, &key);
+ free(pubname);
+ if (ret != KNOT_EOK) {
+ goto fail;
+ }
+
+ if (!pub_only) {
+ bind_privkey_t bpriv = { .time_publish = ctx->now, .time_activate = ctx->now };
+
+ char *privname = gen_keyfilename(import_file, ".private", ".key");
+ if (privname == NULL) {
+ goto fail;
+ }
+
+ ret = bind_privkey_parse(privname, &bpriv);
+ free(privname);
+ if (ret != DNSSEC_EOK) {
+ goto fail;
+ }
+
+ dnssec_binary_t pem = { 0 };
+ ret = bind_privkey_to_pem(key, &bpriv, &pem);
+ if (ret != DNSSEC_EOK) {
+ bind_privkey_free(&bpriv);
+ goto fail;
+ }
+
+ bind_privkey_to_timing(&bpriv, &timing);
+
+ bind_privkey_free(&bpriv);
+
+ ret = dnssec_keystore_import(ctx->keystore, &pem, &keyid);
+ dnssec_binary_free(&pem);
+ if (ret != DNSSEC_EOK) {
+ goto fail;
+ }
+ } else {
+ timing.publish = ctx->now;
+
+ ret = dnssec_key_get_keyid(key, &keyid);
+ if (ret != DNSSEC_EOK) {
+ goto fail;
+ }
+ }
+
+ // allocate kasp key
+ knot_kasp_key_t *kkey = calloc(1, sizeof(*kkey));
+ if (!kkey) {
+ ret = KNOT_ENOMEM;
+ goto fail;
+ }
+
+ kkey->id = keyid;
+ kkey->key = key;
+ kkey->timing = timing;
+ kkey->is_pub_only = pub_only;
+ kkey->is_ksk = (dnssec_key_get_flags(kkey->key) == DNSKEY_FLAGS_KSK);
+ kkey->is_zsk = !kkey->is_ksk;
+
+ // append to zone
+ ret = kasp_zone_append(ctx->zone, kkey);
+ free(kkey);
+ if (ret != KNOT_EOK) {
+ goto fail;
+ }
+ ret = kdnssec_ctx_commit(ctx);
+ if (ret == KNOT_EOK) {
+ printf("%s\n", keyid);
+ return KNOT_EOK;
+ }
+fail:
+ dnssec_key_free(key);
+ free(keyid);
+ return knot_error_from_libdnssec(ret);
+}
+
+static void err_import_key(char *keyid, const char *file)
+{
+ ERR2("failed to get key%s%s from %s%s",
+ *keyid == '\0' ? "" : " ", keyid,
+ *file == '\0' ? "the keystore" : "file ", file);
+}
+
+static int import_key(kdnssec_ctx_t *ctx, unsigned backend, const char *param,
+ int argc, char *argv[])
+{
+ if (ctx == NULL || param == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ // parse params
+ knot_time_t now = knot_time();
+ knot_kasp_key_timing_t timing = { .publish = now, .active = now };
+ kdnssec_generate_flags_t flags = 0;
+ uint16_t keysize = 0;
+ if (!genkeyargs(argc, argv, false, &flags, &ctx->policy->algorithm,
+ &keysize, &timing, NULL)) {
+ return KNOT_EINVAL;
+ }
+
+ int ret = check_timers(&timing);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ normalize_generate_flags(&flags);
+
+ dnssec_key_t *key = NULL;
+ char *keyid = NULL;
+
+ if (backend == KEYSTORE_BACKEND_PEM) {
+ // open file
+ int fd = open(param, O_RDONLY, 0);
+ if (fd == -1) {
+ err_import_key("", param);
+ return knot_map_errno();
+ }
+
+ // determine size
+ off_t fsize = lseek(fd, 0, SEEK_END);
+ if (fsize == -1) {
+ close(fd);
+ err_import_key("", param);
+ return knot_map_errno();
+ }
+ if (lseek(fd, 0, SEEK_SET) == -1) {
+ close(fd);
+ err_import_key("", param);
+ return knot_map_errno();
+ }
+
+ // alloc memory
+ dnssec_binary_t pem = { 0 };
+ ret = dnssec_binary_alloc(&pem, fsize);
+ if (ret != DNSSEC_EOK) {
+ close(fd);
+ err_import_key("", param);
+ goto fail;
+ }
+
+ // read pem
+ ssize_t read_count = read(fd, pem.data, pem.size);
+ close(fd);
+ if (read_count == -1) {
+ dnssec_binary_free(&pem);
+ ret = knot_map_errno();
+ err_import_key("", param);
+ goto fail;
+ }
+
+ // put pem to keystore
+ ret = dnssec_keystore_import(ctx->keystore, &pem, &keyid);
+ dnssec_binary_free(&pem);
+ if (ret != DNSSEC_EOK) {
+ err_import_key(keyid, param);
+ goto fail;
+ }
+ } else {
+ assert(backend == KEYSTORE_BACKEND_PKCS11);
+ keyid = strdup(param);
+ }
+
+ // create dnssec key
+ ret = dnssec_key_new(&key);
+ if (ret != DNSSEC_EOK) {
+ goto fail;
+ }
+ ret = dnssec_key_set_dname(key, ctx->zone->dname);
+ if (ret != DNSSEC_EOK) {
+ goto fail;
+ }
+ dnssec_key_set_flags(key, dnskey_flags(flags & DNSKEY_GENERATE_SEP_ON));
+ dnssec_key_set_algorithm(key, ctx->policy->algorithm);
+
+ // fill key structure from keystore (incl. pubkey from privkey computation)
+ ret = dnssec_keystore_get_private(ctx->keystore, keyid, key);
+ if (ret != DNSSEC_EOK) {
+ err_import_key(keyid, "");
+ goto fail;
+ }
+
+ // allocate kasp key
+ knot_kasp_key_t *kkey = calloc(1, sizeof(*kkey));
+ if (kkey == NULL) {
+ ret = KNOT_ENOMEM;
+ goto fail;
+ }
+ kkey->id = keyid;
+ kkey->key = key;
+ kkey->timing = timing;
+ kkey->is_ksk = (flags & DNSKEY_GENERATE_KSK);
+ kkey->is_zsk = (flags & DNSKEY_GENERATE_ZSK);
+
+ // append to zone
+ ret = kasp_zone_append(ctx->zone, kkey);
+ free(kkey);
+ if (ret != KNOT_EOK) {
+ goto fail;
+ }
+ ret = kdnssec_ctx_commit(ctx);
+ if (ret == KNOT_EOK) {
+ printf("%s\n", keyid);
+ return KNOT_EOK;
+ }
+fail:
+ dnssec_key_free(key);
+ free(keyid);
+ return knot_error_from_libdnssec(ret);
+}
+
+int keymgr_import_pem(kdnssec_ctx_t *ctx, const char *import_file, int argc, char *argv[])
+{
+ return import_key(ctx, KEYSTORE_BACKEND_PEM, import_file, argc, argv);
+}
+
+int keymgr_import_pkcs11(kdnssec_ctx_t *ctx, char *key_id, int argc, char *argv[])
+{
+ if (!dnssec_keyid_is_valid(key_id)) {
+ return DNSSEC_INVALID_KEY_ID;
+ }
+
+ if (ctx->keystore_type != KEYSTORE_BACKEND_PKCS11) {
+ knot_dname_txt_storage_t dname_str;
+ (void)knot_dname_to_str(dname_str, ctx->zone->dname, sizeof(dname_str));
+ ERR2("not a PKCS #11 keystore for zone %s", dname_str);
+ return KNOT_ERROR;
+ }
+
+ dnssec_keyid_normalize(key_id);
+ return import_key(ctx, KEYSTORE_BACKEND_PKCS11, key_id, argc, argv);
+}
+
+int keymgr_nsec3_salt_print(kdnssec_ctx_t *ctx)
+{
+ dnssec_binary_t salt_bin;
+ knot_time_t created;
+ int ret = kasp_db_load_nsec3salt(ctx->kasp_db, ctx->zone->dname,
+ &salt_bin, &created);
+ switch (ret) {
+ case KNOT_EOK:
+ printf("Current salt: ");
+ if (salt_bin.size == 0) {
+ printf("-");
+ }
+ for (size_t i = 0; i < salt_bin.size; i++) {
+ printf("%02X", (unsigned)salt_bin.data[i]);
+ }
+ printf("\n");
+ free(salt_bin.data);
+ break;
+ case KNOT_ENOENT:
+ printf("-- no salt --\n");
+ ret = KNOT_EOK;
+ break;
+ }
+ return ret;
+}
+
+int keymgr_nsec3_salt_set(kdnssec_ctx_t *ctx, const char *new_salt)
+{
+ assert(new_salt);
+
+ dnssec_binary_t salt_bin = { 0 };
+ if (strcmp(new_salt, "-") != 0) {
+ salt_bin.data = hex_to_bin(new_salt, &salt_bin.size);
+ if (salt_bin.data == NULL) {
+ return KNOT_EMALF;
+ }
+ }
+ if (salt_bin.size != ctx->policy->nsec3_salt_length) {
+ WARN2("specified salt doesn't match configured salt length (%d)",
+ (int)ctx->policy->nsec3_salt_length);
+ }
+ int ret = kasp_db_store_nsec3salt(ctx->kasp_db, ctx->zone->dname,
+ &salt_bin, knot_time());
+ if (salt_bin.size > 0) {
+ free(salt_bin.data);
+ }
+ return ret;
+}
+
+int keymgr_serial_print(kdnssec_ctx_t *ctx, kaspdb_serial_t type)
+{
+ uint32_t serial = 0;
+ int ret = kasp_db_load_serial(ctx->kasp_db, ctx->zone->dname,
+ type, &serial);
+ switch (ret) {
+ case KNOT_EOK:
+ printf("Current serial: %u\n", serial);
+ break;
+ case KNOT_ENOENT:
+ printf("-- no serial --\n");
+ ret = KNOT_EOK;
+ break;
+ }
+ return ret;
+}
+
+int keymgr_serial_set(kdnssec_ctx_t *ctx, kaspdb_serial_t type, uint32_t new_serial)
+{
+ return kasp_db_store_serial(ctx->kasp_db, ctx->zone->dname,
+ type, new_serial);
+}
+
+static void print_tsig(dnssec_tsig_algorithm_t mac, const char *name,
+ const dnssec_binary_t *secret)
+{
+ assert(name);
+ assert(secret);
+
+ const char *mac_name = dnssec_tsig_algorithm_to_name(mac);
+ assert(mac_name);
+
+ // client format (as a comment)
+ printf("# %s:%s:%.*s\n", mac_name, name, (int)secret->size, secret->data);
+
+ // server format
+ printf("key:\n");
+ printf(" - id: %s\n", name);
+ printf(" algorithm: %s\n", mac_name);
+ printf(" secret: %.*s\n", (int)secret->size, secret->data);
+}
+
+int keymgr_generate_tsig(const char *tsig_name, const char *alg_name, int bits)
+{
+ dnssec_tsig_algorithm_t alg = dnssec_tsig_algorithm_from_name(alg_name);
+ if (alg == DNSSEC_TSIG_UNKNOWN) {
+ return KNOT_INVALID_KEY_ALGORITHM;
+ }
+
+ int optimal_bits = dnssec_tsig_optimal_key_size(alg);
+ if (bits == 0) {
+ bits = optimal_bits;
+ }
+
+ // round up bits to bytes
+ bits = (bits + CHAR_BIT - 1) / CHAR_BIT * CHAR_BIT;
+
+ if (bits < optimal_bits) {
+ WARN2("optimal key size for %s is at least %d bits",
+ dnssec_tsig_algorithm_to_name(alg), optimal_bits);
+ }
+ assert(bits % CHAR_BIT == 0);
+
+ _cleanup_binary_ dnssec_binary_t key = { 0 };
+ int r = dnssec_binary_alloc(&key, bits / CHAR_BIT);
+ if (r != DNSSEC_EOK) {
+ ERR2("failed to allocate memory");
+ return knot_error_from_libdnssec(r);
+ }
+
+ r = gnutls_rnd(GNUTLS_RND_KEY, key.data, key.size);
+ if (r != 0) {
+ ERR2("failed to generate secret the key");
+ return knot_error_from_libdnssec(r);
+ }
+
+ _cleanup_binary_ dnssec_binary_t key_b64 = { 0 };
+ r = dnssec_binary_to_base64(&key, &key_b64);
+ if (r != DNSSEC_EOK) {
+ ERR2("failed to convert the key to Base64");
+ return knot_error_from_libdnssec(r);
+ }
+
+ print_tsig(alg, tsig_name, &key_b64);
+
+ return KNOT_EOK;
+}
+
+static bool is_hex(const char *string)
+{
+ for (const char *p = string; *p != '\0'; p++) {
+ if (!is_xdigit(*p)) {
+ return false;
+ }
+ }
+ return (*string != '\0');
+}
+
+int keymgr_get_key(kdnssec_ctx_t *ctx, const char *key_spec, knot_kasp_key_t **key)
+{
+ // Check if type of key spec is prescribed.
+ bool is_keytag = false, is_id = false;
+ if (strncasecmp(key_spec, "keytag=", 7) == 0) {
+ key_spec += 7;
+ is_keytag = true;
+ } else if (strncasecmp(key_spec, "id=", 3) == 0) {
+ key_spec += 3;
+ is_id = true;
+ }
+
+ uint16_t keytag = 0;
+ bool can_be_keytag = (str_to_u16(key_spec, &keytag) == KNOT_EOK);
+ long spec_len = strlen(key_spec);
+
+ // Check if input is a valid key spec.
+ if ((is_keytag && !can_be_keytag) ||
+ (is_id && !is_hex(key_spec)) ||
+ (!can_be_keytag && !is_hex(key_spec))) {
+ ERR2("invalid key specification");
+ return KNOT_EINVAL;
+ }
+
+ *key = NULL;
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *candidate = &ctx->zone->keys[i];
+
+ bool keyid_match = strncmp(candidate->id, key_spec, spec_len) == 0; // May be just a prefix.
+ bool keytag_match = can_be_keytag &&
+ dnssec_key_get_keytag(candidate->key) == keytag;
+
+ // Terminate if found exact key ID match.
+ if (keyid_match && !is_keytag && strlen(candidate->id) == spec_len) {
+ *key = candidate;
+ break;
+ }
+ // Check for key ID prefix or tag match.
+ if ((is_keytag && keytag_match) || // Tag is prescribed.
+ (is_id && keyid_match) || // Key ID is prescribed.
+ ((!is_keytag && !is_id) && (keyid_match || keytag_match))) { // Nothing is prescribed.
+ if (*key == NULL) {
+ *key = candidate;
+ } else {
+ ERR2("key not specified uniquely, please use id=Full_Key_ID");
+ return KNOT_EINVAL;
+ }
+ }
+ }
+ if (*key == NULL) {
+ ERR2("key not found");
+ return KNOT_ENOENT;
+ }
+ return KNOT_EOK;
+}
+
+int keymgr_foreign_key_id(char *argv[], knot_lmdb_db_t *kaspdb, knot_dname_t **key_zone, char **key_id)
+{
+ *key_zone = knot_dname_from_str_alloc(argv[3]);
+ if (*key_zone == NULL) {
+ return KNOT_ENOMEM;
+ }
+ knot_dname_to_lower(*key_zone);
+
+ kdnssec_ctx_t kctx = { 0 };
+ int ret = kdnssec_ctx_init(conf(), &kctx, *key_zone, kaspdb, NULL);
+ if (ret != KNOT_EOK) {
+ ERR2("failed to initialize zone %s (%s)", argv[0], knot_strerror(ret));
+ free(*key_zone);
+ *key_zone = NULL;
+ return KNOT_ENOZONE;
+ }
+ knot_kasp_key_t *key;
+ ret = keymgr_get_key(&kctx, argv[2], &key);
+ if (ret == KNOT_EOK) {
+ *key_id = strdup(key->id);
+ if (*key_id == NULL) {
+ ret = KNOT_ENOMEM;
+ }
+ }
+ kdnssec_ctx_deinit(&kctx);
+ return ret;
+}
+
+int keymgr_set_timing(knot_kasp_key_t *key, int argc, char *argv[])
+{
+ knot_kasp_key_timing_t temp = key->timing;
+ kdnssec_generate_flags_t flags = ((key->is_ksk ? DNSKEY_GENERATE_KSK : 0) | (key->is_zsk ? DNSKEY_GENERATE_ZSK : 0));
+
+ if (genkeyargs(argc, argv, true, &flags, NULL, NULL, &temp, NULL)) {
+ int ret = check_timers(&temp);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ key->timing = temp;
+ if (key->is_ksk != (bool)(flags & DNSKEY_GENERATE_KSK) ||
+ key->is_zsk != (bool)(flags & DNSKEY_GENERATE_ZSK) ||
+ flags & DNSKEY_GENERATE_SEP_SPEC) {
+ normalize_generate_flags(&flags);
+ key->is_ksk = (flags & DNSKEY_GENERATE_KSK);
+ key->is_zsk = (flags & DNSKEY_GENERATE_ZSK);
+ return dnssec_key_set_flags(key->key, dnskey_flags(flags & DNSKEY_GENERATE_SEP_ON));
+ }
+ return KNOT_EOK;
+ }
+ return KNOT_EINVAL;
+}
+
+typedef struct {
+ const char *name;
+ size_t offset;
+} timer_ctx_t;
+
+static const timer_ctx_t timers[] = {
+ { "created", offsetof(knot_kasp_key_timing_t, created) },
+ { "pre-active", offsetof(knot_kasp_key_timing_t, pre_active) },
+ { "publish", offsetof(knot_kasp_key_timing_t, publish) },
+ { "ready", offsetof(knot_kasp_key_timing_t, ready) },
+ { "active", offsetof(knot_kasp_key_timing_t, active) },
+ { "retire-active", offsetof(knot_kasp_key_timing_t, retire_active) },
+ { "retire", offsetof(knot_kasp_key_timing_t, retire) },
+ { "post-active", offsetof(knot_kasp_key_timing_t, post_active) },
+ { "revoke", offsetof(knot_kasp_key_timing_t, revoke) },
+ { "remove", offsetof(knot_kasp_key_timing_t, remove) },
+ { NULL }
+};
+
+static void print_key_brief(const knot_kasp_key_t *key, keymgr_list_params_t *params)
+{
+ const bool c = params->color;
+
+ printf("%s %s%5u%s ",
+ key->id, COL_BOLD(c), dnssec_key_get_keytag(key->key), COL_RST(c));
+
+ printf("%s%s%s%s ",
+ COL_BOLD(c),
+ (key->is_ksk ? (key->is_zsk ? COL_YELW(c) : COL_RED(c)) : COL_GRN(c)),
+ (key->is_ksk ? (key->is_zsk ? "CSK" : "KSK") : "ZSK"),
+ COL_RST(c));
+
+ uint8_t alg = dnssec_key_get_algorithm(key->key);
+ const knot_lookup_t *alg_info = knot_lookup_by_id(knot_dnssec_alg_names, alg);
+ if (alg_info != NULL) {
+ printf("%s", alg_info->name);
+ if (alg <= DNSSEC_KEY_ALGORITHM_RSA_SHA512) {
+ printf("%s/%u%s", COL_DIM(c), dnssec_key_get_size(key->key), COL_RST(c));
+ }
+ } else {
+ printf("ALGORITHM_%u", alg);
+ }
+
+ if (key->is_pub_only) {
+ printf(" %s%spublic-only%s", COL_BOLD(c), COL_MGNT(c), COL_RST(c));
+ }
+
+ static char buf[100];
+ knot_time_t now = knot_time();
+ for (const timer_ctx_t *t = &timers[0]; t->name != NULL; t++) {
+ knot_time_t *val = (void *)(&key->timing) + t->offset;
+ if (*val == 0) {
+ continue;
+ }
+ bool past = (knot_time_cmp(*val, now) <= 0);
+ const char *UNDR = past ? COL_UNDR(c) : "";
+ const char *BOLD = past ? "" : COL_BOLD(c);
+ for (const timer_ctx_t *t2 = t + 1; past && t2->name != NULL; t2++) {
+ knot_time_t *val2 = (void *)(&key->timing) + t2->offset;
+ if (knot_time_cmp(*val2, now) <= 0) {
+ UNDR = "";
+ break;
+ }
+ }
+ (void)knot_time_print(params->format, *val, buf, sizeof(buf));
+ printf(" %s%s%s=%s%s%s", UNDR, t->name, COL_RST(c), BOLD, buf, COL_RST(c));
+ }
+ printf("\n");
+}
+
+static void print_key_full(const knot_kasp_key_t *key, knot_time_print_t format)
+{
+ printf("%s ksk=%s zsk=%s tag=%05d algorithm=%-2d size=%-4u public-only=%s", key->id,
+ (key->is_ksk ? "yes" : "no "), (key->is_zsk ? "yes" : "no "),
+ dnssec_key_get_keytag(key->key), (int)dnssec_key_get_algorithm(key->key),
+ dnssec_key_get_size(key->key), (key->is_pub_only ? "yes" : "no "));
+
+ static char buf[100];
+ for (const timer_ctx_t *t = &timers[0]; t->name != NULL; t++) {
+ knot_time_t *val = (void *)(&key->timing) + t->offset;
+ (void)knot_time_print(format, *val, buf, sizeof(buf));
+ printf(" %s=%s", t->name, buf);
+ }
+ printf("\n");
+}
+
+static void print_key_json(const knot_kasp_key_t *key, knot_time_print_t format,
+ jsonw_t *w, const char *zone_name)
+{
+ jsonw_str(w, "zone", zone_name);
+ jsonw_str(w, "id", key->id);
+ jsonw_bool(w, "ksk", key->is_ksk);
+ jsonw_bool(w, "zsk", key->is_zsk);
+ jsonw_int(w, "tag", dnssec_key_get_keytag(key->key));
+ jsonw_ulong(w, "algorithm", dnssec_key_get_algorithm(key->key));
+ jsonw_int(w, "size", dnssec_key_get_size(key->key));
+ jsonw_bool(w, "public-only", key->is_pub_only);
+
+ static char buf[100];
+ for (const timer_ctx_t *t = &timers[0]; t->name != NULL; t++) {
+ knot_time_t *val = (void *)(&key->timing) + t->offset;
+ (void)knot_time_print(format, *val, buf, sizeof(buf));
+
+ if (format == TIME_PRINT_UNIX) {
+ jsonw_int(w, t->name, *val);
+ } else {
+ jsonw_str(w, t->name, buf);
+ }
+ }
+}
+
+typedef struct {
+ knot_time_t val;
+ const knot_kasp_key_t *key;
+} key_sort_item_t;
+
+static int key_sort(const void *a, const void *b)
+{
+ const key_sort_item_t *key_a = a;
+ const key_sort_item_t *key_b = b;
+ return knot_time_cmp(key_a->val, key_b->val);
+}
+
+int keymgr_list_keys(kdnssec_ctx_t *ctx, keymgr_list_params_t *params)
+{
+ if (ctx->zone->num_keys == 0) {
+ return KNOT_EOK;
+ }
+
+ if (params->extended) {
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *key = &ctx->zone->keys[i];
+ print_key_full(key, params->format);
+ }
+ } else if (params->json) {
+ jsonw_t *w = jsonw_new(stdout, " ");
+ if (w == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ knot_dname_txt_storage_t name;
+ (void)knot_dname_to_str(name, ctx->zone->dname, sizeof(name));
+
+ jsonw_list(w, NULL);
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *key = &ctx->zone->keys[i];
+ jsonw_object(w, NULL);
+ print_key_json(key, params->format, w, name);
+ jsonw_end(w); // object
+ }
+ jsonw_end(w); // list
+ jsonw_free(&w);
+ } else {
+ key_sort_item_t items[ctx->zone->num_keys];
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *key = &ctx->zone->keys[i];
+ items[i].key = key;
+ if (knot_time_cmp(key->timing.pre_active, key->timing.publish) < 0) {
+ items[i].val = key->timing.pre_active;
+ } else {
+ items[i].val = key->timing.publish;
+ }
+ }
+ qsort(&items, ctx->zone->num_keys, sizeof(items[0]), key_sort);
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ print_key_brief(items[i].key, params);
+ }
+ }
+ return KNOT_EOK;
+}
+
+static int print_ds(const knot_dname_t *dname, const dnssec_binary_t *rdata)
+{
+ wire_ctx_t ctx = wire_ctx_init(rdata->data, rdata->size);
+ if (wire_ctx_available(&ctx) < 4) {
+ return KNOT_EMALF;
+ }
+
+ char *name = knot_dname_to_str_alloc(dname);
+ if (!name) {
+ return KNOT_ENOMEM;
+ }
+
+ uint16_t keytag = wire_ctx_read_u16(&ctx);
+ uint8_t algorithm = wire_ctx_read_u8(&ctx);
+ uint8_t digest_type = wire_ctx_read_u8(&ctx);
+
+ size_t digest_size = wire_ctx_available(&ctx);
+
+ printf("%s DS %d %d %d ", name, keytag, algorithm, digest_type);
+ for (size_t i = 0; i < digest_size; i++) {
+ printf("%02x", ctx.position[i]);
+ }
+ printf("\n");
+
+ free(name);
+ return KNOT_EOK;
+}
+
+static int create_and_print_ds(const knot_dname_t *zone_name,
+ const dnssec_key_t *key, dnssec_key_digest_t digest)
+{
+ _cleanup_binary_ dnssec_binary_t rdata = { 0 };
+ int r = dnssec_key_create_ds(key, digest, &rdata);
+ if (r != DNSSEC_EOK) {
+ return knot_error_from_libdnssec(r);
+ }
+
+ return print_ds(zone_name, &rdata);
+}
+
+int keymgr_generate_ds(const knot_dname_t *dname, const knot_kasp_key_t *key)
+{
+ static const dnssec_key_digest_t digests[] = {
+ DNSSEC_KEY_DIGEST_SHA256,
+ DNSSEC_KEY_DIGEST_SHA384,
+ 0
+ };
+
+ int ret = KNOT_EOK;
+ for (int i = 0; digests[i] != 0 && ret == KNOT_EOK; i++) {
+ ret = create_and_print_ds(dname, key->key, digests[i]);
+ }
+
+ return ret;
+}
+
+int keymgr_generate_dnskey(const knot_dname_t *dname, const knot_kasp_key_t *key)
+{
+ const dnssec_key_t *dnskey = key->key;
+
+ char *name = knot_dname_to_str_alloc(dname);
+ if (!name) {
+ return KNOT_ENOMEM;
+ }
+
+ uint16_t flags = dnssec_key_get_flags(dnskey);
+ uint8_t algorithm = dnssec_key_get_algorithm(dnskey);
+
+ dnssec_binary_t pubkey = { 0 };
+ int ret = dnssec_key_get_pubkey(dnskey, &pubkey);
+ if (ret != DNSSEC_EOK) {
+ free(name);
+ return knot_error_from_libdnssec(ret);
+ }
+
+ uint8_t *base64_output = NULL;
+ int len = knot_base64_encode_alloc(pubkey.data, pubkey.size, &base64_output);
+ if (len < 0) {
+ free(name);
+ return len;
+ }
+
+ printf("%s DNSKEY %u 3 %u %.*s\n", name, flags, algorithm, len, base64_output);
+
+ free(base64_output);
+ free(name);
+ return KNOT_EOK;
+}
+
+int keymgr_list_zones(knot_lmdb_db_t *kaspdb, bool json)
+{
+ jsonw_t *w;
+ list_t zones;
+ init_list(&zones);
+ int ret = kasp_db_list_zones(kaspdb, &zones);
+ if (ret != KNOT_EOK) {
+ ERR2("failed to initialize KASP (%s)", knot_strerror(ret));
+ return ret;
+ }
+
+ knot_dname_txt_storage_t name;
+ ptrnode_t *node;
+
+ if (json) {
+ w = jsonw_new(stdout, " ");
+ if (w == NULL) {
+ ERR2("failed to allocate memory");
+ ptrlist_deep_free(&zones, NULL);
+ return KNOT_ENOMEM;
+ }
+ jsonw_list(w, NULL);
+ }
+ WALK_LIST(node, zones) {
+ (void)knot_dname_to_str(name, node->d, sizeof(name));
+ if (json) {
+ jsonw_str(w, NULL, name);
+ } else {
+ printf("%s\n", name);
+ }
+ }
+ if (json) {
+ jsonw_end(w); // list
+ jsonw_free(&w);
+ }
+
+ ptrlist_deep_free(&zones, NULL);
+ return KNOT_EOK;
+}
diff --git a/src/utils/keymgr/functions.h b/src/utils/keymgr/functions.h
new file mode 100644
index 0000000..9c16d80
--- /dev/null
+++ b/src/utils/keymgr/functions.h
@@ -0,0 +1,62 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdio.h>
+
+#include "knot/dnssec/context.h"
+
+typedef struct {
+ knot_time_print_t format;
+ bool extended;
+ bool color;
+ bool json;
+} keymgr_list_params_t;
+
+int parse_timestamp(char *arg, knot_time_t *stamp);
+
+int keymgr_generate_key(kdnssec_ctx_t *ctx, int argc, char *argv[]);
+
+int keymgr_import_bind(kdnssec_ctx_t *ctx, const char *import_file, bool pub_only);
+
+int keymgr_import_pem(kdnssec_ctx_t *ctx, const char *import_file, int argc, char *argv[]);
+
+int keymgr_import_pkcs11(kdnssec_ctx_t *ctx, char *key_id, int argc, char *argv[]);
+
+int keymgr_nsec3_salt_print(kdnssec_ctx_t *ctx);
+
+int keymgr_nsec3_salt_set(kdnssec_ctx_t *ctx, const char *new_salt);
+
+int keymgr_serial_print(kdnssec_ctx_t *ctx, kaspdb_serial_t type);
+
+int keymgr_serial_set(kdnssec_ctx_t *ctx, kaspdb_serial_t type, uint32_t new_serial);
+
+int keymgr_generate_tsig(const char *tsig_name, const char *alg_name, int bits);
+
+int keymgr_get_key(kdnssec_ctx_t *ctx, const char *key_spec, knot_kasp_key_t **key);
+
+int keymgr_foreign_key_id(char *argv[], knot_lmdb_db_t *kaspdb, knot_dname_t **key_zone, char **key_id);
+
+int keymgr_set_timing(knot_kasp_key_t *key, int argc, char *argv[]);
+
+int keymgr_list_keys(kdnssec_ctx_t *ctx, keymgr_list_params_t *params);
+
+int keymgr_generate_ds(const knot_dname_t *dname, const knot_kasp_key_t *key);
+
+int keymgr_generate_dnskey(const knot_dname_t *dname, const knot_kasp_key_t *key);
+
+int keymgr_list_zones(knot_lmdb_db_t *kaspdb, bool json);
diff --git a/src/utils/keymgr/main.c b/src/utils/keymgr/main.c
new file mode 100644
index 0000000..355fd3a
--- /dev/null
+++ b/src/utils/keymgr/main.c
@@ -0,0 +1,416 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <getopt.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "contrib/strtonum.h"
+#include "knot/dnssec/zone-keys.h"
+#include "libknot/libknot.h"
+#include "utils/common/msg.h"
+#include "utils/common/params.h"
+#include "utils/common/signal.h"
+#include "utils/common/util_conf.h"
+#include "utils/keymgr/functions.h"
+#include "utils/keymgr/offline_ksk.h"
+
+#define PROGRAM_NAME "keymgr"
+
+signal_ctx_t signal_ctx = { 0 }; // global, needed by signal handler
+
+static void print_help(void)
+{
+ printf("Usage:\n"
+ " %s [-c | -C | -D <path>] [options] <zone_name> <command>\n"
+ " %s [-c | -C | -D <path>] [-j] -l\n"
+ " %s -t <tsig_name> [<algorithm> [<bits>]]\n"
+ "\n"
+ "Config options:\n"
+ " -c, --config <file> Path to a textual configuration file.\n"
+ " (default %s)\n"
+ " -C, --confdb <dir> Path to a configuration database directory.\n"
+ " (default %s)\n"
+ " -D, --dir <path> Path to a KASP database directory, use default configuration.\n"
+ "\n"
+ "Options:\n"
+ " -t, --tsig <name> [alg] Generate a TSIG key.\n"
+ " -e, --extended Extended output (listing of keys with full description).\n"
+ " -j, --json Print the zones or keys in JSON format.\n"
+ " -l, --list List all zones that have at least one key in KASP database.\n"
+ " -x, --mono Don't color the output.\n"
+ " -X, --color Force output colorization in the normal mode.\n"
+ " -h, --help Print the program help.\n"
+ " -V, --version Print the program version.\n"
+ "\n"
+ "Commands:\n"
+ " list List all zone's DNSSEC keys.\n"
+ " generate Generate new DNSSEC key.\n"
+ " (syntax: generate <attribute_name>=<value>...)\n"
+ " import-bind Import BIND-style key file pair (.key + .private).\n"
+ " (syntax: import-bind <key_file_name>)\n"
+ " import-pub Import public-only key to be published in the zone (in BIND .key format).\n"
+ " (syntax: import-pub <key_file_name>)\n"
+ " import-pem Import key in PEM format. Specify its parameters manually.\n"
+ " (syntax: import-pem <pem_file_path> <attribute_name>=<value>...)\n"
+ " import-pkcs11 Import key stored in PKCS11 storage. Specify its parameters manually.\n"
+ " (syntax: import-pkcs11 <key_id> <attribute_name>=<value>...)\n"
+ " nsec3-salt Print current NSEC3 salt. If a parameter is specified, set new salt.\n"
+ " (syntax: nsec3-salt [<new_salt>])\n"
+ " local-serial Print SOA serial stored in KASP database when using on-slave signing.\n"
+ " If a parameter is specified, set new serial.\n"
+ " (syntax: local-serial <new_serial>)\n"
+ " master-serial Print SOA serial of the remote master stored in KASP database when using on-slave signing.\n"
+ " If a parameter is specified, set new master serial.\n"
+ " (syntax: master-serial <new_serial>)\n"
+ " ds Generate DS record(s) for specified key.\n"
+ " (syntax: ds <key_spec>)\n"
+ " dnskey Generate DNSKEY record for specified key.\n"
+ " (syntax: dnskey <key_spec>)\n"
+ " share Share an existing key of another zone with the specified zone.\n"
+ " (syntax: share <full_key_ID> <zone2share_from>\n"
+ " delete Remove the specified key from zone.\n"
+ " (syntax: delete <key_spec>)\n"
+ " set Set existing key's timing attribute.\n"
+ " (syntax: set <key_spec> <attribute_name>=<value>...)\n"
+ "\n"
+ "Commands related to Offline KSK feature:\n"
+ " pregenerate Pre-generate ZSKs for later rollovers with offline KSK.\n"
+ " (syntax: pregenerate [<from>] <to>)\n"
+ " show-offline Print pre-generated offline key-related records for specified time interval (possibly to infinity).\n"
+ " (syntax: show-offline [<from>] [<to>])\n"
+ " del-offline Delete pre-generated offline key-related records in specified time interval.\n"
+ " (syntax: del-offline <from> <to>)\n"
+ " del-all-old Delete old keys that are in state 'removed'.\n"
+ " generate-ksr Print to stdout KeySigningRequest based on pre-generated ZSKS.\n"
+ " (syntax: generate-ksr [<from>] <to>)\n"
+ " sign-ksr Read KeySigningRequest from a file, sign it and print SignedKeyResponse to stdout.\n"
+ " (syntax: sign-ksr <ksr_file>)\n"
+ " validate-skr Validate RRSIGs in a SignedKeyResponse (if not corrupt).\n"
+ " (syntax: validate-skr <skr_file>)\n"
+ " import-skr Import DNSKEY record signatures from a SignedKeyResponse.\n"
+ " (syntax: import-skr <skr_file>)\n"
+ "\n"
+ "Key specification:\n"
+ " either the key tag (number) or [a prefix of] key ID, with an optional\n"
+ " [id=|keytag=] prefix.\n"
+ "\n"
+ "Key attributes:\n"
+ " algorithm The key cryptographic algorithm: either name (e.g. RSASHA256) or\n"
+ " number.\n"
+ " size The key size in bits.\n"
+ " ksk Whether the generated/imported key shall be Key Signing Key.\n"
+ " created/publish/ready/active/retire/remove The timestamp of the key\n"
+ " lifetime event (e.g. published=+1d active=1499770874)\n",
+ PROGRAM_NAME, PROGRAM_NAME, PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR);
+}
+
+static int key_command(int argc, char *argv[], int opt_ind, knot_lmdb_db_t *kaspdb,
+ keymgr_list_params_t *list_params)
+{
+ if (argc < opt_ind + 2) {
+ ERR2("zone name or command not specified");
+ print_help();
+ return KNOT_EINVAL;
+ }
+ argc -= opt_ind;
+ argv += opt_ind;
+
+ knot_dname_t *zone_name = knot_dname_from_str_alloc(argv[0]);
+ if (zone_name == NULL) {
+ return KNOT_ENOMEM;
+ }
+ knot_dname_to_lower(zone_name);
+
+ kdnssec_ctx_t kctx = { 0 };
+
+ int ret = kdnssec_ctx_init(conf(), &kctx, zone_name, kaspdb, NULL);
+ if (ret != KNOT_EOK) {
+ ERR2("failed to initialize KASP (%s)", knot_strerror(ret));
+ goto main_end;
+ }
+
+#define CHECK_MISSING_ARG(msg) \
+ if (argc < 3) { \
+ ERR2("%s", (msg)); \
+ ret = KNOT_EINVAL; \
+ goto main_end; \
+ }
+
+#define CHECK_MISSING_ARG2(msg) \
+ if (argc < 4) { \
+ ERR2("%s", (msg)); \
+ ret = KNOT_EINVAL; \
+ goto main_end; \
+ }
+
+ bool print_ok_on_succes = true;
+ if (strcmp(argv[1], "generate") == 0) {
+ ret = keymgr_generate_key(&kctx, argc - 2, argv + 2);
+ print_ok_on_succes = false;
+ } else if (strcmp(argv[1], "import-bind") == 0) {
+ CHECK_MISSING_ARG("BIND-style key to import not specified");
+ ret = keymgr_import_bind(&kctx, argv[2], false);
+ } else if (strcmp(argv[1], "import-pub") == 0) {
+ CHECK_MISSING_ARG("BIND-style key to import not specified");
+ ret = keymgr_import_bind(&kctx, argv[2], true);
+ } else if (strcmp(argv[1], "import-pem") == 0) {
+ CHECK_MISSING_ARG("PEM file to import not specified");
+ ret = keymgr_import_pem(&kctx, argv[2], argc - 3, argv + 3);
+ } else if (strcmp(argv[1], "import-pkcs11") == 0) {
+ CHECK_MISSING_ARG("Key ID to import not specified");
+ ret = keymgr_import_pkcs11(&kctx, argv[2], argc - 3, argv + 3);
+ } else if (strcmp(argv[1], "nsec3-salt") == 0) {
+ if (argc > 2) {
+ ret = keymgr_nsec3_salt_set(&kctx, argv[2]);
+ } else {
+ ret = keymgr_nsec3_salt_print(&kctx);
+ print_ok_on_succes = false;
+ }
+ } else if (strcmp(argv[1], "local-serial") == 0 || strcmp(argv[1], "master-serial") == 0 ) {
+ kaspdb_serial_t type = (argv[1][0] == 'm' ? KASPDB_SERIAL_MASTER : KASPDB_SERIAL_LASTSIGNED);
+ if (argc > 2) {
+ uint32_t new_serial = 0;
+ if ((ret = str_to_u32(argv[2], &new_serial)) == KNOT_EOK) {
+ ret = keymgr_serial_set(&kctx, type, new_serial);
+ }
+ } else {
+ ret = keymgr_serial_print(&kctx, type);
+ print_ok_on_succes = false;
+ }
+ } else if (strcmp(argv[1], "set") == 0) {
+ CHECK_MISSING_ARG("Key is not specified");
+ knot_kasp_key_t *key2set;
+ ret = keymgr_get_key(&kctx, argv[2], &key2set);
+ if (ret == KNOT_EOK) {
+ ret = keymgr_set_timing(key2set, argc - 3, argv + 3);
+ if (ret == KNOT_EOK) {
+ ret = kdnssec_ctx_commit(&kctx);
+ }
+ }
+ } else if (strcmp(argv[1], "list") == 0) {
+ list_params->format = TIME_PRINT_UNIX;
+ if (argc > 2 && strcmp(argv[2], "human") == 0) {
+ list_params->format = TIME_PRINT_HUMAN_MIXED;
+ } else if (argc > 2 && strcmp(argv[2], "iso") == 0) {
+ list_params->format = TIME_PRINT_ISO8601;
+ }
+ ret = keymgr_list_keys(&kctx, list_params);
+ print_ok_on_succes = false;
+ } else if (strcmp(argv[1], "ds") == 0 || strcmp(argv[1], "dnskey") == 0) {
+ int (*generate_rr)(const knot_dname_t *, const knot_kasp_key_t *) = keymgr_generate_dnskey;
+ if (strcmp(argv[1], "ds") == 0) {
+ generate_rr = keymgr_generate_ds;
+ }
+ if (argc < 3) {
+ for (int i = 0; i < kctx.zone->num_keys && ret == KNOT_EOK; i++) {
+ if (kctx.zone->keys[i].is_ksk) {
+ ret = generate_rr(zone_name, &kctx.zone->keys[i]);
+ }
+ }
+ } else {
+ knot_kasp_key_t *key2rr;
+ ret = keymgr_get_key(&kctx, argv[2], &key2rr);
+ if (ret == KNOT_EOK) {
+ ret = generate_rr(zone_name, key2rr);
+ }
+ }
+ print_ok_on_succes = false;
+ } else if (strcmp(argv[1], "share") == 0) {
+ CHECK_MISSING_ARG("Key to be shared is not specified");
+ CHECK_MISSING_ARG2("Zone to be shared from not specified");
+ knot_dname_t *other_zone = NULL;
+ char *key_to_share = NULL;
+ ret = keymgr_foreign_key_id(argv, kaspdb, &other_zone, &key_to_share);
+ if (ret == KNOT_EOK) {
+ ret = kasp_db_share_key(kctx.kasp_db, other_zone, kctx.zone->dname, key_to_share);
+ }
+ free(other_zone);
+ free(key_to_share);
+ } else if (strcmp(argv[1], "delete") == 0) {
+ CHECK_MISSING_ARG("Key is not specified");
+ knot_kasp_key_t *key2del;
+ ret = keymgr_get_key(&kctx, argv[2], &key2del);
+ if (ret == KNOT_EOK) {
+ ret = kdnssec_delete_key(&kctx, key2del);
+ }
+ } else if (strcmp(argv[1], "pregenerate") == 0) {
+ CHECK_MISSING_ARG("Timestamp to not specified");
+ ret = keymgr_pregenerate_zsks(&kctx, argc > 3 ? argv[2] : NULL,
+ argc > 3 ? argv[3] : argv[2]);
+ } else if (strcmp(argv[1], "show-offline") == 0) {
+ ret = keymgr_print_offline_records(&kctx, argc > 2 ? argv[2] : NULL,
+ argc > 3 ? argv[3] : NULL);
+ print_ok_on_succes = false;
+ } else if (strcmp(argv[1], "del-offline") == 0) {
+ CHECK_MISSING_ARG2("Timestamps from-to not specified");
+ ret = keymgr_delete_offline_records(&kctx, argv[2], argv[3]);
+ } else if (strcmp(argv[1], "del-all-old") == 0) {
+ ret = keymgr_del_all_old(&kctx);
+ } else if (strcmp(argv[1], "generate-ksr") == 0) {
+ CHECK_MISSING_ARG("Timestamps to not specified");
+ ret = keymgr_print_ksr(&kctx, argc > 3 ? argv[2] : NULL,
+ argc > 3 ? argv[3] : argv[2]);
+ print_ok_on_succes = false;
+ } else if (strcmp(argv[1], "sign-ksr") == 0) {
+ CHECK_MISSING_ARG("Input file not specified");
+ ret = keymgr_sign_ksr(&kctx, argv[2]);
+ print_ok_on_succes = false;
+ } else if (strcmp(argv[1], "validate-skr") == 0) {
+ CHECK_MISSING_ARG("Input file not specified");
+ ret = keymgr_validate_skr(&kctx, argv[2]);
+ } else if (strcmp(argv[1], "import-skr") == 0) {
+ CHECK_MISSING_ARG("Input file not specified");
+ ret = keymgr_import_skr(&kctx, argv[2]);
+ } else {
+ ERR2("invalid command '%s'", argv[1]);
+ goto main_end;
+ }
+
+#undef CHECK_MISSING_ARG
+
+ if (ret == KNOT_EOK) {
+ printf("%s", print_ok_on_succes ? "OK\n" : "");
+ } else {
+ ERR2("%s", knot_strerror(ret));
+ }
+
+main_end:
+ kdnssec_ctx_deinit(&kctx);
+ free(zone_name);
+
+ return ret;
+}
+
+int main(int argc, char *argv[])
+{
+ knot_lmdb_db_t kaspdb = { 0 };
+
+ struct option opts[] = {
+ { "config", required_argument, NULL, 'c' },
+ { "confdb", required_argument, NULL, 'C' },
+ { "dir", required_argument, NULL, 'D' },
+ { "tsig", required_argument, NULL, 't' },
+ { "extended", no_argument, NULL, 'e' },
+ { "list", no_argument, NULL, 'l' },
+ { "brief", no_argument, NULL, 'b' }, // Legacy.
+ { "mono", no_argument, NULL, 'x' },
+ { "color", no_argument, NULL, 'X' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "json", no_argument, NULL, 'j' },
+ { NULL }
+ };
+
+ tzset();
+
+ signal_ctx.close_db = &kaspdb;
+ signal_init_std();
+
+ int ret;
+ bool just_list = false;
+ keymgr_list_params_t list_params = { 0 };
+
+ list_params.color = isatty(STDOUT_FILENO);
+
+ int opt = 0, parm = 0;
+ while ((opt = getopt_long(argc, argv, "c:C:D:t:ejlbxXhV", opts, NULL)) != -1) {
+ switch (opt) {
+ case 'c':
+ if (util_conf_init_file(optarg) != KNOT_EOK) {
+ goto failure;
+ }
+ break;
+ case 'C':
+ if (util_conf_init_confdb(optarg) != KNOT_EOK) {
+ goto failure;
+ }
+ break;
+ case 'D':
+ if (util_conf_init_justdb("kasp-db", optarg) != KNOT_EOK) {
+ goto failure;
+ }
+ break;
+ case 't':
+ if (argc > optind + 1) {
+ (void)str_to_int(argv[optind + 1], &parm, 0, 65536);
+ }
+ ret = keymgr_generate_tsig(optarg, (argc > optind ? argv[optind] : "hmac-sha256"), parm);
+ if (ret != KNOT_EOK) {
+ ERR2("failed to generate TSIG (%s)", knot_strerror(ret));
+ goto failure;
+ }
+ goto success;
+ case 'e':
+ list_params.extended = true;
+ break;
+ case 'j':
+ list_params.json = true;
+ break;
+ case 'l':
+ just_list = true;
+ break;
+ case 'b':
+ WARN2("option '--brief' is deprecated and enabled by default");
+ break;
+ case 'x':
+ list_params.color = false;
+ break;
+ case 'X':
+ list_params.color = true;
+ break;
+ case 'h':
+ print_help();
+ goto success;
+ case 'V':
+ print_version(PROGRAM_NAME);
+ goto success;
+ default:
+ print_help();
+ goto failure;
+ }
+ }
+
+ signal_ctx.color = list_params.color;
+
+ if (util_conf_init_default(true) != KNOT_EOK) {
+ goto failure;
+ }
+
+ util_update_privileges();
+
+ conf_val_t mapsize = conf_db_param(conf(), C_KASP_DB_MAX_SIZE);
+ char *kasp_dir = conf_db(conf(), C_KASP_DB);
+ knot_lmdb_init(&kaspdb, kasp_dir, conf_int(&mapsize), 0, "keys_db");
+ free(kasp_dir);
+
+ if (just_list) {
+ ret = keymgr_list_zones(&kaspdb, list_params.json);
+ } else {
+ ret = key_command(argc, argv, optind, &kaspdb, &list_params);
+ }
+ knot_lmdb_deinit(&kaspdb);
+ if (ret != KNOT_EOK) {
+ goto failure;
+ }
+
+success:
+ util_conf_deinit();
+ return EXIT_SUCCESS;
+failure:
+ util_conf_deinit();
+ return EXIT_FAILURE;
+}
diff --git a/src/utils/keymgr/offline_ksk.c b/src/utils/keymgr/offline_ksk.c
new file mode 100644
index 0000000..253199f
--- /dev/null
+++ b/src/utils/keymgr/offline_ksk.c
@@ -0,0 +1,582 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <time.h>
+
+#include "utils/keymgr/offline_ksk.h"
+#include "contrib/strtonum.h"
+#include "knot/dnssec/kasp/policy.h"
+#include "knot/dnssec/key-events.h"
+#include "knot/dnssec/key_records.h"
+#include "knot/dnssec/rrset-sign.h"
+#include "knot/dnssec/zone-events.h"
+#include "knot/dnssec/zone-keys.h"
+#include "knot/dnssec/zone-sign.h"
+#include "libzscanner/scanner.h"
+#include "utils/common/msg.h"
+#include "utils/keymgr/functions.h"
+
+#define KSR_SKR_VER "1.0"
+
+static int pregenerate_once(kdnssec_ctx_t *ctx, knot_time_t *next)
+{
+ zone_sign_reschedule_t resch = { 0 };
+
+ // generate ZSKs
+ int ret = knot_dnssec_key_rollover(ctx, KEY_ROLL_ALLOW_ZSK_ROLL, &resch);
+ if (ret != KNOT_EOK) {
+ ERR2("key rollover failed");
+ return ret;
+ }
+ // we don't need to do anything explicitly with the generated ZSKs
+ // they're simply stored in KASP db
+
+ *next = resch.next_rollover;
+ return KNOT_EOK;
+}
+
+// please free *_dnskey and keyset even if returned error
+static int load_dnskey_rrset(kdnssec_ctx_t *ctx, knot_rrset_t **_dnskey, zone_keyset_t *keyset)
+{
+ // prepare the DNSKEY rrset to be signed
+ knot_rrset_t *dnskey = knot_rrset_new(ctx->zone->dname, KNOT_RRTYPE_DNSKEY,
+ KNOT_CLASS_IN, ctx->policy->dnskey_ttl, NULL);
+ if (dnskey == NULL) {
+ return KNOT_ENOMEM;
+ }
+ *_dnskey = dnskey;
+
+ int ret = load_zone_keys(ctx, keyset, false);
+ if (ret != KNOT_EOK) {
+ ERR2("failed to load keys");
+ return ret;
+ }
+
+ for (int i = 0; i < keyset->count; i++) {
+ zone_key_t *key = &keyset->keys[i];
+ if (key->is_public) {
+ ret = rrset_add_zone_key(dnskey, key);
+ if (ret != KNOT_EOK) {
+ ERR2("failed to add zone key");
+ return ret;
+ }
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+int keymgr_pregenerate_zsks(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to)
+{
+ knot_time_t from = 0, to;
+ int ret = parse_timestamp(arg_to, &to);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ if (arg_from != NULL) {
+ ret = parse_timestamp(arg_from, &from);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ knot_time_t next = (from == 0 ? ctx->now : from);
+ ret = KNOT_EOK;
+
+ ctx->keep_deleted_keys = true;
+ ctx->policy->manual = false;
+
+ if (ctx->policy->dnskey_ttl == UINT32_MAX ||
+ ctx->policy->zone_maximal_ttl == UINT32_MAX) {
+ ERR2("dnskey-ttl or zone-max-ttl not configured");
+ return KNOT_ESEMCHECK;
+ }
+
+ while (ret == KNOT_EOK && knot_time_cmp(next, to) <= 0) {
+ ctx->now = next;
+ ret = pregenerate_once(ctx, &next);
+ }
+
+ return ret;
+}
+
+static int dump_rrset_to_buf(const knot_rrset_t *rrset, char **buf, size_t *buf_size)
+{
+ if (*buf == NULL) {
+ *buf = malloc(*buf_size);
+ if (*buf == NULL) {
+ return KNOT_ENOMEM;
+ }
+ }
+
+ knot_dump_style_t style = {
+ .wrap = true,
+ .show_ttl = true,
+ .verbose = true,
+ .original_ttl = true,
+ .human_timestamp = true
+ };
+ return knot_rrset_txt_dump(rrset, buf, buf_size, &style);
+}
+
+static void print_header(const char *of_what, knot_time_t timestamp, const char *contents)
+{
+ char date[64] = { 0 };
+ (void)knot_time_print(TIME_PRINT_ISO8601, timestamp, date, sizeof(date));
+ printf(";; %s %"PRIu64" (%s) =========\n%s", of_what,
+ timestamp, date, contents);
+}
+
+int keymgr_print_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to)
+{
+ knot_time_t from = 0, to = 0;
+ if (arg_from != NULL) {
+ int ret = parse_timestamp(arg_from, &from);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+ if (arg_to != NULL) {
+ int ret = parse_timestamp(arg_to, &to);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ bool empty = true;
+ char *buf = NULL;
+ size_t buf_size = 512;
+ while (true) {
+ if (arg_to != NULL && knot_time_cmp(from, to) > 0) {
+ break;
+ }
+ knot_time_t next;
+ key_records_t r = { { 0 } };
+ int ret = kasp_db_load_offline_records(ctx->kasp_db, ctx->zone->dname,
+ &from, &next, &r);
+ if (ret == KNOT_ENOENT) {
+ break;
+ } else if (ret != KNOT_EOK) {
+ free(buf);
+ return ret;
+ }
+
+ ret = key_records_dump(&buf, &buf_size, &r, true);
+ key_records_clear(&r);
+ if (ret != KNOT_EOK) {
+ free(buf);
+ return ret;
+ }
+ print_header("Offline records for", from, buf);
+ empty = false;
+
+ if (next == 0) {
+ break;
+ }
+ from = next;
+ }
+ free(buf);
+
+ /* If from is lower than the first record's timestamp, try to start
+ from the first one's instead of empty output. */
+ if (empty && from > 0) {
+ knot_time_t last = 0;
+ int ret = key_records_last_timestamp(ctx, &last);
+ if (ret == KNOT_EOK && knot_time_cmp(last, from) > 0) {
+ return keymgr_print_offline_records(ctx, 0, arg_to);
+ }
+ }
+ return KNOT_EOK;
+}
+
+int keymgr_delete_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to)
+{
+ knot_time_t from, to;
+ int ret = parse_timestamp(arg_from, &from);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ ret = parse_timestamp(arg_to, &to);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ return kasp_db_delete_offline_records(ctx->kasp_db, ctx->zone->dname, from, to);
+}
+
+int keymgr_del_all_old(kdnssec_ctx_t *ctx)
+{
+ for (size_t i = 0; i < ctx->zone->num_keys; ) {
+ knot_kasp_key_t *key = &ctx->zone->keys[i];
+ if (knot_time_cmp(key->timing.remove, ctx->now) < 0) {
+ int ret = kdnssec_delete_key(ctx, key);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ } else {
+ i++;
+ }
+ }
+ return kdnssec_ctx_commit(ctx);
+}
+
+static void print_generated_message(void)
+{
+ char buf[64] = { 0 };
+ knot_time_print(TIME_PRINT_ISO8601, knot_time(), buf, sizeof(buf));
+ printf("generated at %s by Knot DNS %s\n", buf, VERSION);
+}
+
+static int ksr_once(kdnssec_ctx_t *ctx, char **buf, size_t *buf_size, knot_time_t *next_ksr)
+{
+ knot_rrset_t *dnskey = NULL;
+ zone_keyset_t keyset = { 0 };
+ int ret = load_dnskey_rrset(ctx, &dnskey, &keyset);
+ if (ret != KNOT_EOK) {
+ goto done;
+ }
+ ret = dump_rrset_to_buf(dnskey, buf, buf_size);
+ if (ret >= 0) {
+ print_header("KeySigningRequest "KSR_SKR_VER, ctx->now, *buf);
+ ret = KNOT_EOK;
+ }
+
+done:
+ if (ret == KNOT_EOK && next_ksr != NULL) {
+ *next_ksr = knot_get_next_zone_key_event(&keyset);
+ }
+ knot_rrset_free(dnskey, NULL);
+ free_zone_keys(&keyset);
+ return ret;
+}
+
+#define OFFLINE_KSK_CONF_CHECK \
+ if (!ctx->policy->offline_ksk || !ctx->policy->manual) { \
+ ERR2("offline-ksk and manual must be enabled in configuration"); \
+ return KNOT_ESEMCHECK; \
+ }
+
+int keymgr_print_ksr(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to)
+{
+ OFFLINE_KSK_CONF_CHECK
+
+ knot_time_t from, to;
+ int ret = parse_timestamp(arg_to, &to);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ if (arg_from == NULL) {
+ ret = key_records_last_timestamp(ctx, &from);
+ } else {
+ ret = parse_timestamp(arg_from, &from);
+ }
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ char *buf = NULL;
+ size_t buf_size = 4096;
+ while (ret == KNOT_EOK && knot_time_cmp(from, to) < 0) {
+ ctx->now = from;
+ ret = ksr_once(ctx, &buf, &buf_size, &from);
+ }
+ if (ret != KNOT_EOK) {
+ free(buf);
+ return ret;
+ }
+ ctx->now = to;
+ // force end of period as a KSR timestamp
+ ret = ksr_once(ctx, &buf, &buf_size, NULL);
+
+ printf(";; KeySigningRequest %s ", KSR_SKR_VER);
+ print_generated_message();
+
+ free(buf);
+ return ret;
+}
+
+typedef struct {
+ int ret;
+ key_records_t r;
+ knot_time_t timestamp;
+ kdnssec_ctx_t *kctx;
+} ksr_sign_ctx_t;
+
+static int ksr_sign_dnskey(kdnssec_ctx_t *ctx, knot_rrset_t *zsk, knot_time_t now,
+ knot_time_t *next_sign)
+{
+ zone_keyset_t keyset = { 0 };
+ char *buf = NULL;
+ size_t buf_size = 4096;
+ knot_time_t rrsigs_expire = 0;
+
+ ctx->now = now;
+ ctx->policy->dnskey_ttl = zsk->ttl;
+
+ knot_timediff_t rrsig_refresh = ctx->policy->rrsig_refresh_before;
+ if (rrsig_refresh == UINT32_MAX) { // not setting rrsig-refresh prohibited by documentation, but we need to do something
+ rrsig_refresh = ctx->policy->dnskey_ttl + ctx->policy->propagation_delay;
+ }
+
+ int ret = load_zone_keys(ctx, &keyset, false);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ key_records_t r;
+ key_records_init(ctx, &r);
+
+ ret = knot_zone_sign_add_dnskeys(&keyset, ctx, &r, NULL, NULL);
+ if (ret != KNOT_EOK) {
+ goto done;
+ }
+
+ ret = knot_rdataset_merge(&r.dnskey.rrs, &zsk->rrs, NULL);
+ if (ret != KNOT_EOK) {
+ goto done;
+ }
+
+ // no check if the KSK used for signing (in keyset) is contained in DNSKEY record being signed (in KSR) !
+ for (int i = 0; i < keyset.count; i++) {
+ ret = key_records_sign(&keyset.keys[i], &r, ctx, &rrsigs_expire);
+ if (ret != KNOT_EOK) {
+ goto done;
+ }
+ }
+ ret = key_records_dump(&buf, &buf_size, &r, true);
+ if (ret == KNOT_EOK) {
+ print_header("SignedKeyResponse "KSR_SKR_VER, ctx->now, buf);
+ *next_sign = knot_time_min(
+ knot_get_next_zone_key_event(&keyset),
+ knot_time_add(rrsigs_expire, -rrsig_refresh)
+ );
+ }
+
+done:
+ free(buf);
+ key_records_clear(&r);
+ free_zone_keys(&keyset);
+ return ret;
+}
+
+static int process_skr_between_ksrs(ksr_sign_ctx_t *ctx, knot_time_t from, knot_time_t to)
+{
+ for (knot_time_t t = from; t < to /* if (t == infinity) stop */; ) {
+ int ret = ksr_sign_dnskey(ctx->kctx, &ctx->r.dnskey, t, &t);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+ return KNOT_EOK;
+}
+
+static void ksr_sign_header(zs_scanner_t *sc)
+{
+ ksr_sign_ctx_t *ctx = sc->process.data;
+
+ // parse header
+ _unused_ float header_ver;
+ char next_str[21] = { 0 };
+ if (sc->error.code != 0 || ctx->ret != KNOT_EOK ||
+ sscanf((const char *)sc->buffer, "; KeySigningRequest %f %20s",
+ &header_ver, next_str) < 1) {
+ return;
+ }
+
+ knot_time_t next_timestamp;
+ if (str_to_u64(next_str, &next_timestamp) != KNOT_EOK) {
+ // trailing header without timestamp
+ next_timestamp = 0;
+ }
+
+ // sign previous KSR and inbetween KSK changes
+ if (ctx->timestamp > 0) {
+ knot_time_t inbetween_from;
+ ctx->ret = ksr_sign_dnskey(ctx->kctx, &ctx->r.dnskey, ctx->timestamp,
+ &inbetween_from);
+ if (next_timestamp > 0 && ctx->ret == KNOT_EOK) {
+ ctx->ret = process_skr_between_ksrs(ctx, inbetween_from,
+ next_timestamp);
+ }
+ key_records_clear_rdatasets(&ctx->r);
+ }
+
+ // start new KSR
+ ctx->timestamp = next_timestamp;
+}
+
+static void ksr_sign_once(zs_scanner_t *sc)
+{
+ ksr_sign_ctx_t *ctx = sc->process.data;
+ if (sc->error.code == 0 && ctx->ret == KNOT_EOK) {
+ ctx->ret = knot_rrset_add_rdata(&ctx->r.dnskey, sc->r_data, sc->r_data_length, NULL);
+ ctx->r.dnskey.ttl = sc->r_ttl;
+ }
+}
+
+static void skr_import_header(zs_scanner_t *sc)
+{
+ ksr_sign_ctx_t *ctx = sc->process.data;
+
+ // parse header
+ _unused_ float header_ver;
+ char next_str[21] = { 0 };
+ if (sc->error.code != 0 || ctx->ret != KNOT_EOK ||
+ sscanf((const char *)sc->buffer, "; SignedKeyResponse %f %20s",
+ &header_ver, next_str) < 1) {
+ return;
+ }
+
+ knot_time_t next_timestamp;
+ if (str_to_u64(next_str, &next_timestamp) != KNOT_EOK) {
+ // trailing header without timestamp
+ next_timestamp = 0;
+ }
+
+ // delete possibly existing conflicting offline records
+ ctx->ret = kasp_db_delete_offline_records(
+ ctx->kctx->kasp_db, ctx->kctx->zone->dname, next_timestamp, 0
+ );
+
+ // store previous SKR
+ if (ctx->timestamp > 0 && ctx->ret == KNOT_EOK) {
+ ctx->ret = key_records_verify(&ctx->r, ctx->kctx, ctx->timestamp);
+ if (ctx->ret != KNOT_EOK) {
+ return;
+ }
+ if (next_timestamp > 0) {
+ ctx->ret = key_records_verify(&ctx->r, ctx->kctx, next_timestamp - 1);
+ if (ctx->ret != KNOT_EOK) {
+ return;
+ }
+ }
+ ctx->ret = kasp_db_store_offline_records(ctx->kctx->kasp_db,
+ ctx->timestamp, &ctx->r);
+ key_records_clear_rdatasets(&ctx->r);
+ }
+
+ // start new SKR
+ ctx->timestamp = next_timestamp;
+}
+
+static void skr_validate_header(zs_scanner_t *sc)
+{
+ ksr_sign_ctx_t *ctx = sc->process.data;
+
+ _unused_ float header_ver;
+ char next_str[21] = { 0 };
+ if (sc->error.code != 0 || ctx->ret != KNOT_EOK ||
+ sscanf((const char *)sc->buffer, "; SignedKeyResponse %f %20s",
+ &header_ver, next_str) < 1) {
+ return;
+ }
+
+ knot_time_t next_timestamp;
+ if (str_to_u64(next_str, &next_timestamp) != KNOT_EOK) {
+ // trailing header without timestamp
+ next_timestamp = 0;
+ }
+
+ if (ctx->timestamp > 0 && ctx->ret == KNOT_EOK) {
+ int ret = key_records_verify(&ctx->r, ctx->kctx, ctx->timestamp);
+ if (ret != KNOT_EOK) { // ctx->ret untouched
+ ERR2("invalid SignedKeyResponse for %"KNOT_TIME_PRINTF" (%s)",
+ ctx->timestamp, knot_strerror(ret));
+ }
+ if (next_timestamp > 0) {
+ ret = key_records_verify(&ctx->r, ctx->kctx, next_timestamp - 1);
+ if (ret != KNOT_EOK) { // ctx->ret untouched
+ ERR2("invalid SignedKeyResponse for %"KNOT_TIME_PRINTF" (%s)",
+ next_timestamp - 1, knot_strerror(ret));
+ }
+ }
+ key_records_clear_rdatasets(&ctx->r);
+ }
+
+ ctx->timestamp = next_timestamp;
+}
+
+static void skr_import_once(zs_scanner_t *sc)
+{
+ ksr_sign_ctx_t *ctx = sc->process.data;
+ if (sc->error.code == 0 && ctx->ret == KNOT_EOK) {
+ ctx->ret = key_records_add_rdata(&ctx->r, sc->r_type, sc->r_data,
+ sc->r_data_length, sc->r_ttl);
+ }
+}
+
+static int read_ksr_skr(kdnssec_ctx_t *ctx, const char *infile,
+ void (*cb_header)(zs_scanner_t *), void (*cb_record)(zs_scanner_t *))
+{
+ zs_scanner_t sc = { 0 };
+ int ret = zs_init(&sc, "", KNOT_CLASS_IN, 0);
+ if (ret < 0) {
+ return KNOT_ERROR;
+ }
+
+ ret = zs_set_input_file(&sc, infile);
+ if (ret < 0) {
+ zs_deinit(&sc);
+ return (sc.error.code == ZS_FILE_ACCESS) ? KNOT_EFACCES : KNOT_EFILE;
+ }
+
+ ksr_sign_ctx_t pctx = { 0 };
+ key_records_init(ctx, &pctx.r);
+ pctx.kctx = ctx;
+ ret = zs_set_processing(&sc, cb_record, NULL, &pctx);
+ if (ret < 0) {
+ zs_deinit(&sc);
+ return KNOT_EBUSY;
+ }
+ sc.process.comment = cb_header;
+
+ ret = zs_parse_all(&sc);
+
+ if (sc.error.code != 0) {
+ ret = KNOT_EMALF;
+ } else if (pctx.ret != KNOT_EOK) {
+ ret = pctx.ret;
+ } else if (ret < 0 || pctx.r.dnskey.rrs.count > 0 || pctx.r.cdnskey.rrs.count > 0 ||
+ pctx.r.cds.rrs.count > 0 || pctx.r.rrsig.rrs.count > 0) {
+ ret = KNOT_EMALF;
+ }
+ key_records_clear(&pctx.r);
+ zs_deinit(&sc);
+ return ret;
+}
+
+int keymgr_sign_ksr(kdnssec_ctx_t *ctx, const char *ksr_file)
+{
+ OFFLINE_KSK_CONF_CHECK
+
+ int ret = read_ksr_skr(ctx, ksr_file, ksr_sign_header, ksr_sign_once);
+ printf(";; SignedKeyResponse %s ", KSR_SKR_VER);
+ print_generated_message();
+ return ret;
+}
+
+int keymgr_import_skr(kdnssec_ctx_t *ctx, const char *skr_file)
+{
+ OFFLINE_KSK_CONF_CHECK
+
+ return read_ksr_skr(ctx, skr_file, skr_import_header, skr_import_once);
+}
+
+int keymgr_validate_skr(kdnssec_ctx_t *ctx, const char *skr_file)
+{
+ return read_ksr_skr(ctx, skr_file, skr_validate_header, skr_import_once);
+}
diff --git a/src/utils/keymgr/offline_ksk.h b/src/utils/keymgr/offline_ksk.h
new file mode 100644
index 0000000..bf0e085
--- /dev/null
+++ b/src/utils/keymgr/offline_ksk.h
@@ -0,0 +1,35 @@
+/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/dnssec/context.h"
+
+int keymgr_pregenerate_zsks(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to);
+
+int keymgr_print_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to);
+
+int keymgr_delete_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to);
+
+int keymgr_del_all_old(kdnssec_ctx_t *ctx);
+
+int keymgr_print_ksr(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to);
+
+int keymgr_sign_ksr(kdnssec_ctx_t *ctx, const char *ksr_file);
+
+int keymgr_import_skr(kdnssec_ctx_t *ctx, const char *skr_file);
+
+int keymgr_validate_skr(kdnssec_ctx_t *ctx, const char *skr_file);
diff --git a/src/utils/khost/khost_main.c b/src/utils/khost/khost_main.c
new file mode 100644
index 0000000..75b0db7
--- /dev/null
+++ b/src/utils/khost/khost_main.c
@@ -0,0 +1,45 @@
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+
+#include "libdnssec/crypto.h"
+#include "utils/khost/khost_params.h"
+#include "utils/kdig/kdig_exec.h"
+#include "libknot/libknot.h"
+
+int main(int argc, char *argv[])
+{
+ int ret = EXIT_SUCCESS;
+
+ tzset();
+
+ kdig_params_t params;
+ if (khost_parse(&params, argc, argv) == KNOT_EOK) {
+ if (!params.stop) {
+ dnssec_crypto_init();
+ if (kdig_exec(&params) != KNOT_EOK) {
+ ret = EXIT_FAILURE;
+ }
+ dnssec_crypto_cleanup();
+ }
+ } else {
+ ret = EXIT_FAILURE;
+ }
+
+ khost_clean(&params);
+ return ret;
+}
diff --git a/src/utils/khost/khost_params.c b/src/utils/khost/khost_params.c
new file mode 100644
index 0000000..1423e09
--- /dev/null
+++ b/src/utils/khost/khost_params.c
@@ -0,0 +1,365 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <getopt.h>
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "utils/khost/khost_params.h"
+#include "utils/kdig/kdig_params.h"
+#include "utils/common/msg.h"
+#include "utils/common/params.h"
+#include "utils/common/resolv.h"
+#include "libknot/libknot.h"
+#include "contrib/strtonum.h"
+#include "contrib/ucw/lists.h"
+
+#define PROGRAM_NAME "khost"
+
+#define DEFAULT_RETRIES_HOST 1
+#define DEFAULT_TIMEOUT_HOST 2
+
+static const style_t DEFAULT_STYLE_HOST = {
+ .format = FORMAT_HOST,
+ .style = {
+ .wrap = false,
+ .show_class = true,
+ .show_ttl = true,
+ .verbose = false,
+ .original_ttl = false,
+ .empty_ttl = false,
+ .human_ttl = false,
+ .human_timestamp = true,
+ .generic = false,
+ .ascii_to_idn = name_to_idn
+ },
+ .show_query = false,
+ .show_header = false,
+ .show_edns = false,
+ .show_question = true,
+ .show_answer = true,
+ .show_authority = true,
+ .show_additional = true,
+ .show_tsig = false,
+ .show_footer = false
+};
+
+static int khost_init(kdig_params_t *params)
+{
+ // Initialize params with kdig defaults.
+ int ret = kdig_init(params);
+
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // Set khost specific defaults.
+ free(params->config->port);
+ params->config->port = strdup(DEFAULT_DNS_PORT);
+ params->config->retries = DEFAULT_RETRIES_HOST;
+ params->config->wait = DEFAULT_TIMEOUT_HOST;
+ params->config->class_num = KNOT_CLASS_IN;
+ params->config->style = DEFAULT_STYLE_HOST;
+ params->config->idn = true;
+
+ // Check port.
+ if (params->config->port == NULL) {
+ query_free(params->config);
+ return KNOT_ENOMEM;
+ }
+
+ return KNOT_EOK;
+}
+
+void khost_clean(kdig_params_t *params)
+{
+ if (params == NULL) {
+ DBG_NULL;
+ return;
+ }
+
+ kdig_clean(params);
+}
+
+static int parse_server(const char *value, list_t *servers, const char *def_port)
+{
+ if (params_parse_server(value, servers, def_port) != KNOT_EOK) {
+ ERR("invalid server %s", value);
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+static int parse_name(const char *value, list_t *queries, const query_t *conf)
+{
+ char *reverse = get_reverse_name(value);
+ char *ascii_name = (char *)value;
+ query_t *query;
+
+ if (conf->idn) {
+ ascii_name = name_from_idn(value);
+ if (ascii_name == NULL) {
+ free(reverse);
+ return KNOT_EINVAL;
+ }
+ }
+
+ // If name is not FQDN, append trailing dot.
+ char *fqd_name = get_fqd_name(ascii_name);
+
+ if (conf->idn) {
+ free(ascii_name);
+ }
+
+ // RR type is known.
+ if (conf->type_num >= 0) {
+ if (conf->type_num == KNOT_RRTYPE_PTR) {
+ free(fqd_name);
+
+ // Check for correct address.
+ if (reverse == NULL) {
+ ERR("invalid IPv4/IPv6 address %s", value);
+ return KNOT_EINVAL;
+ }
+
+ // Add reverse query for address.
+ query = query_create(reverse, conf);
+ free(reverse);
+ if (query == NULL) {
+ return KNOT_ENOMEM;
+ }
+ add_tail(queries, (node_t *)query);
+ } else {
+ free(reverse);
+
+ // Add query for name and specified type.
+ query = query_create(fqd_name, conf);
+ free(fqd_name);
+ if (query == NULL) {
+ return KNOT_ENOMEM;
+ }
+ add_tail(queries, (node_t *)query);
+ }
+ // RR type is unknown, use defaults.
+ } else {
+ if (reverse == NULL) {
+ // Add query for name and type A.
+ query = query_create(fqd_name, conf);
+ if (query == NULL) {
+ free(fqd_name);
+ return KNOT_ENOMEM;
+ }
+ query->type_num = KNOT_RRTYPE_A;
+ add_tail(queries, (node_t *)query);
+
+ // Add query for name and type AAAA.
+ query = query_create(fqd_name, conf);
+ if (query == NULL) {
+ free(fqd_name);
+ return KNOT_ENOMEM;
+ }
+ query->type_num = KNOT_RRTYPE_AAAA;
+ query->style.hide_cname = true;
+ add_tail(queries, (node_t *)query);
+
+ // Add query for name and type MX.
+ query = query_create(fqd_name, conf);
+ if (query == NULL) {
+ free(fqd_name);
+ return KNOT_ENOMEM;
+ }
+ free(fqd_name);
+ query->type_num = KNOT_RRTYPE_MX;
+ query->style.hide_cname = true;
+ add_tail(queries, (node_t *)query);
+ } else {
+ free(fqd_name);
+
+ // Add reverse query for address.
+ query = query_create(reverse, conf);
+ free(reverse);
+ if (query == NULL) {
+ return KNOT_ENOMEM;
+ }
+ query->type_num = KNOT_RRTYPE_PTR;
+ add_tail(queries, (node_t *)query);
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+static void print_help(void)
+{
+ printf("Usage: %s [-4] [-6] [-adhrsTvVw] [-c class] [-t type]\n"
+ " [-R retries] [-W time] name [server]\n\n"
+ " -4 Use IPv4 protocol only.\n"
+ " -6 Use IPv6 protocol only.\n"
+ " -a Same as -t ANY -v.\n"
+ " -d Allow debug messages.\n"
+ " -h, --help Print the program help.\n"
+ " -r Disable recursion.\n"
+ " -T Use TCP protocol.\n"
+ " -v Verbose output.\n"
+ " -V, --version Print the program version.\n"
+ " -w Wait forever.\n"
+ " -c Set query class.\n"
+ " -t Set query type.\n"
+ " -R Set number of UDP retries.\n"
+ " -W Set wait interval.\n",
+ PROGRAM_NAME);
+}
+
+int khost_parse(kdig_params_t *params, int argc, char *argv[])
+{
+ if (params == NULL || argv == NULL) {
+ DBG_NULL;
+ return KNOT_EINVAL;
+ }
+
+ if (khost_init(params) != KNOT_EOK) {
+ return KNOT_ERROR;
+ }
+
+#ifdef LIBIDN
+ // Set up localization.
+ if (setlocale(LC_CTYPE, "") == NULL) {
+ params->config->idn = false;
+ }
+#endif
+
+ query_t *conf = params->config;
+ uint16_t rclass, rtype;
+ int64_t serial;
+ bool notify;
+
+ // Long options.
+ struct option opts[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL }
+ };
+
+ // Command line options processing.
+ int opt = 0;
+ while ((opt = getopt_long(argc, argv, "46adhrsTvVwc:t:R:W:", opts, NULL))
+ != -1) {
+ switch (opt) {
+ case '4':
+ conf->ip = IP_4;
+ break;
+ case '6':
+ conf->ip = IP_6;
+ break;
+ case 'a':
+ conf->type_num = KNOT_RRTYPE_ANY;
+ conf->style.format = FORMAT_FULL;
+ conf->style.show_header = true;
+ conf->style.show_edns = true;
+ conf->style.show_footer = true;
+ break;
+ case 'd':
+ msg_enable_debug(1);
+ break;
+ case 'h':
+ print_help();
+ params->stop = false;
+ return KNOT_EOK;
+ case 'r':
+ conf->flags.rd_flag = false;
+ break;
+ case 'T':
+ conf->protocol = PROTO_TCP;
+ break;
+ case 'v':
+ conf->style.format = FORMAT_FULL;
+ conf->style.show_header = true;
+ conf->style.show_edns = true;
+ conf->style.show_footer = true;
+ break;
+ case 'V':
+ print_version(PROGRAM_NAME);
+ params->stop = false;
+ return KNOT_EOK;
+ case 'w':
+ conf->wait = -1;
+ break;
+ case 'c':
+ if (params_parse_class(optarg, &rclass) != KNOT_EOK) {
+ ERR("invalid class '%s'", optarg);
+ return KNOT_EINVAL;
+ }
+ conf->class_num = rclass;
+ break;
+ case 't':
+ if (params_parse_type(optarg, &rtype, &serial, &notify)
+ != KNOT_EOK) {
+ ERR("invalid type '%s'", optarg);
+ return KNOT_EINVAL;
+ }
+ conf->type_num = rtype;
+ conf->serial = serial;
+ conf->notify = notify;
+
+ // If NOTIFY, reset default RD flag.
+ if (conf->notify) {
+ conf->flags.rd_flag = false;
+ }
+ break;
+ case 'R':
+ if (str_to_u32(optarg, &conf->retries) != KNOT_EOK) {
+ ERR("invalid retries '%s'", optarg);
+ return KNOT_EINVAL;
+ }
+ break;
+ case 'W':
+ if (params_parse_wait(optarg, &conf->wait) != KNOT_EOK) {
+ ERR("invalid wait '%s'", optarg);
+ return KNOT_EINVAL;
+ }
+ break;
+ default:
+ print_help();
+ return KNOT_ENOTSUP;
+ }
+ }
+
+ // Process non-option parameters.
+ switch (argc - optind) {
+ case 2:
+ if (parse_server(argv[optind + 1], &conf->servers, conf->port)
+ != KNOT_EOK) {
+ return KNOT_EINVAL;
+ }
+ case 1: // Fall through.
+ if (parse_name(argv[optind], &params->queries, conf)
+ != KNOT_EOK) {
+ return KNOT_EINVAL;
+ }
+ break;
+ default:
+ print_help();
+ return KNOT_ENOTSUP;
+ }
+
+ // Complete missing data in queries based on defaults.
+ complete_queries(&params->queries, params->config);
+
+ return KNOT_EOK;
+}
diff --git a/src/utils/khost/khost_params.h b/src/utils/khost/khost_params.h
new file mode 100644
index 0000000..6f019fe
--- /dev/null
+++ b/src/utils/khost/khost_params.h
@@ -0,0 +1,22 @@
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "utils/kdig/kdig_params.h"
+
+int khost_parse(kdig_params_t *params, int argc, char *argv[]);
+void khost_clean(kdig_params_t *params);
diff --git a/src/utils/kjournalprint/main.c b/src/utils/kjournalprint/main.c
new file mode 100644
index 0000000..3ba0019
--- /dev/null
+++ b/src/utils/kjournalprint/main.c
@@ -0,0 +1,491 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <getopt.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "libknot/libknot.h"
+#include "knot/journal/journal_basic.h"
+#include "knot/journal/journal_metadata.h"
+#include "knot/journal/journal_read.h"
+#include "knot/journal/serialization.h"
+#include "knot/zone/zone-dump.h"
+#include "utils/common/msg.h"
+#include "utils/common/params.h"
+#include "utils/common/signal.h"
+#include "utils/common/util_conf.h"
+#include "contrib/color.h"
+#include "contrib/strtonum.h"
+#include "contrib/string.h"
+#include "contrib/time.h"
+
+#define PROGRAM_NAME "kjournalprint"
+
+knot_lmdb_db_t journal_db = { 0 };
+signal_ctx_t signal_ctx = { 0 }; // global, needed by signal handler
+
+static void print_help(void)
+{
+ printf("Usage:\n"
+ " %s [-c | -C | -D <path>] [options] <zone_name>\n"
+ " %s [-c | -C | -D <path>] -z\n"
+ "\n"
+ "Config options:\n"
+ " -c, --config <file> Path to a textual configuration file.\n"
+ " (default %s)\n"
+ " -C, --confdb <dir> Path to a configuration database directory.\n"
+ " (default %s)\n"
+ " -D, --dir <path> Path to a journal database directory, use default\n"
+ " configuration.\n"
+ "Options:\n"
+ " -z, --zone-list Instead of reading the journal, display the list\n"
+ " of zones in the DB.\n"
+ " -l, --limit <num> Read only <num> newest changes.\n"
+ " -s, --serial <soa> Start with a specific SOA serial.\n"
+ " -H, --check Additional journal semantic checks.\n"
+ " -d, --debug Debug mode output.\n"
+ " -x, --mono Get output without coloring.\n"
+ " -n, --no-color An alias for -x, deprecated.\n"
+ " -X, --color Force output coloring.\n"
+ " -h, --help Print the program help.\n"
+ " -V, --version Print the program version.\n",
+ PROGRAM_NAME, PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR);
+}
+
+typedef struct {
+ bool debug;
+ bool color;
+ bool check;
+ int limit;
+ int counter;
+ uint32_t serial;
+ bool from_serial;
+ size_t changes;
+} print_params_t;
+
+static void print_changeset(const changeset_t *chs, uint64_t timestamp, print_params_t *params)
+{
+ char time_buf[64] = { 0 };
+ (void)knot_time_print(TIME_PRINT_UNIX, timestamp, time_buf, sizeof(time_buf));
+
+ static size_t count = 1;
+ if (chs->soa_from == NULL) {
+ printf("%s;; Zone-in-journal, serial: %u, changeset: %zu, timestamp: %s%s\n",
+ COL_YELW(params->color),
+ knot_soa_serial(chs->soa_to->rrs.rdata),
+ count++,
+ time_buf,
+ COL_RST(params->color));
+ } else {
+ printf("%s;; Changes between zone versions: %u -> %u, changeset: %zu, timestamp: %s%s\n",
+ COL_YELW(params->color),
+ knot_soa_serial(chs->soa_from->rrs.rdata),
+ knot_soa_serial(chs->soa_to->rrs.rdata),
+ count++,
+ time_buf,
+ COL_RST(params->color));
+ }
+ changeset_print(chs, stdout, params->color);
+}
+
+knot_dynarray_declare(rrtype, uint16_t, DYNARRAY_VISIBILITY_STATIC, 100)
+knot_dynarray_define(rrtype, uint16_t, DYNARRAY_VISIBILITY_STATIC)
+
+typedef struct {
+ rrtype_dynarray_t *arr;
+ size_t *counter;
+} rrtypelist_ctx_t;
+
+static void rrtypelist_add(rrtype_dynarray_t *arr, uint16_t add_type)
+{
+ bool already_present = false;
+ knot_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 int rrtypelist_callback(zone_node_t *node, void *data)
+{
+ rrtypelist_ctx_t *ctx = data;
+ for (int i = 0; i < node->rrset_count; i++) {
+ knot_rrset_t rrset = node_rrset_at(node, i);
+ rrtypelist_add(ctx->arr, rrset.type);
+ *ctx->counter += rrset.rrs.count;
+ }
+ return KNOT_EOK;
+}
+
+static void print_changeset_debugmode(const changeset_t *chs, uint64_t timestamp)
+{
+ char time_buf[64] = { 0 };
+ (void)knot_time_print(TIME_PRINT_HUMAN_MIXED, timestamp, time_buf, sizeof(time_buf));
+
+ // detect all types
+ rrtype_dynarray_t types = { 0 };
+ size_t count_minus = 1, count_plus = 1; // 1 for SOA which is always present but not iterated
+ rrtypelist_ctx_t ctx_minus = { &types, &count_minus }, ctx_plus = { &types, &count_plus };
+ (void)zone_contents_apply(chs->remove, rrtypelist_callback, &ctx_minus);
+ (void)zone_contents_nsec3_apply(chs->remove, rrtypelist_callback, &ctx_minus);
+ (void)zone_contents_apply(chs->add, rrtypelist_callback, &ctx_plus);
+ (void)zone_contents_nsec3_apply(chs->add, rrtypelist_callback, &ctx_plus);
+
+ if (chs->soa_from == NULL) {
+ printf("Zone-in-journal %u +++: %zu\t size: %zu\t timestamp: %s\t", knot_soa_serial(chs->soa_to->rrs.rdata),
+ count_plus, changeset_serialized_size(chs), time_buf);
+ } else {
+ printf("%u -> %u ---: %zu\t +++: %zu\t size: %zu\t timestamp: %s\t", knot_soa_serial(chs->soa_from->rrs.rdata),
+ knot_soa_serial(chs->soa_to->rrs.rdata), count_minus, count_plus, changeset_serialized_size(chs), time_buf);
+ }
+
+ char temp[100];
+ knot_dynarray_foreach(rrtype, uint16_t, i, types) {
+ (void)knot_rrtype_to_string(*i, temp, sizeof(temp));
+ printf(" %s", temp);
+ }
+ printf("\n");
+}
+
+static int count_changeset_cb(_unused_ bool special, const changeset_t *ch, _unused_ uint64_t timestamp, void *ctx)
+{
+ print_params_t *params = ctx;
+ if (ch != NULL) {
+ params->counter++;
+ }
+ return KNOT_EOK;
+}
+
+static int print_changeset_cb(bool special, const changeset_t *ch, uint64_t timestamp, void *ctx)
+{
+ print_params_t *params = ctx;
+ if (ch != NULL && params->counter++ >= params->limit) {
+ if (params->debug) {
+ print_changeset_debugmode(ch, timestamp);
+ params->changes++;
+ } else {
+ print_changeset(ch, timestamp, params);
+ }
+ if (special && params->debug) {
+ printf("---------------------------------------------\n");
+ }
+ }
+ return KNOT_EOK;
+}
+
+int print_journal(char *path, knot_dname_t *name, print_params_t *params)
+{
+ zone_journal_t j = { &journal_db, name };
+ bool exists;
+ uint64_t occupied, occupied_all;
+
+ knot_lmdb_init(&journal_db, path, 0, journal_env_flags(JOURNAL_MODE_ROBUST, true), NULL);
+ int ret = knot_lmdb_exists(&journal_db);
+ if (ret == KNOT_EOK) {
+ ret = knot_lmdb_open(&journal_db);
+ }
+ if (ret != KNOT_EOK) {
+ knot_lmdb_deinit(&journal_db);
+ return ret;
+ }
+
+ ret = journal_info(j, &exists, NULL, NULL, NULL, NULL, NULL, &occupied, &occupied_all);
+ if (ret != KNOT_EOK || !exists) {
+ ERR2("zone not exists in the journal DB %s", path);
+ knot_lmdb_deinit(&journal_db);
+ return ret == KNOT_EOK ? KNOT_ENOENT : ret;
+ }
+
+ if (params->check) {
+ ret = journal_sem_check(j);
+ if (ret > 0) {
+ ERR2("semantic check failed with code %d", ret);
+ } else if (ret != KNOT_EOK) {
+ ERR2("semantic check failed (%s)", knot_strerror(ret));
+ }
+ }
+
+ if (params->limit >= 0 && ret == KNOT_EOK) {
+ if (params->from_serial) {
+ ret = journal_walk_from(j, params->serial, count_changeset_cb, params);
+ } else {
+ ret = journal_walk(j, count_changeset_cb, params);
+ }
+ }
+ if (ret == KNOT_EOK) {
+ if (params->limit < 0 || params->counter <= params->limit) {
+ params->limit = 0;
+ } else {
+ params->limit = params->counter - params->limit;
+ }
+ params->counter = 0;
+ if (params->from_serial) {
+ ret = journal_walk_from(j, params->serial, print_changeset_cb, params);
+ } else {
+ ret = journal_walk(j, print_changeset_cb, params);
+ }
+ }
+
+ if (params->debug && ret == KNOT_EOK) {
+ printf("Total number of changesets: %zu\n", params->changes);
+ printf("Occupied this zone (approx): %"PRIu64" KiB\n", occupied / 1024);
+ printf("Occupied all zones together: %"PRIu64" KiB\n", occupied_all / 1024);
+ }
+
+ knot_lmdb_deinit(&journal_db);
+ return ret;
+}
+
+static int add_zone_to_list(const knot_dname_t *zone, void *list)
+{
+ knot_dname_t *copy = knot_dname_copy(zone, NULL);
+ if (copy == NULL) {
+ return KNOT_ENOMEM;
+ }
+ return ptrlist_add(list, copy, NULL) == NULL ? KNOT_ENOMEM : KNOT_EOK;
+}
+
+static int list_zone(const knot_dname_t *zone, bool detailed, knot_lmdb_db_t *jdb, uint64_t *occupied_all)
+{
+ knot_dname_txt_storage_t zone_str;
+ if (knot_dname_to_str(zone_str, zone, sizeof(zone_str)) == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ if (detailed) {
+ zone_journal_t j = { jdb, zone };
+ uint32_t first_serial = 0, last_serial = 0;
+ bool exists = false, zij = false;
+ uint64_t occupied;
+
+ int ret = journal_info(j, &exists, &first_serial, &zij, &last_serial,
+ NULL, NULL, &occupied, occupied_all);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ assert(exists);
+ printf("%-28s %8"PRIu64" %10u %10u %3s\n",
+ zone_str, occupied / 1024, first_serial, last_serial, zij ? "yes" : "no");
+ } else {
+ printf("%s\n", zone_str);
+ }
+ return KNOT_EOK;
+}
+
+int list_zones(char *path, bool detailed)
+{
+ knot_lmdb_init(&journal_db, path, 0, journal_env_flags(JOURNAL_MODE_ROBUST, true), NULL);
+
+ list_t zones;
+ init_list(&zones);
+ ptrnode_t *zone;
+ uint64_t occupied_all = 0;
+ bool first = detailed;
+
+ int ret = journals_walk(&journal_db, add_zone_to_list, &zones);
+ WALK_LIST(zone, zones) {
+ if (ret != KNOT_EOK) {
+ break;
+ } else if (first) {
+ printf(";; <zone name> <occupied KiB> <first serial> <last serial> <full zone>\n");
+ first = false;
+ }
+ ret = list_zone(zone->d, detailed, &journal_db, &occupied_all);
+ }
+
+ knot_lmdb_deinit(&journal_db);
+ ptrlist_deep_free(&zones, NULL);
+
+ if (detailed && ret == KNOT_EOK) {
+ printf(";; Occupied all zones together: %"PRIu64" KiB\n", occupied_all / 1024);
+ }
+ return ret;
+}
+
+int main(int argc, char *argv[])
+{
+ bool justlist = false;
+
+ print_params_t params = {
+ .debug = false,
+ .color = isatty(STDOUT_FILENO),
+ .check = false,
+ .limit = -1,
+ .from_serial = false,
+ };
+
+ struct option opts[] = {
+ { "config", required_argument, NULL, 'c' },
+ { "confdb", required_argument, NULL, 'C' },
+ { "dir", required_argument, NULL, 'D' },
+ { "limit", required_argument, NULL, 'l' },
+ { "serial", required_argument, NULL, 's' },
+ { "zone-list", no_argument, NULL, 'z' },
+ { "check", no_argument, NULL, 'H' },
+ { "debug", no_argument, NULL, 'd' },
+ { "no-color", no_argument, NULL, 'n' },
+ { "mono", no_argument, NULL, 'x' },
+ { "color", no_argument, NULL, 'X' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL }
+ };
+
+ signal_ctx.close_db = &journal_db;
+ signal_init_std();
+
+ int opt = 0;
+ while ((opt = getopt_long(argc, argv, "c:C:D:l:s:zHdnxXhV", opts, NULL)) != -1) {
+ switch (opt) {
+ case 'c':
+ if (util_conf_init_file(optarg) != KNOT_EOK) {
+ goto failure;
+ }
+ break;
+ case 'C':
+ if (util_conf_init_confdb(optarg) != KNOT_EOK) {
+ goto failure;
+ }
+ break;
+ case 'D':
+ if (util_conf_init_justdb("journal-db", optarg) != KNOT_EOK) {
+ goto failure;
+ }
+ break;
+ case 'l':
+ if (str_to_int(optarg, &params.limit, 0, INT_MAX) != KNOT_EOK) {
+ print_help();
+ goto failure;
+ }
+ break;
+ case 's':
+ if (str_to_u32(optarg, &params.serial) != KNOT_EOK) {
+ print_help();
+ goto failure;
+ }
+ params.from_serial = true;
+ break;
+ case 'z':
+ justlist = true;
+ break;
+ case 'H':
+ params.check = true;
+ break;
+ case 'd':
+ params.debug = true;
+ break;
+ case 'n':
+ case 'x':
+ params.color = false;
+ break;
+ case 'X':
+ params.color = true;
+ break;
+ case 'h':
+ print_help();
+ goto success;
+ case 'V':
+ print_version(PROGRAM_NAME);
+ goto success;
+ default:
+ print_help();
+ goto failure;
+ }
+ }
+
+ // Backward compatibility.
+ if ((justlist && (argc - optind > 0)) || (!justlist && (argc - optind > 1))) {
+ WARN2("obsolete parameter specified");
+ if (util_conf_init_justdb("journal-db", argv[optind]) != KNOT_EOK) {
+ goto failure;
+ }
+ optind++;
+ }
+
+ signal_ctx.color = params.color;
+
+ if (util_conf_init_default(true) != KNOT_EOK) {
+ goto failure;
+ }
+
+ char *db = conf_db(conf(), C_JOURNAL_DB);
+
+ if (justlist) {
+ int ret = list_zones(db, params.debug);
+ free(db);
+ switch (ret) {
+ case KNOT_ENOENT:
+ INFO2("No zones in journal DB");
+ // FALLTHROUGH
+ case KNOT_EOK:
+ goto success;
+ case KNOT_ENODB:
+ ERR2("the journal DB does not exist");
+ goto failure;
+ case KNOT_EMALF:
+ ERR2("the journal DB is broken");
+ goto failure;
+ default:
+ ERR2("failed to load zone list (%s)", knot_strerror(ret));
+ goto failure;
+ }
+ } else {
+ if (argc - optind != 1) {
+ print_help();
+ free(db);
+ goto failure;
+ }
+ knot_dname_t *name = knot_dname_from_str_alloc(argv[optind]);
+ knot_dname_to_lower(name);
+
+ int ret = print_journal(db, name, &params);
+ free(name);
+ free(db);
+ switch (ret) {
+ case KNOT_ENOENT:
+ if (params.from_serial) {
+ INFO2("The journal is empty or the serial not present");
+ } else {
+ INFO2("The journal is empty");
+ }
+ break;
+ case KNOT_ENODB:
+ ERR2("the journal DB does not exist");
+ goto failure;
+ case KNOT_EOUTOFZONE:
+ ERR2("the journal DB does not contain the specified zone");
+ goto failure;
+ case KNOT_EOK:
+ break;
+ default:
+ ERR2("failed to load changesets (%s)", knot_strerror(ret));
+ goto failure;
+ }
+ }
+
+success:
+ util_conf_deinit();
+ return EXIT_SUCCESS;
+failure:
+ util_conf_deinit();
+ return EXIT_FAILURE;
+}
diff --git a/src/utils/knotc/commands.c b/src/utils/knotc/commands.c
new file mode 100644
index 0000000..e5cb455
--- /dev/null
+++ b/src/utils/knotc/commands.c
@@ -0,0 +1,1377 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "libknot/libknot.h"
+#include "knot/common/log.h"
+#include "knot/ctl/commands.h"
+#include "knot/conf/conf.h"
+#include "knot/conf/confdb.h"
+#include "knot/conf/module.h"
+#include "knot/conf/tools.h"
+#include "knot/zone/zonefile.h"
+#include "knot/zone/zone-load.h"
+#include "contrib/color.h"
+#include "contrib/macros.h"
+#include "contrib/string.h"
+#include "contrib/strtonum.h"
+#include "contrib/openbsd/strlcat.h"
+#include "utils/knotc/commands.h"
+
+#define CMD_EXIT "exit"
+
+#define CMD_STATUS "status"
+#define CMD_STOP "stop"
+#define CMD_RELOAD "reload"
+#define CMD_STATS "stats"
+
+#define CMD_ZONE_CHECK "zone-check"
+#define CMD_ZONE_STATUS "zone-status"
+#define CMD_ZONE_RELOAD "zone-reload"
+#define CMD_ZONE_REFRESH "zone-refresh"
+#define CMD_ZONE_RETRANSFER "zone-retransfer"
+#define CMD_ZONE_NOTIFY "zone-notify"
+#define CMD_ZONE_FLUSH "zone-flush"
+#define CMD_ZONE_BACKUP "zone-backup"
+#define CMD_ZONE_RESTORE "zone-restore"
+#define CMD_ZONE_SIGN "zone-sign"
+#define CMD_ZONE_KEYS_LOAD "zone-keys-load"
+#define CMD_ZONE_KEY_ROLL "zone-key-rollover"
+#define CMD_ZONE_KSK_SBM "zone-ksk-submitted"
+#define CMD_ZONE_FREEZE "zone-freeze"
+#define CMD_ZONE_THAW "zone-thaw"
+#define CMD_ZONE_XFR_FREEZE "zone-xfr-freeze"
+#define CMD_ZONE_XFR_THAW "zone-xfr-thaw"
+
+#define CMD_ZONE_READ "zone-read"
+#define CMD_ZONE_BEGIN "zone-begin"
+#define CMD_ZONE_COMMIT "zone-commit"
+#define CMD_ZONE_ABORT "zone-abort"
+#define CMD_ZONE_DIFF "zone-diff"
+#define CMD_ZONE_GET "zone-get"
+#define CMD_ZONE_SET "zone-set"
+#define CMD_ZONE_UNSET "zone-unset"
+#define CMD_ZONE_PURGE "zone-purge"
+#define CMD_ZONE_STATS "zone-stats"
+
+#define CMD_CONF_INIT "conf-init"
+#define CMD_CONF_CHECK "conf-check"
+#define CMD_CONF_IMPORT "conf-import"
+#define CMD_CONF_EXPORT "conf-export"
+#define CMD_CONF_LIST "conf-list"
+#define CMD_CONF_READ "conf-read"
+#define CMD_CONF_BEGIN "conf-begin"
+#define CMD_CONF_COMMIT "conf-commit"
+#define CMD_CONF_ABORT "conf-abort"
+#define CMD_CONF_DIFF "conf-diff"
+#define CMD_CONF_GET "conf-get"
+#define CMD_CONF_SET "conf-set"
+#define CMD_CONF_UNSET "conf-unset"
+
+#define CTL_LOG_STR "failed to control"
+
+#define CTL_SEND(type, data) \
+ ret = knot_ctl_send(args->ctl, (type), (data)); \
+ if (ret != KNOT_EOK) { \
+ log_error(CTL_LOG_STR" (%s)", knot_strerror(ret)); \
+ return ret; \
+ }
+
+#define CTL_SEND_DATA CTL_SEND(KNOT_CTL_TYPE_DATA, &data)
+#define CTL_SEND_BLOCK CTL_SEND(KNOT_CTL_TYPE_BLOCK, NULL)
+
+static int check_args(cmd_args_t *args, int min, int max)
+{
+ if (max == 0 && args->argc > 0) {
+ log_error("command doesn't take arguments");
+ return KNOT_EINVAL;
+ } else if (min == max && args->argc != min) {
+ log_error("command requires %i arguments", min);
+ return KNOT_EINVAL;
+ } else if (args->argc < min) {
+ log_error("command requires at least %i arguments", min);
+ return KNOT_EINVAL;
+ } else if (max > 0 && args->argc > max) {
+ log_error("command takes at most %i arguments", max);
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+static int check_conf_args(cmd_args_t *args)
+{
+ // Mask relevant flags.
+ cmd_flag_t flags = args->desc->flags;
+ flags &= CMD_FOPT_ITEM | CMD_FREQ_ITEM | CMD_FOPT_DATA;
+
+ switch (args->argc) {
+ case 0:
+ if (flags == CMD_FNONE || (flags & CMD_FOPT_ITEM)) {
+ return KNOT_EOK;
+ }
+ break;
+ case 1:
+ if (flags & (CMD_FOPT_ITEM | CMD_FREQ_ITEM)) {
+ return KNOT_EOK;
+ }
+ break;
+ default:
+ if (flags != CMD_FNONE) {
+ return KNOT_EOK;
+ }
+ break;
+ }
+
+ log_error("invalid number of arguments");
+
+ return KNOT_EINVAL;
+}
+
+static int get_conf_key(const char *key, knot_ctl_data_t *data)
+{
+ // Get key0.
+ const char *key0 = key;
+
+ // Check for id.
+ char *id = strchr(key, '[');
+ if (id != NULL) {
+ // Separate key0 and id.
+ *id++ = '\0';
+
+ // Check for id end.
+ char *id_end = id;
+ while ((id_end = strchr(id_end, ']')) != NULL) {
+ // Check for escaped character.
+ if (*(id_end - 1) != '\\') {
+ break;
+ }
+ id_end++;
+ }
+
+ // Check for unclosed id.
+ if (id_end == NULL) {
+ log_error("(missing bracket after identifier) %s", id);
+ return KNOT_EINVAL;
+ }
+
+ // Separate id and key1.
+ *id_end = '\0';
+
+ key = id_end + 1;
+
+ // Key1 or nothing must follow.
+ if (*key != '.' && *key != '\0') {
+ log_error("(unexpected token) %s", key);
+ return KNOT_EINVAL;
+ }
+ }
+
+ // Check for key1.
+ char *key1 = strchr(key, '.');
+ if (key1 != NULL) {
+ // Separate key0/id and key1.
+ *key1++ = '\0';
+
+ if (*key1 == '\0') {
+ log_error("(missing item specification)");
+ return KNOT_EINVAL;
+ }
+ }
+
+ (*data)[KNOT_CTL_IDX_SECTION] = key0;
+ (*data)[KNOT_CTL_IDX_ITEM] = key1;
+ (*data)[KNOT_CTL_IDX_ID] = id;
+
+ return KNOT_EOK;
+}
+
+static void format_data(cmd_args_t *args, knot_ctl_type_t data_type,
+ knot_ctl_data_t *data, bool *empty)
+{
+ const char *error = (*data)[KNOT_CTL_IDX_ERROR];
+ const char *flags = (*data)[KNOT_CTL_IDX_FLAGS];
+ const char *key0 = (*data)[KNOT_CTL_IDX_SECTION];
+ const char *key1 = (*data)[KNOT_CTL_IDX_ITEM];
+ const char *id = (*data)[KNOT_CTL_IDX_ID];
+ const char *zone = (*data)[KNOT_CTL_IDX_ZONE];
+ const char *owner = (*data)[KNOT_CTL_IDX_OWNER];
+ const char *ttl = (*data)[KNOT_CTL_IDX_TTL];
+ const char *type = (*data)[KNOT_CTL_IDX_TYPE];
+ const char *value = (*data)[KNOT_CTL_IDX_DATA];
+
+ bool col = false;
+ char status_col[32] = "";
+
+ static bool first_status_item = true;
+
+ const char *sign = NULL;
+ if (ctl_has_flag(flags, CTL_FLAG_DIFF_ADD)) {
+ sign = CTL_FLAG_DIFF_ADD;
+ } else if (ctl_has_flag(flags, CTL_FLAG_DIFF_REM)) {
+ sign = CTL_FLAG_DIFF_REM;
+ }
+
+ switch (args->desc->cmd) {
+ case CTL_STATUS:
+ if (error != NULL) {
+ printf("error: (%s)%s%s", error,
+ (type != NULL) ? " " : "",
+ (type != NULL) ? type : "");
+ } else if (value != NULL) {
+ printf("%s", value);
+ *empty = false;
+ }
+ break;
+ case CTL_STOP:
+ case CTL_RELOAD:
+ case CTL_CONF_BEGIN:
+ case CTL_CONF_ABORT:
+ // Only error message is expected here.
+ if (error != NULL) {
+ printf("error: (%s)", error);
+ }
+ break;
+ case CTL_ZONE_STATUS:
+ if (error == NULL) {
+ col = args->extended ? args->color_force : args->color;
+ }
+ if (!ctl_has_flag(flags, CTL_FLAG_STATUS_EMPTY)) {
+ strlcat(status_col, COL_BOLD(col), sizeof(status_col));
+ }
+ if (ctl_has_flag(flags, CTL_FLAG_STATUS_SLAVE)) {
+ strlcat(status_col, COL_RED(col), sizeof(status_col));
+ } else {
+ strlcat(status_col, COL_GRN(col), sizeof(status_col));
+ }
+ if (ctl_has_flag(flags, CTL_FLAG_STATUS_MEMBER)) {
+ strlcat(status_col, COL_UNDR(col), sizeof(status_col));
+ }
+ // FALLTHROUGH
+ case CTL_ZONE_RELOAD:
+ case CTL_ZONE_REFRESH:
+ case CTL_ZONE_RETRANSFER:
+ case CTL_ZONE_NOTIFY:
+ case CTL_ZONE_FLUSH:
+ case CTL_ZONE_BACKUP:
+ case CTL_ZONE_RESTORE:
+ case CTL_ZONE_SIGN:
+ case CTL_ZONE_KEYS_LOAD:
+ case CTL_ZONE_KEY_ROLL:
+ case CTL_ZONE_KSK_SBM:
+ case CTL_ZONE_FREEZE:
+ case CTL_ZONE_THAW:
+ case CTL_ZONE_BEGIN:
+ case CTL_ZONE_COMMIT:
+ case CTL_ZONE_ABORT:
+ case CTL_ZONE_PURGE:
+ if (data_type == KNOT_CTL_TYPE_DATA) {
+ printf("%s%s%s%s%s%s%s%s%s%s",
+ (!(*empty) ? "\n" : ""),
+ (error != NULL ? "error: " : ""),
+ (zone != NULL ? "[" : ""),
+ (zone != NULL ? status_col : ""),
+ (zone != NULL ? zone : ""),
+ (zone != NULL ? COL_RST(col) : ""),
+ (zone != NULL ? "]" : ""),
+ (error != NULL ? " (" : ""),
+ (error != NULL ? error : ""),
+ (error != NULL ? ")" : ""));
+ *empty = false;
+ }
+ if (args->desc->cmd == CTL_ZONE_STATUS && type != NULL) {
+ if (data_type == KNOT_CTL_TYPE_DATA) {
+ first_status_item = true;
+ }
+ if (!args->extended &&
+ (value == 0 || strcmp(value, STATUS_EMPTY) == 0) &&
+ strcmp(type, "serial") != 0) {
+ return;
+ }
+
+ printf("%s %s: %s%s%s",
+ (first_status_item ? "" : " |"),
+ type, COL_BOLD(col), value, COL_RST(col));
+ first_status_item = false;
+ }
+ break;
+ case CTL_CONF_COMMIT: // Can return a check error context.
+ case CTL_CONF_LIST:
+ case CTL_CONF_READ:
+ case CTL_CONF_DIFF:
+ case CTL_CONF_GET:
+ case CTL_CONF_SET:
+ case CTL_CONF_UNSET:
+ if (data_type == KNOT_CTL_TYPE_DATA) {
+ printf("%s%s%s%s%s%s%s%s%s%s%s%s",
+ (!(*empty) ? "\n" : ""),
+ (error != NULL ? "error: (" : ""),
+ (error != NULL ? error : ""),
+ (error != NULL ? ") " : ""),
+ (sign != NULL ? sign : ""),
+ (key0 != NULL ? key0 : ""),
+ (id != NULL ? "[" : ""),
+ (id != NULL ? id : ""),
+ (id != NULL ? "]" : ""),
+ (key1 != NULL ? "." : ""),
+ (key1 != NULL ? key1 : ""),
+ (value != NULL ? " =" : ""));
+ *empty = false;
+ }
+ if (value != NULL) {
+ printf(" %s", value);
+ }
+ break;
+ case CTL_ZONE_READ:
+ case CTL_ZONE_DIFF:
+ case CTL_ZONE_GET:
+ case CTL_ZONE_SET:
+ case CTL_ZONE_UNSET:
+ printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
+ (!(*empty) ? "\n" : ""),
+ (error != NULL ? "error: (" : ""),
+ (error != NULL ? error : ""),
+ (error != NULL ? ") " : ""),
+ (zone != NULL ? "[" : ""),
+ (zone != NULL ? zone : ""),
+ (zone != NULL ? "] " : ""),
+ (sign != NULL ? sign : ""),
+ (owner != NULL ? owner : ""),
+ (ttl != NULL ? " " : ""),
+ (ttl != NULL ? ttl : ""),
+ (type != NULL ? " " : ""),
+ (type != NULL ? type : ""),
+ (value != NULL ? " " : ""),
+ (value != NULL ? value : ""));
+ *empty = false;
+ break;
+ case CTL_STATS:
+ case CTL_ZONE_STATS:
+ printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
+ (!(*empty) ? "\n" : ""),
+ (error != NULL ? "error: (" : ""),
+ (error != NULL ? error : ""),
+ (error != NULL ? ") " : ""),
+ (zone != NULL ? "[" : ""),
+ (zone != NULL ? zone : ""),
+ (zone != NULL ? "] " : ""),
+ (key0 != NULL ? key0 : ""),
+ (key1 != NULL ? "." : ""),
+ (key1 != NULL ? key1 : ""),
+ (id != NULL ? "[" : ""),
+ (id != NULL ? id : ""),
+ (id != NULL ? "]" : ""),
+ (value != NULL ? " = " : ""),
+ (value != NULL ? value : ""));
+ *empty = false;
+ break;
+ default:
+ assert(0);
+ }
+}
+
+static void format_block(ctl_cmd_t cmd, bool failed, bool empty)
+{
+ switch (cmd) {
+ case CTL_STATUS:
+ printf("%s\n", (failed || !empty) ? "" : "Running");
+ break;
+ case CTL_STOP:
+ printf("%s\n", failed ? "" : "Stopped");
+ break;
+ case CTL_RELOAD:
+ printf("%s\n", failed ? "" : "Reloaded");
+ break;
+ case CTL_CONF_BEGIN:
+ case CTL_CONF_COMMIT:
+ case CTL_CONF_ABORT:
+ case CTL_CONF_SET:
+ case CTL_CONF_UNSET:
+ case CTL_ZONE_RELOAD:
+ case CTL_ZONE_REFRESH:
+ case CTL_ZONE_RETRANSFER:
+ case CTL_ZONE_NOTIFY:
+ case CTL_ZONE_FLUSH:
+ case CTL_ZONE_BACKUP:
+ case CTL_ZONE_RESTORE:
+ case CTL_ZONE_SIGN:
+ case CTL_ZONE_KEYS_LOAD:
+ case CTL_ZONE_KEY_ROLL:
+ case CTL_ZONE_KSK_SBM:
+ case CTL_ZONE_FREEZE:
+ case CTL_ZONE_THAW:
+ case CTL_ZONE_XFR_FREEZE:
+ case CTL_ZONE_XFR_THAW:
+ case CTL_ZONE_BEGIN:
+ case CTL_ZONE_COMMIT:
+ case CTL_ZONE_ABORT:
+ case CTL_ZONE_SET:
+ case CTL_ZONE_UNSET:
+ case CTL_ZONE_PURGE:
+ printf("%s\n", failed ? "" : "OK");
+ break;
+ case CTL_ZONE_STATUS:
+ case CTL_ZONE_READ:
+ case CTL_ZONE_DIFF:
+ case CTL_ZONE_GET:
+ case CTL_CONF_LIST:
+ case CTL_CONF_READ:
+ case CTL_CONF_DIFF:
+ case CTL_CONF_GET:
+ case CTL_ZONE_STATS:
+ case CTL_STATS:
+ printf("%s", empty ? "" : "\n");
+ break;
+ default:
+ assert(0);
+ }
+}
+
+static int ctl_receive(cmd_args_t *args)
+{
+ bool failed = false;
+ bool empty = true;
+
+ while (true) {
+ knot_ctl_type_t type;
+ knot_ctl_data_t data;
+
+ int ret = knot_ctl_receive(args->ctl, &type, &data);
+ if (ret != KNOT_EOK) {
+ log_error(CTL_LOG_STR" (%s)", knot_strerror(ret));
+ return ret;
+ }
+
+ switch (type) {
+ case KNOT_CTL_TYPE_END:
+ log_error(CTL_LOG_STR" (%s)", knot_strerror(KNOT_EMALF));
+ return KNOT_EMALF;
+ case KNOT_CTL_TYPE_BLOCK:
+ format_block(args->desc->cmd, failed, empty);
+ return failed ? KNOT_ERROR : KNOT_EOK;
+ case KNOT_CTL_TYPE_DATA:
+ case KNOT_CTL_TYPE_EXTRA:
+ format_data(args, type, &data, &empty);
+ break;
+ default:
+ assert(0);
+ return KNOT_EINVAL;
+ }
+
+ if (data[KNOT_CTL_IDX_ERROR] != NULL) {
+ failed = true;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+static int cmd_ctl(cmd_args_t *args)
+{
+ int ret = check_args(args, 0, (args->desc->cmd == CTL_STATUS ? 1 : 0));
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ knot_ctl_data_t data = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd),
+ [KNOT_CTL_IDX_FLAGS] = args->flags,
+ [KNOT_CTL_IDX_TYPE] = args->argc > 0 ? args->argv[0] : NULL
+ };
+
+ CTL_SEND_DATA
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+static int set_stats_items(cmd_args_t *args, knot_ctl_data_t *data)
+{
+ int min_args, max_args;
+ switch (args->desc->cmd) {
+ case CTL_STATS: min_args = 0; max_args = 1; break;
+ case CTL_ZONE_STATS: min_args = 1; max_args = 2; break;
+ default:
+ assert(0);
+ return KNOT_EINVAL;
+ }
+
+ // Check the number of arguments.
+ int ret = check_args(args, min_args, max_args);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ int idx = 0;
+
+ // Set ZONE name.
+ if (args->argc > idx && args->desc->cmd == CTL_ZONE_STATS) {
+ if (strcmp(args->argv[idx], "--") != 0) {
+ (*data)[KNOT_CTL_IDX_ZONE] = args->argv[idx];
+ }
+ idx++;
+ }
+
+ if (args->argc > idx) {
+ (*data)[KNOT_CTL_IDX_SECTION] = args->argv[idx];
+
+ char *item = strchr(args->argv[idx], '.');
+ if (item != NULL) {
+ // Separate section and item.
+ *item++ = '\0';
+ (*data)[KNOT_CTL_IDX_ITEM] = item;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+static int cmd_stats_ctl(cmd_args_t *args)
+{
+ knot_ctl_data_t data = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd),
+ [KNOT_CTL_IDX_FLAGS] = args->flags,
+ };
+
+ int ret = set_stats_items(args, &data);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ CTL_SEND_DATA
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+static int zone_exec(cmd_args_t *args, int (*fcn)(const knot_dname_t *, void *),
+ void *data)
+{
+ bool failed = false;
+
+ // Process specified zones.
+ if (args->argc > 0) {
+ knot_dname_storage_t id;
+
+ for (int i = 0; i < args->argc; i++) {
+ if (knot_dname_from_str(id, args->argv[i], sizeof(id)) == NULL) {
+ log_zone_str_error(args->argv[i], "invalid name");
+ failed = true;
+ continue;
+ }
+ knot_dname_to_lower(id);
+
+ if (!conf_rawid_exists(conf(), C_ZONE, id, knot_dname_size(id))) {
+ log_zone_error(id, "%s", knot_strerror(KNOT_ENOZONE));
+ failed = true;
+ continue;
+ }
+
+ if (fcn(id, data) != KNOT_EOK) {
+ failed = true;
+ }
+ }
+ // Process all configured zones.
+ } else {
+ for (conf_iter_t iter = conf_iter(conf(), C_ZONE);
+ iter.code == KNOT_EOK; conf_iter_next(conf(), &iter)) {
+ conf_val_t val = conf_iter_id(conf(), &iter);
+ const knot_dname_t *id = conf_dname(&val);
+
+ if (fcn(id, data) != KNOT_EOK) {
+ failed = true;
+ }
+ }
+ }
+
+ return failed ? KNOT_ERROR : KNOT_EOK;
+}
+
+static int zone_check(const knot_dname_t *dname, void *data)
+{
+ cmd_args_t *args = data;
+
+ conf_val_t load = conf_zone_get(conf(), C_ZONEFILE_LOAD, dname);
+ if (conf_opt(&load) == ZONEFILE_LOAD_NONE) {
+ return KNOT_EOK;
+ }
+
+ zone_contents_t *contents = NULL;
+ conf_val_t mode = conf_zone_get(conf(), C_SEM_CHECKS, dname);
+ int ret = zone_load_contents(conf(), dname, &contents, conf_opt(&mode), args->force);
+ zone_contents_deep_free(contents);
+ if (ret != KNOT_EOK && ret != KNOT_ESEMCHECK) {
+ knot_dname_txt_storage_t name;
+ (void)knot_dname_to_str(name, dname, sizeof(name));
+ log_error("[%s] failed to check zone file (%s)", name, knot_strerror(ret));
+ }
+
+ return ret;
+}
+
+static int cmd_zone_check(cmd_args_t *args)
+{
+ return zone_exec(args, zone_check, args);
+}
+
+static int cmd_zone_key_roll_ctl(cmd_args_t *args)
+{
+ int ret = check_args(args, 2, 2);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ knot_ctl_data_t data = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd),
+ [KNOT_CTL_IDX_FLAGS] = args->flags,
+ [KNOT_CTL_IDX_ZONE] = args->argv[0],
+ [KNOT_CTL_IDX_TYPE] = args->argv[1],
+ };
+
+ CTL_SEND_DATA
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+static int cmd_zone_ctl(cmd_args_t *args)
+{
+ knot_ctl_data_t data = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd),
+ [KNOT_CTL_IDX_FLAGS] = args->flags,
+ };
+
+ // Check the number of arguments.
+ int ret = check_args(args, (args->desc->flags & CMD_FREQ_ZONE) ? 1 : 0, -1);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (args->desc->cmd == CTL_ZONE_PURGE && !args->force) {
+ log_error("force option required!");
+ return KNOT_EDENIED;
+ }
+
+ // Ignore all zones argument.
+ if (args->argc == 1 && strcmp(args->argv[0], "--") == 0) {
+ args->argc = 0;
+ }
+
+ if (args->argc == 0) {
+ CTL_SEND_DATA
+ }
+ for (int i = 0; i < args->argc; i++) {
+ data[KNOT_CTL_IDX_ZONE] = args->argv[i];
+
+ CTL_SEND_DATA
+ }
+
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+#define FILTER_IMPORT_NOPURGE "+nopurge"
+
+typedef struct {
+ const char *name;
+ char id;
+ bool with_data; // Only ONE filter of each filter_desc_t may have data!
+} filter_desc_t;
+
+const filter_desc_t zone_flush_filters[] = {
+ { "+outdir", CTL_FILTER_FLUSH_OUTDIR, true },
+ { NULL },
+};
+
+const filter_desc_t zone_backup_filters[] = {
+ { "+backupdir", CTL_FILTER_BACKUP_OUTDIR, true },
+ { "+zonefile", CTL_FILTER_BACKUP_ZONEFILE, false },
+ { "+nozonefile", CTL_FILTER_BACKUP_NOZONEFILE, false },
+ { "+journal", CTL_FILTER_BACKUP_JOURNAL, false },
+ { "+nojournal", CTL_FILTER_BACKUP_NOJOURNAL, false },
+ { "+timers", CTL_FILTER_BACKUP_TIMERS, false },
+ { "+notimers", CTL_FILTER_BACKUP_NOTIMERS, false },
+ { "+kaspdb", CTL_FILTER_BACKUP_KASPDB, false },
+ { "+nokaspdb", CTL_FILTER_BACKUP_NOKASPDB, false },
+ { "+keysonly", CTL_FILTER_BACKUP_KEYSONLY, false },
+ { "+nokeysonly", CTL_FILTER_BACKUP_NOKEYSONLY, false },
+ { "+catalog", CTL_FILTER_BACKUP_CATALOG, false },
+ { "+nocatalog", CTL_FILTER_BACKUP_NOCATALOG, false },
+ { "+quic", CTL_FILTER_BACKUP_QUIC, false },
+ { "+noquic", CTL_FILTER_BACKUP_NOQUIC, false },
+ { NULL },
+};
+
+const filter_desc_t zone_status_filters[] = {
+ { "+role", CTL_FILTER_STATUS_ROLE },
+ { "+serial", CTL_FILTER_STATUS_SERIAL },
+ { "+transaction", CTL_FILTER_STATUS_TRANSACTION },
+ { "+freeze", CTL_FILTER_STATUS_FREEZE },
+ { "+catalog", CTL_FILTER_STATUS_CATALOG },
+ { "+events", CTL_FILTER_STATUS_EVENTS },
+ { NULL },
+};
+
+const filter_desc_t zone_purge_filters[] = {
+ { "+expire", CTL_FILTER_PURGE_EXPIRE },
+ { "+zonefile", CTL_FILTER_PURGE_ZONEFILE },
+ { "+journal", CTL_FILTER_PURGE_JOURNAL },
+ { "+timers", CTL_FILTER_PURGE_TIMERS },
+ { "+kaspdb", CTL_FILTER_PURGE_KASPDB },
+ { "+catalog", CTL_FILTER_PURGE_CATALOG },
+ { "+orphan", CTL_FILTER_PURGE_ORPHAN },
+ { NULL },
+};
+
+const filter_desc_t null_filter = { NULL };
+
+#define MAX_FILTERS sizeof(zone_backup_filters) / sizeof(filter_desc_t) - 1
+
+static const filter_desc_t *get_filter(ctl_cmd_t cmd, const char *filter_name)
+{
+ const filter_desc_t *fd = NULL;
+ switch (cmd) {
+ case CTL_ZONE_FLUSH:
+ fd = zone_flush_filters;
+ break;
+ case CTL_ZONE_BACKUP:
+ case CTL_ZONE_RESTORE:
+ fd = zone_backup_filters;
+ break;
+ case CTL_ZONE_STATUS:
+ fd = zone_status_filters;
+ break;
+ case CTL_ZONE_PURGE:
+ fd = zone_purge_filters;
+ break;
+ default:
+ return &null_filter;
+ }
+ for (size_t i = 0; fd[i].name != NULL; i++) {
+ if (strcmp(fd[i].name, filter_name) == 0) {
+ return &fd[i];
+ }
+ }
+ return &null_filter;
+}
+
+static int cmd_zone_filter_ctl(cmd_args_t *args)
+{
+ knot_ctl_data_t data = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd),
+ [KNOT_CTL_IDX_FLAGS] = args->flags,
+ };
+
+ if (args->desc->cmd == CTL_ZONE_PURGE && !args->force) {
+ log_error("force option required!");
+ return KNOT_EDENIED;
+ }
+
+ char filter_buff[MAX_FILTERS + 1] = { 0 };
+
+ // First, process the filters.
+ for (int i = 0; i < args->argc; i++) {
+ if (args->argv[i][0] == '+') {
+ if (data[KNOT_CTL_IDX_FILTER] == NULL) {
+ data[KNOT_CTL_IDX_FILTER] = filter_buff;
+ }
+ char filter_id[2] = { get_filter(args->desc->cmd, args->argv[i])->id, 0 };
+ if (filter_id[0] == '\0') {
+ log_error("unknown filter: %s", args->argv[i]);
+ return KNOT_EINVAL;
+ }
+ if (strchr(filter_buff, filter_id[0]) == NULL) {
+ assert(strlen(filter_buff) < MAX_FILTERS);
+ strlcat(filter_buff, filter_id, sizeof(filter_buff));
+ }
+ if (get_filter(args->desc->cmd, args->argv[i])->with_data) {
+ data[KNOT_CTL_IDX_DATA] = args->argv[++i];
+ }
+ }
+ }
+
+ // Second, process zones.
+ int ret;
+ int sentzones = 0;
+ bool twodash = false;
+ for (int i = 0; i < args->argc; i++) {
+ // Skip filters.
+ if (args->argv[i][0] == '+') {
+ if (get_filter(args->desc->cmd, args->argv[i])->with_data) {
+ i++;
+ }
+ continue;
+ }
+
+ if (strcmp(args->argv[i], "--") != 0) {
+ data[KNOT_CTL_IDX_ZONE] = args->argv[i];
+ CTL_SEND_DATA
+ sentzones++;
+ } else {
+ twodash = true;
+ }
+ }
+
+ if ((args->desc->flags & CMD_FREQ_ZONE) && sentzones == 0 && !twodash) {
+ log_error("zone must be specified (or -- for all zones)");
+ return KNOT_EDENIED;
+ }
+
+ if (sentzones == 0) {
+ CTL_SEND_DATA
+ }
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+static int set_rdata(cmd_args_t *args, int pos, char *rdata, size_t rdata_len)
+{
+ rdata[0] = '\0';
+
+ for (int i = pos; i < args->argc; i++) {
+ if (i > pos && strlcat(rdata, " ", rdata_len) >= rdata_len) {
+ return KNOT_ESPACE;
+ }
+ if (strlcat(rdata, args->argv[i], rdata_len) >= rdata_len) {
+ return KNOT_ESPACE;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+static int set_node_items(cmd_args_t *args, knot_ctl_data_t *data, char *rdata,
+ size_t rdata_len)
+{
+ int min_args, max_args;
+ switch (args->desc->cmd) {
+ case CTL_ZONE_READ:
+ case CTL_ZONE_GET: min_args = 1; max_args = 3; break;
+ case CTL_ZONE_DIFF: min_args = 1; max_args = 1; break;
+ case CTL_ZONE_SET: min_args = 3; max_args = -1; break;
+ case CTL_ZONE_UNSET: min_args = 2; max_args = -1; break;
+ default:
+ assert(0);
+ return KNOT_EINVAL;
+ }
+
+ // Check the number of arguments.
+ int ret = check_args(args, min_args, max_args);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ int idx = 0;
+
+ // Set ZONE name.
+ assert(args->argc > idx);
+ if (strcmp(args->argv[idx], "--") != 0) {
+ (*data)[KNOT_CTL_IDX_ZONE] = args->argv[idx];
+ }
+ idx++;
+
+ // Set OWNER name if specified.
+ if (args->argc > idx) {
+ (*data)[KNOT_CTL_IDX_OWNER] = args->argv[idx];
+ idx++;
+ }
+
+ // Set TTL only with an editing operation.
+ if (args->argc > idx) {
+ uint32_t num;
+ uint16_t type;
+ if (knot_rrtype_from_string(args->argv[idx], &type) != 0 &&
+ str_to_u32(args->argv[idx], &num) == KNOT_EOK) {
+ switch (args->desc->cmd) {
+ case CTL_ZONE_SET:
+ case CTL_ZONE_UNSET:
+ (*data)[KNOT_CTL_IDX_TTL] = args->argv[idx];
+ idx++;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ // Set record TYPE if specified.
+ if (args->argc > idx) {
+ (*data)[KNOT_CTL_IDX_TYPE] = args->argv[idx];
+ idx++;
+ }
+
+ // Set record DATA if specified.
+ if (args->argc > idx) {
+ ret = set_rdata(args, idx, rdata, rdata_len);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ (*data)[KNOT_CTL_IDX_DATA] = rdata;
+ }
+
+ return KNOT_EOK;
+}
+
+static int cmd_zone_node_ctl(cmd_args_t *args)
+{
+ knot_ctl_data_t data = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd),
+ [KNOT_CTL_IDX_FLAGS] = args->flags,
+ };
+
+ char rdata[65536]; // Maximum item size in libknot control interface.
+
+ int ret = set_node_items(args, &data, rdata, sizeof(rdata));
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ CTL_SEND_DATA
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+static int cmd_conf_init(cmd_args_t *args)
+{
+ int ret = check_args(args, 0, 0);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = conf_db_check(conf(), &conf()->read_txn);
+ if ((ret >= KNOT_EOK || ret == KNOT_CONF_EVERSION)) {
+ if (ret != KNOT_EOK && !args->force) {
+ log_error("use force option to overwrite the existing "
+ "destination and ensure the server is not running!");
+ return KNOT_EDENIED;
+ }
+
+ ret = conf_import(conf(), "", 0);
+ }
+
+ if (ret == KNOT_EOK) {
+ log_info("OK");
+ } else {
+ log_error("init (%s)", knot_strerror(ret));
+ }
+
+ return ret;
+}
+
+static int conf_check_group(const yp_item_t *group, const uint8_t *id, size_t id_len)
+{
+ knotd_conf_check_extra_t extra = {
+ .conf = conf(),
+ .txn = &conf()->read_txn,
+ .check = true
+ };
+ knotd_conf_check_args_t args = {
+ .id = id,
+ .id_len = id_len,
+ .extra = &extra
+ };
+
+ bool non_empty = false;
+ bool error = false;
+
+ // Check the group sub-items.
+ for (yp_item_t *item = group->sub_items; item->name != NULL; item++) {
+ args.item = item;
+
+ conf_val_t bin;
+ conf_db_get(conf(), &conf()->read_txn, group->name, item->name,
+ id, id_len, &bin);
+ if (bin.code == KNOT_ENOENT) {
+ continue;
+ } else if (bin.code != KNOT_EOK) {
+ log_error("failed to read the configuration DB (%s)",
+ knot_strerror(bin.code));
+ return bin.code;
+ }
+
+ non_empty = true;
+
+ // Check the item value(s).
+ size_t values = conf_val_count(&bin);
+ for (size_t i = 1; i <= values; i++) {
+ conf_val(&bin);
+ args.data = bin.data;
+ args.data_len = bin.len;
+
+ int ret = conf_exec_callbacks(&args);
+ if (ret != KNOT_EOK) {
+ log_error("config, item '%s%s%s%s.%s' (%s)",
+ group->name + 1,
+ (id != NULL ? "[" : ""),
+ (id != NULL ? (const char *)id : ""),
+ (id != NULL ? "]" : ""),
+ item->name + 1,
+ args.err_str);
+ error = true;
+ }
+ if (values > 1) {
+ conf_val_next(&bin);
+ }
+ }
+ }
+
+ // Check the group item itself.
+ if (id != NULL || non_empty) {
+ args.item = group;
+ args.data = NULL;
+ args.data_len = 0;
+
+ int ret = conf_exec_callbacks(&args);
+ if (ret != KNOT_EOK) {
+ log_error("config, section '%s%s%s%s' (%s)",
+ group->name + 1,
+ (id != NULL ? "[" : ""),
+ (id != NULL ? (const char *)id : ""),
+ (id != NULL ? "]" : ""),
+ args.err_str);
+ error = true;
+ }
+ }
+
+ return error ? KNOT_ESEMCHECK : KNOT_EOK;
+}
+
+static int cmd_conf_check(cmd_args_t *args) // Similar to conf_io_check().
+{
+ int ret = check_args(args, 0, 0);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (conf()->filename == NULL) { // Config file already checked.
+ for (yp_item_t *item = conf()->schema; item->name != NULL; item++) {
+ // Skip include item.
+ if (item->type != YP_TGRP) {
+ continue;
+ }
+
+ // Group without identifiers.
+ if (!(item->flags & YP_FMULTI)) {
+ ret = conf_check_group(item, NULL, 0);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ continue;
+ }
+
+ conf_iter_t iter;
+ ret = conf_db_iter_begin(conf(), &conf()->read_txn, item->name, &iter);
+ if (ret == KNOT_ENOENT) {
+ continue;
+ } else if (ret != KNOT_EOK) {
+ log_error("failed to read the configuration DB (%s)",
+ knot_strerror(ret));
+ return ret;
+ }
+
+ while (ret == KNOT_EOK) {
+ const uint8_t *id;
+ size_t id_len;
+ ret = conf_db_iter_id(conf(), &iter, &id, &id_len);
+ if (ret != KNOT_EOK) {
+ conf_db_iter_finish(conf(), &iter);
+ log_error("failed to read the configuration DB (%s)",
+ knot_strerror(ret));
+ return ret;
+ }
+
+ // Check the group with this identifier.
+ ret = conf_check_group(item, id, id_len);
+ if (ret != KNOT_EOK) {
+ conf_db_iter_finish(conf(), &iter);
+ return ret;
+ }
+
+ ret = conf_db_iter_next(conf(), &iter);
+ }
+ if (ret != KNOT_EOF) {
+ log_error("failed to read the configuration DB (%s)",
+ knot_strerror(ret));
+ return ret;
+ }
+ }
+ }
+
+ log_info("Configuration is valid");
+
+ return KNOT_EOK;
+}
+
+static int cmd_conf_import(cmd_args_t *args)
+{
+ int ret = check_args(args, 1, 2);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ import_flag_t flags = IMPORT_FILE;
+ if (args->argc == 2) {
+ const char *filter = args->argv[1];
+ if (strcmp(filter, FILTER_IMPORT_NOPURGE) == 0) {
+ flags |= IMPORT_NO_PURGE;
+ } else {
+ log_error("unknown filter: %s", filter);
+ return KNOT_EINVAL;
+ }
+ }
+
+ ret = conf_db_check(conf(), &conf()->read_txn);
+ if ((ret >= KNOT_EOK || ret == KNOT_CONF_EVERSION)) {
+ if (ret != KNOT_EOK && !args->force) {
+ log_error("use force option to modify/overwrite the existing "
+ "destination and ensure the server is not running!");
+ return KNOT_EDENIED;
+ }
+
+ // Import to a cloned conf to avoid external module conflict.
+ conf_t *new_conf = NULL;
+ ret = conf_clone(&new_conf);
+ if (ret == KNOT_EOK) {
+ yp_schema_purge_dynamic(new_conf->schema);
+ log_debug("loading modules for imported configuration");
+ ret = conf_mod_load_common(new_conf);
+ if (ret == KNOT_EOK) {
+ log_debug("importing confdb from file '%s'", args->argv[0]);
+ ret = conf_import(new_conf, args->argv[0], flags);
+ }
+ conf_free(new_conf);
+ }
+ }
+
+ if (ret == KNOT_EOK) {
+ log_info("OK");
+ } else {
+ log_error("import (%s)", knot_strerror(ret));
+ }
+
+ return ret;
+}
+
+static int cmd_conf_export(cmd_args_t *args)
+{
+ int ret = check_args(args, 0, 1);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // Stdout is the default output file.
+ const char *file_name = NULL;
+ if (args->argc > 0) {
+ file_name = args->argv[0];
+ log_debug("exporting confdb into file '%s'", file_name);
+ }
+
+ ret = conf_export(conf(), file_name, YP_SNONE);
+
+ if (ret == KNOT_EOK) {
+ if (args->argc > 0) {
+ log_info("OK");
+ }
+ } else {
+ log_error("export (%s)", knot_strerror(ret));
+ }
+
+ return ret;
+}
+
+static int cmd_conf_ctl(cmd_args_t *args)
+{
+ // Check the number of arguments.
+ int ret = check_conf_args(args);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ char flags[16] = "";
+ strlcat(flags, args->flags, sizeof(flags));
+ if (args->desc->flags & CMD_FLIST_SCHEMA) {
+ strlcat(flags, CTL_FLAG_LIST_SCHEMA, sizeof(flags));
+ }
+
+ knot_ctl_data_t data = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd),
+ [KNOT_CTL_IDX_FLAGS] = flags,
+ };
+
+ // Send the command without parameters.
+ if (args->argc == 0) {
+ CTL_SEND_DATA
+ // Set the first item argument.
+ } else {
+ ret = get_conf_key(args->argv[0], &data);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // Send if only one argument or item without values.
+ if (args->argc == 1 || !(args->desc->flags & CMD_FOPT_DATA)) {
+ CTL_SEND_DATA
+ }
+ }
+
+ // Send the item values or the other items.
+ for (int i = 1; i < args->argc; i++) {
+ if (args->desc->flags & CMD_FOPT_DATA) {
+ data[KNOT_CTL_IDX_DATA] = args->argv[i];
+ } else {
+ ret = get_conf_key(args->argv[i], &data);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ CTL_SEND_DATA
+ }
+
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+const cmd_desc_t cmd_table[] = {
+ { CMD_EXIT, NULL, CTL_NONE },
+
+ { CMD_STATUS, cmd_ctl, CTL_STATUS, CMD_FOPT_DATA},
+ { CMD_STOP, cmd_ctl, CTL_STOP },
+ { CMD_RELOAD, cmd_ctl, CTL_RELOAD },
+ { CMD_STATS, cmd_stats_ctl, CTL_STATS },
+
+ { CMD_ZONE_CHECK, cmd_zone_check, CTL_NONE, CMD_FOPT_ZONE | CMD_FREAD },
+ { CMD_ZONE_STATUS, cmd_zone_filter_ctl, CTL_ZONE_STATUS, CMD_FOPT_ZONE },
+ { CMD_ZONE_RELOAD, cmd_zone_ctl, CTL_ZONE_RELOAD, CMD_FOPT_ZONE },
+ { CMD_ZONE_REFRESH, cmd_zone_ctl, CTL_ZONE_REFRESH, CMD_FOPT_ZONE },
+ { CMD_ZONE_RETRANSFER, cmd_zone_ctl, CTL_ZONE_RETRANSFER, CMD_FOPT_ZONE },
+ { CMD_ZONE_NOTIFY, cmd_zone_ctl, CTL_ZONE_NOTIFY, CMD_FOPT_ZONE },
+ { CMD_ZONE_FLUSH, cmd_zone_filter_ctl, CTL_ZONE_FLUSH, CMD_FOPT_ZONE },
+ { CMD_ZONE_BACKUP, cmd_zone_filter_ctl, CTL_ZONE_BACKUP, CMD_FOPT_ZONE },
+ { CMD_ZONE_RESTORE, cmd_zone_filter_ctl, CTL_ZONE_RESTORE, CMD_FOPT_ZONE },
+ { CMD_ZONE_SIGN, cmd_zone_ctl, CTL_ZONE_SIGN, CMD_FOPT_ZONE },
+ { CMD_ZONE_KEYS_LOAD, cmd_zone_ctl, CTL_ZONE_KEYS_LOAD, CMD_FOPT_ZONE },
+ { CMD_ZONE_KEY_ROLL, cmd_zone_key_roll_ctl, CTL_ZONE_KEY_ROLL, CMD_FREQ_ZONE },
+ { CMD_ZONE_KSK_SBM, cmd_zone_ctl, CTL_ZONE_KSK_SBM, CMD_FREQ_ZONE | CMD_FOPT_ZONE },
+ { CMD_ZONE_FREEZE, cmd_zone_ctl, CTL_ZONE_FREEZE, CMD_FOPT_ZONE },
+ { CMD_ZONE_THAW, cmd_zone_ctl, CTL_ZONE_THAW, CMD_FOPT_ZONE },
+ { CMD_ZONE_XFR_FREEZE, cmd_zone_ctl, CTL_ZONE_XFR_FREEZE, CMD_FOPT_ZONE },
+ { CMD_ZONE_XFR_THAW, cmd_zone_ctl, CTL_ZONE_XFR_THAW, CMD_FOPT_ZONE },
+
+ { CMD_ZONE_READ, cmd_zone_node_ctl, CTL_ZONE_READ, CMD_FREQ_ZONE },
+ { CMD_ZONE_BEGIN, cmd_zone_ctl, CTL_ZONE_BEGIN, CMD_FREQ_ZONE | CMD_FOPT_ZONE },
+ { CMD_ZONE_COMMIT, cmd_zone_ctl, CTL_ZONE_COMMIT, CMD_FREQ_ZONE | CMD_FOPT_ZONE },
+ { CMD_ZONE_ABORT, cmd_zone_ctl, CTL_ZONE_ABORT, CMD_FREQ_ZONE | CMD_FOPT_ZONE },
+ { CMD_ZONE_DIFF, cmd_zone_node_ctl, CTL_ZONE_DIFF, CMD_FREQ_ZONE },
+ { CMD_ZONE_GET, cmd_zone_node_ctl, CTL_ZONE_GET, CMD_FREQ_ZONE },
+ { CMD_ZONE_SET, cmd_zone_node_ctl, CTL_ZONE_SET, CMD_FREQ_ZONE },
+ { CMD_ZONE_UNSET, cmd_zone_node_ctl, CTL_ZONE_UNSET, CMD_FREQ_ZONE },
+ { CMD_ZONE_PURGE, cmd_zone_filter_ctl, CTL_ZONE_PURGE, CMD_FREQ_ZONE | CMD_FOPT_ZONE },
+ { CMD_ZONE_STATS, cmd_stats_ctl, CTL_ZONE_STATS, CMD_FREQ_ZONE },
+
+ { CMD_CONF_INIT, cmd_conf_init, CTL_NONE, CMD_FWRITE },
+ { CMD_CONF_CHECK, cmd_conf_check, CTL_NONE, CMD_FREAD | CMD_FREQ_MOD },
+ { CMD_CONF_IMPORT, cmd_conf_import, CTL_NONE, CMD_FWRITE | CMD_FOPT_MOD },
+ { CMD_CONF_EXPORT, cmd_conf_export, CTL_NONE, CMD_FREAD | CMD_FOPT_MOD },
+ { CMD_CONF_LIST, cmd_conf_ctl, CTL_CONF_LIST, CMD_FOPT_ITEM | CMD_FLIST_SCHEMA },
+ { CMD_CONF_READ, cmd_conf_ctl, CTL_CONF_READ, CMD_FOPT_ITEM },
+ { CMD_CONF_BEGIN, cmd_conf_ctl, CTL_CONF_BEGIN },
+ { CMD_CONF_COMMIT, cmd_conf_ctl, CTL_CONF_COMMIT },
+ { CMD_CONF_ABORT, cmd_conf_ctl, CTL_CONF_ABORT },
+ { CMD_CONF_DIFF, cmd_conf_ctl, CTL_CONF_DIFF, CMD_FOPT_ITEM | CMD_FREQ_TXN },
+ { CMD_CONF_GET, cmd_conf_ctl, CTL_CONF_GET, CMD_FOPT_ITEM | CMD_FREQ_TXN },
+ { CMD_CONF_SET, cmd_conf_ctl, CTL_CONF_SET, CMD_FREQ_ITEM | CMD_FOPT_DATA | CMD_FREQ_TXN | CMD_FLIST_SCHEMA},
+ { CMD_CONF_UNSET, cmd_conf_ctl, CTL_CONF_UNSET, CMD_FOPT_ITEM | CMD_FOPT_DATA | CMD_FREQ_TXN },
+ { NULL }
+};
+
+static const cmd_help_t cmd_help_table[] = {
+ { CMD_EXIT, "", "Exit interactive mode." },
+ { "", "", "" },
+ { CMD_STATUS, "[<detail>]", "Check if the server is running." },
+ { CMD_STOP, "", "Stop the server if running." },
+ { CMD_RELOAD, "", "Reload the server configuration and modified zones." },
+ { CMD_STATS, "[<module>[.<counter>]]", "Show global statistics counter(s)." },
+ { "", "", "" },
+ { CMD_ZONE_CHECK, "[<zone>...]", "Check if the zone can be loaded. (*)" },
+ { CMD_ZONE_STATUS, "[<zone>...] [<filter>...]", "Show the zone status." },
+ { CMD_ZONE_RELOAD, "[<zone>...]", "Reload a zone from a disk. (#)" },
+ { CMD_ZONE_REFRESH, "[<zone>...]", "Force slave zone refresh. (#)" },
+ { CMD_ZONE_NOTIFY, "[<zone>...]", "Send a NOTIFY message to all configured remotes. (#)" },
+ { CMD_ZONE_RETRANSFER, "[<zone>...]", "Force slave zone retransfer (no serial check). (#)" },
+ { CMD_ZONE_FLUSH, "[<zone>...] [<filter>...]", "Flush zone journal into the zone file. (#)" },
+ { CMD_ZONE_BACKUP, "[<zone>...] [<filter>...] +backupdir <dir>", "Backup zone data and metadata. (#)" },
+ { CMD_ZONE_RESTORE, "[<zone>...] [<filter>...] +backupdir <dir>", "Restore zone data and metadata. (#)" },
+ { CMD_ZONE_SIGN, "[<zone>...]", "Re-sign the automatically signed zone. (#)" },
+ { CMD_ZONE_KEYS_LOAD, "[<zone>...]", "Re-load keys from KASP database, sign the zone. (#)" },
+ { CMD_ZONE_KEY_ROLL, " <zone> ksk|zsk", "Trigger immediate key rollover. (#)" },
+ { CMD_ZONE_KSK_SBM, " <zone>...", "When KSK submission, confirm parent's DS presence. (#)" },
+ { CMD_ZONE_FREEZE, "[<zone>...]", "Temporarily postpone automatic zone-changing events. (#)" },
+ { CMD_ZONE_THAW, "[<zone>...]", "Dismiss zone freeze. (#)" },
+ { CMD_ZONE_XFR_FREEZE, "[<zone>...]", "Temporarily disable outgoing AXFR/IXFR. (#)" },
+ { CMD_ZONE_XFR_THAW, "[<zone>...]", "Dismiss outgoing XFR freeze. (#)" },
+ { "", "", "" },
+ { CMD_ZONE_READ, "<zone> [<owner> [<type>]]", "Get zone data that are currently being presented." },
+ { CMD_ZONE_BEGIN, "<zone>...", "Begin a zone transaction." },
+ { CMD_ZONE_COMMIT, "<zone>...", "Commit the zone transaction." },
+ { CMD_ZONE_ABORT, "<zone>...", "Abort the zone transaction." },
+ { CMD_ZONE_DIFF, "<zone>", "Get zone changes within the transaction." },
+ { CMD_ZONE_GET, "<zone> [<owner> [<type>]]", "Get zone data within the transaction." },
+ { CMD_ZONE_SET, "<zone> <owner> [<ttl>] <type> <rdata>", "Add zone record within the transaction." },
+ { CMD_ZONE_UNSET, "<zone> <owner> [<type> [<rdata>]]", "Remove zone data within the transaction." },
+ { CMD_ZONE_PURGE, "<zone>... [<filter>...]", "Purge zone data, zone file, journal, timers, and KASP data. (#)" },
+ { CMD_ZONE_STATS, "<zone> [<module>[.<counter>]]", "Show zone statistics counter(s)."},
+ { "", "", "" },
+ { CMD_CONF_INIT, "", "Initialize the confdb. (*)" },
+ { CMD_CONF_CHECK, "", "Check the server configuration. (*)" },
+ { CMD_CONF_IMPORT, " <filename> [+nopurge]", "Import a config file into the confdb. (*)" },
+ { CMD_CONF_EXPORT, "[<filename>]", "Export the confdb into a config file or stdout. (*)" },
+ { CMD_CONF_LIST, "[<item>...]", "List the confdb sections or section items." },
+ { CMD_CONF_READ, "[<item>...]", "Get the item from the active confdb." },
+ { CMD_CONF_BEGIN, "", "Begin a writing confdb transaction." },
+ { CMD_CONF_COMMIT, "", "Commit the confdb transaction." },
+ { CMD_CONF_ABORT, "", "Rollback the confdb transaction." },
+ { CMD_CONF_DIFF, "[<item>...]", "Get the item difference within the transaction." },
+ { CMD_CONF_GET, "[<item>...]", "Get the item data within the transaction." },
+ { CMD_CONF_SET, " <item> [<data>...]", "Set the item data within the transaction." },
+ { CMD_CONF_UNSET, "[<item>] [<data>...]", "Unset the item data within the transaction." },
+ { NULL }
+};
+
+void print_commands(void)
+{
+ printf("\nActions:\n");
+
+ for (const cmd_help_t *cmd = cmd_help_table; cmd->name != NULL; cmd++) {
+ printf(" %-18s %-38s %s\n", cmd->name, cmd->params, cmd->desc);
+ }
+
+ printf("\n"
+ "Note:\n"
+ " Use @ owner to denote the zone name.\n"
+ " Empty or '--' <zone> parameter means all zones or all zones with a transaction.\n"
+ " Type <item> parameter in the form of <section>[<identifier>].<name>.\n"
+ " (*) indicates a local operation which requires a configuration.\n"
+ " (#) indicates an optionally blocking operation.\n"
+ " The '-b' and '-f' options can be placed right after the command name.\n");
+}
diff --git a/src/utils/knotc/commands.h b/src/utils/knotc/commands.h
new file mode 100644
index 0000000..22c3035
--- /dev/null
+++ b/src/utils/knotc/commands.h
@@ -0,0 +1,74 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "libknot/control/control.h"
+#include "knot/ctl/commands.h"
+
+/*! \brief Command condition flags. */
+typedef enum {
+ CMD_FNONE = 0, /*!< Empty flag. */
+ CMD_FREAD = 1 << 0, /*!< Required read access to config or confdb. */
+ CMD_FWRITE = 1 << 1, /*!< Required write access to confdb. */
+ CMD_FOPT_ITEM = 1 << 2, /*!< Optional item argument. */
+ CMD_FREQ_ITEM = 1 << 3, /*!< Required item argument. */
+ CMD_FOPT_DATA = 1 << 4, /*!< Optional item data argument. */
+ CMD_FOPT_ZONE = 1 << 5, /*!< Optional zone name argument. */
+ CMD_FREQ_ZONE = 1 << 6, /*!< Required zone name argument. */
+ CMD_FREQ_TXN = 1 << 7, /*!< Required open confdb transaction. */
+ CMD_FOPT_MOD = 1 << 8, /*!< Optional configured modules dependency. */
+ CMD_FREQ_MOD = 1 << 9, /*!< Required configured modules dependency. */
+ CMD_FLIST_SCHEMA = 1 << 10, /*!< List schema or possible option values. */
+} cmd_flag_t;
+
+struct cmd_desc;
+typedef struct cmd_desc cmd_desc_t;
+
+/*! \brief Command callback arguments. */
+typedef struct {
+ const cmd_desc_t *desc;
+ knot_ctl_t *ctl;
+ int argc;
+ const char **argv;
+ char flags[4];
+ bool force;
+ bool extended;
+ bool color;
+ bool color_force;
+ bool blocking;
+} cmd_args_t;
+
+/*! \brief Command callback description. */
+struct cmd_desc {
+ const char *name;
+ int (*fcn)(cmd_args_t *);
+ ctl_cmd_t cmd;
+ cmd_flag_t flags;
+};
+
+/*! \brief Command description. */
+typedef struct {
+ const char *name;
+ const char *params;
+ const char *desc;
+} cmd_help_t;
+
+/*! \brief Table of commands. */
+extern const cmd_desc_t cmd_table[];
+
+/*! \brief Prints commands help. */
+void print_commands(void);
diff --git a/src/utils/knotc/interactive.c b/src/utils/knotc/interactive.c
new file mode 100644
index 0000000..a03b8d3
--- /dev/null
+++ b/src/utils/knotc/interactive.c
@@ -0,0 +1,450 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <histedit.h>
+
+#include "knot/common/log.h"
+#include "utils/common/lookup.h"
+#include "utils/knotc/interactive.h"
+#include "utils/knotc/commands.h"
+#include "contrib/openbsd/strlcat.h"
+#include "contrib/string.h"
+
+#define PROGRAM_NAME "knotc"
+#define HISTORY_FILE ".knotc_history"
+
+extern params_t params;
+
+typedef struct {
+ const char **args;
+ int count;
+ bool dname;
+} dup_check_ctx_t;
+
+static void cmds_lookup(EditLine *el, const char *str, size_t str_len)
+{
+ lookup_t lookup;
+ int ret = lookup_init(&lookup);
+ if (ret != KNOT_EOK) {
+ return;
+ }
+
+ // Fill the lookup with command names.
+ for (const cmd_desc_t *desc = cmd_table; desc->name != NULL; desc++) {
+ ret = lookup_insert(&lookup, desc->name, NULL);
+ if (ret != KNOT_EOK) {
+ goto cmds_lookup_finish;
+ }
+ }
+
+ (void)lookup_complete(&lookup, str, str_len, el, true);
+
+cmds_lookup_finish:
+ lookup_deinit(&lookup);
+}
+
+static void remove_duplicates(lookup_t *lookup, dup_check_ctx_t *check_ctx)
+{
+ if (check_ctx == NULL) {
+ return;
+ }
+
+ knot_dname_txt_storage_t dname = "";
+ for (int i = 0; i < check_ctx->count; i++) {
+ const char *arg = (check_ctx->args)[i];
+ size_t len = strlen(arg);
+ if (check_ctx->dname && len > 1 && arg[len - 1] != '.') {
+ strlcat(dname, arg, sizeof(dname));
+ strlcat(dname, ".", sizeof(dname));
+ arg = dname;
+ }
+ (void)lookup_remove(lookup, arg);
+ }
+}
+
+static void local_zones_lookup(EditLine *el, const char *str, size_t str_len,
+ dup_check_ctx_t *check_ctx)
+{
+ lookup_t lookup;
+ int ret = lookup_init(&lookup);
+ if (ret != KNOT_EOK) {
+ return;
+ }
+
+ knot_dname_txt_storage_t buff;
+
+ // Fill the lookup with local zone names.
+ for (conf_iter_t iter = conf_iter(conf(), C_ZONE);
+ iter.code == KNOT_EOK; conf_iter_next(conf(), &iter)) {
+ conf_val_t val = conf_iter_id(conf(), &iter);
+ char *name = knot_dname_to_str(buff, conf_dname(&val), sizeof(buff));
+
+ ret = lookup_insert(&lookup, name, NULL);
+ if (ret != KNOT_EOK) {
+ conf_iter_finish(conf(), &iter);
+ goto local_zones_lookup_finish;
+ }
+ }
+
+ remove_duplicates(&lookup, check_ctx);
+ (void)lookup_complete(&lookup, str, str_len, el, true);
+
+local_zones_lookup_finish:
+ lookup_deinit(&lookup);
+}
+
+static void list_separators(EditLine *el, const char *separators)
+{
+ lookup_t lookup;
+ if (lookup_init(&lookup) != KNOT_EOK) {
+ return;
+ }
+
+ size_t count = strlen(separators);
+ for (int i = 0; i < count; i++) {
+ char sep[2] = { separators[i] };
+ (void)lookup_insert(&lookup, sep, NULL);
+ }
+ (void)lookup_complete(&lookup, "", 0, el, false);
+
+ lookup_deinit(&lookup);
+}
+
+static bool rmt_lookup(EditLine *el, const char *str, size_t str_len,
+ const char *section, const char *item, const char *id,
+ dup_check_ctx_t *check_ctx, bool add_space, const char *flags,
+ knot_ctl_idx_t idx)
+{
+ const cmd_desc_t *desc = cmd_table;
+ while (desc->name != NULL && desc->cmd != CTL_CONF_LIST) {
+ desc++;
+ }
+ assert(desc->name != NULL);
+
+ knot_ctl_data_t query = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(desc->cmd),
+ [KNOT_CTL_IDX_SECTION] = section,
+ [KNOT_CTL_IDX_ITEM] = item,
+ [KNOT_CTL_IDX_ID] = id,
+ [KNOT_CTL_IDX_FLAGS] = flags
+ };
+
+ lookup_t lookup;
+ knot_ctl_t *ctl = NULL;
+ bool found = false;
+
+ if (set_ctl(&ctl, params.socket, DEFAULT_CTL_TIMEOUT_MS, desc) != KNOT_EOK ||
+ knot_ctl_send(ctl, KNOT_CTL_TYPE_DATA, &query) != KNOT_EOK ||
+ knot_ctl_send(ctl, KNOT_CTL_TYPE_BLOCK, NULL) != KNOT_EOK ||
+ lookup_init(&lookup) != KNOT_EOK) {
+ unset_ctl(ctl);
+ return found;
+ }
+
+ while (true) {
+ knot_ctl_type_t type;
+ knot_ctl_data_t reply;
+
+ if (knot_ctl_receive(ctl, &type, &reply) != KNOT_EOK) {
+ goto rmt_lookup_finish;
+ }
+
+ if (type != KNOT_CTL_TYPE_DATA && type != KNOT_CTL_TYPE_EXTRA) {
+ break;
+ }
+
+ const char *error = reply[KNOT_CTL_IDX_ERROR];
+ if (error != NULL) {
+ printf("\nnotice: (%s)\n", error);
+ goto rmt_lookup_finish;
+ }
+
+ // Insert the received name into the lookup.
+ if (lookup_insert(&lookup, reply[idx], NULL) != KNOT_EOK) {
+ goto rmt_lookup_finish;
+ }
+ }
+
+ remove_duplicates(&lookup, check_ctx);
+ if (lookup_complete(&lookup, str, str_len, el, add_space) == KNOT_EOK &&
+ str != NULL && strcmp(lookup.found.key, str) == 0) {
+ found = true;
+ }
+
+rmt_lookup_finish:
+ lookup_deinit(&lookup);
+ unset_ctl(ctl);
+
+ return found;
+}
+
+static bool id_lookup(EditLine *el, const char *str, size_t str_len,
+ const char *section, const cmd_desc_t *cmd_desc,
+ dup_check_ctx_t *ctx, bool add_space, bool zones)
+{
+ char flags[4] = "";
+ if (zones) {
+ strlcat(flags, CTL_FLAG_LIST_ZONES, sizeof(flags));
+ } else if (cmd_desc->flags & CMD_FREQ_TXN) {
+ strlcat(flags, CTL_FLAG_LIST_TXN, sizeof(flags));
+ }
+
+ return rmt_lookup(el, str, str_len, section, NULL, NULL, ctx, add_space,
+ flags, KNOT_CTL_IDX_ID);
+}
+
+static void val_lookup(EditLine *el, const char *str, size_t str_len,
+ const char *section, const char *item, const char *id,
+ dup_check_ctx_t *ctx, bool list_schema)
+{
+ char flags[4] = CTL_FLAG_LIST_TXN;
+ if (list_schema) {
+ strlcat(flags, CTL_FLAG_LIST_SCHEMA, sizeof(flags));
+ }
+
+ (void)rmt_lookup(el, str, str_len, section, item, id, ctx, true,
+ flags, KNOT_CTL_IDX_DATA);
+}
+
+static bool list_lookup(EditLine *el, const char *str, const char *section)
+{
+ const char *flags = CTL_FLAG_LIST_SCHEMA;
+ knot_ctl_idx_t idx = (section == NULL) ? KNOT_CTL_IDX_SECTION : KNOT_CTL_IDX_ITEM;
+
+ return rmt_lookup(el, str, strlen(str), section, NULL, NULL, NULL,
+ section != NULL, flags, idx);
+}
+
+static void item_lookup(EditLine *el, const char *str, const cmd_desc_t *cmd_desc)
+{
+ // List all sections.
+ if (str == NULL) {
+ (void)list_lookup(el, "", NULL);
+ return;
+ }
+
+ // Check for id specification.
+ char *id = (strchr(str, '['));
+ if (id != NULL) {
+ char *section = strndup(str, id - str);
+
+ // Check for completed id specification.
+ char *id_stop = (strchr(id, ']'));
+ if (id_stop != NULL) {
+ // Complete the item name.
+ if (*(id_stop + 1) == '.') {
+ (void)list_lookup(el, id_stop + 2, section);
+ } else {
+ list_separators(el, ".");
+ }
+ } else {
+ // Complete the section id.
+ if (id_lookup(el, id + 1, strlen(id + 1), section, cmd_desc,
+ NULL, false, false)) {
+ list_separators(el, "]");
+ }
+ }
+
+ free(section);
+ } else {
+ // Check for item specification.
+ char *dot = (strchr(str, '.'));
+ if (dot != NULL) {
+ // Complete the item name.
+ char *section = strndup(str, dot - str);
+ (void)list_lookup(el, dot + 1, section);
+ free(section);
+ } else {
+ // Complete the section name.
+ if (list_lookup(el, str, NULL)) {
+ list_separators(el, "[.");
+ }
+ }
+ }
+}
+
+static unsigned char complete(EditLine *el, int ch)
+{
+ int argc, token, pos;
+ const char **argv;
+
+ const LineInfo *li = el_line(el);
+ Tokenizer *tok = tok_init(NULL);
+
+ // Parse the line.
+ int ret = tok_line(tok, li, &argc, &argv, &token, &pos);
+ if (ret != 0) {
+ goto complete_exit;
+ }
+
+ // Show possible commands.
+ if (argc == 0) {
+ print_commands();
+ goto complete_exit;
+ }
+
+ // Complete the command name.
+ if (token == 0) {
+ cmds_lookup(el, argv[0], pos);
+ goto complete_exit;
+ }
+
+ // Find the command descriptor.
+ const cmd_desc_t *desc = cmd_table;
+ while (desc->name != NULL && strcmp(desc->name, argv[0]) != 0) {
+ desc++;
+ }
+ if (desc->name == NULL) {
+ goto complete_exit;
+ }
+
+ // Finish if a command with no or unsupported arguments.
+ if (desc->flags == CMD_FNONE || desc->flags == CMD_FREAD ||
+ desc->flags == CMD_FWRITE) {
+ goto complete_exit;
+ }
+
+ ret = set_config(desc, &params);
+ if (ret != KNOT_EOK) {
+ goto complete_exit;
+ }
+
+ // Complete the zone name.
+ if (desc->flags & (CMD_FREQ_ZONE | CMD_FOPT_ZONE)) {
+ if (token > 1 && !(desc->flags & CMD_FOPT_ZONE)) {
+ goto complete_exit;
+ }
+
+ dup_check_ctx_t ctx = { &argv[1], token - 1, true };
+ if (desc->flags & CMD_FREAD) {
+ local_zones_lookup(el, argv[token], pos, &ctx);
+ } else {
+ id_lookup(el, argv[token], pos, "zone", desc, &ctx, true, true);
+ }
+ goto complete_exit;
+ // Complete the section/id/item name or item value.
+ } else if (desc->flags & (CMD_FOPT_ITEM | CMD_FREQ_ITEM)) {
+ if (token == 1) {
+ item_lookup(el, argv[1], desc);
+ } else if (desc->flags & CMD_FOPT_DATA) {
+ char section[YP_MAX_TXT_KEY_LEN + 1] = "";
+ char item[YP_MAX_TXT_KEY_LEN + 1] = "";
+ char id[KNOT_DNAME_TXT_MAXLEN + 1] = "";
+
+ assert(YP_MAX_TXT_KEY_LEN == 127);
+ assert(KNOT_DNAME_TXT_MAXLEN == 1004);
+ if (sscanf(argv[1], "%127[^[][%1004[^]]].%127s", section, id, item) == 3 ||
+ sscanf(argv[1], "%127[^.].%127s", section, item) == 2) {
+ dup_check_ctx_t ctx = { &argv[2], token - 2 };
+ val_lookup(el, argv[token], pos, section, item, id,
+ &ctx, desc->flags & CMD_FLIST_SCHEMA);
+ }
+ }
+ goto complete_exit;
+ }
+
+complete_exit:
+ conf_update(NULL, CONF_UPD_FNONE);
+ tok_reset(tok);
+ tok_end(tok);
+
+ return CC_REDISPLAY;
+}
+
+static char *prompt(EditLine *el)
+{
+ return PROGRAM_NAME"> ";
+}
+
+int interactive_loop(params_t *process_params)
+{
+ char *hist_file = NULL;
+ const char *home = getenv("HOME");
+ if (home != NULL) {
+ hist_file = sprintf_alloc("%s/%s", home, HISTORY_FILE);
+ }
+ if (hist_file == NULL) {
+ log_notice("failed to get home directory");
+ }
+
+ EditLine *el = el_init(PROGRAM_NAME, stdin, stdout, stderr);
+ if (el == NULL) {
+ log_error("interactive mode not available");
+ free(hist_file);
+ return KNOT_ERROR;
+ }
+
+ History *hist = history_init();
+ if (hist == NULL) {
+ log_error("interactive mode not available");
+ el_end(el);
+ free(hist_file);
+ return KNOT_ERROR;
+ }
+
+ HistEvent hev = { 0 };
+ history(hist, &hev, H_SETSIZE, 1000);
+ history(hist, &hev, H_SETUNIQUE, 1);
+ el_set(el, EL_HIST, history, hist);
+ history(hist, &hev, H_LOAD, hist_file);
+
+ el_set(el, EL_TERMINAL, NULL);
+ el_set(el, EL_EDITOR, "emacs");
+ el_set(el, EL_PROMPT, prompt);
+ el_set(el, EL_SIGNAL, 1);
+ el_source(el, NULL);
+
+ // Warning: these two el_sets()'s always leak -- in libedit2 library!
+ // For more details see this commit's message.
+ el_set(el, EL_ADDFN, PROGRAM_NAME"-complete",
+ "Perform "PROGRAM_NAME" completion.", complete);
+ el_set(el, EL_BIND, "^I", PROGRAM_NAME"-complete", NULL);
+
+ int count;
+ const char *line;
+ while ((line = el_gets(el, &count)) != NULL && count > 0) {
+ Tokenizer *tok = tok_init(NULL);
+
+ // Tokenize the current line.
+ int argc;
+ const char **argv;
+ const LineInfo *li = el_line(el);
+ int ret = tok_line(tok, li, &argc, &argv, NULL, NULL);
+ if (ret == 0 && argc != 0) {
+ history(hist, &hev, H_ENTER, line);
+ history(hist, &hev, H_SAVE, hist_file);
+
+ // Process the command.
+ ret = process_cmd(argc, argv, process_params);
+ }
+
+ tok_reset(tok);
+ tok_end(tok);
+
+ // Check for the exit command.
+ if (ret == KNOT_CTL_ESTOP) {
+ break;
+ }
+ }
+
+ history_end(hist);
+ free(hist_file);
+
+ el_end(el);
+
+ return KNOT_EOK;
+}
diff --git a/src/utils/knotc/interactive.h b/src/utils/knotc/interactive.h
new file mode 100644
index 0000000..59690c7
--- /dev/null
+++ b/src/utils/knotc/interactive.h
@@ -0,0 +1,26 @@
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "utils/knotc/process.h"
+
+/*!
+ * Executes an interactive processing loop.
+ *
+ * \param[in] params Utility parameters.
+ */
+int interactive_loop(params_t *params);
diff --git a/src/utils/knotc/main.c b/src/utils/knotc/main.c
new file mode 100644
index 0000000..dad3671
--- /dev/null
+++ b/src/utils/knotc/main.c
@@ -0,0 +1,181 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <getopt.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "contrib/strtonum.h"
+#include "knot/common/log.h"
+#include "utils/common/params.h"
+#include "utils/common/signal.h"
+#include "utils/knotc/commands.h"
+#include "utils/knotc/interactive.h"
+#include "utils/knotc/process.h"
+
+#define PROGRAM_NAME "knotc"
+#define SPACE " "
+
+signal_ctx_t signal_ctx = { 0 }; // global, needed by signal handler
+
+static void print_help(void)
+{
+ printf("Usage: %s [-c | -C <path>] [options] <action>\n"
+ "\n"
+ "Config options:\n"
+ " -c, --config <file> "SPACE"Use a textual configuration file.\n"
+ " "SPACE" (default %s)\n"
+ " -C, --confdb <dir> "SPACE"Use a binary configuration database directory.\n"
+ " "SPACE" (default %s)\n"
+ "Options:\n"
+ " -m, --max-conf-size <MiB>"SPACE"Set maximum size of the configuration database (max 10000 MiB).\n"
+ " "SPACE" (default %d MiB)\n"
+ " -s, --socket <path> "SPACE"Use a control UNIX socket path.\n"
+ " "SPACE" (default %s)\n"
+ " -t, --timeout <sec> "SPACE"Use a control socket timeout (max 86400 seconds).\n"
+ " "SPACE" (default %u seconds)\n"
+ " -b, --blocking "SPACE"Zone event trigger commands wait until the event is finished.\n"
+ " -e, --extended "SPACE"Show extended output.\n"
+ " -f, --force "SPACE"Forced operation. Overrides some checks.\n"
+ " -x, --mono "SPACE"Don't color the output.\n"
+ " -X, --color "SPACE"Force output colorization.\n"
+ " -v, --verbose "SPACE"Enable debug output.\n"
+ " -h, --help "SPACE"Print the program help.\n"
+ " -V, --version "SPACE"Print the program version.\n",
+ PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR,
+ CONF_MAPSIZE, RUN_DIR "/knot.sock", DEFAULT_CTL_TIMEOUT_MS / 1000);
+
+ print_commands();
+}
+
+params_t params = {
+ .max_conf_size = (size_t)CONF_MAPSIZE * 1024 * 1024,
+ .timeout = -1
+};
+
+int main(int argc, char **argv)
+{
+ /* Long options. */
+ struct option opts[] = {
+ { "config", required_argument, NULL, 'c' },
+ { "confdb", required_argument, NULL, 'C' },
+ { "max-conf-size", required_argument, NULL, 'm' },
+ { "socket", required_argument, NULL, 's' },
+ { "timeout", required_argument, NULL, 't' },
+ { "blocking", no_argument, NULL, 'b' },
+ { "extended", no_argument, NULL, 'e' },
+ { "force", no_argument, NULL, 'f' },
+ { "mono", no_argument, NULL, 'x' },
+ { "color", no_argument, NULL, 'X' },
+ { "verbose", no_argument, NULL, 'v' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL }
+ };
+
+ /* Set the time zone. */
+ tzset();
+
+ /* Inititalize the termination signals handler. */
+ signal_init_std();
+
+ params.color = isatty(STDOUT_FILENO);
+ params.color_force = false;
+
+ /* Parse command line arguments */
+ int opt = 0;
+ while ((opt = getopt_long(argc, argv, "+c:C:m:s:t:befxXvhV", opts, NULL)) != -1) {
+ switch (opt) {
+ case 'c':
+ params.orig_config = optarg;
+ break;
+ case 'C':
+ params.orig_confdb = optarg;
+ break;
+ case 'm':
+ if (str_to_size(optarg, &params.max_conf_size, 1, 10000) != KNOT_EOK) {
+ print_help();
+ return EXIT_FAILURE;
+ }
+ /* Convert to bytes. */
+ params.max_conf_size *= 1024 * 1024;
+ break;
+ case 's':
+ params.socket = optarg;
+ break;
+ case 't':
+ if (str_to_int(optarg, &params.timeout, 0, 86400) != KNOT_EOK) {
+ print_help();
+ return EXIT_FAILURE;
+ }
+ /* Convert to milliseconds. */
+ params.timeout *= 1000;
+ break;
+ case 'b':
+ params.blocking = true;
+ break;
+ case 'e':
+ params.extended = true;
+ break;
+ case 'f':
+ params.force = true;
+ break;
+ case 'v':
+ params.verbose = true;
+ break;
+ case 'x':
+ params.color = false;
+ break;
+ case 'X':
+ params.color = true;
+ params.color_force = true;
+ break;
+ case 'h':
+ print_help();
+ return EXIT_SUCCESS;
+ case 'V':
+ print_version(PROGRAM_NAME);
+ return EXIT_SUCCESS;
+ default:
+ print_help();
+ return EXIT_FAILURE;
+ }
+ }
+
+ signal_ctx.color = params.color;
+
+ /* Set up simplified logging just to stdout/stderr. */
+ log_init();
+ log_levels_set(LOG_TARGET_STDOUT, LOG_SOURCE_ANY,
+ LOG_MASK(LOG_INFO) | LOG_MASK(LOG_NOTICE));
+ log_levels_set(LOG_TARGET_STDERR, LOG_SOURCE_ANY, LOG_UPTO(LOG_WARNING));
+ log_levels_set(LOG_TARGET_SYSLOG, LOG_SOURCE_ANY, 0);
+ log_flag_set(LOG_FLAG_NOTIMESTAMP | LOG_FLAG_NOINFO);
+ if (params.verbose) {
+ log_levels_add(LOG_TARGET_STDOUT, LOG_SOURCE_ANY, LOG_MASK(LOG_DEBUG));
+ }
+
+ int ret;
+ if (argc - optind < 1) {
+ ret = interactive_loop(&params);
+ } else {
+ ret = process_cmd(argc - optind, (const char **)argv + optind, &params);
+ }
+
+ log_close();
+
+ return (ret == KNOT_EOK) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/src/utils/knotc/process.c b/src/utils/knotc/process.c
new file mode 100644
index 0000000..5bfc07c
--- /dev/null
+++ b/src/utils/knotc/process.c
@@ -0,0 +1,291 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <sys/stat.h>
+
+#include "contrib/openbsd/strlcat.h"
+#include "knot/conf/conf.h"
+#include "knot/common/log.h"
+#include "utils/knotc/commands.h"
+#include "utils/knotc/process.h"
+
+static const cmd_desc_t *get_cmd_desc(const char *command)
+{
+ /* Find requested command. */
+ const cmd_desc_t *desc = cmd_table;
+ while (desc->name != NULL) {
+ if (strcmp(desc->name, command) == 0) {
+ break;
+ }
+ desc++;
+ }
+ if (desc->name == NULL) {
+ log_error("invalid command '%s'", command);
+ return NULL;
+ }
+
+ return desc;
+}
+
+static bool get_cmd_force_flag(const char *arg)
+{
+ if (strcmp(arg, "-f") == 0 || strcmp(arg, "--force") == 0) {
+ return true;
+ }
+ return false;
+}
+
+static bool get_cmd_blocking_flag(const char *arg)
+{
+ if (strcmp(arg, "-b") == 0 || strcmp(arg, "--blocking") == 0) {
+ return true;
+ }
+ return false;
+}
+
+int set_config(const cmd_desc_t *desc, params_t *params)
+{
+ /* Reset the configuration paths (needed in the interactive mode). */
+ params->config = params->orig_config;
+ params->confdb = params->orig_confdb;
+
+ if (params->config != NULL && params->confdb != NULL) {
+ log_error("ambiguous configuration source");
+ return KNOT_EINVAL;
+ }
+
+ /* Mask relevant flags. */
+ cmd_flag_t flags = desc->flags & (CMD_FREAD | CMD_FWRITE);
+ cmd_flag_t mod_flags = desc->flags & (CMD_FOPT_MOD | CMD_FREQ_MOD);
+
+ /* Choose the optimal config source. */
+ struct stat st;
+ bool import = false;
+ if (flags == CMD_FNONE && params->socket != NULL) {
+ /* Control operation, known socket, skip configuration. */
+ return KNOT_EOK;
+ } else if (params->confdb != NULL) {
+ import = false;
+ } else if (flags == CMD_FWRITE) {
+ import = false;
+ params->confdb = CONF_DEFAULT_DBDIR;
+ } else if (params->config != NULL){
+ import = true;
+ } else if (conf_db_exists(CONF_DEFAULT_DBDIR)) {
+ import = false;
+ params->confdb = CONF_DEFAULT_DBDIR;
+ } else if (stat(CONF_DEFAULT_FILE, &st) == 0) {
+ import = true;
+ params->config = CONF_DEFAULT_FILE;
+ } else if (flags != CMD_FNONE) {
+ log_error("no configuration source available");
+ return KNOT_EINVAL;
+ }
+
+ const char *src = import ? params->config : params->confdb;
+ log_debug("%s '%s'", import ? "config" : "confdb",
+ (src != NULL) ? src : "empty");
+
+ /* Prepare config flags. */
+ conf_flag_t conf_flags = CONF_FNOHOSTNAME;
+ if (params->confdb != NULL && !(flags & CMD_FWRITE)) {
+ conf_flags |= CONF_FREADONLY;
+ }
+ if (import || mod_flags & CMD_FOPT_MOD) {
+ conf_flags |= CONF_FOPTMODULES;
+ } else if (mod_flags & CMD_FREQ_MOD) {
+ conf_flags |= CONF_FREQMODULES;
+ }
+
+ /* Open confdb. */
+ conf_t *new_conf = NULL;
+ int ret = conf_new(&new_conf, conf_schema, params->confdb,
+ params->max_conf_size, conf_flags);
+ if (ret != KNOT_EOK) {
+ log_error("failed to open configuration database '%s' (%s)",
+ (params->confdb != NULL) ? params->confdb : "",
+ knot_strerror(ret));
+ return ret;
+ }
+
+ /* Import the config file. */
+ if (import) {
+ ret = conf_import(new_conf, params->config, IMPORT_FILE);
+ if (ret != KNOT_EOK) {
+ log_error("failed to load configuration file '%s' (%s)",
+ params->config, knot_strerror(ret));
+ conf_free(new_conf);
+ return ret;
+ }
+ }
+
+ /* Update to the new config. */
+ conf_update(new_conf, CONF_UPD_FNONE);
+
+ return KNOT_EOK;
+}
+
+int set_ctl(knot_ctl_t **ctl, const char *socket, int timeout, const cmd_desc_t *desc)
+{
+ if (desc == NULL) {
+ *ctl = NULL;
+ return KNOT_EINVAL;
+ }
+
+ /* Mask relevant flags. */
+ cmd_flag_t flags = desc->flags & (CMD_FREAD | CMD_FWRITE);
+
+ /* Check if control socket is required. */
+ if (flags != CMD_FNONE) {
+ *ctl = NULL;
+ return KNOT_EOK;
+ }
+
+ /* Get control socket path. */
+ char *path = NULL;
+ if (socket != NULL) {
+ path = strdup(socket);
+ } else {
+ conf_val_t listen_val = conf_get(conf(), C_CTL, C_LISTEN);
+ conf_val_t rundir_val = conf_get(conf(), C_SRV, C_RUNDIR);
+ char *rundir = conf_abs_path(&rundir_val, NULL);
+ path = conf_abs_path(&listen_val, rundir);
+ free(rundir);
+ }
+ if (path == NULL) {
+ log_error("empty control socket path");
+ return KNOT_EINVAL;
+ }
+
+ log_debug("socket '%s'", path);
+
+ *ctl = knot_ctl_alloc();
+ if (*ctl == NULL) {
+ free(path);
+ return KNOT_ENOMEM;
+ }
+
+ knot_ctl_set_timeout(*ctl, timeout);
+
+ int ret = knot_ctl_connect(*ctl, path);
+ if (ret != KNOT_EOK) {
+ knot_ctl_free(*ctl);
+ *ctl = NULL;
+ log_error("failed to connect to socket '%s' (%s)", path,
+ knot_strerror(ret));
+ free(path);
+ return ret;
+ }
+
+ free(path);
+
+ return KNOT_EOK;
+}
+
+void unset_ctl(knot_ctl_t *ctl)
+{
+ if (ctl == NULL) {
+ return;
+ }
+
+ int ret = knot_ctl_send(ctl, KNOT_CTL_TYPE_END, NULL);
+ if (ret != KNOT_EOK && ret != KNOT_ECONN) {
+ log_error("failed to finish control (%s)", knot_strerror(ret));
+ }
+
+ knot_ctl_close(ctl);
+ knot_ctl_free(ctl);
+}
+
+int process_cmd(int argc, const char **argv, params_t *params)
+{
+ if (argc == 0) {
+ return KNOT_ENOTSUP;
+ }
+
+ /* Check the command name. */
+ const cmd_desc_t *desc = get_cmd_desc(argv[0]);
+ if (desc == NULL) {
+ return KNOT_ENOENT;
+ }
+
+ /* Check for exit. */
+ if (desc->fcn == NULL) {
+ return KNOT_CTL_ESTOP;
+ }
+
+ /* Set up the configuration. */
+ int ret = set_config(desc, params);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ /* Prepare command parameters. */
+ cmd_args_t args = {
+ .desc = desc,
+ .argc = argc - 1,
+ .argv = argv + 1,
+ .force = params->force,
+ .extended = params->extended,
+ .color = params->color,
+ .color_force = params->color_force,
+ .blocking = params->blocking
+ };
+
+ /* Check for special flags after command. */
+ while (args.argc > 0) {
+ if (get_cmd_force_flag(args.argv[0])) {
+ args.force = true;
+ args.argc--;
+ args.argv++;
+ } else if (get_cmd_blocking_flag(args.argv[0])) {
+ args.blocking = true;
+ args.argc--;
+ args.argv++;
+ } else {
+ break;
+ }
+ }
+
+ /* Prepare flags parameter. */
+ if (args.force) {
+ strlcat(args.flags, CTL_FLAG_FORCE, sizeof(args.flags));
+ }
+ if (args.blocking) {
+ strlcat(args.flags, CTL_FLAG_BLOCKING, sizeof(args.flags));
+ }
+
+ /* Set control interface if necessary. */
+ int cmd_timeout = params->timeout != -1 ? params->timeout : DEFAULT_CTL_TIMEOUT_MS;
+ if (args.blocking && params->timeout == -1) {
+ cmd_timeout = 0;
+ }
+ ret = set_ctl(&args.ctl, params->socket, cmd_timeout, desc);
+ if (ret != KNOT_EOK) {
+ conf_update(NULL, CONF_UPD_FNONE);
+ return ret;
+ }
+
+ /* Execute the command. */
+ ret = desc->fcn(&args);
+
+ /* Cleanup */
+ unset_ctl(args.ctl);
+ conf_update(NULL, CONF_UPD_FNONE);
+
+ return ret;
+}
diff --git a/src/utils/knotc/process.h b/src/utils/knotc/process.h
new file mode 100644
index 0000000..3946131
--- /dev/null
+++ b/src/utils/knotc/process.h
@@ -0,0 +1,78 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "utils/knotc/commands.h"
+
+#define DEFAULT_CTL_TIMEOUT_MS (60 * 1000)
+
+/*! Utility command line parameters. */
+typedef struct {
+ const char *orig_config;
+ const char *orig_confdb;
+ const char *config;
+ const char *confdb;
+ size_t max_conf_size;
+ const char *socket;
+ bool verbose;
+ bool extended;
+ bool force;
+ bool blocking;
+ int timeout;
+ bool color;
+ bool color_force;
+} params_t;
+
+/*!
+ * Prepares a proper configuration according to the specified command.
+ *
+ * \param[in] desc Utility command descriptor.
+ * \param[in] params Utility parameters.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int set_config(const cmd_desc_t *desc, params_t *params);
+
+/*!
+ * Establishes a control interface if necessary.
+ *
+ * \param[in] ctl Control context.
+ * \param[in] socket Control socket path.
+ * \param[in] timeout Control socket timeout.
+ * \param[in] desc Utility command descriptor.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int set_ctl(knot_ctl_t **ctl, const char *socket, int timeout, const cmd_desc_t *desc);
+
+/*!
+ * Cleans up the control context.
+ *
+ * \param[in] ctl Control context.
+ */
+void unset_ctl(knot_ctl_t *ctl);
+
+/*!
+ * Processes the given utility command.
+ *
+ * \param[in] argc Number of command arguments.
+ * \param[in] argv Command arguments.
+ * \param[in] params Utility parameters.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int process_cmd(int argc, const char **argv, params_t *params);
diff --git a/src/utils/knotd/main.c b/src/utils/knotd/main.c
new file mode 100644
index 0000000..2355d9e
--- /dev/null
+++ b/src/utils/knotd/main.c
@@ -0,0 +1,670 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <signal.h>
+#include <sys/stat.h>
+#include <urcu.h>
+
+#ifdef ENABLE_CAP_NG
+#include <cap-ng.h>
+#endif
+
+#include "libdnssec/crypto.h"
+#include "libknot/libknot.h"
+#include "contrib/strtonum.h"
+#include "contrib/time.h"
+#include "knot/ctl/process.h"
+#include "knot/conf/conf.h"
+#include "knot/conf/migration.h"
+#include "knot/conf/module.h"
+#include "knot/common/log.h"
+#include "knot/common/process.h"
+#include "knot/common/stats.h"
+#include "knot/common/systemd.h"
+#include "knot/server/server.h"
+#include "knot/server/tcp-handler.h"
+
+#define PROGRAM_NAME "knotd"
+
+/* Signal flags. */
+static volatile bool sig_req_stop = false;
+static volatile bool sig_req_reload = false;
+static volatile bool sig_req_zones_reload = false;
+
+static int make_daemon(int nochdir, int noclose)
+{
+ int ret;
+
+ switch (fork()) {
+ case -1:
+ /* Error */
+ return -1;
+ case 0:
+ /* Forked */
+ break;
+ default:
+ /* Exit the main process */
+ _exit(0);
+ }
+
+ if (setsid() == -1) {
+ return -1;
+ }
+
+ if (!nochdir) {
+ ret = chdir("/");
+ if (ret == -1)
+ return errno;
+ }
+
+ if (!noclose) {
+ ret = close(STDIN_FILENO);
+ ret += close(STDOUT_FILENO);
+ ret += close(STDERR_FILENO);
+ if (ret < 0) {
+ return errno;
+ }
+
+ int fd = open("/dev/null", O_RDWR);
+ if (fd == -1) {
+ return errno;
+ }
+
+ if (dup2(fd, STDIN_FILENO) < 0) {
+ close(fd);
+ return errno;
+ }
+ if (dup2(fd, STDOUT_FILENO) < 0) {
+ close(fd);
+ return errno;
+ }
+ if (dup2(fd, STDERR_FILENO) < 0) {
+ close(fd);
+ return errno;
+ }
+ close(fd);
+ }
+
+ return 0;
+}
+
+struct signal {
+ int signum;
+ bool handle;
+};
+
+/*! \brief Signals used by the server. */
+static const struct signal SIGNALS[] = {
+ { SIGHUP, true }, /* Reload server. */
+ { SIGUSR1, true }, /* Reload zones. */
+ { SIGINT, true }, /* Terminate server. */
+ { SIGTERM, true }, /* Terminate server. */
+ { SIGALRM, false }, /* Internal thread synchronization. */
+ { SIGPIPE, false }, /* Ignored. Some I/O errors. */
+ { 0 }
+};
+
+/*! \brief Server signal handler. */
+static void handle_signal(int signum)
+{
+ switch (signum) {
+ case SIGHUP:
+ sig_req_reload = true;
+ break;
+ case SIGUSR1:
+ sig_req_zones_reload = true;
+ break;
+ case SIGINT:
+ case SIGTERM:
+ if (sig_req_stop) {
+ exit(EXIT_FAILURE);
+ }
+ sig_req_stop = true;
+ break;
+ default:
+ /* ignore */
+ break;
+ }
+}
+
+/*! \brief Setup signal handlers and blocking mask. */
+static void setup_signals(void)
+{
+ /* Block all signals. */
+ static sigset_t all;
+ sigfillset(&all);
+ sigdelset(&all, SIGPROF);
+ sigdelset(&all, SIGQUIT);
+ sigdelset(&all, SIGILL);
+ sigdelset(&all, SIGABRT);
+ sigdelset(&all, SIGBUS);
+ sigdelset(&all, SIGFPE);
+ sigdelset(&all, SIGSEGV);
+ pthread_sigmask(SIG_SETMASK, &all, NULL);
+
+ /* Setup handlers. */
+ struct sigaction action = { .sa_handler = handle_signal };
+ for (const struct signal *s = SIGNALS; s->signum > 0; s++) {
+ sigaction(s->signum, &action, NULL);
+ }
+}
+
+/*! \brief Unblock server control signals. */
+static void enable_signals(void)
+{
+ sigset_t mask;
+ sigemptyset(&mask);
+
+ for (const struct signal *s = SIGNALS; s->signum > 0; s++) {
+ if (s->handle) {
+ sigaddset(&mask, s->signum);
+ }
+ }
+
+ pthread_sigmask(SIG_UNBLOCK, &mask, NULL);
+}
+
+/*! \brief Drop POSIX 1003.1e capabilities. */
+static void drop_capabilities(void)
+{
+#ifdef ENABLE_CAP_NG
+ /* Drop all capabilities. */
+ if (capng_have_capability(CAPNG_EFFECTIVE, CAP_SETPCAP)) {
+ capng_clear(CAPNG_SELECT_BOTH);
+
+ /* Apply. */
+ if (capng_apply(CAPNG_SELECT_BOTH) < 0) {
+ log_error("failed to set process capabilities (%s)",
+ strerror(errno));
+ }
+ } else {
+ log_info("process not allowed to set capabilities, skipping");
+ }
+#endif /* ENABLE_CAP_NG */
+}
+
+static void check_loaded(server_t *server)
+{
+ static bool finished = false;
+ if (finished) {
+ return;
+ }
+
+ /* Avoid traversing the zonedb too frequently. */
+ static struct timespec last = { 0 };
+ struct timespec now = time_now();
+ if (last.tv_sec == now.tv_sec) {
+ return;
+ }
+ last = now;
+
+ if (!(conf()->cache.srv_dbus_event & DBUS_EVENT_RUNNING)) {
+ finished = true;
+ return;
+ }
+
+ knot_zonedb_iter_t *it = knot_zonedb_iter_begin(server->zone_db);
+ while (!knot_zonedb_iter_finished(it)) {
+ zone_t *zone = (zone_t *)knot_zonedb_iter_val(it);
+ if (zone->contents == NULL) {
+ knot_zonedb_iter_free(it);
+ return;
+ }
+ knot_zonedb_iter_next(it);
+ }
+ knot_zonedb_iter_free(it);
+
+ finished = true;
+ systemd_emit_running(true);
+}
+
+/*! \brief Event loop listening for signals and remote commands. */
+static void event_loop(server_t *server, const char *socket, bool daemonize,
+ unsigned long pid)
+{
+ knot_ctl_t *ctl = knot_ctl_alloc();
+ if (ctl == NULL) {
+ log_fatal("control, failed to initialize (%s)",
+ knot_strerror(KNOT_ENOMEM));
+ return;
+ }
+
+ // Set control timeout.
+ knot_ctl_set_timeout(ctl, conf()->cache.ctl_timeout);
+
+ /* Get control socket configuration. */
+ char *listen;
+ if (socket == NULL) {
+ conf_val_t listen_val = conf_get(conf(), C_CTL, C_LISTEN);
+ conf_val_t rundir_val = conf_get(conf(), C_SRV, C_RUNDIR);
+ char *rundir = conf_abs_path(&rundir_val, NULL);
+ listen = conf_abs_path(&listen_val, rundir);
+ free(rundir);
+ } else {
+ listen = strdup(socket);
+ }
+ if (listen == NULL) {
+ knot_ctl_free(ctl);
+ log_fatal("control, empty socket path");
+ return;
+ }
+
+ log_info("control, binding to '%s'", listen);
+
+ /* Bind the control socket. */
+ int ret = knot_ctl_bind(ctl, listen);
+ if (ret != KNOT_EOK) {
+ knot_ctl_free(ctl);
+ log_fatal("control, failed to bind socket '%s' (%s)",
+ listen, knot_strerror(ret));
+ free(listen);
+ return;
+ }
+ free(listen);
+
+ enable_signals();
+
+ /* Notify systemd about successful start. */
+ systemd_ready_notify();
+ if (daemonize) {
+ log_info("server started as a daemon, PID %lu", pid);
+ } else {
+ log_info("server started in the foreground, PID %lu", pid);
+ }
+
+ /* Run event loop. */
+ for (;;) {
+ /* Interrupts. */
+ if (sig_req_reload && !sig_req_stop) {
+ sig_req_reload = false;
+ server_reload(server, RELOAD_FULL);
+ }
+ if (sig_req_zones_reload && !sig_req_stop) {
+ sig_req_zones_reload = false;
+ reload_t mode = server->catalog_upd_signal ? RELOAD_CATALOG : RELOAD_ZONES;
+ server->catalog_upd_signal = false;
+ server_update_zones(conf(), server, mode);
+ }
+ if (sig_req_stop) {
+ break;
+ }
+
+ // Update control timeout.
+ knot_ctl_set_timeout(ctl, conf()->cache.ctl_timeout);
+
+ if (sig_req_reload || sig_req_zones_reload) {
+ continue;
+ }
+
+ check_loaded(server);
+
+ ret = knot_ctl_accept(ctl);
+ if (ret != KNOT_EOK) {
+ continue;
+ }
+
+ ret = ctl_process(ctl, server);
+ knot_ctl_close(ctl);
+ if (ret == KNOT_CTL_ESTOP) {
+ break;
+ }
+ }
+
+ if (conf()->cache.srv_dbus_event & DBUS_EVENT_RUNNING) {
+ systemd_emit_running(false);
+ }
+
+ /* Unbind the control socket. */
+ knot_ctl_unbind(ctl);
+ knot_ctl_free(ctl);
+}
+
+static void print_help(void)
+{
+ printf("Usage: %s [-c | -C <path>] [options]\n"
+ "\n"
+ "Config options:\n"
+ " -c, --config <file> Use a textual configuration file.\n"
+ " (default %s)\n"
+ " -C, --confdb <dir> Use a binary configuration database directory.\n"
+ " (default %s)\n"
+ "Options:\n"
+ " -m, --max-conf-size <MiB> Set maximum size of the configuration database (max 10000 MiB).\n"
+ " (default %d MiB)\n"
+ " -s, --socket <path> Use a remote control UNIX socket path.\n"
+ " (default %s)\n"
+ " -d, --daemonize=[dir] Run the server as a daemon (with new root directory).\n"
+ " -v, --verbose Enable debug output.\n"
+ " -h, --help Print the program help.\n"
+ " -V, --version Print the program version.\n",
+ PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR,
+ CONF_MAPSIZE, RUN_DIR "/knot.sock");
+}
+
+static void print_version(void)
+{
+ printf("%s (Knot DNS), version %s\n", PROGRAM_NAME, PACKAGE_VERSION);
+}
+
+static int set_config(const char *confdb, const char *config, size_t max_conf_size)
+{
+ if (config != NULL && confdb != NULL) {
+ log_fatal("ambiguous configuration source");
+ return KNOT_EINVAL;
+ }
+
+ /* Choose the optimal config source. */
+ bool import = false;
+ if (confdb != NULL) {
+ import = false;
+ } else if (config != NULL){
+ import = true;
+ } else if (conf_db_exists(CONF_DEFAULT_DBDIR)) {
+ import = false;
+ confdb = CONF_DEFAULT_DBDIR;
+ } else {
+ import = true;
+ config = CONF_DEFAULT_FILE;
+ }
+
+ /* Open confdb. */
+ conf_t *new_conf = NULL;
+ int ret = conf_new(&new_conf, conf_schema, confdb, max_conf_size, CONF_FREQMODULES);
+ if (ret != KNOT_EOK) {
+ log_fatal("failed to open configuration database '%s' (%s)",
+ (confdb != NULL) ? confdb : "", knot_strerror(ret));
+ return ret;
+ }
+
+ /* Import the config file. */
+ if (import) {
+ ret = conf_import(new_conf, config, IMPORT_FILE | IMPORT_REINIT_CACHE);
+ if (ret != KNOT_EOK) {
+ log_fatal("failed to load configuration file '%s' (%s)",
+ config, knot_strerror(ret));
+ conf_free(new_conf);
+ return ret;
+ }
+ }
+
+ // Migrate from old schema.
+ ret = conf_migrate(new_conf);
+ if (ret != KNOT_EOK) {
+ log_error("failed to migrate configuration (%s)", knot_strerror(ret));
+ }
+
+ /* Update to the new config. */
+ conf_update(new_conf, CONF_UPD_FNONE);
+
+ return KNOT_EOK;
+}
+
+int main(int argc, char **argv)
+{
+ bool daemonize = false;
+ const char *config = NULL;
+ const char *confdb = NULL;
+ size_t max_conf_size = (size_t)CONF_MAPSIZE * 1024 * 1024;
+ const char *daemon_root = "/";
+ char *socket = NULL;
+ bool verbose = false;
+
+ /* Long options. */
+ struct option opts[] = {
+ { "config", required_argument, NULL, 'c' },
+ { "confdb", required_argument, NULL, 'C' },
+ { "max-conf-size", required_argument, NULL, 'm' },
+ { "socket", required_argument, NULL, 's' },
+ { "daemonize", optional_argument, NULL, 'd' },
+ { "verbose", no_argument, NULL, 'v' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL }
+ };
+
+ /* Set the time zone. */
+ tzset();
+
+ /* Parse command line arguments. */
+ int opt = 0;
+ while ((opt = getopt_long(argc, argv, "c:C:m:s:dvhV", opts, NULL)) != -1) {
+ switch (opt) {
+ case 'c':
+ config = optarg;
+ break;
+ case 'C':
+ confdb = optarg;
+ break;
+ case 'm':
+ if (str_to_size(optarg, &max_conf_size, 1, 10000) != KNOT_EOK) {
+ print_help();
+ return EXIT_FAILURE;
+ }
+ /* Convert to bytes. */
+ max_conf_size *= 1024 * 1024;
+ break;
+ case 's':
+ socket = optarg;
+ break;
+ case 'd':
+ daemonize = true;
+ if (optarg) {
+ daemon_root = optarg;
+ }
+ break;
+ case 'v':
+ verbose = true;
+ break;
+ case 'h':
+ print_help();
+ return EXIT_SUCCESS;
+ case 'V':
+ print_version();
+ return EXIT_SUCCESS;
+ default:
+ print_help();
+ return EXIT_FAILURE;
+ }
+ }
+
+ /* Check for non-option parameters. */
+ if (argc - optind > 0) {
+ print_help();
+ return EXIT_FAILURE;
+ }
+
+ /* Set file creation mask to remove all permissions for others. */
+ umask(S_IROTH | S_IWOTH | S_IXOTH);
+
+ /* Now check if we want to daemonize. */
+ if (daemonize) {
+ if (make_daemon(1, 0) != 0) {
+ fprintf(stderr, "Daemonization failed, shutting down...\n");
+ return EXIT_FAILURE;
+ }
+ }
+
+ /* Setup base signal handling. */
+ setup_signals();
+
+ /* Initialize cryptographic backend. */
+ dnssec_crypto_init();
+
+ /* Initialize pseudorandom number generator. */
+ srand(time(NULL));
+
+ /* Initialize logging subsystem. */
+ log_init();
+ if (verbose) {
+ log_levels_add(LOG_TARGET_STDOUT, LOG_SOURCE_ANY, LOG_MASK(LOG_DEBUG));
+ }
+
+ /* Set up the configuration */
+ int ret = set_config(confdb, config, max_conf_size);
+ if (ret != KNOT_EOK) {
+ log_close();
+ dnssec_crypto_cleanup();
+ return EXIT_FAILURE;
+ }
+
+ /* Reconfigure logging. */
+ log_reconfigure(conf());
+
+ /* Initialize server. */
+ server_t server;
+ ret = server_init(&server, conf()->cache.srv_bg_threads);
+ if (ret != KNOT_EOK) {
+ log_fatal("failed to initialize server (%s)", knot_strerror(ret));
+ conf_free(conf());
+ log_close();
+ dnssec_crypto_cleanup();
+ return EXIT_FAILURE;
+ }
+
+ /* Reconfigure server workers, interfaces, and databases.
+ * @note This MUST be done before we drop privileges. */
+ ret = server_reconfigure(conf(), &server);
+ if (ret != KNOT_EOK) {
+ log_fatal("failed to configure server");
+ server_wait(&server);
+ server_deinit(&server);
+ conf_free(conf());
+ log_close();
+ dnssec_crypto_cleanup();
+ return EXIT_FAILURE;
+ }
+
+ /* Alter privileges. */
+ int uid, gid;
+ if (conf_user(conf(), &uid, &gid) != KNOT_EOK ||
+ log_update_privileges(uid, gid) != KNOT_EOK ||
+ proc_update_privileges(uid, gid) != KNOT_EOK) {
+ log_fatal("failed to drop privileges");
+ server_wait(&server);
+ server_deinit(&server);
+ conf_free(conf());
+ log_close();
+ dnssec_crypto_cleanup();
+ return EXIT_FAILURE;
+ }
+
+ if (conf()->cache.srv_dbus_event != DBUS_EVENT_NONE) {
+ ret = systemd_dbus_open();
+ if (ret != KNOT_EOK) {
+ log_error("d-bus: failed to open system bus (%s)",
+ knot_strerror(ret));
+ } else {
+ log_info("d-bus: connected to system bus");
+ }
+ int64_t delay = conf_get_int(conf(), C_SRV, C_DBUS_INIT_DELAY);
+ sleep(delay);
+ }
+
+ /* Drop POSIX capabilities. */
+ drop_capabilities();
+
+ /* Activate global query modules. */
+ conf_activate_modules(conf(), &server, NULL, conf()->query_modules,
+ &conf()->query_plan);
+
+ /* Check and create PID file. */
+ unsigned long pid = pid_check_and_create();
+ if (pid == 0) {
+ server_wait(&server);
+ server_deinit(&server);
+ conf_free(conf());
+ systemd_dbus_close();
+ log_close();
+ dnssec_crypto_cleanup();
+ return EXIT_FAILURE;
+ }
+
+ if (daemonize) {
+ if (chdir(daemon_root) != 0) {
+ log_warning("failed to change working directory to %s",
+ daemon_root);
+ } else {
+ log_info("changed directory to %s", daemon_root);
+ }
+ }
+
+ /* Now we're going multithreaded. */
+ rcu_register_thread();
+
+ /* Populate zone database. */
+ log_info("loading %zu zones", conf_id_count(conf(), C_ZONE));
+ server_update_zones(conf(), &server, RELOAD_ZONES);
+
+ /* Check number of loaded zones. */
+ if (knot_zonedb_size(server.zone_db) == 0) {
+ log_warning("no zones loaded");
+ }
+
+ stats_reconfigure(conf(), &server);
+
+ /* Start it up. */
+ log_info("starting server");
+ conf_val_t async_val = conf_get(conf(), C_SRV, C_ASYNC_START);
+ ret = server_start(&server, conf_bool(&async_val));
+ if (ret != KNOT_EOK) {
+ log_fatal("failed to start server (%s)", knot_strerror(ret));
+ server_wait(&server);
+ stats_deinit();
+ server_deinit(&server);
+ rcu_unregister_thread();
+ pid_cleanup();
+ conf_free(conf());
+ systemd_dbus_close();
+ log_close();
+ dnssec_crypto_cleanup();
+ return EXIT_FAILURE;
+ }
+
+ /* Start the event loop. */
+ event_loop(&server, socket, daemonize, pid);
+
+ /* Teardown server. */
+ server_stop(&server);
+ server_wait(&server);
+ stats_deinit();
+
+ /* Cleanup PID file. */
+ pid_cleanup();
+
+ /* Free server and configuration. */
+ server_deinit(&server);
+ conf_free(conf());
+
+ /* Unhook from RCU. */
+ rcu_unregister_thread();
+
+ systemd_dbus_close();
+
+ log_info("shutting down");
+ log_close();
+
+ dnssec_crypto_cleanup();
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/utils/knsec3hash/knsec3hash.c b/src/utils/knsec3hash/knsec3hash.c
new file mode 100644
index 0000000..a7bac97
--- /dev/null
+++ b/src/utils/knsec3hash/knsec3hash.c
@@ -0,0 +1,187 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <getopt.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "contrib/base32hex.h"
+#include "contrib/string.h"
+#include "contrib/strtonum.h"
+#include "libdnssec/error.h"
+#include "libdnssec/nsec.h"
+#include "libknot/libknot.h"
+#include "utils/common/msg.h"
+#include "utils/common/params.h"
+
+#define PROGRAM_NAME "knsec3hash"
+
+/*!
+ * \brief Print program help (and example).
+ */
+static void print_help(void)
+{
+ printf("Usage: " PROGRAM_NAME " <salt> <algorithm> <iterations> <domain-name>\n");
+ printf("Example: " PROGRAM_NAME " c01dcafe 1 10 knot-dns.cz\n");
+ printf("Alternative usage: "PROGRAM_NAME " <algorithm> <flags> <iterations> <salt> <domain-name>\n");
+ printf("Example: " PROGRAM_NAME " 1 0 10 c01dcafe knot-dns.cz\n");
+}
+
+/*!
+ * \brief Parse NSEC3 salt.
+ */
+static int str_to_salt(const char *str, dnssec_binary_t *salt)
+{
+ if (strcmp(str, "-") == 0) {
+ salt->size = 0;
+ return DNSSEC_EOK;
+ } else {
+ salt->data = hex_to_bin(str, &salt->size);
+ return (salt->data != NULL ? DNSSEC_EOK : DNSSEC_EINVAL);
+ }
+}
+
+/*!
+ * \brief Parse NSEC3 parameters and fill structure with NSEC3 parameters.
+ */
+static bool parse_nsec3_params(dnssec_nsec3_params_t *params, const char *salt_str,
+ const char *algorithm_str, const char *iterations_str)
+{
+ uint8_t algorithm = 0;
+ int r = str_to_u8(algorithm_str, &algorithm);
+ if (r != KNOT_EOK) {
+ ERR2("invalid algorithm number");
+ return false;
+ }
+
+ uint16_t iterations = 0;
+ r = str_to_u16(iterations_str, &iterations);
+ if (r != KNOT_EOK) {
+ ERR2("invalid iteration count");
+ return false;
+ }
+
+ dnssec_binary_t salt = { 0 };
+ r = str_to_salt(salt_str, &salt);
+ if (r != DNSSEC_EOK) {
+ ERR2("invalid salt (%s)", knot_strerror(r));
+ return false;
+ }
+
+ if (salt.size > UINT8_MAX) {
+ ERR2("invalid salt, maximum length is %d bytes", UINT8_MAX);
+ dnssec_binary_free(&salt);
+ return false;
+ }
+
+ params->algorithm = algorithm;
+ params->iterations = iterations;
+ params->salt = salt;
+ params->flags = 0;
+
+ return true;
+}
+
+/*!
+ * \brief Entry point of 'knsec3hash'.
+ */
+int main(int argc, char *argv[])
+{
+ struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL }
+ };
+
+ int opt = 0;
+ while ((opt = getopt_long(argc, argv, "hV", options, NULL)) != -1) {
+ switch(opt) {
+ case 'h':
+ print_help();
+ return EXIT_SUCCESS;
+ case 'V':
+ print_version(PROGRAM_NAME);
+ return EXIT_SUCCESS;
+ default:
+ print_help();
+ return EXIT_FAILURE;
+ }
+ }
+
+ bool new_params = false;
+ if (argc == 6) {
+ // knsec3hash <algorithm> <flags> <iterations> <salt> <domain>
+ new_params = true;
+ } else if (argc != 5) {
+ // knsec3hash <salt> <algorithm> <iterations> <domain>
+ print_help();
+ return EXIT_FAILURE;
+ }
+
+ int exit_code = EXIT_FAILURE;
+ dnssec_nsec3_params_t nsec3_params = { 0 };
+
+ dnssec_binary_t dname = { 0 };
+ dnssec_binary_t digest = { 0 };
+ dnssec_binary_t digest_print = { 0 };
+
+ if (new_params) {
+ if (!parse_nsec3_params(&nsec3_params, argv[4], argv[1], argv[3])) {
+ goto fail;
+ }
+ } else {
+ if (!parse_nsec3_params(&nsec3_params, argv[1], argv[2], argv[3])) {
+ goto fail;
+ }
+ }
+
+ dname.data = knot_dname_from_str_alloc(argv[new_params ? 5 : 4]);
+ if (dname.data == NULL) {
+ ERR2("cannot parse domain name");
+ goto fail;
+ }
+ knot_dname_to_lower(dname.data);
+ dname.size = knot_dname_size(dname.data);
+
+ int r = dnssec_nsec3_hash(&dname, &nsec3_params, &digest);
+ if (r != DNSSEC_EOK) {
+ ERR2("cannot compute NSEC3 hash (%s)", knot_strerror(r));
+ goto fail;
+ }
+
+ r = knot_base32hex_encode_alloc(digest.data, digest.size, &digest_print.data);
+ if (r < 0) {
+ ERR2("cannot encode computed hash (%s)", knot_strerror(r));
+ goto fail;
+ }
+ digest_print.size = r;
+
+ exit_code = EXIT_SUCCESS;
+
+ printf("%.*s (salt=%s, hash=%d, iterations=%d)\n", (int)digest_print.size,
+ digest_print.data, argv[1], nsec3_params.algorithm,
+ nsec3_params.iterations);
+
+fail:
+ dnssec_nsec3_params_free(&nsec3_params);
+ dnssec_binary_free(&dname);
+ dnssec_binary_free(&digest);
+ dnssec_binary_free(&digest_print);
+
+ return exit_code;
+}
diff --git a/src/utils/knsupdate/knsupdate_exec.c b/src/utils/knsupdate/knsupdate_exec.c
new file mode 100644
index 0000000..e201711
--- /dev/null
+++ b/src/utils/knsupdate/knsupdate_exec.c
@@ -0,0 +1,1059 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include "libdnssec/random.h"
+#include "utils/knsupdate/knsupdate_exec.h"
+#include "utils/knsupdate/knsupdate_interactive.h"
+#include "utils/common/exec.h"
+#include "utils/common/msg.h"
+#include "utils/common/netio.h"
+#include "utils/common/params.h"
+#include "utils/common/sign.h"
+#include "utils/common/token.h"
+#include "libknot/libknot.h"
+#include "contrib/ctype.h"
+#include "contrib/getline.h"
+#include "contrib/string.h"
+#include "contrib/strtonum.h"
+#include "contrib/openbsd/strlcpy.h"
+
+/* Declarations of cmd parse functions. */
+typedef int (*cmd_handle_f)(const char *lp, knsupdate_params_t *params);
+int cmd_add(const char* lp, knsupdate_params_t *params);
+int cmd_answer(const char* lp, knsupdate_params_t *params);
+int cmd_class(const char* lp, knsupdate_params_t *params);
+int cmd_debug(const char* lp, knsupdate_params_t *params);
+int cmd_del(const char* lp, knsupdate_params_t *params);
+int cmd_gsstsig(const char* lp, knsupdate_params_t *params);
+int cmd_key(const char* lp, knsupdate_params_t *params);
+int cmd_local(const char* lp, knsupdate_params_t *params);
+int cmd_nxdomain(const char *lp, knsupdate_params_t *params);
+int cmd_nxrrset(const char *lp, knsupdate_params_t *params);
+int cmd_oldgsstsig(const char* lp, knsupdate_params_t *params);
+int cmd_origin(const char* lp, knsupdate_params_t *params);
+int cmd_prereq(const char* lp, knsupdate_params_t *params);
+int cmd_exit(const char* lp, knsupdate_params_t *params);
+int cmd_realm(const char* lp, knsupdate_params_t *params);
+int cmd_send(const char* lp, knsupdate_params_t *params);
+int cmd_server(const char* lp, knsupdate_params_t *params);
+int cmd_show(const char* lp, knsupdate_params_t *params);
+int cmd_ttl(const char* lp, knsupdate_params_t *params);
+int cmd_update(const char* lp, knsupdate_params_t *params);
+int cmd_yxdomain(const char *lp, knsupdate_params_t *params);
+int cmd_yxrrset(const char *lp, knsupdate_params_t *params);
+int cmd_zone(const char* lp, knsupdate_params_t *params);
+
+/* Sorted list of commands.
+ * This way we could identify command byte-per-byte and
+ * cancel early if the next is lexicographically greater.
+ */
+const char* knsupdate_cmd_array[] = {
+ "\x3" "add",
+ "\x6" "answer",
+ "\x5" "class", /* {classname} */
+ "\x5" "debug",
+ "\x3" "del",
+ "\x6" "delete",
+ "\x4" "exit",
+ "\x7" "gsstsig",
+ "\x3" "key", /* {[alg:]name} {secret} */
+ "\x5" "local", /* {address} [port] */
+ "\x8" "nxdomain",
+ "\x7" "nxrrset",
+ "\xa" "oldgsstsig",
+ "\x6" "origin", /* {name} */
+ "\x6" "prereq", /* (nx|yx)(domain|rrset) {domain-name} ... */
+ "\x4" "quit",
+ "\x5" "realm", /* {[realm_name]} */
+ "\x4" "send",
+ "\x6" "server", /* {servername} [port] */
+ "\x4" "show",
+ "\x3" "ttl", /* {seconds} */
+ "\x6" "update", /* (add|delete) {domain-name} ... */
+ "\x8" "yxdomain",
+ "\x7" "yxrrset",
+ "\x4" "zone", /* {zonename} */
+ NULL
+};
+
+cmd_handle_f cmd_handle[] = {
+ cmd_add,
+ cmd_answer,
+ cmd_class,
+ cmd_debug,
+ cmd_del,
+ cmd_del, /* delete/del synonyms */
+ cmd_exit,
+ cmd_gsstsig,
+ cmd_key,
+ cmd_local,
+ cmd_nxdomain,
+ cmd_nxrrset,
+ cmd_oldgsstsig,
+ cmd_origin,
+ cmd_prereq,
+ cmd_exit, /* exit/quit synonyms */
+ cmd_realm,
+ cmd_send,
+ cmd_server,
+ cmd_show,
+ cmd_ttl,
+ cmd_update,
+ cmd_yxdomain,
+ cmd_yxrrset,
+ cmd_zone,
+};
+
+/* {prereq} command table. */
+const char* pq_array[] = {
+ "\x8" "nxdomain",
+ "\x7" "nxrrset",
+ "\x8" "yxdomain",
+ "\x7" "yxrrset",
+ NULL
+};
+
+enum {
+ PQ_NXDOMAIN = 0,
+ PQ_NXRRSET,
+ PQ_YXDOMAIN,
+ PQ_YXRRSET
+};
+
+/* RR parser flags */
+enum {
+ PARSE_NODEFAULT = 1 << 0, /* Do not fill defaults. */
+ PARSE_NAMEONLY = 1 << 1, /* Parse only name. */
+ PARSE_NOTTL = 1 << 2 /* Ignore TTL item. */
+};
+
+static bool dname_isvalid(const char *lp)
+{
+ knot_dname_t *dn = knot_dname_from_str_alloc(lp);
+ if (dn == NULL) {
+ return false;
+ }
+ knot_dname_free(dn, NULL);
+ return true;
+}
+
+/* This is probably redundant, but should be a bit faster so let's keep it. */
+static int parse_full_rr(zs_scanner_t *s, const char* lp)
+{
+ if (zs_set_input_string(s, lp, strlen(lp)) != 0 ||
+ zs_parse_all(s) != 0) {
+ ERR("invalid record (%s)", zs_strerror(s->error.code));
+ return KNOT_EPARSEFAIL;
+ }
+
+ /* Class must not differ from specified. */
+ if (s->r_class != s->default_class) {
+ char cls_s[16] = "";
+ knot_rrclass_to_string(s->default_class, cls_s, sizeof(cls_s));
+ ERR("class mismatch '%s'", cls_s);
+ return KNOT_EPARSEFAIL;
+ }
+
+ return KNOT_EOK;
+}
+
+static int parse_partial_rr(zs_scanner_t *s, const char *lp, unsigned flags)
+{
+ knot_dname_txt_storage_t owner_str = { 0 };
+
+ /* Extract owner. */
+ size_t len = strcspn(lp, SEP_CHARS);
+ memcpy(owner_str, lp, len);
+
+ /* Check if ORIGIN (@) or FQDN. */
+ bool origin = false;
+ bool fqdn = true;
+ if (len == 1 && owner_str[0] == '@') {
+ origin = true;
+ fqdn = false;
+ } else if (owner_str[len - 1] != '.') {
+ fqdn = false;
+ }
+
+ /* Convert textual owner to dname. */
+ if (!origin) {
+ knot_dname_storage_t owner;
+ if (knot_dname_from_str(owner, owner_str, sizeof(owner)) == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ s->r_owner_length = knot_dname_size(owner);
+ memcpy(s->r_owner, owner, s->r_owner_length);
+ } else {
+ s->r_owner_length = 0;
+ }
+
+ /* Append origin if not FQDN. */
+ if (!fqdn) {
+ if (!origin) {
+ s->r_owner_length--;
+ }
+ memcpy(s->r_owner + s->r_owner_length, s->zone_origin,
+ s->zone_origin_length);
+ s->r_owner_length += s->zone_origin_length;
+ }
+
+ lp = tok_skipspace(lp + len);
+
+ /* Initialize */
+ s->r_type = KNOT_RRTYPE_ANY;
+ s->r_class = s->default_class;
+ s->r_data_length = 0;
+ if (flags & PARSE_NODEFAULT) {
+ s->r_ttl = 0;
+ } else {
+ s->r_ttl = s->default_ttl;
+ }
+
+ /* Parse only name? */
+ if (flags & PARSE_NAMEONLY) {
+ if (*lp != '\0') {
+ WARN("ignoring input data '%s'", lp);
+ }
+ return KNOT_EOK;
+ }
+
+ /* Now there could be [ttl] [class] [type [data...]]. */
+ char *np = NULL;
+ long ttl = strtol(lp, &np, 10);
+ if (ttl >= 0 && np && (*np == '\0' || is_space(*np))) {
+ DBG("%s: parsed ttl=%lu", __func__, ttl);
+ if (flags & PARSE_NOTTL) {
+ WARN("ignoring TTL value '%ld'", ttl);
+ } else {
+ s->r_ttl = ttl;
+ }
+ lp = tok_skipspace(np);
+ }
+
+ uint16_t num;
+ char *buff = NULL;
+ char *cls = NULL;
+ char *type = NULL;
+
+ /* Try to find class. */
+ len = strcspn(lp, SEP_CHARS);
+ if (len > 0) {
+ buff = strndup(lp, len);
+ }
+
+ if (knot_rrclass_from_string(buff, &num) == 0) {
+ /* Class must not differ from specified. */
+ if (num != s->default_class) {
+ ERR("class mismatch '%s'", buff);
+ free(buff);
+ return KNOT_EPARSEFAIL;
+ }
+ cls = buff;
+ buff = NULL;
+ s->r_class = num;
+ DBG("%s: parsed class=%u '%s'", __func__, s->r_class, cls);
+ lp = tok_skipspace(lp + len);
+ }
+
+ /* Try to parser type. */
+ if (cls != NULL) {
+ len = strcspn(lp, SEP_CHARS);
+ if (len > 0) {
+ buff = strndup(lp, len);
+ }
+ }
+ if (knot_rrtype_from_string(buff, &num) == 0) {
+ type = buff;
+ buff = NULL;
+ s->r_type = num;
+ DBG("%s: parsed type=%u '%s'", __func__, s->r_type, type);
+ lp = tok_skipspace(lp + len);
+ }
+
+ free(buff);
+
+ /* Remainder */
+ if (*lp == '\0') {
+ free(cls);
+ free(type);
+ return KNOT_EOK;
+ }
+
+ /* Need to parse rdata, synthetize input. */
+ char *rr = sprintf_alloc(" %u IN %s %s\n", s->r_ttl, type, lp);
+ free(cls);
+ free(type);
+ if (rr == NULL) {
+ return KNOT_ENOMEM;
+ }
+ if (zs_set_input_string(s, rr, strlen(rr)) != 0 ||
+ zs_parse_all(s) != 0) {
+ ERR("invalid rdata (%s)", zs_strerror(s->error.code));
+ return KNOT_EPARSEFAIL;
+ }
+ free(rr);
+
+ return KNOT_EOK;
+}
+
+static srv_info_t *parse_host(const char *lp, const char* default_port)
+{
+ /* Extract server address. */
+ srv_info_t *srv = NULL;
+ size_t len = strcspn(lp, SEP_CHARS);
+ char *addr = strndup(lp, len);
+ if (!addr) return NULL;
+ DBG("%s: parsed addr: %s", __func__, addr);
+
+ /* Store port/service if present. */
+ lp = tok_skipspace(lp + len);
+ if (*lp == '\0') {
+ srv = srv_info_create(addr, default_port);
+ free(addr);
+ return srv;
+ }
+
+ len = strcspn(lp, SEP_CHARS);
+ char *port = strndup(lp, len);
+ if (!port) {
+ free(addr);
+ return NULL;
+ }
+ DBG("%s: parsed port: %s", __func__, port);
+
+ /* Create server struct. */
+ srv = srv_info_create(addr, port);
+ free(addr);
+ free(port);
+ return srv;
+}
+
+/* Append parsed RRSet to list. */
+static int rr_list_append(zs_scanner_t *s, list_t *target_list, knot_mm_t *mm)
+{
+ knot_rrset_t *rr = knot_rrset_new(s->r_owner, s->r_type, s->r_class,
+ s->r_ttl, NULL);
+ if (!rr) {
+ DBG("%s: failed to create rrset", __func__);
+ return KNOT_ENOMEM;
+ }
+
+ /* Create RDATA. */
+ int ret = knot_rrset_add_rdata(rr, s->r_data, s->r_data_length, NULL);
+ if (ret != KNOT_EOK) {
+ DBG("%s: failed to set rrset from wire (%s)",
+ __func__, knot_strerror(ret));
+ knot_rrset_free(rr, NULL);
+ return ret;
+ }
+
+ if (ptrlist_add(target_list, rr, mm) == NULL) {
+ knot_rrset_free(rr, NULL);
+ return KNOT_ENOMEM;
+ }
+
+ return KNOT_EOK;
+}
+
+/*! \brief Write RRSet list to packet section. */
+static int rr_list_to_packet(knot_pkt_t *dst, list_t *list)
+{
+ assert(dst != NULL);
+ assert(list != NULL);
+
+ ptrnode_t *node;
+ WALK_LIST(node, *list) {
+ int ret = knot_pkt_put(dst, KNOT_COMPR_HINT_NONE,
+ (knot_rrset_t *)node->d, 0);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+/*! \brief Build UPDATE query. */
+static int build_query(knsupdate_params_t *params)
+{
+ /* Clear old query. */
+ knot_pkt_t *query = params->query;
+ knot_pkt_clear(query);
+
+ /* Write question. */
+ knot_wire_set_id(query->wire, dnssec_random_uint16_t());
+ knot_wire_set_opcode(query->wire, KNOT_OPCODE_UPDATE);
+ knot_dname_t *qname = knot_dname_from_str_alloc(params->zone);
+ int ret = knot_pkt_put_question(query, qname, params->class_num,
+ KNOT_RRTYPE_SOA);
+ knot_dname_free(qname, NULL);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ /* Now, PREREQ => ANSWER section. */
+ ret = knot_pkt_begin(query, KNOT_ANSWER);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ /* Write PREREQ. */
+ ret = rr_list_to_packet(query, &params->prereq_list);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ /* Now, UPDATE data => AUTHORITY section. */
+ ret = knot_pkt_begin(query, KNOT_AUTHORITY);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ /* Write UPDATE data. */
+ return rr_list_to_packet(query, &params->update_list);
+}
+
+static int pkt_sendrecv(knsupdate_params_t *params)
+{
+ net_t net;
+ int ret;
+
+ ret = net_init(params->srcif,
+ params->server,
+ get_iptype(params->ip, params->server),
+ get_socktype(params->protocol, KNOT_RRTYPE_SOA),
+ params->wait,
+ NET_FLAGS_NONE,
+ NULL,
+ NULL,
+ &net);
+ if (ret != KNOT_EOK) {
+ return -1;
+ }
+
+ ret = net_connect(&net);
+ DBG("%s: send_msg = %d", __func__, net.sockfd);
+ if (ret != KNOT_EOK) {
+ net_clean(&net);
+ return -1;
+ }
+
+ ret = net_send(&net, params->query->wire, params->query->size);
+ if (ret != KNOT_EOK) {
+ net_close(&net);
+ net_clean(&net);
+ return -1;
+ }
+
+ /* Clear response buffer. */
+ knot_pkt_clear(params->answer);
+
+ /* Wait for reception. */
+ int rb = net_receive(&net, params->answer->wire, params->answer->max_size);
+ DBG("%s: receive_msg = %d", __func__, rb);
+ if (rb <= 0) {
+ net_close(&net);
+ net_clean(&net);
+ return -1;
+ } else {
+ params->answer->size = rb;
+ }
+
+ net_close(&net);
+ net_clean(&net);
+
+ return rb;
+}
+
+int knsupdate_process_line(const char *line, knsupdate_params_t *params)
+{
+ /* Check for empty line or comment. */
+ if (line[0] == '\0' || line[0] == ';') {
+ return KNOT_EOK;
+ }
+
+ int ret = tok_find(line, knsupdate_cmd_array);
+ if (ret < 0) {
+ return ret; /* Syntax error - do nothing. */
+ }
+
+ const char *cmd = knsupdate_cmd_array[ret];
+ const char *val = tok_skipspace(line + TOK_L(cmd));
+ params->parser.error.counter = 0; /* Reset possible previous error. */
+ ret = cmd_handle[ret](val, params);
+ if (ret != KNOT_EOK) {
+ DBG("operation '%s' failed (%s) on line '%s'",
+ TOK_S(cmd), knot_strerror(ret), line);
+ }
+
+ return ret;
+}
+
+static bool is_terminal(FILE *file)
+{
+ int fd = fileno(file);
+ assert(fd >= 0);
+ return isatty(fd);
+}
+
+static int process_lines(knsupdate_params_t *params, FILE *input)
+{
+ char *buf = NULL;
+ size_t buflen = 0;
+ if(is_terminal(input)) {
+ return interactive_loop(params);
+ }
+ int ret = KNOT_EOK;
+
+ /* Process lines. */
+ while (!params->stop && knot_getline(&buf, &buflen, input) != -1) {
+ /* Remove leading and trailing white space. */
+ char *line = strstrip(buf);
+ ret = knsupdate_process_line(line, params);
+ memset(line, 0, strlen(line));
+ free(line);
+ if (ret != KNOT_EOK) {
+ break;
+ }
+ }
+
+ if (buf != NULL) {
+ memset(buf, 0, buflen);
+ free(buf);
+ }
+
+ return ret;
+}
+
+int knsupdate_exec(knsupdate_params_t *params)
+{
+ if (!params) {
+ return KNOT_EINVAL;
+ }
+
+ int ret = KNOT_EOK;
+
+ /* If no file specified, enter the interactive mode. */
+ if (EMPTY_LIST(params->qfiles)) {
+ return process_lines(params, stdin);
+ }
+
+ /* Read from each specified file. */
+ ptrnode_t *n;
+ WALK_LIST(n, params->qfiles) {
+ const char *filename = (const char*)n->d;
+ if (strcmp(filename, "-") == 0) {
+ ret = process_lines(params, stdin);
+ if (ret != KNOT_EOK) {
+ break;
+ }
+ continue;
+ }
+
+ FILE *fp = fopen(filename, "r");
+ if (!fp) {
+ ERR("failed to open '%s' (%s)",
+ filename, strerror(errno));
+ ret = KNOT_EFILE;
+ break;
+ }
+ ret = process_lines(params, fp);
+ fclose(fp);
+ if (ret != KNOT_EOK) {
+ break;
+ }
+ }
+
+ return ret;
+}
+
+int cmd_update(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ /* update is optional token, next add|del|delete */
+ int bp = tok_find(lp, knsupdate_cmd_array);
+ if (bp < 0) return bp; /* Syntax error. */
+
+ /* allow only specific tokens */
+ cmd_handle_f *h = cmd_handle;
+ if (h[bp] != cmd_add && h[bp] != cmd_del) {
+ ERR("unexpected token '%s' after 'update', allowed: '%s'",
+ lp, "{add|del|delete}");
+ return KNOT_EPARSEFAIL;
+ }
+
+ return h[bp](tok_skipspace(lp + TOK_L(knsupdate_cmd_array[bp])), params);
+}
+
+int cmd_add(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ if (parse_full_rr(&params->parser, lp) != KNOT_EOK) {
+ return KNOT_EPARSEFAIL;
+ }
+
+ return rr_list_append(&params->parser, &params->update_list, &params->mm);
+}
+
+int cmd_del(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ zs_scanner_t *rrp = &params->parser;
+ int ret = parse_partial_rr(rrp, lp, PARSE_NODEFAULT);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ /* Check owner name. */
+ if (rrp->r_owner_length == 0) {
+ ERR("failed to parse owner name '%s'", lp);
+ return KNOT_EPARSEFAIL;
+ }
+
+ rrp->r_ttl = 0; /* Set TTL = 0 when deleting. */
+
+ /* When deleting whole RRSet, use ANY class */
+ if (rrp->r_data_length == 0) {
+ rrp->r_class = KNOT_CLASS_ANY;
+ } else {
+ rrp->r_class = KNOT_CLASS_NONE;
+ }
+
+ return rr_list_append(rrp, &params->update_list, &params->mm);
+}
+
+int cmd_class(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ uint16_t cls;
+
+ if (knot_rrclass_from_string(lp, &cls) != 0) {
+ ERR("failed to parse class '%s'", lp);
+ return KNOT_EPARSEFAIL;
+ }
+
+ params->class_num = cls;
+ params->parser.default_class = params->class_num;
+
+ return KNOT_EOK;
+}
+
+int cmd_ttl(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ uint32_t ttl = 0;
+
+ if (str_to_u32(lp, &ttl) != KNOT_EOK) {
+ ERR("failed to parse ttl '%s'", lp);
+ return KNOT_EPARSEFAIL;
+ }
+
+ return knsupdate_set_ttl(params, ttl);
+}
+
+int cmd_debug(const char* lp, _unused_ knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ msg_enable_debug(1);
+
+ return KNOT_EOK;
+}
+
+int cmd_nxdomain(const char *lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ zs_scanner_t *s = &params->parser;
+ int ret = parse_partial_rr(s, lp, PARSE_NODEFAULT | PARSE_NAMEONLY);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ s->r_ttl = 0;
+ s->r_class = KNOT_CLASS_NONE;
+
+ return rr_list_append(s, &params->prereq_list, &params->mm);
+}
+
+int cmd_yxdomain(const char *lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ zs_scanner_t *s = &params->parser;
+ int ret = parse_partial_rr(s, lp, PARSE_NODEFAULT | PARSE_NAMEONLY);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ s->r_ttl = 0;
+ s->r_class = KNOT_CLASS_ANY;
+
+ return rr_list_append(s, &params->prereq_list, &params->mm);
+}
+
+int cmd_nxrrset(const char *lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ zs_scanner_t *s = &params->parser;
+ int ret = parse_partial_rr(s, lp, PARSE_NOTTL);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ /* Check owner name. */
+ if (s->r_owner_length == 0) {
+ ERR("failed to parse prereq owner name '%s'", lp);
+ return KNOT_EPARSEFAIL;
+ }
+
+ s->r_ttl = 0;
+ s->r_class = KNOT_CLASS_NONE;
+
+ return rr_list_append(s, &params->prereq_list, &params->mm);
+}
+
+int cmd_yxrrset(const char *lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ zs_scanner_t *s = &params->parser;
+ int ret = parse_partial_rr(s, lp, PARSE_NOTTL);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ /* Check owner name. */
+ if (s->r_owner_length == 0) {
+ ERR("failed to parse prereq owner name '%s'", lp);
+ return KNOT_EPARSEFAIL;
+ }
+
+ s->r_ttl = 0;
+ if (s->r_data_length > 0) {
+ s->r_class = KNOT_CLASS_IN;
+ } else {
+ s->r_class = KNOT_CLASS_ANY;
+ }
+
+ return rr_list_append(s, &params->prereq_list, &params->mm);
+}
+
+int cmd_prereq(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ /* Scan prereq specifier ([ny]xrrset|[ny]xdomain) */
+ int prereq_type = tok_find(lp, pq_array);
+ if (prereq_type < 0) {
+ return prereq_type;
+ }
+
+ const char *tok = pq_array[prereq_type];
+ DBG("%s: type %s", __func__, TOK_S(tok));
+ lp = tok_skipspace(lp + TOK_L(tok));
+ if (strlen(lp) == 0) {
+ ERR("missing prerequisite owner name");
+ return KNOT_EINVAL;
+ }
+
+ int ret = KNOT_EOK;
+ switch(prereq_type) {
+ case PQ_NXDOMAIN:
+ ret = cmd_nxdomain(lp, params);
+ break;
+ case PQ_YXDOMAIN:
+ ret = cmd_yxdomain(lp, params);
+ break;
+ case PQ_NXRRSET:
+ ret = cmd_nxrrset(lp, params);
+ break;
+ case PQ_YXRRSET:
+ ret = cmd_yxrrset(lp, params);
+ break;
+ default:
+ ret = KNOT_ERROR;
+ }
+
+ return ret;
+}
+
+int cmd_exit(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ params->stop = true;
+
+ return KNOT_EOK;
+}
+
+int cmd_send(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+ DBG("sending packet");
+
+ if (params->zone == NULL) {
+ ERR("no zone specified");
+ return KNOT_EINVAL;
+ }
+
+ /* Build query packet. */
+ int ret = build_query(params);
+ if (ret != KNOT_EOK) {
+ ERR("failed to build UPDATE message (%s)", knot_strerror(ret));
+ return ret;
+ }
+
+ /* Sign if key specified. */
+ sign_context_t sign_ctx = { 0 };
+ if (params->tsig_key.name) {
+ ret = sign_context_init_tsig(&sign_ctx, &params->tsig_key);
+ if (ret != KNOT_EOK) {
+ ERR("failed to initialize signing context (%s)",
+ knot_strerror(ret));
+ return ret;
+ }
+
+ ret = sign_packet(params->query, &sign_ctx);
+ if (ret != KNOT_EOK) {
+ ERR("failed to sign UPDATE message (%s)",
+ knot_strerror(ret));
+ sign_context_deinit(&sign_ctx);
+ return ret;
+ }
+ }
+
+ int rb = 0;
+ /* Send/recv message (1 try + N retries). */
+ int tries = 1 + params->retries;
+ for (; tries > 0; --tries) {
+ rb = pkt_sendrecv(params);
+ if (rb > 0) {
+ break;
+ }
+ }
+
+ /* Check Send/recv result. */
+ if (rb <= 0) {
+ sign_context_deinit(&sign_ctx);
+ return KNOT_ECONNREFUSED;
+ }
+
+ /* Parse response. */
+ ret = knot_pkt_parse(params->answer, KNOT_PF_NOCANON);
+ if (ret != KNOT_EOK) {
+ ERR("failed to parse response (%s)", knot_strerror(ret));
+ sign_context_deinit(&sign_ctx);
+ return ret;
+ }
+
+ /* Check signature if expected. */
+ if (params->tsig_key.name) {
+ ret = verify_packet(params->answer, &sign_ctx);
+ sign_context_deinit(&sign_ctx);
+ if (ret != KNOT_EOK) {
+ print_packet(params->answer, NULL, 0, -1, 0, true,
+ &params->style);
+ ERR("reply verification (%s)", knot_strerror(ret));
+ return ret;
+ }
+ }
+
+ /* Free RRSet lists. */
+ knsupdate_reset(params);
+
+ /* Check return code. */
+ if (knot_pkt_ext_rcode(params->answer) != KNOT_RCODE_NOERROR) {
+ print_packet(params->answer, NULL, 0, -1, 0, true, &params->style);
+ ERR("update failed with error '%s'",
+ knot_pkt_ext_rcode_name(params->answer));
+ ret = KNOT_ERROR;
+ } else {
+ DBG("update success");
+ }
+
+ return ret;
+}
+
+int cmd_zone(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ /* Check zone name. */
+ if (!dname_isvalid(lp)) {
+ ERR("failed to parse zone '%s'", lp);
+ return KNOT_EPARSEFAIL;
+ }
+
+ free(params->zone);
+ params->zone = strdup(lp);
+
+ return KNOT_EOK;
+}
+
+int cmd_server(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ /* Parse host. */
+ srv_info_t *srv = parse_host(lp, params->server->service);
+ if (!srv) {
+ ERR("failed to parse server '%s'", lp);
+ return KNOT_ENOMEM;
+ }
+
+ srv_info_free(params->server);
+ params->server = srv;
+
+ return KNOT_EOK;
+}
+
+int cmd_local(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ /* Parse host. */
+ srv_info_t *srv = parse_host(lp, "0");
+ if (!srv) {
+ ERR("failed to parse local '%s'", lp);
+ return KNOT_ENOMEM;
+ }
+
+ srv_info_free(params->srcif);
+ params->srcif = srv;
+
+ return KNOT_EOK;
+}
+
+int cmd_show(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ if (!params->query) {
+ return KNOT_EOK;
+ }
+
+ printf("Update query:\n");
+ build_query(params);
+ print_packet(params->query, NULL, 0, -1, 0, false, &params->style);
+ printf("\n");
+
+ return KNOT_EOK;
+}
+
+int cmd_answer(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ if (!params->answer) {
+ return KNOT_EOK;
+ }
+
+ printf("Answer:\n");
+ print_packet(params->answer, NULL, 0, -1, 0, true, &params->style);
+
+ return KNOT_EOK;
+}
+
+int cmd_key(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ /* Convert to default format. */
+ char *kstr = strdup(lp);
+ if (!kstr) {
+ return KNOT_ENOMEM;
+ }
+
+ int ret = KNOT_EOK;
+
+ /* Search for the name secret separation. Allow also alg:name:key form. */
+ char *sep = strchr(kstr, ' ');
+ if (sep != NULL) {
+ /* Replace ' ' with ':'. More spaces are ignored in base64. */
+ *sep = ':';
+ }
+
+ /* Override existing key. */
+ knot_tsig_key_deinit(&params->tsig_key);
+
+ ret = knot_tsig_key_init_str(&params->tsig_key, kstr);
+ if (ret != KNOT_EOK) {
+ ERR("invalid key specification");
+ }
+
+ free(kstr);
+
+ return ret;
+}
+
+int cmd_origin(const char* lp, knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ /* Check zone name. */
+ if (!dname_isvalid(lp)) {
+ ERR("failed to parse zone '%s'", lp);
+ return KNOT_EPARSEFAIL;
+ }
+
+ return knsupdate_set_origin(params, lp);
+}
+
+/*
+ * Not implemented.
+ */
+
+int cmd_gsstsig(const char* lp, _unused_ knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ ERR("gsstsig not supported");
+
+ return KNOT_ENOTSUP;
+}
+
+int cmd_oldgsstsig(const char* lp, _unused_ knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ ERR("oldgsstsig not supported");
+
+ return KNOT_ENOTSUP;
+}
+
+int cmd_realm(const char* lp, _unused_ knsupdate_params_t *params)
+{
+ DBG("%s: lp='%s'", __func__, lp);
+
+ ERR("realm not supported");
+
+ return KNOT_ENOTSUP;
+}
diff --git a/src/utils/knsupdate/knsupdate_exec.h b/src/utils/knsupdate/knsupdate_exec.h
new file mode 100644
index 0000000..36ca43b
--- /dev/null
+++ b/src/utils/knsupdate/knsupdate_exec.h
@@ -0,0 +1,25 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "utils/knsupdate/knsupdate_params.h"
+
+extern const char* knsupdate_cmd_array[];
+
+int knsupdate_exec(knsupdate_params_t *params);
+
+int knsupdate_process_line(const char *line, knsupdate_params_t *params);
diff --git a/src/utils/knsupdate/knsupdate_interactive.c b/src/utils/knsupdate/knsupdate_interactive.c
new file mode 100644
index 0000000..0e4ed66
--- /dev/null
+++ b/src/utils/knsupdate/knsupdate_interactive.c
@@ -0,0 +1,177 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <histedit.h>
+
+#include "contrib/string.h"
+#include "utils/common/lookup.h"
+#include "utils/common/msg.h"
+#include "utils/knsupdate/knsupdate_exec.h"
+#include "utils/knsupdate/knsupdate_interactive.h"
+
+#define HISTORY_FILE ".knsupdate_history"
+
+static char *prompt(EditLine *el)
+{
+ return PROGRAM_NAME"> ";
+}
+
+static void print_commands(void)
+{
+ printf("\n");
+
+ for (const char **cmd = knsupdate_cmd_array; *cmd != NULL; cmd++) {
+ printf(" %-18s\n", (*cmd) + 1);
+ }
+}
+
+static void cmds_lookup(EditLine *el, const char *str, size_t str_len)
+{
+ lookup_t lookup;
+ int ret = lookup_init(&lookup);
+ if (ret != KNOT_EOK) {
+ return;
+ }
+
+ // Fill the lookup with command names.
+ for (const char **desc = knsupdate_cmd_array; *desc != NULL; desc++) {
+ ret = lookup_insert(&lookup, (*desc) + 1, NULL);
+ if (ret != KNOT_EOK) {
+ goto cmds_lookup_finish;
+ }
+ }
+
+ (void)lookup_complete(&lookup, str, str_len, el, true);
+
+cmds_lookup_finish:
+ lookup_deinit(&lookup);
+}
+
+static unsigned char complete(EditLine *el, int ch)
+{
+ int argc, token, pos;
+ const char **argv;
+
+ const LineInfo *li = el_line(el);
+ Tokenizer *tok = tok_init(NULL);
+
+ // Parse the line.
+ int ret = tok_line(tok, li, &argc, &argv, &token, &pos);
+ if (ret != 0) {
+ goto complete_exit;
+ }
+
+ // Show possible commands.
+ if (argc == 0) {
+ print_commands();
+ goto complete_exit;
+ }
+
+ // Complete the command name.
+ if (token == 0) {
+ cmds_lookup(el, argv[0], pos);
+ goto complete_exit;
+ }
+
+ // Find the command descriptor.
+ const char **desc = knsupdate_cmd_array;
+ while (*desc != NULL && strcmp((*desc) + 1, argv[0]) != 0) {
+ desc++;
+ }
+ if (*desc == NULL) {
+ goto complete_exit;
+ }
+
+complete_exit:
+ tok_reset(tok);
+ tok_end(tok);
+
+ return CC_REDISPLAY;
+}
+
+int interactive_loop(knsupdate_params_t *params)
+{
+ char *hist_file = NULL;
+ const char *home = getenv("HOME");
+ if (home != NULL) {
+ hist_file = sprintf_alloc("%s/%s", home, HISTORY_FILE);
+ }
+ if (hist_file == NULL) {
+ INFO("failed to get home directory");
+ }
+
+ EditLine *el = el_init(PROGRAM_NAME, stdin, stdout, stderr);
+ if (el == NULL) {
+ ERR("interactive mode not available");
+ free(hist_file);
+ return KNOT_ERROR;
+ }
+
+ History *hist = history_init();
+ if (hist == NULL) {
+ ERR("interactive mode not available");
+ el_end(el);
+ free(hist_file);
+ return KNOT_ERROR;
+ }
+
+ HistEvent hev = { 0 };
+ history(hist, &hev, H_SETSIZE, 1000);
+ history(hist, &hev, H_SETUNIQUE, 1);
+ el_set(el, EL_HIST, history, hist);
+ history(hist, &hev, H_LOAD, hist_file);
+
+ el_set(el, EL_TERMINAL, NULL);
+ el_set(el, EL_EDITOR, "emacs");
+ el_set(el, EL_PROMPT, prompt);
+ el_set(el, EL_SIGNAL, 1);
+ el_source(el, NULL);
+
+ // Warning: these two el_sets()'s always leak -- in libedit2 library!
+ // For more details see this commit's message.
+ el_set(el, EL_ADDFN, PROGRAM_NAME"-complete",
+ "Perform "PROGRAM_NAME" completion.", complete);
+ el_set(el, EL_BIND, "^I", PROGRAM_NAME"-complete", NULL);
+
+ int count;
+ const char *line;
+ while ((line = el_gets(el, &count)) != NULL && count > 0) {
+ char command[count + 1];
+ memcpy(command, line, count);
+ command[count] = '\0';
+ // Removes trailing newline
+ size_t cmd_len = strcspn(command, "\n");
+ command[cmd_len] = '\0';
+
+ if (cmd_len > 0) {
+ history(hist, &hev, H_ENTER, command);
+ history(hist, &hev, H_SAVE, hist_file);
+ }
+
+ // Process the command.
+ (void)knsupdate_process_line(command, params);
+ if (params->stop) {
+ break;
+ }
+ }
+
+ history_end(hist);
+ free(hist_file);
+
+ el_end(el);
+
+ return KNOT_EOK;
+}
diff --git a/src/utils/knsupdate/knsupdate_interactive.h b/src/utils/knsupdate/knsupdate_interactive.h
new file mode 100644
index 0000000..e2a2428
--- /dev/null
+++ b/src/utils/knsupdate/knsupdate_interactive.h
@@ -0,0 +1,26 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "utils/knsupdate/knsupdate_params.h"
+
+/*!
+ * Executes an interactive processing loop.
+ *
+ * \param[in] params Utility parameters.
+ */
+int interactive_loop(knsupdate_params_t *params);
diff --git a/src/utils/knsupdate/knsupdate_main.c b/src/utils/knsupdate/knsupdate_main.c
new file mode 100644
index 0000000..ab65c1e
--- /dev/null
+++ b/src/utils/knsupdate/knsupdate_main.c
@@ -0,0 +1,46 @@
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+
+#include "libdnssec/crypto.h"
+#include "utils/knsupdate/knsupdate_exec.h"
+#include "utils/knsupdate/knsupdate_params.h"
+#include "libknot/libknot.h"
+
+int main(int argc, char *argv[])
+{
+
+ int ret = EXIT_SUCCESS;
+
+ tzset();
+
+ knsupdate_params_t params;
+ if (knsupdate_parse(&params, argc, argv) == KNOT_EOK) {
+ if (!params.stop) {
+ dnssec_crypto_init();
+ if (knsupdate_exec(&params) != KNOT_EOK) {
+ ret = EXIT_FAILURE;
+ }
+ dnssec_crypto_cleanup();
+ }
+ } else {
+ ret = EXIT_FAILURE;
+ }
+
+ knsupdate_clean(&params);
+ return ret;
+}
diff --git a/src/utils/knsupdate/knsupdate_params.c b/src/utils/knsupdate/knsupdate_params.c
new file mode 100644
index 0000000..f9fa41f
--- /dev/null
+++ b/src/utils/knsupdate/knsupdate_params.c
@@ -0,0 +1,304 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <getopt.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "utils/knsupdate/knsupdate_params.h"
+#include "utils/common/msg.h"
+#include "utils/common/netio.h"
+#include "libknot/libknot.h"
+#include "libknot/tsig.h"
+#include "contrib/mempattern.h"
+#include "contrib/strtonum.h"
+#include "contrib/ucw/mempool.h"
+
+#define DEFAULT_RETRIES_NSUPDATE 3
+#define DEFAULT_TIMEOUT_NSUPDATE 12
+
+static const style_t DEFAULT_STYLE_NSUPDATE = {
+ .format = FORMAT_NSUPDATE,
+ .style = {
+ .wrap = false,
+ .show_class = true,
+ .show_ttl = true,
+ .verbose = false,
+ .original_ttl = false,
+ .empty_ttl = false,
+ .human_ttl = false,
+ .human_timestamp = true,
+ .generic = false,
+ .ascii_to_idn = NULL
+ },
+ .show_query = false,
+ .show_header = true,
+ .show_section = true,
+ .show_edns = false,
+ .show_question = true,
+ .show_answer = true,
+ .show_authority = true,
+ .show_additional = true,
+ .show_tsig = true,
+ .show_footer = false
+};
+
+static int parser_set_default(zs_scanner_t *s, const char *fmt, ...)
+{
+ /* Format string. */
+ char buf[512]; /* Must suffice for domain name and TTL. */
+ va_list ap;
+ va_start(ap, fmt);
+ int n = vsnprintf(buf, sizeof(buf), fmt, ap);
+ va_end(ap);
+
+ if (n < 0 || (size_t)n >= sizeof(buf)) {
+ return ZS_EINVAL;
+ }
+
+ /* Buffer must contain newline */
+ if (zs_set_input_string(s, buf, n) != 0 ||
+ zs_parse_all(s) != 0) {
+ return s->error.code;
+ }
+
+ return KNOT_EOK;
+}
+
+static int knsupdate_init(knsupdate_params_t *params)
+{
+ memset(params, 0, sizeof(knsupdate_params_t));
+
+ /* Initialize lists. */
+ init_list(&params->qfiles);
+ init_list(&params->update_list);
+ init_list(&params->prereq_list);
+
+ /* Initialize memory context. */
+ mm_ctx_mempool(&params->mm, MM_DEFAULT_BLKSIZE);
+
+ /* Default server. */
+ params->server = srv_info_create(DEFAULT_IPV4_NAME, DEFAULT_DNS_PORT);
+ if (!params->server)
+ return KNOT_ENOMEM;
+
+ /* Default settings. */
+ params->ip = IP_ALL;
+ params->protocol = PROTO_ALL;
+ params->class_num = KNOT_CLASS_IN;
+ params->retries = DEFAULT_RETRIES_NSUPDATE;
+ params->wait = DEFAULT_TIMEOUT_NSUPDATE;
+
+ /* Initialize RR parser. */
+ if (zs_init(&params->parser, ".", params->class_num, 3600) != 0 ||
+ zs_set_processing(&params->parser, NULL, NULL, NULL) != 0) {
+ zs_deinit(&params->parser);
+ return KNOT_ENOMEM;
+ }
+
+ /* Default style. */
+ params->style = DEFAULT_STYLE_NSUPDATE;
+
+ /* Create query/answer packets. */
+ params->query = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, &params->mm);
+ params->answer = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, &params->mm);
+
+ return KNOT_EOK;
+}
+
+void knsupdate_clean(knsupdate_params_t *params)
+{
+ if (params == NULL) {
+ return;
+ }
+
+ /* Clear current query. */
+ knsupdate_reset(params);
+
+ /* Free qfiles. */
+ ptrlist_free(&params->qfiles, &params->mm);
+
+ srv_info_free(params->server);
+ srv_info_free(params->srcif);
+ free(params->zone);
+ zs_deinit(&params->parser);
+ knot_pkt_free(params->query);
+ knot_pkt_free(params->answer);
+ knot_tsig_key_deinit(&params->tsig_key);
+
+ /* Clean up the structure. */
+ mp_delete(params->mm.ctx);
+ memset(params, 0, sizeof(*params));
+}
+
+/*! \brief Free RRSet list. */
+static void rr_list_free(list_t *list, knot_mm_t *mm)
+{
+ assert(list != NULL);
+ assert(mm != NULL);
+
+ ptrnode_t *node;
+ WALK_LIST(node, *list) {
+ knot_rrset_t *rrset = (knot_rrset_t *)node->d;
+ knot_rrset_free(rrset, NULL);
+ }
+ ptrlist_free(list, mm);
+}
+
+void knsupdate_reset(knsupdate_params_t *params)
+{
+ /* Free ADD/REMOVE RRSets. */
+ rr_list_free(&params->update_list, &params->mm);
+
+ /* Free PREREQ RRSets. */
+ rr_list_free(&params->prereq_list, &params->mm);
+}
+
+static void print_help(void)
+{
+ printf("Usage: %s [-d] [-v] [-k keyfile | -y [hmac:]name:key]\n"
+ " [-p port] [-t timeout] [-r retries] [filename]\n",
+ PROGRAM_NAME);
+}
+
+int knsupdate_parse(knsupdate_params_t *params, int argc, char *argv[])
+{
+ if (params == NULL || argv == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ int ret = knsupdate_init(params);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // Long options.
+ struct option opts[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL }
+ };
+
+ /* Command line options processing. */
+ int opt = 0;
+ while ((opt = getopt_long(argc, argv, "dhDvVp:t:r:y:k:", opts, NULL))
+ != -1) {
+ switch (opt) {
+ case 'd':
+ case 'D': /* Extra debugging. */
+ msg_enable_debug(1);
+ break;
+ case 'h':
+ print_help();
+ params->stop = true;
+ return KNOT_EOK;
+ case 'v':
+ params->protocol = PROTO_TCP;
+ break;
+ case 'V':
+ print_version(PROGRAM_NAME);
+ params->stop = true;
+ return KNOT_EOK;
+ case 'p':
+ free(params->server->service);
+ params->server->service = strdup(optarg);
+ if (!params->server->service) {
+ ERR("failed to set default port '%s'", optarg);
+ return KNOT_ENOMEM;
+ }
+ break;
+ case 'r':
+ ret = str_to_u32(optarg, &params->retries);
+ if (ret != KNOT_EOK) {
+ ERR("invalid retries '%s'", optarg);
+ return ret;
+ }
+ break;
+ case 't':
+ ret = params_parse_wait(optarg, &params->wait);
+ if (ret != KNOT_EOK) {
+ ERR("invalid timeout '%s'", optarg);
+ return ret;
+ }
+ break;
+ case 'y':
+ knot_tsig_key_deinit(&params->tsig_key);
+ ret = knot_tsig_key_init_str(&params->tsig_key, optarg);
+ if (ret != KNOT_EOK) {
+ ERR("failed to parse key '%s'", optarg);
+ return ret;
+ }
+ break;
+ case 'k':
+ knot_tsig_key_deinit(&params->tsig_key);
+ ret = knot_tsig_key_init_file(&params->tsig_key, optarg);
+ if (ret != KNOT_EOK) {
+ ERR("failed to parse keyfile '%s'", optarg);
+ return ret;
+ }
+ break;
+ default:
+ print_help();
+ return KNOT_ENOTSUP;
+ }
+ }
+
+ /* No retries for TCP. */
+ if (params->protocol == PROTO_TCP) {
+ params->retries = 0;
+ } else {
+ /* If wait/tries < 1 s, set 1 second for each try. */
+ if (params->wait > 0 &&
+ (uint32_t)params->wait < ( 1 + params->retries)) {
+ params->wait = 1;
+ } else {
+ params->wait /= (1 + params->retries);
+ }
+ }
+
+ /* Process non-option parameters. */
+ for (; optind < argc; ++optind) {
+ ptrlist_add(&params->qfiles, argv[optind], &params->mm);
+ }
+
+ return ret;
+}
+
+int knsupdate_set_ttl(knsupdate_params_t *params, const uint32_t ttl)
+{
+ int ret = parser_set_default(&params->parser, "$TTL %u\n", ttl);
+ if (ret != KNOT_EOK) {
+ ERR("failed to set default TTL, %s", zs_strerror(ret));
+ }
+ return ret;
+}
+
+int knsupdate_set_origin(knsupdate_params_t *params, const char *origin)
+{
+ char *fqdn = get_fqd_name(origin);
+
+ int ret = parser_set_default(&params->parser, "$ORIGIN %s\n", fqdn);
+
+ free(fqdn);
+
+ if (ret != KNOT_EOK) {
+ ERR("failed to set default origin, %s", zs_strerror(ret));
+ }
+ return ret;
+}
diff --git a/src/utils/knsupdate/knsupdate_params.h b/src/utils/knsupdate/knsupdate_params.h
new file mode 100644
index 0000000..1933244
--- /dev/null
+++ b/src/utils/knsupdate/knsupdate_params.h
@@ -0,0 +1,72 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include "utils/common/netio.h"
+#include "utils/common/params.h"
+#include "utils/common/sign.h"
+#include "libknot/libknot.h"
+#include "libzscanner/scanner.h"
+#include "contrib/ucw/lists.h"
+
+#define PROGRAM_NAME "knsupdate"
+
+/*! \brief knsupdate-specific params data. */
+typedef struct {
+ /*!< Stop processing - just print help, version,... */
+ bool stop;
+ /*!< List of files with query data. */
+ list_t qfiles;
+ /*!< List of nameservers to query to. */
+ srv_info_t *server;
+ /*!< Local interface (optional). */
+ srv_info_t *srcif;
+ /*!< Version of ip protocol to use. */
+ ip_t ip;
+ /*!< Type (TCP, UDP) protocol to use. */
+ protocol_t protocol;
+ /*!< Default class number. */
+ uint16_t class_num;
+ /*!< Number of UDP retries. */
+ uint32_t retries;
+ /*!< Wait for network response in seconds (-1 means forever). */
+ int32_t wait;
+ /*!< Current zone. */
+ char *zone;
+ /*!< RR parser. */
+ zs_scanner_t parser;
+ /*!< Current packet. */
+ knot_pkt_t *query;
+ /*!< Current response. */
+ knot_pkt_t *answer;
+ /*< Lists of RRSets. */
+ list_t update_list, prereq_list;
+ /*!< Transaction signature context. */
+ knot_tsig_key_t tsig_key;
+ /*!< Default output settings. */
+ style_t style;
+ /*!< Memory context. */
+ knot_mm_t mm;
+} knsupdate_params_t;
+
+int knsupdate_parse(knsupdate_params_t *params, int argc, char *argv[]);
+int knsupdate_set_ttl(knsupdate_params_t *params, const uint32_t ttl);
+int knsupdate_set_origin(knsupdate_params_t *params, const char *origin);
+void knsupdate_clean(knsupdate_params_t *params);
+void knsupdate_reset(knsupdate_params_t *params);
diff --git a/src/utils/kxdpgun/ip_route.c b/src/utils/kxdpgun/ip_route.c
new file mode 100644
index 0000000..003c764
--- /dev/null
+++ b/src/utils/kxdpgun/ip_route.c
@@ -0,0 +1,358 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <time.h>
+
+#include <arpa/inet.h>
+#include <libmnl/libmnl.h>
+#include <linux/if_ether.h>
+#include <linux/if_link.h>
+#include <linux/rtnetlink.h>
+#include <net/if.h>
+
+#include "utils/kxdpgun/ip_route.h"
+#include "contrib/sockaddr.h"
+
+#define ROUTE_LOOKUP_LOOP_LIMIT 10000
+
+static size_t addr_len(int family)
+{
+ switch (family) {
+ case AF_INET:
+ return sizeof(struct in_addr);
+ case AF_INET6:
+ return sizeof(struct in6_addr);
+ default:
+ return 0;
+ }
+}
+
+static int send_dummy_pkt(const struct sockaddr_storage *ip)
+{
+ static const uint8_t dummy_pkt[] = {
+ // dummy data
+ 0x08, 0x00, 0xec, 0x72, 0x0b, 0x87, 0x00, 0x06,
+
+ //padding
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ };
+
+ int fd = socket(ip->ss_family, SOCK_RAW,
+ ip->ss_family == AF_INET6 ? IPPROTO_ICMPV6 : IPPROTO_ICMP);
+ if (fd < 0) {
+ return -errno;
+ }
+ int ret = sendto(fd, dummy_pkt, sizeof(dummy_pkt), 0, (const struct sockaddr *)ip,
+ ip->ss_family == AF_INET6 ? sizeof(struct sockaddr_in6) :
+ sizeof(struct sockaddr_in));
+ if (ret < 0) {
+ ret = -errno;
+ }
+ close(fd);
+ return ret;
+}
+
+static int netlink_query(int family, uint16_t type, mnl_cb_t cb, void *data,
+ void *qextra, size_t qextra_len, uint16_t qextra_type)
+{
+ // open and bind NETLINK socket
+ struct mnl_socket *nl = mnl_socket_open(NETLINK_ROUTE);
+ if (nl == NULL) {
+ return -errno;
+ }
+ if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
+ mnl_socket_close(nl);
+ return -errno;
+ }
+ unsigned portid = mnl_socket_get_portid(nl);
+ int ret = 0;
+
+ // allocate request
+ char buf[MNL_SOCKET_BUFFER_SIZE];
+ struct nlmsghdr *nlh = mnl_nlmsg_put_header(buf);
+ if (nlh == NULL) {
+ ret = -ENOMEM;
+ goto end;
+ }
+ unsigned seq = time(NULL);
+ nlh->nlmsg_seq = seq;
+ nlh->nlmsg_type = type;
+ nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+ struct rtmsg *rtm = mnl_nlmsg_put_extra_header(nlh, sizeof(*rtm));
+ if (rtm == NULL) {
+ ret = -ENOMEM;
+ goto end;
+ }
+
+ if (qextra_len > 0) {
+ nlh->nlmsg_flags = NLM_F_REQUEST;
+ rtm->rtm_dst_len = qextra_len * 8; // 8 bits per byte
+ mnl_attr_put(nlh, qextra_type, qextra_len, qextra);
+ }
+
+ // send request
+ rtm->rtm_family = family;
+ ret = mnl_socket_sendto(nl, nlh, nlh->nlmsg_len);
+ if (ret < 0) {
+ ret = -errno;
+ goto end;
+ }
+
+ // collect replies with callback
+ while ((ret = mnl_socket_recvfrom(nl, buf, sizeof(buf))) > 0) {
+ ret = mnl_cb_run(buf, ret, seq, portid, cb, data);
+ if (ret <= MNL_CB_STOP) {
+ break;
+ }
+ if (qextra_len > 0) {
+ break;
+ }
+ }
+ ret = ret < 0 ? -errno : 0;
+
+end:
+ mnl_socket_close(nl);
+ return ret;
+}
+
+typedef struct {
+ const struct sockaddr_storage *ip;
+ struct sockaddr_storage *via;
+ struct sockaddr_storage *src;
+ char *dev;
+ uint64_t priority; // top 32 bits: unmatched address bits; bottom 32 bits: route metric priority
+ unsigned match;
+
+ // intermediate callback data
+ const struct nlattr *tb[RTA_MAX+1];
+} ip_route_get_ctx_t;
+
+static int validate_attr_route(const struct nlattr *attr, void *data)
+{
+ /* skip unsupported attribute in user-space */
+ if (mnl_attr_type_valid(attr, RTA_MAX) < 0) {
+ return MNL_CB_OK;
+ }
+
+ ip_route_get_ctx_t *ctx = data;
+
+ int type = mnl_attr_get_type(attr);
+ switch(type) {
+ case RTA_TABLE:
+ case RTA_OIF:
+ case RTA_FLOW:
+ case RTA_PRIORITY:
+ if (mnl_attr_validate(attr, MNL_TYPE_U32) < 0) {
+ return MNL_CB_ERROR;
+ }
+ break;
+ case RTA_DST:
+ case RTA_SRC:
+ case RTA_PREFSRC:
+ case RTA_GATEWAY:
+ if (mnl_attr_validate2(attr, MNL_TYPE_BINARY, addr_len(ctx->ip->ss_family)) < 0) {
+ return MNL_CB_ERROR;
+ }
+ break;
+ case RTA_METRICS:
+ if (mnl_attr_validate(attr, MNL_TYPE_NESTED) < 0) {
+ return MNL_CB_ERROR;
+ }
+ break;
+ }
+ ctx->tb[type] = attr;
+ return MNL_CB_OK;
+}
+
+static void attr2addr(const struct nlattr *attr, int family, struct sockaddr_storage *out)
+{
+ if (attr == NULL) {
+ out->ss_family = AF_UNSPEC;
+ return;
+ }
+ out->ss_family = family;
+ if (family == AF_INET6) {
+ struct in6_addr *addr = mnl_attr_get_payload(attr);
+ memcpy(&((struct sockaddr_in6 *)out)->sin6_addr, addr, sizeof(*addr));
+ } else {
+ struct in_addr *addr = mnl_attr_get_payload(attr);
+ memcpy(&((struct sockaddr_in *)out)->sin_addr, addr, sizeof(*addr));
+ }
+}
+
+static void attr2dev(const struct nlattr *attr, char *out) // out must have IFNAMSIZ length
+{
+ *out = '\0';
+
+ if (attr != NULL) {
+ if_indextoname(mnl_attr_get_u32(attr), out);
+ }
+}
+
+static uint32_t attr2prio(const struct nlattr *attr)
+{
+ if (attr == NULL) {
+ return 0; // 0 is the default metric priority in linux
+ }
+ return mnl_attr_get_u32(attr);
+}
+
+static int ip_route_get_cb(const struct nlmsghdr *nlh, void *data)
+{
+ ip_route_get_ctx_t *ctx = data;
+ struct rtmsg *rm = mnl_nlmsg_get_payload(nlh);
+ if (rm->rtm_family != ctx->ip->ss_family) {
+ return MNL_CB_ERROR;
+ }
+
+ mnl_attr_parse(nlh, sizeof(*rm), validate_attr_route, data);
+
+ uint64_t new_metric = addr_len(rm->rtm_family) * 8 - rm->rtm_dst_len;
+ new_metric = (new_metric << 32) + attr2prio(ctx->tb[RTA_PRIORITY]);
+ if (new_metric >= ctx->priority) {
+ return MNL_CB_OK;
+ }
+
+ struct sockaddr_storage dst;
+ attr2addr(ctx->tb[RTA_DST], rm->rtm_family, &dst);
+
+ if (rm->rtm_dst_len == 0 ||
+ sockaddr_net_match(&dst, ctx->ip, rm->rtm_dst_len)) {
+ attr2addr(ctx->tb[RTA_PREFSRC], rm->rtm_family, ctx->src);
+ attr2addr(ctx->tb[RTA_GATEWAY], rm->rtm_family, ctx->via);
+ attr2dev(ctx->tb[RTA_OIF], ctx->dev);
+ ctx->match++;
+ ctx->priority = new_metric;
+ }
+
+ memset(ctx->tb, 0, sizeof(void *) * (RTA_MAX+1));
+ return MNL_CB_OK;
+}
+
+int ip_route_get(const struct sockaddr_storage *ip,
+ struct sockaddr_storage *via,
+ struct sockaddr_storage *src,
+ char *dev)
+{
+ struct sockaddr_storage last_via = { 0 };
+ ip_route_get_ctx_t ctx = { ip, &last_via, src, dev, 0, 0 };
+ do {
+ ctx.priority = UINT64_MAX;
+
+ size_t qextra_len;
+ void *qextra = sockaddr_raw(ip, &qextra_len);
+ int ret = netlink_query(ip->ss_family, RTM_GETROUTE,
+ ip_route_get_cb, &ctx, qextra,
+ qextra_len, IFA_ADDRESS);
+ if (ret != 0) {
+ return ret;
+ }
+ if (last_via.ss_family == ip->ss_family) { // not AF_UNSPEC
+ memcpy(via, &last_via, sizeof(*via));
+ }
+
+ // next loop will search for path to "via"
+ ctx.ip = via;
+ } while (last_via.ss_family != AF_UNSPEC &&
+ ctx.priority != UINT64_MAX && // avoid loop when nothing found
+ ctx.match < ROUTE_LOOKUP_LOOP_LIMIT); // avoid loop when looped route
+
+ return src->ss_family == ip->ss_family ? 0 : -ENOENT;
+}
+
+typedef struct {
+ const struct sockaddr_storage *ip;
+ uint8_t *mac;
+ unsigned match;
+
+ // intermediate callback data
+ const struct nlattr *tb[RTA_MAX+1];
+} ip_neigh_ctx_t;
+
+static int validate_attr_neigh(const struct nlattr *attr, void *data)
+{
+ /* skip unsupported attribute in user-space */
+ if (mnl_attr_type_valid(attr, NDA_MAX) < 0) {
+ return MNL_CB_OK;
+ }
+
+ ip_neigh_ctx_t *ctx = data;
+
+ int type = mnl_attr_get_type(attr);
+ switch (type) {
+ case NDA_DST:
+ if (mnl_attr_validate2(attr, MNL_TYPE_BINARY, addr_len(ctx->ip->ss_family)) < 0) {
+ return MNL_CB_ERROR;
+ }
+ break;
+ case NDA_LLADDR:
+ if (mnl_attr_validate2(attr, MNL_TYPE_BINARY, ETH_ALEN) < 0) {
+ return MNL_CB_ERROR;
+ }
+ break;
+ }
+
+ ctx->tb[type] = attr;
+ return MNL_CB_OK;
+}
+
+static int ip_neigh_cb(const struct nlmsghdr *nlh, void *data)
+{
+ ip_neigh_ctx_t *ctx = data;
+ struct rtmsg *rm = mnl_nlmsg_get_payload(nlh);
+ if (rm->rtm_family != ctx->ip->ss_family) {
+ return MNL_CB_ERROR;
+ }
+
+ mnl_attr_parse(nlh, sizeof(*rm), validate_attr_neigh, data);
+
+ struct sockaddr_storage dst;
+ attr2addr(ctx->tb[NDA_DST], rm->rtm_family, &dst);
+
+ if (sockaddr_cmp((struct sockaddr_storage *)&dst, (struct sockaddr_storage *)ctx->ip, true) == 0 &&
+ ctx->tb[NDA_LLADDR] != NULL) {
+ memcpy(ctx->mac, mnl_attr_get_payload(ctx->tb[NDA_LLADDR]), ETH_ALEN);
+ ctx->match++;
+ }
+
+ memset(ctx->tb, 0, sizeof(void *) * (RTA_MAX+1));
+ return MNL_CB_OK;
+}
+
+int ip_neigh_get(const struct sockaddr_storage *ip, bool dummy_sendto, uint8_t *mac)
+{
+ if (dummy_sendto) {
+ int ret = send_dummy_pkt(ip);
+ if (ret < 0) {
+ return ret;
+ }
+ usleep(10000);
+ }
+ ip_neigh_ctx_t ctx = { ip, mac, 0 };
+ int ret = netlink_query(ip->ss_family, RTM_GETNEIGH, ip_neigh_cb, &ctx,
+ NULL, 0, 0);
+ if (ret == 0 && ctx.match == 0) {
+ return -ENOENT;
+ }
+ return ret;
+}
diff --git a/src/utils/kxdpgun/ip_route.h b/src/utils/kxdpgun/ip_route.h
new file mode 100644
index 0000000..c35c25b
--- /dev/null
+++ b/src/utils/kxdpgun/ip_route.h
@@ -0,0 +1,46 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdint.h>
+#include <sys/socket.h>
+
+/*!
+ * \brief Get route to an IPv4/6 from system routing table.
+ *
+ * \param ip IPv4 or IPv6 to search route to.
+ * \param via Out: gateway (first hop) on the route, or AF_UNSPEC if same subnet.
+ * \param src Out: local outgoing IP address on the route.
+ * \param dev Out: local network interface on the route (you must pre-allocate IFNAMSIZ bytes!).
+ *
+ * \return 0 on success, negative errno otherwise.
+ */
+int ip_route_get(const struct sockaddr_storage *ip,
+ struct sockaddr_storage *via,
+ struct sockaddr_storage *src,
+ char *dev);
+
+/*!
+ * \brief Obtain neighbour's MAC addr from system neighbour table.
+ *
+ * \param ip IPv4 or IPv6 of the neighbour in question.
+ * \param dummy_sendto Attempt sendto() to target IP in order to let the system fill the neighbour table.
+ * \param mac Out: MAC address of the neighbour (you must pre-allocate ETH_ALEN bytes!).
+ *
+ * \return 0 on success, -ENOENT if neighbour not found, negative errno otherwise.
+ */
+int ip_neigh_get(const struct sockaddr_storage *ip, bool dummy_sendto, uint8_t *mac);
diff --git a/src/utils/kxdpgun/load_queries.c b/src/utils/kxdpgun/load_queries.c
new file mode 100644
index 0000000..8ecac48
--- /dev/null
+++ b/src/utils/kxdpgun/load_queries.c
@@ -0,0 +1,164 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "load_queries.h"
+#include "libknot/libknot.h"
+#include "utils/common/msg.h"
+
+#define ERR_PREFIX "failed loading queries "
+
+enum qflags {
+ QFLAG_EDNS = 1,
+ QFLAG_DO = 2,
+};
+
+struct pkt_payload *global_payloads = NULL;
+
+void free_global_payloads(void)
+{
+ struct pkt_payload *g_payloads_p = global_payloads, *tmp;
+ while (g_payloads_p != NULL) {
+ tmp = g_payloads_p;
+ g_payloads_p = tmp->next;
+ free(tmp);
+ }
+ global_payloads = NULL;
+}
+
+bool load_queries(const char *filename, uint16_t edns_size, uint16_t msgid, size_t maxcount)
+{
+ size_t read = 0;
+ FILE *f = fopen(filename, "r");
+ if (f == NULL) {
+ ERR2(ERR_PREFIX "file '%s' (%s)", filename, strerror(errno));
+ return false;
+ }
+ struct pkt_payload *g_payloads_top = NULL;
+
+ struct {
+ char line[KNOT_DNAME_TXT_MAXLEN + 256];
+ char dname_txt[KNOT_DNAME_TXT_MAXLEN + 1];
+ uint8_t dname[KNOT_DNAME_MAXLEN];
+ char type_txt[128];
+ char flags_txt[128];
+ } *bufs;
+ bufs = malloc(sizeof(*bufs)); // avoiding too much stuff on stack
+ if (bufs == NULL) {
+ ERR2(ERR_PREFIX "(out of memory)");
+ goto fail;
+ }
+
+ while (read < maxcount) {
+ if (fgets(bufs->line, sizeof(bufs->line), f) == NULL) {
+ break;
+ }
+ bufs->flags_txt[0] = '\0';
+ int ret = sscanf(bufs->line, "%s%s%s", bufs->dname_txt, bufs->type_txt, bufs->flags_txt);
+ if (ret < 2) {
+ ERR2(ERR_PREFIX "(faulty line): '%.*s'",
+ (int)strcspn(bufs->line, "\n"), bufs->line);
+ goto fail;
+ }
+
+ void *pret = knot_dname_from_str(bufs->dname, bufs->dname_txt, sizeof(bufs->dname));
+ if (pret == NULL) {
+ ERR2(ERR_PREFIX "(faulty dname): '%s'", bufs->dname_txt);
+ goto fail;
+ }
+
+ uint16_t type;
+ ret = knot_rrtype_from_string(bufs->type_txt, &type);
+ if (ret < 0) {
+ ERR2(ERR_PREFIX "(faulty type): '%s'", bufs->type_txt);
+ goto fail;
+ }
+
+ enum qflags flags = 0;
+ switch (bufs->flags_txt[0]) {
+ case '\0':
+ break;
+ case 'e':
+ case 'E':
+ flags |= QFLAG_EDNS;
+ break;
+ case 'd':
+ case 'D':
+ flags |= QFLAG_EDNS | QFLAG_DO;
+ break;
+ default:
+ ERR2(ERR_PREFIX "(faulty flag): '%s'", bufs->flags_txt);
+ goto fail;
+ }
+
+ size_t dname_len = knot_dname_size(bufs->dname);
+ size_t pkt_len = KNOT_WIRE_HEADER_SIZE + 2 * sizeof(uint16_t) + dname_len;
+ if (flags & QFLAG_EDNS) {
+ pkt_len += KNOT_EDNS_MIN_SIZE;
+ }
+
+ struct pkt_payload *pkt = calloc(1, sizeof(struct pkt_payload) + pkt_len);
+ if (pkt == NULL) {
+ ERR2(ERR_PREFIX "(out of memory)");
+ goto fail;
+ }
+ pkt->len = pkt_len;
+ memcpy(pkt->payload, &msgid, sizeof(msgid));
+ pkt->payload[2] = 0x01; // RD bit
+ pkt->payload[5] = 0x01; // 1 question
+ pkt->payload[11] = (flags & QFLAG_EDNS) ? 0x01 : 0x00;
+ memcpy(pkt->payload + 12, bufs->dname, dname_len);
+ pkt->payload[dname_len + 12] = type >> 8;
+ pkt->payload[dname_len + 13] = type & 0xff;
+ pkt->payload[dname_len + 15] = KNOT_CLASS_IN;
+ if (flags & QFLAG_EDNS) {
+ pkt->payload[dname_len + 18] = KNOT_RRTYPE_OPT;
+ pkt->payload[dname_len + 19] = edns_size >> 8;
+ pkt->payload[dname_len + 20] = edns_size & 0xff;
+ pkt->payload[dname_len + 23] = (flags & QFLAG_DO) ? 0x80 : 0x00;
+ }
+
+ // add pkt to list global_payloads
+ if (g_payloads_top == NULL) {
+ global_payloads = pkt;
+ g_payloads_top = pkt;
+ } else {
+ g_payloads_top->next = pkt;
+ g_payloads_top = pkt;
+ }
+ read++;
+ }
+
+ if (global_payloads == NULL) {
+ ERR2(ERR_PREFIX "(no queries in file)");
+ goto fail;
+ }
+
+ free(bufs);
+ fclose(f);
+ return true;
+
+fail:
+ free_global_payloads();
+ free(bufs);
+ fclose(f);
+ return false;
+}
diff --git a/src/utils/kxdpgun/load_queries.h b/src/utils/kxdpgun/load_queries.h
new file mode 100644
index 0000000..3d7bace
--- /dev/null
+++ b/src/utils/kxdpgun/load_queries.h
@@ -0,0 +1,32 @@
+/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+
+struct pkt_payload {
+ struct pkt_payload *next;
+ size_t len;
+ uint8_t payload[];
+};
+
+extern struct pkt_payload *global_payloads;
+
+bool load_queries(const char *filename, uint16_t edns_size, uint16_t msgid, size_t maxcount);
+
+void free_global_payloads(void);
diff --git a/src/utils/kxdpgun/main.c b/src/utils/kxdpgun/main.c
new file mode 100644
index 0000000..c9db312
--- /dev/null
+++ b/src/utils/kxdpgun/main.c
@@ -0,0 +1,1502 @@
+/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <getopt.h>
+#include <ifaddrs.h>
+#include <inttypes.h>
+#include <net/if.h>
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <net/if.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/resource.h>
+
+#include "libknot/libknot.h"
+#include "libknot/xdp.h"
+#include "libknot/xdp/tcp_iobuf.h"
+#ifdef ENABLE_QUIC
+#include <gnutls/gnutls.h>
+#include "libknot/quic/quic.h"
+#endif // ENABLE_QUIC
+#include "contrib/macros.h"
+#include "contrib/mempattern.h"
+#include "contrib/openbsd/strlcat.h"
+#include "contrib/openbsd/strlcpy.h"
+#include "contrib/os.h"
+#include "contrib/sockaddr.h"
+#include "contrib/toeplitz.h"
+#include "contrib/ucw/mempool.h"
+#include "utils/common/msg.h"
+#include "utils/common/params.h"
+#include "utils/kxdpgun/ip_route.h"
+#include "utils/kxdpgun/load_queries.h"
+
+#define PROGRAM_NAME "kxdpgun"
+#define SPACE " "
+
+enum {
+ KXDPGUN_WAIT,
+ KXDPGUN_START,
+ KXDPGUN_STOP,
+};
+
+volatile int xdp_trigger = KXDPGUN_WAIT;
+
+volatile unsigned stats_trigger = 0;
+
+unsigned global_cpu_aff_start = 0;
+unsigned global_cpu_aff_step = 1;
+
+#define REMOTE_PORT_DEFAULT 53
+#define REMOTE_PORT_DOQ_DEFAULT 853
+#define LOCAL_PORT_MIN 2000
+#define LOCAL_PORT_MAX 65535
+#define QUIC_THREAD_PORTS 100
+
+#define RCODE_MAX (0x0F + 1)
+
+typedef struct {
+ size_t collected;
+ uint64_t duration;
+ uint64_t qry_sent;
+ uint64_t synack_recv;
+ uint64_t ans_recv;
+ uint64_t finack_recv;
+ uint64_t rst_recv;
+ uint64_t size_recv;
+ uint64_t wire_recv;
+ uint64_t rcodes_recv[RCODE_MAX];
+ pthread_mutex_t mutex;
+} kxdpgun_stats_t;
+
+static kxdpgun_stats_t global_stats = { 0 };
+
+typedef enum {
+ KXDPGUN_IGNORE_NONE = 0,
+ KXDPGUN_IGNORE_QUERY = (1 << 0),
+ KXDPGUN_IGNORE_LASTBYTE = (1 << 1),
+ KXDPGUN_IGNORE_CLOSE = (1 << 2),
+ KXDPGUN_REUSE_CONN = (1 << 3),
+} xdp_gun_ignore_t;
+
+typedef struct {
+ union {
+ struct sockaddr_in local_ip4;
+ struct sockaddr_in6 local_ip;
+ struct sockaddr_storage local_ip_ss;
+ };
+ union {
+ struct sockaddr_in target_ip4;
+ struct sockaddr_in6 target_ip;
+ struct sockaddr_storage target_ip_ss;
+ };
+ char dev[IFNAMSIZ];
+ uint64_t qps, duration;
+ unsigned at_once;
+ uint16_t msgid;
+ uint16_t edns_size;
+ uint16_t vlan_tci;
+ uint8_t local_mac[6], target_mac[6];
+ uint8_t local_ip_range;
+ bool ipv6;
+ bool tcp;
+ bool quic;
+ bool quic_full_handshake;
+ const char *qlog_dir;
+ const char *sending_mode;
+ xdp_gun_ignore_t ignore1;
+ knot_tcp_ignore_t ignore2;
+ uint16_t target_port;
+ knot_xdp_filter_flag_t flags;
+ unsigned n_threads, thread_id;
+ knot_eth_rss_conf_t *rss_conf;
+ knot_xdp_config_t xdp_config;
+} xdp_gun_ctx_t;
+
+const static xdp_gun_ctx_t ctx_defaults = {
+ .dev[0] = '\0',
+ .edns_size = 1232,
+ .qps = 1000,
+ .duration = 5000000UL, // usecs
+ .at_once = 10,
+ .sending_mode = "",
+ .target_port = 0,
+ .flags = KNOT_XDP_FILTER_UDP | KNOT_XDP_FILTER_PASS,
+ .xdp_config = { .extra_frames = true },
+};
+
+static void sigterm_handler(int signo)
+{
+ assert(signo == SIGTERM || signo == SIGINT);
+ xdp_trigger = KXDPGUN_STOP;
+}
+
+static void sigusr_handler(int signo)
+{
+ assert(signo == SIGUSR1);
+ if (global_stats.collected == 0) {
+ stats_trigger++;
+ }
+}
+
+static void clear_stats(kxdpgun_stats_t *st)
+{
+ pthread_mutex_lock(&st->mutex);
+ st->duration = 0;
+ st->qry_sent = 0;
+ st->synack_recv = 0;
+ st->ans_recv = 0;
+ st->finack_recv = 0;
+ st->rst_recv = 0;
+ st->size_recv = 0;
+ st->wire_recv = 0;
+ st->collected = 0;
+ memset(st->rcodes_recv, 0, sizeof(st->rcodes_recv));
+ pthread_mutex_unlock(&st->mutex);
+}
+
+static size_t collect_stats(kxdpgun_stats_t *into, const kxdpgun_stats_t *what)
+{
+ pthread_mutex_lock(&into->mutex);
+ into->duration = MAX(into->duration, what->duration);
+ into->qry_sent += what->qry_sent;
+ into->synack_recv += what->synack_recv;
+ into->ans_recv += what->ans_recv;
+ into->finack_recv += what->finack_recv;
+ into->rst_recv += what->rst_recv;
+ into->size_recv += what->size_recv;
+ into->wire_recv += what->wire_recv;
+ for (int i = 0; i < RCODE_MAX; i++) {
+ into->rcodes_recv[i] += what->rcodes_recv[i];
+ }
+ size_t res = ++into->collected;
+ pthread_mutex_unlock(&into->mutex);
+ return res;
+}
+
+static void print_stats(kxdpgun_stats_t *st, bool tcp, bool quic, bool recv, uint64_t qps)
+{
+ pthread_mutex_lock(&st->mutex);
+
+#define ps(counter) ((counter) * 1000 / (st->duration / 1000))
+#define pct(counter) ((counter) * 100.0 / st->qry_sent)
+
+ const char *name = tcp ? "SYNs: " : quic ? "initials:" : "queries: ";
+ printf("total %s %"PRIu64" (%"PRIu64" pps) (%f%%)\n", name,
+ st->qry_sent, ps(st->qry_sent), 100.0 * st->qry_sent / (st->duration / 1000000.0 * qps));
+ if (st->qry_sent > 0 && recv) {
+ if (tcp || quic) {
+ name = tcp ? "established:" : "handshakes: ";
+ printf("total %s %"PRIu64" (%"PRIu64" pps) (%f%%)\n", name,
+ st->synack_recv, ps(st->synack_recv), pct(st->synack_recv));
+ }
+ printf("total replies: %"PRIu64" (%"PRIu64" pps) (%f%%)\n",
+ st->ans_recv, ps(st->ans_recv), pct(st->ans_recv));
+ if (tcp) {
+ printf("total closed: %"PRIu64" (%"PRIu64" pps) (%f%%)\n",
+ st->finack_recv, ps(st->finack_recv), pct(st->finack_recv));
+ }
+ if (st->rst_recv > 0) {
+ printf("total reset: %"PRIu64" (%"PRIu64" pps) (%f%%)\n",
+ st->rst_recv, ps(st->rst_recv), pct(st->rst_recv));
+ }
+ printf("average DNS reply size: %"PRIu64" B\n",
+ st->ans_recv > 0 ? st->size_recv / st->ans_recv : 0);
+ printf("average Ethernet reply rate: %"PRIu64" bps (%.2f Mbps)\n",
+ ps(st->wire_recv * 8), ps((float)st->wire_recv * 8 / (1000 * 1000)));
+
+ for (int i = 0; i < RCODE_MAX; i++) {
+ if (st->rcodes_recv[i] > 0) {
+ const knot_lookup_t *rcode = knot_lookup_by_id(knot_rcode_names, i);
+ const char *rcname = rcode == NULL ? "unknown" : rcode->name;
+ int space = MAX(9 - strlen(rcname), 0);
+ printf("responded %s: %.*s%"PRIu64"\n",
+ rcname, space, " ", st->rcodes_recv[i]);
+ }
+ }
+ }
+ printf("duration: %"PRIu64" s\n", (st->duration / (1000 * 1000)));
+
+ pthread_mutex_unlock(&st->mutex);
+}
+
+inline static void timer_start(struct timespec *timesp)
+{
+ clock_gettime(CLOCK_MONOTONIC, timesp);
+}
+
+inline static uint64_t timer_end(struct timespec *timesp)
+{
+ struct timespec end;
+ clock_gettime(CLOCK_MONOTONIC, &end);
+ uint64_t res = (end.tv_sec - timesp->tv_sec) * (uint64_t)1000000;
+ res += ((int64_t)end.tv_nsec - timesp->tv_nsec) / 1000;
+ return res;
+}
+
+static unsigned addr_bits(bool ipv6)
+{
+ return ipv6 ? 128 : 32;
+}
+
+static void shuffle_sockaddr4(struct sockaddr_in *dst, struct sockaddr_in *src, uint64_t increment)
+{
+ memcpy(&dst->sin_addr, &src->sin_addr, sizeof(dst->sin_addr));
+ if (increment > 0) {
+ dst->sin_addr.s_addr = htobe32(be32toh(src->sin_addr.s_addr) + increment);
+ }
+}
+
+static void shuffle_sockaddr6(struct sockaddr_in6 *dst, struct sockaddr_in6 *src, uint64_t increment)
+{
+ memcpy(&dst->sin6_addr, &src->sin6_addr, sizeof(dst->sin6_addr));
+ if (increment > 0) {
+ uint64_t *dst_addr = (uint64_t *)&dst->sin6_addr;
+ uint64_t *src_addr = (uint64_t *)&src->sin6_addr;
+ dst_addr[1] = htobe64(be64toh(src_addr[1]) + increment);
+ }
+}
+
+static void shuffle_sockaddr(struct sockaddr_in6 *dst, struct sockaddr_in6 *src,
+ uint16_t port, uint64_t increment)
+{
+ dst->sin6_family = src->sin6_family;
+ dst->sin6_port = htobe16(port);
+ if (src->sin6_family == AF_INET6) {
+ shuffle_sockaddr6(dst, src, increment);
+ } else {
+ shuffle_sockaddr4((struct sockaddr_in *)dst, (struct sockaddr_in *)src, increment);
+ }
+}
+
+static void next_payload(struct pkt_payload **payload, int increment)
+{
+ if (*payload == NULL) {
+ *payload = global_payloads;
+ }
+ for (int i = 0; i < increment; i++) {
+ if ((*payload)->next == NULL) {
+ *payload = global_payloads;
+ } else {
+ *payload = (*payload)->next;
+ }
+ }
+}
+
+static void put_dns_payload(struct iovec *put_into, bool zero_copy, xdp_gun_ctx_t *ctx, struct pkt_payload **payl)
+{
+ if (zero_copy) {
+ put_into->iov_base = (*payl)->payload;
+ } else {
+ memcpy(put_into->iov_base, (*payl)->payload, (*payl)->len);
+ }
+ put_into->iov_len = (*payl)->len;
+ next_payload(payl, ctx->n_threads);
+}
+
+#ifdef ENABLE_QUIC
+static uint16_t get_rss_id(xdp_gun_ctx_t *ctx, uint16_t local_port)
+{
+ assert(ctx->rss_conf);
+
+ const uint8_t *key = (const uint8_t *)&(ctx->rss_conf->data[ctx->rss_conf->table_size]);
+ const size_t key_len = ctx->rss_conf->key_size;
+ uint8_t data[2 * sizeof(struct in6_addr) + 2 * sizeof(uint16_t)];
+
+ size_t addr_len;
+ if (ctx->ipv6) {
+ addr_len = sizeof(struct in6_addr);
+ memcpy(data, &ctx->target_ip.sin6_addr, addr_len);
+ memcpy(data + addr_len, &ctx->local_ip.sin6_addr, addr_len);
+ } else {
+ addr_len = sizeof(struct in_addr);
+ memcpy(data, &ctx->target_ip4.sin_addr, addr_len);
+ memcpy(data + addr_len, &ctx->local_ip4.sin_addr, addr_len);
+ }
+
+ uint16_t src_port = htobe16(ctx->target_port);
+ memcpy(data + 2 * addr_len, &src_port, sizeof(src_port));
+ uint16_t dst_port = htobe16(local_port);
+ memcpy(data + 2 * addr_len + sizeof(uint16_t), &dst_port, sizeof(dst_port));
+
+ size_t data_len = 2 * addr_len + 2 * sizeof(uint16_t);
+ uint16_t hash = toeplitz_hash(key, key_len, data, data_len);
+
+ return ctx->rss_conf->data[hash & ctx->rss_conf->mask];
+}
+
+static uint16_t adjust_port(xdp_gun_ctx_t *ctx, uint16_t local_port)
+{
+ assert(UINT16_MAX == LOCAL_PORT_MAX);
+
+ if (local_port < LOCAL_PORT_MIN) {
+ local_port = LOCAL_PORT_MIN;
+ }
+
+ if (ctx->rss_conf == NULL) {
+ return local_port;
+ }
+
+ for (int i = 0; i < UINT16_MAX; i++) {
+ if (ctx->thread_id == get_rss_id(ctx, local_port)) {
+ break;
+ }
+ local_port++;
+ if (local_port < LOCAL_PORT_MIN) {
+ local_port = LOCAL_PORT_MIN;
+ }
+ }
+
+ return local_port;
+}
+#endif // ENABLE_QUIC
+
+static unsigned alloc_pkts(knot_xdp_msg_t *pkts, struct knot_xdp_socket *xsk,
+ xdp_gun_ctx_t *ctx, uint64_t tick)
+{
+ uint64_t unique = (tick * ctx->n_threads + ctx->thread_id) * ctx->at_once;
+
+ knot_xdp_msg_flag_t flags = ctx->ipv6 ? KNOT_XDP_MSG_IPV6 : 0;
+ if (ctx->tcp) {
+ flags |= (KNOT_XDP_MSG_TCP | KNOT_XDP_MSG_SYN | KNOT_XDP_MSG_MSS);
+ } else if (ctx->quic) {
+ return ctx->at_once; // NOOP
+ }
+ if (ctx->vlan_tci != 0) {
+ flags |= KNOT_XDP_MSG_VLAN;
+ }
+
+ for (unsigned i = 0; i < ctx->at_once; i++) {
+ int ret = knot_xdp_send_alloc(xsk, flags, &pkts[i]);
+ if (ret != KNOT_EOK) {
+ return i;
+ }
+
+ uint16_t port_range = LOCAL_PORT_MAX - LOCAL_PORT_MIN + 1;
+ uint16_t local_port = LOCAL_PORT_MIN + unique % port_range;
+ uint64_t ip_incr = (unique / port_range) % (1 << (addr_bits(ctx->ipv6) - ctx->local_ip_range));
+ shuffle_sockaddr(&pkts[i].ip_from, &ctx->local_ip, local_port, ip_incr);
+ shuffle_sockaddr(&pkts[i].ip_to, &ctx->target_ip, ctx->target_port, 0);
+
+ memcpy(pkts[i].eth_from, ctx->local_mac, 6);
+ memcpy(pkts[i].eth_to, ctx->target_mac, 6);
+
+ pkts[i].vlan_tci = ctx->vlan_tci;
+
+ unique++;
+ }
+ return ctx->at_once;
+}
+
+inline static bool check_dns_payload(struct iovec *payl, xdp_gun_ctx_t *ctx,
+ kxdpgun_stats_t *st)
+{
+ if (payl->iov_len < KNOT_WIRE_HEADER_SIZE ||
+ memcmp(payl->iov_base, &ctx->msgid, sizeof(ctx->msgid)) != 0) {
+ return false;
+ }
+ st->rcodes_recv[((uint8_t *)payl->iov_base)[3] & 0x0F]++;
+ st->size_recv += payl->iov_len;
+ st->ans_recv++;
+ return true;
+}
+
+#ifdef ENABLE_QUIC
+static int quic_alloc_cb(knot_quic_reply_t *rpl)
+{
+ xdp_gun_ctx_t *ctx = rpl->in_ctx;
+ knot_xdp_msg_t *msg = rpl->out_ctx;
+
+ unsigned flags = ctx->ipv6 ? KNOT_XDP_MSG_IPV6 : 0;
+ if (ctx->vlan_tci != 0) {
+ flags |= KNOT_XDP_MSG_VLAN;
+ }
+
+ int ret = knot_xdp_send_alloc(rpl->sock, flags, msg);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ memcpy(msg->eth_from, ctx->local_mac, sizeof(ctx->local_mac));
+ memcpy(msg->eth_to, ctx->target_mac, sizeof(ctx->target_mac));
+ memcpy(&msg->ip_from, &ctx->local_ip, sizeof(msg->ip_from));
+ memcpy(&msg->ip_to, &ctx->target_ip, sizeof(msg->ip_to));
+
+ msg->vlan_tci = ctx->vlan_tci;
+
+ return KNOT_EOK;
+}
+
+static int quic_reply_alloc_cb(knot_quic_reply_t *rpl)
+{
+ return knot_xdp_reply_alloc(rpl->sock, rpl->in_ctx, rpl->out_ctx);
+}
+
+static int quic_send_cb(knot_quic_reply_t *rpl)
+{
+ uint32_t sent = 0;
+ return knot_xdp_send(rpl->sock, rpl->out_ctx, 1, &sent);
+}
+
+static void quic_free_cb(knot_quic_reply_t *rpl)
+{
+ knot_xdp_send_free(rpl->sock, rpl->out_ctx, 1);
+}
+#endif // ENABLE_QUIC
+
+void *xdp_gun_thread(void *_ctx)
+{
+ xdp_gun_ctx_t *ctx = _ctx;
+ struct knot_xdp_socket *xsk;
+ struct timespec timer;
+ knot_xdp_msg_t pkts[ctx->at_once];
+ uint64_t errors = 0, lost = 0, duration = 0;
+ kxdpgun_stats_t local_stats = { 0 };
+ unsigned stats_triggered = 0;
+ knot_tcp_table_t *tcp_table = NULL;
+#ifdef ENABLE_QUIC
+ knot_quic_table_t *quic_table = NULL;
+ struct knot_quic_creds *quic_creds = NULL;
+ list_t quic_sessions;
+ init_list(&quic_sessions);
+#endif // ENABLE_QUIC
+ list_t reuse_conns;
+ init_list(&reuse_conns);
+ const uint64_t extra_wait = ctx->quic ? 4000000 : 1000000;
+
+ if (ctx->tcp) {
+ tcp_table = knot_tcp_table_new(ctx->qps, NULL);
+ if (tcp_table == NULL) {
+ ERR2("failed to allocate TCP connection table");
+ return NULL;
+ }
+ }
+ if (ctx->quic) {
+#ifdef ENABLE_QUIC
+ quic_creds = knot_quic_init_creds_peer(NULL, NULL, 0);
+ if (quic_creds == NULL) {
+ ERR2("failed to initialize QUIC context");
+ return NULL;
+ }
+ quic_table = knot_quic_table_new(ctx->qps * 100, SIZE_MAX, SIZE_MAX, 1232, quic_creds);
+ if (quic_table == NULL) {
+ ERR2("failed to allocate QUIC connection table");
+ return NULL;
+ }
+ quic_table->qlog_dir = ctx->qlog_dir;
+#else
+ assert(0);
+#endif // ENABLE_QUIC
+ }
+
+ knot_xdp_load_bpf_t mode = (ctx->thread_id == 0 ?
+ KNOT_XDP_LOAD_BPF_ALWAYS : KNOT_XDP_LOAD_BPF_NEVER);
+ /*
+ * This mutex prevents libbpf from logging:
+ * 'libbpf: can't get link by id (5535): Resource temporarily unavailable'
+ */
+ pthread_mutex_lock(&global_stats.mutex);
+ int ret = knot_xdp_init(&xsk, ctx->dev, ctx->thread_id, ctx->flags,
+ LOCAL_PORT_MIN, LOCAL_PORT_MIN, mode, &ctx->xdp_config);
+ pthread_mutex_unlock(&global_stats.mutex);
+ if (ret != KNOT_EOK) {
+ ERR2("failed to initialize XDP socket#%u on interface %s (%s)",
+ ctx->thread_id, ctx->dev, knot_strerror(ret));
+ knot_tcp_table_free(tcp_table);
+ return NULL;
+ }
+
+ if (ctx->thread_id == 0) {
+ INFO2("using interface %s, XDP threads %u, %s%s%s, %s mode",
+ ctx->dev, ctx->n_threads,
+ (ctx->tcp ? "TCP" : ctx->quic ? "QUIC" : "UDP"),
+ (ctx->sending_mode[0] != '\0' ? " mode " : ""),
+ (ctx->sending_mode[0] != '\0' ? ctx->sending_mode : ""),
+ (knot_eth_xdp_mode(if_nametoindex(ctx->dev)) == KNOT_XDP_MODE_FULL ?
+ "native" : "emulated"));
+ }
+
+ struct pollfd pfd = { knot_xdp_socket_fd(xsk), POLLIN, 0 };
+
+ while (xdp_trigger == KXDPGUN_WAIT) {
+ usleep(1000);
+ }
+
+ uint64_t tick = 0;
+ struct pkt_payload *payload_ptr = NULL;
+ next_payload(&payload_ptr, ctx->thread_id);
+
+#ifdef ENABLE_QUIC
+ knot_xdp_msg_t msg_out;
+ knot_quic_reply_t quic_send_reply = {
+ .out_payload = &msg_out.payload,
+ .in_ctx = ctx,
+ .out_ctx = &msg_out,
+ .sock = xsk,
+ .alloc_reply = quic_alloc_cb,
+ .send_reply = quic_send_cb,
+ .free_reply = quic_free_cb,
+ };
+ knot_quic_reply_t quic_reply = {
+ .out_payload = &msg_out.payload,
+ .out_ctx = &msg_out,
+ .sock = xsk,
+ .alloc_reply = quic_reply_alloc_cb,
+ .send_reply = quic_send_cb,
+ .free_reply = quic_free_cb,
+ };
+
+ ctx->target_ip.sin6_port = htobe16(ctx->target_port);
+ knot_sweep_stats_t sweep_stats = { 0 };
+
+ uint16_t local_ports[QUIC_THREAD_PORTS];
+ uint16_t port = LOCAL_PORT_MIN;
+ for (int i = 0; ctx->quic && i < QUIC_THREAD_PORTS; ++i) {
+ local_ports[i] = adjust_port(ctx, port);
+ port = local_ports[i] + 1;
+ assert(port >= LOCAL_PORT_MIN);
+ }
+ size_t local_ports_it = 0;
+#endif // ENABLE_QUIC
+
+ timer_start(&timer);
+
+ while (duration < ctx->duration + extra_wait) {
+
+ // sending part
+ if (duration < ctx->duration) {
+ while (1) {
+ knot_xdp_send_prepare(xsk);
+ unsigned alloced = alloc_pkts(pkts, xsk, ctx, tick);
+ if (alloced < ctx->at_once) {
+ lost += ctx->at_once - alloced;
+ if (alloced == 0) {
+ break;
+ }
+ }
+
+ if (ctx->tcp) {
+ for (int i = 0; i < alloced; i++) {
+ pkts[i].payload.iov_len = 0;
+
+ if (!EMPTY_LIST(reuse_conns)) {
+ ptrnode_t *n = HEAD(reuse_conns);
+ knot_tcp_relay_t *rl = n->d;
+ rem_node(&n->n);
+ free(n);
+ struct iovec payl;
+ put_dns_payload(&payl, true, ctx, &payload_ptr);
+ ret = knot_tcp_reply_data(rl, tcp_table,
+ (ctx->ignore1 & KXDPGUN_IGNORE_LASTBYTE),
+ payl.iov_base, payl.iov_len);
+ if (ret == KNOT_EOK) {
+ ret = knot_tcp_send(xsk, rl, 1, ctx->at_once);
+ }
+ if (ret == KNOT_EOK) {
+ pkts[i].flags &= ~KNOT_XDP_MSG_SYN; // skip sending respective packet
+ local_stats.qry_sent++;
+ }
+ free(rl);
+ }
+ }
+ } else if (ctx->quic) {
+#ifdef ENABLE_QUIC
+ uint16_t local_port = local_ports[local_ports_it++ % QUIC_THREAD_PORTS];
+ ctx->local_ip.sin6_port = htobe16(local_port);
+
+ for (unsigned i = 0; i < ctx->at_once; i++) {
+ knot_quic_conn_t *newconn = NULL;
+ if (!EMPTY_LIST(reuse_conns)) {
+ ptrnode_t *n = HEAD(reuse_conns);
+ newconn = n->d;
+ rem_node(&n->n);
+ assert(HEAD(reuse_conns) != n);
+ free(n);
+ if (newconn->streams_count < 1) {
+ newconn = NULL; // un-re-usable conn
+ } else {
+ ctx->local_ip.sin6_port = knot_quic_conn_local_port(newconn);
+ ret = KNOT_EOK;
+ }
+ }
+ if (newconn == NULL) {
+ ret = knot_quic_client(quic_table, &ctx->target_ip, &ctx->local_ip,
+ NULL, &newconn);
+ }
+ if (ret == KNOT_EOK) {
+ struct iovec tmp = {
+ knot_quic_stream_add_data(newconn, (newconn->streams_first + newconn->streams_count) * 4,
+ NULL, payload_ptr->len),
+ 0
+ };
+ put_dns_payload(&tmp, false, ctx, &payload_ptr);
+ if (newconn->streams_count < 2) {
+ if (EMPTY_LIST(quic_sessions)) {
+ newconn->streams_count = -1;
+ } else {
+ void *session = HEAD(quic_sessions);
+ rem_node(session);
+ (void)knot_quic_session_load(newconn, session);
+ }
+ }
+ ret = knot_quic_send(quic_table, newconn, &quic_send_reply, 1,
+ (ctx->ignore1 & KXDPGUN_IGNORE_LASTBYTE) ? KNOT_QUIC_SEND_IGNORE_LASTBYTE : 0);
+ }
+ if (ret == KNOT_EOK) {
+ local_stats.qry_sent++;
+ }
+ }
+ (void)knot_xdp_send_finish(xsk);
+#endif // ENABLE_QUIC
+ break;
+ } else {
+ for (int i = 0; i < alloced; i++) {
+ put_dns_payload(&pkts[i].payload, false,
+ ctx, &payload_ptr);
+ }
+ }
+
+ uint32_t really_sent = 0;
+ if (knot_xdp_send(xsk, pkts, alloced, &really_sent) != KNOT_EOK) {
+ lost += alloced;
+ }
+ local_stats.qry_sent += really_sent;
+ (void)knot_xdp_send_finish(xsk);
+
+ break;
+ }
+ }
+
+ // receiving part
+ if (!(ctx->flags & KNOT_XDP_FILTER_DROP)) {
+ while (1) {
+ ret = poll(&pfd, 1, 0);
+ if (ret < 0) {
+ errors++;
+ break;
+ }
+ if (!pfd.revents) {
+ break;
+ }
+
+ uint32_t recvd = 0;
+ size_t wire = 0;
+ (void)knot_xdp_recv(xsk, pkts, ctx->at_once, &recvd, &wire);
+ if (recvd == 0) {
+ break;
+ }
+ if (ctx->tcp) {
+ knot_tcp_relay_t relays[recvd];
+ ret = knot_tcp_recv(relays, pkts, recvd, tcp_table, NULL, ctx->ignore2);
+ if (ret != KNOT_EOK) {
+ errors++;
+ break;
+ }
+
+ for (size_t i = 0; i < recvd; i++) {
+ knot_tcp_relay_t *rl = &relays[i];
+ struct iovec payl;
+ switch (rl->action) {
+ case XDP_TCP_ESTABLISH:
+ local_stats.synack_recv++;
+ if (ctx->ignore1 & KXDPGUN_IGNORE_QUERY) {
+ break;
+ }
+ put_dns_payload(&payl, true, ctx, &payload_ptr);
+ ret = knot_tcp_reply_data(rl, tcp_table,
+ (ctx->ignore1 & KXDPGUN_IGNORE_LASTBYTE),
+ payl.iov_base, payl.iov_len);
+ if (ret != KNOT_EOK) {
+ errors++;
+ }
+ break;
+ case XDP_TCP_CLOSE:
+ local_stats.finack_recv++;
+ break;
+ case XDP_TCP_RESET:
+ local_stats.rst_recv++;
+ break;
+ default:
+ break;
+ }
+ for (size_t j = 0; rl->inbf != NULL && j < rl->inbf->n_inbufs; j++) {
+ if (check_dns_payload(&rl->inbf->inbufs[j], ctx, &local_stats)) {
+ if (!(ctx->ignore1 & KXDPGUN_IGNORE_CLOSE)) {
+ rl->answer = XDP_TCP_CLOSE;
+ } else if ((ctx->ignore1 & KXDPGUN_REUSE_CONN)) {
+ knot_tcp_relay_t *rl_copy = malloc(sizeof(*rl));
+ memcpy(rl_copy, rl, sizeof(*rl));
+ ptrlist_add(&reuse_conns, rl_copy, NULL);
+ rl_copy->answer = XDP_TCP_NOOP;
+ rl_copy->auto_answer = 0;
+ }
+ }
+ }
+ }
+
+ ret = knot_tcp_send(xsk, relays, recvd, ctx->at_once);
+ if (ret != KNOT_EOK) {
+ errors++;
+ }
+ (void)knot_xdp_send_finish(xsk);
+
+ knot_tcp_cleanup(tcp_table, relays, recvd);
+ } else if (ctx->quic) {
+#ifdef ENABLE_QUIC
+ for (size_t i = 0; i < recvd; i++) {
+ knot_xdp_msg_t *msg_in = &pkts[i];
+ knot_quic_conn_t *conn;
+
+ quic_reply.ip_rem = (struct sockaddr_storage *)&msg_in->ip_from;
+ quic_reply.ip_loc = (struct sockaddr_storage *)&msg_in->ip_to;
+ quic_reply.in_payload = &msg_in->payload;
+ quic_reply.in_ctx = msg_in;
+
+ ret = knot_quic_handle(quic_table, &quic_reply, 5000000000L, &conn);
+ if (ret == KNOT_ECONN) {
+ local_stats.rst_recv++;
+ knot_quic_cleanup(&conn, 1);
+ continue;
+ } else if (ret != 0) {
+ errors++;
+ knot_quic_cleanup(&conn, 1);
+ break;
+ }
+
+ if (conn == NULL || conn->conn == NULL) {
+ knot_quic_cleanup(&conn, 1);
+ continue;
+ }
+
+ if (!ctx->quic_full_handshake && knot_quic_session_available(conn)) {
+ void *session = knot_quic_session_save(conn);
+ if (session != NULL) {
+ add_tail(&quic_sessions, session);
+ }
+ }
+
+ if ((conn->flags & KNOT_QUIC_CONN_HANDSHAKE_DONE) && conn->streams_count == -1) {
+ conn->streams_count = 1;
+
+ local_stats.synack_recv++;
+ if ((ctx->ignore1 & KXDPGUN_IGNORE_QUERY)) {
+ knot_quic_table_rem(conn, quic_table);
+ knot_quic_cleanup(&conn, 1);
+ continue;
+ }
+ }
+ if (!(conn->flags & KNOT_QUIC_CONN_HANDSHAKE_DONE) && conn->streams_count == -1) {
+ knot_quic_table_rem(conn, quic_table);
+ knot_quic_cleanup(&conn, 1);
+ continue;
+ }
+ assert(conn->streams_count > 0);
+
+ if ((ctx->ignore2 & XDP_TCP_IGNORE_ESTABLISH)) {
+ knot_quic_table_rem(conn, quic_table);
+ knot_quic_cleanup(&conn, 1);
+ local_stats.synack_recv++;
+ continue;
+ }
+
+ int64_t s0id;
+ knot_quic_stream_t *stream0 = knot_quic_stream_get_process(conn, &s0id);
+ if (stream0 != NULL && stream0->inbufs != NULL && stream0->inbufs->n_inbufs > 0) {
+ check_dns_payload(&stream0->inbufs->inbufs[0], ctx, &local_stats);
+ stream0->inbufs->n_inbufs = 0; // signal that data have been read out
+
+ if ((ctx->ignore2 & XDP_TCP_IGNORE_DATA_ACK)) {
+ knot_quic_table_rem(conn, quic_table);
+ knot_quic_cleanup(&conn, 1);
+ continue;
+ } else if ((ctx->ignore1 & KXDPGUN_REUSE_CONN)) {
+ if (conn->streams_count > 1) { // keep the number of outstanding streams below MAX_STREAMS_PER_CONN, while preserving at least one at all times
+ knot_quic_conn_stream_free(conn, conn->streams_first * 4);
+ }
+ ptrlist_add(&reuse_conns, conn, NULL);
+ }
+ }
+ ret = knot_quic_send(quic_table, conn, &quic_reply, 4,
+ (ctx->ignore1 & KXDPGUN_IGNORE_LASTBYTE) ? KNOT_QUIC_SEND_IGNORE_LASTBYTE : 0);
+ if (ret != KNOT_EOK) {
+ errors++;
+ }
+
+ if (!(ctx->ignore1 & KXDPGUN_IGNORE_CLOSE) && (conn->flags & KNOT_QUIC_CONN_SESSION_TAKEN) &&
+ stream0 != NULL && stream0->inbufs != NULL && stream0->inbufs->n_inbufs == 0) {
+ assert(!(ctx->ignore2 & XDP_TCP_IGNORE_DATA_ACK));
+ quic_reply.handle_ret = KNOT_QUIC_HANDLE_RET_CLOSE;
+ ret = knot_quic_send(quic_table, conn, &quic_reply, 1, 0);
+ knot_quic_table_rem(conn, quic_table);
+ knot_quic_cleanup(&conn, 1);
+ if (ret != KNOT_EOK) {
+ errors++;
+ }
+ }
+ }
+ (void)knot_xdp_send_finish(xsk);
+#endif // ENABLE_QUIC
+ } else {
+ for (int i = 0; i < recvd; i++) {
+ (void)check_dns_payload(&pkts[i].payload, ctx,
+ &local_stats);
+ }
+ }
+ local_stats.wire_recv += wire;
+ knot_xdp_recv_finish(xsk, pkts, recvd);
+ pfd.revents = 0;
+ }
+ }
+
+#ifdef ENABLE_QUIC
+ if (ctx->quic) {
+ (void)knot_quic_table_sweep(quic_table, NULL, &sweep_stats);
+ }
+#endif // ENABLE_QUIC
+
+ // speed and signal part
+ uint64_t dura_exp = (local_stats.qry_sent * 1000000) / ctx->qps;
+ duration = timer_end(&timer);
+ if (xdp_trigger == KXDPGUN_STOP && ctx->duration > duration) {
+ ctx->duration = duration;
+ }
+ if (stats_trigger > stats_triggered) {
+ assert(stats_trigger == stats_triggered + 1);
+ stats_triggered++;
+
+ local_stats.duration = duration;
+ size_t collected = collect_stats(&global_stats, &local_stats);
+ assert(collected <= ctx->n_threads);
+ if (collected == ctx->n_threads) {
+ print_stats(&global_stats, ctx->tcp, ctx->quic,
+ !(ctx->flags & KNOT_XDP_FILTER_DROP),
+ ctx->qps * ctx->n_threads);
+ clear_stats(&global_stats);
+ }
+ }
+ if (dura_exp > duration) {
+ usleep(dura_exp - duration);
+ }
+ if (duration > ctx->duration) {
+ usleep(1000);
+ }
+ tick++;
+ }
+
+ knot_xdp_deinit(xsk);
+
+ if (ctx->tcp) {
+ ptrlist_deep_free(&reuse_conns, NULL);
+ } else if (ctx->quic) {
+ ptrlist_free(&reuse_conns, NULL); // stored conns get freed as part of xyz_table_free
+ } else {
+ assert(EMPTY_LIST(reuse_conns));
+ }
+ knot_tcp_table_free(tcp_table);
+
+#ifdef ENABLE_QUIC
+ knot_quic_table_free(quic_table);
+ struct knot_quic_session *n, *nxt;
+ WALK_LIST_DELSAFE(n, nxt, quic_sessions) {
+ knot_quic_session_load(NULL, n);
+ }
+ knot_quic_free_creds(quic_creds);
+#endif // ENABLE_QUIC
+
+ char recv_str[40] = "", lost_str[40] = "", err_str[40] = "";
+ if (!(ctx->flags & KNOT_XDP_FILTER_DROP)) {
+ (void)snprintf(recv_str, sizeof(recv_str), ", received %"PRIu64, local_stats.ans_recv);
+ }
+ if (lost > 0) {
+ (void)snprintf(lost_str, sizeof(lost_str), ", lost %"PRIu64, lost);
+ }
+ if (errors > 0) {
+ (void)snprintf(err_str, sizeof(err_str), ", errors %"PRIu64, errors);
+ }
+ INFO2("thread#%02u: sent %"PRIu64"%s%s%s",
+ ctx->thread_id, local_stats.qry_sent, recv_str, lost_str, err_str);
+ local_stats.duration = ctx->duration;
+ collect_stats(&global_stats, &local_stats);
+
+ return NULL;
+}
+
+static int dev2mac(const char *dev, uint8_t *mac)
+{
+ struct ifreq ifr;
+ memset(&ifr, 0, sizeof(ifr));
+ int fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fd < 0) {
+ return -errno;
+ }
+ strlcpy(ifr.ifr_name, dev, IFNAMSIZ);
+
+ int ret = ioctl(fd, SIOCGIFHWADDR, &ifr);
+ if (ret >= 0) {
+ memcpy(mac, ifr.ifr_hwaddr.sa_data, 6);
+ } else {
+ ret = -errno;
+ }
+ close(fd);
+ return ret;
+}
+
+static bool mac_empty(const uint8_t *mac)
+{
+ static const uint8_t unset_mac[6] = { 0 };
+ return (memcmp(mac, unset_mac, sizeof(unset_mac)) == 0);
+}
+
+static int mac_sscan(const char *src, uint8_t *dst)
+{
+ int tmp[6];
+ if (6 != sscanf(src, "%2x:%2x:%2x:%2x:%2x:%2x",
+ &tmp[0], &tmp[1], &tmp[2], &tmp[3], &tmp[4], &tmp[5])) {
+ return KNOT_EINVAL;
+ }
+
+ for (int i = 0; i < 6; i++) {
+ dst[i] = (uint8_t)tmp[i];
+ }
+
+ return KNOT_EOK;
+}
+
+static bool configure_target(char *target_str, char *local_ip, xdp_gun_ctx_t *ctx)
+{
+ int val;
+ char *at = strrchr(target_str, '@');
+ if (at != NULL && (val = atoi(at + 1)) > 0 && val <= 0xffff) {
+ ctx->target_port = val;
+ *at = '\0';
+ }
+
+ ctx->ipv6 = false;
+ if (inet_pton(AF_INET, target_str, &ctx->target_ip4.sin_addr) <= 0) {
+ ctx->ipv6 = true;
+ ctx->target_ip.sin6_family = AF_INET6;
+ if (inet_pton(AF_INET6, target_str, &ctx->target_ip.sin6_addr) <= 0) {
+ ERR2("invalid target IP");
+ return false;
+ }
+ } else {
+ ctx->target_ip.sin6_family = AF_INET;
+ }
+
+ struct sockaddr_storage via = { 0 };
+ if (local_ip == NULL || ctx->dev[0] == '\0' || mac_empty(ctx->target_mac)) {
+ char auto_dev[IFNAMSIZ];
+ int ret = ip_route_get(&ctx->target_ip_ss,
+ &via,
+ &ctx->local_ip_ss,
+ (ctx->dev[0] == '\0') ? ctx->dev : auto_dev);
+ if (ret < 0) {
+ ERR2("can't find route to '%s' (%s)", target_str, strerror(-ret));
+ return false;
+ }
+ }
+
+ ctx->local_ip_range = addr_bits(ctx->ipv6); // by default use one IP
+ if (local_ip != NULL) {
+ at = strrchr(local_ip, '/');
+ if (at != NULL && (val = atoi(at + 1)) > 0 && val <= ctx->local_ip_range) {
+ ctx->local_ip_range = val;
+ *at = '\0';
+ }
+ if (ctx->ipv6) {
+ if (ctx->local_ip_range < 64 ||
+ inet_pton(AF_INET6, local_ip, &ctx->local_ip.sin6_addr) <= 0) {
+ ERR2("invalid local IPv6 or unsupported prefix length");
+ return false;
+ }
+ } else {
+ if (inet_pton(AF_INET, local_ip, &ctx->local_ip4.sin_addr) <= 0) {
+ ERR2("invalid local IPv4");
+ return false;
+ }
+ }
+ }
+
+ if (mac_empty(ctx->target_mac)) {
+ const struct sockaddr_storage *neigh = (via.ss_family == AF_UNSPEC) ?
+ &ctx->target_ip_ss : &via;
+ int ret = ip_neigh_get(neigh, true, ctx->target_mac);
+ if (ret < 0) {
+ char neigh_str[256] = { 0 };
+ sockaddr_tostr(neigh_str, sizeof(neigh_str), (struct sockaddr_storage *)neigh);
+ ERR2("failed to get remote MAC of target/gateway '%s' (%s)",
+ neigh_str, strerror(-ret));
+ return false;
+ }
+ }
+
+ if (mac_empty(ctx->local_mac)) {
+ int ret = dev2mac(ctx->dev, ctx->local_mac);
+ if (ret < 0) {
+ ERR2("failed to get MAC of device '%s' (%s)", ctx->dev, strerror(-ret));
+ return false;
+ }
+ }
+
+ int ret = knot_eth_queues(ctx->dev);
+ if (ret >= 0) {
+ ctx->n_threads = ret;
+ } else {
+ ERR2("unable to get number of queues for '%s' (%s)", ctx->dev,
+ knot_strerror(ret));
+ return false;
+ }
+
+ if (ctx->n_threads > 1 && ctx->quic) {
+ ret = knot_eth_rss(ctx->dev, &ctx->rss_conf);
+ if (ret != 0) {
+ WARN2("unable to read NIC RSS configuration for '%s' (%s)",
+ ctx->dev, knot_strerror(ret));
+ }
+ }
+
+ return true;
+}
+
+static void print_help(void)
+{
+ printf("Usage: %s [options] -i <queries_file> <dest_ip>\n"
+ "\n"
+ "Options:\n"
+ " -t, --duration <sec> "SPACE"Duration of traffic generation.\n"
+ " "SPACE" (default is %"PRIu64" seconds)\n"
+ " -T, --tcp[=debug_mode] "SPACE"Send queries over TCP.\n"
+ " -U, --quic[=debug_mode] "SPACE"Send queries over QUIC.\n"
+ " -Q, --qps <qps> "SPACE"Number of queries-per-second (approximately) to be sent.\n"
+ " "SPACE" (default is %"PRIu64" qps)\n"
+ " -b, --batch <size> "SPACE"Send queries in a batch of defined size.\n"
+ " "SPACE" (default is %d for UDP, %d for TCP)\n"
+ " -r, --drop "SPACE"Drop incoming responses (disables response statistics).\n"
+ " -p, --port <port> "SPACE"Remote destination port.\n"
+ " "SPACE" (default is %d for UDP/TCP, %u for QUIC)\n"
+ " -F, --affinity <spec> "SPACE"CPU affinity in the format [<cpu_start>][s<cpu_step>].\n"
+ " "SPACE" (default is %s)\n"
+ " -i, --infile <file> "SPACE"Path to a file with query templates.\n"
+ " -I, --interface <ifname> "SPACE"Override auto-detected interface for outgoing communication.\n"
+ " -l, --local <ip[/prefix]>"SPACE"Override auto-detected source IP address or subnet.\n"
+ " -L, --local-mac <MAC> "SPACE"Override auto-detected local MAC address.\n"
+ " -R, --remote-mac <MAC> "SPACE"Override auto-detected remote MAC address.\n"
+ " -v, --vlan <id> "SPACE"Add VLAN 802.1Q header with the given id.\n"
+ " -e, --edns-size <size> "SPACE"EDNS UDP payload size, range 512-4096 (default 1232)\n"
+ " -m, --mode <mode> "SPACE"Set XDP mode (auto, copy, generic).\n"
+ " -G, --qlog <path> "SPACE"Output directory for qlog (useful for QUIC only).\n"
+ " -h, --help "SPACE"Print the program help.\n"
+ " -V, --version "SPACE"Print the program version.\n"
+ "\n"
+ "Parameters:\n"
+ " <dest_ip> "SPACE"IPv4 or IPv6 address of the remote destination.\n",
+ PROGRAM_NAME, ctx_defaults.duration / 1000000, ctx_defaults.qps,
+ ctx_defaults.at_once, 1, REMOTE_PORT_DEFAULT, REMOTE_PORT_DOQ_DEFAULT, "0s1");
+}
+
+static bool sending_mode(const char *arg, xdp_gun_ctx_t *ctx)
+{
+ if (arg == NULL) {
+ ctx->sending_mode = "";
+ return true;
+ } else if (strlen(arg) != 1) {
+ goto mode_invalid;
+ }
+ ctx->sending_mode = arg;
+
+ switch (ctx->sending_mode[0]) {
+ case '0':
+ if (!ctx->quic) {
+ goto mode_unavailable;
+ }
+ ctx->quic_full_handshake = true;
+ break;
+ case '1':
+ ctx->ignore1 = KXDPGUN_IGNORE_QUERY;
+ ctx->ignore2 = XDP_TCP_IGNORE_ESTABLISH | XDP_TCP_IGNORE_FIN;
+ break;
+ case '2':
+ ctx->ignore1 = KXDPGUN_IGNORE_QUERY;
+ break;
+ case '3':
+ ctx->ignore1 = KXDPGUN_IGNORE_QUERY;
+ ctx->ignore2 = XDP_TCP_IGNORE_FIN;
+ break;
+ case '5':
+ ctx->ignore1 = KXDPGUN_IGNORE_LASTBYTE;
+ ctx->ignore2 = XDP_TCP_IGNORE_FIN;
+ break;
+ case '7':
+ ctx->ignore1 = KXDPGUN_IGNORE_CLOSE;
+ ctx->ignore2 = XDP_TCP_IGNORE_DATA_ACK | XDP_TCP_IGNORE_FIN;
+ break;
+ case '8':
+ ctx->ignore1 = KXDPGUN_IGNORE_CLOSE;
+ ctx->ignore2 = XDP_TCP_IGNORE_FIN;
+ break;
+ case '9':
+ if (!ctx->tcp) {
+ goto mode_unavailable;
+ }
+ ctx->ignore2 = XDP_TCP_IGNORE_FIN;
+ break;
+ case 'R':
+ ctx->ignore1 = KXDPGUN_IGNORE_CLOSE | KXDPGUN_REUSE_CONN;
+ ctx->quic_full_handshake = true;
+ break;
+ default:
+ goto mode_invalid;
+ }
+
+ return true;
+mode_unavailable:
+ ERR2("mode '%s' not available", optarg);
+ return false;
+mode_invalid:
+ ERR2("invalid mode '%s'", optarg);
+ return false;
+}
+
+static int set_mode(const char *arg, knot_xdp_config_t *config)
+{
+ assert(arg != NULL);
+ assert(config != NULL);
+
+ if (strcmp(arg, "auto") == 0) {
+ config->force_copy = false;
+ config->force_generic = false;
+ return KNOT_EOK;
+ }
+
+ if (strcmp(arg, "copy") == 0) {
+ config->force_copy = true;
+ config->force_generic = false;
+ return KNOT_EOK;
+ }
+
+ if (strcmp(arg, "generic") == 0) {
+ config->force_copy = false;
+ config->force_generic = true;
+ return KNOT_EOK;
+ }
+
+ return KNOT_EINVAL;
+}
+
+static bool get_opts(int argc, char *argv[], xdp_gun_ctx_t *ctx)
+{
+ struct option opts[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "duration", required_argument, NULL, 't' },
+ { "qps", required_argument, NULL, 'Q' },
+ { "batch", required_argument, NULL, 'b' },
+ { "drop", no_argument, NULL, 'r' },
+ { "port", required_argument, NULL, 'p' },
+ { "tcp", optional_argument, NULL, 'T' },
+ { "quic", optional_argument, NULL, 'U' },
+ { "affinity", required_argument, NULL, 'F' },
+ { "interface", required_argument, NULL, 'I' },
+ { "local", required_argument, NULL, 'l' },
+ { "infile", required_argument, NULL, 'i' },
+ { "local-mac", required_argument, NULL, 'L' },
+ { "remote-mac", required_argument, NULL, 'R' },
+ { "vlan", required_argument, NULL, 'v' },
+ { "edns-size", required_argument, NULL, 'e' },
+ { "mode", required_argument, NULL, 'm' },
+ { "qlog", required_argument, NULL, 'G' },
+ { NULL }
+ };
+
+ int opt = 0, arg;
+ bool default_at_once = true;
+ double argf;
+ char *argcp, *local_ip = NULL, *filename = NULL;
+ while ((opt = getopt_long(argc, argv, "hVt:Q:b:rp:T::U::F:I:l:i:L:R:v:e:m:G:", opts, NULL)) != -1) {
+ switch (opt) {
+ case 'h':
+ print_help();
+ exit(EXIT_SUCCESS);
+ case 'V':
+ print_version(PROGRAM_NAME);
+ exit(EXIT_SUCCESS);
+ case 't':
+ assert(optarg);
+ argf = atof(optarg);
+ if (argf > 0) {
+ ctx->duration = argf * 1000000.0;
+ assert(ctx->duration >= 1000);
+ } else {
+ ERR2("invalid duration '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'Q':
+ assert(optarg);
+ arg = atoi(optarg);
+ if (arg > 0) {
+ ctx->qps = arg;
+ } else {
+ ERR2("invalid QPS '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'b':
+ assert(optarg);
+ arg = atoi(optarg);
+ if (arg > 0) {
+ default_at_once = false;
+ ctx->at_once = arg;
+ } else {
+ ERR2("invalid batch size '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'r':
+ ctx->flags &= ~KNOT_XDP_FILTER_PASS;
+ ctx->flags |= KNOT_XDP_FILTER_DROP;
+ break;
+ case 'p':
+ assert(optarg);
+ arg = atoi(optarg);
+ if (arg > 0 && arg <= 0xffff) {
+ ctx->target_port = arg;
+ } else {
+ ERR2("invalid port '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'T':
+ ctx->tcp = true;
+ ctx->quic = false;
+ ctx->flags &= ~(KNOT_XDP_FILTER_UDP | KNOT_XDP_FILTER_QUIC);
+ ctx->flags |= KNOT_XDP_FILTER_TCP;
+ if (default_at_once) {
+ ctx->at_once = 1;
+ }
+ if (!sending_mode(optarg, ctx)) {
+ return false;
+ }
+ break;
+ case 'U':
+#ifdef ENABLE_QUIC
+ ctx->quic = true;
+ ctx->tcp = false;
+ ctx->flags &= ~(KNOT_XDP_FILTER_UDP | KNOT_XDP_FILTER_TCP);
+ ctx->flags |= KNOT_XDP_FILTER_QUIC;
+ if (ctx->target_port == 0) {
+ ctx->target_port = REMOTE_PORT_DOQ_DEFAULT;
+ }
+ if (default_at_once) {
+ ctx->at_once = 1;
+ }
+ if (!sending_mode(optarg, ctx)) {
+ return false;
+ }
+#else
+ ERR2("QUIC not available");
+ return false;
+#endif // ENABLE_QUIC
+ break;
+ case 'F':
+ assert(optarg);
+ if ((arg = atoi(optarg)) > 0) {
+ global_cpu_aff_start = arg;
+ }
+ argcp = strchr(optarg, 's');
+ if (argcp != NULL && (arg = atoi(argcp + 1)) > 0) {
+ global_cpu_aff_step = arg;
+ }
+ break;
+ case 'I':
+ strlcpy(ctx->dev, optarg, IFNAMSIZ);
+ break;
+ case 'l':
+ local_ip = optarg;
+ break;
+ case 'i':
+ filename = optarg;
+ break;
+ case 'L':
+ if (mac_sscan(optarg, ctx->local_mac) != KNOT_EOK) {
+ ERR2("invalid local MAC address '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'R':
+ if (mac_sscan(optarg, ctx->target_mac) != KNOT_EOK) {
+ ERR2("invalid remote MAC address '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'v':
+ assert(optarg);
+ arg = atoi(optarg);
+ if (arg > 0 && arg < 4095) {
+ uint16_t id = arg;
+ ctx->vlan_tci = htobe16(id);
+ } else {
+ ERR2("invalid VLAN id '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'e':
+ assert(optarg);
+ arg = atoi(optarg);
+ if (arg >= 512 && arg <= 4096) {
+ ctx->edns_size = arg;
+ } else {
+ ERR2("invalid edns size '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'm':
+ assert(optarg);
+ if (set_mode(optarg, &ctx->xdp_config) != KNOT_EOK) {
+ ERR2("invalid mode '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'G':
+ ctx->qlog_dir = optarg;
+ break;
+ default:
+ print_help();
+ return false;
+ }
+ }
+ if (filename == NULL) {
+ print_help();
+ return false;
+ }
+ size_t qcount = ctx->duration / 1000000 * ctx->qps;
+ if (!load_queries(filename, ctx->edns_size, ctx->msgid, qcount)) {
+ return false;
+ }
+ if (global_payloads == NULL || argc - optind != 1) {
+ print_help();
+ return false;
+ }
+
+ if (ctx->target_port == 0) {
+ ctx->target_port = REMOTE_PORT_DEFAULT;
+ }
+
+ if (!configure_target(argv[optind], local_ip, ctx)) {
+ return false;
+ }
+
+ if (ctx->qps < ctx->n_threads) {
+ WARN2("QPS increased to the number of threads/queues: %u", ctx->n_threads);
+ ctx->qps = ctx->n_threads;
+ }
+ ctx->qps /= ctx->n_threads;
+
+ return true;
+}
+
+int main(int argc, char *argv[])
+{
+ xdp_gun_ctx_t ctx = ctx_defaults, *thread_ctxs = NULL;
+ ctx.msgid = time(NULL) % UINT16_MAX;
+ pthread_t *threads = NULL;
+
+ if (!get_opts(argc, argv, &ctx)) {
+ free_global_payloads();
+ return EXIT_FAILURE;
+ }
+
+ thread_ctxs = calloc(ctx.n_threads, sizeof(*thread_ctxs));
+ threads = calloc(ctx.n_threads, sizeof(*threads));
+ if (thread_ctxs == NULL || threads == NULL) {
+ ERR2("out of memory");
+ free(thread_ctxs);
+ free(threads);
+ free_global_payloads();
+ return EXIT_FAILURE;
+ }
+ for (int i = 0; i < ctx.n_threads; i++) {
+ thread_ctxs[i] = ctx;
+ thread_ctxs[i].thread_id = i;
+ }
+
+ if (!linux_at_least(5, 11)) {
+ struct rlimit min_limit = { RLIM_INFINITY, RLIM_INFINITY }, cur_limit = { 0 };
+ if (getrlimit(RLIMIT_MEMLOCK, &cur_limit) != 0 ||
+ cur_limit.rlim_cur != min_limit.rlim_cur ||
+ cur_limit.rlim_max != min_limit.rlim_max) {
+ int ret = setrlimit(RLIMIT_MEMLOCK, &min_limit);
+ if (ret != 0) {
+ WARN2("unable to increase RLIMIT_MEMLOCK: %s",
+ strerror(errno));
+ }
+ }
+ }
+
+ pthread_mutex_init(&global_stats.mutex, NULL);
+
+ struct sigaction stop_action = { .sa_handler = sigterm_handler };
+ struct sigaction stats_action = { .sa_handler = sigusr_handler };
+ sigaction(SIGINT, &stop_action, NULL);
+ sigaction(SIGTERM, &stop_action, NULL);
+ sigaction(SIGUSR1, &stats_action, NULL);
+
+ for (size_t i = 0; i < ctx.n_threads; i++) {
+ unsigned affinity = global_cpu_aff_start + i * global_cpu_aff_step;
+ cpu_set_t set;
+ CPU_ZERO(&set);
+ CPU_SET(affinity, &set);
+ (void)pthread_create(&threads[i], NULL, xdp_gun_thread, &thread_ctxs[i]);
+ int ret = pthread_setaffinity_np(threads[i], sizeof(cpu_set_t), &set);
+ if (ret != 0) {
+ WARN2("failed to set affinity of thread#%zu to CPU#%u", i, affinity);
+ }
+ usleep(20000);
+ }
+ usleep(1000000);
+
+ xdp_trigger = KXDPGUN_START;
+ usleep(1000000);
+
+ for (size_t i = 0; i < ctx.n_threads; i++) {
+ pthread_join(threads[i], NULL);
+ }
+ if (global_stats.duration > 0 && global_stats.qry_sent > 0) {
+ print_stats(&global_stats, ctx.tcp, ctx.quic, !(ctx.flags & KNOT_XDP_FILTER_DROP), ctx.qps * ctx.n_threads);
+ }
+ pthread_mutex_destroy(&global_stats.mutex);
+
+ free(ctx.rss_conf);
+ free(thread_ctxs);
+ free(threads);
+ free_global_payloads();
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/utils/kzonecheck/main.c b/src/utils/kzonecheck/main.c
new file mode 100644
index 0000000..3a2b620
--- /dev/null
+++ b/src/utils/kzonecheck/main.c
@@ -0,0 +1,185 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <getopt.h>
+#include <libgen.h>
+#include <stdio.h>
+
+#include "contrib/time.h"
+#include "contrib/tolower.h"
+#include "libknot/libknot.h"
+#include "knot/common/log.h"
+#include "knot/zone/semantic-check.h"
+#include "utils/common/msg.h"
+#include "utils/common/params.h"
+#include "utils/kzonecheck/zone_check.h"
+
+#define PROGRAM_NAME "kzonecheck"
+
+#define STDIN_SUBST "-"
+#define STDIN_REPL "/dev/stdin"
+
+static void print_help(void)
+{
+ printf("Usage: %s [options] <filename>\n"
+ "\n"
+ "Options:\n"
+ " -o, --origin <zone_origin> Zone name.\n"
+ " (default filename without .zone)\n"
+ " -d, --dnssec <on|off> Also check DNSSEC-related records.\n"
+ " -t, --time <timestamp> Current time specification.\n"
+ " (default current UNIX time)\n"
+ " -p, --print Print the zone on stdout.\n"
+ " -v, --verbose Enable debug output.\n"
+ " -h, --help Print the program help.\n"
+ " -V, --version Print the program version.\n",
+ PROGRAM_NAME);
+}
+
+static bool str2bool(const char *s)
+{
+ switch (knot_tolower(s[0])) {
+ case '1':
+ case 'y':
+ case 't':
+ return true;
+ case 'o':
+ return knot_tolower(s[1]) == 'n';
+ default:
+ return false;
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ const char *origin = NULL;
+ bool verbose = false, print = false;
+ semcheck_optional_t optional = SEMCHECK_DNSSEC_AUTO; // default value for --dnssec
+ knot_time_t check_time = (knot_time_t)time(NULL);
+
+ /* Long options. */
+ struct option opts[] = {
+ { "origin", required_argument, NULL, 'o' },
+ { "time", required_argument, NULL, 't' },
+ { "dnssec", required_argument, NULL, 'd' },
+ { "print", no_argument, NULL, 'p' },
+ { "verbose", no_argument, NULL, 'v' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL }
+ };
+
+ /* Set the time zone. */
+ tzset();
+
+ /* Parse command line arguments */
+ int opt = 0;
+ while ((opt = getopt_long(argc, argv, "o:t:d:pvVh", opts, NULL)) != -1) {
+ switch (opt) {
+ case 'o':
+ origin = optarg;
+ break;
+ case 'p':
+ print = true;
+ break;
+ case 'v':
+ verbose = true;
+ break;
+ case 'h':
+ print_help();
+ return EXIT_SUCCESS;
+ case 'V':
+ print_version(PROGRAM_NAME);
+ return EXIT_SUCCESS;
+ case 'd':
+ optional = str2bool(optarg) ? SEMCHECK_DNSSEC_ON : SEMCHECK_DNSSEC_OFF;
+ break;
+ case 't':
+ if (knot_time_parse("YMDhms|#|+-#U|+-#",
+ optarg, &check_time) != KNOT_EOK) {
+ ERR2("unknown time format");
+ return EXIT_FAILURE;
+ }
+ break;
+ default:
+ print_help();
+ return EXIT_FAILURE;
+ }
+ }
+
+ /* Check if there's at least one remaining non-option. */
+ if (optind >= argc) {
+ ERR2("expected zone file name");
+ print_help();
+ return EXIT_FAILURE;
+ }
+
+ char *filename = argv[optind];
+ if (strncmp(filename, STDIN_SUBST, sizeof(STDIN_SUBST)) == 0) {
+ filename = STDIN_REPL;
+ }
+
+ char *zonename;
+ if (origin == NULL) {
+ /* Get zone name from file name. */
+ const char *ext = ".zone";
+ zonename = basename(filename);
+ if (strcmp(zonename + strlen(zonename) - strlen(ext), ext) == 0) {
+ zonename = strndup(zonename, strlen(zonename) - strlen(ext));
+ } else {
+ zonename = strdup(zonename);
+ }
+ } else {
+ zonename = strdup(origin);
+ }
+
+ log_init();
+ log_levels_set(LOG_TARGET_STDOUT, LOG_SOURCE_ANY, 0);
+ log_levels_set(LOG_TARGET_STDERR, LOG_SOURCE_ANY, 0);
+ log_levels_set(LOG_TARGET_SYSLOG, LOG_SOURCE_ANY, 0);
+ log_flag_set(LOG_FLAG_NOTIMESTAMP | LOG_FLAG_NOINFO);
+ if (verbose) {
+ log_levels_add(LOG_TARGET_STDOUT, LOG_SOURCE_ANY, LOG_UPTO(LOG_DEBUG));
+ }
+
+ knot_dname_t *dname = knot_dname_from_str_alloc(zonename);
+ knot_dname_to_lower(dname);
+ free(zonename);
+ int ret = zone_check(filename, dname, optional, (time_t)check_time, print);
+ knot_dname_free(dname, NULL);
+
+ log_close();
+
+ switch (ret) {
+ case KNOT_EOK:
+ if (verbose) {
+ INFO2("No semantic error found");
+ }
+ return EXIT_SUCCESS;
+ case KNOT_EZONEINVAL:
+ ERR2("serious semantic error detected");
+ // FALLTHROUGH
+ case KNOT_ESEMCHECK:
+ return EXIT_FAILURE;
+ case KNOT_EACCES:
+ case KNOT_EFILE:
+ ERR2("failed to load the zone file");
+ return EXIT_FAILURE;
+ default:
+ ERR2("failed to run semantic checks (%s)", knot_strerror(ret));
+ return EXIT_FAILURE;
+ }
+}
diff --git a/src/utils/kzonecheck/zone_check.c b/src/utils/kzonecheck/zone_check.c
new file mode 100644
index 0000000..542e152
--- /dev/null
+++ b/src/utils/kzonecheck/zone_check.c
@@ -0,0 +1,101 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <assert.h>
+
+#include "utils/kzonecheck/zone_check.h"
+
+#include "knot/zone/contents.h"
+#include "knot/zone/zonefile.h"
+#include "knot/zone/zone-dump.h"
+#include "utils/common/msg.h"
+
+typedef struct {
+ sem_handler_t handler;
+ unsigned errors[SEM_ERR_UNKNOWN + 1]; /*!< Counting errors by type. */
+ unsigned error_count; /*!< Total error count. */
+} err_handler_stats_t;
+
+static void err_callback(sem_handler_t *handler, const zone_contents_t *zone,
+ const knot_dname_t *node, sem_error_t error, const char *data)
+{
+ assert(handler != NULL);
+ assert(zone != NULL);
+ err_handler_stats_t *stats = (err_handler_stats_t *)handler;
+
+ knot_dname_txt_storage_t buff;
+ char *owner = knot_dname_to_str(buff, (node != NULL ? node : zone->apex->owner),
+ sizeof(buff));
+ if (owner == NULL) {
+ owner = "";
+ }
+
+ fprintf(stderr, "[%s] %s%s%s\n", owner, sem_error_msg(error),
+ (data != NULL ? " " : ""),
+ (data != NULL ? data : ""));
+
+ stats->errors[error]++;
+ stats->error_count++;
+}
+
+static void print_statistics(err_handler_stats_t *stats)
+{
+ fprintf(stderr, "\nError summary:\n");
+ for (sem_error_t i = 0; i <= SEM_ERR_UNKNOWN; ++i) {
+ if (stats->errors[i] > 0) {
+ fprintf(stderr, "%4u\t%s\n", stats->errors[i], sem_error_msg(i));
+ }
+ }
+}
+
+int zone_check(const char *zone_file, const knot_dname_t *zone_name,
+ semcheck_optional_t optional, time_t time, bool print)
+{
+ err_handler_stats_t stats = {
+ .handler = { .cb = err_callback },
+ };
+
+ zloader_t zl;
+ int ret = zonefile_open(&zl, zone_file, zone_name, optional, time);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ zl.err_handler = (sem_handler_t *)&stats;
+ zl.creator->master = true;
+
+ zone_contents_t *contents = zonefile_load(&zl);
+ zonefile_close(&zl);
+ if (contents == NULL && !stats.handler.error) {
+ return KNOT_ERROR;
+ }
+
+ if (stats.error_count > 0) {
+ print_statistics(&stats);
+ ret = stats.handler.error ? KNOT_EZONEINVAL : KNOT_ESEMCHECK;
+ if (print) {
+ fprintf(stderr, "\n");
+ }
+ }
+
+ if (print) {
+ printf(";; Zone dump (Knot DNS %s)\n", PACKAGE_VERSION);
+ zone_dump_text(contents, stdout, false, NULL);
+ }
+ zone_contents_deep_free(contents);
+
+ return ret;
+}
diff --git a/src/utils/kzonecheck/zone_check.h b/src/utils/kzonecheck/zone_check.h
new file mode 100644
index 0000000..7039f16
--- /dev/null
+++ b/src/utils/kzonecheck/zone_check.h
@@ -0,0 +1,23 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/zone/semantic-check.h"
+#include "libknot/libknot.h"
+
+int zone_check(const char *zone_file, const knot_dname_t *zone_name,
+ semcheck_optional_t optional, time_t time, bool print);
diff --git a/src/utils/kzonesign/main.c b/src/utils/kzonesign/main.c
new file mode 100644
index 0000000..e70abb6
--- /dev/null
+++ b/src/utils/kzonesign/main.c
@@ -0,0 +1,283 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <getopt.h>
+#include <stdlib.h>
+
+#include "knot/dnssec/zone-events.h"
+#include "knot/updates/zone-update.h"
+#include "knot/server/server.h"
+#include "knot/zone/adjust.h"
+#include "knot/zone/zone-load.h"
+#include "knot/zone/zonefile.h"
+#include "utils/common/msg.h"
+#include "utils/common/params.h"
+#include "utils/common/signal.h"
+#include "utils/common/util_conf.h"
+#include "contrib/strtonum.h"
+
+#define PROGRAM_NAME "kzonesign"
+
+signal_ctx_t signal_ctx = { 0 }; // global, needed by signal handler
+
+static void print_help(void)
+{
+ printf("Usage: %s [-c | -C <path>] [options] <zone_name>\n"
+ "\n"
+ "Config options:\n"
+ " -c, --config <file> Path to a textual configuration file.\n"
+ " (default %s)\n"
+ " -C, --confdb <dir> Path to a configuration database directory.\n"
+ " (default %s)\n"
+ "Options:\n"
+ " -o, --outdir <dir_name> Output directory.\n"
+ " -r, --rollover Allow key rollovers and NSEC3 re-salt.\n"
+ " -v, --verify Only verify if zone is signed correctly.\n"
+ " -t, --time <timestamp> Current time specification.\n"
+ " (default current UNIX time)\n"
+ " -h, --help Print the program help.\n"
+ " -V, --version Print the program version.\n",
+ PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR);
+}
+
+typedef struct {
+ const char *zone_name_str;
+ knot_dname_storage_t zone_name;
+ const char *outdir;
+ zone_sign_roll_flags_t rollover;
+ int64_t timestamp;
+ bool verify;
+} sign_params_t;
+
+static int zonesign(sign_params_t *params)
+{
+ char *zonefile = NULL;
+ zone_contents_t *unsigned_conts = NULL;
+ zone_t *zone_struct = NULL;
+ zone_update_t up = { 0 };
+ server_t fake_server = { 0 };
+ zone_sign_reschedule_t next_sign = { 0 };
+ int ret = KNOT_ERROR;
+
+ // set the kaspdb for close in emergency
+ signal_ctx.close_db = &fake_server.kaspdb;
+
+ conf_val_t val = conf_zone_get(conf(), C_DOMAIN, params->zone_name);
+ if (val.code != KNOT_EOK) {
+ ERR2("zone '%s' not configured", params->zone_name_str);
+ ret = KNOT_ENOENT;
+ goto fail;
+ }
+ val = conf_zone_get(conf(), C_DNSSEC_POLICY, params->zone_name);
+ if (val.code != KNOT_EOK) {
+ WARN2("DNSSEC policy not configured for zone '%s', taking defaults",
+ params->zone_name_str);
+ }
+
+ zone_struct = zone_new(params->zone_name);
+ if (zone_struct == NULL) {
+ ERR2("out of memory");
+ ret = KNOT_ENOMEM;
+ goto fail;
+ }
+
+ ret = zone_load_contents(conf(), params->zone_name, &unsigned_conts,
+ SEMCHECK_MANDATORY_SOFT, false);
+ if (ret != KNOT_EOK) {
+ ERR2("failed to load zone contents (%s)", knot_strerror(ret));
+ goto fail;
+ }
+
+ ret = zone_update_from_contents(&up, zone_struct, unsigned_conts, UPDATE_FULL);
+ if (ret != KNOT_EOK) {
+ ERR2("failed to initialize zone update (%s)", knot_strerror(ret));
+ zone_contents_deep_free(unsigned_conts);
+ goto fail;
+ }
+
+ if (params->verify) {
+ val = conf_zone_get(conf(), C_ADJUST_THR, params->zone_name);
+ ret = zone_adjust_full(up.new_cont, conf_int(&val));
+ if (ret != KNOT_EOK) {
+ ERR2("failed to adjust the zone (%s)", knot_strerror(ret));
+ zone_update_clear(&up);
+ goto fail;
+ }
+
+ ret = knot_dnssec_validate_zone(&up, conf(), params->timestamp, false);
+ if (ret != KNOT_EOK) {
+ ERR2("DNSSEC validation failed (%s)", knot_strerror(ret));
+ char type_str[16];
+ knot_dname_txt_storage_t name_str;
+ if (knot_dname_to_str(name_str, up.validation_hint.node, sizeof(name_str)) != NULL &&
+ knot_rrtype_to_string(up.validation_hint.rrtype, type_str, sizeof(type_str)) >= 0) {
+ ERR2("affected node: '%s' type '%s'", name_str, type_str);
+ }
+ } else {
+ INFO2("DNSSEC validation successful");
+ }
+ zone_update_clear(&up);
+ goto fail;
+ }
+
+ kasp_db_ensure_init(&fake_server.kaspdb, conf());
+ zone_struct->server = &fake_server;
+
+ ret = knot_dnssec_zone_sign(&up, conf(), 0, params->rollover,
+ params->timestamp, &next_sign);
+ if (ret == KNOT_DNSSEC_ENOKEY) { // exception: allow generating initial keys
+ params->rollover = KEY_ROLL_ALLOW_ALL;
+ ret = knot_dnssec_zone_sign(&up, conf(), 0, params->rollover,
+ params->timestamp, &next_sign);
+ }
+ if (ret != KNOT_EOK) {
+ ERR2("failed to sign the zone (%s)", knot_strerror(ret));
+ zone_update_clear(&up);
+ goto fail;
+ }
+
+ if (params->outdir == NULL) {
+ zonefile = conf_zonefile(conf(), params->zone_name);
+ ret = zonefile_write(zonefile, up.new_cont);
+ } else {
+ zone_contents_t *temp = zone_struct->contents;
+ zone_struct->contents = up.new_cont;
+ ret = zone_dump_to_dir(conf(), zone_struct, params->outdir);
+ zone_struct->contents = temp;
+ }
+ zone_update_clear(&up);
+ if (ret != KNOT_EOK) {
+ if (params->outdir == NULL) {
+ ERR2("failed to update zone file '%s' (%s)",
+ zonefile, knot_strerror(ret));
+ } else {
+ ERR2("failed to flush signed zone to '%s' file (%s)",
+ params->outdir, knot_strerror(ret));
+
+ }
+ goto fail;
+ }
+
+ INFO2("Next signing: %"KNOT_TIME_PRINTF, next_sign.next_sign);
+ if (params->rollover) {
+ INFO2("Next roll-over: %"KNOT_TIME_PRINTF, next_sign.next_rollover);
+ if (next_sign.next_nsec3resalt) {
+ INFO2("Next NSEC3 re-salt: %"KNOT_TIME_PRINTF, next_sign.next_nsec3resalt);
+ }
+ if (next_sign.plan_ds_check) {
+ INFO2("KSK submission to parent zone needed");
+ }
+ }
+
+fail:
+ if (fake_server.kaspdb.path != NULL) {
+ knot_lmdb_deinit(&fake_server.kaspdb);
+ }
+ zone_free(&zone_struct);
+ free(zonefile);
+
+ return ret;
+}
+
+int main(int argc, char *argv[])
+{
+ sign_params_t params = { 0 };
+
+ struct option opts[] = {
+ { "config", required_argument, NULL, 'c' },
+ { "confdb", required_argument, NULL, 'C' },
+ { "outdir", required_argument, NULL, 'o' },
+ { "rollover", no_argument, NULL, 'r' },
+ { "verify" , no_argument, NULL, 'v' },
+ { "time", required_argument, NULL, 't' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL }
+ };
+
+ tzset();
+ signal_init_std();
+
+ int opt = 0;
+ while ((opt = getopt_long(argc, argv, "c:C:o:rvt:hV", opts, NULL)) != -1) {
+ switch (opt) {
+ case 'c':
+ if (util_conf_init_file(optarg) != KNOT_EOK) {
+ goto failure;
+ }
+ break;
+ case 'C':
+ if (util_conf_init_confdb(optarg) != KNOT_EOK) {
+ goto failure;
+ }
+ break;
+ case 'o':
+ params.outdir = optarg;
+ break;
+ case 'r':
+ params.rollover = KEY_ROLL_ALLOW_ALL;
+ break;
+ case 'v':
+ params.verify = true;
+ break;
+ case 't':
+ ; uint32_t num = 0;
+ if (str_to_u32(optarg, &num) != KNOT_EOK || num == 0) {
+ print_help();
+ goto failure;
+ }
+ params.timestamp = num;
+ break;
+ case 'h':
+ print_help();
+ goto success;
+ case 'V':
+ print_version(PROGRAM_NAME);
+ goto success;
+ default:
+ print_help();
+ goto failure;
+ }
+ }
+ if (argc - optind != 1) {
+ ERR2("missing zone name");
+ print_help();
+ goto failure;
+ }
+ params.zone_name_str = argv[optind];
+ if (knot_dname_from_str(params.zone_name, params.zone_name_str,
+ sizeof(params.zone_name)) == NULL) {
+ ERR2("invalid zone name '%s'", params.zone_name_str);
+ print_help();
+ goto failure;
+ }
+ knot_dname_to_lower(params.zone_name);
+
+ if (util_conf_init_default(false) != KNOT_EOK) {
+ goto failure;
+ }
+
+ if (zonesign(&params) != KNOT_EOK) {
+ goto failure;
+ }
+
+success:
+ util_conf_deinit();
+ return EXIT_SUCCESS;
+failure:
+ util_conf_deinit();
+ return EXIT_FAILURE;
+}