summaryrefslogtreecommitdiffstats
path: root/src/utils/common
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:24:08 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:24:08 +0000
commitf449f278dd3c70e479a035f50a9bb817a9b433ba (patch)
tree8ca2bfb785dda9bb4d573acdf9b42aea9cd51383 /src/utils/common
parentInitial commit. (diff)
downloadknot-upstream.tar.xz
knot-upstream.zip
Adding upstream version 3.2.6.upstream/3.2.6upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/utils/common')
-rw-r--r--src/utils/common/cert.c61
-rw-r--r--src/utils/common/cert.h36
-rw-r--r--src/utils/common/exec.c982
-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.c525
-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.c896
-rw-r--r--src/utils/common/netio.h239
-rw-r--r--src/utils/common/params.c343
-rw-r--r--src/utils/common/params.h168
-rw-r--r--src/utils/common/quic.c887
-rw-r--r--src/utils/common/quic.h125
-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/tls.c739
-rw-r--r--src/utils/common/tls.h81
-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
28 files changed, 6795 insertions, 0 deletions
diff --git a/src/utils/common/cert.c b/src/utils/common/cert.c
new file mode 100644
index 0000000..1b76b23
--- /dev/null
+++ b/src/utils/common/cert.c
@@ -0,0 +1,61 @@
+/* Copyright (C) 2016 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 <gnutls/abstract.h>
+#include <gnutls/crypto.h>
+
+#include "utils/common/cert.h"
+#include "libknot/error.h"
+
+static int spki_hash(gnutls_x509_crt_t cert, gnutls_digest_algorithm_t alg,
+ uint8_t *hash, size_t size)
+{
+ if (!cert || !hash || gnutls_hash_get_len(alg) != size) {
+ return KNOT_EINVAL;
+ }
+
+ gnutls_pubkey_t key = { 0 };
+ if (gnutls_pubkey_init(&key) != GNUTLS_E_SUCCESS) {
+ return KNOT_ENOMEM;
+ }
+
+ if (gnutls_pubkey_import_x509(key, cert, 0) != GNUTLS_E_SUCCESS) {
+ gnutls_pubkey_deinit(key);
+ return KNOT_ERROR;
+ }
+
+ gnutls_datum_t der = { 0 };
+ if (gnutls_pubkey_export2(key, GNUTLS_X509_FMT_DER, &der) != GNUTLS_E_SUCCESS) {
+ gnutls_pubkey_deinit(key);
+ return KNOT_ERROR;
+ }
+
+ int ret = gnutls_hash_fast(alg, der.data, der.size, hash);
+
+ gnutls_free(der.data);
+ gnutls_pubkey_deinit(key);
+
+ if (ret != GNUTLS_E_SUCCESS) {
+ return KNOT_ERROR;
+ }
+
+ return KNOT_EOK;
+}
+
+int cert_get_pin(gnutls_x509_crt_t cert, uint8_t *pin, size_t size)
+{
+ return spki_hash(cert, GNUTLS_DIG_SHA256, pin, size);
+}
diff --git a/src/utils/common/cert.h b/src/utils/common/cert.h
new file mode 100644
index 0000000..51e3d53
--- /dev/null
+++ b/src/utils/common/cert.h
@@ -0,0 +1,36 @@
+/* Copyright (C) 2016 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 <gnutls/x509.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#define CERT_PIN_LEN 32
+
+/*!
+ * \brief Get certificate pin value.
+ *
+ * The pin is a SHA-256 hash of the X.509 SubjectPublicKeyInfo.
+ *
+ * \param[in] crt Certificate.
+ * \param[out] pin Pin.
+ * \param[in] size Length of the pin, must be CERT_PIN_LEN.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int cert_get_pin(gnutls_x509_crt_t crt, uint8_t *pin, size_t size);
diff --git a/src/utils/common/exec.c b/src/utils/common/exec.c
new file mode 100644
index 0000000..dfecd9a
--- /dev/null
+++ b/src/utils/common/exec.c
@@ -0,0 +1,982 @@
+/* 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/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)
+{
+ char flags[64] = "";
+ char unknown_rcode[64] = "";
+ char unknown_opcode[64] = "";
+
+ const char *rcode_str = NULL;
+ const char *opcode_str = NULL;
+
+ // 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);
+ }
+
+ uint16_t id = knot_wire_get_id(packet->wire);
+ uint16_t qdcount = knot_wire_get_qdcount(packet->wire);
+ uint16_t ancount = knot_wire_get_ancount(packet->wire);
+ uint16_t nscount = knot_wire_get_nscount(packet->wire);
+ uint16_t 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)
+{
+ 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) {
+ return;
+ }
+
+ char str[16];
+
+ jsonw_list(w, name);
+
+ for (int i = 0; i < section->count; i++) {
+ const knot_rrset_t *rr = knot_pkt_rr(section, i);
+ 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 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 (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 = knot_wire_get_qdcount(packet->wire);
+ uint16_t ancount = knot_wire_get_ancount(packet->wire);
+ uint16_t nscount = knot_wire_get_nscount(packet->wire);
+ uint16_t arcount = knot_wire_get_arcount(packet->wire);
+
+ // 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..de98586
--- /dev/null
+++ b/src/utils/common/https.c
@@ -0,0 +1,525 @@
+/* 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)
+{
+ // size of number in text form (base 10)
+ char content_length[sizeof(size_t) * 3 + 1]; // limit for x->inf: log10(2^(8*sizeof(x))-1)/sizeof(x) = 2,408239965 -> 3
+ int content_length_len = sprintf(content_length, "%zu", ctx->send_buflen);
+
+ 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..4f31551
--- /dev/null
+++ b/src/utils/common/netio.c
@@ -0,0 +1,896 @@
+/* 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 <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/proxyv2/proxyv2.h"
+#include "contrib/sockaddr.h"
+
+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 int socktype,
+ char **dst)
+{
+ char addr_str[SOCKADDR_STRLEN] = {0};
+
+ // Get network address string and port number.
+ sockaddr_tostr(addr_str, sizeof(addr_str), ss);
+
+ // Calculate needed buffer size
+ const char *sock_name = get_sockname(socktype);
+ size_t buflen = strlen(addr_str) + strlen(sock_name) + 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, sock_name);
+ 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 tls_params_t *tls_params,
+ const https_params_t *https_params,
+ const quic_params_t *quic_params,
+ 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;
+ }
+
+ // Prepare for TLS.
+ if (tls_params != NULL && tls_params->enable) {
+ int ret = 0;
+#ifdef LIBNGHTTP2
+ // Prepare for HTTPS.
+ if (https_params != NULL && https_params->enable) {
+ ret = tls_ctx_init(&net->tls, tls_params,
+ GNUTLS_NONBLOCK, net->wait);
+ if (ret != KNOT_EOK) {
+ net_clean(net);
+ return ret;
+ }
+ 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) {
+ 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;
+ }
+ ret = quic_ctx_init(&net->quic, &net->tls, quic_params);
+ if (ret != KNOT_EOK) {
+ net_clean(net);
+ return ret;
+ }
+ } else
+#endif //ENABLE_QUIC
+ {
+ 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;
+}
+
+#ifdef ENABLE_QUIC
+static int fd_set_recv_ecn(int fd, int family)
+{
+ unsigned int tos = 1;
+ switch (family) {
+ case AF_INET:
+#ifdef IP_RECVTOS
+ if (setsockopt(fd, IPPROTO_IP, IP_RECVTOS, &tos, sizeof(tos)) == -1) {
+ return knot_map_errno();
+ }
+#endif
+ break;
+ case AF_INET6:
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_RECVTCLASS, &tos, sizeof(tos)) == -1) {
+ return knot_map_errno();
+ }
+ break;
+ default:
+ return KNOT_EINVAL;
+ }
+ return KNOT_EOK;
+}
+#endif
+
+
+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,
+ net->socktype, &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);
+ close(sockfd);
+ 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);
+ close(sockfd);
+ 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);
+ close(sockfd);
+ 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) {
+ close(sockfd);
+ 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) {
+ close(sockfd);
+ return ret;
+ }
+ ret = tls_ctx_connect(&net->tls, sockfd, fastopen,
+ (struct sockaddr_storage *)net->srv->ai_addr);
+ }
+ if (ret != KNOT_EOK) {
+ close(sockfd);
+ return ret;
+ }
+ }
+ }
+#ifdef ENABLE_QUIC
+ else if (net->socktype == SOCK_DGRAM) {
+ if (net->quic.params.enable) {
+ // Establish QUIC connection.
+ ret = fd_set_recv_ecn(sockfd, net->srv->ai_family);
+ if (ret != KNOT_EOK) {
+ close(sockfd);
+ return ret;
+ }
+ ret = tls_ctx_setup_remote_endpoint(&net->tls,
+ doq_alpn, 4, QUIC_PRIORITY, net_get_remote(net));
+ if (ret != 0) {
+ close(sockfd);
+ return ret;
+ }
+ ret = quic_ctx_connect(&net->quic, sockfd,
+ (struct addrinfo *)net->srv);
+ if (ret != KNOT_EOK) {
+ close(sockfd);
+ 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,
+ net->socktype, &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, net->socktype, &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..824b7a6
--- /dev/null
+++ b/src/utils/common/netio.h
@@ -0,0 +1,239 @@
+/* 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 <netdb.h>
+#include <stdint.h>
+#include <sys/socket.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 int socket type to the common string one.
+ *
+ * \param ss Socket address storage.
+ * \param socktype Socket type (SOCK_STREAM or SOCK_DGRAM).
+ * \param dst Output string.
+ */
+void get_addr_str(const struct sockaddr_storage *ss,
+ const int 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 tls_params TLS parameters.
+ * \param https_params HTTPS parameters.
+ * \param flags Connection flags.
+ * \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 tls_params_t *tls_params,
+ const https_params_t *https_params,
+ const quic_params_t *quic_params,
+ const struct sockaddr *proxy_src,
+ const struct sockaddr *proxy_dst,
+ net_t *net);
+
+/*!
+ * \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..d70d3e0
--- /dev/null
+++ b/src/utils/common/params.h
@@ -0,0 +1,168 @@
+/* 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 <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;
+
+ /*!< 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..f73b8c4
--- /dev/null
+++ b/src/utils/common/quic.c
@@ -0,0 +1,887 @@
+/* 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 <stddef.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 "libdnssec/error.h"
+#include "libdnssec/random.h"
+#include "libknot/xdp/tcp_iobuf.h"
+#include "utils/common/params.h"
+
+#define quic_ceil_duration_to_ms(x) (((x) + NGTCP2_MILLISECONDS - 1) / NGTCP2_MILLISECONDS)
+#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 quic_timeout(ts, wait) (((ts) + NGTCP2_SECONDS * (wait)) <= quic_timestamp())
+
+const gnutls_datum_t doq_alpn[] = {
+ {
+ .data = (unsigned char *)"doq",
+ .size = 3
+ },{
+ .data = (unsigned char *)"doq-i12",
+ .size = 7
+ },{
+ .data = (unsigned char *)"doq-i11",
+ .size = 7
+ },{
+ .data = (unsigned char *)"doq-i03",
+ .size = 7
+ }
+};
+
+#define set_application_error(ctx, error_code, reason, reason_len) \
+ ngtcp2_connection_close_error_set_application_error(&(ctx)->last_err, \
+ error_code, reason, reason_len)
+
+#define set_transport_error(ctx, error_code, reason, reason_len) \
+ ngtcp2_connection_close_error_set_transport_error(&(ctx)->last_err, \
+ error_code, reason, reason_len)
+
+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) {
+ return 0;
+ }
+
+ struct iovec in = {
+ .iov_base = (uint8_t *)data,
+ .iov_len = datalen
+ };
+
+ int ret = knot_tcp_inbuf_update(&ctx->stream.in_buffer, in,
+ &ctx->stream.in_parsed, &ctx->stream.in_parsed_size,
+ &ctx->stream.in_parsed_total);
+ if (ret != KNOT_EOK) {
+ return NGTCP2_ERR_CALLBACK_FAILURE;
+ }
+
+ ctx->idle_ts = quic_timestamp();
+ 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 != -1) {
+ return KNOT_EOK;
+ }
+
+ int ret = ngtcp2_conn_open_bidi_stream(ctx->conn, &ctx->stream.id, NULL);
+ if (ret) {
+ return KNOT_ERROR;
+ }
+
+ ctx->stream.resets = 3;
+
+ return KNOT_EOK;
+}
+
+static int extend_max_bidi_streams_cb(ngtcp2_conn *conn, uint64_t max_streams,
+ void *user_data)
+{
+ (void)conn;
+
+ quic_ctx_t *ctx = (quic_ctx_t *)user_data;
+ if(max_streams > 0) {
+ int ret = quic_open_bidi_stream(ctx);
+ if (ret != KNOT_EOK) {
+ return NGTCP2_ERR_CALLBACK_FAILURE;
+ }
+ }
+ return 0;
+}
+
+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) {
+ if (--ctx->stream.resets <= 0) {
+ //TODO test
+ set_transport_error(ctx, NGTCP2_PROTOCOL_VIOLATION, NULL, 0);
+ quic_ctx_close(ctx);
+ }
+ }
+
+ 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 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,
+ extend_max_bidi_streams_cb,
+ 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,
+ NULL, /* recv_rx_key */
+ 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();
+ size_t tb_send = 0;
+ for (int i = 0; i < datavlen; ++i) {
+ tb_send += datav[i].len;
+ }
+
+ while(1) {
+ int64_t stream = -1;
+ uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_NONE;
+ if (datavlen != 0) {
+ flags = NGTCP2_WRITE_STREAM_FLAG_FIN;
+ stream = 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, datav, datavlen,
+ ts);
+ if (send_datalen == tb_send) {
+ ctx->stream.out_ack = send_datalen;
+ datav = NULL;
+ datavlen = 0;
+ }
+ if (nwrite < 0) {
+ switch(nwrite) {
+ case NGTCP2_ERR_WRITE_MORE:
+ assert(0);
+ continue;
+ case NGTCP2_ERR_STREAM_SHUT_WR:
+ ctx->stream.id = -1;
+ // [[ fallthrough ]]
+ default:
+ set_transport_error(ctx,
+ ngtcp2_err_infer_quic_transport_error_code(nwrite),
+ NULL, 0);
+ return KNOT_NET_ESEND;
+ }
+ } else if (nwrite == 0) {
+ ngtcp2_conn_update_pkt_tx_time(ctx->conn, ts);
+ return KNOT_EOK;
+ }
+
+ msg_iov.iov_len = (size_t)nwrite;
+
+ int ret = quic_set_enc(sockfd, family, ctx->pi.ecn);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (sendmsg(sockfd, &msg, 0) == -1) {
+ set_transport_error(ctx, NGTCP2_INTERNAL_ERROR, NULL,
+ 0);
+ return KNOT_NET_ESEND;
+ }
+ }
+ 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 = quic_get_ecn(&msg, from.sin6_family);
+ if (errno == ENOENT) {
+ pi = NULL;
+ } else if (errno != 0) {
+ return knot_map_errno();
+ }
+
+ int ret = ngtcp2_conn_read_pkt(ctx->conn,
+ ngtcp2_conn_get_path(ctx->conn),
+ pi, enc_buf, nwrite,
+ quic_timestamp());
+ if (ret != 0) {
+ if (ret == NGTCP2_ERR_DROP_CONN) {
+ ctx->state = CLOSED;
+ } else 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 &&
+ ctx->stream.in_parsed_it < ctx->stream.in_parsed_size) {
+ struct iovec *it =
+ &ctx->stream.in_parsed[ctx->stream.in_parsed_it];
+ if (buf_len < it->iov_len) {
+ return KNOT_ENOMEM;
+ }
+ ctx->stream.in_parsed_it++;
+ size_t len = it->iov_len;
+ memcpy(buf, it->iov_base, len);
+ if (ctx->stream.in_parsed_it == ctx->stream.in_parsed_size) {
+ free(ctx->stream.in_parsed);
+ ctx->stream.in_parsed = NULL;
+ ctx->stream.in_parsed_size = 0;
+ }
+ 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;
+}
+
+int quic_set_enc(int sockfd, int family, uint32_t ecn)
+{
+ switch (family) {
+ case AF_INET:
+ if (setsockopt(sockfd, IPPROTO_IP, IP_TOS, &ecn,
+ (socklen_t)sizeof(ecn)) == -1) {
+ return knot_map_errno();
+ }
+ break;
+ case AF_INET6:
+ if (setsockopt(sockfd, IPPROTO_IPV6, IPV6_TCLASS, &ecn,
+ (socklen_t)sizeof(ecn)) == -1) {
+ return knot_map_errno();
+ }
+ break;
+ default:
+ return KNOT_ENOTSUP;
+ }
+ return KNOT_EOK;
+}
+
+uint32_t quic_get_ecn(struct msghdr *msg, const int family)
+{
+ errno = 0;
+ switch (family) {
+ case AF_INET:
+ for (struct cmsghdr *cmsg = CMSG_FIRSTHDR(msg); cmsg;
+ cmsg = CMSG_NXTHDR(msg, cmsg)) {
+ if (cmsg->cmsg_level == IPPROTO_IP &&
+ cmsg->cmsg_type == IP_TOS && cmsg->cmsg_len) {
+ return *(uint8_t *)CMSG_DATA(cmsg);
+ }
+ }
+ errno = ENOENT;
+ break;
+ case AF_INET6:
+ for (struct cmsghdr *cmsg = CMSG_FIRSTHDR(msg); cmsg;
+ cmsg = CMSG_NXTHDR(msg, cmsg)) {
+ if (cmsg->cmsg_level == IPPROTO_IPV6 &&
+ cmsg->cmsg_type == IPV6_TCLASS && cmsg->cmsg_len) {
+ return *(uint8_t *)CMSG_DATA(cmsg);
+ }
+ }
+ errno = ENOENT;
+ break;
+ default:
+ errno = ENOTSUP;
+ }
+
+ return 0;
+}
+
+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 = OPENING;
+ ctx->stream.id = -1;
+ set_application_error(ctx, DOQ_NO_ERROR, NULL, 0);
+ if (quic_generate_secret(ctx->secret, sizeof(ctx->secret)) != KNOT_EOK) {
+ tls_ctx_deinit(ctx->tls);
+ return KNOT_ENOMEM;
+ }
+
+ gnutls_certificate_set_verify_function(tls_ctx->credentials,
+ verify_certificate);
+
+ return KNOT_EOK;
+}
+
+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) {
+ tls_ctx_deinit(ctx->tls);
+ 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) {
+ tls_ctx_deinit(ctx->tls);
+ return ret;
+ }
+ dcid.datalen = 18;
+ ret = dnssec_random_buffer(dcid.data, dcid.datalen);
+ if (ret != DNSSEC_EOK) {
+ tls_ctx_deinit(ctx->tls);
+ return ret;
+ }
+
+ ctx->idle_ts = quic_timestamp();
+
+ ngtcp2_settings settings;
+ ngtcp2_settings_default(&settings);
+ settings.initial_ts = ctx->idle_ts;
+ 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;
+
+ 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) {
+ tls_ctx_deinit(ctx->tls);
+ 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 (ngtcp2_conn_client_new(&ctx->conn, &dcid, &scid, &path,
+ NGTCP2_PROTO_VER_V1, &quic_client_callbacks,
+ &settings, &params, NULL, ctx) != 0) {
+ tls_ctx_deinit(ctx->tls);
+ 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) {
+ tls_ctx_deinit(ctx->tls);
+ return KNOT_NET_ECONNECT;
+ }
+ gnutls_session_set_ptr(ctx->tls->session, ctx);
+ ngtcp2_conn_set_tls_native_handle(ctx->conn, ctx->tls->session);
+
+ // Initialize poll descriptor structure.
+ struct pollfd pfd = {
+ .fd = sockfd,
+ .events = POLLIN,
+ .revents = 0,
+ };
+ ctx->tls->sockfd = sockfd;
+
+ int timeout = ctx->tls->wait * 1000;
+ while(ctx->state != CONNECTED) {
+ if (quic_timeout(ctx->idle_ts, ctx->tls->wait)) {
+ WARN("QUIC, peer took too long to respond");
+ tls_ctx_deinit(ctx->tls);
+ return KNOT_NET_ETIMEOUT;
+ }
+ ret = quic_send(ctx, sockfd, dst_addr->ai_family);
+ if (ret != KNOT_EOK) {
+ tls_ctx_deinit(ctx->tls);
+ return ret;
+ }
+
+ ret = poll(&pfd, 1, timeout);
+ if (ret < 0) {
+ tls_ctx_deinit(ctx->tls);
+ return knot_map_errno();
+ } else if (ret == 0) {
+ continue;
+ }
+
+ ret = quic_recv(ctx, sockfd);
+ if (ret != KNOT_EOK) {
+ tls_ctx_deinit(ctx->tls);
+ return ret;
+ }
+ const ngtcp2_transport_params *pp =
+ ngtcp2_conn_get_remote_transport_params(ctx->conn);
+ if (pp != NULL) {
+ timeout = quic_ceil_duration_to_ms(pp->max_ack_delay);
+ }
+ }
+
+ 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;
+ }
+
+ uint16_t query_length = htons(buf_len);
+ ngtcp2_vec datav[] = {
+ {
+ .base = (uint8_t *)&query_length,
+ .len = sizeof(uint16_t)
+ },{
+ .base = (uint8_t *)buf,
+ .len = buf_len
+ }
+ };
+ size_t datavlen = sizeof(datav)/sizeof(*datav);
+ ngtcp2_vec *pdatav = datav;
+
+ struct pollfd pfd = {
+ .fd = sockfd,
+ .events = POLLIN,
+ .revents = 0,
+ };
+
+ // Open stream when connection keep-opened
+ if (ctx->stream.id == -1) {
+ quic_open_bidi_stream(ctx);
+ quic_send(ctx, sockfd, srv->ai_family);
+ }
+
+ int timeout = ctx->tls->wait * 1000;
+ while (ctx->stream.out_ack == 0) {
+ if (quic_timeout(ctx->idle_ts, ctx->tls->wait)) {
+ WARN("QUIC, failed to send");
+ set_application_error(ctx, DOQ_REQUEST_CANCELLED,
+ (uint8_t *)"Connection timeout",
+ sizeof("Connection timeout") - 1);
+ return KNOT_NET_ETIMEOUT;
+ }
+ int ret = quic_send_data(ctx, sockfd, srv->ai_family, pdatav,
+ datavlen);
+ if (ret != KNOT_EOK) {
+ WARN("QUIC, failed to send");
+ return ret;
+ }
+ if (ctx->stream.out_ack > 0) {
+ pdatav = NULL;
+ datavlen = 0;
+ }
+
+ const ngtcp2_transport_params *pp =
+ ngtcp2_conn_get_remote_transport_params(ctx->conn);
+ if (pp != NULL) {
+ timeout = quic_ceil_duration_to_ms(pp->max_ack_delay);
+ }
+ ret = poll(&pfd, 1, timeout);
+ if (ret < 0) {
+ WARN("QUIC, failed to send");
+ return knot_map_errno();
+ } else if (ret == 0) {
+ continue;
+ }
+ ret = quic_recv(ctx, sockfd);
+ if (ret != KNOT_EOK) {
+ WARN("QUIC, failed to send");
+ return ret;
+ }
+ if (ctx->stream.in_parsed_size) {
+ return KNOT_EOK;
+ }
+ }
+
+ 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,
+ };
+
+ int timeout = ctx->tls->wait * 1000;
+ while (!quic_timeout(ctx->idle_ts, ctx->tls->wait)) {
+ const ngtcp2_transport_params *pp =
+ ngtcp2_conn_get_remote_transport_params(ctx->conn);
+ if (pp != NULL) {
+ timeout = quic_ceil_duration_to_ms(pp->max_ack_delay);
+ }
+ 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) {
+ 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");
+ set_application_error(ctx, DOQ_REQUEST_CANCELLED,
+ (uint8_t *)"Connection timeout",
+ sizeof("Connection timeout") - 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) {
+ quic_set_enc(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);
+ }
+
+ if (ctx->stream.in_parsed != NULL) {
+ free(ctx->stream.in_parsed);
+ }
+}
+
+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..74623f1
--- /dev/null
+++ b/src/utils/common/quic.h
@@ -0,0 +1,125 @@
+/* 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 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_CIPHERS "-CIPHER-ALL:+AES-128-GCM:+AES-256-GCM:+CHACHA20-POLY1305:+AES-128-CCM"
+#define QUIC_DEFAULT_GROUPS "-GROUP-ALL:+GROUP-SECP256R1:+GROUP-X25519:+GROUP-SECP384R1:+GROUP-SECP521R1"
+#define QUIC_PRIORITY "%DISABLE_TLS13_COMPAT_MODE:NORMAL:"QUIC_DEFAULT_VERSION":"QUIC_DEFAULT_CIPHERS":"QUIC_DEFAULT_GROUPS
+
+typedef enum {
+ OPENING,
+ CONNECTED,
+ CLOSING,
+ CLOSED
+} 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 iovec *in_parsed;
+ size_t in_parsed_size;
+ size_t in_parsed_total;
+ size_t in_parsed_it;
+ int resets;
+ } stream;
+ ngtcp2_connection_close_error last_err;
+ uint8_t secret[32];
+ tls_ctx_t *tls;
+ ngtcp2_conn *conn;
+ ngtcp2_pkt_info pi;
+ quic_state_t state;
+ uint64_t idle_ts;
+} 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_set_enc(int sockfd, int family, uint32_t ecn);
+
+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/tls.c b/src/utils/common/tls.c
new file mode 100644
index 0000000..c440769
--- /dev/null
+++ b/src/utils/common/tls.c
@@ -0,0 +1,739 @@
+/* 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 <arpa/inet.h>
+#include <stdbool.h>
+#include <string.h>
+#include <poll.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/cert.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 };
+ ret = cert_get_pin(cert, cert_pin, sizeof(cert_pin));
+ if (ret != KNOT_EOK) {
+ gnutls_x509_crt_deinit(cert);
+ return GNUTLS_E_CERTIFICATE_ERROR;
+ }
+
+ // 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) {
+ gnutls_deinit(ctx->session);
+ 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) {
+ gnutls_deinit(ctx->session);
+ 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) {
+ gnutls_deinit(ctx->session);
+ return KNOT_EINVAL;
+ }
+
+ if (remote != NULL) {
+ ret = gnutls_server_name_set(ctx->session, GNUTLS_NAME_DNS, remote,
+ strlen(remote));
+ if (ret != GNUTLS_E_SUCCESS) {
+ gnutls_deinit(ctx->session);
+ 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
+ gnutls_deinit(ctx->session);
+ 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");
+ gnutls_deinit(ctx->session);
+ 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) {
+ 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..7c25ab7
--- /dev/null
+++ b/src/utils/common/tls.h
@@ -0,0 +1,81 @@
+/* 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 <netdb.h>
+#include <gnutls/gnutls.h>
+
+#include "contrib/sockaddr.h"
+#include "contrib/ucw/lists.h"
+
+/*! \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..231f800
--- /dev/null
+++ b/src/utils/common/util_conf.c
@@ -0,0 +1,139 @@
+/* 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 <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, true, false);
+ 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, false, false);
+ 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);