diff options
Diffstat (limited to '')
71 files changed, 23111 insertions, 0 deletions
diff --git a/src/utils/Makefile.inc b/src/utils/Makefile.inc new file mode 100644 index 0000000..b39b10d --- /dev/null +++ b/src/utils/Makefile.inc @@ -0,0 +1,188 @@ +bin_PROGRAMS = +sbin_PROGRAMS = + +if HAVE_LIBUTILS +noinst_LTLIBRARIES += libknotus.la + +libknotus_la_CPPFLAGS = $(embedded_libngtcp2_CFLAGS) \ + $(AM_CPPFLAGS) $(CFLAG_VISIBILITY) $(gnutls_CFLAGS) \ + $(libedit_CFLAGS) $(libidn2_CFLAGS) $(libidn_CFLAGS) \ + $(libkqueue_CFLAGS) $(libnghttp2_CFLAGS) $(libngtcp2_CFLAGS) \ + $(lmdb_CFLAGS) +libknotus_la_LDFLAGS = $(AM_LDFLAGS) $(LDFLAG_EXCLUDE_LIBS) +libknotus_la_LIBADD = $(libidn2_LIBS) $(libidn_LIBS) $(libnghttp2_LIBS) $(libngtcp2_LIBS) +libknotus_LIBS = libknotus.la libknot.la libdnssec.la $(libcontrib_LIBS) \ + $(gnutls_LIBS) $(libedit_LIBS) + +if EMBEDDED_LIBNGTCP2 +libknotus_la_LIBADD += $(libembngtcp2_LIBS) +endif EMBEDDED_LIBNGTCP2 + +libknotus_la_SOURCES = \ + utils/common/exec.c \ + utils/common/exec.h \ + utils/common/hex.c \ + utils/common/hex.h \ + utils/common/https.c \ + utils/common/https.h \ + utils/common/lookup.c \ + utils/common/lookup.h \ + utils/common/msg.c \ + utils/common/msg.h \ + utils/common/netio.c \ + utils/common/netio.h \ + utils/common/params.c \ + utils/common/params.h \ + utils/common/quic.c \ + utils/common/quic.h \ + utils/common/resolv.c \ + utils/common/resolv.h \ + utils/common/sign.c \ + utils/common/sign.h \ + utils/common/signal.c \ + utils/common/signal.h \ + utils/common/tls.c \ + utils/common/tls.h \ + utils/common/token.c \ + utils/common/token.h \ + utils/common/util_conf.c \ + utils/common/util_conf.h +endif HAVE_LIBUTILS + +if HAVE_UTILS +bin_PROGRAMS += kdig khost knsec3hash knsupdate + +kdig_SOURCES = \ + utils/kdig/kdig_exec.c \ + utils/kdig/kdig_exec.h \ + utils/kdig/kdig_main.c \ + utils/kdig/kdig_params.c \ + utils/kdig/kdig_params.h + +khost_SOURCES = \ + utils/kdig/kdig_exec.c \ + utils/kdig/kdig_exec.h \ + utils/kdig/kdig_params.c \ + utils/kdig/kdig_params.h \ + utils/khost/khost_main.c \ + utils/khost/khost_params.c \ + utils/khost/khost_params.h + +knsec3hash_SOURCES = \ + utils/knsec3hash/knsec3hash.c + +knsupdate_SOURCES = \ + utils/knsupdate/knsupdate_exec.c \ + utils/knsupdate/knsupdate_exec.h \ + utils/knsupdate/knsupdate_interactive.c \ + utils/knsupdate/knsupdate_interactive.h \ + utils/knsupdate/knsupdate_main.c \ + utils/knsupdate/knsupdate_params.c \ + utils/knsupdate/knsupdate_params.h + +kdig_CPPFLAGS = $(libknotus_la_CPPFLAGS) +kdig_LDADD = $(libknotus_LIBS) +khost_CPPFLAGS = $(libknotus_la_CPPFLAGS) +khost_LDADD = $(libknotus_LIBS) +knsec3hash_CPPFLAGS = $(libknotus_la_CPPFLAGS) +knsec3hash_LDADD = libknot.la libdnssec.la $(libcontrib_LIBS) +knsupdate_CPPFLAGS = $(libknotus_la_CPPFLAGS) +knsupdate_LDADD = $(libknotus_LIBS) libzscanner.la + +if HAVE_DNSTAP +kdig_CPPFLAGS += $(DNSTAP_CFLAGS) +kdig_LDADD += $(libdnstap_LIBS) +khost_CPPFLAGS += $(DNSTAP_CFLAGS) +khost_LDADD += $(libdnstap_LIBS) +endif HAVE_DNSTAP + +if ENABLE_XDP +sbin_PROGRAMS += kxdpgun +kxdpgun_SOURCES = \ + utils/kxdpgun/ip_route.c \ + utils/kxdpgun/ip_route.h \ + utils/kxdpgun/load_queries.c \ + utils/kxdpgun/load_queries.h \ + utils/kxdpgun/main.c + +kxdpgun_CPPFLAGS = $(libknotus_la_CPPFLAGS) $(libmnl_CFLAGS) +kxdpgun_LDADD = libknot.la $(libcontrib_LIBS) $(libmnl_LIBS) $(pthread_LIBS) +if ENABLE_QUIC +kxdpgun_CPPFLAGS += $(gnutls_CFLAGS) +kxdpgun_LDADD += $(gnutls_LIBS) +endif ENABLE_QUIC +endif ENABLE_XDP +endif HAVE_UTILS + +if HAVE_DAEMON +# Create storage and run-time directories +install-data-hook: + $(INSTALL) -d $(DESTDIR)/@config_dir@ + $(INSTALL) -d $(DESTDIR)/@run_dir@ + $(INSTALL) -d $(DESTDIR)/@storage_dir@ + +sbin_PROGRAMS += knotc knotd + +knotc_SOURCES = \ + utils/knotc/commands.c \ + utils/knotc/commands.h \ + utils/knotc/interactive.c \ + utils/knotc/interactive.h \ + utils/knotc/process.c \ + utils/knotc/process.h \ + utils/knotc/main.c + +knotd_SOURCES = \ + utils/knotd/main.c + +knotc_CPPFLAGS = $(libknotus_la_CPPFLAGS) +knotc_LDADD = $(libknotd_LIBS) $(libknotus_LIBS) +knotc_LDFLAGS = $(AM_LDFLAGS) -rdynamic +knotd_CPPFLAGS = $(libknotus_la_CPPFLAGS) $(liburcu_CFLAGS) $(systemd_CFLAGS) +knotd_LDADD = $(malloc_LIBS) $(libknotd_LIBS) $(cap_ng_LIBS) +knotd_LDFLAGS = $(AM_LDFLAGS) -rdynamic + +if HAVE_UTILS +bin_PROGRAMS += kzonecheck kzonesign +sbin_PROGRAMS += keymgr kjournalprint kcatalogprint + +kzonecheck_SOURCES = \ + utils/kzonecheck/main.c \ + utils/kzonecheck/zone_check.c \ + utils/kzonecheck/zone_check.h + +kzonesign_SOURCES = \ + utils/kzonesign/main.c + +keymgr_SOURCES = \ + utils/keymgr/bind_privkey.c \ + utils/keymgr/bind_privkey.h \ + utils/keymgr/functions.c \ + utils/keymgr/functions.h \ + utils/keymgr/offline_ksk.c \ + utils/keymgr/offline_ksk.h \ + utils/keymgr/main.c + +kjournalprint_SOURCES = \ + utils/kjournalprint/main.c + +kcatalogprint_SOURCES = \ + utils/kcatalogprint/main.c + +kzonecheck_CPPFLAGS = $(libknotus_la_CPPFLAGS) +kzonecheck_LDADD = $(libknotd_LIBS) +kzonecheck_LDFLAGS = $(AM_LDFLAGS) -rdynamic +kzonesign_CPPFLAGS = $(libknotus_la_CPPFLAGS) +kzonesign_LDADD = $(libknotd_LIBS) $(libknotus_LIBS) +kzonesign_LDFLAGS = $(AM_LDFLAGS) -rdynamic +keymgr_CPPFLAGS = $(libknotus_la_CPPFLAGS) +keymgr_LDADD = $(libknotd_LIBS) $(libknotus_LIBS) +keymgr_LDFLAGS = $(AM_LDFLAGS) -rdynamic +kjournalprint_CPPFLAGS = $(libknotus_la_CPPFLAGS) +kjournalprint_LDADD = $(libknotd_LIBS) $(libknotus_LIBS) +kjournalprint_LDFLAGS = $(AM_LDFLAGS) -rdynamic +kcatalogprint_CPPFLAGS = $(libknotus_la_CPPFLAGS) +kcatalogprint_LDADD = $(libknotd_LIBS) $(libknotus_LIBS) +kcatalogprint_LDFLAGS = $(AM_LDFLAGS) -rdynamic +endif HAVE_UTILS +endif HAVE_DAEMON diff --git a/src/utils/common/exec.c b/src/utils/common/exec.c new file mode 100644 index 0000000..798ae42 --- /dev/null +++ b/src/utils/common/exec.c @@ -0,0 +1,1212 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <arpa/inet.h> +#include <stdlib.h> +#include <time.h> + +#include "libdnssec/random.h" +#include "utils/common/exec.h" +#include "utils/common/msg.h" +#include "utils/common/netio.h" +#include "utils/common/params.h" +#include "libknot/libknot.h" +#include "contrib/ctype.h" +#include "contrib/macros.h" +#include "contrib/sockaddr.h" +#include "contrib/time.h" +#include "contrib/openbsd/strlcat.h" +#include "contrib/ucw/lists.h" +#include "contrib/wire_ctx.h" + +static const char *JSON_INDENT = " "; + +static knot_lookup_t rtypes[] = { + { KNOT_RRTYPE_A, "has IPv4 address" }, + { KNOT_RRTYPE_NS, "nameserver is" }, + { KNOT_RRTYPE_CNAME, "is an alias for" }, + { KNOT_RRTYPE_SOA, "start of authority is" }, + { KNOT_RRTYPE_PTR, "points to" }, + { KNOT_RRTYPE_MX, "mail is handled by" }, + { KNOT_RRTYPE_TXT, "description is" }, + { KNOT_RRTYPE_AAAA, "has IPv6 address" }, + { KNOT_RRTYPE_LOC, "location is" }, + { KNOT_RRTYPE_DS, "delegation signature is" }, + { KNOT_RRTYPE_SSHFP, "SSH fingerprint is" }, + { KNOT_RRTYPE_RRSIG, "RR set signature is" }, + { KNOT_RRTYPE_DNSKEY, "DNSSEC key is" }, + { KNOT_RRTYPE_TLSA, "has TLS certificate" }, + { 0, NULL } +}; + +static void print_header(const knot_pkt_t *packet, const style_t *style) +{ + if (packet->size < KNOT_WIRE_OFFSET_QDCOUNT) { + return; + } + + char flags[64] = ""; + char unknown_rcode[64] = ""; + char unknown_opcode[64] = ""; + + const char *rcode_str = NULL; + const char *opcode_str = NULL; + + uint16_t qdcount = 0, ancount = 0, nscount = 0, arcount = 0; + + uint16_t id = knot_wire_get_id(packet->wire); + + // Get extended RCODE. + const char *code_name = knot_pkt_ext_rcode_name(packet); + if (code_name[0] != '\0') { + rcode_str = code_name; + } else { + uint16_t code = knot_pkt_ext_rcode(packet); + (void)snprintf(unknown_rcode, sizeof(unknown_rcode), "RCODE %d", code); + rcode_str = unknown_rcode; + } + + // Get OPCODE. + uint8_t code = knot_wire_get_opcode(packet->wire); + const knot_lookup_t *opcode = knot_lookup_by_id(knot_opcode_names, code); + if (opcode != NULL) { + opcode_str = opcode->name; + } else { + (void)snprintf(unknown_opcode, sizeof(unknown_opcode), "OPCODE %d", code); + opcode_str = unknown_opcode; + } + + // Get flags. + size_t flags_rest = sizeof(flags); + const size_t flag_len = 4; + if (knot_wire_get_qr(packet->wire) != 0 && flags_rest > flag_len) { + flags_rest -= strlcat(flags, " qr", flags_rest); + } + if (knot_wire_get_aa(packet->wire) != 0 && flags_rest > flag_len) { + flags_rest -= strlcat(flags, " aa", flags_rest); + } + if (knot_wire_get_tc(packet->wire) != 0 && flags_rest > flag_len) { + flags_rest -= strlcat(flags, " tc", flags_rest); + } + if (knot_wire_get_rd(packet->wire) != 0 && flags_rest > flag_len) { + flags_rest -= strlcat(flags, " rd", flags_rest); + } + if (knot_wire_get_ra(packet->wire) != 0 && flags_rest > flag_len) { + flags_rest -= strlcat(flags, " ra", flags_rest); + } + if (knot_wire_get_z(packet->wire) != 0 && flags_rest > flag_len) { + flags_rest -= strlcat(flags, " z", flags_rest); + } + if (knot_wire_get_ad(packet->wire) != 0 && flags_rest > flag_len) { + flags_rest -= strlcat(flags, " ad", flags_rest); + } + if (knot_wire_get_cd(packet->wire) != 0 && flags_rest > flag_len) { + strlcat(flags, " cd", flags_rest); + } + + if (packet->size >= KNOT_WIRE_HEADER_SIZE) { + qdcount = knot_wire_get_qdcount(packet->wire); + ancount = knot_wire_get_ancount(packet->wire); + nscount = knot_wire_get_nscount(packet->wire); + arcount = knot_wire_get_arcount(packet->wire); + + if (knot_pkt_has_tsig(packet)) { + arcount++; + } + } + + // Print formatted info. + switch (style->format) { + case FORMAT_NSUPDATE: + printf(";; ->>HEADER<<- opcode: %s; status: %s; id: %u\n" + ";; Flags:%1s; " + "ZONE: %u; PREREQ: %u; UPDATE: %u; ADDITIONAL: %u\n", + opcode_str, rcode_str, id, flags, qdcount, ancount, + nscount, arcount); + break; + default: + printf(";; ->>HEADER<<- opcode: %s; status: %s; id: %u\n" + ";; Flags:%1s; " + "QUERY: %u; ANSWER: %u; AUTHORITY: %u; ADDITIONAL: %u\n", + opcode_str, rcode_str, id, flags, qdcount, ancount, + nscount, arcount); + break; + } +} + +static void print_footer(const size_t total_len, + const size_t msg_count, + const size_t rr_count, + const net_t *net, + const float elapsed, + time_t exec_time, + const bool incoming) +{ + struct tm tm; + char date[64]; + + // Get current timestamp. + if (exec_time == 0) { + exec_time = time(NULL); + } + + // Create formatted date-time string. + localtime_r(&exec_time, &tm); + strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S %Z", &tm); + + // Print messages statistics. + if (incoming) { + printf(";; Received %zu B", total_len); + } else { + printf(";; Sent %zu B", total_len); + } + + // If multimessage (XFR) print additional statistics. + if (msg_count > 0) { + printf(" (%zu messages, %zu records)\n", msg_count, rr_count); + } else { + printf("\n"); + } + // Print date. + printf(";; Time %s\n", date); + + // Print connection statistics. + if (net != NULL) { + if (incoming) { + printf(";; From %s", net->remote_str); + } else { + printf(";; To %s", net->remote_str); + } + + if (elapsed >= 0) { + printf(" in %.1f ms\n", elapsed); + } else { + printf("\n"); + } + } +} + +static void print_hex(const uint8_t *data, uint16_t len) +{ + for (int i = 0; i < len; i++) { + printf("%02X", data[i]); + } +} + +static void print_nsid(const uint8_t *data, uint16_t len) +{ + if (len == 0) { + return; + } + + print_hex(data, len); + + // Check if printable string. + for (int i = 0; i < len; i++) { + if (!is_print(data[i])) { + return; + } + } + printf(" \"%.*s\"", len, data); +} + +static bool print_text(const uint8_t *data, uint16_t len) +{ + if (len == 0) { + return false; + } + + // Check if printable string. + for (int i = 0; i < len; i++) { + if (!is_print(data[i])) { + return false; + } + } + printf("%.*s", len, data); + return true; +} + +static void print_edns_client_subnet(const uint8_t *data, uint16_t len) +{ + knot_edns_client_subnet_t ecs = { 0 }; + int ret = knot_edns_client_subnet_parse(&ecs, data, len); + if (ret != KNOT_EOK) { + return; + } + + struct sockaddr_storage addr = { 0 }; + ret = knot_edns_client_subnet_get_addr(&addr, &ecs); + assert(ret == KNOT_EOK); + + char addr_str[SOCKADDR_STRLEN] = { 0 }; + sockaddr_tostr(addr_str, sizeof(addr_str), &addr); + + printf("%s/%u/%u", addr_str, ecs.source_len, ecs.scope_len); +} + +static void print_ede(const uint8_t *data, uint16_t len) +{ + if (len < 2) { + printf("(malformed)"); + return; + } + + + uint16_t errcode; + memcpy(&errcode, data, sizeof(errcode)); + errcode = be16toh(errcode); + + const knot_lookup_t *item = knot_lookup_by_id(knot_edns_ede_names, errcode); + const char *strerr = (item != NULL) ? item->name : "Unknown code"; + + if (len > 2) { + printf("%hu (%s): '%.*s'", errcode, strerr, (int)(len - 2), data + 2); + } else { + printf("%hu (%s)", errcode, strerr); + } +} + +static void print_expire(const uint8_t *data, uint16_t len) +{ + if (len == 0) { + printf("(empty)"); + } else if (len != sizeof(uint32_t)) { + printf("(malformed)"); + } else { + char str[80] = ""; + uint32_t timer = knot_wire_read_u32(data); + if (knot_time_print_human(timer, str, sizeof(str), false) > 0) { + printf("%u (%s)", timer, str); + } else { + printf("%u", timer); + } + } +} + +static void print_section_opt(const knot_pkt_t *packet, const style_t *style) +{ + if (style->present_edns) { + size_t buflen = 8192; + char *buf = calloc(buflen, 1); + int ret = knot_rrset_txt_dump_edns(packet->opt_rr, + knot_wire_get_rcode(packet->wire), + buf, buflen, &style->style); + if (ret < 0) { + WARN("can't print OPT record (%s)", knot_strerror(ret)); + } else { + printf(". 0 ANY EDNS\t\t\t%s\n", buf); + } + free(buf); + return; + } + + char unknown_ercode[64] = ""; + const char *ercode_str = NULL; + + uint16_t ercode = knot_edns_get_ext_rcode(packet->opt_rr); + if (ercode > 0) { + ercode = knot_edns_whole_rcode(ercode, + knot_wire_get_rcode(packet->wire)); + } + + const knot_lookup_t *item = knot_lookup_by_id(knot_rcode_names, ercode); + if (item != NULL) { + ercode_str = item->name; + } else { + (void)snprintf(unknown_ercode, sizeof(unknown_ercode), "RCODE %d", ercode); + ercode_str = unknown_ercode; + } + + printf(";; Version: %u; flags: %s; UDP size: %u B; ext-rcode: %s\n", + knot_edns_get_version(packet->opt_rr), + (knot_edns_do(packet->opt_rr) != 0) ? "do" : "", + knot_edns_get_payload(packet->opt_rr), + ercode_str); + + assert(packet->opt_rr->rrs.count > 0); + knot_rdata_t *rdata = packet->opt_rr->rrs.rdata; + wire_ctx_t wire = wire_ctx_init_const(rdata->data, rdata->len); + + while (wire_ctx_available(&wire) >= KNOT_EDNS_OPTION_HDRLEN) { + uint16_t opt_code = wire_ctx_read_u16(&wire); + uint16_t opt_len = wire_ctx_read_u16(&wire); + uint8_t *opt_data = wire.position; + + if (wire.error != KNOT_EOK) { + WARN("invalid OPT record data"); + return; + } + + switch (opt_code) { + case KNOT_EDNS_OPTION_NSID: + printf(";; NSID: "); + print_nsid(opt_data, opt_len); + break; + case KNOT_EDNS_OPTION_CLIENT_SUBNET: + printf(";; CLIENT-SUBNET: "); + print_edns_client_subnet(opt_data, opt_len); + break; + case KNOT_EDNS_OPTION_PADDING: + printf(";; PADDING: %u B", opt_len); + break; + case KNOT_EDNS_OPTION_COOKIE: + printf(";; COOKIE: "); + print_hex(opt_data, opt_len); + break; + case KNOT_EDNS_OPTION_EDE: + printf(";; EDE: "); + print_ede(opt_data, opt_len); + break; + case KNOT_EDNS_OPTION_EXPIRE: + printf(";; EXPIRE: "); + print_expire(opt_data, opt_len); + break; + default: + printf(";; Option (%u): ", opt_code); + if (style->show_edns_opt_text) { + if (!print_text(opt_data, opt_len)) { + print_hex(opt_data, opt_len); + } + } else { + print_hex(opt_data, opt_len); + } + } + printf("\n"); + + wire_ctx_skip(&wire, opt_len); + } + + if (wire_ctx_available(&wire) > 0) { + WARN("invalid OPT record data"); + } +} + +static void print_section_question(const knot_dname_t *owner, + const uint16_t qclass, + const uint16_t qtype, + const style_t *style) +{ + size_t buflen = 8192; + char *buf = calloc(buflen, 1); + + // Don't print zero TTL. + knot_dump_style_t qstyle = style->style; + qstyle.empty_ttl = true; + + knot_rrset_t *question = knot_rrset_new(owner, qtype, qclass, 0, NULL); + + if (knot_rrset_txt_dump_header(question, 0, buf, buflen, &qstyle) < 0) { + WARN("can't print whole question section"); + } + + printf("%s\n", buf); + + knot_rrset_free(question, NULL); + free(buf); +} + +static void print_section_full(const knot_rrset_t *rrsets, + const uint16_t count, + const style_t *style, + const bool no_tsig) +{ + size_t buflen = 8192; + char *buf = calloc(buflen, 1); + + for (size_t i = 0; i < count; i++) { + // Ignore OPT records. + if (rrsets[i].type == KNOT_RRTYPE_OPT) { + continue; + } + + // Exclude TSIG record. + if (no_tsig && rrsets[i].type == KNOT_RRTYPE_TSIG) { + continue; + } + + if (knot_rrset_txt_dump(&rrsets[i], &buf, &buflen, + &(style->style)) < 0) { + WARN("can't print whole section"); + break; + } + printf("%s", buf); + } + + free(buf); +} + +static void print_section_dig(const knot_rrset_t *rrsets, + const uint16_t count, + const style_t *style) +{ + size_t buflen = 8192; + char *buf = calloc(buflen, 1); + + for (size_t i = 0; i < count; i++) { + const knot_rrset_t *rrset = &rrsets[i]; + uint16_t rrset_rdata_count = rrset->rrs.count; + for (uint16_t j = 0; j < rrset_rdata_count; j++) { + while (knot_rrset_txt_dump_data(rrset, j, buf, buflen, + &(style->style)) < 0) { + buflen += 4096; + // Oversize protection. + if (buflen > 100000) { + WARN("can't print whole section"); + break; + } + + char *newbuf = realloc(buf, buflen); + if (newbuf == NULL) { + WARN("can't print whole section"); + break; + } + buf = newbuf; + } + printf("%s\n", buf); + } + } + + free(buf); +} + +static void print_section_host(const knot_rrset_t *rrsets, + const uint16_t count, + const style_t *style) +{ + size_t buflen = 8192; + char *buf = calloc(buflen, 1); + + for (size_t i = 0; i < count; i++) { + const knot_rrset_t *rrset = &rrsets[i]; + const knot_lookup_t *descr; + char type[32] = "NULL"; + char *owner; + + owner = knot_dname_to_str_alloc(rrset->owner); + if (style->style.ascii_to_idn != NULL) { + style->style.ascii_to_idn(&owner); + } + descr = knot_lookup_by_id(rtypes, rrset->type); + + uint16_t rrset_rdata_count = rrset->rrs.count; + for (uint16_t j = 0; j < rrset_rdata_count; j++) { + if (rrset->type == KNOT_RRTYPE_CNAME && + style->hide_cname) { + continue; + } + + while (knot_rrset_txt_dump_data(rrset, j, buf, buflen, + &(style->style)) < 0) { + buflen += 4096; + // Oversize protection. + if (buflen > 100000) { + WARN("can't print whole section"); + break; + } + + char *newbuf = realloc(buf, buflen); + if (newbuf == NULL) { + WARN("can't print whole section"); + break; + } + buf = newbuf; + } + + if (descr != NULL) { + printf("%s %s %s\n", owner, descr->name, buf); + } else { + knot_rrtype_to_string(rrset->type, type, sizeof(type)); + printf("%s has %s record %s\n", owner, type, buf); + } + } + + free(owner); + } + + free(buf); +} + +static void print_error_host(const knot_pkt_t *packet, const style_t *style) +{ + char type[32] = "Unknown"; + const char *rcode_str = "Unknown"; + + knot_rrtype_to_string(knot_pkt_qtype(packet), type, sizeof(type)); + + // Get extended RCODE. + const char *code_name = knot_pkt_ext_rcode_name(packet); + if (code_name[0] != '\0') { + rcode_str = code_name; + } + + // Get record owner. + char *owner = knot_dname_to_str_alloc(knot_pkt_qname(packet)); + if (style->style.ascii_to_idn != NULL) { + style->style.ascii_to_idn(&owner); + } + + if (knot_pkt_ext_rcode(packet) == KNOT_RCODE_NOERROR) { + printf("Host %s has no %s record\n", owner, type); + } else { + printf("Host %s type %s error: %s\n", owner, type, rcode_str); + } + + free(owner); +} + +static void json_dname(jsonw_t *w, const char *key, const knot_dname_t *dname) +{ + knot_dname_txt_storage_t name; + if (knot_dname_to_str(name, dname, sizeof(name)) != NULL) { + jsonw_str(w, key, name); + } +} + +static void json_rdata(jsonw_t *w, const knot_rrset_t *rrset) +{ + char type[16]; + if (knot_rrtype_to_string(rrset->type, type, sizeof(type)) <= 0 || + strncmp(type, "TYPE", 4) == 0) { // Unknown/hex format. + return; + } + + char key[32] = "rdata"; + strlcat(key, type, sizeof(key)); + + char data[16384]; + const knot_dump_style_t *style = &KNOT_DUMP_STYLE_DEFAULT; + if (knot_rrset_txt_dump_data(rrset, 0, data, sizeof(data), style) > 0) { + jsonw_str(w, key, data); + } +} + +static void json_print_section(jsonw_t *w, const char *name, + const knot_pktsection_t *section) +{ + if (section->count == 0 || + (section->count == 1 && knot_pkt_rr(section, 0)->type == KNOT_RRTYPE_OPT)) { + return; + } + + char str[16]; + + jsonw_list(w, name); + + bool first_opt = true; + for (int i = 0; i < section->count; i++) { + const knot_rrset_t *rr = knot_pkt_rr(section, i); + if (rr->type == KNOT_RRTYPE_OPT && first_opt) { + first_opt = false; + continue; + } + jsonw_object(w, NULL); + json_dname(w, "NAME", rr->owner); + jsonw_int(w, "TYPE", rr->type); + if (knot_rrtype_to_string(rr->type, str, sizeof(str)) > 0) { + jsonw_str(w, "TYPEname", str); + } + jsonw_int(w, "CLASS", rr->rclass); + if (rr->type != KNOT_RRTYPE_OPT && // OPT class meaning is different. + knot_rrclass_to_string(rr->rclass, str, sizeof(str)) > 0) { + jsonw_str(w, "CLASSname", str); + } + jsonw_int(w, "TTL", rr->ttl); + if (rr->type != KNOT_RRTYPE_OPT) { // OPT with HEX rdata. + json_rdata(w, rr); + } + jsonw_int(w, "RDLENGTH", rr->rrs.rdata->len); + if (rr->rrs.rdata->len > 0 ) { + jsonw_hex(w, "RDATAHEX", rr->rrs.rdata->data, rr->rrs.rdata->len); + } + jsonw_end(w); + } + + jsonw_end(w); +} + +static void json_print_edns_generic(jsonw_t *w, const knot_rrset_t *rr) +{ + jsonw_object(w, "EDNS"); + json_dname(w, "NAME", rr->owner); + jsonw_int(w, "CLASS", rr->rclass); + jsonw_int(w, "TTL", rr->ttl); + if (rr->rrs.count > 0) { + jsonw_int(w, "RDLENGTH", rr->rrs.rdata->len); + jsonw_hex(w, "RDATAHEX", rr->rrs.rdata->data, rr->rrs.rdata->len); + } + jsonw_end(w); +} + +static void json_edns_unknown(jsonw_t *w, uint8_t *optdata, uint16_t optype, uint16_t optlen) +{ + char name[9] = { 0 }; + (void)snprintf(name, sizeof(name), "OPT%hu", optype); + jsonw_hex(w, name, optdata, optlen); +} + +static bool all_zero(const uint8_t * const str, const size_t len) +{ + for (const uint8_t *p = str; p != str + len; p++) { + if (*p != 0) { + return false; + } + } + return true; +} + +static bool all_print(const uint8_t * const str, const size_t len) +{ + for (const uint8_t *p = str; p != str + len; p++) { + if (!is_print(*p)) { + return false; + } + } + return true; +} + +static void json_edns_ecs(jsonw_t *w, uint8_t *optdata, uint16_t optlen, + char *tmps, size_t tmps_size) +{ + knot_edns_client_subnet_t ecs = { 0 }; + struct sockaddr_storage addr = { 0 }; + + int ret = knot_edns_client_subnet_parse(&ecs, optdata, optlen); + if (ret == KNOT_EOK) { + ret = knot_edns_client_subnet_get_addr(&addr, &ecs); + } + if (ret == KNOT_EOK) { + ret = sockaddr_tostr(tmps, tmps_size, &addr); + assert(ret > 0); + + (void)snprintf(tmps + ret, tmps_size - ret, + "/%d/%d", ecs.source_len, ecs.scope_len); + + jsonw_str(w, "ECS", tmps); + } else { + jsonw_hex(w, "ECS", optdata, optlen); + } +} + +static void json_edns_opt(jsonw_t *w, uint8_t *optdata, uint16_t optype, uint16_t optlen) +{ + char tmps[KNOT_DNAME_TXT_MAXLEN] = { 0 }; + uint32_t tmpu = 0; + uint16_t tmphu = 0; + + switch (optype) { + case KNOT_EDNS_OPTION_NSID: + jsonw_object(w, "NSID"); + jsonw_hex(w, "HEX", optdata, optlen); + if (all_print(optdata, optlen)) { + jsonw_str_len(w, "TEXT", optdata, optlen, true); + } + jsonw_end(w); + break; + case KNOT_EDNS_OPTION_CLIENT_SUBNET: + json_edns_ecs(w, optdata, optlen, tmps, sizeof(tmps)); + break; + case KNOT_EDNS_OPTION_EXPIRE: + if (optlen == 0) { + jsonw_str(w, "EXPIRE", "NONE"); + } else if (optlen == sizeof(tmpu)) { + tmpu = knot_wire_read_u32(optdata); + (void)snprintf(tmps, sizeof(tmps), "%u", tmpu); + jsonw_str(w, "EXPIRE", tmps); + } else { + json_edns_unknown(w, optdata, optype, optlen); + } + break; + case KNOT_EDNS_OPTION_COOKIE: + jsonw_list(w, "COOKIE"); + tmphu = MIN(optlen, KNOT_EDNS_COOKIE_CLNT_SIZE); + jsonw_hex(w, NULL, optdata, tmphu); + if (optlen > tmphu) { + jsonw_hex(w, NULL, optdata + tmphu, optlen - tmphu); + } + jsonw_end(w); + break; + case KNOT_EDNS_OPTION_TCP_KEEPALIVE: + if (optlen == sizeof(tmphu)) { + tmphu = knot_wire_read_u16(optdata); + jsonw_int(w, "KEEPALIVE", tmphu); + } else { + json_edns_unknown(w, optdata, optype, optlen); + } + break; + case KNOT_EDNS_OPTION_PADDING: + jsonw_object(w, "PADDING"); + jsonw_int(w, "LENGTH", optlen); + if (!all_zero(optdata, optlen)) { + jsonw_hex(w, "HEX", optdata, optlen); + } + jsonw_end(w); + break; + case KNOT_EDNS_OPTION_CHAIN: + if (knot_dname_wire_check(optdata, optdata + optlen, NULL) > 0 && + knot_dname_to_str(tmps, optdata, sizeof(tmps)) != NULL) { + jsonw_str(w, "CHAIN", tmps); + } else { + json_edns_unknown(w, optdata, optype, optlen); + } + break; + case KNOT_EDNS_OPTION_EDE: + if (optlen < sizeof(uint16_t)) { + json_edns_unknown(w, optdata, optype, optlen); + } else { + tmphu = knot_wire_read_u16(optdata); + jsonw_object(w, "EDE"); + jsonw_int(w, "CODE", tmphu); + const knot_lookup_t *item = knot_lookup_by_id(knot_edns_ede_names, tmphu); + if (item != NULL) { + jsonw_str(w, "Purpose", item->name); + } + if (optlen > 2) { + jsonw_str_len(w, "TEXT", optdata + 2, optlen - 2, true); + } + jsonw_end(w); + } + break; + default: + json_edns_unknown(w, optdata, optype, optlen); + break; + } +} + +static void json_print_edns(jsonw_t *w, const knot_pkt_t *pkt) +{ + assert(pkt != NULL && pkt->opt_rr != NULL); + + if (pkt->opt_rr->owner[0] != '\0' || pkt->opt_rr->rrs.count != 1) { + json_print_edns_generic(w, pkt->opt_rr); + return; + } + + char tmp[11] = { 0 }; + + jsonw_object(w, "EDNS"); + uint16_t version = (pkt->opt_rr->ttl & 0x00ff0000) >> 16; + uint16_t flags = pkt->opt_rr->ttl & 0xffff, mask = (1 << 15); + jsonw_int(w, "Version", version); + jsonw_list(w, "FLAGS"); + for (int i = 0; i < 16; i++) { + if ((flags & mask)) { + if ((mask & KNOT_EDNS_DO_MASK)) { + jsonw_str(w, NULL, "DO"); + } else { + (void)snprintf(tmp, sizeof(tmp), "BIT%d", i); + jsonw_str(w, NULL, tmp); + } + } + mask >>= 1; + } + jsonw_end(w); + + const knot_lookup_t *item = knot_lookup_by_id(knot_rcode_names, knot_pkt_ext_rcode(pkt)); + (void)snprintf(tmp, sizeof(tmp), "RCODE%hu", knot_pkt_ext_rcode(pkt)); + jsonw_str(w, "RCODE", item == NULL ? tmp : item->name); + jsonw_int(w, "UDPSIZE", knot_edns_get_payload(pkt->opt_rr)); + + assert(pkt->opt_rr->rrs.count == 1); + wire_ctx_t opts = wire_ctx_init(pkt->opt_rr->rrs.rdata->data, pkt->opt_rr->rrs.rdata->len); + while (wire_ctx_available(&opts) > 0 && opts.error == KNOT_EOK) { + uint16_t optype = wire_ctx_read_u16(&opts); + uint16_t optlen = wire_ctx_read_u16(&opts); + if (wire_ctx_can_read(&opts, optlen) == KNOT_EOK) { + json_edns_opt(w, opts.position, optype, optlen); + wire_ctx_skip(&opts, optlen); + } + } + jsonw_end(w); +} + +static void print_packet_json(jsonw_t *w, const knot_pkt_t *pkt, time_t time) +{ + if (pkt == NULL) { + return; + } + + char str[16]; + + struct tm tm; + char date[64]; + localtime_r(&time, &tm); + strftime(date, sizeof(date), "%Y-%m-%dT%H:%M:%S%z", &tm); + jsonw_str(w, "dateString", date); + jsonw_ulong(w, "dateSeconds", time); + + jsonw_int(w, "msgLength", pkt->size); + + if (pkt->parsed >= KNOT_WIRE_HEADER_SIZE) { + jsonw_int(w, "ID", knot_wire_get_id(pkt->wire)); + jsonw_int(w, "QR", (bool)knot_wire_get_qr(pkt->wire)); + jsonw_int(w, "Opcode", knot_wire_get_opcode(pkt->wire)); + jsonw_int(w, "AA", (bool)knot_wire_get_aa(pkt->wire)); + jsonw_int(w, "TC", (bool)knot_wire_get_tc(pkt->wire)); + jsonw_int(w, "RD", (bool)knot_wire_get_rd(pkt->wire)); + jsonw_int(w, "RA", (bool)knot_wire_get_ra(pkt->wire)); + jsonw_int(w, "AD", (bool)knot_wire_get_ad(pkt->wire)); + jsonw_int(w, "CD", (bool)knot_wire_get_cd(pkt->wire)); + jsonw_int(w, "RCODE", knot_wire_get_rcode(pkt->wire)); + jsonw_int(w, "QDCOUNT", knot_wire_get_qdcount(pkt->wire)); + jsonw_int(w, "ANCOUNT", knot_wire_get_ancount(pkt->wire)); + jsonw_int(w, "NSCOUNT", knot_wire_get_nscount(pkt->wire)); + jsonw_int(w, "ARCOUNT", knot_wire_get_arcount(pkt->wire)); + } + if (knot_wire_get_qdcount(pkt->wire) == 1) { + json_dname(w, "QNAME", knot_pkt_qname(pkt)); + jsonw_int(w, "QTYPE", knot_pkt_qtype(pkt)); + if (knot_rrtype_to_string(knot_pkt_qtype(pkt), str, sizeof(str)) > 0) { + jsonw_str(w, "QTYPEname", str); + } + jsonw_int(w, "QCLASS", knot_pkt_qclass(pkt)); + if (knot_rrclass_to_string(knot_pkt_qclass(pkt), str, sizeof(str)) > 0) { + jsonw_str(w, "QCLASSname", str); + } + } + if (pkt->rrset_count) { + json_print_section(w, "answerRRs", knot_pkt_section(pkt, KNOT_ANSWER)); + json_print_section(w, "authorityRRs", knot_pkt_section(pkt, KNOT_AUTHORITY)); + json_print_section(w, "additionalRRs", knot_pkt_section(pkt, KNOT_ADDITIONAL)); + } + if (knot_pkt_has_edns(pkt)) { + json_print_edns(w, pkt); + } + if (pkt->parsed < pkt->size) { + jsonw_hex(w, "messageOctetsHEX", pkt->wire, pkt->size); + } +} + +knot_pkt_t *create_empty_packet(const uint16_t max_size) +{ + // Create packet skeleton. + knot_pkt_t *packet = knot_pkt_new(NULL, max_size, NULL); + if (packet == NULL) { + DBG_NULL; + return NULL; + } + + // Set random sequence id. + knot_wire_set_id(packet->wire, dnssec_random_uint16_t()); + + return packet; +} + +jsonw_t *print_header_xfr_json(const knot_pkt_t *query, + const time_t exec_time, + const style_t *style) +{ + if (style == NULL) { + DBG_NULL; + return NULL; + } + + jsonw_t *w = jsonw_new(stdout, JSON_INDENT); + if (w == NULL) { + return NULL; + } + + if (style->show_query) { + jsonw_object(w, NULL); + jsonw_object(w, "queryMessage"); + print_packet_json(w, query, exec_time); + jsonw_end(w); + jsonw_list(w, "responseMessage"); + } else { + jsonw_list(w, NULL); + } + + return w; +} + +void print_data_xfr_json(jsonw_t *w, + const knot_pkt_t *reply, + const time_t exec_time) +{ + if (w == NULL) { + DBG_NULL; + return; + } + + jsonw_object(w, NULL); + print_packet_json(w, reply, exec_time); + jsonw_end(w); +} + +void print_footer_xfr_json(jsonw_t **w, + const style_t *style) +{ + if (w == NULL || style == NULL) { + DBG_NULL; + return; + } + + jsonw_end(*w); // list (responseMessage) + if (style->show_query) { + jsonw_end(*w); // object + } + + jsonw_free(w); + *w = NULL; +} + +void print_header_xfr(const knot_pkt_t *packet, const style_t *style) +{ + if (style == NULL) { + DBG_NULL; + return; + } + + char xfr[16] = "AXFR"; + + switch (knot_pkt_qtype(packet)) { + case KNOT_RRTYPE_AXFR: + break; + case KNOT_RRTYPE_IXFR: + xfr[0] = 'I'; + break; + default: + return; + } + + if (style->show_header) { + char *owner = knot_dname_to_str_alloc(knot_pkt_qname(packet)); + if (style->style.ascii_to_idn != NULL) { + style->style.ascii_to_idn(&owner); + } + if (owner != NULL) { + printf(";; %s for %s\n", xfr, owner); + free(owner); + } + } +} + +void print_data_xfr(const knot_pkt_t *packet, + const style_t *style) +{ + if (packet == NULL || style == NULL) { + DBG_NULL; + return; + } + + const knot_pktsection_t *answers = knot_pkt_section(packet, KNOT_ANSWER); + + switch (style->format) { + case FORMAT_DIG: + print_section_dig(knot_pkt_rr(answers, 0), answers->count, style); + break; + case FORMAT_HOST: + print_section_host(knot_pkt_rr(answers, 0), answers->count, style); + break; + case FORMAT_FULL: + print_section_full(knot_pkt_rr(answers, 0), answers->count, style, true); + + // Print TSIG record. + if (style->show_tsig && knot_pkt_has_tsig(packet)) { + print_section_full(packet->tsig_rr, 1, style, false); + } + break; + default: + break; + } +} + +void print_footer_xfr(const size_t total_len, + const size_t msg_count, + const size_t rr_count, + const net_t *net, + const float elapsed, + const time_t exec_time, + const style_t *style) +{ + if (style == NULL) { + DBG_NULL; + return; + } + + if (style->show_footer) { + print_footer(total_len, msg_count, rr_count, net, elapsed, + exec_time, true); + } +} + +void print_packets_json(const knot_pkt_t *query, + const knot_pkt_t *reply, + const net_t *net, + const time_t exec_time, + const style_t *style) +{ + if (style == NULL) { + DBG_NULL; + return; + } + + jsonw_t *w = jsonw_new(stdout, JSON_INDENT); + if (w == NULL) { + return; + } + jsonw_object(w, NULL); + + if (style->show_query) { + jsonw_object(w, "queryMessage"); + print_packet_json(w, query, exec_time); + jsonw_end(w); + jsonw_object(w, "responseMessage"); + } + + print_packet_json(w, reply, exec_time); + + if (style->show_query) { + jsonw_end(w); + } + + jsonw_end(w); + jsonw_free(&w); +} + +void print_packet(const knot_pkt_t *packet, + const net_t *net, + const size_t size, + const float elapsed, + const time_t exec_time, + const bool incoming, + const style_t *style) +{ + if (packet == NULL || style == NULL) { + DBG_NULL; + return; + } + + const knot_pktsection_t *answers = knot_pkt_section(packet, + KNOT_ANSWER); + const knot_pktsection_t *authority = knot_pkt_section(packet, + KNOT_AUTHORITY); + const knot_pktsection_t *additional = knot_pkt_section(packet, + KNOT_ADDITIONAL); + + uint16_t qdcount = packet->parsed >= KNOT_WIRE_OFFSET_ANCOUNT ? + knot_wire_get_qdcount(packet->wire) : 0; + uint16_t ancount = packet->sections[KNOT_ANSWER].count; + uint16_t nscount = packet->sections[KNOT_AUTHORITY].count; + uint16_t arcount = packet->sections[KNOT_ADDITIONAL].count; + + // Disable additionals printing if there are no other records. + // OPT record may be placed anywhere within additionals! + if (knot_pkt_has_edns(packet) && arcount == 1) { + arcount = 0; + } + + // Print packet information header. + if (style->show_header) { + if (net != NULL) { +#ifdef ENABLE_QUIC + if (net->quic.params.enable) { + print_quic(&net->quic); + } else +#endif + { + print_tls(&net->tls); +#ifdef LIBNGHTTP2 + print_https(&net->https); +#endif + } + } + print_header(packet, style); + } + + // Print EDNS section. + if (style->show_edns && knot_pkt_has_edns(packet)) { + printf("%s", style->show_section ? "\n;; EDNS PSEUDOSECTION:\n" : ";;"); + print_section_opt(packet, style); + } + + // Print DNS sections. + switch (style->format) { + case FORMAT_DIG: + if (ancount > 0) { + print_section_dig(knot_pkt_rr(answers, 0), ancount, style); + } + break; + case FORMAT_HOST: + if (ancount > 0) { + print_section_host(knot_pkt_rr(answers, 0), ancount, style); + } else { + print_error_host(packet, style); + } + break; + case FORMAT_NSUPDATE: + if (style->show_question && qdcount > 0) { + printf("%s", style->show_section ? "\n;; ZONE SECTION:\n;; " : ";;"); + print_section_question(knot_pkt_qname(packet), + knot_pkt_qclass(packet), + knot_pkt_qtype(packet), + style); + } + + if (style->show_answer && ancount > 0) { + printf("%s", style->show_section ? "\n;; PREREQUISITE SECTION:\n" : ""); + print_section_full(knot_pkt_rr(answers, 0), ancount, style, true); + } + + if (style->show_authority && nscount > 0) { + printf("%s", style->show_section ? "\n;; UPDATE SECTION:\n" : ""); + print_section_full(knot_pkt_rr(authority, 0), nscount, style, true); + } + + if (style->show_additional && arcount > 0) { + printf("%s", style->show_section ? "\n;; ADDITIONAL DATA:\n" : ""); + print_section_full(knot_pkt_rr(additional, 0), arcount, style, true); + } + break; + case FORMAT_FULL: + if (style->show_question && qdcount > 0) { + printf("%s", style->show_section ? "\n;; QUESTION SECTION:\n;; " : ";;"); + print_section_question(knot_pkt_wire_qname(packet), + knot_pkt_qclass(packet), + knot_pkt_qtype(packet), + style); + } + + if (style->show_answer && ancount > 0) { + printf("%s", style->show_section ? "\n;; ANSWER SECTION:\n" : ""); + print_section_full(knot_pkt_rr(answers, 0), ancount, style, true); + } + + if (style->show_authority && nscount > 0) { + printf("%s", style->show_section ? "\n;; AUTHORITY SECTION:\n" : ""); + print_section_full(knot_pkt_rr(authority, 0), nscount, style, true); + } + + if (style->show_additional && arcount > 0) { + printf("%s", style->show_section ? "\n;; ADDITIONAL SECTION:\n" : ""); + print_section_full(knot_pkt_rr(additional, 0), arcount, style, true); + } + break; + default: + break; + } + + // Print TSIG section. + if (style->show_tsig && knot_pkt_has_tsig(packet)) { + printf("%s", style->show_section ? "\n;; TSIG PSEUDOSECTION:\n" : ""); + print_section_full(packet->tsig_rr, 1, style, false); + } + + // Print packet statistics. + if (style->show_footer) { + printf("\n"); + print_footer(size, 0, 0, net, elapsed, exec_time, incoming); + } +} diff --git a/src/utils/common/exec.h b/src/utils/common/exec.h new file mode 100644 index 0000000..359926c --- /dev/null +++ b/src/utils/common/exec.h @@ -0,0 +1,137 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <time.h> + +#include "utils/common/netio.h" +#include "utils/common/params.h" +#include "libknot/libknot.h" +#include "contrib/json.h" + +/*! + * \brief Allocates empty packet and sets packet size and random id. + * + * \param max_size Maximal packet size. + * + * \retval packet if success. + * \retval NULL if error. + */ +knot_pkt_t *create_empty_packet(const uint16_t max_size); + +/*! + * \brief Prints information header for transfer. + * + * \param packet Parsed packet. + * \param style Style of the output. + */ +void print_header_xfr(const knot_pkt_t *packet, const style_t *style); + +/*! + * \brief Prints answer section for 1 transfer message. + * + * \param packet Response packet. + * \param style Style of the output. + */ +void print_data_xfr(const knot_pkt_t *packet, const style_t *style); + +/*! + * \brief Prints trailing statistics for transfer. + * + * \param total_len Total reply size (all messages). + * \param msg_count Number of messages. + * \param rr_count Total number of answer records. + * \param net Connection information. + * \param elapsed Total elapsed time. + * \param exec_time Time of the packet creation. + * \param style Style of the output. + */ +void print_footer_xfr(const size_t total_len, + const size_t msg_count, + const size_t rr_count, + const net_t *net, + const float elapsed, + const time_t exec_time, + const style_t *style); + +/*! + * \brief Prints initial JSON part of XFR output. + * + * \param query Query packet. + * \param exec_time Time of the packet creation. + * \param style Style of the output. + * + * \retval JSON witter if success. + * \retval NULL if error. + */ +jsonw_t *print_header_xfr_json(const knot_pkt_t *query, + const time_t exec_time, + const style_t *style); + +/*! + * \brief Prints one XFR reply packet in JSON. + * + * \param w JSON writter. + * \param reply Reply packet (possibly one of many). + * \param exec_time Time of the packet creation. + */ +void print_data_xfr_json(jsonw_t *w, + const knot_pkt_t *reply, + const time_t exec_time); + +/*! + * \brief Prints trailing JSON part of XFR output. + * + * \param w JSON writter. + * \param style Style of the output. + */ +void print_footer_xfr_json(jsonw_t **w, + const style_t *style); + +/*! + * \brief Prints one or query/reply pair of DNS packets in JSON format. + * + * \param query Query DNS packet. + * \param reply Reply DNS packet. + * \param net Connection information. + * \param exec_time Time of the packet creation. + * \param style Style of the output. + */ +void print_packets_json(const knot_pkt_t *query, + const knot_pkt_t *reply, + const net_t *net, + const time_t exec_time, + const style_t *style); + +/*! + * \brief Prints one DNS packet. + * + * \param packet DNS packet. + * \param net Connection information. + * \param size Original packet wire size. + * \param elapsed Total elapsed time. + * \param exec_time Time of the packet creation. + * \param incoming Indicates if the packet is input. + * \param style Style of the output. + */ +void print_packet(const knot_pkt_t *packet, + const net_t *net, + const size_t size, + const float elapsed, + const time_t exec_time, + const bool incoming, + const style_t *style); diff --git a/src/utils/common/hex.c b/src/utils/common/hex.c new file mode 100644 index 0000000..9683446 --- /dev/null +++ b/src/utils/common/hex.c @@ -0,0 +1,82 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include "libknot/libknot.h" +#include "contrib/ctype.h" +#include "contrib/tolower.h" + +/*! + * \brief Convert HEX char to byte. + * \note Expects valid lowercase letters. + */ +static uint8_t hex_to_num(int c) +{ + if (c >= '0' && c <= '9') { + return c - '0'; + } else { + return c - 'a' + 10; + } +} + +/*! + * \brief Convert string encoded in hex to bytes. + */ +int hex_decode(const char *input, uint8_t **output, size_t *output_size) +{ + if (!input || input[0] == '\0' || !output || !output_size) { + return KNOT_EINVAL; + } + + // input validation (length and content) + + size_t input_size = strlen(input); + if (input_size % 2 != 0) { + return KNOT_EMALF; + } + + for (size_t i = 0; i < input_size; i++) { + if (!is_xdigit(input[i])) { + return KNOT_EMALF; + } + } + + // output allocation + + size_t result_size = input_size / 2; + assert(result_size > 0); + uint8_t *result = malloc(result_size); + if (!result) { + return KNOT_ENOMEM; + } + + // conversion + + for (size_t i = 0; i < result_size; i++) { + int high_nib = knot_tolower(input[2 * i]); + int low_nib = knot_tolower(input[2 * i + 1]); + + result[i] = hex_to_num(high_nib) << 4 | hex_to_num(low_nib); + } + + *output = result; + *output_size = result_size; + + return KNOT_EOK; +} diff --git a/src/utils/common/hex.h b/src/utils/common/hex.h new file mode 100644 index 0000000..efe81be --- /dev/null +++ b/src/utils/common/hex.h @@ -0,0 +1,31 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> +#include <stdlib.h> + +/*! + * \brief Convert string encoded in hex to bytes. + * + * \param input Hex encoded input string. + * \param output Decoded bytes. + * \param output_size Size of the output. + * + * \return Error code, KNOT_EOK if successful. + */ +int hex_decode(const char *input, uint8_t **output, size_t *output_size); diff --git a/src/utils/common/https.c b/src/utils/common/https.c new file mode 100644 index 0000000..e673b28 --- /dev/null +++ b/src/utils/common/https.c @@ -0,0 +1,528 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <arpa/inet.h> +#include <poll.h> +#include <stdlib.h> +#include <string.h> + +#include "contrib/base64url.h" +#include "contrib/macros.h" +#include "contrib/musl/inet_ntop.h" +#include "contrib/openbsd/strlcat.h" +#include "contrib/openbsd/strlcpy.h" +#include "contrib/url-parser/url_parser.h" +#include "libknot/errcode.h" +#include "utils/common/https.h" +#include "utils/common/msg.h" + +#define is_read(ctx) (ctx->stream == -1) + +int https_params_copy(https_params_t *dst, const https_params_t *src) +{ + if (dst == NULL || src == NULL) { + return KNOT_EINVAL; + } + + dst->enable = src->enable; + dst->method = src->method; + if (src->path != NULL) { + dst->path = strdup(src->path); + if (dst->path == NULL) { + return KNOT_ENOMEM; + } + } + + return KNOT_EOK; +} + +void https_params_clean(https_params_t *params) +{ + if (params == NULL) { + return; + } + + params->enable = false; + params->method = GET; + free(params->path); + params->path = NULL; +} + +#ifdef LIBNGHTTP2 + +#define HTTP_STATUS_SUCCESS 200 +#define HTTPS_MAX_STREAMS 16 +#define HTTPS_AUTHORITY_LEN (INET6_ADDRSTRLEN + 2) + +#define MAKE_NV(K, KS, V, VS) \ + { (uint8_t *)K, (uint8_t *)V, KS, VS, NGHTTP2_NV_FLAG_NONE } + +#define MAKE_STATIC_NV(K, V) \ + MAKE_NV(K, sizeof(K) - 1, V, sizeof(V) - 1) + +static const char default_path[] = "/dns-query"; +static const char default_query[] = "?dns="; + +static const nghttp2_settings_entry settings[] = { + { NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, HTTPS_MAX_STREAMS } +}; + +const gnutls_datum_t doh_alpn = { + .data = (unsigned char *)"h2", + .size = 2 +}; + +static bool https_status_is_redirect(unsigned long status) +{ + switch (status) { + case 301UL: + case 302UL: + case 307UL: + case 308UL: + return true; + } + return false; +} + +static ssize_t https_send_callback(nghttp2_session *session, const uint8_t *data, + size_t length, int flags, void *user_data) +{ + assert(user_data); + + gnutls_session_t tls_session = ((https_ctx_t *)user_data)->tls->session; + ssize_t len = 0; + + gnutls_record_cork(tls_session); + if ((len = gnutls_record_send(tls_session, data, length)) <= 0) { + WARN("TLS, failed to send"); + return KNOT_NET_ESEND; + } + return len; +} + +static int https_on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, + void *user_data) +{ + assert(user_data); + + gnutls_session_t tls_session = ((https_ctx_t *)user_data)->tls->session; + while (gnutls_record_check_corked(tls_session) > 0) { + int ret = gnutls_record_uncork(tls_session, 0); + if (ret < 0 && gnutls_error_is_fatal(ret) != 0) { + WARN("TLS, failed to send (%s)", gnutls_strerror(ret)); + return KNOT_NET_ESEND; + } + } + return KNOT_EOK; +} + +static ssize_t https_recv_callback(nghttp2_session *session, uint8_t *data, size_t length, + int flags, void *user_data) +{ + assert(user_data); + + https_ctx_t *ctx = (https_ctx_t *)user_data; + struct pollfd pfd = { + .fd = ctx->tls->sockfd, + .events = POLLIN, + .revents = 0, + }; + + ssize_t ret = 0; + while ((ret = gnutls_record_recv(ctx->tls->session, data, length)) <= 0) { + if (is_read(ctx)) { //Unblock `nghttp2_session_recv(nghttp2_session)` + return NGHTTP2_ERR_WOULDBLOCK; + } + if (ret == 0) { + WARN("TLS, peer has closed the connection"); + return KNOT_NET_ERECV; + } else if (gnutls_error_is_fatal(ret)) { + WARN("TLS, failed to receive reply (%s)", + gnutls_strerror(ret)); + return KNOT_NET_ERECV; + } else if (poll(&pfd, 1, 1000 * ctx->tls->wait) != 1) { + WARN("TLS, peer took too long to respond"); + return KNOT_ETIMEOUT; + } + } + + return ret; +} + +static int https_on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags, int32_t stream_id, + const uint8_t *data, size_t len, void *user_data) +{ + assert(user_data); + + https_ctx_t *ctx = (https_ctx_t *)user_data; + if (ctx->stream == stream_id) { + int cpy_len = MIN(len, ctx->recv_buflen); + memcpy(ctx->recv_buf, data, cpy_len); + ctx->recv_buf += cpy_len; + ctx->recv_buflen -= cpy_len; + } + return KNOT_EOK; +} + +static int https_on_stream_close_callback(nghttp2_session *session, int32_t stream_id, uint32_t error_code, void *user_data) +{ + assert(user_data); + + https_ctx_t *ctx = (https_ctx_t *)user_data; + if (ctx->stream == stream_id) { + ctx->stream = -1; + } + return KNOT_EOK; +} + +static int https_on_header_callback(nghttp2_session *session, const nghttp2_frame *frame, + const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen, + uint8_t flags, void *user_data) +{ + assert(user_data); + https_ctx_t *ctx = (https_ctx_t *)user_data; + + if (!strncasecmp(":status", (const char *)name, namelen)) { + char *end; + long status; + status = strtoul((const char *)value, &end, 10); + if (value != (const uint8_t *)end) { + ctx->status = status; + } + } + else if (!strncasecmp("location", (const char *)name, namelen) && + https_status_is_redirect(ctx->status)) { + struct http_parser_url redirect_url; + http_parser_parse_url((const char *)value, valuelen, 0, &redirect_url); + + bool r_auth = redirect_url.field_set & (1 << UF_HOST); + bool r_path = redirect_url.field_set & (1 << UF_PATH); + char *old_auth = ctx->authority, *old_path = ctx->path; + + if (r_auth) { + ctx->authority = strndup((const char *)(value + redirect_url.field_data[UF_HOST].off), + redirect_url.field_data[UF_HOST].len); + } + if (r_path) { + ctx->path = strndup((const char *)(value + redirect_url.field_data[UF_PATH].off), + redirect_url.field_data[UF_PATH].len); + } + WARN("HTTP redirect (%s%s)->(%s%s)", old_auth, old_path, ctx->authority, ctx->path); + if (r_auth) { + free(old_auth); + } + if (r_path) { + free(old_path); + } + return https_send_dns_query(ctx, ctx->send_buf, ctx->send_buflen); + } + return KNOT_EOK; +} + +int https_ctx_init(https_ctx_t *ctx, tls_ctx_t *tls_ctx, const https_params_t *params) +{ + if (ctx == NULL || tls_ctx == NULL || params == NULL) { + return KNOT_EINVAL; + } + if (ctx->session != NULL) { // Already initialized before + return KNOT_EINVAL; + } + if (!params->enable) { + return KNOT_EINVAL; + } + + nghttp2_session_callbacks *callbacks; + nghttp2_session_callbacks_new(&callbacks); + nghttp2_session_callbacks_set_send_callback(callbacks, https_send_callback); + nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, https_on_frame_send_callback); + nghttp2_session_callbacks_set_recv_callback(callbacks, https_recv_callback); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, https_on_data_chunk_recv_callback); + nghttp2_session_callbacks_set_on_header_callback(callbacks, https_on_header_callback); + nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, https_on_stream_close_callback); + + int ret = nghttp2_session_client_new(&(ctx->session), callbacks, ctx); + if (ret != 0) { + return KNOT_EINVAL; + } + + nghttp2_session_callbacks_del(callbacks); + + if (pthread_mutex_init(&ctx->recv_mx, NULL) != 0) { + return KNOT_EINVAL; + } + + ctx->tls = tls_ctx; + ctx->params = *params; + ctx->authority = (tls_ctx->params->hostname) ? strdup(tls_ctx->params->hostname) : NULL; + ctx->path = strdup((ctx->params.path) ? ctx->params.path : (char *)default_path); + ctx->stream = -1; + + return KNOT_EOK; +} + +static int sockaddr_to_authority(char *buf, const size_t buf_len, const struct sockaddr_storage *ss) +{ + if (buf == NULL || ss == NULL) { + return KNOT_EINVAL; + } + + const char *out = NULL; + + /* Convert IPv6 network address string. */ + if (ss->ss_family == AF_INET6) { + if (buf_len < HTTPS_AUTHORITY_LEN) { + return KNOT_EINVAL; + } + + const struct sockaddr_in6 *s = (const struct sockaddr_in6 *)ss; + buf[0] = '['; + + out = knot_inet_ntop(ss->ss_family, &s->sin6_addr, buf + 1, buf_len - 1); + if (out == NULL) { + return KNOT_EINVAL; + } + + buf += strlen(buf); + buf[0] = ']'; + buf[1] = '\0'; + /* Convert IPv4 network address string. */ + } else if (ss->ss_family == AF_INET) { + if (buf_len < INET_ADDRSTRLEN) { + return KNOT_EINVAL; + } + + const struct sockaddr_in *s = (const struct sockaddr_in *)ss; + + out = knot_inet_ntop(ss->ss_family, &s->sin_addr, buf, buf_len); + if (out == NULL) { + return KNOT_EINVAL; + } + /* Unknown network address family. */ + } else { + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +int https_ctx_connect(https_ctx_t *ctx, int sockfd, bool fastopen, + struct sockaddr_storage *addr) +{ + if (ctx == NULL || addr == NULL) { + return KNOT_EINVAL; + } + + // Create TLS connection + int ret = tls_ctx_connect(ctx->tls, sockfd, fastopen, addr); + if (ret != KNOT_EOK) { + return ret; + } + + // Perform HTTP handshake + ret = nghttp2_submit_settings(ctx->session, NGHTTP2_FLAG_NONE, settings, + sizeof(settings) / sizeof(*settings)); + if (ret != 0) { + return KNOT_NET_ESOCKET; + } + ret = nghttp2_session_send(ctx->session); + if (ret != 0) { + return KNOT_NET_ESOCKET; + } + + // Save authority server + if (ctx->authority == NULL) { + ctx->authority = calloc(HTTPS_AUTHORITY_LEN, 1); + ret = sockaddr_to_authority(ctx->authority, HTTPS_AUTHORITY_LEN, addr); + if (ret != KNOT_EOK) { + free(ctx->authority); + ctx->authority = NULL; + return KNOT_EINVAL; + } + } + + return KNOT_EOK; +} + +static int https_send_dns_query_common(https_ctx_t *ctx, nghttp2_nv *hdrs, size_t hdrs_len, nghttp2_data_provider *data_provider) +{ + assert(hdrs != NULL && hdrs_len > 0); + + ctx->stream = nghttp2_submit_request(ctx->session, NULL, hdrs, hdrs_len, + data_provider, NULL); + if (ctx->stream < 0) { + return KNOT_NET_ESEND; + } + int ret = nghttp2_session_send(ctx->session); + if (ret != 0) { + return KNOT_NET_ESEND; + } + + return KNOT_EOK; +} + +static int https_send_dns_query_get(https_ctx_t *ctx) +{ + const size_t dns_query_len = strlen(ctx->path) + + sizeof(default_query) + + (ctx->send_buflen * 4) / 3 + 3; + char dns_query[dns_query_len]; + strlcpy(dns_query, ctx->path, dns_query_len); + strlcat(dns_query, default_query, dns_query_len); + + size_t tmp_strlen = strlen(dns_query); + int32_t ret = knot_base64url_encode(ctx->send_buf, ctx->send_buflen, + (uint8_t *)(dns_query + tmp_strlen), dns_query_len - tmp_strlen - 1); + if (ret < 0) { + return KNOT_EINVAL; + } + + nghttp2_nv hdrs[] = { + MAKE_STATIC_NV(":method", "GET"), + MAKE_STATIC_NV(":scheme", "https"), + MAKE_NV(":authority", 10, ctx->authority, strlen(ctx->authority)), + MAKE_NV(":path", 5, dns_query, tmp_strlen + ret), + MAKE_STATIC_NV("accept", "application/dns-message"), + }; + + return https_send_dns_query_common(ctx, hdrs, sizeof(hdrs) / sizeof(*hdrs), + NULL); +} + +static ssize_t https_send_data_callback(nghttp2_session *session, int32_t stream_id, + uint8_t *buf, size_t length, uint32_t *data_flags, + nghttp2_data_source *source, void *user_data) +{ + https_data_provider_t *buffer = source->ptr; + ssize_t sent = (length < buffer->buf_len) ? length : buffer->buf_len; + + memcpy(buf, buffer->buf, sent); + buffer->buf += sent; + buffer->buf_len -= sent; + if (!buffer->buf_len) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + } + + return sent; +} + +static int https_send_dns_query_post(https_ctx_t *ctx) +{ + // limit for x->inf: log10(2^(8*sizeof(x))-1)/sizeof(x) = 2,408239965 -> 3 + size_t capacity = sizeof(size_t) * 3 + 1; + // size of number in text form (base 10) + char content_length[capacity]; + int content_length_len = snprintf(content_length, capacity, "%zu", ctx->send_buflen); + assert(content_length_len > 0 && content_length_len < capacity); + + nghttp2_nv hdrs[] = { + MAKE_STATIC_NV(":method", "POST"), + MAKE_STATIC_NV(":scheme", "https"), + MAKE_NV(":authority", 10, ctx->authority, strlen(ctx->authority)), + MAKE_NV(":path", 5, ctx->path, strlen(ctx->path)), + MAKE_STATIC_NV("accept", "application/dns-message"), + MAKE_STATIC_NV("content-type", "application/dns-message"), + MAKE_NV("content-length", 14, content_length, content_length_len) + }; + + https_data_provider_t data = { + .buf = ctx->send_buf, + .buf_len = ctx->send_buflen + }; + + nghttp2_data_provider data_provider = { + .source.ptr = &data, + .read_callback = https_send_data_callback + }; + + return https_send_dns_query_common(ctx, hdrs, sizeof(hdrs) / sizeof(*hdrs), + &data_provider); +} + +int https_send_dns_query(https_ctx_t *ctx, const uint8_t *buf, const size_t buf_len) +{ + if (ctx == NULL || buf == NULL || buf_len == 0) { + return KNOT_EINVAL; + } + + ctx->send_buf = buf; + ctx->send_buflen = buf_len; + + assert(ctx->params.method == POST || ctx->params.method == GET); + + if (ctx->params.method == POST) { + return https_send_dns_query_post(ctx); + } else { + return https_send_dns_query_get(ctx); + } +} + +int https_recv_dns_response(https_ctx_t *ctx, uint8_t *buf, const size_t buf_len) +{ + if (ctx == NULL || buf == NULL || buf_len == 0) { + return KNOT_EINVAL; + } + + pthread_mutex_lock(&ctx->recv_mx); + ctx->recv_buf = buf; + ctx->recv_buflen = buf_len; + + int ret = nghttp2_session_recv(ctx->session); + if (ret != 0) { + pthread_mutex_unlock(&ctx->recv_mx); + return KNOT_NET_ERECV; + } + ctx->recv_buf = NULL; + + pthread_mutex_unlock(&ctx->recv_mx); + + if (ctx->status != HTTP_STATUS_SUCCESS) { + print_https(ctx); + return KNOT_NET_ERECV; + } + + assert(buf_len >= ctx->recv_buflen); + return buf_len - ctx->recv_buflen; +} + +void https_ctx_deinit(https_ctx_t *ctx) +{ + if (ctx == NULL) { + return; + } + + nghttp2_session_del(ctx->session); + ctx->session = NULL; + pthread_mutex_destroy(&ctx->recv_mx); + free(ctx->path); + ctx->path = NULL; + free(ctx->authority); + ctx->authority = NULL; +} + +void print_https(const https_ctx_t *ctx) +{ + if (!ctx || !ctx->params.enable || !ctx->authority || !ctx->path) { + return; + } + + printf(";; HTTP session (HTTP/2-%s)-(%s%s)-(status: %lu)\n", + ctx->params.method == POST ? "POST" : "GET", ctx->authority, + ctx->path, ctx->status); +} + +#endif //LIBNGHTTP2 diff --git a/src/utils/common/https.h b/src/utils/common/https.h new file mode 100644 index 0000000..aed1cd5 --- /dev/null +++ b/src/utils/common/https.h @@ -0,0 +1,150 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdbool.h> + +/*! \brief HTTP method to transfer query. */ +typedef enum { + POST, + GET +} https_method_t; + +/*! \brief HTTPS parameters. */ +typedef struct { + /*! Use HTTPS indicator. */ + bool enable; + /*! HTTP method to transfer query. */ + https_method_t method; + /*! Path */ + char *path; +} https_params_t; + +int https_params_copy(https_params_t *dst, const https_params_t *src); +void https_params_clean(https_params_t *params); + +#ifdef LIBNGHTTP2 + +#include <netinet/in.h> +#include <pthread.h> +#include <sys/socket.h> +#include <nghttp2/nghttp2.h> + +#include "utils/common/tls.h" + +extern const gnutls_datum_t doh_alpn; + +/*! \brief Structure that stores data source for DATA frames. */ +typedef struct { + const uint8_t *buf; + size_t buf_len; +} https_data_provider_t; + +/*! \brief HTTPS context. */ +typedef struct { + // Parameters + https_params_t params; + + // Contexts + nghttp2_session *session; + tls_ctx_t *tls; + char *authority; + char *path; + + // Send destination + const uint8_t *send_buf; + size_t send_buflen; + + // Recv destination + uint8_t *recv_buf; + size_t recv_buflen; + unsigned long status; + + // Recv locks + pthread_mutex_t recv_mx; + int32_t stream; +} https_ctx_t; + +/*! + * \brief Initialize HTTPS context. + * + * \param ctx HTTPS context. + * \param tls_ctx TLS context. + * \param params Parameter table. + * + * \retval KNOT_EOK When initialized. + * \retval KNOT_EINVAL When parameters are invalid. + */ +int https_ctx_init(https_ctx_t *ctx, tls_ctx_t *tls_ctx, const https_params_t *params); + +/*! + * \brief Create TLS connection and perform HTTPS handshake. + * + * \param ctx HTTPS context. + * \param sockfd Socket descriptor. + * \param fastopen Use TCP Fast Open indication. + * \param addr Socket address storage with address to server side. + * + * \retval KNOT_EOK When successfully connected. + * \retval KNOT_EINVAL When parameters are invalid. + * \retval KNOT_NET_ESOCKET When socket is no accessible. + * \retval KNOT_NET_ETIMEOUT When server respond takes too long. + * \retval KNOT_NET_ECONNECT When unnable to connect to the server. + */ +int https_ctx_connect(https_ctx_t *ctx, int sockfd, bool fastopen, + struct sockaddr_storage *addr); + +/*! + * \brief Send buffer as DNS message over HTTPS. + * + * \param ctx HTTPS context. + * \param buf Buffer with DNS message in wire format. + * \param buf_len Length of buffer. + * + * \retval KNOT_EOK When successfully sent. + * \retval KNOT_EINVAL When parameters are invalid. + * \retval KNOT_NET_ESEND When error occurs while sending a data. + */ +int https_send_dns_query(https_ctx_t *ctx, const uint8_t *buf, const size_t buf_len); + +/*! + * \brief Receive DATA frame as HTTPS packet, and store it into buffer. + * + * \param ctx HTTPS context. + * \param buf Buffer where will be DNS response stored. + * \param buf_len Length of buffer. + * + * \retval >=0 Number of bytes received in DATA frame. + * \retval KNOT_NET_ERECV When error while receive. + */ +int https_recv_dns_response(https_ctx_t *ctx, uint8_t *buf, const size_t buf_len); + +/*! + * \brief Deinitialize HTTPS context. + * + * \param ctx HTTPS context. + */ +void https_ctx_deinit(https_ctx_t *ctx); + +/*! + * \brief Prints information about HTTPS context. + * + * \param ctx HTTPS context. + */ +void print_https(const https_ctx_t *ctx); + +#endif //LIBNGHTTP2 diff --git a/src/utils/common/lookup.c b/src/utils/common/lookup.c new file mode 100644 index 0000000..e7f6084 --- /dev/null +++ b/src/utils/common/lookup.c @@ -0,0 +1,295 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <string.h> + +#include "utils/common/lookup.h" +#include "contrib/mempattern.h" +#include "contrib/ucw/mempool.h" +#include "libknot/error.h" + +int lookup_init(lookup_t *lookup) +{ + if (lookup == NULL) { + return KNOT_EINVAL; + } + memset(lookup, 0, sizeof(*lookup)); + + mm_ctx_mempool(&lookup->mm, MM_DEFAULT_BLKSIZE); + lookup->trie = trie_create(&lookup->mm); + if (lookup->trie == NULL) { + mp_delete(lookup->mm.ctx); + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +static void reset_output(lookup_t *lookup) +{ + if (lookup == NULL) { + return; + } + + mm_free(&lookup->mm, lookup->found.key); + lookup->found.key = NULL; + lookup->found.data = NULL; + + lookup->iter.count = 0; + + mm_free(&lookup->mm, lookup->iter.first_key); + lookup->iter.first_key = NULL; + + trie_it_free(lookup->iter.it); + lookup->iter.it = NULL; +} + +void lookup_deinit(lookup_t *lookup) +{ + if (lookup == NULL) { + return; + } + + reset_output(lookup); + + trie_free(lookup->trie); + mp_delete(lookup->mm.ctx); +} + +int lookup_insert(lookup_t *lookup, const char *str, void *data) +{ + if (lookup == NULL || str == NULL) { + return KNOT_EINVAL; + } + + size_t str_len = strlen(str); + if (str_len == 0) { + return KNOT_EINVAL; + } + + trie_val_t *val = trie_get_ins(lookup->trie, (const trie_key_t *)str, str_len); + if (val == NULL) { + return KNOT_ENOMEM; + } + *val = data; + + return KNOT_EOK; +} + +int lookup_remove(lookup_t *lookup, const char *str) +{ + if (lookup == NULL || str == NULL) { + return KNOT_EINVAL; + } + + size_t str_len = strlen(str); + if (str_len > 0) { + (void)trie_del(lookup->trie, (const trie_key_t *)str, str_len, NULL); + } + + return KNOT_EOK; +} + +static int set_key(lookup_t *lookup, char **dst, const char *key, size_t key_len) +{ + if (*dst != NULL) { + mm_free(&lookup->mm, *dst); + } + *dst = mm_alloc(&lookup->mm, key_len + 1); + if (*dst == NULL) { + return KNOT_ENOMEM; + } + memcpy(*dst, key, key_len); + (*dst)[key_len] = '\0'; + + return KNOT_EOK; +} + +int lookup_search(lookup_t *lookup, const char *str, size_t str_len) +{ + if (lookup == NULL) { + return KNOT_EINVAL; + } + + // Change NULL string to the empty one. + if (str == NULL) { + str = ""; + } + + reset_output(lookup); + + size_t new_len = 0; + trie_it_t *it = trie_it_begin(lookup->trie); + for (; !trie_it_finished(it); trie_it_next(it)) { + size_t len; + const char *key = (const char *)trie_it_key(it, &len); + + // Compare with a shorter key. + if (len < str_len) { + int ret = memcmp(str, key, len); + if (ret >= 0) { + continue; + } else { + break; + } + } + + // Compare with an equal length or longer key. + int ret = memcmp(str, key, str_len); + if (ret == 0) { + lookup->iter.count++; + + // First candidate. + if (lookup->iter.count == 1) { + ret = set_key(lookup, &lookup->found.key, key, len); + if (ret != KNOT_EOK) { + break; + } + lookup->found.data = *trie_it_val(it); + new_len = len; + // Another candidate. + } else if (new_len > str_len) { + if (new_len > len) { + new_len = len; + } + while (memcmp(lookup->found.key, key, new_len) != 0) { + new_len--; + } + } + // Stop if greater than the key, and also than all the following keys. + } else if (ret < 0) { + break; + } + } + trie_it_free(it); + + switch (lookup->iter.count) { + case 0: + return KNOT_ENOENT; + case 1: + return KNOT_EOK; + default: + // Store full name of the first candidate. + if (set_key(lookup, &lookup->iter.first_key, lookup->found.key, + strlen(lookup->found.key)) != KNOT_EOK) { + return KNOT_ENOMEM; + } + lookup->found.key[new_len] = '\0'; + lookup->found.data = NULL; + + return KNOT_EFEWDATA; + } +} + +void lookup_list(lookup_t *lookup) +{ + if (lookup == NULL || lookup->iter.first_key == NULL) { + return; + } + + if (lookup->iter.it != NULL) { + if (trie_it_finished(lookup->iter.it)) { + trie_it_free(lookup->iter.it); + lookup->iter.it = NULL; + return; + } + + trie_it_next(lookup->iter.it); + + size_t len; + const char *key = (const char *)trie_it_key(lookup->iter.it, &len); + + int ret = set_key(lookup, &lookup->found.key, key, len); + if (ret == KNOT_EOK) { + lookup->found.data = *trie_it_val(lookup->iter.it); + } + return; + } + + lookup->iter.it = trie_it_begin(lookup->trie); + while (!trie_it_finished(lookup->iter.it)) { + size_t len; + const char *key = (const char *)trie_it_key(lookup->iter.it, &len); + + if (strncmp(key, lookup->iter.first_key, len) == 0) { + int ret = set_key(lookup, &lookup->found.key, key, len); + if (ret == KNOT_EOK) { + lookup->found.data = *trie_it_val(lookup->iter.it); + } + break; + } + trie_it_next(lookup->iter.it); + } +} + +static void print_options(lookup_t *lookup, EditLine *el) +{ + // Get terminal lines. + unsigned lines = 0; + if (el_get(el, EL_GETTC, "li", &lines) != 0 || lines < 3) { + return; + } + + for (size_t i = 1; i <= lookup->iter.count; i++) { + lookup_list(lookup); + printf("\n%s", lookup->found.key); + + if (i > 1 && i % (lines - 1) == 0 && i < lookup->iter.count) { + printf("\n Display next from %zu possibilities? (y or n)", + lookup->iter.count); + char next; + el_getc(el, &next); + if (next != 'y') { + break; + } + } + } + + printf("\n"); + fflush(stdout); +} + +int lookup_complete(lookup_t *lookup, const char *str, size_t str_len, + EditLine *el, bool add_space) +{ + if (lookup == NULL || el == NULL) { + return KNOT_EINVAL; + } + + // Try to complete the command name. + int ret = lookup_search(lookup, str, str_len); + switch (ret) { + case KNOT_EOK: + el_deletestr(el, str_len); + el_insertstr(el, lookup->found.key); + if (add_space) { + el_insertstr(el, " "); + } + break; + case KNOT_EFEWDATA: + if (strlen(lookup->found.key) > str_len) { + el_deletestr(el, str_len); + el_insertstr(el, lookup->found.key); + } else { + print_options(lookup, el); + } + break; + default: + break; + } + + return ret; +} diff --git a/src/utils/common/lookup.h b/src/utils/common/lookup.h new file mode 100644 index 0000000..b6dc8ee --- /dev/null +++ b/src/utils/common/lookup.h @@ -0,0 +1,124 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <histedit.h> + +#include "libknot/mm_ctx.h" +#include "contrib/qp-trie/trie.h" + +/*! Lookup context. */ +typedef struct { + /*! Memory pool context. */ + knot_mm_t mm; + /*! Main trie storage. */ + trie_t *trie; + + /*! Current (iteration) data context. */ + struct { + /*! Stored key. */ + char *key; + /*! Corresponding key data. */ + void *data; + } found; + + /*! Iteration context. */ + struct { + /*! Total number of possibilities. */ + size_t count; + /*! The first possibility. */ + char *first_key; + /*! Hat-trie iterator. */ + trie_it_t *it; + } iter; +} lookup_t; + +/*! + * Initializes the lookup context. + * + * \param[in] lookup Lookup context. + * + * \return Error code, KNOT_EOK if successful. + */ +int lookup_init(lookup_t *lookup); + +/*! + * Deinitializes the lookup context. + * + * \param[in] lookup Lookup context. + */ +void lookup_deinit(lookup_t *lookup); + +/*! + * Inserts given key and data into the lookup. + * + * \param[in] lookup Lookup context. + * \param[in] str Textual key. + * \param[in] data Key textual data. + * + * \return Error code, KNOT_EOK if successful. + */ +int lookup_insert(lookup_t *lookup, const char *str, void *data); + +/*! + * Removes given key from the lookup. + * + * \param[in] lookup Lookup context. + * \param[in] str Textual key. + * + * \return Error code, KNOT_EOK if successful. + */ +int lookup_remove(lookup_t *lookup, const char *str); + +/*! + * Searches the lookup container for the given key. + * + * \note If one candidate, lookup.found contains the key/data, + * if more candidates, lookup.found contains the common key prefix and + * lookup.iter.first_key is the first candidate key. + * + * \param[in] lookup Lookup context. + * \param[in] str Textual key. + * \param[in] str_len Textual key length. + * + * \return Error code, KNOT_EOK if 1 candidate, KNOT_ENOENT if no candidate, + * and KNOT_EFEWDATA if more candidates are possible. + */ +int lookup_search(lookup_t *lookup, const char *str, size_t str_len); + +/*! + * Moves the lookup iterator to the next key candidate. + * + * \note lookup.found is updated. + * + * \param[in] lookup Lookup context. + */ +void lookup_list(lookup_t *lookup); + +/*! + * Completes the string based on the lookup content or prints all candidates. + * + * \param[in] lookup Lookup context. + * \param[in] str Textual key. + * \param[in] str_len Textual key length. + * \param[in] el Editline context. + * \param[in] add_space Add one space after completed string flag. + * + * \return Error code, same as lookup_search(). + */ +int lookup_complete(lookup_t *lookup, const char *str, size_t str_len, + EditLine *el, bool add_space); diff --git a/src/utils/common/msg.c b/src/utils/common/msg.c new file mode 100644 index 0000000..c125297 --- /dev/null +++ b/src/utils/common/msg.c @@ -0,0 +1,40 @@ +/* Copyright (C) 2011 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> + +#include "utils/common/msg.h" + +static volatile int MSG_DBG_STATE = 0; /* True if debugging is enabled. */ + +int msg_enable_debug(int val) +{ + return MSG_DBG_STATE = val; +} + +int msg_debug(const char *fmt, ...) +{ + int n = 0; + if (MSG_DBG_STATE) { + va_list ap; + va_start(ap, fmt); + n = vprintf(fmt, ap); + va_end(ap); + } + return n; +} diff --git a/src/utils/common/msg.h b/src/utils/common/msg.h new file mode 100644 index 0000000..d2ed57e --- /dev/null +++ b/src/utils/common/msg.h @@ -0,0 +1,42 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdio.h> + +#define ERROR_ ";; ERROR: " +#define INFO_ ";; INFO: " +#define WARNING_ ";; WARNING: " +#define DEBUG_ ";; DEBUG: " + +#define ERR(msg, ...) { fprintf(stderr, ERROR_ msg "\n", ##__VA_ARGS__); fflush(stderr); } +#define INFO(msg, ...) { fprintf(stdout, INFO_ msg "\n", ##__VA_ARGS__); fflush(stdout); } +#define WARN(msg, ...) { fprintf(stderr, WARNING_ msg "\n", ##__VA_ARGS__); fflush(stderr); } +#define DBG(msg, ...) { msg_debug(DEBUG_ msg "\n", ##__VA_ARGS__); fflush(stdout); } + +/*! \brief Enable/disable debugging. */ +int msg_enable_debug(int val); + +/*! \brief Print debug message. */ +int msg_debug(const char *fmt, ...); + +/*! \brief Debug message for null input. */ +#define DBG_NULL DBG("%s: null parameter", __func__) + +#define ERR2(msg, ...) { fprintf(stderr, "error: " msg "\n", ##__VA_ARGS__); fflush(stderr); } +#define WARN2(msg, ...) { fprintf(stderr, "warning: " msg "\n", ##__VA_ARGS__); fflush(stderr); } +#define INFO2(msg, ...) { fprintf(stdout, msg "\n", ##__VA_ARGS__); fflush(stdout); } diff --git a/src/utils/common/netio.c b/src/utils/common/netio.c new file mode 100644 index 0000000..eed14ee --- /dev/null +++ b/src/utils/common/netio.c @@ -0,0 +1,925 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <arpa/inet.h> +#include <fcntl.h> +#include <netdb.h> +#include <poll.h> +#include <stdlib.h> +#include <netinet/in.h> +#include <sys/types.h> // OpenBSD +#include <netinet/tcp.h> // TCP_FASTOPEN +#include <sys/socket.h> + +#ifdef HAVE_SYS_UIO_H +#include <sys/uio.h> +#endif + +#include "utils/common/netio.h" +#include "utils/common/msg.h" +#include "utils/common/tls.h" +#include "libknot/libknot.h" +#include "contrib/net.h" +#include "contrib/proxyv2/proxyv2.h" +#include "contrib/sockaddr.h" + +static knot_probe_proto_t get_protocol(const net_t *net) +{ +#ifdef ENABLE_QUIC + if (net->quic.params.enable) { + return KNOT_PROBE_PROTO_QUIC; + } else +#endif +#ifdef LIBNGHTTP2 + if (net->https.params.enable) { + return KNOT_PROBE_PROTO_HTTPS; + } else +#endif + if (net->tls.params != NULL && net->tls.params->enable) { + return KNOT_PROBE_PROTO_TLS; + } else if (net->socktype == PROTO_TCP) { + return KNOT_PROBE_PROTO_TCP; + } else { + assert(net->socktype == PROTO_UDP); + return KNOT_PROBE_PROTO_UDP; + } +} + +static const char *get_protocol_str(const knot_probe_proto_t proto) +{ + switch (proto) { + case KNOT_PROBE_PROTO_UDP: + return "UDP"; + case KNOT_PROBE_PROTO_QUIC: + return "QUIC"; + case KNOT_PROBE_PROTO_TCP: + return "TCP"; + case KNOT_PROBE_PROTO_TLS: + return "TLS"; + case KNOT_PROBE_PROTO_HTTPS: + return "HTTPS"; + default: + return "UNKNOWN"; + } +} + +srv_info_t *srv_info_create(const char *name, const char *service) +{ + if (name == NULL || service == NULL) { + DBG_NULL; + return NULL; + } + + // Create output structure. + srv_info_t *server = calloc(1, sizeof(srv_info_t)); + + // Check output. + if (server == NULL) { + return NULL; + } + + // Fill output. + server->name = strdup(name); + server->service = strdup(service); + + if (server->name == NULL || server->service == NULL) { + srv_info_free(server); + return NULL; + } + + // Return result. + return server; +} + +void srv_info_free(srv_info_t *server) +{ + if (server == NULL) { + DBG_NULL; + return; + } + + free(server->name); + free(server->service); + free(server); +} + +int get_iptype(const ip_t ip, const srv_info_t *server) +{ + bool unix_socket = (server->name[0] == '/'); + + switch (ip) { + case IP_4: + return AF_INET; + case IP_6: + return AF_INET6; + default: + return unix_socket ? AF_UNIX : AF_UNSPEC; + } +} + +int get_socktype(const protocol_t proto, const uint16_t type) +{ + switch (proto) { + case PROTO_TCP: + return SOCK_STREAM; + case PROTO_UDP: + return SOCK_DGRAM; + default: + if (type == KNOT_RRTYPE_AXFR || type == KNOT_RRTYPE_IXFR) { + return SOCK_STREAM; + } else { + return SOCK_DGRAM; + } + } +} + +const char *get_sockname(const int socktype) +{ + switch (socktype) { + case SOCK_STREAM: + return "TCP"; + case SOCK_DGRAM: + return "UDP"; + default: + return "UNKNOWN"; + } +} + +static int get_addr(const srv_info_t *server, + const int iptype, + const int socktype, + struct addrinfo **info) +{ + struct addrinfo hints; + + // Set connection hints. + memset(&hints, 0, sizeof(hints)); + hints.ai_family = iptype; + hints.ai_socktype = socktype; + + // Get connection parameters. + int ret = getaddrinfo(server->name, server->service, &hints, info); + switch (ret) { + case 0: + return 0; +#ifdef EAI_ADDRFAMILY /* EAI_ADDRFAMILY isn't implemented in FreeBSD/macOS anymore. */ + case EAI_ADDRFAMILY: + break; +#else /* FreeBSD, macOS, and likely others return EAI_NONAME instead. */ + case EAI_NONAME: + if (iptype != AF_UNSPEC) { + break; + } + /* FALLTHROUGH */ +#endif /* EAI_ADDRFAMILY */ + default: + ERR("%s for %s@%s", gai_strerror(ret), server->name, server->service); + } + return -1; +} + +void get_addr_str(const struct sockaddr_storage *ss, + const knot_probe_proto_t protocol, + char **dst) +{ + char addr_str[SOCKADDR_STRLEN] = { 0 }; + const char *proto_str = get_protocol_str(protocol); + + // Get network address string and port number. + sockaddr_tostr(addr_str, sizeof(addr_str), ss); + + // Calculate needed buffer size + size_t buflen = strlen(addr_str) + strlen(proto_str) + 3 /* () */; + + // Free previous string if any and write result + free(*dst); + *dst = malloc(buflen); + if (*dst != NULL) { + int ret = snprintf(*dst, buflen, "%s(%s)", addr_str, proto_str); + if (ret <= 0 || ret >= buflen) { + **dst = '\0'; + } + } +} + +int net_init(const srv_info_t *local, + const srv_info_t *remote, + const int iptype, + const int socktype, + const int wait, + const net_flags_t flags, + const struct sockaddr *proxy_src, + const struct sockaddr *proxy_dst, + net_t *net) +{ + if (remote == NULL || net == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + // Clean network structure. + memset(net, 0, sizeof(*net)); + net->sockfd = -1; + + if (iptype == AF_UNIX) { + struct addrinfo *info = calloc(1, sizeof(struct addrinfo)); + info->ai_addr = calloc(1, sizeof(struct sockaddr_storage)); + info->ai_addrlen = sizeof(struct sockaddr_un); + info->ai_socktype = socktype; + info->ai_family = iptype; + int ret = sockaddr_set_raw((struct sockaddr_storage *)info->ai_addr, + AF_UNIX, (const uint8_t *)remote->name, + strlen(remote->name)); + if (ret != KNOT_EOK) { + free(info->ai_addr); + free(info); + return ret; + } + net->remote_info = info; + } else { + // Get remote address list. + if (get_addr(remote, iptype, socktype, &net->remote_info) != 0) { + net_clean(net); + return KNOT_NET_EADDR; + } + } + + // Set current remote address. + net->srv = net->remote_info; + + // Get local address if specified. + if (local != NULL) { + if (get_addr(local, iptype, socktype, &net->local_info) != 0) { + net_clean(net); + return KNOT_NET_EADDR; + } + } + + // Store network parameters. + net->sockfd = -1; + net->iptype = iptype; + net->socktype = socktype; + net->wait = wait; + net->local = local; + net->remote = remote; + net->flags = flags; + net->proxy.src = proxy_src; + net->proxy.dst = proxy_dst; + + if ((bool)(proxy_src == NULL) != (bool)(proxy_dst == NULL) || + (proxy_src != NULL && proxy_src->sa_family != proxy_dst->sa_family)) { + net_clean(net); + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +int net_init_crypto(net_t *net, + const tls_params_t *tls_params, + const https_params_t *https_params, + const quic_params_t *quic_params) +{ + if (net == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + if (tls_params == NULL || !tls_params->enable) { + return KNOT_EOK; + } + + tls_ctx_deinit(&net->tls); +#ifdef LIBNGHTTP2 + // Prepare for HTTPS. + if (https_params != NULL && https_params->enable) { + int ret = tls_ctx_init(&net->tls, tls_params, + GNUTLS_NONBLOCK, net->wait); + if (ret != KNOT_EOK) { + net_clean(net); + return ret; + } + https_ctx_deinit(&net->https); + ret = https_ctx_init(&net->https, &net->tls, https_params); + if (ret != KNOT_EOK) { + net_clean(net); + return ret; + } + } else +#endif //LIBNGHTTP2 +#ifdef ENABLE_QUIC + if (quic_params != NULL && quic_params->enable) { + int ret = tls_ctx_init(&net->tls, tls_params, + GNUTLS_NONBLOCK | GNUTLS_ENABLE_EARLY_DATA | + GNUTLS_NO_END_OF_EARLY_DATA, net->wait); + if (ret != KNOT_EOK) { + net_clean(net); + return ret; + } + quic_ctx_deinit(&net->quic); + ret = quic_ctx_init(&net->quic, &net->tls, quic_params); + if (ret != KNOT_EOK) { + net_clean(net); + return ret; + } + } else +#endif //ENABLE_QUIC + { + int ret = tls_ctx_init(&net->tls, tls_params, + GNUTLS_NONBLOCK, net->wait); + if (ret != KNOT_EOK) { + net_clean(net); + return ret; + } + } + + return KNOT_EOK; +} + +/*! + * Connect with TCP Fast Open. + */ +static int fastopen_connect(int sockfd, const struct addrinfo *srv) +{ +#if defined( __FreeBSD__) + const int enable = 1; + return setsockopt(sockfd, IPPROTO_TCP, TCP_FASTOPEN, &enable, sizeof(enable)); +#elif defined(__APPLE__) + // connection is performed lazily when first data are sent + struct sa_endpoints ep = {0}; + ep.sae_dstaddr = srv->ai_addr; + ep.sae_dstaddrlen = srv->ai_addrlen; + int flags = CONNECT_DATA_IDEMPOTENT|CONNECT_RESUME_ON_READ_WRITE; + + return connectx(sockfd, &ep, SAE_ASSOCID_ANY, flags, NULL, 0, NULL, NULL); +#elif defined(__linux__) + // connect() will be called implicitly with sendto(), sendmsg() + return 0; +#else + errno = ENOTSUP; + return -1; +#endif +} + +/*! + * Sends data with TCP Fast Open. + */ +static int fastopen_send(int sockfd, const struct msghdr *msg, int timeout) +{ +#if defined(__FreeBSD__) || defined(__APPLE__) + return sendmsg(sockfd, msg, 0); +#elif defined(__linux__) + int ret = sendmsg(sockfd, msg, MSG_FASTOPEN); + if (ret == -1 && errno == EINPROGRESS) { + struct pollfd pfd = { + .fd = sockfd, + .events = POLLOUT, + .revents = 0, + }; + if (poll(&pfd, 1, 1000 * timeout) != 1) { + errno = ETIMEDOUT; + return -1; + } + ret = sendmsg(sockfd, msg, 0); + } + return ret; +#else + errno = ENOTSUP; + return -1; +#endif +} + +static char *net_get_remote(const net_t *net) +{ + if (net->tls.params->sni != NULL) { + return net->tls.params->sni; + } else if (net->tls.params->hostname != NULL) { + return net->tls.params->hostname; + } else if (strchr(net->remote_str, ':') == NULL) { + char *at = strchr(net->remote_str, '@'); + if (at != NULL && strncmp(net->remote->name, net->remote_str, + at - net->remote_str)) { + return net->remote->name; + } + } + return NULL; +} + +int net_connect(net_t *net) +{ + if (net == NULL || net->srv == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + // Set remote information string. + get_addr_str((struct sockaddr_storage *)net->srv->ai_addr, + get_protocol(net), &net->remote_str); + + // Create socket. + int sockfd = socket(net->srv->ai_family, net->socktype, 0); + if (sockfd == -1) { + WARN("can't create socket for %s", net->remote_str); + return KNOT_NET_ESOCKET; + } + + // Initialize poll descriptor structure. + struct pollfd pfd = { + .fd = sockfd, + .events = POLLOUT, + .revents = 0, + }; + + // Set non-blocking socket. + if (fcntl(sockfd, F_SETFL, O_NONBLOCK) == -1) { + WARN("can't set non-blocking socket for %s", net->remote_str); + return KNOT_NET_ESOCKET; + } + + // Bind address to socket if specified. + if (net->local_info != NULL) { + if (bind(sockfd, net->local_info->ai_addr, + net->local_info->ai_addrlen) == -1) { + WARN("can't assign address %s", net->local->name); + return KNOT_NET_ESOCKET; + } + } else { + // Ensure source port is always randomized (even for TCP). + struct sockaddr_storage local = { .ss_family = net->srv->ai_family }; + (void)bind(sockfd, (struct sockaddr *)&local, sockaddr_len(&local)); + } + + int ret = 0; + if (net->socktype == SOCK_STREAM) { + int cs = 1, err; + socklen_t err_len = sizeof(err); + bool fastopen = net->flags & NET_FLAGS_FASTOPEN; + +#ifdef TCP_NODELAY + (void)setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &cs, sizeof(cs)); +#endif + + // Establish a connection. + if (net->tls.params == NULL || !fastopen) { + if (fastopen) { + ret = fastopen_connect(sockfd, net->srv); + } else { + ret = connect(sockfd, net->srv->ai_addr, net->srv->ai_addrlen); + } + if (ret != 0 && errno != EINPROGRESS) { + WARN("can't connect to %s", net->remote_str); + net_close(net); + return KNOT_NET_ECONNECT; + } + + // Check for connection timeout. + if (!fastopen && poll(&pfd, 1, 1000 * net->wait) != 1) { + WARN("connection timeout for %s", net->remote_str); + net_close(net); + return KNOT_NET_ECONNECT; + } + + // Check if NB socket is writeable. + cs = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, &err_len); + if (cs < 0 || err != 0) { + WARN("can't connect to %s", net->remote_str); + net_close(net); + return KNOT_NET_ECONNECT; + } + } + + if (net->tls.params != NULL) { +#ifdef LIBNGHTTP2 + if (net->https.params.enable) { + // Establish HTTPS connection. + char *remote = net_get_remote(net); + ret = tls_ctx_setup_remote_endpoint(&net->tls, &doh_alpn, 1, NULL, + remote); + if (ret != 0) { + net_close(net); + return ret; + } + if (remote && net->https.authority == NULL) { + net->https.authority = strdup(remote); + } + ret = https_ctx_connect(&net->https, sockfd, fastopen, + (struct sockaddr_storage *)net->srv->ai_addr); + } else +#endif //LIBNGHTTP2 + { + // Establish TLS connection. + ret = tls_ctx_setup_remote_endpoint(&net->tls, &dot_alpn, 1, NULL, + net_get_remote(net)); + if (ret != 0) { + net_close(net); + return ret; + } + ret = tls_ctx_connect(&net->tls, sockfd, fastopen, + (struct sockaddr_storage *)net->srv->ai_addr); + } + if (ret != KNOT_EOK) { + net_close(net); + return ret; + } + } + } +#ifdef ENABLE_QUIC + else if (net->socktype == SOCK_DGRAM) { + if (net->quic.params.enable) { + // Establish QUIC connection. + ret = net_cmsg_ecn_enable(sockfd, net->srv->ai_family); + if (ret != KNOT_EOK && ret != KNOT_ENOTSUP) { + net_close(net); + return ret; + } + ret = tls_ctx_setup_remote_endpoint(&net->tls, + &doq_alpn, 1, QUIC_PRIORITY, net_get_remote(net)); + if (ret != 0) { + net_close(net); + return ret; + } + ret = quic_ctx_connect(&net->quic, sockfd, + (struct addrinfo *)net->srv); + if (ret != KNOT_EOK) { + net_close(net); + return ret; + } + } + } +#endif + + // Store socket descriptor. + net->sockfd = sockfd; + + return KNOT_EOK; +} + +int net_set_local_info(net_t *net) +{ + if (net == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + socklen_t local_addr_len = sizeof(struct sockaddr_storage); + + struct addrinfo *new_info = calloc(1, sizeof(*new_info) + local_addr_len); + if (new_info == NULL) { + return KNOT_ENOMEM; + } + + new_info->ai_addr = (struct sockaddr *)(new_info + 1); + new_info->ai_family = net->srv->ai_family; + new_info->ai_socktype = net->srv->ai_socktype; + new_info->ai_protocol = net->srv->ai_protocol; + new_info->ai_addrlen = local_addr_len; + + if (getsockname(net->sockfd, new_info->ai_addr, &local_addr_len) == -1) { + WARN("can't get local address"); + free(new_info); + return KNOT_NET_ESOCKET; + } + + if (net->local_info != NULL) { + if (net->local == NULL) { + free(net->local_info); + } else { + freeaddrinfo(net->local_info); + } + } + + net->local_info = new_info; + + get_addr_str((struct sockaddr_storage *)net->local_info->ai_addr, + get_protocol(net), &net->local_str); + + return KNOT_EOK; +} + +int net_send(const net_t *net, const uint8_t *buf, const size_t buf_len) +{ + if (net == NULL || buf == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + +#ifdef ENABLE_QUIC + // Send data over QUIC. + if (net->quic.params.enable) { + int ret = quic_send_dns_query((quic_ctx_t *)&net->quic, + net->sockfd, net->srv, buf, buf_len); + if (ret != KNOT_EOK) { + WARN("can't send query to %s", net->remote_str); + return KNOT_NET_ESEND; + } + } else +#endif + // Send data over UDP. + if (net->socktype == SOCK_DGRAM) { + char proxy_buf[PROXYV2_HEADER_MAXLEN]; + struct iovec iov[2] = { + { .iov_base = proxy_buf, .iov_len = 0 }, + { .iov_base = (void *)buf, .iov_len = buf_len } + }; + + struct msghdr msg = { + .msg_name = net->srv->ai_addr, + .msg_namelen = net->srv->ai_addrlen, + .msg_iov = &iov[1], + .msg_iovlen = 1 + }; + + if (net->proxy.src != NULL && net->proxy.src->sa_family != 0) { + int ret = proxyv2_write_header(proxy_buf, sizeof(proxy_buf), + SOCK_DGRAM, net->proxy.src, + net->proxy.dst); + if (ret < 0) { + WARN("can't send proxied query to %s", net->remote_str); + return KNOT_NET_ESEND; + } + iov[0].iov_len = ret; + msg.msg_iov--; + msg.msg_iovlen++; + } + + ssize_t total = iov[0].iov_len + iov[1].iov_len; + + if (sendmsg(net->sockfd, &msg, 0) != total) { + WARN("can't send query to %s", net->remote_str); + return KNOT_NET_ESEND; + } +#ifdef LIBNGHTTP2 + // Send data over HTTPS + } else if (net->https.params.enable) { + int ret = https_send_dns_query((https_ctx_t *)&net->https, buf, buf_len); + if (ret != KNOT_EOK) { + WARN("can't send query to %s", net->remote_str); + return KNOT_NET_ESEND; + } +#endif //LIBNGHTTP2 + // Send data over TLS. + } else if (net->tls.params != NULL) { + int ret = tls_ctx_send((tls_ctx_t *)&net->tls, buf, buf_len); + if (ret != KNOT_EOK) { + WARN("can't send query to %s", net->remote_str); + return KNOT_NET_ESEND; + } + // Send data over TCP. + } else { + bool fastopen = net->flags & NET_FLAGS_FASTOPEN; + + char proxy_buf[PROXYV2_HEADER_MAXLEN]; + uint16_t pktsize = htons(buf_len); // Leading packet length bytes. + struct iovec iov[3] = { + { .iov_base = proxy_buf, .iov_len = 0 }, + { .iov_base = &pktsize, .iov_len = sizeof(pktsize) }, + { .iov_base = (void *)buf, .iov_len = buf_len } + }; + + struct msghdr msg = { + .msg_name = net->srv->ai_addr, + .msg_namelen = net->srv->ai_addrlen, + .msg_iov = &iov[1], + .msg_iovlen = 2 + }; + + if (net->srv->ai_addr->sa_family == AF_UNIX) { + msg.msg_name = NULL; + } + + if (net->proxy.src != NULL && net->proxy.src->sa_family != 0) { + int ret = proxyv2_write_header(proxy_buf, sizeof(proxy_buf), + SOCK_STREAM, net->proxy.src, + net->proxy.dst); + if (ret < 0) { + WARN("can't send proxied query to %s", net->remote_str); + return KNOT_NET_ESEND; + } + iov[0].iov_len = ret; + msg.msg_iov--; + msg.msg_iovlen++; + } + + ssize_t total = iov[0].iov_len + iov[1].iov_len + iov[2].iov_len; + + int ret = 0; + if (fastopen) { + ret = fastopen_send(net->sockfd, &msg, net->wait); + } else { + ret = sendmsg(net->sockfd, &msg, 0); + } + if (ret != total) { + WARN("can't send query to %s", net->remote_str); + return KNOT_NET_ESEND; + } + } + + return KNOT_EOK; +} + +int net_receive(const net_t *net, uint8_t *buf, const size_t buf_len) +{ + if (net == NULL || buf == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + // Initialize poll descriptor structure. + struct pollfd pfd = { + .fd = net->sockfd, + .events = POLLIN, + .revents = 0, + }; + +#ifdef ENABLE_QUIC + // Receive data over QUIC. + if (net->quic.params.enable) { + int ret = quic_recv_dns_response((quic_ctx_t *)&net->quic, buf, + buf_len, net->srv); + if (ret < 0) { + WARN("can't receive reply from %s", net->remote_str); + return KNOT_NET_ERECV; + } + return ret; + } else +#endif + // Receive data over UDP. + if (net->socktype == SOCK_DGRAM) { + struct sockaddr_storage from; + memset(&from, '\0', sizeof(from)); + + // Receive replies unless correct reply or timeout. + while (true) { + socklen_t from_len = sizeof(from); + + // Wait for datagram data. + if (poll(&pfd, 1, 1000 * net->wait) != 1) { + WARN("response timeout for %s", + net->remote_str); + return KNOT_NET_ETIMEOUT; + } + + // Receive whole UDP datagram. + ssize_t ret = recvfrom(net->sockfd, buf, buf_len, 0, + (struct sockaddr *)&from, &from_len); + if (ret <= 0) { + WARN("can't receive reply from %s", + net->remote_str); + return KNOT_NET_ERECV; + } + + // Compare reply address with the remote one. + if (from_len > sizeof(from) || + memcmp(&from, net->srv->ai_addr, from_len) != 0) { + char *src = NULL; + get_addr_str(&from, get_protocol(net), &src); + WARN("unexpected reply source %s", src); + free(src); + continue; + } + + return ret; + } +#ifdef LIBNGHTTP2 + // Receive data over HTTPS. + } else if (net->https.params.enable) { + int ret = https_recv_dns_response((https_ctx_t *)&net->https, buf, buf_len); + if (ret < 0) { + WARN("can't receive reply from %s", net->remote_str); + return KNOT_NET_ERECV; + } + return ret; +#endif //LIBNGHTTP2 + // Receive data over TLS. + } else if (net->tls.params != NULL) { + int ret = tls_ctx_receive((tls_ctx_t *)&net->tls, buf, buf_len); + if (ret < 0) { + WARN("can't receive reply from %s", net->remote_str); + return KNOT_NET_ERECV; + } + return ret; + // Receive data over TCP. + } else { + uint32_t total = 0; + + uint16_t msg_len = 0; + // Receive TCP message header. + while (total < sizeof(msg_len)) { + if (poll(&pfd, 1, 1000 * net->wait) != 1) { + WARN("response timeout for %s", + net->remote_str); + return KNOT_NET_ETIMEOUT; + } + + // Receive piece of message. + ssize_t ret = recv(net->sockfd, (uint8_t *)&msg_len + total, + sizeof(msg_len) - total, 0); + if (ret <= 0) { + WARN("can't receive reply from %s", + net->remote_str); + return KNOT_NET_ERECV; + } + total += ret; + } + + // Convert number to host format. + msg_len = ntohs(msg_len); + if (msg_len > buf_len) { + return KNOT_ESPACE; + } + + total = 0; + + // Receive whole answer message by parts. + while (total < msg_len) { + if (poll(&pfd, 1, 1000 * net->wait) != 1) { + WARN("response timeout for %s", + net->remote_str); + return KNOT_NET_ETIMEOUT; + } + + // Receive piece of message. + ssize_t ret = recv(net->sockfd, buf + total, msg_len - total, 0); + if (ret <= 0) { + WARN("can't receive reply from %s", + net->remote_str); + return KNOT_NET_ERECV; + } + total += ret; + } + + return total; + } + + return KNOT_NET_ERECV; +} + +void net_close(net_t *net) +{ + if (net == NULL) { + DBG_NULL; + return; + } + +#ifdef ENABLE_QUIC + if (net->quic.params.enable) { + quic_ctx_close(&net->quic); + } +#endif + tls_ctx_close(&net->tls); + close(net->sockfd); + net->sockfd = -1; +} + +void net_clean(net_t *net) +{ + if (net == NULL) { + DBG_NULL; + return; + } + + free(net->local_str); + free(net->remote_str); + net->local_str = NULL; + net->remote_str = NULL; + + if (net->local_info != NULL) { + if (net->local == NULL) { + free(net->local_info); + } else { + freeaddrinfo(net->local_info); + } + net->local_info = NULL; + } + + if (net->remote_info != NULL) { + if (net->remote_info->ai_addr->sa_family == AF_UNIX) { + free(net->remote_info->ai_addr); + free(net->remote_info); + } else { + freeaddrinfo(net->remote_info); + } + net->remote_info = NULL; + } + +#ifdef LIBNGHTTP2 + https_ctx_deinit(&net->https); +#endif +#ifdef ENABLE_QUIC + quic_ctx_deinit(&net->quic); +#endif + tls_ctx_deinit(&net->tls); +} diff --git a/src/utils/common/netio.h b/src/utils/common/netio.h new file mode 100644 index 0000000..772784d --- /dev/null +++ b/src/utils/common/netio.h @@ -0,0 +1,253 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <netdb.h> +#include <stdint.h> +#include <sys/socket.h> + +#include "libknot/probe/data.h" +#include "utils/common/https.h" +#include "utils/common/params.h" +#include "utils/common/quic.h" +#include "utils/common/tls.h" + +/*! \brief Structure containing server information. */ +typedef struct { + /*! List node (for list container). */ + node_t n; + /*! Name or address of the server. */ + char *name; + /*! Name or number of the service. */ + char *service; +} srv_info_t; + +typedef enum { + NET_FLAGS_NONE = 0, + NET_FLAGS_FASTOPEN = 1 << 0, +} net_flags_t; + +typedef struct { + /*! Socket descriptor. */ + int sockfd; + + /*! IP protocol type. */ + int iptype; + /*! Socket type. */ + int socktype; + /*! Timeout for all network operations. */ + int wait; + /*! Connection flags. */ + net_flags_t flags; + + /*! Local interface parameters. */ + const srv_info_t *local; + /*! Remote server parameters. */ + const srv_info_t *remote; + + /*! Local description string (used for logging). */ + char *local_str; + /*! Remote description string (used for logging). */ + char *remote_str; + + /*! Output from getaddrinfo for remote server. If the server is + * specified using domain name, this structure may contain more + * results. + */ + struct addrinfo *remote_info; + /*! Currently used result from remote_info. */ + struct addrinfo *srv; + /*! Output from getaddrinfo for local address. Only first result is + * used. + */ + struct addrinfo *local_info; + + /*! TLS context. */ + tls_ctx_t tls; +#ifdef LIBNGHTTP2 + /*! HTTPS context. */ + https_ctx_t https; +#endif +#ifdef ENABLE_QUIC + /*! QUIC context. */ + quic_ctx_t quic; +#endif + struct { + const struct sockaddr *src; + const struct sockaddr *dst; + } proxy; +} net_t; + +/*! + * \brief Creates and fills server structure. + * + * \param name Address or host name. + * \param service Port number or service name. + * + * \retval server if success. + * \retval NULL if error. + */ +srv_info_t *srv_info_create(const char *name, const char *service); + +/*! + * \brief Destroys server structure. + * + * \param server Server structure to destroy. + */ +void srv_info_free(srv_info_t *server); + +/*! + * \brief Translates enum IP version type to int version. + * + * \param ip IP version to convert. + * \param server Server structure. + * + * \retval AF_INET, AF_INET6, AF_UNIX, or AF_UNSPEC. + */ +int get_iptype(const ip_t ip, const srv_info_t *server); + +/*! + * \brief Translates enum IP protocol type to int version in context to the + * current DNS query type. + * + * \param proto IP protocol type to convert. + * \param type DNS query type number. + * + * \retval SOCK_STREAM or SOCK_DGRAM. + */ +int get_socktype(const protocol_t proto, const uint16_t type); + +/*! + * \brief Translates int socket type to the common string one. + * + * \param socktype Socket type (SOCK_STREAM or SOCK_DGRAM). + * + * \retval "TCP" or "UDP". + */ +const char *get_sockname(const int socktype); + +/*! + * \brief Translates protocol type to a common string. + * + * \param ss Socket address storage. + * \param socktype Protocol type. + * \param dst Output string. + */ +void get_addr_str(const struct sockaddr_storage *ss, + const knot_probe_proto_t socktype, + char **dst); + +/*! + * \brief Initializes network structure and resolves local and remote addresses. + * + * \param local Local address and service description. + * \param remote Remote address and service description. + * \param iptype IP version. + * \param socktype Socket type. + * \param wait Network timeout interval. + * \param flags Connection flags. + * \param proxy_src Proxy source address. + * \param proxy_dst Proxy destination address. + * \param net Network structure to initialize. + * + * \retval KNOT_EOK if success. + * \retval errcode if error. + */ +int net_init(const srv_info_t *local, + const srv_info_t *remote, + const int iptype, + const int socktype, + const int wait, + const net_flags_t flags, + const struct sockaddr *proxy_src, + const struct sockaddr *proxy_dst, + net_t *net); + +/*! + * \brief (Re)initializes crypto protocols in network structure. + * + * \param net Network structure to initialize. + * \param tls_params TLS parameters. + * \param https_params HTTPS parameters. + * \param quic_params QUIC parameters. + * + * \retval KNOT_EOK if success. + * \retval errcode if error. + */ +int net_init_crypto(net_t *net, + const tls_params_t *tls_params, + const https_params_t *https_params, + const quic_params_t *quic_params); + +/*! + * \brief Creates socket and connects (if TCP) to remote address specified + * by net->srv. + * + * \param net Connection parameters. + * + * \retval KNOT_EOK if success. + * \retval errcode if error. + */ +int net_connect(net_t *net); + +/*! + * \brief Fills in local address information. + * + * \param net Connection parameters. + * + * \retval KNOT_EOK if success. + * \retval errcode if error. + */ +int net_set_local_info(net_t *net); + +/*! + * \brief Sends data to connected remote server. + * + * \param net Connection parameters. + * \param buf Data to send. + * \param buf_len Length of the data to send. + * + * \retval KNOT_EOK if success. + * \retval errcode if error. + */ +int net_send(const net_t *net, const uint8_t *buf, const size_t buf_len); + +/*! + * \brief Receives data from connected remote server. + * + * \param net Connection parameters. + * \param buf Buffer for incoming data. + * \param buf_len Length of the buffer. + * + * \retval >=0 length of successfully received data. + * \retval errcode if error. + */ +int net_receive(const net_t *net, uint8_t *buf, const size_t buf_len); + +/*! + * \brief Closes current network connection. + * + * \param net Connection parameters. + */ +void net_close(net_t *net); + +/*! + * \brief Cleans up network structure. + * + * \param net Connection parameters. + */ +void net_clean(net_t *net); diff --git a/src/utils/common/params.c b/src/utils/common/params.c new file mode 100644 index 0000000..4db4b9e --- /dev/null +++ b/src/utils/common/params.c @@ -0,0 +1,343 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <arpa/inet.h> +#include <stdio.h> +#include <stdlib.h> +#include <netinet/in.h> +#include <sys/socket.h> + +#ifdef LIBIDN +#include LIBIDN_HEADER +#endif + +#include "utils/common/params.h" +#include "utils/common/msg.h" +#include "utils/common/resolv.h" +#include "utils/common/token.h" +#include "libknot/libknot.h" +#include "contrib/macros.h" +#include "contrib/mempattern.h" +#include "contrib/openbsd/strlcpy.h" +#include "contrib/strtonum.h" + +#define IPV4_REVERSE_DOMAIN "in-addr.arpa." +#define IPV6_REVERSE_DOMAIN "ip6.arpa." + +char *name_from_idn(const char *idn_name) { +#ifdef LIBIDN + char *name = NULL; + + int rc = idna_to_ascii_lz(idn_name, &name, 0); + if (rc != IDNA_SUCCESS) { + ERR("IDNA (%s)", idna_strerror(rc)); + return NULL; + } + + return name; +#endif + return strdup(idn_name); +} + +void name_to_idn(char **name) { +#ifdef LIBIDN + char *idn_name = NULL; + + int rc = idna_to_unicode_8zlz(*name, &idn_name, 0); + if (rc != IDNA_SUCCESS) { + return; + } + + free(*name); + *name = idn_name; +#endif + return; +} + +/*! + * \brief Checks if string is a prefix of reference string. + * + * \param pref Prefix string. + * \param pref_len Prefix length. + * \param str Reference string (must have trailing zero). + * + * \retval -1 \a pref is not a prefix of \a str. + * \retval 0<= number of chars after prefix \a pref in \a str. + */ +static int cmp_prefix(const char *pref, const size_t pref_len, + const char *str) +{ + size_t i = 0; + while (1) { + // Different characters => NOT prefix. + if (pref[i] != str[i]) { + return -1; + } + + i++; + + // Pref IS a prefix of pref. + if (i == pref_len) { + size_t rest = 0; + while (str[i + rest] != '\0') { + rest++; + } + return rest; + // Pref is longer then ref => NOT prefix. + } else if (str[i] == '\0') { + return -1; + } + } +} + +int best_param(const char *str, const size_t str_len, const param_t *tbl, + bool *unique) +{ + if (str == NULL || str_len == 0 || tbl == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + int best_pos = -1; + int best_match = INT_MAX; + size_t matches = 0; + for (int i = 0; tbl[i].name != NULL; i++) { + int ret = cmp_prefix(str, str_len, tbl[i].name); + switch (ret) { + case -1: + continue; + case 0: + *unique = true; + return i; + default: + if (ret < best_match) { + best_pos = i; + best_match = ret; + } + matches++; + } + } + + switch (matches) { + case 0: + return KNOT_ENOTSUP; + case 1: + *unique = true; + return best_pos; + default: + *unique = false; + return best_pos; + } +} + +char *get_reverse_name(const char *name) +{ + struct in_addr addr4; + struct in6_addr addr6; + int ret; + char buf[128] = "\0"; + + if (name == NULL) { + DBG_NULL; + return NULL; + } + + // Check name for IPv4 address, IPv6 address or other. + if (inet_pton(AF_INET, name, &addr4) == 1) { + uint32_t num = ntohl(addr4.s_addr); + + // Create IPv4 reverse FQD name. + ret = snprintf(buf, sizeof(buf), "%u.%u.%u.%u.%s", + (num >> 0) & 0xFF, (num >> 8) & 0xFF, + (num >> 16) & 0xFF, (num >> 24) & 0xFF, + IPV4_REVERSE_DOMAIN); + if (ret < 0 || (size_t)ret >= sizeof(buf)) { + return NULL; + } + + return strdup(buf); + } else if (inet_pton(AF_INET6, name, &addr6) == 1) { + char *pos = buf; + size_t len = sizeof(buf); + uint8_t left, right; + + // Create IPv6 reverse name. + for (int i = 15; i >= 0; i--) { + left = ((addr6.s6_addr)[i] & 0xF0) >> 4; + right = (addr6.s6_addr)[i] & 0x0F; + + ret = snprintf(pos, len, "%x.%x.", right, left); + if (ret < 0 || (size_t)ret >= len) { + return NULL; + } + + pos += ret; + len -= ret; + } + + // Add IPv6 reverse domain. + ret = snprintf(pos, len, "%s", IPV6_REVERSE_DOMAIN); + if (ret < 0 || (size_t)ret >= len) { + return NULL; + } + + return strdup(buf); + } else { + return NULL; + } +} + +char *get_fqd_name(const char *name) +{ + char *fqd_name = NULL; + + if (name == NULL) { + DBG_NULL; + return NULL; + } + + size_t name_len = strlen(name); + + // If the name is FQDN, make a copy. + if (name[name_len - 1] == '.') { + fqd_name = strdup(name); + // Else make a copy and append a trailing dot. + } else { + size_t fqd_name_size = name_len + 2; + fqd_name = malloc(fqd_name_size); + if (fqd_name != NULL) { + strlcpy(fqd_name, name, fqd_name_size); + fqd_name[name_len] = '.'; + fqd_name[name_len + 1] = 0; + } + } + + return fqd_name; +} + +int params_parse_class(const char *value, uint16_t *rclass) +{ + if (value == NULL || rclass == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + if (knot_rrclass_from_string(value, rclass) == 0) { + return KNOT_EOK; + } else { + return KNOT_EINVAL; + } +} + +int params_parse_type(const char *value, uint16_t *rtype, int64_t *serial, + bool *notify) +{ + if (value == NULL || rtype == NULL || serial == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + // Find and parse type name. + size_t param_pos = strcspn(value, "="); + char *type_char = strndup(value, param_pos); + + if (knot_rrtype_from_string(type_char, rtype) != 0) { + size_t cmp_len = MAX(strlen("NOTIFY"), param_pos); + if (strncasecmp(type_char, "NOTIFY", cmp_len) == 0) { + *rtype = KNOT_RRTYPE_SOA; + *notify = true; + } else { + free(type_char); + return KNOT_EINVAL; + } + } else { + *notify = false; + } + + free(type_char); + + // Parse additional parameter. + if (param_pos == strlen(value)) { + // IXFR requires serial parameter. + if (*rtype == KNOT_RRTYPE_IXFR) { + DBG("SOA serial is required for IXFR query"); + return KNOT_EINVAL; + } else { + *serial = -1; + } + } else { + // Additional parameter is accepted for IXFR or NOTIFY. + if (*rtype == KNOT_RRTYPE_IXFR || *notify) { + const char *param_str = value + 1 + param_pos; + char *end; + + // Convert string to serial. + unsigned long long num = strtoull(param_str, &end, 10); + + // Check for bad serial string. + if (end == param_str || *end != '\0' || num > UINT32_MAX) { + DBG("bad SOA serial '%s'", param_str); + return KNOT_EINVAL; + } + + *serial = num; + } else { + DBG("unsupported parameter '%s'", value); + return KNOT_EINVAL; + } + } + + return KNOT_EOK; +} + +int params_parse_server(const char *value, list_t *servers, const char *def_port) +{ + if (value == NULL || servers == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + // Add specified nameserver. + srv_info_t *server = parse_nameserver(value, def_port); + if (server == NULL) { + return KNOT_EINVAL; + } + add_tail(servers, (node_t *)server); + + return KNOT_EOK; +} + +int params_parse_wait(const char *value, int32_t *dst) +{ + if (value == NULL || dst == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + uint32_t num = 0; + int ret = str_to_u32(value, &num); + if (ret != KNOT_EOK) { + return ret; + } + + if (num < 1 || num > INT32_MAX / 1000) { + num = INT32_MAX / 1000; + } + + *dst = num; + + return KNOT_EOK; +} diff --git a/src/utils/common/params.h b/src/utils/common/params.h new file mode 100644 index 0000000..8b7565e --- /dev/null +++ b/src/utils/common/params.h @@ -0,0 +1,170 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <limits.h> +#include <stdint.h> +#include <stdbool.h> +#include <stdio.h> + +#include "libknot/libknot.h" +#include "contrib/ucw/lists.h" + +#define DEFAULT_IPV4_NAME "127.0.0.1" +#define DEFAULT_IPV6_NAME "::1" +#define DEFAULT_DNS_PORT "53" +#define DEFAULT_DNS_HTTPS_PORT "443" +#define DEFAULT_DNS_QUIC_PORT "853" +#define DEFAULT_DNS_TLS_PORT "853" +#define DEFAULT_UDP_SIZE 512 +#define DEFAULT_EDNS_SIZE 4096 +#define MAX_PACKET_SIZE 65535 + +#define SEP_CHARS "\n\t " + +/*! \brief Variants of IP protocol. */ +typedef enum { + IP_ALL, + IP_4, + IP_6 +} ip_t; + +/*! \brief Variants of transport protocol. */ +typedef enum { + PROTO_ALL, + PROTO_TCP, + PROTO_UDP +} protocol_t; + +/*! \brief Variants of output type. */ +typedef enum { + /*!< Verbose output (same for host and dig). */ + FORMAT_FULL, + /*!< Short dig output. */ + FORMAT_DIG, + /*!< Brief host output. */ + FORMAT_HOST, + /*!< Brief nsupdate output. */ + FORMAT_NSUPDATE, + /*!< Machine readable JSON format (RFC 8427). */ + FORMAT_JSON +} format_t; + +/*! \brief Text output settings. */ +typedef struct { + /*!< Output format. */ + format_t format; + + /*!< Style of rrset dump. */ + knot_dump_style_t style; + + /*!< Show query packet. */ + bool show_query; + /*!< Show header info. */ + bool show_header; + /*!< Show section name. */ + bool show_section; + /*!< Show EDNS pseudosection. */ + bool show_edns; + /*!< Show unknown EDNS options in printable format. */ + bool show_edns_opt_text; + /*!< Show QUERY/ZONE section. */ + bool show_question; + /*!< Show ANSWER/PREREQ section. */ + bool show_answer; + /*!< Show UPDATE/AUTHORITY section. */ + bool show_authority; + /*!< Show ADDITIONAL section. */ + bool show_additional; + /*!< Show TSIG pseudosection. */ + bool show_tsig; + /*!< Show footer info. */ + bool show_footer; + /*!< Display EDNS in Presentation format. */ + bool present_edns; + + /*!< KHOST - Hide CNAME record in answer (duplicity reduction). */ + bool hide_cname; +} style_t; + +/*! \brief Parameter handler. */ +typedef int (*param_handle_f)(const char *arg, void *params); + +/*! \brief Parameter argument type. */ +typedef enum { + ARG_NONE, + ARG_REQUIRED, + ARG_OPTIONAL +} arg_t; + +/*! \brief Parameter specification. */ +typedef struct { + const char *name; + arg_t arg; + param_handle_f handler; +} param_t; + +inline static void print_version(const char *program_name) +{ + printf("%s (Knot DNS), version %s\n", program_name, PACKAGE_VERSION); +} + +/*! + * \brief Transforms localized IDN string to ASCII punycode. + * + * \param idn_name IDN name to transform. + * + * \retval NULL if transformation fails. + * \retval string if ok. + */ +char *name_from_idn(const char *idn_name); + +/*! + * \brief Transforms ASCII punycode to localized IDN string. + * + * If an error occurs or IDN support is missing, this function does nothing. + * + * \param name ASCII name to transform and replace with IDN name. + */ +void name_to_idn(char **name); + +/*! + * \brief Find the best parameter match in table based on prefix equality. + * + * \param str Parameter name to look up. + * \param str_len Parameter name length. + * \param tbl Parameter table. + * \param unique Indication if output is unique result. + * + * \retval >=0 looked up parameter position in \a tbl. + * \retval err if error. + */ +int best_param(const char *str, const size_t str_len, const param_t *tbl, + bool *unique); + +char *get_reverse_name(const char *name); + +char *get_fqd_name(const char *name); + +int params_parse_class(const char *value, uint16_t *rclass); + +int params_parse_type(const char *value, uint16_t *rtype, int64_t *serial, + bool *notify); + +int params_parse_server(const char *value, list_t *servers, const char *def_port); + +int params_parse_wait(const char *value, int32_t *dst); diff --git a/src/utils/common/quic.c b/src/utils/common/quic.c new file mode 100644 index 0000000..55e5408 --- /dev/null +++ b/src/utils/common/quic.c @@ -0,0 +1,813 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stddef.h> + +#include "contrib/net.h" +#include "libknot/errcode.h" +#include "utils/common/quic.h" +#include "utils/common/msg.h" + +int quic_params_copy(quic_params_t *dst, const quic_params_t *src) +{ + if (dst == NULL || src == NULL) { + return KNOT_EINVAL; + } + + dst->enable = src->enable; + + return KNOT_EOK; +} + +void quic_params_clean(quic_params_t *params) +{ + if (params == NULL) { + return; + } + + params->enable = false; +} + +#ifdef ENABLE_QUIC + +#include <assert.h> +#include <poll.h> +#include <gnutls/crypto.h> + +#include <ngtcp2/ngtcp2_crypto.h> +#include <ngtcp2/ngtcp2_crypto_gnutls.h> + +#include "contrib/macros.h" +#include "libdnssec/error.h" +#include "libdnssec/random.h" +#include "libknot/xdp/tcp_iobuf.h" +#include "utils/common/params.h" + +#define quic_get_encryption_level(level) ngtcp2_crypto_gnutls_from_gnutls_record_encryption_level(level) +#define quic_send(ctx, sockfd, family) quic_send_data(ctx, sockfd, family, NULL, 0) +#define set_application_error(ctx, error_code, reason, reason_len) \ + ngtcp2_ccerr_set_application_error(&(ctx)->last_err, \ + error_code, reason, reason_len) +#define set_transport_error(ctx, error_code, reason, reason_len) \ + ngtcp2_ccerr_set_transport_error(&(ctx)->last_err, \ + error_code, reason, reason_len) + +const gnutls_datum_t doq_alpn = { + (unsigned char *)"doq", 3 +}; + +static int recv_stream_data_cb(ngtcp2_conn *conn, uint32_t flags, + int64_t stream_id, uint64_t offset, const uint8_t *data, + size_t datalen, void *user_data, void *stream_user_data) +{ + (void)conn; + (void)flags; + (void)offset; + (void)stream_user_data; + + quic_ctx_t *ctx = (quic_ctx_t *)user_data; + + if (stream_id != ctx->stream.id) { + const uint8_t msg[] = "Unknown stream"; + set_application_error(ctx, DOQ_PROTOCOL_ERROR, msg, sizeof(msg) - 1); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + struct iovec in = { + .iov_base = (uint8_t *)data, + .iov_len = datalen + }; + + int ret = knot_tcp_inbufs_upd(&ctx->stream.in_buffer, in, true, + &ctx->stream.in_parsed, + &ctx->stream.in_parsed_total); + if (ret != KNOT_EOK) { + const uint8_t msg[] = "Malformed payload"; + set_application_error(ctx, DOQ_PROTOCOL_ERROR, msg, sizeof(msg) - 1); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + ctx->stream.in_parsed_it = 0; + return 0; +} + +static int stream_open_cb(ngtcp2_conn *conn, int64_t stream_id, + void *user_data) +{ + (void)conn; + + quic_ctx_t *ctx = (quic_ctx_t *)user_data; + set_application_error(ctx, DOQ_PROTOCOL_ERROR, NULL, 0); + return NGTCP2_ERR_CALLBACK_FAILURE; +} + +static int acked_stream_data_offset_cb(ngtcp2_conn *conn, int64_t stream_id, + uint64_t offset, uint64_t datalen, void *user_data, + void *stream_user_data) +{ + (void)conn; + (void)offset; + (void)stream_user_data; + + quic_ctx_t *ctx = (quic_ctx_t *)user_data; + if (ctx->stream.id == stream_id) { + ctx->stream.out_ack -= datalen; + } + return KNOT_EOK; +} + +static int stream_close_cb(ngtcp2_conn *conn, uint32_t flags, + int64_t stream_id, uint64_t app_error_code, void *user_data, + void *stream_user_data) +{ + (void)conn; + (void)flags; + (void)app_error_code; + (void)stream_user_data; + + quic_ctx_t *ctx = (quic_ctx_t *)user_data; + if (ctx && stream_id == ctx->stream.id) { + ctx->stream.id = -1; + } + return KNOT_EOK; +} + +static int quic_open_bidi_stream(quic_ctx_t *ctx) +{ + if (ctx->stream.id >= 0) { + return KNOT_EOK; + } + + int ret = ngtcp2_conn_open_bidi_stream(ctx->conn, &ctx->stream.id, NULL); + if (ret) { + return KNOT_ERROR; + } + return KNOT_EOK; +} + +static void rand_cb(uint8_t *dest, size_t destlen, + const ngtcp2_rand_ctx *rand_ctx) +{ + (void)rand_ctx; + + dnssec_random_buffer(dest, destlen); +} + +static int get_new_connection_id_cb(ngtcp2_conn *conn, ngtcp2_cid *cid, + uint8_t *token, size_t cidlen, void *user_data) +{ + (void)conn; + + quic_ctx_t *ctx = (quic_ctx_t *)user_data; + + if (dnssec_random_buffer(cid->data, cidlen) != DNSSEC_EOK) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + cid->datalen = cidlen; + + if (ngtcp2_crypto_generate_stateless_reset_token(token, ctx->secret, + sizeof(ctx->secret), cid) != 0) + { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +static int stream_reset_cb(ngtcp2_conn *conn, int64_t stream_id, + uint64_t final_size, uint64_t app_error_code, void *user_data, + void *stream_user_data) +{ + quic_ctx_t *ctx = (quic_ctx_t *)user_data; + if (ctx->stream.id == stream_id) { + set_transport_error(ctx, NGTCP2_PROTOCOL_VIOLATION, NULL, 0); + quic_ctx_close(ctx); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +static int handshake_confirmed_cb(ngtcp2_conn *conn, void *user_data) +{ + (void)conn; + + quic_ctx_t *ctx = (quic_ctx_t *)user_data; + ctx->state = CONNECTED; + return 0; +} + +static int recv_rx_key_cb(ngtcp2_conn *conn, ngtcp2_encryption_level level, + void *user_data) +{ + quic_ctx_t *ctx = user_data; + if (level == NGTCP2_ENCRYPTION_LEVEL_1RTT) { + ctx->state = CONNECTED; + } + + return 0; +} + +static const ngtcp2_callbacks quic_client_callbacks = { + ngtcp2_crypto_client_initial_cb, + NULL, /* recv_client_initial */ + ngtcp2_crypto_recv_crypto_data_cb, + NULL, /* handshake_completed */ + NULL, /* recv_version_negotiation */ + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + recv_stream_data_cb, + acked_stream_data_offset_cb, + stream_open_cb, + stream_close_cb, + NULL, /* recv_stateless_reset */ + ngtcp2_crypto_recv_retry_cb, + NULL, /* extend_max_bidi_streams */ + NULL, /* extend_max_local_streams_uni */ + rand_cb, + get_new_connection_id_cb, + NULL, /* remove_connection_id */ + ngtcp2_crypto_update_key_cb, + NULL, /* path_validation */ + NULL, /* select_preferred_address */ + stream_reset_cb, + NULL, /* extend_max_remote_streams_bidi */ + NULL, /* extend_max_remote_streams_uni */ + NULL, /* extend_max_stream_data */ + NULL, /* dcid_status */ + handshake_confirmed_cb, + NULL, /* recv_new_token */ + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + NULL, /* recv_datagram */ + NULL, /* ack_datagram */ + NULL, /* lost_datagram */ + ngtcp2_crypto_get_path_challenge_data_cb, + NULL, /* stream_stop_sending */ + ngtcp2_crypto_version_negotiation_cb, + recv_rx_key_cb, + NULL /* recv_tx_key */ +}; + +static int hook_func(gnutls_session_t session, unsigned int htype, + unsigned when, unsigned int incoming, const gnutls_datum_t *msg) +{ + (void)session; + (void)htype; + (void)when; + (void)incoming; + (void)msg; + + return GNUTLS_E_SUCCESS; +} + +static int quic_send_data(quic_ctx_t *ctx, int sockfd, int family, + ngtcp2_vec *datav, size_t datavlen) +{ + uint8_t enc_buf[MAX_PACKET_SIZE]; + struct iovec msg_iov = { + .iov_base = enc_buf, + .iov_len = 0 + }; + struct msghdr msg = { + .msg_iov = &msg_iov, + .msg_iovlen = 1 + }; + uint64_t ts = quic_timestamp(); + + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_NONE; + int64_t stream_id = -1; + if (datavlen > 0) { + flags = NGTCP2_WRITE_STREAM_FLAG_FIN; + stream_id = ctx->stream.id; + } + ngtcp2_ssize send_datalen = 0; + ngtcp2_ssize nwrite = ngtcp2_conn_writev_stream(ctx->conn, + (ngtcp2_path *)ngtcp2_conn_get_path(ctx->conn), &ctx->pi, + enc_buf, sizeof(enc_buf), &send_datalen, flags, stream_id, + datav, datavlen, ts); + if (nwrite <= 0) { + switch(nwrite) { + case 0: + ngtcp2_conn_update_pkt_tx_time(ctx->conn, ts); + return KNOT_EOK; + case NGTCP2_ERR_WRITE_MORE: + assert(0); + return KNOT_NET_ESEND; + default: + set_transport_error(ctx, + ngtcp2_err_infer_quic_transport_error_code(nwrite), + NULL, 0); + if (ngtcp2_err_is_fatal(nwrite)) { + return KNOT_NET_ESEND; + } else { + return KNOT_EOK; + } + } + } + + msg_iov.iov_len = (size_t)nwrite; + + int ret = net_ecn_set(sockfd, family, ctx->pi.ecn); + if (ret != KNOT_EOK && ret != KNOT_ENOTSUP) { + return ret; + } + + if (sendmsg(sockfd, &msg, 0) == -1) { + set_transport_error(ctx, NGTCP2_INTERNAL_ERROR, NULL, 0); + return KNOT_NET_ESEND; + } + + if (send_datalen > 0) { + return send_datalen; + } + + return KNOT_EOK; +} + +static int quic_recv(quic_ctx_t *ctx, int sockfd) +{ + uint8_t enc_buf[MAX_PACKET_SIZE]; + uint8_t msg_ctrl[CMSG_SPACE(sizeof(uint8_t))]; + struct sockaddr_in6 from = { 0 }; + struct iovec msg_iov = { + .iov_base = enc_buf, + .iov_len = sizeof(enc_buf) + }; + struct msghdr msg = { + .msg_name = &from, + .msg_namelen = sizeof(from), + .msg_iov = &msg_iov, + .msg_iovlen = 1, + .msg_control = msg_ctrl, + .msg_controllen = sizeof(msg_ctrl), + .msg_flags = 0 + }; + + ssize_t nwrite = recvmsg(sockfd, &msg, 0); + if (nwrite <= 0) { + return knot_map_errno(); + } + ngtcp2_pkt_info *pi = &ctx->pi; + ctx->pi.ecn = net_cmsg_ecn(&msg); + + int ret = ngtcp2_conn_read_pkt(ctx->conn, + ngtcp2_conn_get_path(ctx->conn), + pi, enc_buf, nwrite, + quic_timestamp()); + if (ngtcp2_err_is_fatal(ret)) { + set_transport_error(ctx, + ngtcp2_err_infer_quic_transport_error_code(ret), + NULL, 0); + return KNOT_NET_ERECV; + } + return KNOT_EOK; +} + +static int quic_respcpy(quic_ctx_t *ctx, uint8_t *buf, const size_t buf_len) +{ + assert(ctx && buf && buf_len > 0); + if (ctx->stream.in_parsed != NULL) { + knot_tcp_inbufs_upd_res_t *cur = ctx->stream.in_parsed; + struct iovec *it = &cur->inbufs[ctx->stream.in_parsed_it]; + if (buf_len < it->iov_len) { + return KNOT_ENOMEM; + } + size_t len = it->iov_len; + memcpy(buf, it->iov_base, len); + if (++ctx->stream.in_parsed_it == cur->n_inbufs) { + ctx->stream.in_parsed_it = 0; + ctx->stream.in_parsed = cur->next; + free(cur); + } + return len; + } + return 0; +} + +uint64_t quic_timestamp(void) +{ + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { + return 0; + } + + return (uint64_t)ts.tv_sec * NGTCP2_SECONDS + (uint64_t)ts.tv_nsec; +} + +int quic_generate_secret(uint8_t *buf, size_t buflen) +{ + assert(buf != NULL && buflen > 0 && buflen <= 32); + uint8_t rand[16], hash[32]; + int ret = dnssec_random_buffer(rand, sizeof(rand)); + if (ret != DNSSEC_EOK) { + return KNOT_ERROR; + } + ret = gnutls_hash_fast(GNUTLS_DIG_SHA256, rand, sizeof(rand), hash); + if (ret != 0) { + return KNOT_ERROR; + } + memcpy(buf, hash, buflen); + return KNOT_EOK; +} + +static int verify_certificate(gnutls_session_t session) +{ + quic_ctx_t *ctx = gnutls_session_get_ptr(session); + return tls_certificate_verification(ctx->tls); +} + +static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) +{ + return ((quic_ctx_t *)conn_ref->user_data)->conn; +} + +int quic_ctx_init(quic_ctx_t *ctx, tls_ctx_t *tls_ctx, const quic_params_t *params) +{ + if (ctx == NULL || tls_ctx == NULL || params == NULL) { + return KNOT_EINVAL; + } + + ctx->conn_ref = (ngtcp2_crypto_conn_ref) { + .get_conn = get_conn, + .user_data = ctx + }; + ctx->params = *params; + ctx->tls = tls_ctx; + ctx->state = CLOSED; + ctx->stream.id = -1; + set_application_error(ctx, DOQ_NO_ERROR, NULL, 0); + if (quic_generate_secret(ctx->secret, sizeof(ctx->secret)) != KNOT_EOK) { + return KNOT_ENOMEM; + } + + gnutls_certificate_set_verify_function( + tls_ctx->credentials, + verify_certificate); + + return KNOT_EOK; +} + +static int get_expiry(ngtcp2_conn *ctx) +{ + ngtcp2_tstamp now = quic_timestamp(); + ngtcp2_tstamp expiry = ngtcp2_conn_get_expiry(ctx); + if (expiry == UINT64_MAX) { + return -1; + } else if (expiry < now) { + return 0; + } + /* ceil((expiry - now) / NGTCP2_MILLISECONDS) */ + return (expiry - now + NGTCP2_MILLISECONDS - 1) / NGTCP2_MILLISECONDS; +} + +int quic_ctx_connect(quic_ctx_t *ctx, int sockfd, struct addrinfo *dst_addr) +{ + if (connect(sockfd, (const struct sockaddr *)(dst_addr->ai_addr), + dst_addr->ai_addrlen) != 0) + { + return knot_map_errno(); + } + + ngtcp2_cid dcid, scid; + scid.datalen = NGTCP2_MAX_CIDLEN; + int ret = dnssec_random_buffer(scid.data, scid.datalen); + if (ret != DNSSEC_EOK) { + return ret; + } + dcid.datalen = 18; + ret = dnssec_random_buffer(dcid.data, dcid.datalen); + if (ret != DNSSEC_EOK) { + return ret; + } + + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + settings.initial_ts = quic_timestamp(); + settings.handshake_timeout = ctx->tls->wait * NGTCP2_SECONDS; + + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + params.initial_max_streams_uni = 0; + params.initial_max_streams_bidi = 0; + params.initial_max_stream_data_bidi_local = NGTCP2_MAX_VARINT; + params.initial_max_data = NGTCP2_MAX_VARINT; + params.max_ack_delay = 1 * NGTCP2_SECONDS; + params.max_idle_timeout = ctx->tls->wait * NGTCP2_SECONDS; + + struct sockaddr_in6 src_addr; + socklen_t src_addr_len = sizeof(src_addr); + ret = getsockname(sockfd, (struct sockaddr *)&src_addr, &src_addr_len); + if (ret < 0) { + return knot_map_errno(); + } + ngtcp2_path path = { + .local = { + .addrlen = src_addr_len, + .addr = (struct sockaddr *)&src_addr + }, + .remote = { + .addrlen = sizeof(*(dst_addr->ai_addr)), + .addr = (struct sockaddr *)(dst_addr->ai_addr) + }, + .user_data = NULL + }; + + if (ctx->conn) { + ngtcp2_conn_del(ctx->conn); + ctx->conn = NULL; + } + + if (ngtcp2_conn_client_new(&ctx->conn, &dcid, &scid, &path, + NGTCP2_PROTO_VER_V1, &quic_client_callbacks, + &settings, ¶ms, NULL, ctx) != 0) { + return KNOT_NET_ECONNECT; + } + gnutls_handshake_set_hook_function(ctx->tls->session, + GNUTLS_HANDSHAKE_ANY, + GNUTLS_HOOK_POST, hook_func); + ret = ngtcp2_crypto_gnutls_configure_client_session(ctx->tls->session); + if (ret != KNOT_EOK) { + return KNOT_NET_ECONNECT; + } + gnutls_session_set_ptr(ctx->tls->session, ctx); + ngtcp2_conn_set_tls_native_handle(ctx->conn, ctx->tls->session); + + struct pollfd pfd = { + .fd = sockfd, + .events = POLLIN, + .revents = 0, + }; + ctx->tls->sockfd = sockfd; + + while (ctx->state != CONNECTED) { + ret = quic_send(ctx, sockfd, dst_addr->ai_family); + if (ret != KNOT_EOK) { + return ret; + } + + int timeout = get_expiry(ctx->conn); + ret = poll(&pfd, 1, timeout); + if (ret == 0) { + ret = ngtcp2_conn_handle_expiry(ctx->conn, quic_timestamp()); + } else if (ret < 0) { + return knot_map_errno(); + } + + ret = quic_recv(ctx, sockfd); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +static int offset_span(ngtcp2_vec **vec, size_t *veclen, size_t sub) +{ + ngtcp2_vec *new_vec = *vec; + size_t new_veclen = *veclen; + + while (sub) { + if (new_veclen == 0) { + return KNOT_EINVAL; + } + size_t part = MIN(sub, new_vec->len); + new_vec->base += part; + new_vec->len -= part; + sub -= part; + const int empty = ((new_vec->len == 0) ? 1 : 0); + new_vec += empty; + new_veclen -= empty; + } + *vec = new_vec; + *veclen = new_veclen; + + return KNOT_EOK; +} + +int quic_send_dns_query(quic_ctx_t *ctx, int sockfd, struct addrinfo *srv, + const uint8_t *buf, const size_t buf_len) +{ + if (ctx == NULL || buf == NULL) { + return KNOT_EINVAL; + } + + if (ctx->state < CONNECTED) { + return KNOT_ECONN; + } + + uint16_t query_length = htons(buf_len); + ngtcp2_vec datav[] = { + {(uint8_t *)&query_length, sizeof(uint16_t)}, + {(uint8_t *)buf, buf_len} + }; + size_t datavlen = sizeof(datav) / sizeof(*datav); + ngtcp2_vec *pdatav = datav; + + struct pollfd pfd = { + .fd = sockfd, + .events = POLLIN, + .revents = 0, + }; + + assert(ctx->stream.id < 0); + int ret = quic_open_bidi_stream(ctx); + if (ret != KNOT_EOK) { + return ret; + } + + ctx->stream.out_ack = 0; + for (ngtcp2_vec *it = datav; it < datav + datavlen; ++it) { + ctx->stream.out_ack += it->len; + } + + while (ctx->stream.out_ack > 0) { + ret = quic_send_data(ctx, sockfd, srv->ai_family, pdatav, datavlen); + if (ret < 0) { + WARN("QUIC, failed to send"); + return ret; + } else if (ret > 0) { + ret = offset_span(&pdatav, &datavlen, ret); + if (ret != KNOT_EOK) { + return ret; + } + } + + int timeout = get_expiry(ctx->conn); + if (timeout > 0 && datavlen > 0) { + continue; + } + ret = poll(&pfd, 1, timeout); + if (ret < 0) { + WARN("QUIC, failed to send"); + return knot_map_errno(); + } else if (ret == 0) { + ret = ngtcp2_conn_handle_expiry(ctx->conn, quic_timestamp()); + continue; + } + ret = quic_recv(ctx, sockfd); + if (ret != KNOT_EOK) { + WARN("QUIC, failed to send"); + return ret; + } + } + + return KNOT_EOK; +} + +int quic_recv_dns_response(quic_ctx_t *ctx, uint8_t *buf, const size_t buf_len, + struct addrinfo *srv) +{ + if (ctx == NULL || ctx->tls == NULL || buf == NULL) { + return KNOT_EINVAL; + } + + int ret = quic_respcpy(ctx, buf, buf_len); + if (ret != 0) { + return ret; + } else if (ctx->stream.id < 0) { + return KNOT_NET_ERECV; + } + + int sockfd = ctx->tls->sockfd; + + struct pollfd pfd = { + .fd = sockfd, + .events = POLLIN, + .revents = 0, + }; + + while (1) { + int timeout = get_expiry(ctx->conn); + ret = poll(&pfd, 1, timeout); + if (ret < 0) { + WARN("QUIC, failed to receive reply (%s)", + knot_strerror(errno)); + return knot_map_errno(); + } else if (ret == 0) { + ret = ngtcp2_conn_handle_expiry(ctx->conn, quic_timestamp()); + WARN("QUIC, peer took too long to respond"); + goto send; + } + + ret = quic_recv(ctx, sockfd); + if (ret != KNOT_EOK) { + WARN("QUIC, failed to receive reply (%s)", + knot_strerror(ret)); + return ret; + } + ret = quic_respcpy(ctx, buf, buf_len); + if (ret != 0) { + if (ret < 0) { + WARN("QUIC, failed to receive reply (%s)", + knot_strerror(ret)); + } + return ret; + } else if (ctx->stream.id < 0) { + return KNOT_NET_ERECV; + } + + send: + ret = quic_send(ctx, sockfd, srv->ai_family); + if (ret != KNOT_EOK) { + WARN("QUIC, failed to receive reply (%s)", + knot_strerror(ret)); + return ret; + } + } + + WARN("QUIC, peer took too long to respond"); + const uint8_t msg[] = "Connection timeout"; + set_application_error(ctx, DOQ_REQUEST_CANCELLED, msg, sizeof(msg) - 1); + + return KNOT_NET_ETIMEOUT; +} + +#define quic_ctx_write_close(ctx, dest, dest_len, ts) \ + ngtcp2_conn_write_connection_close((ctx)->conn, (ngtcp2_path *)ngtcp2_conn_get_path((ctx)->conn), \ + &(ctx)->pi, dest, dest_len, &(ctx)->last_err, ts) + +void quic_ctx_close(quic_ctx_t *ctx) +{ + if (ctx == NULL || ctx->state == CLOSED) { + return; + } + + uint8_t enc_buf[MAX_PACKET_SIZE]; + struct iovec msg_iov = { + .iov_base = enc_buf, + .iov_len = 0 + }; + struct msghdr msg = { + .msg_iov = &msg_iov, + .msg_iovlen = 1 + }; + + ngtcp2_ssize nwrite = quic_ctx_write_close(ctx, enc_buf, sizeof(enc_buf), + quic_timestamp()); + if (nwrite <= 0) { + return; + } + + msg_iov.iov_len = nwrite; + + struct sockaddr_in6 si = { 0 }; + socklen_t si_len = sizeof(si); + if (getsockname(ctx->tls->sockfd, (struct sockaddr *)&si, &si_len) == 0) { + (void)net_ecn_set(ctx->tls->sockfd, si.sin6_family, ctx->pi.ecn); + } + + (void)sendmsg(ctx->tls->sockfd, &msg, 0); + ctx->state = CLOSED; +} + +void quic_ctx_deinit(quic_ctx_t *ctx) +{ + if (ctx == NULL) { + return; + } + + if (ctx->conn) { + ngtcp2_conn_del(ctx->conn); + ctx->conn = NULL; + } + + if (ctx->stream.in_buffer.iov_base != NULL) { + free(ctx->stream.in_buffer.iov_base); + ctx->stream.in_buffer.iov_base = NULL; + } + + while (ctx->stream.in_parsed != NULL) { + knot_tcp_inbufs_upd_res_t *tofree = ctx->stream.in_parsed; + ctx->stream.in_parsed = tofree->next; + free(tofree); + } +} + +void print_quic(const quic_ctx_t *ctx) +{ + if (ctx == NULL || !ctx->params.enable || ctx->tls->session == NULL) { + return; + } + + char *msg = gnutls_session_get_desc(ctx->tls->session); + printf(";; QUIC session (QUICv%d)-%s\n", ngtcp2_conn_get_negotiated_version(ctx->conn), msg); + gnutls_free(msg); +} + +#endif diff --git a/src/utils/common/quic.h b/src/utils/common/quic.h new file mode 100644 index 0000000..fd70d27 --- /dev/null +++ b/src/utils/common/quic.h @@ -0,0 +1,118 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdbool.h> + +/*! \brief QUIC parameters. */ +typedef struct { + /*! Use QUIC indicator. */ + bool enable; +} quic_params_t; + +int quic_params_copy(quic_params_t *dst, const quic_params_t *src); + +void quic_params_clean(quic_params_t *params); + +#ifdef ENABLE_QUIC + +#include <ngtcp2/ngtcp2.h> +#include <ngtcp2/ngtcp2_crypto.h> + +#include "utils/common/tls.h" + +#define QUIC_DEFAULT_VERSION "-VERS-ALL:+VERS-TLS1.3" +#define QUIC_DEFAULT_GROUPS "-GROUP-ALL:+GROUP-X25519:+GROUP-SECP256R1:+GROUP-SECP384R1:+GROUP-SECP521R1" +#define QUIC_PRIORITY "%DISABLE_TLS13_COMPAT_MODE:NORMAL:"QUIC_DEFAULT_VERSION":"QUIC_DEFAULT_GROUPS + +typedef enum { + CLOSED, // Initialized + CONNECTED, // RTT-0 + VERIFIED, // RTT-1 +} quic_state_t; + +typedef enum { + /*! No error. This is used when the connection or stream needs to be + closed, but there is no error to signal. */ + DOQ_NO_ERROR = 0x0, + /*! The DoQ implementation encountered an internal error and is + incapable of pursuing the transaction or the connection. */ + DOQ_INTERNAL_ERROR = 0x1, + /*! The DoQ implementation encountered a protocol error and is forcibly + aborting the connection. */ + DOQ_PROTOCOL_ERROR = 0x2, + /*! A DoQ client uses this to signal that it wants to cancel an + outstanding transaction. */ + DOQ_REQUEST_CANCELLED = 0x3, + /*! A DoQ implementation uses this to signal when closing a connection + due to excessive load. */ + DOQ_EXCESSIVE_LOAD = 0x4, + /*! A DoQ implementation uses this in the absence of a more specific + error code. */ + DOQ_UNSPECIFIED_ERROR = 0x5, + /*! Alternative error code used for tests. */ + DOQ_ERROR_RESERVED = 0xd098ea5e +} quic_doq_error_t; + +typedef struct { + ngtcp2_crypto_conn_ref conn_ref; + // Parameters + quic_params_t params; + + // Context + ngtcp2_settings settings; + struct { + int64_t id; + uint64_t out_ack; + struct iovec in_buffer; + struct knot_tcp_inbufs_upd_res *in_parsed; + size_t in_parsed_it; + size_t in_parsed_total; + } stream; + ngtcp2_ccerr last_err; + uint8_t secret[32]; + tls_ctx_t *tls; + ngtcp2_conn *conn; + ngtcp2_pkt_info pi; + quic_state_t state; +} quic_ctx_t; + +extern const gnutls_datum_t doq_alpn; + +uint64_t quic_timestamp(void); + +int quic_generate_secret(uint8_t *buf, size_t buflen); + +uint32_t quic_get_ecn(struct msghdr *msg, const int family); + +int quic_ctx_init(quic_ctx_t *ctx, tls_ctx_t *tls_ctx, const quic_params_t *params); + +int quic_ctx_connect(quic_ctx_t *ctx, int sockfd, struct addrinfo *dst_addr); + +int quic_send_dns_query(quic_ctx_t *ctx, int sockfd, struct addrinfo *srv, + const uint8_t *buf, const size_t buf_len); + +int quic_recv_dns_response(quic_ctx_t *ctx, uint8_t *buf, const size_t buf_len, + struct addrinfo *srv); + +void quic_ctx_close(quic_ctx_t *ctx); + +void quic_ctx_deinit(quic_ctx_t *ctx); + +void print_quic(const quic_ctx_t *ctx); + +#endif //ENABLE_QUIC diff --git a/src/utils/common/resolv.c b/src/utils/common/resolv.c new file mode 100644 index 0000000..674a760 --- /dev/null +++ b/src/utils/common/resolv.c @@ -0,0 +1,211 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> + +#include "utils/common/resolv.h" +#include "utils/common/msg.h" +#include "utils/common/params.h" +#include "libknot/libknot.h" +#include "contrib/ucw/lists.h" + +#define RESOLV_FILE "/etc/resolv.conf" + +srv_info_t* parse_nameserver(const char *str, const char *def_port) +{ + char *host = NULL, *port = NULL; + const char *addr = NULL, *sep = NULL; + size_t addr_len = 0; + char separator = ':'; + + if (str == NULL || def_port == NULL) { + DBG_NULL; + return NULL; + } + + const size_t str_len = strlen(str); + const char *str_end = str + str_len; + + // UNIX socket path. + if (*str == '/') { + return srv_info_create(str, "UNIX"); + // [address]:port notation. + } else if (*str == '[') { + addr = str + 1; + const char *addr_end = strchr(addr, ']'); + // Missing closing bracket -> stop processing. + if (addr_end == NULL) { + return NULL; + } + addr_len = addr_end - addr; + str += 1 + addr_len + 1; + // Address@port notation. + } else if ((sep = strchr(str, '@')) != NULL) { + addr = str; + addr_len = sep - addr; + str += addr_len; + separator = '@'; + // Address#port notation. + } else if ((sep = strchr(str, '#')) != NULL) { + addr = str; + addr_len = sep - addr; + str += addr_len; + separator = '#'; + // IPv4:port notation. + } else if ((sep = strchr(str, ':')) != NULL) { + addr = str; + // Not IPv4 address -> no port. + if (strchr(sep + 1, ':') != NULL) { + addr_len = str_len; + str = str_end; + } else { + addr_len = sep - addr; + str += addr_len; + } + // No port specified. + } else { + addr = str; + addr_len = str_len; + str = str_end; + } + + // Process port. + if (str < str_end) { + // Check port separator. + if (*str != separator) { + return NULL; + } + str++; + + // Check for missing port. + if (str >= str_end) { + return NULL; + } + + port = strdup(str); + } else { + port = strdup(def_port); + } + + host = strndup(addr, addr_len); + + // Create server structure. + srv_info_t *server = srv_info_create(host, port); + + free(host); + free(port); + + return server; +} + +static size_t get_resolv_nameservers(list_t *servers, const char *def_port) +{ + char line[512]; + + // Open config file. + FILE *f = fopen(RESOLV_FILE, "r"); + if (f == NULL) { + return 0; + } + + // Read lines from config file. + while (fgets(line, sizeof(line), f) != NULL) { + size_t len; + char *pos = line; + char *option, *value; + + // Find leading white characters. + len = strspn(pos, SEP_CHARS); + pos += len; + + // Start of the first token. + option = pos; + + // Find length of the token. + len = strcspn(pos, SEP_CHARS); + pos += len; + + // Check if the token is not empty. + if (len == 0) { + continue; + } + + // Find separating white characters. + len = strspn(pos, SEP_CHARS); + pos += len; + + // Check if there is a separation between tokens. + if (len == 0) { + continue; + } + + // Copy of the second token. + value = strndup(pos, strcspn(pos, SEP_CHARS)); + + // Process value with respect to option name. + if (strncmp(option, "nameserver", strlen("nameserver")) == 0) { + srv_info_t *server; + + server = parse_nameserver(value, def_port); + + // If value is correct, add nameserver to the list. + if (server != NULL) { + add_tail(servers, (node_t *)server); + } + } + + // Drop value string. + free(value); + } + + // Close config file. + fclose(f); + + // Return number of servers. + return list_size(servers); +} + +void get_nameservers(list_t *servers, const char *def_port) +{ + if (servers == NULL || def_port == NULL) { + DBG_NULL; + return; + } + + // Initialize list of servers. + init_list(servers); + + // Read nameservers from resolv file or use the default ones. + if (get_resolv_nameservers(servers, def_port) == 0) { + srv_info_t *server; + + // Add default ipv6 nameservers. + server = srv_info_create(DEFAULT_IPV6_NAME, def_port); + + if (server != NULL) { + add_tail(servers, (node_t *)server); + } + + // Add default ipv4 nameservers. + server = srv_info_create(DEFAULT_IPV4_NAME, def_port); + + if (server != NULL) { + add_tail(servers, (node_t *)server); + } + } +} diff --git a/src/utils/common/resolv.h b/src/utils/common/resolv.h new file mode 100644 index 0000000..fb751d1 --- /dev/null +++ b/src/utils/common/resolv.h @@ -0,0 +1,24 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "utils/common/netio.h" +#include "contrib/ucw/lists.h" + +srv_info_t* parse_nameserver(const char *str, const char *def_port); + +void get_nameservers(list_t *servers, const char *def_port); diff --git a/src/utils/common/sign.c b/src/utils/common/sign.c new file mode 100644 index 0000000..84284d3 --- /dev/null +++ b/src/utils/common/sign.c @@ -0,0 +1,109 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <string.h> + +#include "utils/common/sign.h" +#include "libknot/errcode.h" +#include "libknot/tsig-op.h" + +int sign_context_init_tsig(sign_context_t *ctx, const knot_tsig_key_t *key) +{ + if (!ctx || !key) { + return KNOT_EINVAL; + } + + size_t digest_size = dnssec_tsig_algorithm_size(key->algorithm); + if (digest_size == 0) { + return KNOT_EINVAL; + } + + uint8_t *digest = calloc(1, digest_size); + if (!digest) { + return KNOT_ENOMEM; + } + + ctx->digest_size = digest_size; + ctx->digest = digest; + ctx->tsig_key = key; + + return KNOT_EOK; +} + +void sign_context_deinit(sign_context_t *ctx) +{ + if (!ctx) { + return; + } + + free(ctx->digest); + + memset(ctx, 0, sizeof(*ctx)); +} + +int sign_packet(knot_pkt_t *pkt, sign_context_t *sign_ctx) +{ + if (pkt == NULL || sign_ctx == NULL || sign_ctx->digest == NULL) { + return KNOT_EINVAL; + } + + uint8_t *wire = pkt->wire; + size_t *wire_size = &pkt->size; + size_t max_size = pkt->max_size; + + int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(sign_ctx->tsig_key)); + if (ret != KNOT_EOK) { + return ret; + } + + return knot_tsig_sign(wire, wire_size, max_size, NULL, 0, + sign_ctx->digest, &sign_ctx->digest_size, + sign_ctx->tsig_key, 0, 0); +} + +int verify_packet(const knot_pkt_t *pkt, const sign_context_t *sign_ctx) +{ + if (pkt == NULL || sign_ctx == NULL || sign_ctx->digest == NULL) { + return KNOT_EINVAL; + } + + const uint8_t *wire = pkt->wire; + const size_t *wire_size = &pkt->size; + + if (pkt->tsig_rr == NULL) { + return KNOT_ENOTSIG; + } + + int ret = knot_tsig_client_check(pkt->tsig_rr, wire, *wire_size, + sign_ctx->digest, sign_ctx->digest_size, + sign_ctx->tsig_key, 0); + if (ret != KNOT_EOK) { + return ret; + } + + switch (knot_tsig_rdata_error(pkt->tsig_rr)) { + case KNOT_RCODE_BADSIG: + return KNOT_TSIG_EBADSIG; + case KNOT_RCODE_BADKEY: + return KNOT_TSIG_EBADKEY; + case KNOT_RCODE_BADTIME: + return KNOT_TSIG_EBADTIME; + case KNOT_RCODE_BADTRUNC: + return KNOT_TSIG_EBADTRUNC; + default: + return KNOT_EOK; + } +} diff --git a/src/utils/common/sign.h b/src/utils/common/sign.h new file mode 100644 index 0000000..52f41ef --- /dev/null +++ b/src/utils/common/sign.h @@ -0,0 +1,63 @@ +/* Copyright (C) 2015 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/packet/pkt.h" +#include "libknot/tsig.h" + +/*! + * \brief Holds data required between signing and signature verification. + */ +struct sign_context { + size_t digest_size; + uint8_t *digest; + const knot_tsig_key_t *tsig_key; +}; + +typedef struct sign_context sign_context_t; + +/*! + * \brief Initialize signing context for TSIG. + */ +int sign_context_init_tsig(sign_context_t *ctx, const knot_tsig_key_t *key); + +/*! + * \brief Clean up signing context. + * + * \param ctx Sign context. + */ +void sign_context_deinit(sign_context_t *ctx); + +/*! + * \brief Signs outgoing DNS packet. + * + * \param pkt Packet to sign. + * \param sign_ctx Signing context. + * + * \return Error code, KNOT_EOK if successful. + */ +int sign_packet(knot_pkt_t *pkt, sign_context_t *sign_ctx); + +/*! + * \brief Verifies signature for incoming DNS packet. + * + * \param pkt Packet verify sign. + * \param sign_ctx Signing context. + * + * \return Error code, KNOT_EOK if successful. + */ +int verify_packet(const knot_pkt_t *pkt, const sign_context_t *sign_ctx); diff --git a/src/utils/common/signal.c b/src/utils/common/signal.c new file mode 100644 index 0000000..953fb12 --- /dev/null +++ b/src/utils/common/signal.c @@ -0,0 +1,66 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <signal.h> +#include <stdio.h> + +#include "utils/common/signal.h" + +#include "contrib/color.h" +#include "knot/conf/base.h" + +extern signal_ctx_t signal_ctx; // It must be defined as global in each utility. +int SIGNAL_REPEAT = 1; + +static void signal_handler(int signum) +{ + // Allow a repeated signal during the handler run (in case + // the handler gets stuck). + sigset_t set; + (void)sigaddset(&set, signum); + (void)sigprocmask(SIG_UNBLOCK, &set, NULL); + + if (--SIGNAL_REPEAT < 0) { + abort(); + } + + (void)printf("%s%s\n", COL_RST(signal_ctx.color), + signum == SIGINT ? "" : strsignal(signum)); + + conf_t *config = conf(); + if (config != NULL && config->api != NULL) { + config->api->deinit(config->db); + } + + if (signal_ctx.close_db != NULL) { + knot_lmdb_close(signal_ctx.close_db); + } + + exit(EXIT_FAILURE); +} + +void signal_init_std(void) +{ + struct sigaction sigact = { .sa_handler = signal_handler }; + + sigaction(SIGHUP, &sigact, NULL); + sigaction(SIGINT, &sigact, NULL); + sigaction(SIGPIPE, &sigact, NULL); + sigaction(SIGALRM, &sigact, NULL); + sigaction(SIGTERM, &sigact, NULL); + sigaction(SIGUSR1, &sigact, NULL); + sigaction(SIGUSR2, &sigact, NULL); +} diff --git a/src/utils/common/signal.h b/src/utils/common/signal.h new file mode 100644 index 0000000..0955c9d --- /dev/null +++ b/src/utils/common/signal.h @@ -0,0 +1,37 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/journal/knot_lmdb.h" + +/*! + * \brief Data passed to the signal handler. + */ +typedef struct { + knot_lmdb_db_t *close_db; // LMDB database to be closed + bool color; // do a terminal color reset +} signal_ctx_t; + +/*! + * \brief Prepares a signal handler for a clean shutdown. + * + * \note Configures common break signals to initiate close of confdb + * and of another LMDB database defined by the global variable + * signal_ctx_t signal_ctx. If set to NULL, only confdb + * is closed. + */ +void signal_init_std(void); diff --git a/src/utils/common/tls.c b/src/utils/common/tls.c new file mode 100644 index 0000000..276ae16 --- /dev/null +++ b/src/utils/common/tls.c @@ -0,0 +1,736 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <arpa/inet.h> +#include <stdbool.h> +#include <string.h> +#include <poll.h> +#include <stdint.h> +#include <stdlib.h> +#include <gnutls/gnutls.h> +#include <gnutls/ocsp.h> +#include <gnutls/x509.h> +#define GNUTLS_VERSION_FASTOPEN_READY 0x030503 +#if GNUTLS_VERSION_NUMBER >= GNUTLS_VERSION_FASTOPEN_READY +#include <gnutls/socket.h> +#endif + +#include "utils/common/tls.h" +#include "utils/common/msg.h" +#include "contrib/base64.h" +#include "libknot/errcode.h" + +const gnutls_datum_t dot_alpn = { + (unsigned char *)"dot", 3 +}; + +void tls_params_init(tls_params_t *params) +{ + if (params == NULL) { + return; + } + + memset(params, 0, sizeof(*params)); + + init_list(¶ms->ca_files); + init_list(¶ms->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(¶ms->ca_files, NULL); + + WALK_LIST_DELSAFE(node, nxt, params->pins) { + free(node->d); + } + ptrlist_free(¶ms->pins, NULL); + + free(params->hostname); + free(params->sni); + free(params->keyfile); + free(params->certfile); + + memset(params, 0, sizeof(*params)); +} + +static bool check_pin(const uint8_t *cert_pin, size_t cert_pin_len, const list_t *pins) +{ + if (EMPTY_LIST(*pins)) { + return false; + } + + ptrnode_t *n; + WALK_LIST(n, *pins) { + uint8_t *pin = (uint8_t *)n->d; + if (pin[0] == cert_pin_len && + memcmp(cert_pin, &pin[1], cert_pin_len) == 0) { + return true; + } + } + + return false; +} + +static bool verify_ocsp(gnutls_session_t *session) +{ + bool ret = false; + + gnutls_ocsp_resp_t ocsp_resp; + bool deinit_ocsp_resp = false; + + gnutls_x509_crt_t server_cert; + bool deinit_server_cert = false; + + gnutls_certificate_credentials_t xcred; + bool deinit_xcreds = false; + + gnutls_x509_crt_t issuer_cert; + bool deinit_issuer_cert = false; + + gnutls_datum_t ocsp_resp_raw; + if (gnutls_ocsp_status_request_get(*session, &ocsp_resp_raw) != GNUTLS_E_SUCCESS) { + WARN("TLS, unable to retrieve stapled OCSP data"); + goto cleanup; + } + if (gnutls_ocsp_resp_init(&ocsp_resp) != GNUTLS_E_SUCCESS) { + WARN("TLS, unable to init OCSP data"); + goto cleanup; + } + deinit_ocsp_resp = true; + if (gnutls_ocsp_resp_import(ocsp_resp, &ocsp_resp_raw) != GNUTLS_E_SUCCESS) { + WARN("TLS, unable to import OCSP response"); + goto cleanup; + } + + unsigned int cert_list_size = 0; + const gnutls_datum_t *cert_list = gnutls_certificate_get_peers(*session, &cert_list_size); + if (cert_list_size == 0) { + WARN("TLS, unable to retrieve peer certs when verifying OCSP"); + goto cleanup; + } + if (gnutls_x509_crt_init(&server_cert) != GNUTLS_E_SUCCESS) { + WARN("TLS, unable to init server cert when verifying OCSP"); + goto cleanup; + } + deinit_server_cert = true; + if (gnutls_x509_crt_import(server_cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { + WARN("TLS, unable to import server cert when verifying OCSP"); + goto cleanup; + } + + if (gnutls_certificate_allocate_credentials(&xcred) != GNUTLS_E_SUCCESS) { + WARN("TLS, unable to allocate credentials when verifying OCSP"); + goto cleanup; + } + deinit_xcreds = true; + + if (gnutls_certificate_get_issuer(xcred, server_cert, &issuer_cert, 0) != GNUTLS_E_SUCCESS) { + if (cert_list_size < 2) { + WARN("TLS, unable to get issuer (CA) cert when verifying OCSP"); + goto cleanup; + } + if (gnutls_x509_crt_init(&issuer_cert) != GNUTLS_E_SUCCESS) { + WARN("TLS, unable to init issuer cert when verifying OCSP"); + goto cleanup; + } + deinit_issuer_cert = true; + if (gnutls_x509_crt_import(issuer_cert, &cert_list[1], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { + WARN("TLS, unable to import issuer cert when verifying OCSP"); + goto cleanup; + } + } + + unsigned int status; + time_t this_upd, next_upd, now = time(0); + if (gnutls_ocsp_resp_check_crt(ocsp_resp, 0, server_cert) != GNUTLS_E_SUCCESS) { + WARN("TLS, OCSP response either empty or not for provided server cert"); + goto cleanup; + } + if (gnutls_ocsp_resp_verify_direct(ocsp_resp, issuer_cert, &status, 0) != GNUTLS_E_SUCCESS) { + WARN("TLS, unable to verify OCSP response against issuer cert"); + goto cleanup; + } + if (status != 0) { + WARN("TLS, got a non-zero status when verifying OCSP response against issuer cert"); + goto cleanup; + } + if (gnutls_ocsp_resp_get_single(ocsp_resp, 0, NULL, NULL, NULL, NULL, &status, + &this_upd, &next_upd, NULL, NULL) != GNUTLS_E_SUCCESS) { + WARN("TLS, error reading OCSP response"); + goto cleanup; + } + if (status == GNUTLS_OCSP_CERT_REVOKED) { + WARN("TLS, OCSP data shows that cert was revoked"); + goto cleanup; + } + if (next_upd == -1) { + tls_ctx_t *ctx = gnutls_session_get_ptr(*session); + assert(now >= this_upd); + assert(ctx->params->ocsp_stapling > 0); + if (now - this_upd > ctx->params->ocsp_stapling) { + WARN("TLS, OCSP response is out of date."); + goto cleanup; + } + } else { + if (next_upd < now) { + WARN("TLS, a newer OCSP response is available but was not sent"); + goto cleanup; + } + } + + // Only if we get here is the ocsp result completely valid. + ret = true; + +cleanup: + if (deinit_issuer_cert) { + gnutls_x509_crt_deinit(issuer_cert); + } + if (deinit_xcreds) { + gnutls_certificate_free_credentials(xcred); + } + if (deinit_server_cert) { + gnutls_x509_crt_deinit(server_cert); + } + if (deinit_ocsp_resp) { + gnutls_ocsp_resp_deinit(ocsp_resp); + } + + return ret; +} + +static int check_certificates(gnutls_session_t session, const list_t *pins) +{ + if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) { + DBG("TLS, invalid certificate type"); + return GNUTLS_E_CERTIFICATE_ERROR; + } + + unsigned cert_list_size; + const gnutls_datum_t *cert_list = + gnutls_certificate_get_peers(session, &cert_list_size); + if (cert_list == NULL || cert_list_size == 0) { + DBG("TLS, empty certificate list"); + return GNUTLS_E_CERTIFICATE_ERROR; + } + + size_t matches = 0; + + DBG("TLS, received certificate hierarchy:"); + for (int i = 0; i < cert_list_size; i++) { + gnutls_x509_crt_t cert; + int ret = gnutls_x509_crt_init(&cert); + if (ret != GNUTLS_E_SUCCESS) { + return ret; + } + + ret = gnutls_x509_crt_import(cert, &cert_list[i], GNUTLS_X509_FMT_DER); + if (ret != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + return ret; + } + + gnutls_datum_t cert_name = { 0 }; + ret = gnutls_x509_crt_get_dn2(cert, &cert_name); + if (ret != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + return ret; + } + DBG(" #%i, %s", i + 1, cert_name.data); + gnutls_free(cert_name.data); + + uint8_t cert_pin[CERT_PIN_LEN] = { 0 }; + size_t cert_pin_size = sizeof(cert_pin); + ret = gnutls_x509_crt_get_key_id(cert, GNUTLS_KEYID_USE_SHA256, + cert_pin, &cert_pin_size); + if (ret != 0) { + gnutls_x509_crt_deinit(cert); + return ret; + } + + // Check if correspond to a specified PIN. + bool match = check_pin(cert_pin, sizeof(cert_pin), pins); + if (match) { + matches++; + } + + uint8_t *txt_pin; + ret = knot_base64_encode_alloc(cert_pin, sizeof(cert_pin), &txt_pin); + if (ret < 0) { + gnutls_x509_crt_deinit(cert); + return ret; + } + DBG(" SHA-256 PIN: %.*s%s", ret, txt_pin, match ? ", MATCH" : ""); + free(txt_pin); + + gnutls_x509_crt_deinit(cert); + } + + if (matches > 0) { + return GNUTLS_E_SUCCESS; + } else if (EMPTY_LIST(*pins)) { + DBG("TLS, skipping certificate PIN check"); + return GNUTLS_E_SUCCESS; + } else { + DBG("TLS, no certificate PIN match"); + return GNUTLS_E_CERTIFICATE_ERROR; + } +} + +static bool do_verification(const tls_params_t *params) +{ + return params->hostname != NULL || params->system_ca || + !EMPTY_LIST(params->ca_files) || params->ocsp_stapling > 0; +} + +int tls_certificate_verification(tls_ctx_t *ctx) +{ + gnutls_session_t session = ctx->session; + // Check for pinned certificates and print certificate hierarchy. + int ret = check_certificates(session, &ctx->params->pins); + if (ret != GNUTLS_E_SUCCESS) { + return ret; + } + + if (!do_verification(ctx->params)) { + DBG("TLS, skipping certificate verification"); + return GNUTLS_E_SUCCESS; + } + + if (ctx->params->ocsp_stapling > 0 && !verify_ocsp(&session)) { + WARN("TLS, failed to validate required OCSP data"); + return GNUTLS_E_CERTIFICATE_ERROR; + } + + // Set server certificate check. + gnutls_typed_vdata_st data[2] = { + { .type = GNUTLS_DT_KEY_PURPOSE_OID, + .data = (void *)GNUTLS_KP_TLS_WWW_SERVER }, + { .type = GNUTLS_DT_DNS_HOSTNAME, + .data = (void *)ctx->params->hostname } + }; + size_t data_count = (ctx->params->hostname != NULL) ? 2 : 1; + if (data_count == 1) { + WARN("TLS, no hostname provided, will not verify certificate owner") + } + + unsigned int status; + ret = gnutls_certificate_verify_peers(session, data, data_count, &status); + if (ret != GNUTLS_E_SUCCESS) { + WARN("TLS, failed to verify peer certificate"); + return GNUTLS_E_CERTIFICATE_ERROR; + } + + gnutls_datum_t msg; + ret = gnutls_certificate_verification_status_print( + status, gnutls_certificate_type_get(session), &msg, 0); + if (ret == GNUTLS_E_SUCCESS) { + DBG("TLS, %s", msg.data); + } + gnutls_free(msg.data); + + if (status != 0) { + return GNUTLS_E_CERTIFICATE_ERROR; + } + + return GNUTLS_E_SUCCESS; +} + +static int verify_certificate(gnutls_session_t session) +{ + tls_ctx_t *ctx = gnutls_session_get_ptr(session); + return tls_certificate_verification(ctx); +} + +int tls_ctx_init(tls_ctx_t *ctx, const tls_params_t *params, + unsigned int flags, int wait) + +{ + if (ctx == NULL || params == NULL || !params->enable) { + return KNOT_EINVAL; + } + + memset(ctx, 0, sizeof(*ctx)); + ctx->params = params; + ctx->wait = wait; + ctx->sockfd = -1; + + int ret = gnutls_certificate_allocate_credentials(&ctx->credentials); + if (ret != GNUTLS_E_SUCCESS) { + return KNOT_ENOMEM; + } + + // Import system certificates. + if (ctx->params->system_ca || + (ctx->params->hostname != NULL && EMPTY_LIST(ctx->params->ca_files))) { + ret = gnutls_certificate_set_x509_system_trust(ctx->credentials); + if (ret < 0) { + WARN("TLS, failed to import system certificates (%s)", + gnutls_strerror_name(ret)); + return KNOT_ERROR; + } else { + DBG("TLS, imported %i system certificates", ret); + } + } + + // Import provided certificate files. + ptrnode_t *n; + WALK_LIST(n, ctx->params->ca_files) { + const char *file = (char *)n->d; + ret = gnutls_certificate_set_x509_trust_file(ctx->credentials, file, + GNUTLS_X509_FMT_PEM); + if (ret < 0) { + WARN("TLS, failed to import certificate file '%s' (%s)", + file, gnutls_strerror_name(ret)); + return KNOT_ERROR; + } else { + DBG("TLS, imported %i certificates from '%s'", ret, file); + } + } + + gnutls_certificate_set_verify_function(ctx->credentials, verify_certificate); + + // Setup client keypair if specified. Both key and cert files must be provided. + if (params->keyfile != NULL && params->certfile != NULL) { + // First, try PEM. + ret = gnutls_certificate_set_x509_key_file(ctx->credentials, + params->certfile, params->keyfile, GNUTLS_X509_FMT_PEM); + if (ret != GNUTLS_E_SUCCESS) { + // If PEM didn't work, try DER. + ret = gnutls_certificate_set_x509_key_file(ctx->credentials, + params->certfile, params->keyfile, GNUTLS_X509_FMT_DER); + } + + if (ret != GNUTLS_E_SUCCESS) { + WARN("TLS, failed to add client certfile '%s' and keyfile '%s'", + params->certfile, params->keyfile); + return KNOT_ERROR; + } else { + DBG("TLS, added client certfile '%s' and keyfile '%s'", + params->certfile, params->keyfile); + } + } else if (params->keyfile != NULL) { + WARN("TLS, cannot use client keyfile without a certfile"); + return KNOT_ERROR; + } else if (params->certfile != NULL) { + WARN("TLS, cannot use client certfile without a keyfile"); + return KNOT_ERROR; + } + + ret = gnutls_init(&ctx->session, GNUTLS_CLIENT | flags); + if (ret != GNUTLS_E_SUCCESS) { + return KNOT_ENOMEM; + } + + ret = gnutls_credentials_set(ctx->session, GNUTLS_CRD_CERTIFICATE, + ctx->credentials); + if (ret != GNUTLS_E_SUCCESS) { + return KNOT_ERROR; + } + + return KNOT_EOK; +} + +int tls_ctx_setup_remote_endpoint(tls_ctx_t *ctx, const gnutls_datum_t *alpn, + size_t alpn_size, const char *priority, const char *remote) +{ + if (ctx == NULL || ctx->session == NULL || ctx->credentials == NULL) { + return KNOT_EINVAL; + } + int ret = 0; + if (alpn != NULL) { + ret = gnutls_alpn_set_protocols(ctx->session, alpn, alpn_size, 0); + if (ret != GNUTLS_E_SUCCESS) { + return KNOT_NET_ECONNECT; + } + } + + if (priority != NULL) { + ret = gnutls_priority_set_direct(ctx->session, priority, NULL); + } else { + ret = gnutls_set_default_priority(ctx->session); + } + if (ret != GNUTLS_E_SUCCESS) { + return KNOT_EINVAL; + } + + if (remote != NULL) { + ret = gnutls_server_name_set(ctx->session, GNUTLS_NAME_DNS, remote, + strlen(remote)); + if (ret != GNUTLS_E_SUCCESS) { + return KNOT_EINVAL; + } + } + return KNOT_EOK; +} + +int tls_ctx_connect(tls_ctx_t *ctx, int sockfd, bool fastopen, + struct sockaddr_storage *addr) +{ + if (ctx == NULL) { + return KNOT_EINVAL; + } + + int ret = 0; + gnutls_session_set_ptr(ctx->session, ctx); + + if (fastopen) { +#if GNUTLS_VERSION_NUMBER >= GNUTLS_VERSION_FASTOPEN_READY + gnutls_transport_set_fastopen(ctx->session, sockfd, (struct sockaddr *)addr, + sockaddr_len(addr), 0); +#else + return KNOT_ENOTSUP; +#endif + } else { + gnutls_transport_set_int(ctx->session, sockfd); + } + + gnutls_handshake_set_timeout(ctx->session, 1000 * ctx->wait); + + // Initialize poll descriptor structure. + struct pollfd pfd = { + .fd = sockfd, + .events = POLLIN, + .revents = 0, + }; + + // Perform the TLS handshake + do { + ret = gnutls_handshake(ctx->session); + if (ret != GNUTLS_E_SUCCESS && gnutls_error_is_fatal(ret) == 0) { + if (poll(&pfd, 1, 1000 * ctx->wait) != 1) { + WARN("TLS, peer took too long to respond"); + return KNOT_NET_ETIMEOUT; + } + } + } while (ret != GNUTLS_E_SUCCESS && gnutls_error_is_fatal(ret) == 0); + if (ret != GNUTLS_E_SUCCESS) { + WARN("TLS, handshake failed (%s)", gnutls_strerror(ret)); + tls_ctx_close(ctx); + return KNOT_NET_ESOCKET; + } + + // Save the socket descriptor. + ctx->sockfd = sockfd; + + return KNOT_EOK; +} + +int tls_ctx_send(tls_ctx_t *ctx, const uint8_t *buf, const size_t buf_len) +{ + if (ctx == NULL || buf == NULL) { + return KNOT_EINVAL; + } + + uint16_t msg_len = htons(buf_len); + + gnutls_record_cork(ctx->session); + + if (gnutls_record_send(ctx->session, &msg_len, sizeof(msg_len)) <= 0) { + WARN("TLS, failed to send"); + return KNOT_NET_ESEND; + } + if (gnutls_record_send(ctx->session, buf, buf_len) <= 0) { + WARN("TLS, failed to send"); + return KNOT_NET_ESEND; + } + + while (gnutls_record_check_corked(ctx->session) > 0) { + int ret = gnutls_record_uncork(ctx->session, 0); + if (ret < 0 && gnutls_error_is_fatal(ret) != 0) { + WARN("TLS, failed to send (%s)", gnutls_strerror(ret)); + return KNOT_NET_ESEND; + } + } + + return KNOT_EOK; +} + +int tls_ctx_receive(tls_ctx_t *ctx, uint8_t *buf, const size_t buf_len) +{ + if (ctx == NULL || buf == NULL) { + return KNOT_EINVAL; + } + + // Initialize poll descriptor structure. + struct pollfd pfd = { + .fd = ctx->sockfd, + .events = POLLIN, + .revents = 0, + }; + + uint32_t total = 0; + uint16_t msg_len = 0; + + // Receive message header. + while (total < sizeof(msg_len)) { + ssize_t ret = gnutls_record_recv(ctx->session, + (uint8_t *)&msg_len + total, + sizeof(msg_len) - total); + if (ret > 0) { + total += ret; + } else if (ret == 0) { + WARN("TLS, peer has closed the connection"); + return KNOT_NET_ERECV; + } else if (gnutls_error_is_fatal(ret) != 0) { + WARN("TLS, failed to receive reply (%s)", + gnutls_strerror(ret)); + return KNOT_NET_ERECV; + } else if (poll(&pfd, 1, 1000 * ctx->wait) != 1) { + WARN("TLS, peer took too long to respond"); + return KNOT_NET_ETIMEOUT; + } + } + + // Convert number to host format. + msg_len = ntohs(msg_len); + if (msg_len > buf_len) { + return KNOT_ESPACE; + } + + total = 0; + + // Receive data over TLS + while (total < msg_len) { + ssize_t ret = gnutls_record_recv(ctx->session, buf + total, + msg_len - total); + if (ret > 0) { + total += ret; + } else if (ret == 0) { + WARN("TLS, peer has closed the connection"); + return KNOT_NET_ERECV; + } else if (gnutls_error_is_fatal(ret) != 0) { + WARN("TLS, failed to receive reply (%s)", + gnutls_strerror(ret)); + return KNOT_NET_ERECV; + } else if (poll(&pfd, 1, 1000 * ctx->wait) != 1) { + WARN("TLS, peer took too long to respond"); + return KNOT_NET_ETIMEOUT; + } + } + + return total; +} + +void tls_ctx_close(tls_ctx_t *ctx) +{ + if (ctx == NULL || ctx->session == NULL || ctx->sockfd < 0) { + return; + } + + gnutls_bye(ctx->session, GNUTLS_SHUT_RDWR); +} + +void tls_ctx_deinit(tls_ctx_t *ctx) +{ + if (ctx == NULL) { + return; + } + + if (ctx->credentials != NULL) { + gnutls_certificate_free_credentials(ctx->credentials); + ctx->credentials = NULL; + } + if (ctx->session != NULL) { + gnutls_deinit(ctx->session); + ctx->session = NULL; + } +} + +void print_tls(const tls_ctx_t *ctx) +{ + if (ctx == NULL || ctx->params == NULL || !ctx->params->enable || ctx->session == NULL) { + return; + } + + char *msg = gnutls_session_get_desc(ctx->session); + printf(";; TLS session %s\n", msg); + gnutls_free(msg); +} diff --git a/src/utils/common/tls.h b/src/utils/common/tls.h new file mode 100644 index 0000000..623db1d --- /dev/null +++ b/src/utils/common/tls.h @@ -0,0 +1,83 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> +#include <netdb.h> +#include <gnutls/gnutls.h> + +#include "contrib/sockaddr.h" +#include "contrib/ucw/lists.h" + +#define CERT_PIN_LEN 32 + +/*! \brief TLS parameters. */ +typedef struct { + /*! Use TLS indicator. */ + bool enable; + /*! Import system certificates indicator. */ + bool system_ca; + /*! Certificate files to import. */ + list_t ca_files; + /*! Pinned certificates. */ + list_t pins; + /*! Required server hostname. */ + char *hostname; + /*! Optional server name indicator. */ + char *sni; + /*! Optional client keyfile name. */ + char *keyfile; + /*! Optional client certfile name. */ + char *certfile; + /*! Optional validity of stapled OCSP response for the server cert. */ + uint32_t ocsp_stapling; +} tls_params_t; + +/*! \brief TLS context. */ +typedef struct { + /*! TLS handshake timeout. */ + int wait; + /*! Socket descriptor. */ + int sockfd; + /*! TLS parameters. */ + const tls_params_t *params; + /*! GnuTLS session handle. */ + gnutls_session_t session; + /*! GnuTLS credentials handle. */ + gnutls_certificate_credentials_t credentials; +} tls_ctx_t; + +extern const gnutls_datum_t dot_alpn; + +void tls_params_init(tls_params_t *params); +int tls_params_copy(tls_params_t *dst, const tls_params_t *src); +void tls_params_clean(tls_params_t *params); + +int tls_certificate_verification(tls_ctx_t *ctx); + +int tls_ctx_init(tls_ctx_t *ctx, const tls_params_t *params, + unsigned int flags, int wait); +int tls_ctx_setup_remote_endpoint(tls_ctx_t *ctx, const gnutls_datum_t *alpn, + size_t alpn_size, const char *priority, const char *remote); +int tls_ctx_connect(tls_ctx_t *ctx, int sockfd, + bool fastopen, struct sockaddr_storage *addr); + +int tls_ctx_send(tls_ctx_t *ctx, const uint8_t *buf, const size_t buf_len); +int tls_ctx_receive(tls_ctx_t *ctx, uint8_t *buf, const size_t buf_len); +void tls_ctx_close(tls_ctx_t *ctx); +void tls_ctx_deinit(tls_ctx_t *ctx); +void print_tls(const tls_ctx_t *ctx); diff --git a/src/utils/common/token.c b/src/utils/common/token.c new file mode 100644 index 0000000..10e788e --- /dev/null +++ b/src/utils/common/token.c @@ -0,0 +1,115 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "utils/common/token.h" +#include "utils/common/msg.h" +#include "libknot/libknot.h" +#include "contrib/ctype.h" + +int tok_scan(const char* lp, const char **tbl, int *lpm) +{ + if (lp == NULL || tbl == NULL || *tbl == NULL || lpm == NULL) { + DBG_NULL; + return -1; + } + + const char *prefix = lp; /* Ptr to line start. */ + int i = 0, pl = 1; /* Match index, prefix length. */ + unsigned char len = 0; /* Read length. */ + for(;;) { + const char *tok = tbl[i]; + if (*lp == '\0' || is_space(*lp)) { + if (tok && TOK_L(tok) == len) { /* Consumed whole w? */ + return i; /* Identifier */ + } else { /* Word is shorter than cmd? */ + break; + } + } + + /* Find next prefix match. */ + ++len; + while (tok) { + if (TOK_L(tok) >= len) { /* Is prefix of current token */ + if (*lp < tok[pl]) { /* Terminate early. */ + tok = NULL; + break; /* No match could be found. */ + } + if (*lp == tok[pl]) { /* Match */ + if(lpm) *lpm = i; + ++pl; + break; + } + } + + /* No early cut, no match - seek next. */ + while ((tok = tbl[++i]) != NULL) { + if (TOK_L(tok) >= len && + memcmp(TOK_S(tok), prefix, len) == 0) { + break; + } + } + } + + if (tok == NULL) { + break; /* All tokens exhausted. */ + } else { + ++lp; /* Next char */ + } + } + + return -1; +} + +int tok_find(const char *lp, const char **tbl) +{ + if (lp == NULL || tbl == NULL || *tbl == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + int lpm = -1; + int bp = 0; + if ((bp = tok_scan(lp, tbl, &lpm)) < 0) { + if (lpm > -1) { + ERR("unexpected literal: '%s', did you mean '%s' ?", + lp, TOK_S(tbl[lpm])); + } else { + ERR("unexpected literal: '%s'", lp); + } + + return KNOT_EPARSEFAIL; + } + + return bp; +} + +const char *tok_skipspace(const char *lp) +{ + if (lp == NULL) { + DBG_NULL; + return NULL; + } + + while (is_space(*lp)) { + lp += 1; + } + + return lp; +} diff --git a/src/utils/common/token.h b/src/utils/common/token.h new file mode 100644 index 0000000..fab4ea1 --- /dev/null +++ b/src/utils/common/token.h @@ -0,0 +1,65 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdio.h> + +/*! + * \brief Example of token table: + * + * \warning Table _must_ be lexicographically ordered. + * + * const char *tok_tbl[] = { + * // LEN STRING + * "\x4" "abcd", + * "\x5" "class", + * NULL // END + * } + */ +/*! \brief String part of the token. */ +#define TOK_S(x) ((x)+1) +/*! \brief Len of the token. */ +#define TOK_L(x) ((unsigned char)(x)[0]) + +/*! + * \brief Scan for matching token described by a match table. + * + * Table consists of strings, prefixed with 1B length. + * + * \param lp Pointer to current line. + * \param tbl Match description table. + * \param lpm Pointer to longest prefix match. + * \retval index to matching record. + * \retval -1 if no match is found, lpm may be set to longest prefix match. + */ +int tok_scan(const char* lp, const char **tbl, int *lpm); + +/*! + * \brief Find token from table in a line buffer. + * \param lp Pointer to current line. + * \param tbl Match description table. + * \retval index to matching record. + * \retval error code if no match is found + */ +int tok_find(const char *lp, const char **tbl); + +/*! + * \brief Return pointer to next non-blank character. + * \param lp Pointer to current line. + * \return ptr to next non-blank character. + */ +const char *tok_skipspace(const char *lp); diff --git a/src/utils/common/util_conf.c b/src/utils/common/util_conf.c new file mode 100644 index 0000000..a98f526 --- /dev/null +++ b/src/utils/common/util_conf.c @@ -0,0 +1,139 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <sys/stat.h> +#include <unistd.h> + +#include "utils/common/util_conf.h" + +#include "contrib/string.h" +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "libknot/attribute.h" +#include "utils/common/msg.h" + +bool util_conf_initialized(void) +{ + return (conf() != NULL); +} + +int util_conf_init_confdb(const char *confdb) +{ + if (util_conf_initialized()) { + ERR2("configuration already initialized"); + util_conf_deinit(); + return KNOT_ESEMCHECK; + } + + size_t max_conf_size = (size_t)CONF_MAPSIZE * 1024 * 1024; + + conf_flag_t flags = CONF_FNOHOSTNAME | CONF_FOPTMODULES; + if (confdb != NULL) { + flags |= CONF_FREADONLY; + } + + log_init(); + log_levels_set(LOG_TARGET_STDOUT, LOG_SOURCE_ANY, 0); + log_levels_set(LOG_TARGET_STDERR, LOG_SOURCE_ANY, LOG_UPTO(LOG_WARNING)); + log_levels_set(LOG_TARGET_SYSLOG, LOG_SOURCE_ANY, 0); + log_flag_set(LOG_FLAG_NOTIMESTAMP | LOG_FLAG_NOINFO); + + conf_t *new_conf = NULL; + int ret = conf_new(&new_conf, conf_schema, confdb, max_conf_size, flags); + if (ret != KNOT_EOK) { + ERR2("failed opening configuration database '%s' (%s)", + (confdb == NULL ? "" : confdb), knot_strerror(ret)); + } else { + conf_update(new_conf, CONF_UPD_FNONE); + } + return ret; +} + +int util_conf_init_file(const char *conffile) +{ + int ret = util_conf_init_confdb(NULL); + if (ret != KNOT_EOK) { + return ret; + } + + ret = conf_import(conf(), conffile, IMPORT_FILE); + if (ret != KNOT_EOK) { + ERR2("failed opening configuration file '%s' (%s)", + conffile, knot_strerror(ret)); + } + return ret; +} + +int util_conf_init_justdb(const char *db_type, const char *db_path) +{ + int ret = util_conf_init_confdb(NULL); + if (ret != KNOT_EOK) { + return ret; + } + + char *conf_str = sprintf_alloc("database:\n" + " storage: .\n" + " %s: \"%s\"\n", db_type, db_path); + if (conf_str == NULL) { + return KNOT_ENOMEM; + } + + ret = conf_import(conf(), conf_str, 0); + free(conf_str); + if (ret != KNOT_EOK) { + ERR2("failed creating temporary configuration (%s)", knot_strerror(ret)); + } + return ret; +} + +int util_conf_init_default(bool allow_db) +{ + struct stat st; + if (util_conf_initialized()) { + return KNOT_EOK; + } else if (conf_db_exists(CONF_DEFAULT_DBDIR)) { + return util_conf_init_confdb(CONF_DEFAULT_DBDIR); + } else if (stat(CONF_DEFAULT_FILE, &st) == 0) { + return util_conf_init_file(CONF_DEFAULT_FILE); + } else { + ERR2("couldn't initialize configuration, please provide %s option", + (allow_db ? "-c, -C, or -D" : "-c or -C")); + return KNOT_EINVAL; + } +} + +void util_update_privileges(void) +{ + int uid, gid; + if (conf_user(conf(), &uid, &gid) != KNOT_EOK) { + return; + } + + // Just try to alter process privileges if different from configured. + _unused_ int unused; + if ((gid_t)gid != getgid()) { + unused = setregid(gid, gid); + } + if ((uid_t)uid != getuid()) { + unused = setreuid(uid, uid); + } +} + +void util_conf_deinit(void) +{ + log_close(); + conf_update(NULL, CONF_UPD_FNONE); +} diff --git a/src/utils/common/util_conf.h b/src/utils/common/util_conf.h new file mode 100644 index 0000000..a71d886 --- /dev/null +++ b/src/utils/common/util_conf.h @@ -0,0 +1,86 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdbool.h> + +#include "knot/conf/conf.h" + +/*! + * General note: + * + * Those functions operate and manipulate with conf() singleton. + * Thus they are not threadsafe etc. + * It is expected to use them just inside the main() function. + * + * Those functions already log any error, while returning an errcode. + */ + +/*! + * \brief Return true if conf() for utilities already exists. + */ +bool util_conf_initialized(void); + +/*! + * \brief Initialize conf() for utilities from a configuration database. + * + * \param confdb Path to configuration database. + * + * \return KNOT_E* + */ +int util_conf_init_confdb(const char *confdb); + +/*! + * \brief Initialize conf() for utilities from a config file. + * + * \param conffile Path to Knot configuration file. + * + * \return KNOT_E* + */ +int util_conf_init_file(const char *conffile); + +/*! + * \brief Initialize basic conf() for utilities just with defaults and some database path. + * + * \param db_type Type of the database to be configured. + * \param db_path Path to that database. + * + * \return KNOT_E* + */ +int util_conf_init_justdb(const char *db_type, const char *db_path); + +/*! + * \brief Initialize conf() for utilities based on existence of confDB or config + * file on default locations. + * + * \param allow_db Direct path to a database is allowed. + * + * \return KNOT_E* + */ +int util_conf_init_default(bool allow_db); + +/*! + * \brief Set UID and GID of running utility process to what is configured... + * + * ...so that e.g. opened files have correct owner. + */ +void util_update_privileges(void); + +/*! + * \brief Deinitialize utility conf() from util_conf_init_*(). + */ +void util_conf_deinit(void); diff --git a/src/utils/kcatalogprint/main.c b/src/utils/kcatalogprint/main.c new file mode 100644 index 0000000..0172347 --- /dev/null +++ b/src/utils/kcatalogprint/main.c @@ -0,0 +1,185 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <getopt.h> +#include <stdlib.h> +#include <string.h> + +#include "knot/catalog/catalog_db.h" +#include "utils/common/msg.h" +#include "utils/common/params.h" +#include "utils/common/signal.h" +#include "utils/common/util_conf.h" + +#define PROGRAM_NAME "kcatalogprint" + +static knot_dname_t *filter_member = NULL; +static knot_dname_t *filter_catalog = NULL; + +signal_ctx_t signal_ctx = { 0 }; // global, needed by signal handler + +static void print_help(void) +{ + printf("Usage: %s [-c | -C | -D <path>] [options]\n" + "\n" + "Config options:\n" + " -c, --config <file> Path to a textual configuration file.\n" + " (default %s)\n" + " -C, --confdb <dir> Path to a configuration database directory.\n" + " (default %s)\n" + " -D, --dir <path> Path to a catalog database directory, use default\n" + " configuration.\n" + "Options:\n" + " -a, --catalog <name> Filter the output by catalog zone name.\n" + " -m, --member <name> Filter the output by member zone name.\n" + " -h, --help Print the program help.\n" + " -V, --version Print the program version.\n", + PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR); +} + +static void print_dname(const knot_dname_t *d) +{ + knot_dname_txt_storage_t tmp; + knot_dname_to_str(tmp, d, sizeof(tmp)); + printf("%s ", tmp); +} + +static int catalog_print_cb(const knot_dname_t *mem, const knot_dname_t *ow, + const knot_dname_t *cz, const char *group, void *ctx) +{ + if (filter_catalog != NULL && !knot_dname_is_equal(filter_catalog, cz)) { + return KNOT_EOK; + } + print_dname(mem); + print_dname(ow); + print_dname(cz); + printf("%s\n", group); + (*(ssize_t *)ctx)++; + return KNOT_EOK; +} + +static void catalog_print(catalog_t *cat) +{ + ssize_t total = 0; + + printf(";; <member zone> <record owner> <catalog zone> <group>\n"); + + if (cat != NULL) { + int ret = catalog_open(cat); + if (ret == KNOT_EOK) { + ret = catalog_apply(cat, filter_member, catalog_print_cb, &total, false); + } + if (ret != KNOT_EOK) { + ERR2("failed to print catalog (%s)", knot_strerror(ret)); + return; + } + } + + printf("Total records: %zd\n", total); +} + +static void params_cleanup(void) +{ + free(filter_member); + free(filter_catalog); +} + +int main(int argc, char *argv[]) +{ + catalog_t c = { { 0 } }; + + struct option opts[] = { + { "config", required_argument, NULL, 'c' }, + { "confdb", required_argument, NULL, 'C' }, + { "dir", required_argument, NULL, 'D' }, + { "catalog", required_argument, NULL, 'a' }, + { "member", required_argument, NULL, 'm' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { NULL } + }; + + signal_ctx.close_db = &c.db; + signal_init_std(); + + int opt = 0; + while ((opt = getopt_long(argc, argv, "c:C:D:a:m:hV", opts, NULL)) != -1) { + switch (opt) { + case 'c': + if (util_conf_init_file(optarg) != KNOT_EOK) { + goto failure; + } + break; + case 'C': + if (util_conf_init_confdb(optarg) != KNOT_EOK) { + goto failure; + } + break; + case 'D': + if (util_conf_init_justdb("catalog-db", optarg) != KNOT_EOK) { + goto failure; + } + break; + case 'a': + free(filter_catalog); + filter_catalog = knot_dname_from_str_alloc(optarg); + knot_dname_to_lower(filter_catalog); + break; + case 'm': + free(filter_member); + filter_member = knot_dname_from_str_alloc(optarg); + knot_dname_to_lower(filter_member); + break; + case 'h': + print_help(); + goto success; + case 'V': + print_version(PROGRAM_NAME); + goto success; + default: + print_help(); + goto failure; + } + } + + // Backward compatibility. + if (argc - optind > 0) { + WARN2("obsolete parameter specified"); + if (util_conf_init_justdb("catalog-db", argv[optind]) != KNOT_EOK) { + goto failure; + } + optind++; + } + + if (util_conf_init_default(true) != KNOT_EOK) { + goto failure; + } + + char *db = conf_db(conf(), C_CATALOG_DB); + catalog_init(&c, db, 0); // mapsize grows automatically + free(db); + catalog_print(&c); + catalog_deinit(&c); + +success: + params_cleanup(); + util_conf_deinit(); + return EXIT_SUCCESS; +failure: + params_cleanup(); + util_conf_deinit(); + return EXIT_FAILURE; +} diff --git a/src/utils/kdig/kdig_exec.c b/src/utils/kdig/kdig_exec.c new file mode 100644 index 0000000..7733254 --- /dev/null +++ b/src/utils/kdig/kdig_exec.c @@ -0,0 +1,1324 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdlib.h> +#include <netinet/in.h> +#include <sys/socket.h> +#include <sys/time.h> + +#include "utils/kdig/kdig_exec.h" +#include "utils/common/exec.h" +#include "utils/common/msg.h" +#include "utils/common/netio.h" +#include "utils/common/sign.h" +#include "libknot/libknot.h" +#include "contrib/json.h" +#include "contrib/sockaddr.h" +#include "contrib/time.h" +#include "contrib/ucw/lists.h" + +#if USE_DNSTAP +#include "contrib/dnstap/convert.h" +#include "contrib/dnstap/message.h" +#include "contrib/dnstap/writer.h" +#include "libknot/probe/data.h" + +static int write_dnstap(dt_writer_t *writer, + const bool is_query, + const uint8_t *wire, + const size_t wire_len, + net_t *net, + const struct timespec *mtime) +{ + Dnstap__Message msg; + Dnstap__Message__Type msg_type; + int ret; + int protocol = 0; + + if (writer == NULL) { + return KNOT_EOK; + } + + if (net->local == NULL) { + net_set_local_info(net); + } + + msg_type = is_query ? DNSTAP__MESSAGE__TYPE__TOOL_QUERY : + DNSTAP__MESSAGE__TYPE__TOOL_RESPONSE; + + if (net->socktype == SOCK_DGRAM) { + protocol = IPPROTO_UDP; + } else if (net->socktype == SOCK_STREAM) { + protocol = IPPROTO_TCP; + } + + ret = dt_message_fill(&msg, msg_type, net->local_info->ai_addr, + net->srv->ai_addr, protocol, + wire, wire_len, mtime); + if (ret != KNOT_EOK) { + return ret; + } + + return dt_writer_write(writer, (const ProtobufCMessage *)&msg); +} + +static float get_query_time(const Dnstap__Dnstap *frame) +{ + if (!frame->message->has_query_time_sec || + !frame->message->has_query_time_nsec || + !frame->message->has_response_time_sec || + !frame->message->has_response_time_sec) { + return 0; + } + + struct timespec from = { + .tv_sec = frame->message->query_time_sec, + .tv_nsec = frame->message->query_time_nsec + }; + + struct timespec to = { + .tv_sec = frame->message->response_time_sec, + .tv_nsec = frame->message->response_time_nsec + }; + + return time_diff_ms(&from, &to); +} + +static void fill_remote_addr(net_t *net, Dnstap__Message *message, bool is_initiator) +{ + if (!message->has_socket_family || !message->has_socket_protocol) { + return; + } + + if ((message->response_address.data == NULL && is_initiator) || + message->query_address.data == NULL) { + return; + } + + struct sockaddr_storage ss = { 0 }; + int family = dt_family_decode(message->socket_family); + knot_probe_proto_t proto = dt_protocol_decode(message->socket_protocol); + + ProtobufCBinaryData *addr = NULL; + uint32_t port = 0; + if (is_initiator) { + addr = &message->response_address; + port = message->response_port; + } else { + addr = &message->query_address; + port = message->query_port; + } + + sockaddr_set_raw(&ss, family, addr->data, addr->len); + sockaddr_port_set(&ss, port); + + get_addr_str(&ss, proto, &net->remote_str); +} + +static int process_dnstap(const query_t *query) +{ + dt_reader_t *reader = query->dt_reader; + + if (query->dt_reader == NULL) { + return -1; + } + + bool first_message = true; + + for (;;) { + Dnstap__Dnstap *frame = NULL; + Dnstap__Message *message = NULL; + ProtobufCBinaryData *wire = NULL; + bool is_query; + bool is_initiator; + + // Read next message. + int ret = dt_reader_read(reader, &frame); + if (ret == KNOT_EOF) { + break; + } else if (ret != KNOT_EOK) { + ERR("can't read dnstap message"); + break; + } + + // Check for dnstap message. + if (frame->type == DNSTAP__DNSTAP__TYPE__MESSAGE) { + message = frame->message; + } else { + WARN("ignoring non-dnstap message"); + dt_reader_free_frame(reader, &frame); + continue; + } + + // Check for the type of dnstap message. + if (message->has_response_message) { + wire = &message->response_message; + is_query = false; + } else if (message->has_query_message) { + wire = &message->query_message; + is_query = true; + } else { + WARN("dnstap frame contains no message"); + dt_reader_free_frame(reader, &frame); + continue; + } + + // Ignore query message if requested. + if (is_query && !query->style.show_query) { + dt_reader_free_frame(reader, &frame); + continue; + } + + // Get the message role. + is_initiator = dt_message_role_is_initiator(message->type); + + // Create dns packet based on dnstap wire data. + knot_pkt_t *pkt = knot_pkt_new(wire->data, wire->len, NULL); + if (pkt == NULL) { + ERR("can't allocate packet"); + dt_reader_free_frame(reader, &frame); + break; + } + + // Parse packet and reconstruct required data. + ret = knot_pkt_parse(pkt, KNOT_PF_NOCANON); + if (ret == KNOT_EOK || ret == KNOT_ETRAIL) { + time_t timestamp = 0; + float query_time = 0.0; + net_t net_ctx = { 0 }; + + if (ret == KNOT_ETRAIL) { + WARN("malformed message (%s)", knot_strerror(ret)); + } + + if (is_query) { + if (message->has_query_time_sec) { + timestamp = message->query_time_sec; + } + } else { + if (message->has_response_time_sec) { + timestamp = message->response_time_sec; + } + query_time = get_query_time(frame); + } + + // Prepare connection information string. + fill_remote_addr(&net_ctx, message, is_initiator); + + if (first_message) { + first_message = false; + } else { + printf("\n"); + } + + print_packet(pkt, &net_ctx, pkt->size, query_time, timestamp, + is_query ^ is_initiator, &query->style); + + net_clean(&net_ctx); + } else { + ERR("can't print dnstap message"); + } + + knot_pkt_free(pkt); + dt_reader_free_frame(reader, &frame); + } + + return 0; +} +#endif // USE_DNSTAP + +static int add_query_edns(knot_pkt_t *packet, const query_t *query, uint16_t max_size) +{ + /* Initialize OPT RR. */ + knot_rrset_t opt_rr; + int ret = knot_edns_init(&opt_rr, max_size, 0, + query->edns > -1 ? query->edns : 0, &packet->mm); + if (ret != KNOT_EOK) { + return ret; + } + + if (query->flags.do_flag) { + knot_edns_set_do(&opt_rr); + } + + /* Append NSID. */ + if (query->nsid) { + ret = knot_edns_add_option(&opt_rr, KNOT_EDNS_OPTION_NSID, + 0, NULL, &packet->mm); + if (ret != KNOT_EOK) { + knot_rrset_clear(&opt_rr, &packet->mm); + return ret; + } + } + + /* Append EDNS-client-subnet. */ + if (query->subnet.family != AF_UNSPEC) { + uint16_t size = knot_edns_client_subnet_size(&query->subnet); + uint8_t data[size]; + + ret = knot_edns_client_subnet_write(data, size, &query->subnet); + if (ret != KNOT_EOK) { + knot_rrset_clear(&opt_rr, &packet->mm); + return ret; + } + + ret = knot_edns_add_option(&opt_rr, KNOT_EDNS_OPTION_CLIENT_SUBNET, + size, data, &packet->mm); + if (ret != KNOT_EOK) { + knot_rrset_clear(&opt_rr, &packet->mm); + return ret; + } + } + + /* Append a cookie option if present. */ + if (query->cc.len > 0) { + uint16_t size = knot_edns_cookie_size(&query->cc, &query->sc); + uint8_t data[size]; + + ret = knot_edns_cookie_write(data, size, &query->cc, &query->sc); + if (ret != KNOT_EOK) { + knot_rrset_clear(&opt_rr, &packet->mm); + return ret; + } + + ret = knot_edns_add_option(&opt_rr, KNOT_EDNS_OPTION_COOKIE, + size, data, &packet->mm); + if (ret != KNOT_EOK) { + knot_rrset_clear(&opt_rr, &packet->mm); + return ret; + } + } + + /* Append EDNS Padding. */ + int padding = query->padding; + if (padding != -3 && query->alignment > 0) { + padding = knot_edns_alignment_size(packet->size, + knot_rrset_size(&opt_rr), + query->alignment); + } else if (query->padding == -2 || (query->padding == -1 && query->tls.enable)) { + padding = knot_pkt_default_padding_size(packet, &opt_rr); + } + if (padding > -1) { + uint8_t zeros[padding]; + memset(zeros, 0, sizeof(zeros)); + + ret = knot_edns_add_option(&opt_rr, KNOT_EDNS_OPTION_PADDING, + padding, zeros, &packet->mm); + if (ret != KNOT_EOK) { + knot_rrset_clear(&opt_rr, &packet->mm); + return ret; + } + } + + /* Append custom EDNS options. */ + node_t *node; + WALK_LIST(node, query->edns_opts) { + ednsopt_t *opt = (ednsopt_t *)node; + ret = knot_edns_add_option(&opt_rr, opt->code, opt->length, + opt->data, &packet->mm); + if (ret != KNOT_EOK) { + knot_rrset_clear(&opt_rr, &packet->mm); + return ret; + } + } + + /* Add prepared OPT to packet. */ + ret = knot_pkt_put(packet, KNOT_COMPR_HINT_NONE, &opt_rr, KNOT_PF_FREE); + if (ret != KNOT_EOK) { + knot_rrset_clear(&opt_rr, &packet->mm); + } + + return ret; +} + +static bool do_padding(const query_t *query) +{ + return (query->padding != -3) && // Disabled padding. + (query->padding > -1 || query->alignment > 0 || // Explicit padding. + query->padding == -2 || // Default padding. + (query->padding == -1 && query->tls.enable)); // TLS automatic. +} + +static bool use_edns(const query_t *query) +{ + return query->edns > -1 || query->udp_size > -1 || query->nsid || + query->flags.do_flag || query->subnet.family != AF_UNSPEC || + query->cc.len > 0 || do_padding(query) || + !ednsopt_list_empty(&query->edns_opts); +} + +static knot_pkt_t *create_query_packet(const query_t *query) +{ + // Set packet buffer size. + uint16_t max_size; + if (query->udp_size < 0) { + if (use_edns(query)) { + max_size = DEFAULT_EDNS_SIZE; + } else { + max_size = DEFAULT_UDP_SIZE; + } + } else { + max_size = query->udp_size; + } + + // Create packet skeleton. + knot_pkt_t *packet = create_empty_packet(max_size); + if (packet == NULL) { + return NULL; + } + + // Set ID = 0 for packet send over HTTPS + // Due HTTP cache it is convenient to set the query ID to 0 - GET messages has same header then +#if defined(LIBNGHTTP2) || defined(ENABLE_QUIC) + if (query->https.enable || query->quic.enable) { + knot_wire_set_id(packet->wire, 0); + } +#endif + + // Set flags to wireformat. + if (query->flags.aa_flag) { + knot_wire_set_aa(packet->wire); + } + if (query->flags.tc_flag) { + knot_wire_set_tc(packet->wire); + } + if (query->flags.rd_flag) { + knot_wire_set_rd(packet->wire); + } + if (query->flags.ra_flag) { + knot_wire_set_ra(packet->wire); + } + if (query->flags.z_flag) { + knot_wire_set_z(packet->wire); + } + if (query->flags.ad_flag) { + knot_wire_set_ad(packet->wire); + } + if (query->flags.cd_flag) { + knot_wire_set_cd(packet->wire); + } + + // Set NOTIFY opcode. + if (query->notify) { + knot_wire_set_opcode(packet->wire, KNOT_OPCODE_NOTIFY); + } + + // Set packet question if available. + knot_dname_t *qname = NULL; + if (query->owner != NULL) { + qname = knot_dname_from_str_alloc(query->owner); + if (qname == NULL) { + ERR("'%s' is not a valid domain name", query->owner); + knot_pkt_free(packet); + return NULL; + } + + int ret = knot_pkt_put_question(packet, qname, query->class_num, + query->type_num); + if (ret != KNOT_EOK) { + knot_dname_free(qname, NULL); + knot_pkt_free(packet); + return NULL; + } + } + + // For IXFR query or NOTIFY query with SOA serial, add a proper section. + if (query->serial >= 0) { + if (query->notify) { + knot_pkt_begin(packet, KNOT_ANSWER); + } else { + knot_pkt_begin(packet, KNOT_AUTHORITY); + } + + // SOA rdata in wireformat. + uint8_t wire[22] = { 0x0 }; + + // Create rrset with SOA record. + knot_rrset_t *soa = knot_rrset_new(qname, + KNOT_RRTYPE_SOA, + query->class_num, + 0, + &packet->mm); + knot_dname_free(qname, NULL); + if (soa == NULL) { + knot_pkt_free(packet); + return NULL; + } + + // Fill in blank SOA rdata to rrset. + int ret = knot_rrset_add_rdata(soa, wire, sizeof(wire), &packet->mm); + if (ret != KNOT_EOK) { + knot_rrset_free(soa, &packet->mm); + knot_pkt_free(packet); + return NULL; + } + + // Set SOA serial. + knot_soa_serial_set(soa->rrs.rdata, query->serial); + + ret = knot_pkt_put(packet, KNOT_COMPR_HINT_NONE, soa, KNOT_PF_FREE); + if (ret != KNOT_EOK) { + knot_rrset_free(soa, &packet->mm); + knot_pkt_free(packet); + return NULL; + } + + free(soa); + } else { + knot_dname_free(qname, NULL); + } + + // Begin additional section + knot_pkt_begin(packet, KNOT_ADDITIONAL); + + // Create EDNS section if required. + if (use_edns(query)) { + int ret = add_query_edns(packet, query, max_size); + if (ret != KNOT_EOK) { + ERR("can't set up EDNS section"); + knot_pkt_free(packet); + return NULL; + } + } + + return packet; +} + +static bool check_reply_id(const knot_pkt_t *reply, + const knot_pkt_t *query) +{ + uint16_t query_id = knot_wire_get_id(query->wire); + uint16_t reply_id = knot_wire_get_id(reply->wire); + + if (reply_id != query_id) { + WARN("reply ID (%u) is different from query ID (%u)", + reply_id, query_id); + return false; + } + + return true; +} + +static void check_reply_qr(const knot_pkt_t *reply) +{ + if (!knot_wire_get_qr(reply->wire)) { + WARN("response QR bit not set"); + } +} + +static void check_reply_question(const knot_pkt_t *reply, + const knot_pkt_t *query) +{ + if (knot_wire_get_qdcount(reply->wire) < 1) { + WARN("response doesn't have question section"); + return; + } + + if (!knot_dname_is_equal(knot_pkt_wire_qname(reply), knot_pkt_wire_qname(query)) || + knot_pkt_qclass(reply) != knot_pkt_qclass(query) || + knot_pkt_qtype(reply) != knot_pkt_qtype(query)) { + WARN("query/response question sections are different"); + return; + } +} + +static int64_t first_serial_check(const knot_pkt_t *reply, const knot_pkt_t *query) +{ + const knot_pktsection_t *answer = knot_pkt_section(reply, KNOT_ANSWER); + + if (answer->count <= 0) { + return -1; + } + + const knot_rrset_t *first = knot_pkt_rr(answer, 0); + + if (first->type != KNOT_RRTYPE_SOA) { + return -1; + } else { + if (!knot_dname_is_case_equal(first->owner, knot_pkt_qname(query))) { + WARN("leading SOA owner not matching the requested zone name"); + } + + return knot_soa_serial(first->rrs.rdata); + } +} + +static bool finished_xfr(const uint32_t serial, const knot_pkt_t *reply, + const knot_pkt_t *query, const size_t msg_count, bool is_ixfr) +{ + const knot_pktsection_t *answer = knot_pkt_section(reply, KNOT_ANSWER); + + if (answer->count <= 0) { + return false; + } + + const knot_rrset_t *last = knot_pkt_rr(answer, answer->count - 1); + + if (last->type != KNOT_RRTYPE_SOA) { + return false; + } else if (answer->count == 1 && msg_count == 1) { + return is_ixfr; + } else { + if (!knot_dname_is_case_equal(last->owner, knot_pkt_qname(query))) { + WARN("final SOA owner not matching the requested zone name"); + } + + return knot_soa_serial(last->rrs.rdata) == serial; + } +} + +static int sign_query(knot_pkt_t *pkt, const query_t *query, sign_context_t *ctx) +{ + if (query->tsig_key.name == NULL) { + return KNOT_EOK; + } + + int ret = sign_context_init_tsig(ctx, &query->tsig_key); + if (ret != KNOT_EOK) { + return ret; + } + + ret = sign_packet(pkt, ctx); + if (ret != KNOT_EOK) { + sign_context_deinit(ctx); + return ret; + } + + return KNOT_EOK; +} + +static void net_close_keepopen(net_t *net, const query_t *query) +{ + if (!query->keepopen) { + net_close(net); + } +} + +static int process_query_packet(const knot_pkt_t *query, + net_t *net, + const query_t *query_ctx, + const bool ignore_tc, + const sign_context_t *sign_ctx, + const style_t *style) +{ + struct timespec t_start, t_query, t_end; + time_t timestamp; + knot_pkt_t *reply = NULL; + uint8_t in[MAX_PACKET_SIZE]; + int in_len = 0; + int ret; + + // Get start query time. + timestamp = time(NULL); + t_start = time_now(); + + // Connect to the server if not already connected. + if (net->sockfd < 0) { + ret = net_connect(net); + if (ret != KNOT_EOK) { + return -1; + } + } + + // Send query packet. + ret = net_send(net, query->wire, query->size); + if (ret != KNOT_EOK) { + net_close(net); + return -1; + } + + // Get stop query time and start reply time. + t_query = time_now(); + +#if USE_DNSTAP + struct timespec t_query_full = time_diff(&t_start, &t_query); + t_query_full.tv_sec += timestamp; + + // Make the dnstap copy of the query. + write_dnstap(query_ctx->dt_writer, true, query->wire, query->size, + net, &t_query_full); +#endif // USE_DNSTAP + + // Print query packet if required. + if (style->show_query && style->format != FORMAT_JSON) { + // Create copy of query packet for parsing. + knot_pkt_t *q = knot_pkt_new(query->wire, query->size, NULL); + if (q != NULL) { + if (knot_pkt_parse(q, KNOT_PF_NOCANON) == KNOT_EOK) { + print_packet(q, net, query->size, + time_diff_ms(&t_start, &t_query), + timestamp, false, style); + } else { + ERR("can't print query packet"); + } + knot_pkt_free(q); + } else { + ERR("can't print query packet"); + } + + printf("\n"); + } + + // Loop over incoming messages, unless reply id is correct or timeout. + while (true) { + reply = NULL; + + // Receive a reply message. + in_len = net_receive(net, in, sizeof(in)); + t_end = time_now(); + if (in_len <= 0) { + goto fail; + } + +#if USE_DNSTAP + struct timespec t_end_full = time_diff(&t_start, &t_end); + t_end_full.tv_sec += timestamp; + + // Make the dnstap copy of the response. + write_dnstap(query_ctx->dt_writer, false, in, in_len, net, + &t_end_full); +#endif // USE_DNSTAP + + // Create reply packet structure to fill up. + reply = knot_pkt_new(in, in_len, NULL); + if (reply == NULL) { + ERR("internal error (%s)", knot_strerror(KNOT_ENOMEM)); + goto fail; + } + + // Parse reply to the packet structure. + ret = knot_pkt_parse(reply, KNOT_PF_NOCANON); + if (ret == KNOT_ETRAIL) { + WARN("malformed reply packet (%s)", knot_strerror(ret)); + } else if (ret != KNOT_EOK) { + ERR("malformed reply packet from %s", net->remote_str); + goto fail; + } + + // Compare reply header id. + if (check_reply_id(reply, query)) { + break; + // Check for timeout. + } else if (time_diff_ms(&t_query, &t_end) > 1000 * net->wait) { + goto fail; + } + + knot_pkt_free(reply); + } + + // Check for TC bit and repeat query with TCP if required. + if (knot_wire_get_tc(reply->wire) != 0 && + ignore_tc == false && net->socktype == SOCK_DGRAM) { + printf("\n"); + WARN("truncated reply from %s, retrying over TCP\n", + net->remote_str); + knot_pkt_free(reply); + net_close_keepopen(net, query_ctx); + + net->socktype = SOCK_STREAM; + + return process_query_packet(query, net, query_ctx, true, + sign_ctx, style); + } + + // Check for question sections equality. + check_reply_question(reply, query); + + // Check QR bit + check_reply_qr(reply); + + // Print reply packet. + if (style->format != FORMAT_JSON) { + // Intentionaly start-end because of QUIC can have receive time. + print_packet(reply, net, in_len, time_diff_ms(&t_start, &t_end), + timestamp, true, style); + } else { + knot_pkt_t *q = knot_pkt_new(query->wire, query->size, NULL); + (void)knot_pkt_parse(q, KNOT_PF_NOCANON); + print_packets_json(q, reply, net, timestamp, style); + knot_pkt_free(q); + } + + // Verify signature if a key was specified. + if (sign_ctx->digest != NULL) { + ret = verify_packet(reply, sign_ctx); + if (ret != KNOT_EOK) { + WARN("reply verification for %s (%s)", + net->remote_str, knot_strerror(ret)); + } + } + + // Check for BADCOOKIE RCODE and repeat query with the new cookie if required. + if (knot_pkt_ext_rcode(reply) == KNOT_RCODE_BADCOOKIE && query_ctx->badcookie > 0) { + printf("\n"); + WARN("bad cookie from %s, retrying with the received one\n", + net->remote_str); + net_close_keepopen(net, query_ctx); + + // Prepare new query context. + query_t new_ctx = *query_ctx; + + uint8_t *opt = knot_pkt_edns_option(reply, KNOT_EDNS_OPTION_COOKIE); + if (opt == NULL) { + ERR("bad cookie, missing EDNS section"); + goto fail; + } + + const uint8_t *data = knot_edns_opt_get_data(opt); + uint16_t data_len = knot_edns_opt_get_length(opt); + ret = knot_edns_cookie_parse(&new_ctx.cc, &new_ctx.sc, data, data_len); + if (ret != KNOT_EOK) { + ERR("bad cookie, missing EDNS cookie option"); + goto fail; + } + knot_pkt_free(reply); + + // Restore the original client cookie. + new_ctx.cc = query_ctx->cc; + + new_ctx.badcookie--; + + knot_pkt_t *new_query = create_query_packet(&new_ctx); + ret = process_query_packet(new_query, net, &new_ctx, ignore_tc, + sign_ctx, style); + knot_pkt_free(new_query); + + return ret; + } + + knot_pkt_free(reply); + net_close_keepopen(net, query_ctx); + + return 0; + +fail: + if (style->format != FORMAT_JSON) { + // Intentionaly start-end because of QUIC can have receive time. + print_packet(reply, net, in_len, time_diff_ms(&t_start, &t_end), + timestamp, true, style); + } else { + knot_pkt_t *q = knot_pkt_new(query->wire, query->size, NULL); + (void)knot_pkt_parse(q, KNOT_PF_NOCANON); + print_packets_json(q, reply, net, timestamp, style); + knot_pkt_free(q); + } + + knot_pkt_free(reply); + net_close(net); + + return -1; +} + +static int process_query(const query_t *query, net_t *net) +{ + node_t *server; + knot_pkt_t *out_packet; + int ret; + + // Create query packet. + out_packet = create_query_packet(query); + if (out_packet == NULL) { + ERR("can't create query packet"); + return -1; + } + + // Sign the query. + sign_context_t sign_ctx = { 0 }; + ret = sign_query(out_packet, query, &sign_ctx); + if (ret != KNOT_EOK) { + ERR("can't sign the packet (%s)", knot_strerror(ret)); + return -1; + } + + // Reuse previous connection if available. + if (net->sockfd >= 0) { + DBG("Querying for owner(%s), class(%u), type(%u), reused connection", + query->owner, query->class_num, query->type_num); + + ret = process_query_packet(out_packet, net, query, query->ignore_tc, + &sign_ctx, &query->style); + goto finish; + } + + // Get connection parameters. + int socktype = get_socktype(query->protocol, query->type_num); + int flags = query->fastopen ? NET_FLAGS_FASTOPEN : NET_FLAGS_NONE; + + // Loop over server list to process query. + WALK_LIST(server, query->servers) { + srv_info_t *remote = (srv_info_t *)server; + int iptype = get_iptype(query->ip, remote); + + DBG("Querying for owner(%s), class(%u), type(%u), server(%s), " + "port(%s), protocol(%s)", query->owner, query->class_num, + query->type_num, remote->name, remote->service, + get_sockname(socktype)); + + // Loop over the number of retries. + for (size_t i = 0; i <= query->retries; i++) { + // Initialize network structure for current server. + ret = net_init(query->local, remote, iptype, socktype, + query->wait, flags, + (struct sockaddr *)&query->proxy.src, + (struct sockaddr *)&query->proxy.dst, + net); + if (ret != KNOT_EOK) { + if (ret == KNOT_NET_EADDR) { + // Requested address family not available. + goto next_server; + } + continue; + } + + // Loop over all resolved addresses for remote. + while (net->srv != NULL) { + ret = net_init_crypto(net, &query->tls, &query->https, + &query->quic); + if (ret != 0) { + ERR("failed to initialize crypto context (%s)", + knot_strerror(ret)); + break; + } + + ret = process_query_packet(out_packet, net, + query, + query->ignore_tc, + &sign_ctx, + &query->style); + // If error try next resolved address. + if (ret != 0) { + net->srv = net->srv->ai_next; + if (net->srv != NULL && query->style.show_query) { + printf("\n"); + } + + continue; + } + + break; + } + + // Success. + if (ret == 0) { + goto finish; + } + + if (i < query->retries) { + DBG("retrying server %s@%s(%s)", + remote->name, remote->service, + get_sockname(socktype)); + + if (query->style.show_query) { + printf("\n"); + } + } + + net_clean(net); + } + + ERR("failed to query server %s@%s(%s)", + remote->name, remote->service, get_sockname(socktype)); + + // If not last server, print separation. + if (server->next->next && query->style.show_query) { + printf("\n"); + } +next_server: + continue; + } +finish: + if (!query->keepopen || net->sockfd < 0) { + net_clean(net); + } + sign_context_deinit(&sign_ctx); + knot_pkt_free(out_packet); + + if (ret == KNOT_NET_EADDR) { + WARN("no servers to query"); + } + + return ret; +} + +static int process_xfr_packet(const knot_pkt_t *query, + net_t *net, + const query_t *query_ctx, + const sign_context_t *sign_ctx, + const style_t *style) +{ + struct timespec t_start, t_query, t_end; + time_t timestamp; + knot_pkt_t *reply = NULL; + uint8_t in[MAX_PACKET_SIZE]; + int in_len; + int ret; + int64_t serial = 0; + size_t total_len = 0; + size_t msg_count = 0; + size_t rr_count = 0; + jsonw_t *w = NULL; + + // Get start query time. + timestamp = time(NULL); + t_start = time_now(); + + // Connect to the server if not already connected. + if (net->sockfd < 0) { + ret = net_connect(net); + if (ret != KNOT_EOK) { + return -1; + } + } + + // Send query packet. + ret = net_send(net, query->wire, query->size); + if (ret != KNOT_EOK) { + net_close(net); + return -1; + } + + // Get stop query time and start reply time. + t_query = time_now(); + +#if USE_DNSTAP + struct timespec t_query_full = time_diff(&t_start, &t_query); + t_query_full.tv_sec += timestamp; + + // Make the dnstap copy of the query. + write_dnstap(query_ctx->dt_writer, true, query->wire, query->size, + net, &t_query_full); +#endif // USE_DNSTAP + + // Print query packet if required. + if (style->show_query && style->format != FORMAT_JSON) { + // Create copy of query packet for parsing. + knot_pkt_t *q = knot_pkt_new(query->wire, query->size, NULL); + if (q != NULL) { + if (knot_pkt_parse(q, KNOT_PF_NOCANON) == KNOT_EOK) { + print_packet(q, net, query->size, + time_diff_ms(&t_start, &t_query), + timestamp, false, style); + } else { + ERR("can't print query packet"); + } + knot_pkt_free(q); + } else { + ERR("can't print query packet"); + } + + printf("\n"); + } + + // Loop over reply messages unless first and last SOA serials differ. + while (true) { + reply = NULL; + + // Receive a reply message. + in_len = net_receive(net, in, sizeof(in)); + t_end = time_now(); + if (in_len <= 0) { + goto fail; + } + +#if USE_DNSTAP + struct timespec t_end_full = time_diff(&t_start, &t_end); + t_end_full.tv_sec += timestamp; + + // Make the dnstap copy of the response. + write_dnstap(query_ctx->dt_writer, false, in, in_len, net, + &t_end_full); +#endif // USE_DNSTAP + + // Create reply packet structure to fill up. + reply = knot_pkt_new(in, in_len, NULL); + if (reply == NULL) { + ERR("internal error (%s)", knot_strerror(KNOT_ENOMEM)); + goto fail; + } + + // Parse reply to the packet structure. + ret = knot_pkt_parse(reply, KNOT_PF_NOCANON); + if (ret == KNOT_ETRAIL) { + WARN("malformed reply packet (%s)", knot_strerror(ret)); + } else if (ret != KNOT_EOK) { + ERR("malformed reply packet from %s", net->remote_str); + goto fail; + } + + // Compare reply header id. + if (check_reply_id(reply, query) == false) { + ERR("reply ID mismatch from %s", net->remote_str); + goto fail; + } + + // Print leading transfer information. + if (msg_count == 0) { + if (style->format != FORMAT_JSON) { + print_header_xfr(query, style); + } else { + knot_pkt_t *q = knot_pkt_new(query->wire, query->size, NULL); + (void)knot_pkt_parse(q, KNOT_PF_NOCANON); + w = print_header_xfr_json(q, timestamp, style); + knot_pkt_free(q); + } + } + + // Check for error reply. + if (knot_pkt_ext_rcode(reply) != KNOT_RCODE_NOERROR) { + ERR("server replied with error '%s'", + knot_pkt_ext_rcode_name(reply)); + goto fail; + } + + // The first message has a special treatment. + if (msg_count == 0) { + // Verify 1. signature if a key was specified. + if (sign_ctx->digest != NULL) { + ret = verify_packet(reply, sign_ctx); + if (ret != KNOT_EOK) { + style_t tsig_style = { + .format = style->format, + .style = style->style, + .show_tsig = true + }; + if (style->format != FORMAT_JSON) { + print_data_xfr(reply, &tsig_style); + } + + ERR("reply verification for %s (%s)", + net->remote_str, knot_strerror(ret)); + goto fail; + } + } + + // Read first SOA serial. + serial = first_serial_check(reply, query); + + if (serial < 0) { + ERR("first answer record from %s isn't SOA", + net->remote_str); + goto fail; + } + + // Check for question sections equality. + check_reply_question(reply, query); + + // Check QR bit + check_reply_qr(reply); + } + + msg_count++; + rr_count += knot_wire_get_ancount(reply->wire); + total_len += in_len; + + // Print reply packet. + if (style->format != FORMAT_JSON) { + print_data_xfr(reply, style); + } else { + print_data_xfr_json(w, reply, timestamp); + } + + // Fail to continue if TC is set. + if (knot_wire_get_tc(reply->wire)) { + ERR("truncated reply"); + goto fail; + } + + // Check for finished transfer. + if (finished_xfr(serial, reply, query, msg_count, query_ctx->serial != -1)) { + knot_pkt_free(reply); + break; + } + + knot_pkt_free(reply); + reply = NULL; + } + + // Print full transfer information. + t_end = time_now(); + if (style->format != FORMAT_JSON) { + print_footer_xfr(total_len, msg_count, rr_count, net, + time_diff_ms(&t_query, &t_end), timestamp, style); + } else { + print_footer_xfr_json(&w, style); + } + + net_close_keepopen(net, query_ctx); + + return 0; + +fail: + // Print partial transfer information. + t_end = time_now(); + if (style->format != FORMAT_JSON) { + print_data_xfr(reply, style); + print_footer_xfr(total_len, msg_count, rr_count, net, + time_diff_ms(&t_query, &t_end), timestamp, style); + } else { + print_data_xfr_json(w, reply, timestamp); + print_footer_xfr_json(&w, style); + } + + knot_pkt_free(reply); + net_close(net); + free(w); + + return -1; +} + +static int process_xfr(const query_t *query, net_t *net) +{ + knot_pkt_t *out_packet; + int ret; + + // Create query packet. + out_packet = create_query_packet(query); + if (out_packet == NULL) { + ERR("can't create query packet"); + return -1; + } + + // Sign the query. + sign_context_t sign_ctx = { 0 }; + ret = sign_query(out_packet, query, &sign_ctx); + if (ret != KNOT_EOK) { + ERR("can't sign the packet (%s)", knot_strerror(ret)); + return -1; + } + + // Reuse previous connection if available. + if (net->sockfd >= 0) { + DBG("Querying for owner(%s), class(%u), type(%u), reused connection", + query->owner, query->class_num, query->type_num); + + ret = process_xfr_packet(out_packet, net, query, + &sign_ctx, &query->style); + goto finish; + } + + // Get connection parameters. + int socktype = get_socktype(query->protocol, query->type_num); + int flags = query->fastopen ? NET_FLAGS_FASTOPEN : NET_FLAGS_NONE; + + // Use the first nameserver from the list. + srv_info_t *remote = HEAD(query->servers); + int iptype = get_iptype(query->ip, remote); + + DBG("Querying for owner(%s), class(%u), type(%u), server(%s), " + "port(%s), protocol(%s)", query->owner, query->class_num, + query->type_num, remote->name, remote->service, + get_sockname(socktype)); + + // Initialize network structure. + ret = net_init(query->local, remote, iptype, socktype, query->wait, flags, + (struct sockaddr *)&query->proxy.src, + (struct sockaddr *)&query->proxy.dst, + net); + if (ret != KNOT_EOK) { + sign_context_deinit(&sign_ctx); + knot_pkt_free(out_packet); + return -1; + } + + // Loop over all resolved addresses for remote. + while (net->srv != NULL) { + ret = net_init_crypto(net, &query->tls, &query->https, + &query->quic); + if (ret != 0) { + ERR("failed to initialize crypto context (%s)", + knot_strerror(ret)); + break; + } + + ret = process_xfr_packet(out_packet, net, + query, + &sign_ctx, + &query->style); + // If error try next resolved address. + if (ret != 0) { + net->srv = (net->srv)->ai_next; + continue; + } + + break; + } + + if (ret != 0) { + ERR("failed to query server %s@%s(%s)", + remote->name, remote->service, get_sockname(socktype)); + } +finish: + if (!query->keepopen || net->sockfd < 0) { + net_clean(net); + } + sign_context_deinit(&sign_ctx); + knot_pkt_free(out_packet); + + return ret; +} + +int kdig_exec(const kdig_params_t *params) +{ + node_t *n; + net_t net = { .sockfd = -1 }; + + if (params == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + bool success = true; + + // Loop over query list. + WALK_LIST(n, params->queries) { + query_t *query = (query_t *)n; + + int ret = -1; + switch (query->operation) { + case OPERATION_QUERY: + ret = process_query(query, &net); + break; + case OPERATION_XFR: + ret = process_xfr(query, &net); + break; +#if USE_DNSTAP + case OPERATION_LIST_DNSTAP: + ret = process_dnstap(query); + break; +#endif // USE_DNSTAP + default: + ERR("unsupported operation"); + break; + } + + // All operations must succeed. + if (ret != 0) { + success = false; + } + + // If not last query, print separation. + if (n->next->next && params->config->style.format == FORMAT_FULL) { + printf("\n"); + } + } + + if (net.sockfd >= 0) { + net_close(&net); + net_clean(&net); + } + + return success ? KNOT_EOK : KNOT_ERROR; +} diff --git a/src/utils/kdig/kdig_exec.h b/src/utils/kdig/kdig_exec.h new file mode 100644 index 0000000..99167ce --- /dev/null +++ b/src/utils/kdig/kdig_exec.h @@ -0,0 +1,21 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "utils/kdig/kdig_params.h" + +int kdig_exec(const kdig_params_t *params); diff --git a/src/utils/kdig/kdig_main.c b/src/utils/kdig/kdig_main.c new file mode 100644 index 0000000..534d50e --- /dev/null +++ b/src/utils/kdig/kdig_main.c @@ -0,0 +1,45 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdlib.h> + +#include "libdnssec/crypto.h" +#include "utils/kdig/kdig_params.h" +#include "utils/kdig/kdig_exec.h" +#include "libknot/libknot.h" + +int main(int argc, char *argv[]) +{ + int ret = EXIT_SUCCESS; + + tzset(); + + kdig_params_t params; + if (kdig_parse(¶ms, argc, argv) == KNOT_EOK) { + if (!params.stop) { + dnssec_crypto_init(); + if (kdig_exec(¶ms) != KNOT_EOK) { + ret = EXIT_FAILURE; + } + dnssec_crypto_cleanup(); + } + } else { + ret = EXIT_FAILURE; + } + + kdig_clean(¶ms); + return ret; +} diff --git a/src/utils/kdig/kdig_params.c b/src/utils/kdig/kdig_params.c new file mode 100644 index 0000000..310e890 --- /dev/null +++ b/src/utils/kdig/kdig_params.c @@ -0,0 +1,2765 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <arpa/inet.h> +#include <locale.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> + +#include "utils/kdig/kdig_params.h" +#include "utils/common/hex.h" +#include "utils/common/msg.h" +#include "utils/common/netio.h" +#include "utils/common/params.h" +#include "utils/common/resolv.h" +#include "libknot/descriptor.h" +#include "libknot/libknot.h" +#include "contrib/base64.h" +#include "contrib/sockaddr.h" +#include "contrib/string.h" +#include "contrib/strtonum.h" +#include "contrib/time.h" +#include "contrib/ucw/lists.h" +#include "libdnssec/error.h" +#include "libdnssec/random.h" + +#define PROGRAM_NAME "kdig" + +#define DEFAULT_RETRIES_DIG 2 +#define DEFAULT_TIMEOUT_DIG 5 +#define DEFAULT_ALIGNMENT_SIZE 128 +#define DEFAULT_TLS_OCSP_STAPLING (7 * 24 * 3600) + +#define BADCOOKIE_RETRY_MAX 10 + +static const flags_t DEFAULT_FLAGS_DIG = { + .aa_flag = false, + .tc_flag = false, + .rd_flag = true, + .ra_flag = false, + .z_flag = false, + .ad_flag = true, + .cd_flag = false, + .do_flag = false +}; + +static const style_t DEFAULT_STYLE_DIG = { + .format = FORMAT_FULL, + .style = { + .wrap = false, + .show_class = true, + .show_ttl = true, + .verbose = false, + .original_ttl = false, + .empty_ttl = false, + .human_ttl = false, + .human_timestamp = true, + .generic = false, + .ascii_to_idn = name_to_idn + }, + .show_query = false, + .show_header = true, + .show_section = true, + .show_edns = true, + .show_question = true, + .show_answer = true, + .show_authority = true, + .show_additional = true, + .show_tsig = true, + .show_footer = true +}; + +static int opt_multiline(const char *arg, void *query) +{ + query_t *q = query; + + q->style.style.wrap = true; + q->style.format = FORMAT_FULL; + q->style.show_header = true; + q->style.show_edns = true; + q->style.show_footer = true; + q->style.style.verbose = true; + + return KNOT_EOK; +} + +static int opt_nomultiline(const char *arg, void *query) +{ + query_t *q = query; + + q->style.style.wrap = false; + + return KNOT_EOK; +} + +static int opt_short(const char *arg, void *query) +{ + query_t *q = query; + + q->style.format = FORMAT_DIG; + q->style.show_header = false; + q->style.show_edns = false; + q->style.show_footer = false; + + return KNOT_EOK; +} + +static int opt_noshort(const char *arg, void *query) +{ + query_t *q = query; + + q->style.format = FORMAT_FULL; + + return KNOT_EOK; +} + +static int opt_generic(const char *arg, void *query) +{ + query_t *q = query; + + q->style.style.generic = true; + + return KNOT_EOK; +} + +static int opt_nogeneric(const char *arg, void *query) +{ + query_t *q = query; + + q->style.style.generic = false; + + return KNOT_EOK; +} + +static int opt_aaflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.aa_flag = true; + + return KNOT_EOK; +} + +static int opt_noaaflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.aa_flag = false; + + return KNOT_EOK; +} + +static int opt_tcflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.tc_flag = true; + + return KNOT_EOK; +} + +static int opt_notcflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.tc_flag = false; + + return KNOT_EOK; +} + +static int opt_rdflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.rd_flag = true; + + return KNOT_EOK; +} + +static int opt_nordflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.rd_flag = false; + + return KNOT_EOK; +} + +static int opt_raflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.ra_flag = true; + + return KNOT_EOK; +} + +static int opt_noraflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.ra_flag = false; + + return KNOT_EOK; +} + +static int opt_zflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.z_flag = true; + + return KNOT_EOK; +} + +static int opt_nozflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.z_flag = false; + + return KNOT_EOK; +} + +static int opt_adflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.ad_flag = true; + + return KNOT_EOK; +} + +static int opt_noadflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.ad_flag = false; + + return KNOT_EOK; +} + +static int opt_cdflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.cd_flag = true; + + return KNOT_EOK; +} + +static int opt_nocdflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.cd_flag = false; + + return KNOT_EOK; +} + +static int opt_doflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.do_flag = true; + + return KNOT_EOK; +} + +static int opt_nodoflag(const char *arg, void *query) +{ + query_t *q = query; + + q->flags.do_flag = false; + + return KNOT_EOK; +} + +static int opt_all(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_header = true; + q->style.show_section = true; + q->style.show_edns = true; + q->style.show_question = true; + q->style.show_answer = true; + q->style.show_authority = true; + q->style.show_additional = true; + q->style.show_tsig = true; + q->style.show_footer = true; + + return KNOT_EOK; +} + +static int opt_noall(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_header = false; + q->style.show_section = false; + q->style.show_edns = false; + q->style.show_query = false; + q->style.show_question = false; + q->style.show_answer = false; + q->style.show_authority = false; + q->style.show_additional = false; + q->style.show_tsig = false; + q->style.show_footer = false; + + return KNOT_EOK; +} + +static int opt_qr(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_query = true; + + return KNOT_EOK; +} + +static int opt_noqr(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_query = false; + + return KNOT_EOK; +} + +static int opt_header(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_header = true; + + return KNOT_EOK; +} + +static int opt_noheader(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_header = false; + + return KNOT_EOK; +} + +static int opt_comments(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_section = true; + + return KNOT_EOK; +} + +static int opt_nocomments(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_section = false; + + return KNOT_EOK; +} + +static int opt_opt(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_edns = true; + + return KNOT_EOK; +} + +static int opt_noopt(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_edns = false; + + return KNOT_EOK; +} + +static int opt_opttext(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_edns_opt_text = true; + + return KNOT_EOK; +} + +static int opt_noopttext(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_edns_opt_text = false; + + return KNOT_EOK; +} + +static int opt_question(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_question = true; + + return KNOT_EOK; +} + +static int opt_noquestion(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_question = false; + + return KNOT_EOK; +} + +static int opt_optpresent(const char *arg, void *query) +{ + query_t *q = query; + + q->style.present_edns = true; + + return KNOT_EOK; +} + +static int opt_nooptpresent(const char *arg, void *query) +{ + query_t *q = query; + + q->style.present_edns = false; + + return KNOT_EOK; +} + +static int opt_answer(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_answer = true; + + return KNOT_EOK; +} + +static int opt_noanswer(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_answer = false; + + return KNOT_EOK; +} + +static int opt_authority(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_authority = true; + + return KNOT_EOK; +} + +static int opt_noauthority(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_authority = false; + + return KNOT_EOK; +} + +static int opt_additional(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_additional = true; + + return KNOT_EOK; +} + +static int opt_noadditional(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_additional = false; + q->style.show_edns = false; + q->style.show_tsig = false; + + return KNOT_EOK; +} + +static int opt_tsig(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_tsig = true; + + return KNOT_EOK; +} + +static int opt_notsig(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_tsig = false; + + return KNOT_EOK; +} + +static int opt_stats(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_footer = true; + + return KNOT_EOK; +} + +static int opt_nostats(const char *arg, void *query) +{ + query_t *q = query; + + q->style.show_footer = false; + + return KNOT_EOK; +} + +static int opt_class(const char *arg, void *query) +{ + query_t *q = query; + + q->style.style.show_class = true; + + return KNOT_EOK; +} + +static int opt_noclass(const char *arg, void *query) +{ + query_t *q = query; + + q->style.style.show_class = false; + + return KNOT_EOK; +} + +static int opt_ttl(const char *arg, void *query) +{ + query_t *q = query; + + q->style.style.show_ttl = true; + + return KNOT_EOK; +} + +static int opt_nottl(const char *arg, void *query) +{ + query_t *q = query; + + q->style.style.show_ttl = false; + + return KNOT_EOK; +} + +static int opt_ignore(const char *arg, void *query) +{ + query_t *q = query; + + q->ignore_tc = true; + + return KNOT_EOK; +} + +static int opt_noignore(const char *arg, void *query) +{ + query_t *q = query; + + q->ignore_tc = false; + + return KNOT_EOK; +} + +static int opt_crypto(const char *arg, void *query) +{ + query_t *q = query; + + q->style.style.hide_crypto = false; + + return KNOT_EOK; +} + +static int opt_nocrypto(const char *arg, void *query) +{ + query_t *q = query; + + q->style.style.hide_crypto = true; + + return KNOT_EOK; +} + +static int opt_tcp(const char *arg, void *query) +{ + query_t *q = query; + + q->protocol = PROTO_TCP; + + return KNOT_EOK; +} + +static int opt_notcp(const char *arg, void *query) +{ + query_t *q = query; + + q->protocol = PROTO_UDP; + return opt_ignore(arg, query); +} + +static int opt_fastopen(const char *arg, void *query) +{ + query_t *q = query; + + q->fastopen = true; + + return opt_tcp(arg, query); +} + +static int opt_nofastopen(const char *arg, void *query) +{ + query_t *q = query; + + q->fastopen = false; + + return KNOT_EOK; +} + +static int opt_keepopen(const char *arg, void *query) +{ + query_t *q = query; + + q->keepopen = true; + + return KNOT_EOK; +} + +static int opt_nokeepopen(const char *arg, void *query) +{ + query_t *q = query; + + q->keepopen = false; + + return KNOT_EOK; +} + +static int opt_tls(const char *arg, void *query) +{ + query_t *q = query; + + q->tls.enable = true; + if (q->quic.enable) { + return KNOT_EOK; + } + return opt_tcp(arg, query); +} + +static int opt_notls(const char *arg, void *query) +{ + query_t *q = query; + + tls_params_clean(&q->tls); + tls_params_init(&q->tls); + + return KNOT_EOK; +} + +static int opt_tls_ca(const char *arg, void *query) +{ + query_t *q = query; + + if (arg == NULL) { + q->tls.system_ca = true; + return opt_tls(arg, query); + } else { + if (ptrlist_add(&q->tls.ca_files, strdup(arg), NULL) == NULL) { + return KNOT_ENOMEM; + } + return opt_tls(arg, query); + } +} + +static int opt_notls_ca(const char *arg, void *query) +{ + query_t *q = query; + + q->tls.system_ca = false; + + ptrnode_t *node, *nxt; + WALK_LIST_DELSAFE(node, nxt, q->tls.ca_files) { + free(node->d); + } + ptrlist_free(&q->tls.ca_files, NULL); + + return KNOT_EOK; +} + +static int opt_tls_pin(const char *arg, void *query) +{ + query_t *q = query; + + uint8_t pin[64] = { 0 }; + + int ret = knot_base64_decode((const uint8_t *)arg, strlen(arg), pin, sizeof(pin)); + if (ret < 0) { + ERR("invalid +tls-pin=%s", arg); + return ret; + } else if (ret != CERT_PIN_LEN) { // Check for 256-bit value. + ERR("invalid sha256 hash length +tls-pin=%s", arg); + return KNOT_EINVAL; + } + + uint8_t *item = malloc(1 + ret); // 1 ~ leading data length. + if (item == NULL) { + return KNOT_ENOMEM; + } + item[0] = ret; + memcpy(&item[1], pin, ret); + + if (ptrlist_add(&q->tls.pins, item, NULL) == NULL) { + return KNOT_ENOMEM; + } + + return opt_tls(arg, query); +} + +static int opt_notls_pin(const char *arg, void *query) +{ + query_t *q = query; + + ptrnode_t *node, *nxt; + WALK_LIST_DELSAFE(node, nxt, q->tls.pins) { + free(node->d); + } + ptrlist_free(&q->tls.pins, NULL); + + return KNOT_EOK; +} + +static int opt_tls_hostname(const char *arg, void *query) +{ + query_t *q = query; + + free(q->tls.hostname); + q->tls.hostname = strdup(arg); + + return opt_tls(arg, query); +} + +static int opt_notls_hostname(const char *arg, void *query) +{ + query_t *q = query; + + free(q->tls.hostname); + q->tls.hostname = NULL; + + return KNOT_EOK; +} + +static int opt_tls_sni(const char *arg, void *query) +{ + query_t *q = query; + + free(q->tls.sni); + q->tls.sni = strdup(arg); + + return opt_tls(arg, query); +} + +static int opt_notls_sni(const char *arg, void *query) +{ + query_t *q = query; + + free(q->tls.sni); + q->tls.sni = NULL; + + return KNOT_EOK; +} + +static int opt_tls_keyfile(const char *arg, void *query) +{ + query_t *q = query; + + free(q->tls.keyfile); + q->tls.keyfile = strdup(arg); + + return opt_tls(arg, query); +} + +static int opt_notls_keyfile(const char *arg, void *query) +{ + query_t *q = query; + + free(q->tls.keyfile); + q->tls.keyfile = NULL; + + return KNOT_EOK; +} + +static int opt_tls_certfile(const char *arg, void *query) +{ + query_t *q = query; + + free(q->tls.certfile); + q->tls.certfile = strdup(arg); + + return opt_tls(arg, query); +} + +static int opt_notls_certfile(const char *arg, void *query) +{ + query_t *q = query; + + free(q->tls.certfile); + q->tls.certfile = NULL; + + return KNOT_EOK; +} + +static int opt_tls_ocsp_stapling(const char *arg, void *query) +{ + query_t *q = query; + + if (arg == NULL) { + q->tls.ocsp_stapling = DEFAULT_TLS_OCSP_STAPLING; + return opt_tls(arg, query); + } else { + uint32_t num = 0; + if (str_to_u32(arg, &num) != KNOT_EOK || num == 0) { + ERR("invalid +tls-ocsp-stapling=%s", arg); + return KNOT_EINVAL; + } + + q->tls.ocsp_stapling = 3600 * num; + return opt_tls(arg, query); + } +} + +static int opt_notls_ocsp_stapling(const char *arg, void *query) +{ + query_t *q = query; + + q->tls.ocsp_stapling = 0; + + return KNOT_EOK; +} + +static int opt_https(const char *arg, void *query) +{ +#ifdef LIBNGHTTP2 + query_t *q = query; + + q->https.enable = true; + + if (arg != NULL) { + char *resource = strstr(arg, "://"); + if (resource == NULL) { + resource = (char *)arg; + } else { + resource += 3; // strlen("://") + if (*resource == '\0') { + ERR("invalid +https=%s", arg); + return KNOT_EINVAL; + } + } + + char *tmp_path = strchr(resource, '/'); + if (tmp_path) { + free(q->https.path); + q->https.path = strdup(tmp_path); + + if (tmp_path != resource) { + free(q->tls.hostname); + q->tls.hostname = strndup(resource, (size_t)(tmp_path - resource)); + } + return opt_tls(arg, query); + } else { + return opt_tls_hostname(arg, query); + } + + } + + return opt_tls(arg, query); + +#else + return KNOT_ENOTSUP; +#endif //LIBNGHTTP2 +} + +static int opt_nohttps(const char *arg, void *query) +{ +#ifdef LIBNGHTTP2 + query_t *q = query; + + https_params_clean(&q->https); + + return opt_notls(arg, query); +#else + return KNOT_ENOTSUP; +#endif //LIBNGHTTP2 +} + +static int opt_https_get(const char *arg, void *query) +{ +#ifdef LIBNGHTTP2 + query_t *q = query; + + q->https.method = GET; + + return opt_https(arg, query); +#else + return KNOT_ENOTSUP; +#endif //LIBNGHTTP2 +} + +static int opt_nohttps_get(const char *arg, void *query) +{ +#ifdef LIBNGHTTP2 + query_t *q = query; + + q->https.method = POST; + + return KNOT_EOK; +#else + return KNOT_ENOTSUP; +#endif +} + +static int opt_quic(const char *arg, void *query) +{ +#ifdef ENABLE_QUIC + query_t *q = query; + + q->quic.enable = true; + + opt_tls(arg, query); + opt_notcp(arg, query); + + return KNOT_EOK; +#else + return KNOT_ENOTSUP; +#endif //ENABLE_QUIC +} + +static int opt_noquic(const char *arg, void *query) +{ +#ifdef ENABLE_QUIC + query_t *q = query; + + quic_params_clean(&q->quic); + + return opt_notls(arg, query); +#else + return KNOT_ENOTSUP; +#endif //ENABLE_QUIC +} + +static int opt_nsid(const char *arg, void *query) +{ + query_t *q = query; + + q->nsid = true; + + return KNOT_EOK; +} + +static int opt_nonsid(const char *arg, void *query) +{ + query_t *q = query; + + q->nsid = false; + + return KNOT_EOK; +} + +static int opt_bufsize(const char *arg, void *query) +{ + query_t *q = query; + + uint16_t num = 0; + if (str_to_u16(arg, &num) != KNOT_EOK) { + ERR("invalid +bufsize=%s", arg); + return KNOT_EINVAL; + } + + // Disable EDNS if zero bufsize. + if (num == 0) { + q->udp_size = -1; + } else if (num < KNOT_WIRE_HEADER_SIZE) { + q->udp_size = KNOT_WIRE_HEADER_SIZE; + } else { + q->udp_size = num; + } + + return KNOT_EOK; +} + +static int opt_nobufsize(const char *arg, void *query) +{ + query_t *q = query; + + q->udp_size = -1; + + return KNOT_EOK; +} + +static int opt_cookie(const char *arg, void *query) +{ + query_t *q = query; + + if (arg != NULL) { + uint8_t *input = NULL; + size_t input_len; + + int ret = hex_decode(arg, &input, &input_len); + if (ret != KNOT_EOK) { + ERR("invalid +cookie=%s", arg); + return KNOT_EINVAL; + } + + if (input_len < KNOT_EDNS_COOKIE_CLNT_SIZE) { + ERR("too short client +cookie=%s", arg); + free(input); + return KNOT_EINVAL; + } + q->cc.len = KNOT_EDNS_COOKIE_CLNT_SIZE; + memcpy(q->cc.data, input, q->cc.len); + + input_len -= q->cc.len; + if (input_len > 0) { + if (input_len < KNOT_EDNS_COOKIE_SRVR_MIN_SIZE) { + ERR("too short server +cookie=%s", arg); + free(input); + return KNOT_EINVAL; + } + if (input_len > KNOT_EDNS_COOKIE_SRVR_MAX_SIZE) { + ERR("too long server +cookie=%s", arg); + free(input); + return KNOT_EINVAL; + } + q->sc.len = input_len; + memcpy(q->sc.data, input + q->cc.len, q->sc.len); + } + + free(input); + } else { + q->cc.len = KNOT_EDNS_COOKIE_CLNT_SIZE; + + int ret = dnssec_random_buffer(q->cc.data, q->cc.len); + if (ret != DNSSEC_EOK) { + return knot_error_from_libdnssec(ret); + } + } + + return KNOT_EOK; +} + +static int opt_nocookie(const char *arg, void *query) +{ + query_t *q = query; + q->cc.len = 0; + q->sc.len = 0; + return KNOT_EOK; +} + +static int opt_badcookie(const char *arg, void *query) +{ + query_t *q = query; + q->badcookie = BADCOOKIE_RETRY_MAX; + return KNOT_EOK; +} + +static int opt_nobadcookie(const char *arg, void *query) +{ + query_t *q = query; + q->badcookie = 0; + return KNOT_EOK; +} + +static int opt_padding(const char *arg, void *query) +{ + query_t *q = query; + + if (arg == NULL) { + q->padding = -2; + return KNOT_EOK; + } else { + uint16_t num = 0; + if (str_to_u16(arg, &num) != KNOT_EOK) { + ERR("invalid +padding=%s", arg); + return KNOT_EINVAL; + } + + q->padding = num; + return KNOT_EOK; + } +} + +static int opt_nopadding(const char *arg, void *query) +{ + query_t *q = query; + + q->padding = -3; + + return KNOT_EOK; +} + +static int opt_alignment(const char *arg, void *query) +{ + query_t *q = query; + + if (arg == NULL) { + q->alignment = DEFAULT_ALIGNMENT_SIZE; + return KNOT_EOK; + } else { + uint16_t num = 0; + if (str_to_u16(arg, &num) != KNOT_EOK || num < 2) { + ERR("invalid +alignment=%s", arg); + return KNOT_EINVAL; + } + + q->alignment = num; + return KNOT_EOK; + } +} + +static int opt_noalignment(const char *arg, void *query) +{ + query_t *q = query; + + q->alignment = 0; + + return KNOT_EOK; +} + +static int opt_subnet(const char *arg, void *query) +{ + query_t *q = query; + + char *sep = NULL; + const size_t arg_len = strlen(arg); + const char *arg_end = arg + arg_len; + size_t addr_len = 0; + + // Separate address and network mask. + if ((sep = strchr(arg, '/')) != NULL) { + addr_len = sep - arg; + } else { + addr_len = arg_len; + } + + // Check IP address. + + struct sockaddr_storage ss = { 0 }; + struct addrinfo hints = { .ai_flags = AI_NUMERICHOST }; + struct addrinfo *ai = NULL; + + char *addr_str = strndup(arg, addr_len); + if (getaddrinfo(addr_str, NULL, &hints, &ai) != 0) { + free(addr_str); + ERR("invalid address +subnet=%s", arg); + return KNOT_EINVAL; + } + + memcpy(&ss, ai->ai_addr, ai->ai_addrlen); + freeaddrinfo(ai); + free(addr_str); + + if (knot_edns_client_subnet_set_addr(&q->subnet, &ss) != KNOT_EOK) { + ERR("invalid address +subnet=%s", arg); + return KNOT_EINVAL; + } + + // Parse network mask. + const char *mask = arg; + if (mask + addr_len < arg_end) { + mask += addr_len + 1; + uint8_t num = 0; + if (str_to_u8(mask, &num) != KNOT_EOK || num > q->subnet.source_len) { + ERR("invalid network mask +subnet=%s", arg); + return KNOT_EINVAL; + } + q->subnet.source_len = num; + } + + return KNOT_EOK; +} + +static int opt_nosubnet(const char *arg, void *query) +{ + query_t *q = query; + + q->subnet.family = AF_UNSPEC; + + return KNOT_EOK; +} + +static int opt_edns(const char *arg, void *query) +{ + query_t *q = query; + + if (arg == NULL) { + q->edns = 0; + return KNOT_EOK; + } else { + uint8_t num = 0; + if (str_to_u8(arg, &num) != KNOT_EOK) { + ERR("invalid +edns=%s", arg); + return KNOT_EINVAL; + } + + q->edns = num; + return KNOT_EOK; + } +} + +static int opt_noedns(const char *arg, void *query) +{ + query_t *q = query; + + q->edns = -1; + opt_nodoflag(arg, query); + opt_nonsid(arg, query); + opt_nobufsize(arg, query); + opt_nocookie(arg, query); + opt_nopadding(arg, query); + opt_noalignment(arg, query); + opt_nosubnet(arg, query); + + return KNOT_EOK; +} + +static int opt_timeout(const char *arg, void *query) +{ + query_t *q = query; + + if (params_parse_wait(arg, &q->wait) != KNOT_EOK) { + ERR("invalid +timeout=%s", arg); + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +static int opt_notimeout(const char *arg, void *query) +{ + query_t *q = query; + + (void)params_parse_wait("0", &q->wait); + + return KNOT_EOK; +} + +static int opt_retry(const char *arg, void *query) +{ + query_t *q = query; + + if (str_to_u32(arg, &q->retries) != KNOT_EOK) { + ERR("invalid +retry=%s", arg); + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +static int opt_noretry(const char *arg, void *query) +{ + query_t *q = query; + + q->retries = 0; + + return KNOT_EOK; +} + +static int opt_expire(const char *arg, void *query) +{ + query_t *q = query; + + ednsopt_t *opt = ednsopt_create(KNOT_EDNS_OPTION_EXPIRE, 0, NULL); + add_tail(&q->edns_opts, &opt->n); + + return KNOT_EOK; +} + +static int opt_noexpire(const char *arg, void *query) +{ + query_t *q = query; + + ednsopt_t *node, *nxt; + WALK_LIST_DELSAFE(node, nxt, q->edns_opts) { + ednsopt_t *opt = node; + if (opt->code == KNOT_EDNS_OPTION_EXPIRE) { + rem_node(&opt->n); + ednsopt_free(opt); + } + } + + return KNOT_EOK; +} + +static int parse_ednsopt(const char *arg, ednsopt_t **opt_ptr) +{ + errno = 0; + char *end = NULL; + unsigned long code = strtoul(arg, &end, 10); + if (errno != 0 || arg == end || code > UINT16_MAX) { + return KNOT_EINVAL; + } + + size_t length = 0; + uint8_t *data = NULL; + if (end[0] == ':') { + if (end[1] != '\0') { + int ret = hex_decode(end + 1, &data, &length); + if (ret != KNOT_EOK) { + return ret; + } + if (length > UINT16_MAX) { + free(data); + return KNOT_ERANGE; + } + } + } else if (end[0] != '\0') { + return KNOT_EINVAL; + } + + ednsopt_t *opt = ednsopt_create(code, length, data); + if (opt == NULL) { + free(data); + return KNOT_ENOMEM; + } + + *opt_ptr = opt; + return KNOT_EOK; +} + +static int opt_ednsopt(const char *arg, void *query) +{ + query_t *q = query; + + ednsopt_t *opt = NULL; + int ret = parse_ednsopt(arg, &opt); + if (ret != KNOT_EOK) { + ERR("invalid +ednsopt=%s", arg); + return KNOT_EINVAL; + } + + add_tail(&q->edns_opts, &opt->n); + + return KNOT_EOK; +} + +static int opt_noednsopt(const char *arg, void *query) +{ + query_t *q = query; + + ednsopt_list_deinit(&q->edns_opts); + + return KNOT_EOK; +} + +static int opt_noidn(const char *arg, void *query) +{ + query_t *q = query; + + q->idn = false; + q->style.style.ascii_to_idn = NULL; + + return KNOT_EOK; +} + +static int opt_json(const char *arg, void *query) +{ + query_t *q = query; + + q->style.format = FORMAT_JSON; + + return KNOT_EOK; +} + +static int opt_nojson(const char *arg, void *query) +{ + query_t *q = query; + + q->style.format = FORMAT_FULL; + + return KNOT_EOK; +} + +static int parse_addr(struct sockaddr_storage *addr, const char *arg, const char *def_port) +{ + srv_info_t *info = parse_nameserver(arg, def_port); + if (info == NULL) { + return KNOT_EINVAL; + } + + struct addrinfo *addr_info = NULL; + int ret = getaddrinfo(info->name, info->service, NULL, &addr_info); + srv_info_free(info); + if (ret != 0) { + return KNOT_EINVAL; + } + + memcpy(addr, addr_info->ai_addr, addr_info->ai_addrlen); + freeaddrinfo(addr_info); + + return KNOT_EOK; +} + +static int opt_proxy(const char *arg, void *query) +{ + query_t *q = query; + + const char *sep = strchr(arg, '-'); + if (sep == NULL || sep == arg || *(sep + 1) == '\0') { + ERR("invalid specification +proxy=%s", arg); + return KNOT_EINVAL; + } + + char *src = strndup(arg, sep - arg); + int ret = parse_addr(&q->proxy.src, src, "0"); + if (ret != KNOT_EOK) { + ERR("invalid proxy source address '%s'", src); + free(src); + return KNOT_EINVAL; + } + + const char *dst = sep + 1; + ret = parse_addr(&q->proxy.dst, dst, "53"); + if (ret != KNOT_EOK) { + ERR("invalid proxy destination address '%s'", dst); + free(src); + return KNOT_EINVAL; + } + + if (q->proxy.src.ss_family != q->proxy.dst.ss_family) { + ERR("proxy address family mismatch '%s' versus '%s'", src, dst); + free(src); + return KNOT_EINVAL; + } + free(src); + + return KNOT_EOK; +} + +static int opt_noproxy(const char *arg, void *query) +{ + query_t *q = query; + + q->proxy.src.ss_family = 0; + q->proxy.dst.ss_family = 0; + + return KNOT_EOK; +} + +static const param_t kdig_opts2[] = { + { "multiline", ARG_NONE, opt_multiline }, + { "nomultiline", ARG_NONE, opt_nomultiline }, + + { "short", ARG_NONE, opt_short }, + { "noshort", ARG_NONE, opt_noshort }, + + { "generic", ARG_NONE, opt_generic }, + { "nogeneric", ARG_NONE, opt_nogeneric }, + + { "aaflag", ARG_NONE, opt_aaflag }, + { "noaaflag", ARG_NONE, opt_noaaflag }, + + { "tcflag", ARG_NONE, opt_tcflag }, + { "notcflag", ARG_NONE, opt_notcflag }, + + { "rdflag", ARG_NONE, opt_rdflag }, + { "nordflag", ARG_NONE, opt_nordflag }, + + { "recurse", ARG_NONE, opt_rdflag }, + { "norecurse", ARG_NONE, opt_nordflag }, + + { "raflag", ARG_NONE, opt_raflag }, + { "noraflag", ARG_NONE, opt_noraflag }, + + { "zflag", ARG_NONE, opt_zflag }, + { "nozflag", ARG_NONE, opt_nozflag }, + + { "adflag", ARG_NONE, opt_adflag }, + { "noadflag", ARG_NONE, opt_noadflag }, + + { "cdflag", ARG_NONE, opt_cdflag }, + { "nocdflag", ARG_NONE, opt_nocdflag }, + + { "dnssec", ARG_NONE, opt_doflag }, + { "nodnssec", ARG_NONE, opt_nodoflag }, + + { "all", ARG_NONE, opt_all }, + { "noall", ARG_NONE, opt_noall }, + + { "qr", ARG_NONE, opt_qr }, + { "noqr", ARG_NONE, opt_noqr }, + + { "header", ARG_NONE, opt_header }, + { "noheader", ARG_NONE, opt_noheader }, + + { "comments", ARG_NONE, opt_comments }, + { "nocomments", ARG_NONE, opt_nocomments }, + + { "opt", ARG_NONE, opt_opt }, + { "noopt", ARG_NONE, opt_noopt }, + + { "opttext", ARG_NONE, opt_opttext }, + { "noopttext", ARG_NONE, opt_noopttext }, + + { "optpresent", ARG_NONE, opt_optpresent }, + { "nooptpresent", ARG_NONE, opt_nooptpresent }, + + { "question", ARG_NONE, opt_question }, + { "noquestion", ARG_NONE, opt_noquestion }, + + { "answer", ARG_NONE, opt_answer }, + { "noanswer", ARG_NONE, opt_noanswer }, + + { "authority", ARG_NONE, opt_authority }, + { "noauthority", ARG_NONE, opt_noauthority }, + + { "additional", ARG_NONE, opt_additional }, + { "noadditional", ARG_NONE, opt_noadditional }, + + { "tsig", ARG_NONE, opt_tsig }, + { "notsig", ARG_NONE, opt_notsig }, + + { "stats", ARG_NONE, opt_stats }, + { "nostats", ARG_NONE, opt_nostats }, + + { "class", ARG_NONE, opt_class }, + { "noclass", ARG_NONE, opt_noclass }, + + { "ttl", ARG_NONE, opt_ttl }, + { "nottl", ARG_NONE, opt_nottl }, + + { "crypto", ARG_NONE, opt_crypto }, + { "nocrypto", ARG_NONE, opt_nocrypto }, + + { "tcp", ARG_NONE, opt_tcp }, + { "notcp", ARG_NONE, opt_notcp }, + + { "fastopen", ARG_NONE, opt_fastopen }, + { "nofastopen", ARG_NONE, opt_nofastopen }, + + { "ignore", ARG_NONE, opt_ignore }, + { "noignore", ARG_NONE, opt_noignore }, + + { "keepopen", ARG_NONE, opt_keepopen }, + { "nokeepopen", ARG_NONE, opt_nokeepopen }, + + { "tls", ARG_NONE, opt_tls }, + { "notls", ARG_NONE, opt_notls }, + + { "tls-ca", ARG_OPTIONAL, opt_tls_ca }, + { "notls-ca", ARG_NONE, opt_notls_ca }, + + { "tls-pin", ARG_REQUIRED, opt_tls_pin }, + { "notls-pin", ARG_NONE, opt_notls_pin }, + + { "tls-hostname", ARG_REQUIRED, opt_tls_hostname }, + { "notls-hostname", ARG_NONE, opt_notls_hostname }, + + { "tls-sni", ARG_REQUIRED, opt_tls_sni }, + { "notls-sni", ARG_NONE, opt_notls_sni }, + + { "tls-keyfile", ARG_REQUIRED, opt_tls_keyfile }, + { "notls-keyfile", ARG_NONE, opt_notls_keyfile }, + + { "tls-certfile", ARG_REQUIRED, opt_tls_certfile }, + { "notls-certfile", ARG_NONE, opt_notls_certfile }, + + { "tls-ocsp-stapling", ARG_OPTIONAL, opt_tls_ocsp_stapling }, + { "notls-ocsp-stapling", ARG_NONE, opt_notls_ocsp_stapling }, + + { "https", ARG_OPTIONAL, opt_https }, + { "nohttps", ARG_NONE, opt_nohttps }, + + { "https-get", ARG_NONE, opt_https_get }, + { "nohttps-get", ARG_NONE, opt_nohttps_get }, + + { "quic", ARG_NONE, opt_quic }, + { "noquic", ARG_NONE, opt_noquic }, + + { "nsid", ARG_NONE, opt_nsid }, + { "nonsid", ARG_NONE, opt_nonsid }, + + { "bufsize", ARG_REQUIRED, opt_bufsize }, + { "nobufsize", ARG_NONE, opt_nobufsize }, + + { "padding", ARG_OPTIONAL, opt_padding }, + { "nopadding", ARG_NONE, opt_nopadding }, + + { "alignment", ARG_OPTIONAL, opt_alignment }, + { "noalignment", ARG_NONE, opt_noalignment }, + + { "subnet", ARG_REQUIRED, opt_subnet }, + { "nosubnet", ARG_NONE, opt_nosubnet }, + + { "proxy", ARG_REQUIRED, opt_proxy }, + { "noproxy", ARG_NONE, opt_noproxy }, + + // Obsolete aliases. + { "client", ARG_REQUIRED, opt_subnet }, + { "noclient", ARG_NONE, opt_nosubnet }, + + { "edns", ARG_OPTIONAL, opt_edns }, + { "noedns", ARG_NONE, opt_noedns }, + + { "timeout", ARG_REQUIRED, opt_timeout }, + { "notimeout", ARG_NONE, opt_notimeout }, + + { "retry", ARG_REQUIRED, opt_retry }, + { "noretry", ARG_NONE, opt_noretry }, + + { "expire", ARG_NONE, opt_expire }, + { "noexpire", ARG_NONE, opt_noexpire }, + + { "cookie", ARG_OPTIONAL, opt_cookie }, + { "nocookie", ARG_NONE, opt_nocookie }, + + { "badcookie", ARG_NONE, opt_badcookie }, + { "nobadcookie", ARG_NONE, opt_nobadcookie }, + + { "ednsopt", ARG_REQUIRED, opt_ednsopt }, + { "noednsopt", ARG_NONE, opt_noednsopt }, + + { "json", ARG_NONE, opt_json }, + { "nojson", ARG_NONE, opt_nojson }, + + /* "idn" doesn't work since it must be called before query creation. */ + { "noidn", ARG_NONE, opt_noidn }, + + { NULL } +}; + +query_t *query_create(const char *owner, const query_t *conf) +{ + // Create output structure. + query_t *query = calloc(1, sizeof(query_t)); + + if (query == NULL) { + DBG_NULL; + return NULL; + } + + // Initialization with defaults or with reference query. + if (conf == NULL) { + query->conf = NULL; + query->local = NULL; + query->operation = OPERATION_QUERY; + query->ip = IP_ALL; + query->protocol = PROTO_ALL; + query->fastopen = false; + query->port = strdup(""); + query->udp_size = -1; + query->retries = DEFAULT_RETRIES_DIG; + query->wait = DEFAULT_TIMEOUT_DIG; + query->ignore_tc = false; + query->class_num = -1; + query->type_num = -1; + query->serial = -1; + query->notify = false; + query->flags = DEFAULT_FLAGS_DIG; + query->style = DEFAULT_STYLE_DIG; + query->style.style.now = knot_time(); + query->idn = true; + query->nsid = false; + query->edns = -1; + query->cc.len = 0; + query->sc.len = 0; + query->badcookie = BADCOOKIE_RETRY_MAX; + query->padding = -1; + query->alignment = 0; + tls_params_init(&query->tls); + //query->tsig_key + query->subnet.family = AF_UNSPEC; + ednsopt_list_init(&query->edns_opts); +#if USE_DNSTAP + query->dt_reader = NULL; + query->dt_writer = NULL; +#endif // USE_DNSTAP + } else { + *query = *conf; + query->conf = conf; + if (conf->local != NULL) { + query->local = srv_info_create(conf->local->name, + conf->local->service); + if (query->local == NULL) { + query_free(query); + return NULL; + } + } else { + query->local = NULL; + } + query->port = strdup(conf->port); + tls_params_copy(&query->tls, &conf->tls); + https_params_copy(&query->https, &conf->https); + quic_params_copy(&query->quic, &conf->quic); + if (conf->tsig_key.name != NULL) { + int ret = knot_tsig_key_copy(&query->tsig_key, + &conf->tsig_key); + if (ret != KNOT_EOK) { + query_free(query); + return NULL; + } + } + + int ret = ednsopt_list_dup(&query->edns_opts, &conf->edns_opts); + if (ret != KNOT_EOK) { + query_free(query); + return NULL; + } + +#if USE_DNSTAP + query->dt_reader = conf->dt_reader; + query->dt_writer = conf->dt_writer; +#endif // USE_DNSTAP + } + + // Initialize list of servers. + init_list(&query->servers); + + // Set the query owner if any. + if (owner != NULL) { + if ((query->owner = strdup(owner)) == NULL) { + query_free(query); + return NULL; + } + } + + // Check dynamic allocation. + if (query->port == NULL) { + query_free(query); + return NULL; + } + + return query; +} + +void query_free(query_t *query) +{ + node_t *n, *nxt; + + if (query == NULL) { + DBG_NULL; + return; + } + + // Cleanup servers. + WALK_LIST_DELSAFE(n, nxt, query->servers) { + srv_info_free((srv_info_t *)n); + } + + // Cleanup local address. + if (query->local != NULL) { + srv_info_free(query->local); + } + + tls_params_clean(&query->tls); + https_params_clean(&query->https); + quic_params_clean(&query->quic); + + // Cleanup signing key. + knot_tsig_key_deinit(&query->tsig_key); + + // Cleanup EDNS options. + ednsopt_list_deinit(&query->edns_opts); + +#if USE_DNSTAP + if (query->dt_reader != NULL) { + dt_reader_free(query->dt_reader); + } + if (query->dt_writer != NULL) { + // Global writer can be shared! + if (query->conf == NULL || + query->conf->dt_writer != query->dt_writer) { + dt_writer_free(query->dt_writer); + } + } +#endif // USE_DNSTAP + + free(query->owner); + free(query->port); + free(query); +} + +ednsopt_t *ednsopt_create(uint16_t code, uint16_t length, uint8_t *data) +{ + ednsopt_t *opt = calloc(1, sizeof(*opt)); + if (opt == NULL) { + return NULL; + } + + opt->code = code; + opt->length = length; + opt->data = data; + + return opt; +} + +ednsopt_t *ednsopt_dup(const ednsopt_t *opt) +{ + ednsopt_t *dup = calloc(1, sizeof(*opt)); + if (dup == NULL) { + return NULL; + } + + dup->code = opt->code; + dup->length = opt->length; + dup->data = memdup(opt->data, opt->length); + if (dup->data == NULL) { + free(dup); + return NULL; + } + + return dup; +} + +void ednsopt_free(ednsopt_t *opt) +{ + if (opt == NULL) { + return; + } + + free(opt->data); + free(opt); +} + +void ednsopt_list_init(list_t *list) +{ + init_list(list); +} + +void ednsopt_list_deinit(list_t *list) +{ + node_t *n, *next; + WALK_LIST_DELSAFE(n, next, *list) { + ednsopt_t *opt = (ednsopt_t *)n; + ednsopt_free(opt); + } + + init_list(list); +} + +int ednsopt_list_dup(list_t *dest, const list_t *src) +{ + list_t backup = *dest; + init_list(dest); + + node_t *n; + WALK_LIST(n, *src) { + ednsopt_t *opt = (ednsopt_t *)n; + ednsopt_t *dup = ednsopt_dup(opt); + if (dup == NULL) { + ednsopt_list_deinit(dest); + *dest = backup; + return KNOT_ENOMEM; + } + + add_tail(dest, &dup->n); + } + + return KNOT_EOK; +} + +bool ednsopt_list_empty(const list_t *list) +{ + return EMPTY_LIST(*list); +} + +int kdig_init(kdig_params_t *params) +{ + if (params == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + memset(params, 0, sizeof(*params)); + + params->stop = false; + + // Initialize list of queries. + init_list(¶ms->queries); + + // Create config query. + if ((params->config = query_create(NULL, NULL)) == NULL) { + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +void kdig_clean(kdig_params_t *params) +{ + node_t *n, *nxt; + + if (params == NULL) { + DBG_NULL; + return; + } + + // Clean up queries. + WALK_LIST_DELSAFE(n, nxt, params->queries) { + query_free((query_t *)n); + } + + // Clean up config. + query_free(params->config); + + // Clean up the structure. + memset(params, 0, sizeof(*params)); +} + +static int parse_class(const char *value, query_t *query) +{ + uint16_t rclass; + + if (params_parse_class(value, &rclass) != KNOT_EOK) { + return KNOT_EINVAL; + } + + query->class_num = rclass; + + return KNOT_EOK; +} + +static int parse_keyfile(const char *value, query_t *query) +{ + knot_tsig_key_deinit(&query->tsig_key); + + if (knot_tsig_key_init_file(&query->tsig_key, value) != KNOT_EOK) { + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +static int parse_local(const char *value, query_t *query) +{ + srv_info_t *local = parse_nameserver(value, "0"); + if (local == NULL) { + return KNOT_EINVAL; + } + + if (query->local != NULL) { + srv_info_free(query->local); + } + + query->local = local; + + return KNOT_EOK; +} + +static int parse_name(const char *value, list_t *queries, const query_t *conf) +{ + query_t *query = NULL; + char *ascii_name = (char *)value; + char *fqd_name = NULL; + + if (value != NULL) { + if (conf->idn) { + ascii_name = name_from_idn(value); + if (ascii_name == NULL) { + return KNOT_EINVAL; + } + } + + // If name is not FQDN, append trailing dot. + fqd_name = get_fqd_name(ascii_name); + + if (conf->idn) { + free(ascii_name); + } + } + + // Create new query. + query = query_create(fqd_name, conf); + + free(fqd_name); + + if (query == NULL) { + return KNOT_ENOMEM; + } + + // Add new query to the queries. + add_tail(queries, (node_t *)query); + + return KNOT_EOK; +} + +static int parse_port(const char *value, query_t *query) +{ + char **port; + + // Set current server port (last or query default). + if (list_size(&query->servers) > 0) { + srv_info_t *server = TAIL(query->servers); + port = &(server->service); + } else { + port = &(query->port); + } + + char *new_port = strdup(value); + + if (new_port == NULL) { + return KNOT_ENOMEM; + } + + // Deallocate old string. + free(*port); + + *port = new_port; + + return KNOT_EOK; +} + +static int parse_reverse(const char *value, list_t *queries, const query_t *conf) +{ + query_t *query = NULL; + + // Create reverse name. + char *reverse = get_reverse_name(value); + + if (reverse == NULL) { + return KNOT_EINVAL; + } + + // Create reverse query for given address. + query = query_create(reverse, conf); + + free(reverse); + + if (query == NULL) { + return KNOT_ENOMEM; + } + + // Set type for reverse query. + query->type_num = KNOT_RRTYPE_PTR; + + // Add new query to the queries. + add_tail(queries, (node_t *)query); + + return KNOT_EOK; +} + +static int parse_server(const char *value, kdig_params_t *params) +{ + query_t *query; + + // Set current query (last or config). + if (list_size(¶ms->queries) > 0) { + query = TAIL(params->queries); + } else { + query = params->config; + } + + if (params_parse_server(value, &query->servers, query->port) != KNOT_EOK) { + ERR("invalid server @%s", value); + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +static int parse_tsig(const char *value, query_t *query) +{ + knot_tsig_key_deinit(&query->tsig_key); + + if (knot_tsig_key_init_str(&query->tsig_key, value) != KNOT_EOK) { + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +static int parse_type(const char *value, query_t *query) +{ + uint16_t rtype; + int64_t serial; + bool notify; + + if (params_parse_type(value, &rtype, &serial, ¬ify) != KNOT_EOK) { + return KNOT_EINVAL; + } + + query->type_num = rtype; + query->serial = serial; + query->notify = notify; + + // If NOTIFY, reset default RD flag. + if (query->notify) { + query->flags.rd_flag = false; + } + + return KNOT_EOK; +} + +#if USE_DNSTAP +static int parse_dnstap_output(const char *value, query_t *query) +{ + if (query->dt_writer != NULL) { + if (query->conf == NULL || + query->conf->dt_writer != query->dt_writer) { + dt_writer_free(query->dt_writer); + } + } + + query->dt_writer = dt_writer_create(value, "kdig " PACKAGE_VERSION); + if (query->dt_writer == NULL) { + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +static int parse_dnstap_input(const char *value, query_t *query) +{ + // Just in case, shouldn't happen. + if (query->dt_reader != NULL) { + dt_reader_free(query->dt_reader); + } + + query->dt_reader = dt_reader_create(value); + if (query->dt_reader == NULL) { + return KNOT_EINVAL; + } + + return KNOT_EOK; +} +#endif // USE_DNSTAP + +static void complete_servers(query_t *query, const query_t *conf) +{ + node_t *n; + char *def_port; + + // Decide which default port use. + if (strlen(query->port) > 0) { + def_port = query->port; + } else if (strlen(conf->port) > 0) { + def_port = conf->port; + } else if (query->https.enable) { + def_port = DEFAULT_DNS_HTTPS_PORT; + } else if (query->quic.enable) { + def_port = DEFAULT_DNS_QUIC_PORT; + } else if (query->tls.enable) { + def_port = DEFAULT_DNS_TLS_PORT; + } else { + def_port = DEFAULT_DNS_PORT; + } + + // Complete specified nameservers if any. + if (list_size(&query->servers) > 0) { + WALK_LIST(n, query->servers) { + srv_info_t *s = (srv_info_t *)n; + + // If the port isn't specified yet use the default one. + if (strlen(s->service) == 0) { + free(s->service); + s->service = strdup(def_port); + if (s->service == NULL) { + WARN("can't set port %s", def_port); + return; + } + } + + // Use server name as hostname for TLS if necessary. + if (query->tls.enable && query->tls.hostname == NULL && + (query->tls.system_ca || !EMPTY_LIST(query->tls.ca_files))) { + query->tls.hostname = strdup(s->name); + } + } + // Use servers from config if any. + } else if (list_size(&conf->servers) > 0) { + WALK_LIST(n, conf->servers) { + srv_info_t *s = (srv_info_t *)n; + char *port = def_port; + + // If the port is already specified, use it. + if (strlen(s->service) > 0) { + port = s->service; + } + + srv_info_t *server = srv_info_create(s->name, port); + if (server == NULL) { + WARN("can't set nameserver %s port %s", + s->name, s->service); + return; + } + add_tail(&query->servers, (node_t *)server); + + // Use server name as hostname for TLS if necessary. + if (query->tls.enable && query->tls.hostname == NULL && + (query->tls.system_ca || !EMPTY_LIST(query->tls.ca_files))) { + query->tls.hostname = strdup(s->name); + } + } + // Use system specific. + } else { + get_nameservers(&query->servers, def_port); + } +} + +static bool compare_servers(list_t *s1, list_t *s2) +{ + if (list_size(s1) != list_size(s2)) { + return false; + } + + node_t *n1, *n2 = HEAD(*s2); + WALK_LIST(n1, *s1) { + srv_info_t *i1 = (srv_info_t *)n1, *i2 = (srv_info_t *)n2; + if (strcmp(i1->service, i2->service) != 0 || + strcmp(i1->name, i2->name) != 0) + { + return false; + } + n2 = n2->next; + } + return true; +} + +void complete_queries(list_t *queries, const query_t *conf) +{ + node_t *n; + + if (queries == NULL || conf == NULL) { + DBG_NULL; + return; + } + + // If there is no query, add default query: NS to ".". + if (list_size(queries) == 0) { + query_t *q = query_create(".", conf); + if (q == NULL) { + WARN("can't create query . NS IN"); + return; + } + q->class_num = KNOT_CLASS_IN; + q->type_num = KNOT_RRTYPE_NS; + add_tail(queries, (node_t *)q); + } + + WALK_LIST(n, *queries) { + query_t *q = (query_t *)n; + query_t *q_prev = (HEAD(*queries) != n) ? (query_t *)n->prev : NULL; + + // Fill class number if missing. + if (q->class_num < 0) { + if (conf->class_num >= 0) { + q->class_num = conf->class_num; + } else { + q->class_num = KNOT_CLASS_IN; + } + } + + // Fill type number if missing. + if (q->type_num < 0) { + if (conf->type_num >= 0) { + q->type_num = conf->type_num; + q->serial = conf->serial; + } else { + q->type_num = KNOT_RRTYPE_A; + } + } + + // Set zone transfer if any. + if (q->type_num == KNOT_RRTYPE_AXFR || + q->type_num == KNOT_RRTYPE_IXFR) { + q->operation = OPERATION_XFR; + } + + // Retries only apply to pure UDP. + if (q->protocol == PROTO_TCP || + q->tls.enable || q->https.enable || q->quic.enable) { + q->retries = 0; + } + + // Complete nameservers list. + complete_servers(q, conf); + + // Check if using previous connection makes sense. + if (q_prev != NULL && q_prev->keepopen && + (get_socktype(q->protocol, q->type_num) != + get_socktype(q_prev->protocol, q_prev->type_num) || + q->https.enable != q_prev->https.enable || + q->tls.enable != q_prev->tls.enable || + strcmp(q->port, q_prev->port) != 0 || + !compare_servers(&q->servers, &q_prev->servers))) + { + WARN("connection parameters mismatch for query (%s), " + "ignoring keepopen", q->owner); + q_prev->keepopen = false; + } + } +} + +static void print_help(void) +{ + printf("Usage: %s [-4] [-6] [-d] [-b address] [-c class] [-p port]\n" + " [-q name] [-t type] [-x address] [-k keyfile]\n" + " [-y [algo:]keyname:key] [-E tapfile] [-G tapfile]\n" + " name [type] [class] [@server]\n" + "\n" + " +[no]multiline Wrap long records to more lines.\n" + " +[no]short Show record data only.\n" + " +[no]generic Use generic representation format.\n" + " +[no]aaflag Set AA flag.\n" + " +[no]tcflag Set TC flag.\n" + " +[no]rdflag Set RD flag.\n" + " +[no]recurse Same as +[no]rdflag\n" + " +[no]raflag Set RA flag.\n" + " +[no]zflag Set zero flag bit.\n" + " +[no]adflag Set AD flag.\n" + " +[no]cdflag Set CD flag.\n" + " +[no]dnssec Set DO flag.\n" + " +[no]all Show all packet sections.\n" + " +[no]qr Show query packet.\n" + " +[no]header Show packet header.\n" + " +[no]comments Show commented section names.\n" + " +[no]opt Show EDNS pseudosection.\n" + " +[no]opttext Try to show unknown EDNS options as text.\n" + " +[no]optpresent Show EDNS in presenatation format.\n" + " +[no]question Show question section.\n" + " +[no]answer Show answer section.\n" + " +[no]authority Show authority section.\n" + " +[no]additional Show additional section.\n" + " +[no]tsig Show TSIG pseudosection.\n" + " +[no]stats Show trailing packet statistics.\n" + " +[no]class Show DNS class.\n" + " +[no]ttl Show TTL value.\n" + " +[no]crypto Show binary parts of RRSIGs and DNSKEYs.\n" + " +[no]tcp Use TCP protocol.\n" + " +[no]fastopen Use TCP Fast Open.\n" + " +[no]ignore Don't use TCP automatically if truncated.\n" + " +[no]keepopen Don't close the TCP connection to be reused.\n" + " +[no]tls Use TLS with Opportunistic privacy profile.\n" + " +[no]tls-ca[=FILE] Use TLS with Out-Of-Band privacy profile.\n" + " +[no]tls-pin=BASE64 Use TLS with pinned certificate.\n" + " +[no]tls-hostname=STR Use TLS with remote server hostname.\n" + " +[no]tls-sni=STR Use TLS with Server Name Indication.\n" + " +[no]tls-keyfile=FILE Use TLS with a client keyfile.\n" + " +[no]tls-certfile=FILE Use TLS with a client certfile.\n" + " +[no]tls-ocsp-stapling[=H] Use TLS with a valid stapled OCSP response for the\n" + " server certificate (%u or specify hours).\n" +#ifdef LIBNGHTTP2 + " +[no]https[=URL] Use HTTPS protocol. It's also possible to specify\n" + " URL as [authority][/path] where query will be sent.\n" + " +[no]https-get Use HTTPS protocol with GET method instead of POST.\n" +#endif +#ifdef ENABLE_QUIC + " +[no]quic Use QUIC protocol.\n" +#endif + " +[no]nsid Request NSID.\n" + " +[no]bufsize=B Set EDNS buffer size.\n" + " +[no]padding[=N] Pad with EDNS(0) (default or specify size).\n" + " +[no]alignment[=N] Pad with EDNS(0) to blocksize (%u or specify size).\n" + " +[no]subnet=SUBN Set EDNS(0) client subnet addr/prefix.\n" + " +[no]edns[=N] Use EDNS(=version).\n" + " +[no]timeout=T Set wait for reply interval in seconds.\n" + " +[no]retry=N Set number of retries.\n" + " +[no]expire Set the EXPIRE EDNS option.\n" + " +[no]cookie[=HEX] Attach EDNS(0) cookie to the query.\n" + " +[no]badcookie Repeat a query with the correct cookie.\n" + " +[no]ednsopt=CODE[:HEX] Set custom EDNS option.\n" + " +[no]proxy=SADDR-DADDR Add PROXYv2 header with src and dest addresses.\n" + " +[no]json Use JSON for output encoding (RFC 8427).\n" + " +noidn Disable IDN transformation.\n" + "\n" + " -h, --help Print the program help.\n" + " -V, --version Print the program version.\n", + PROGRAM_NAME, DEFAULT_TLS_OCSP_STAPLING / 3600, DEFAULT_ALIGNMENT_SIZE); +} + +static int parse_opt1(const char *opt, const char *value, kdig_params_t *params, + int *index) +{ + const char *val = value; + size_t len = strlen(opt); + int add = 1; + query_t *query; + + // Set current query (last or config). + if (list_size(¶ms->queries) > 0) { + query = TAIL(params->queries); + } else { + query = params->config; + } + + // If there is no space between option and argument. + if (len > 1) { + val = opt + 1; + add = 0; + } + + switch (opt[0]) { + case '4': + if (len > 1) { + ERR("invalid option -%s", opt); + return KNOT_ENOTSUP; + } + + query->ip = IP_4; + break; + case '6': + if (len > 1) { + ERR("invalid option -%s", opt); + return KNOT_ENOTSUP; + } + + query->ip = IP_6; + break; + case 'b': + if (val == NULL) { + ERR("missing address"); + return KNOT_EINVAL; + } + + if (parse_local(val, query) != KNOT_EOK) { + ERR("bad address %s", val); + return KNOT_EINVAL; + } + *index += add; + break; + case 'd': + msg_enable_debug(1); + break; + case 'h': + if (len > 1) { + ERR("invalid option -%s", opt); + return KNOT_ENOTSUP; + } + + print_help(); + params->stop = true; + break; + case 'c': + if (val == NULL) { + ERR("missing class"); + return KNOT_EINVAL; + } + + if (parse_class(val, query) != KNOT_EOK) { + ERR("bad class %s", val); + return KNOT_EINVAL; + } + *index += add; + break; + case 'k': + if (val == NULL) { + ERR("missing filename"); + return KNOT_EINVAL; + } + + if (parse_keyfile(val, query) != KNOT_EOK) { + ERR("bad keyfile %s", value); + return KNOT_EINVAL; + } + *index += add; + break; + case 'p': + if (val == NULL) { + ERR("missing port"); + return KNOT_EINVAL; + } + + if (parse_port(val, query) != KNOT_EOK) { + ERR("bad port %s", value); + return KNOT_EINVAL; + } + *index += add; + break; + case 'q': + // Allow empty QNAME. + if (parse_name(val, ¶ms->queries, params->config) + != KNOT_EOK) { + ERR("bad query name %s", val); + return KNOT_EINVAL; + } + *index += add; + break; + case 't': + if (val == NULL) { + ERR("missing type"); + return KNOT_EINVAL; + } + + if (parse_type(val, query) != KNOT_EOK) { + ERR("bad type %s", val); + return KNOT_EINVAL; + } + *index += add; + break; + case 'V': + if (len > 1) { + ERR("invalid option -%s", opt); + return KNOT_ENOTSUP; + } + + print_version(PROGRAM_NAME); + params->stop = true; + break; + case 'x': + if (val == NULL) { + ERR("missing address"); + return KNOT_EINVAL; + } + + if (parse_reverse(val, ¶ms->queries, params->config) + != KNOT_EOK) { + ERR("bad reverse name %s", val); + return KNOT_EINVAL; + } + *index += add; + break; + case 'y': + if (val == NULL) { + ERR("missing key"); + return KNOT_EINVAL; + } + + if (parse_tsig(val, query) != KNOT_EOK) { + ERR("bad key %s", value); + return KNOT_EINVAL; + } + *index += add; + break; + case 'E': +#if USE_DNSTAP + if (val == NULL) { + ERR("missing filename"); + return KNOT_EINVAL; + } + + if (parse_dnstap_output(val, query) != KNOT_EOK) { + ERR("unable to open dnstap output file %s", val); + return KNOT_EINVAL; + } + *index += add; +#else + ERR("no dnstap support but -E specified"); + return KNOT_EINVAL; +#endif // USE_DNSTAP + break; + case 'G': +#if USE_DNSTAP + if (val == NULL) { + ERR("missing filename"); + return KNOT_EINVAL; + } + + query = query_create(NULL, params->config); + if (query == NULL) { + return KNOT_ENOMEM; + } + + if (parse_dnstap_input(val, query) != KNOT_EOK) { + ERR("unable to open dnstap input file %s", val); + query_free(query); + return KNOT_EINVAL; + } + + query->operation = OPERATION_LIST_DNSTAP; + add_tail(¶ms->queries, (node_t *)query); + + *index += add; +#else + ERR("no dnstap support but -G specified"); + return KNOT_EINVAL; +#endif // USE_DNSTAP + break; + case '-': + if (strcmp(opt, "-help") == 0) { + print_help(); + params->stop = true; + } else if (strcmp(opt, "-version") == 0) { + print_version(PROGRAM_NAME); + params->stop = true; + } else { + ERR("invalid option: -%s", opt); + return KNOT_ENOTSUP; + } + break; + default: + ERR("invalid option: -%s", opt); + return KNOT_ENOTSUP; + } + + return KNOT_EOK; +} + +static int parse_opt2(const char *value, kdig_params_t *params) +{ + query_t *query; + + // Set current query (last or config). + if (list_size(¶ms->queries) > 0) { + query = TAIL(params->queries); + } else { + query = params->config; + } + + // Get option name. + const char *arg_sep = "="; + size_t opt_len = strcspn(value, arg_sep); + if (opt_len < 1) { + ERR("invalid option: +%s", value); + return KNOT_ENOTSUP; + } + + // Get option argument if any. + const char *arg = NULL; + const char *rest = value + opt_len; + if (strlen(rest) > 0) { + arg = rest + strspn(rest, arg_sep); + } + + // Check if the given option is supported. + bool unique; + int ret = best_param(value, opt_len, kdig_opts2, &unique); + if (ret < 0) { + ERR("invalid option: +%s", value); + return KNOT_ENOTSUP; + } else if (!unique) { + ERR("ambiguous option: +%s", value); + return KNOT_ENOTSUP; + } + + // Check argument presence. + switch (kdig_opts2[ret].arg) { + case ARG_NONE: + if (arg != NULL && *arg != '\0') { + WARN("superfluous option argument: +%s", value); + } + break; + case ARG_REQUIRED: + if (arg == NULL) { + ERR("missing argument: +%s", value); + return KNOT_EFEWDATA; + } + // FALLTHROUGH + case ARG_OPTIONAL: + if (arg != NULL && *arg == '\0') { + ERR("empty argument: +%s", value); + return KNOT_EFEWDATA; + } + break; + } + + // Call option handler. + return kdig_opts2[ret].handler(arg, query); +} + +static int parse_token(const char *value, kdig_params_t *params) +{ + query_t *query; + + // Set current query (last or config). + if (list_size(¶ms->queries) > 0) { + query = TAIL(params->queries); + } else { + query = params->config; + } + + // Try to guess the meaning of the token. + if (strlen(value) == 0) { + ERR("invalid empty parameter"); + } else if (parse_type(value, query) == KNOT_EOK) { + return KNOT_EOK; + } else if (parse_class(value, query) == KNOT_EOK) { + return KNOT_EOK; + } else if (parse_name(value, ¶ms->queries, params->config) == KNOT_EOK) { + return KNOT_EOK; + } else { + ERR("invalid parameter: %s", value); + } + + return KNOT_EINVAL; +} + +int kdig_parse(kdig_params_t *params, int argc, char *argv[]) +{ + if (params == NULL || argv == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + // Initialize parameters. + if (kdig_init(params) != KNOT_EOK) { + return KNOT_ERROR; + } + +#ifdef LIBIDN + // Set up localization. + if (setlocale(LC_CTYPE, "") == NULL) { + WARN("can't setlocale, disabling IDN"); + params->config->idn = false; + params->config->style.style.ascii_to_idn = NULL; + } +#endif + + // Command line parameters processing. + for (int i = 1; i < argc; i++) { + int ret = KNOT_ERROR; + + // Process parameter. + switch (argv[i][0]) { + case '@': + ret = parse_server(argv[i] + 1, params); + break; + case '-': + ret = parse_opt1(argv[i] + 1, argv[i + 1], params, &i); + break; + case '+': + ret = parse_opt2(argv[i] + 1, params); + break; + default: + ret = parse_token(argv[i], params); + break; + } + + // Check return. + switch (ret) { + case KNOT_EOK: + if (params->stop) { + return KNOT_EOK; + } + break; + case KNOT_ENOTSUP: + print_help(); + default: // Fall through. + return ret; + } + } + + // Complete missing data in queries based on defaults. + complete_queries(¶ms->queries, params->config); + + return KNOT_EOK; +} diff --git a/src/utils/kdig/kdig_params.h b/src/utils/kdig/kdig_params.h new file mode 100644 index 0000000..03aafe3 --- /dev/null +++ b/src/utils/kdig/kdig_params.h @@ -0,0 +1,187 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdbool.h> + +#include "utils/common/params.h" +#include "utils/common/exec.h" +#include "utils/common/https.h" +#include "utils/common/quic.h" +#include "utils/common/sign.h" +#include "libknot/libknot.h" +#include "contrib/sockaddr.h" + +#if USE_DNSTAP +# include "contrib/dnstap/reader.h" +# include "contrib/dnstap/writer.h" +#endif // USE_DNSTAP + +/*! \brief Operation mode of kdig. */ +typedef enum { + /*!< Standard 1-message query/reply. */ + OPERATION_QUERY, + /*!< Zone transfer (AXFR or IXFR). */ + OPERATION_XFR, + /*!< Dump dnstap file. */ + OPERATION_LIST_DNSTAP, +} operation_t; + +/*! \brief DNS header and EDNS flags. */ +typedef struct { + /*!< Authoritative answer flag. */ + bool aa_flag; + /*!< Truncated flag. */ + bool tc_flag; + /*!< Recursion desired flag. */ + bool rd_flag; + /*!< Recursion available flag. */ + bool ra_flag; + /*!< Z flag. */ + bool z_flag; + /*!< Authenticated data flag. */ + bool ad_flag; + /*!< Checking disabled flag. */ + bool cd_flag; + /*!< DNSSEC OK flag. */ + bool do_flag; +} flags_t; + +/*! \brief Basic parameters for DNS query. */ +typedef struct query query_t; // Forward declaration due to configuration. +struct query { + /*!< List node (for list container). */ + node_t n; + /*!< Reference to global config. */ + const query_t *conf; + /*!< Name to query on. */ + char *owner; + /*!< List of nameservers to query to. */ + list_t servers; + /*!< Local interface (optional). */ + srv_info_t *local; + /*!< Operation mode. */ + operation_t operation; + /*!< Version of ip protocol to use. */ + ip_t ip; + /*!< Protocol type (TCP, UDP) to use. */ + protocol_t protocol; + /*!< Use TCP Fast Open. */ + bool fastopen; + /*!< Keep TCP connection open. */ + bool keepopen; + /*!< Port/service to connect to. */ + char *port; + /*!< UDP buffer size (16unsigned + -1 uninitialized). */ + int32_t udp_size; + /*!< Number of UDP retries. */ + uint32_t retries; + /*!< Wait for network response in seconds (-1 means forever). */ + int32_t wait; + /*!< Ignore truncated response. */ + bool ignore_tc; + /*!< Class number (16unsigned + -1 uninitialized). */ + int32_t class_num; + /*!< Type number (16unsigned + -1 uninitialized). */ + int32_t type_num; + /*!< SOA serial for IXFR and NOTIFY (32unsigned + -1 uninitialized). */ + int64_t serial; + /*!< NOTIFY query. */ + bool notify; + /*!< Header flags. */ + flags_t flags; + /*!< Output settings. */ + style_t style; + /*!< IDN conversion. */ + bool idn; + /*!< Query for NSID. */ + bool nsid; + /*!< EDNS version (8unsigned + -1 uninitialized). */ + int16_t edns; + /*!< EDNS client cookie. */ + knot_edns_cookie_t cc; + /*!< EDNS server cookie. */ + knot_edns_cookie_t sc; + /*!< Repeat query after BADCOOKIE. */ + int badcookie; + /*!< EDNS0 padding (16unsigned + -1 ~ uninitialized, -2 ~ default, -3 ~ none). */ + int32_t padding; + /*!< Query alignment with EDNS0 padding (0 ~ uninitialized). */ + uint16_t alignment; + /*!< TLS parameters. */ + tls_params_t tls; + /*!< HTTPS parameters. */ + https_params_t https; + /*!< QUIC parameters. */ + quic_params_t quic; + /*!< Transaction signature. */ + knot_tsig_key_t tsig_key; + /*!< EDNS client subnet. */ + knot_edns_client_subnet_t subnet; + /*!< Lits of custom EDNS options. */ + list_t edns_opts; + /*!< PROXYv2 source and destination address. */ + struct { + struct sockaddr_storage src; + struct sockaddr_storage dst; + } proxy; +#if USE_DNSTAP + /*!< Context for dnstap reader input. */ + dt_reader_t *dt_reader; + /*!< Context for dnstap writer output. */ + dt_writer_t *dt_writer; +#endif // USE_DNSTAP +}; + +/*! \brief EDNS option data. */ +typedef struct { + /*! List node (for list container). */ + node_t n; + /*!< OPTION-CODE field. */ + uint16_t code; + /*!< OPTION-LENGTH field. */ + uint16_t length; + /*!< OPTION-DATA field. */ + uint8_t *data; +} ednsopt_t; + +/*! \brief Settings for kdig. */ +typedef struct { + /*!< Stop processing - just print help, version,... */ + bool stop; + /*!< List of DNS queries to process. */ + list_t queries; + /*!< Default settings for queries. */ + query_t *config; +} kdig_params_t; + +query_t *query_create(const char *owner, const query_t *config); +void query_free(query_t *query); +void complete_queries(list_t *queries, const query_t *conf); + +ednsopt_t *ednsopt_create(uint16_t code, uint16_t length, uint8_t *data); +ednsopt_t *ednsopt_dup(const ednsopt_t *opt); +void ednsopt_free(ednsopt_t *opt); + +void ednsopt_list_init(list_t *list); +void ednsopt_list_deinit(list_t *list); +int ednsopt_list_dup(list_t *dst, const list_t *src); +bool ednsopt_list_empty(const list_t *list); + +int kdig_init(kdig_params_t *params); +int kdig_parse(kdig_params_t *params, int argc, char *argv[]); +void kdig_clean(kdig_params_t *params); diff --git a/src/utils/keymgr/bind_privkey.c b/src/utils/keymgr/bind_privkey.c new file mode 100644 index 0000000..9ab895c --- /dev/null +++ b/src/utils/keymgr/bind_privkey.c @@ -0,0 +1,411 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <string.h> + +#include "contrib/ctype.h" +#include "contrib/strtonum.h" +#include "libdnssec/binary.h" +#include "libdnssec/error.h" +#include "libdnssec/pem.h" +#include "libdnssec/shared/shared.h" +#include "utils/keymgr/bind_privkey.h" + +/* -- private key params conversion ---------------------------------------- */ + +/*! + * Private key attribute conversion. + */ +typedef struct param_t { + char *name; + size_t offset; + int (*parse_cb)(char *string, void *data); + void (*free_cb)(void *data); +} param_t; + +static int parse_algorithm(char *string, void *_algorithm); +static int parse_binary(char *string, void *_binary); +static int parse_time(char *string, void *_time); + +static void binary_free(void *_binary) +{ + dnssec_binary_t *binary = _binary; + dnssec_binary_free(binary); +} + +/*! + * Know attributes in private key file. + */ +const param_t PRIVKEY_CONVERSION_TABLE[] = { + #define o(field) offsetof(bind_privkey_t, field) + { "Algorithm", o(algorithm), parse_algorithm, NULL }, + { "Modulus", o(modulus), parse_binary, binary_free }, + { "PublicExponent", o(public_exponent), parse_binary, binary_free }, + { "PrivateExponent", o(private_exponent), parse_binary, binary_free }, + { "Prime1", o(prime_one), parse_binary, binary_free }, + { "Prime2", o(prime_two), parse_binary, binary_free }, + { "Exponent1", o(exponent_one), parse_binary, binary_free }, + { "Exponent2", o(exponent_two), parse_binary, binary_free }, + { "Coefficient", o(coefficient), parse_binary, binary_free }, + { "PrivateKey", o(private_key), parse_binary, binary_free }, + { "Created", o(time_created), parse_time, NULL }, + { "Publish", o(time_publish), parse_time, NULL }, + { "Activate", o(time_activate), parse_time, NULL }, + { "Revoke", o(time_revoke), parse_time, NULL }, + { "Inactive", o(time_inactive), parse_time, NULL }, + { "Delete", o(time_delete), parse_time, NULL }, + { NULL } + #undef o +}; + +/* -- attribute parsing ---------------------------------------------------- */ + +/*! + * Parse key algorithm field. + * + * Example: 7 (NSEC3RSASHA1) + * + * Only the numeric value is decoded, the rest of the value is ignored. + */ +static int parse_algorithm(char *string, void *_algorithm) +{ + char *end = string; + while (*end != '\0' && !is_space(*end)) { + end += 1; + } + *end = '\0'; + + uint8_t *algorithm = _algorithm; + int r = str_to_u8(string, algorithm); + + return (r == KNOT_EOK ? DNSSEC_EOK : DNSSEC_INVALID_KEY_ALGORITHM); +} + +/*! + * Parse binary data encoded in Base64. + * + * Example: AQAB + */ +static int parse_binary(char *string, void *_binary) +{ + dnssec_binary_t base64 = { + .data = (uint8_t *)string, + .size = strlen(string) + }; + + dnssec_binary_t *binary = _binary; + return dnssec_binary_from_base64(&base64, binary); +} + +#define LEGACY_DATE_FORMAT "%Y%m%d%H%M%S" + +/*! + * Parse timestamp in a format in \ref LEGACY_DATE_FORMAT. + * + * Example: 20140415151855 + */ +static int parse_time(char *string, void *_time) +{ + struct tm tm = { 0 }; + + char *end = strptime(string, LEGACY_DATE_FORMAT, &tm); + if (end == NULL || *end != '\0') { + return DNSSEC_MALFORMED_DATA; + } + + time_t *time = _time; + *time = timegm(&tm); + + return DNSSEC_EOK; +} + +/* -- key parsing ---------------------------------------------------------- */ + +/*! + * Strip string value of left and right whitespaces. + * + * \param[in,out] value Start of the string. + * \param[in,out] length Length of the string. + * + */ +static void strip(char **value, size_t *length) +{ + // strip from left + while (*length > 0 && is_space(**value)) { + *value += 1; + *length -= 1; + } + // strip from right + while (*length > 0 && is_space((*value)[*length - 1])) { + *length -= 1; + } +} + +/*! + * Parse one line of the private key file. + */ +static int parse_line(bind_privkey_t *params, char *line, size_t length) +{ + assert(params); + assert(line); + + strip(&line, &length); + if (length == 0) { + return KNOT_EOK; // blank line + } + + char *separator = memchr(line, ':', length); + if (!separator) { + return DNSSEC_MALFORMED_DATA; + } + + char *key = line; + size_t key_length = separator - key; + strip(&key, &key_length); + + char *value = separator + 1; + size_t value_length = (line + length) - value; + strip(&value, &value_length); + + if (key_length == 0 || value_length == 0) { + return DNSSEC_MALFORMED_DATA; + } + + key[key_length] = '\0'; + value[value_length] = '\0'; + + for (const param_t *p = PRIVKEY_CONVERSION_TABLE; p->name != NULL; p++) { + size_t name_length = strlen(p->name); + if (name_length != key_length) { + continue; + } + + if (strcasecmp(p->name, key) != 0) { + continue; + } + + return p->parse_cb(value, (void *)params + p->offset); + } + + // ignore unknown attributes + + return DNSSEC_EOK; +} + +int bind_privkey_parse(const char *filename, bind_privkey_t *params_ptr) +{ + _cleanup_fclose_ FILE *file = fopen(filename, "r"); + if (!file) { + return DNSSEC_NOT_FOUND; + } + + bind_privkey_t params = *params_ptr; + + _cleanup_free_ char *line = NULL; + size_t size = 0; + ssize_t read = 0; + while ((read = getline(&line, &size, file)) != -1) { + int r = parse_line(¶ms, line, read); + if (r != DNSSEC_EOK) { + bind_privkey_free(¶ms); + return r; + } + } + + *params_ptr = params; + + return DNSSEC_EOK; +} + +/* -- freeing -------------------------------------------------------------- */ + +/*! + * Free private key parameters. + */ +void bind_privkey_free(bind_privkey_t *params) +{ + if (!params) { + return; + } + + for (const param_t *p = PRIVKEY_CONVERSION_TABLE; p->name != NULL; p++) { + if (p->free_cb) { + p->free_cb((void *)params + p->offset); + } + } + + clear_struct(params); +} + +/* -- export to PEM -------------------------------------------------------- */ + +static int rsa_params_to_pem(const bind_privkey_t *params, dnssec_binary_t *pem) +{ + _cleanup_x509_privkey_ gnutls_x509_privkey_t key = NULL; + int result = gnutls_x509_privkey_init(&key); + if (result != GNUTLS_E_SUCCESS) { + return DNSSEC_ENOMEM; + } + + gnutls_datum_t m = binary_to_datum(¶ms->modulus); + gnutls_datum_t e = binary_to_datum(¶ms->public_exponent); + gnutls_datum_t d = binary_to_datum(¶ms->private_exponent); + gnutls_datum_t p = binary_to_datum(¶ms->prime_one); + gnutls_datum_t q = binary_to_datum(¶ms->prime_two); + gnutls_datum_t u = binary_to_datum(¶ms->coefficient); + + result = gnutls_x509_privkey_import_rsa_raw(key, &m, &e, &d, &p, &q, &u); + if (result != GNUTLS_E_SUCCESS) { + return DNSSEC_KEY_IMPORT_ERROR; + } + + return dnssec_pem_from_x509(key, pem); +} + +/*! + * \see lib/key/convert.h + */ +static gnutls_ecc_curve_t choose_ecdsa_curve(size_t pubkey_size) +{ + switch (pubkey_size) { +#ifdef HAVE_ED25519 + case 32: return GNUTLS_ECC_CURVE_ED25519; +#endif +#ifdef HAVE_ED448 + case 57: return GNUTLS_ECC_CURVE_ED448; +#endif + case 64: return GNUTLS_ECC_CURVE_SECP256R1; + case 96: return GNUTLS_ECC_CURVE_SECP384R1; + default: return GNUTLS_ECC_CURVE_INVALID; + } +} + +static void ecdsa_extract_public_params(dnssec_key_t *key, gnutls_ecc_curve_t *curve, + gnutls_datum_t *x, gnutls_datum_t *y) +{ + dnssec_binary_t pubkey = { 0 }; + dnssec_key_get_pubkey(key, &pubkey); + + *curve = choose_ecdsa_curve(pubkey.size); + + size_t param_size = pubkey.size / 2; + x->data = pubkey.data; + x->size = param_size; + y->data = pubkey.data + param_size; + y->size = param_size; +} + +static int ecdsa_params_to_pem(dnssec_key_t *dnskey, const bind_privkey_t *params, + dnssec_binary_t *pem) +{ + _cleanup_x509_privkey_ gnutls_x509_privkey_t key = NULL; + int result = gnutls_x509_privkey_init(&key); + if (result != GNUTLS_E_SUCCESS) { + return DNSSEC_ENOMEM; + } + + gnutls_ecc_curve_t curve = 0; + gnutls_datum_t x = { 0 }; + gnutls_datum_t y = { 0 }; + ecdsa_extract_public_params(dnskey, &curve, &x, &y); + + gnutls_datum_t k = binary_to_datum(¶ms->private_key); + + result = gnutls_x509_privkey_import_ecc_raw(key, curve, &x, &y, &k); + if (result != DNSSEC_EOK) { + return DNSSEC_KEY_IMPORT_ERROR; + } + + gnutls_x509_privkey_fix(key); + + return dnssec_pem_from_x509(key, pem); +} + +#if defined(HAVE_ED25519) || defined(HAVE_ED448) +static void eddsa_extract_public_params(dnssec_key_t *key, gnutls_ecc_curve_t *curve, + gnutls_datum_t *x) +{ + dnssec_binary_t pubkey = { 0 }; + dnssec_key_get_pubkey(key, &pubkey); + + *curve = choose_ecdsa_curve(pubkey.size); + + x->data = pubkey.data; + x->size = pubkey.size; +} + +static int eddsa_params_to_pem(dnssec_key_t *dnskey, const bind_privkey_t *params, + dnssec_binary_t *pem) +{ + _cleanup_x509_privkey_ gnutls_x509_privkey_t key = NULL; + int result = gnutls_x509_privkey_init(&key); + if (result != GNUTLS_E_SUCCESS) { + return DNSSEC_ENOMEM; + } + + gnutls_ecc_curve_t curve = 0; + gnutls_datum_t x = { 0 }; + eddsa_extract_public_params(dnskey, &curve, &x); + + gnutls_datum_t k = binary_to_datum(¶ms->private_key); + + result = gnutls_x509_privkey_import_ecc_raw(key, curve, &x, NULL, &k); + if (result != DNSSEC_EOK) { + return DNSSEC_KEY_IMPORT_ERROR; + } + + gnutls_x509_privkey_fix(key); + + return dnssec_pem_from_x509(key, pem); +} +#endif + +int bind_privkey_to_pem(dnssec_key_t *key, bind_privkey_t *params, dnssec_binary_t *pem) +{ + dnssec_key_algorithm_t algorithm = dnssec_key_get_algorithm(key); + switch (algorithm) { + case DNSSEC_KEY_ALGORITHM_RSA_SHA1: + case DNSSEC_KEY_ALGORITHM_RSA_SHA1_NSEC3: + case DNSSEC_KEY_ALGORITHM_RSA_SHA256: + case DNSSEC_KEY_ALGORITHM_RSA_SHA512: + return rsa_params_to_pem(params, pem); + case DNSSEC_KEY_ALGORITHM_ECDSA_P256_SHA256: + case DNSSEC_KEY_ALGORITHM_ECDSA_P384_SHA384: + return ecdsa_params_to_pem(key, params, pem); +#ifdef HAVE_ED25519 + case DNSSEC_KEY_ALGORITHM_ED25519: +#endif +#ifdef HAVE_ED448 + case DNSSEC_KEY_ALGORITHM_ED448: +#endif +#if defined(HAVE_ED25519) || defined(HAVE_ED448) + return eddsa_params_to_pem(key, params, pem); +#endif + default: + return DNSSEC_INVALID_KEY_ALGORITHM; + } +} + +void bind_privkey_to_timing(bind_privkey_t *params, knot_kasp_key_timing_t *timing) +{ + timing->created = (knot_time_t)params->time_created; + timing->publish = (knot_time_t)params->time_publish; + timing->ready = 0; + timing->active = (knot_time_t)params->time_activate; + timing->retire = (knot_time_t)params->time_inactive; + timing->revoke = (knot_time_t)params->time_revoke; + timing->remove = (knot_time_t)params->time_delete; +} diff --git a/src/utils/keymgr/bind_privkey.h b/src/utils/keymgr/bind_privkey.h new file mode 100644 index 0000000..cdb4924 --- /dev/null +++ b/src/utils/keymgr/bind_privkey.h @@ -0,0 +1,72 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> +#include <time.h> + +#include "libdnssec/binary.h" +#include "knot/dnssec/kasp/policy.h" + +/*! + * Legacy private key parameters. + */ +typedef struct { + // key information + uint8_t algorithm; + + // RSA + dnssec_binary_t modulus; + dnssec_binary_t public_exponent; + dnssec_binary_t private_exponent; + dnssec_binary_t prime_one; + dnssec_binary_t prime_two; + dnssec_binary_t exponent_one; + dnssec_binary_t exponent_two; + dnssec_binary_t coefficient; + + // ECDSA + dnssec_binary_t private_key; + + // key lifetime + time_t time_created; + time_t time_publish; + time_t time_activate; + time_t time_revoke; + time_t time_inactive; + time_t time_delete; +} bind_privkey_t; + +/*! + * Extract parameters from legacy private key file. + */ +int bind_privkey_parse(const char *filename, bind_privkey_t *params); + +/*! + * Free private key parameters. + */ +void bind_privkey_free(bind_privkey_t *params); + +/*! + * Generate PEM from pub&priv key. + */ +int bind_privkey_to_pem(dnssec_key_t *key, bind_privkey_t *params, dnssec_binary_t *pem); + +/*! + * Extract timing info. + */ +void bind_privkey_to_timing(bind_privkey_t *params, knot_kasp_key_timing_t *timing); diff --git a/src/utils/keymgr/functions.c b/src/utils/keymgr/functions.c new file mode 100644 index 0000000..bd5345e --- /dev/null +++ b/src/utils/keymgr/functions.c @@ -0,0 +1,1161 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <limits.h> +#include <string.h> +#include <strings.h> +#include <time.h> +#include <fcntl.h> + +#include "utils/keymgr/functions.h" + +#include "utils/common/msg.h" +#include "utils/keymgr/bind_privkey.h" +#include "contrib/base64.h" +#include "contrib/color.h" +#include "contrib/ctype.h" +#include "contrib/json.h" +#include "contrib/string.h" +#include "contrib/strtonum.h" +#include "contrib/tolower.h" +#include "contrib/wire_ctx.h" +#include "libdnssec/error.h" +#include "libdnssec/keyid.h" +#include "libdnssec/shared/shared.h" +#include "knot/dnssec/kasp/policy.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-events.h" +#include "knot/dnssec/zone-keys.h" +#include "knot/dnssec/zone-sign.h" +#include "libzscanner/scanner.h" + +int parse_timestamp(char *arg, knot_time_t *stamp) +{ + int ret = knot_time_parse("YMDhms|'now'+-#u|'t'+-#u|+-#u|'t'+-#|+-#|#", + arg, stamp); + if (ret < 0) { + ERR2("invalid timestamp: %s", arg); + return KNOT_EINVAL; + } + return KNOT_EOK; +} + +static bool init_timestamps(char *arg, knot_kasp_key_timing_t *timing) +{ + knot_time_t *dst = NULL; + + if (strncasecmp(arg, "created=", 8) == 0) { + dst = &timing->created; + } else if (strncasecmp(arg, "publish=", 8) == 0) { + dst = &timing->publish; + } else if (strncasecmp(arg, "ready=", 6) == 0) { + dst = &timing->ready; + } else if (strncasecmp(arg, "active=", 7) == 0) { + dst = &timing->active; + } else if (strncasecmp(arg, "retire=", 7) == 0) { + dst = &timing->retire; + } else if (strncasecmp(arg, "remove=", 7) == 0) { + dst = &timing->remove; + } else if (strncasecmp(arg, "pre_active=", 11) == 0) { + dst = &timing->pre_active; + } else if (strncasecmp(arg, "post_active=", 12) == 0) { + dst = &timing->post_active; + } else if (strncasecmp(arg, "retire_active=", 14) == 0) { + dst = &timing->retire_active; + } else if (strncasecmp(arg, "revoke=", 7) == 0) { + dst = &timing->revoke; + } else { + return false; + } + + knot_time_t stamp; + int ret = parse_timestamp(strchr(arg, '=') + 1, &stamp); + if (ret != KNOT_EOK) { + return true; + } + + *dst = stamp; + + return true; +} + +static bool str2bool(const char *s) +{ + switch (knot_tolower(s[0])) { + case '1': + case 'y': + case 't': + return true; + default: + return false; + } +} + +static void bitmap_set(kdnssec_generate_flags_t *bitmap, int flag, bool onoff) +{ + if (onoff) { + *bitmap |= flag; + } else { + *bitmap &= ~flag; + } +} + +static bool genkeyargs(int argc, char *argv[], bool just_timing, + kdnssec_generate_flags_t *flags, dnssec_key_algorithm_t *algorithm, + uint16_t *keysize, knot_kasp_key_timing_t *timing, + const char **addtopolicy) +{ + // generate algorithms field + char *algnames[256] = { 0 }; + algnames[DNSSEC_KEY_ALGORITHM_RSA_SHA1] = "rsasha1"; + algnames[DNSSEC_KEY_ALGORITHM_RSA_SHA1_NSEC3] = "rsasha1nsec3sha1"; + algnames[DNSSEC_KEY_ALGORITHM_RSA_SHA256] = "rsasha256"; + algnames[DNSSEC_KEY_ALGORITHM_RSA_SHA512] = "rsasha512"; + algnames[DNSSEC_KEY_ALGORITHM_ECDSA_P256_SHA256] = "ecdsap256sha256"; + algnames[DNSSEC_KEY_ALGORITHM_ECDSA_P384_SHA384] = "ecdsap384sha384"; + algnames[DNSSEC_KEY_ALGORITHM_ED25519] = "ed25519"; + algnames[DNSSEC_KEY_ALGORITHM_ED448] = "ed448"; + + // parse args + for (int i = 0; i < argc; i++) { + if (!just_timing && strncasecmp(argv[i], "algorithm=", 10) == 0) { + int alg = 256; // invalid value + (void)str_to_int(argv[i] + 10, &alg, 0, 255); + for (int al = 0; al < 256 && alg > 255; al++) { + if (algnames[al] != NULL && + strcasecmp(argv[i] + 10, algnames[al]) == 0) { + alg = al; + } + } + if (alg > 255) { + ERR2("unknown algorithm: %s", argv[i] + 10); + return false; + } + *algorithm = alg; + } else if (strncasecmp(argv[i], "ksk=", 4) == 0) { + bitmap_set(flags, DNSKEY_GENERATE_KSK, str2bool(argv[i] + 4)); + } else if (strncasecmp(argv[i], "zsk=", 4) == 0) { + bitmap_set(flags, DNSKEY_GENERATE_ZSK, str2bool(argv[i] + 4)); + } else if (strncasecmp(argv[i], "sep=", 4) == 0) { + bitmap_set(flags, DNSKEY_GENERATE_SEP_SPEC, true); + bitmap_set(flags, DNSKEY_GENERATE_SEP_ON, str2bool(argv[i] + 4)); + } else if (!just_timing && strncasecmp(argv[i], "size=", 5) == 0) { + if (str_to_u16(argv[i] + 5, keysize) != KNOT_EOK) { + ERR2("invalid size: '%s'", argv[i] + 5); + return false; + } + } else if (!just_timing && strncasecmp(argv[i], "addtopolicy=", 12) == 0) { + *addtopolicy = argv[i] + 12; + } else if (!init_timestamps(argv[i], timing)) { + ERR2("invalid parameter: %s", argv[i]); + return false; + } + } + + return true; +} + +static bool _check_lower(knot_time_t a, knot_time_t b, + const char *a_name, const char *b_name) +{ + if (knot_time_cmp(a, b) > 0) { + ERR2("timestamp '%s' must be before '%s'", a_name, b_name); + return false; + } + return true; +} + +#define check_lower(t, a, b) if (!_check_lower(t->a, t->b, #a, #b)) return KNOT_ESEMCHECK + +static int check_timers(const knot_kasp_key_timing_t *t) +{ + if (t->pre_active != 0) { + check_lower(t, pre_active, publish); + } + check_lower(t, publish, active); + check_lower(t, active, retire_active); + check_lower(t, active, retire); + check_lower(t, active, post_active); + if (t->post_active == 0) { + check_lower(t, retire, remove); + } + return KNOT_EOK; +} + +#undef check_lower + +// modifies ctx->policy options, so don't do anything afterwards ! +int keymgr_generate_key(kdnssec_ctx_t *ctx, int argc, char *argv[]) +{ + knot_time_t now = knot_time(), infinity = 0; + knot_kasp_key_timing_t gen_timing = { now, infinity, now, infinity, now, infinity, infinity, infinity, infinity }; + kdnssec_generate_flags_t flags = 0; + uint16_t keysize = 0; + const char *addtopolicy = NULL; + if (!genkeyargs(argc, argv, false, &flags, &ctx->policy->algorithm, + &keysize, &gen_timing, &addtopolicy)) { + return KNOT_EINVAL; + } + + int ret = check_timers(&gen_timing); + if (ret != KNOT_EOK) { + return ret; + } + + if ((flags & DNSKEY_GENERATE_KSK) && gen_timing.ready == infinity) { + gen_timing.ready = gen_timing.active; + } + + if (keysize == 0) { + keysize = dnssec_algorithm_key_size_default(ctx->policy->algorithm); + } + if ((flags & DNSKEY_GENERATE_KSK)) { + ctx->policy->ksk_size = keysize; + } else { + ctx->policy->zsk_size = keysize; + } + + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *kasp_key = &ctx->zone->keys[i]; + if ((kasp_key->is_ksk && (flags & DNSKEY_GENERATE_KSK)) && + dnssec_key_get_algorithm(kasp_key->key) != ctx->policy->algorithm) { + WARN2("creating key with different algorithm than " + "configured in the policy"); + break; + } + } + + knot_kasp_key_t *key = NULL; + ret = kdnssec_generate_key(ctx, flags, &key); + if (ret != KNOT_EOK) { + return ret; + } + + key->timing = gen_timing; + + if (addtopolicy != NULL) { + char *last_policy_last = NULL; + + knot_dname_t *unused = NULL; + ret = kasp_db_get_policy_last(ctx->kasp_db, addtopolicy, &unused, + &last_policy_last); + knot_dname_free(unused, NULL); + if (ret != KNOT_EOK && ret != KNOT_ENOENT) { + free(last_policy_last); + return ret; + } + + ret = kasp_db_set_policy_last(ctx->kasp_db, addtopolicy, last_policy_last, + ctx->zone->dname, key->id); + free(last_policy_last); + if (ret != KNOT_EOK) { + return ret; + } + } + + ret = kdnssec_ctx_commit(ctx); + + if (ret == KNOT_EOK) { + printf("%s\n", key->id); + } + + return ret; +} + +static void parse_record(zs_scanner_t *scanner) +{ + dnssec_key_t *key = scanner->process.data; + + if (dnssec_key_get_dname(key) != NULL || + scanner->r_type != KNOT_RRTYPE_DNSKEY) { + scanner->state = ZS_STATE_STOP; + return; + } + + dnssec_binary_t rdata = { + .data = scanner->r_data, + .size = scanner->r_data_length + }; + dnssec_key_set_dname(key, scanner->dname); + dnssec_key_set_rdata(key, &rdata); +} + +int bind_pubkey_parse(const char *filename, dnssec_key_t **key_ptr) +{ + dnssec_key_t *key = NULL; + int result = dnssec_key_new(&key); + if (result != DNSSEC_EOK) { + return KNOT_ENOMEM; + } + + uint16_t cls = KNOT_CLASS_IN; + uint32_t ttl = 0; + zs_scanner_t *scanner = malloc(sizeof(zs_scanner_t)); + if (scanner == NULL) { + dnssec_key_free(key); + return KNOT_ENOMEM; + } + + if (zs_init(scanner, ".", cls, ttl) != 0 || + zs_set_input_file(scanner, filename) != 0 || + zs_set_processing(scanner, parse_record, NULL, key) != 0 || + zs_parse_all(scanner) != 0) { + int ret; + switch (scanner->error.code) { + case ZS_FILE_OPEN: + case ZS_FILE_INVALID: + ret = KNOT_EFILE; + break; + case ZS_FILE_ACCESS: + ret = KNOT_EFACCES; + break; + default: + ret = KNOT_EPARSEFAIL; + } + zs_deinit(scanner); + free(scanner); + dnssec_key_free(key); + return ret; + } + zs_deinit(scanner); + free(scanner); + + if (dnssec_key_get_dname(key) == NULL) { + dnssec_key_free(key); + return KNOT_INVALID_PUBLIC_KEY; + } + + *key_ptr = key; + return KNOT_EOK; +} + +static char *gen_keyfilename(const char *orig, const char *wantsuff, const char *altsuff) +{ + assert(orig && wantsuff && altsuff); + + const char *dot = strrchr(orig, '.'); + + if (dot != NULL && strcmp(dot, wantsuff) == 0) { // Full match. + return strdup(orig); + } else if (dot != NULL && strcmp(dot, altsuff) == 0) { // Replace suffix. + return sprintf_alloc("%.*s%s", (int)(dot - orig), orig, wantsuff); + } else { // Add wanted suffix. + return sprintf_alloc("%s%s", orig, wantsuff); + } +} + +int keymgr_import_bind(kdnssec_ctx_t *ctx, const char *import_file, bool pub_only) +{ + if (ctx == NULL || import_file == NULL) { + return KNOT_EINVAL; + } + + knot_kasp_key_timing_t timing = { 0 }; + dnssec_key_t *key = NULL; + char *keyid = NULL; + + char *pubname = gen_keyfilename(import_file, ".key", ".private"); + if (pubname == NULL) { + return KNOT_EINVAL; + } + + int ret = bind_pubkey_parse(pubname, &key); + free(pubname); + if (ret != KNOT_EOK) { + goto fail; + } + + if (!pub_only) { + bind_privkey_t bpriv = { .time_publish = ctx->now, .time_activate = ctx->now }; + + char *privname = gen_keyfilename(import_file, ".private", ".key"); + if (privname == NULL) { + goto fail; + } + + ret = bind_privkey_parse(privname, &bpriv); + free(privname); + if (ret != DNSSEC_EOK) { + goto fail; + } + + dnssec_binary_t pem = { 0 }; + ret = bind_privkey_to_pem(key, &bpriv, &pem); + if (ret != DNSSEC_EOK) { + bind_privkey_free(&bpriv); + goto fail; + } + + bind_privkey_to_timing(&bpriv, &timing); + + bind_privkey_free(&bpriv); + + ret = dnssec_keystore_import(ctx->keystore, &pem, &keyid); + dnssec_binary_free(&pem); + if (ret != DNSSEC_EOK) { + goto fail; + } + } else { + timing.publish = ctx->now; + + ret = dnssec_key_get_keyid(key, &keyid); + if (ret != DNSSEC_EOK) { + goto fail; + } + } + + // allocate kasp key + knot_kasp_key_t *kkey = calloc(1, sizeof(*kkey)); + if (!kkey) { + ret = KNOT_ENOMEM; + goto fail; + } + + kkey->id = keyid; + kkey->key = key; + kkey->timing = timing; + kkey->is_pub_only = pub_only; + kkey->is_ksk = (dnssec_key_get_flags(kkey->key) == DNSKEY_FLAGS_KSK); + kkey->is_zsk = !kkey->is_ksk; + + // append to zone + ret = kasp_zone_append(ctx->zone, kkey); + free(kkey); + if (ret != KNOT_EOK) { + goto fail; + } + ret = kdnssec_ctx_commit(ctx); + if (ret == KNOT_EOK) { + printf("%s\n", keyid); + return KNOT_EOK; + } +fail: + dnssec_key_free(key); + free(keyid); + return knot_error_from_libdnssec(ret); +} + +static void err_import_key(char *keyid, const char *file) +{ + ERR2("failed to get key%s%s from %s%s", + *keyid == '\0' ? "" : " ", keyid, + *file == '\0' ? "the keystore" : "file ", file); +} + +static int import_key(kdnssec_ctx_t *ctx, unsigned backend, const char *param, + int argc, char *argv[]) +{ + if (ctx == NULL || param == NULL) { + return KNOT_EINVAL; + } + + // parse params + knot_time_t now = knot_time(); + knot_kasp_key_timing_t timing = { .publish = now, .active = now }; + kdnssec_generate_flags_t flags = 0; + uint16_t keysize = 0; + if (!genkeyargs(argc, argv, false, &flags, &ctx->policy->algorithm, + &keysize, &timing, NULL)) { + return KNOT_EINVAL; + } + + int ret = check_timers(&timing); + if (ret != KNOT_EOK) { + return ret; + } + + normalize_generate_flags(&flags); + + dnssec_key_t *key = NULL; + char *keyid = NULL; + + if (backend == KEYSTORE_BACKEND_PEM) { + // open file + int fd = open(param, O_RDONLY, 0); + if (fd == -1) { + err_import_key("", param); + return knot_map_errno(); + } + + // determine size + off_t fsize = lseek(fd, 0, SEEK_END); + if (fsize == -1) { + close(fd); + err_import_key("", param); + return knot_map_errno(); + } + if (lseek(fd, 0, SEEK_SET) == -1) { + close(fd); + err_import_key("", param); + return knot_map_errno(); + } + + // alloc memory + dnssec_binary_t pem = { 0 }; + ret = dnssec_binary_alloc(&pem, fsize); + if (ret != DNSSEC_EOK) { + close(fd); + err_import_key("", param); + goto fail; + } + + // read pem + ssize_t read_count = read(fd, pem.data, pem.size); + close(fd); + if (read_count == -1) { + dnssec_binary_free(&pem); + ret = knot_map_errno(); + err_import_key("", param); + goto fail; + } + + // put pem to keystore + ret = dnssec_keystore_import(ctx->keystore, &pem, &keyid); + dnssec_binary_free(&pem); + if (ret != DNSSEC_EOK) { + err_import_key(keyid, param); + goto fail; + } + } else { + assert(backend == KEYSTORE_BACKEND_PKCS11); + keyid = strdup(param); + } + + // create dnssec key + ret = dnssec_key_new(&key); + if (ret != DNSSEC_EOK) { + goto fail; + } + ret = dnssec_key_set_dname(key, ctx->zone->dname); + if (ret != DNSSEC_EOK) { + goto fail; + } + dnssec_key_set_flags(key, dnskey_flags(flags & DNSKEY_GENERATE_SEP_ON)); + dnssec_key_set_algorithm(key, ctx->policy->algorithm); + + // fill key structure from keystore (incl. pubkey from privkey computation) + ret = dnssec_keystore_get_private(ctx->keystore, keyid, key); + if (ret != DNSSEC_EOK) { + err_import_key(keyid, ""); + goto fail; + } + + // allocate kasp key + knot_kasp_key_t *kkey = calloc(1, sizeof(*kkey)); + if (kkey == NULL) { + ret = KNOT_ENOMEM; + goto fail; + } + kkey->id = keyid; + kkey->key = key; + kkey->timing = timing; + kkey->is_ksk = (flags & DNSKEY_GENERATE_KSK); + kkey->is_zsk = (flags & DNSKEY_GENERATE_ZSK); + + // append to zone + ret = kasp_zone_append(ctx->zone, kkey); + free(kkey); + if (ret != KNOT_EOK) { + goto fail; + } + ret = kdnssec_ctx_commit(ctx); + if (ret == KNOT_EOK) { + printf("%s\n", keyid); + return KNOT_EOK; + } +fail: + dnssec_key_free(key); + free(keyid); + return knot_error_from_libdnssec(ret); +} + +int keymgr_import_pem(kdnssec_ctx_t *ctx, const char *import_file, int argc, char *argv[]) +{ + return import_key(ctx, KEYSTORE_BACKEND_PEM, import_file, argc, argv); +} + +int keymgr_import_pkcs11(kdnssec_ctx_t *ctx, char *key_id, int argc, char *argv[]) +{ + if (!dnssec_keyid_is_valid(key_id)) { + return DNSSEC_INVALID_KEY_ID; + } + + if (ctx->keystore_type != KEYSTORE_BACKEND_PKCS11) { + knot_dname_txt_storage_t dname_str; + (void)knot_dname_to_str(dname_str, ctx->zone->dname, sizeof(dname_str)); + ERR2("not a PKCS #11 keystore for zone %s", dname_str); + return KNOT_ERROR; + } + + dnssec_keyid_normalize(key_id); + return import_key(ctx, KEYSTORE_BACKEND_PKCS11, key_id, argc, argv); +} + +int keymgr_nsec3_salt_print(kdnssec_ctx_t *ctx) +{ + dnssec_binary_t salt_bin; + knot_time_t created; + int ret = kasp_db_load_nsec3salt(ctx->kasp_db, ctx->zone->dname, + &salt_bin, &created); + switch (ret) { + case KNOT_EOK: + printf("Current salt: "); + if (salt_bin.size == 0) { + printf("-"); + } + for (size_t i = 0; i < salt_bin.size; i++) { + printf("%02X", (unsigned)salt_bin.data[i]); + } + printf("\n"); + free(salt_bin.data); + break; + case KNOT_ENOENT: + printf("-- no salt --\n"); + ret = KNOT_EOK; + break; + } + return ret; +} + +int keymgr_nsec3_salt_set(kdnssec_ctx_t *ctx, const char *new_salt) +{ + assert(new_salt); + + dnssec_binary_t salt_bin = { 0 }; + if (strcmp(new_salt, "-") != 0) { + salt_bin.data = hex_to_bin(new_salt, &salt_bin.size); + if (salt_bin.data == NULL) { + return KNOT_EMALF; + } + } + if (salt_bin.size != ctx->policy->nsec3_salt_length) { + WARN2("specified salt doesn't match configured salt length (%d)", + (int)ctx->policy->nsec3_salt_length); + } + int ret = kasp_db_store_nsec3salt(ctx->kasp_db, ctx->zone->dname, + &salt_bin, knot_time()); + if (salt_bin.size > 0) { + free(salt_bin.data); + } + return ret; +} + +int keymgr_serial_print(kdnssec_ctx_t *ctx, kaspdb_serial_t type) +{ + uint32_t serial = 0; + int ret = kasp_db_load_serial(ctx->kasp_db, ctx->zone->dname, + type, &serial); + switch (ret) { + case KNOT_EOK: + printf("Current serial: %u\n", serial); + break; + case KNOT_ENOENT: + printf("-- no serial --\n"); + ret = KNOT_EOK; + break; + } + return ret; +} + +int keymgr_serial_set(kdnssec_ctx_t *ctx, kaspdb_serial_t type, uint32_t new_serial) +{ + return kasp_db_store_serial(ctx->kasp_db, ctx->zone->dname, + type, new_serial); +} + +static void print_tsig(dnssec_tsig_algorithm_t mac, const char *name, + const dnssec_binary_t *secret) +{ + assert(name); + assert(secret); + + const char *mac_name = dnssec_tsig_algorithm_to_name(mac); + assert(mac_name); + + // client format (as a comment) + printf("# %s:%s:%.*s\n", mac_name, name, (int)secret->size, secret->data); + + // server format + printf("key:\n"); + printf(" - id: %s\n", name); + printf(" algorithm: %s\n", mac_name); + printf(" secret: %.*s\n", (int)secret->size, secret->data); +} + +int keymgr_generate_tsig(const char *tsig_name, const char *alg_name, int bits) +{ + dnssec_tsig_algorithm_t alg = dnssec_tsig_algorithm_from_name(alg_name); + if (alg == DNSSEC_TSIG_UNKNOWN) { + return KNOT_INVALID_KEY_ALGORITHM; + } + + int optimal_bits = dnssec_tsig_optimal_key_size(alg); + if (bits == 0) { + bits = optimal_bits; + } + + // round up bits to bytes + bits = (bits + CHAR_BIT - 1) / CHAR_BIT * CHAR_BIT; + + if (bits < optimal_bits) { + WARN2("optimal key size for %s is at least %d bits", + dnssec_tsig_algorithm_to_name(alg), optimal_bits); + } + assert(bits % CHAR_BIT == 0); + + _cleanup_binary_ dnssec_binary_t key = { 0 }; + int r = dnssec_binary_alloc(&key, bits / CHAR_BIT); + if (r != DNSSEC_EOK) { + ERR2("failed to allocate memory"); + return knot_error_from_libdnssec(r); + } + + r = gnutls_rnd(GNUTLS_RND_KEY, key.data, key.size); + if (r != 0) { + ERR2("failed to generate secret the key"); + return knot_error_from_libdnssec(r); + } + + _cleanup_binary_ dnssec_binary_t key_b64 = { 0 }; + r = dnssec_binary_to_base64(&key, &key_b64); + if (r != DNSSEC_EOK) { + ERR2("failed to convert the key to Base64"); + return knot_error_from_libdnssec(r); + } + + print_tsig(alg, tsig_name, &key_b64); + + return KNOT_EOK; +} + +static bool is_hex(const char *string) +{ + for (const char *p = string; *p != '\0'; p++) { + if (!is_xdigit(*p)) { + return false; + } + } + return (*string != '\0'); +} + +int keymgr_get_key(kdnssec_ctx_t *ctx, const char *key_spec, knot_kasp_key_t **key) +{ + // Check if type of key spec is prescribed. + bool is_keytag = false, is_id = false; + if (strncasecmp(key_spec, "keytag=", 7) == 0) { + key_spec += 7; + is_keytag = true; + } else if (strncasecmp(key_spec, "id=", 3) == 0) { + key_spec += 3; + is_id = true; + } + + uint16_t keytag = 0; + bool can_be_keytag = (str_to_u16(key_spec, &keytag) == KNOT_EOK); + long spec_len = strlen(key_spec); + + // Check if input is a valid key spec. + if ((is_keytag && !can_be_keytag) || + (is_id && !is_hex(key_spec)) || + (!can_be_keytag && !is_hex(key_spec))) { + ERR2("invalid key specification"); + return KNOT_EINVAL; + } + + *key = NULL; + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *candidate = &ctx->zone->keys[i]; + + bool keyid_match = strncmp(candidate->id, key_spec, spec_len) == 0; // May be just a prefix. + bool keytag_match = can_be_keytag && + dnssec_key_get_keytag(candidate->key) == keytag; + + // Terminate if found exact key ID match. + if (keyid_match && !is_keytag && strlen(candidate->id) == spec_len) { + *key = candidate; + break; + } + // Check for key ID prefix or tag match. + if ((is_keytag && keytag_match) || // Tag is prescribed. + (is_id && keyid_match) || // Key ID is prescribed. + ((!is_keytag && !is_id) && (keyid_match || keytag_match))) { // Nothing is prescribed. + if (*key == NULL) { + *key = candidate; + } else { + ERR2("key not specified uniquely, please use id=Full_Key_ID"); + return KNOT_EINVAL; + } + } + } + if (*key == NULL) { + ERR2("key not found"); + return KNOT_ENOENT; + } + return KNOT_EOK; +} + +int keymgr_foreign_key_id(char *argv[], knot_lmdb_db_t *kaspdb, knot_dname_t **key_zone, char **key_id) +{ + *key_zone = knot_dname_from_str_alloc(argv[3]); + if (*key_zone == NULL) { + return KNOT_ENOMEM; + } + knot_dname_to_lower(*key_zone); + + kdnssec_ctx_t kctx = { 0 }; + int ret = kdnssec_ctx_init(conf(), &kctx, *key_zone, kaspdb, NULL); + if (ret != KNOT_EOK) { + ERR2("failed to initialize zone %s (%s)", argv[0], knot_strerror(ret)); + free(*key_zone); + *key_zone = NULL; + return KNOT_ENOZONE; + } + knot_kasp_key_t *key; + ret = keymgr_get_key(&kctx, argv[2], &key); + if (ret == KNOT_EOK) { + *key_id = strdup(key->id); + if (*key_id == NULL) { + ret = KNOT_ENOMEM; + } + } + kdnssec_ctx_deinit(&kctx); + return ret; +} + +int keymgr_set_timing(knot_kasp_key_t *key, int argc, char *argv[]) +{ + knot_kasp_key_timing_t temp = key->timing; + kdnssec_generate_flags_t flags = ((key->is_ksk ? DNSKEY_GENERATE_KSK : 0) | (key->is_zsk ? DNSKEY_GENERATE_ZSK : 0)); + + if (genkeyargs(argc, argv, true, &flags, NULL, NULL, &temp, NULL)) { + int ret = check_timers(&temp); + if (ret != KNOT_EOK) { + return ret; + } + key->timing = temp; + if (key->is_ksk != (bool)(flags & DNSKEY_GENERATE_KSK) || + key->is_zsk != (bool)(flags & DNSKEY_GENERATE_ZSK) || + flags & DNSKEY_GENERATE_SEP_SPEC) { + normalize_generate_flags(&flags); + key->is_ksk = (flags & DNSKEY_GENERATE_KSK); + key->is_zsk = (flags & DNSKEY_GENERATE_ZSK); + return dnssec_key_set_flags(key->key, dnskey_flags(flags & DNSKEY_GENERATE_SEP_ON)); + } + return KNOT_EOK; + } + return KNOT_EINVAL; +} + +typedef struct { + const char *name; + size_t offset; +} timer_ctx_t; + +static const timer_ctx_t timers[] = { + { "created", offsetof(knot_kasp_key_timing_t, created) }, + { "pre-active", offsetof(knot_kasp_key_timing_t, pre_active) }, + { "publish", offsetof(knot_kasp_key_timing_t, publish) }, + { "ready", offsetof(knot_kasp_key_timing_t, ready) }, + { "active", offsetof(knot_kasp_key_timing_t, active) }, + { "retire-active", offsetof(knot_kasp_key_timing_t, retire_active) }, + { "retire", offsetof(knot_kasp_key_timing_t, retire) }, + { "post-active", offsetof(knot_kasp_key_timing_t, post_active) }, + { "revoke", offsetof(knot_kasp_key_timing_t, revoke) }, + { "remove", offsetof(knot_kasp_key_timing_t, remove) }, + { NULL } +}; + +static void print_key_brief(const knot_kasp_key_t *key, keymgr_list_params_t *params) +{ + const bool c = params->color; + + printf("%s %s%5u%s ", + key->id, COL_BOLD(c), dnssec_key_get_keytag(key->key), COL_RST(c)); + + printf("%s%s%s%s ", + COL_BOLD(c), + (key->is_ksk ? (key->is_zsk ? COL_YELW(c) : COL_RED(c)) : COL_GRN(c)), + (key->is_ksk ? (key->is_zsk ? "CSK" : "KSK") : "ZSK"), + COL_RST(c)); + + uint8_t alg = dnssec_key_get_algorithm(key->key); + const knot_lookup_t *alg_info = knot_lookup_by_id(knot_dnssec_alg_names, alg); + if (alg_info != NULL) { + printf("%s", alg_info->name); + if (alg <= DNSSEC_KEY_ALGORITHM_RSA_SHA512) { + printf("%s/%u%s", COL_DIM(c), dnssec_key_get_size(key->key), COL_RST(c)); + } + } else { + printf("ALGORITHM_%u", alg); + } + + if (key->is_pub_only) { + printf(" %s%spublic-only%s", COL_BOLD(c), COL_MGNT(c), COL_RST(c)); + } + + static char buf[100]; + knot_time_t now = knot_time(); + for (const timer_ctx_t *t = &timers[0]; t->name != NULL; t++) { + knot_time_t *val = (void *)(&key->timing) + t->offset; + if (*val == 0) { + continue; + } + bool past = (knot_time_cmp(*val, now) <= 0); + const char *UNDR = past ? COL_UNDR(c) : ""; + const char *BOLD = past ? "" : COL_BOLD(c); + for (const timer_ctx_t *t2 = t + 1; past && t2->name != NULL; t2++) { + knot_time_t *val2 = (void *)(&key->timing) + t2->offset; + if (knot_time_cmp(*val2, now) <= 0) { + UNDR = ""; + break; + } + } + (void)knot_time_print(params->format, *val, buf, sizeof(buf)); + printf(" %s%s%s=%s%s%s", UNDR, t->name, COL_RST(c), BOLD, buf, COL_RST(c)); + } + printf("\n"); +} + +static void print_key_full(const knot_kasp_key_t *key, knot_time_print_t format) +{ + printf("%s ksk=%s zsk=%s tag=%05d algorithm=%-2d size=%-4u public-only=%s", key->id, + (key->is_ksk ? "yes" : "no "), (key->is_zsk ? "yes" : "no "), + dnssec_key_get_keytag(key->key), (int)dnssec_key_get_algorithm(key->key), + dnssec_key_get_size(key->key), (key->is_pub_only ? "yes" : "no ")); + + static char buf[100]; + for (const timer_ctx_t *t = &timers[0]; t->name != NULL; t++) { + knot_time_t *val = (void *)(&key->timing) + t->offset; + (void)knot_time_print(format, *val, buf, sizeof(buf)); + printf(" %s=%s", t->name, buf); + } + printf("\n"); +} + +static void print_key_json(const knot_kasp_key_t *key, knot_time_print_t format, + jsonw_t *w, const char *zone_name) +{ + jsonw_str(w, "zone", zone_name); + jsonw_str(w, "id", key->id); + jsonw_bool(w, "ksk", key->is_ksk); + jsonw_bool(w, "zsk", key->is_zsk); + jsonw_int(w, "tag", dnssec_key_get_keytag(key->key)); + jsonw_ulong(w, "algorithm", dnssec_key_get_algorithm(key->key)); + jsonw_int(w, "size", dnssec_key_get_size(key->key)); + jsonw_bool(w, "public-only", key->is_pub_only); + + static char buf[100]; + for (const timer_ctx_t *t = &timers[0]; t->name != NULL; t++) { + knot_time_t *val = (void *)(&key->timing) + t->offset; + (void)knot_time_print(format, *val, buf, sizeof(buf)); + + if (format == TIME_PRINT_UNIX) { + jsonw_int(w, t->name, *val); + } else { + jsonw_str(w, t->name, buf); + } + } +} + +typedef struct { + knot_time_t val; + const knot_kasp_key_t *key; +} key_sort_item_t; + +static int key_sort(const void *a, const void *b) +{ + const key_sort_item_t *key_a = a; + const key_sort_item_t *key_b = b; + return knot_time_cmp(key_a->val, key_b->val); +} + +int keymgr_list_keys(kdnssec_ctx_t *ctx, keymgr_list_params_t *params) +{ + if (ctx->zone->num_keys == 0) { + return KNOT_EOK; + } + + if (params->extended) { + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + print_key_full(key, params->format); + } + } else if (params->json) { + jsonw_t *w = jsonw_new(stdout, " "); + if (w == NULL) { + return KNOT_ENOMEM; + } + + knot_dname_txt_storage_t name; + (void)knot_dname_to_str(name, ctx->zone->dname, sizeof(name)); + + jsonw_list(w, NULL); + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + jsonw_object(w, NULL); + print_key_json(key, params->format, w, name); + jsonw_end(w); // object + } + jsonw_end(w); // list + jsonw_free(&w); + } else { + key_sort_item_t items[ctx->zone->num_keys]; + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + items[i].key = key; + if (knot_time_cmp(key->timing.pre_active, key->timing.publish) < 0) { + items[i].val = key->timing.pre_active; + } else { + items[i].val = key->timing.publish; + } + } + qsort(&items, ctx->zone->num_keys, sizeof(items[0]), key_sort); + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + print_key_brief(items[i].key, params); + } + } + return KNOT_EOK; +} + +static int print_ds(const knot_dname_t *dname, const dnssec_binary_t *rdata) +{ + wire_ctx_t ctx = wire_ctx_init(rdata->data, rdata->size); + if (wire_ctx_available(&ctx) < 4) { + return KNOT_EMALF; + } + + char *name = knot_dname_to_str_alloc(dname); + if (!name) { + return KNOT_ENOMEM; + } + + uint16_t keytag = wire_ctx_read_u16(&ctx); + uint8_t algorithm = wire_ctx_read_u8(&ctx); + uint8_t digest_type = wire_ctx_read_u8(&ctx); + + size_t digest_size = wire_ctx_available(&ctx); + + printf("%s DS %d %d %d ", name, keytag, algorithm, digest_type); + for (size_t i = 0; i < digest_size; i++) { + printf("%02x", ctx.position[i]); + } + printf("\n"); + + free(name); + return KNOT_EOK; +} + +static int create_and_print_ds(const knot_dname_t *zone_name, + const dnssec_key_t *key, dnssec_key_digest_t digest) +{ + _cleanup_binary_ dnssec_binary_t rdata = { 0 }; + int r = dnssec_key_create_ds(key, digest, &rdata); + if (r != DNSSEC_EOK) { + return knot_error_from_libdnssec(r); + } + + return print_ds(zone_name, &rdata); +} + +int keymgr_generate_ds(const knot_dname_t *dname, const knot_kasp_key_t *key) +{ + static const dnssec_key_digest_t digests[] = { + DNSSEC_KEY_DIGEST_SHA256, + DNSSEC_KEY_DIGEST_SHA384, + 0 + }; + + int ret = KNOT_EOK; + for (int i = 0; digests[i] != 0 && ret == KNOT_EOK; i++) { + ret = create_and_print_ds(dname, key->key, digests[i]); + } + + return ret; +} + +int keymgr_generate_dnskey(const knot_dname_t *dname, const knot_kasp_key_t *key) +{ + const dnssec_key_t *dnskey = key->key; + + char *name = knot_dname_to_str_alloc(dname); + if (!name) { + return KNOT_ENOMEM; + } + + uint16_t flags = dnssec_key_get_flags(dnskey); + uint8_t algorithm = dnssec_key_get_algorithm(dnskey); + + dnssec_binary_t pubkey = { 0 }; + int ret = dnssec_key_get_pubkey(dnskey, &pubkey); + if (ret != DNSSEC_EOK) { + free(name); + return knot_error_from_libdnssec(ret); + } + + uint8_t *base64_output = NULL; + int len = knot_base64_encode_alloc(pubkey.data, pubkey.size, &base64_output); + if (len < 0) { + free(name); + return len; + } + + printf("%s DNSKEY %u 3 %u %.*s\n", name, flags, algorithm, len, base64_output); + + free(base64_output); + free(name); + return KNOT_EOK; +} + +int keymgr_list_zones(knot_lmdb_db_t *kaspdb, bool json) +{ + jsonw_t *w; + list_t zones; + init_list(&zones); + int ret = kasp_db_list_zones(kaspdb, &zones); + if (ret != KNOT_EOK) { + ERR2("failed to initialize KASP (%s)", knot_strerror(ret)); + return ret; + } + + knot_dname_txt_storage_t name; + ptrnode_t *node; + + if (json) { + w = jsonw_new(stdout, " "); + if (w == NULL) { + ERR2("failed to allocate memory"); + ptrlist_deep_free(&zones, NULL); + return KNOT_ENOMEM; + } + jsonw_list(w, NULL); + } + WALK_LIST(node, zones) { + (void)knot_dname_to_str(name, node->d, sizeof(name)); + if (json) { + jsonw_str(w, NULL, name); + } else { + printf("%s\n", name); + } + } + if (json) { + jsonw_end(w); // list + jsonw_free(&w); + } + + ptrlist_deep_free(&zones, NULL); + return KNOT_EOK; +} diff --git a/src/utils/keymgr/functions.h b/src/utils/keymgr/functions.h new file mode 100644 index 0000000..9c16d80 --- /dev/null +++ b/src/utils/keymgr/functions.h @@ -0,0 +1,62 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdio.h> + +#include "knot/dnssec/context.h" + +typedef struct { + knot_time_print_t format; + bool extended; + bool color; + bool json; +} keymgr_list_params_t; + +int parse_timestamp(char *arg, knot_time_t *stamp); + +int keymgr_generate_key(kdnssec_ctx_t *ctx, int argc, char *argv[]); + +int keymgr_import_bind(kdnssec_ctx_t *ctx, const char *import_file, bool pub_only); + +int keymgr_import_pem(kdnssec_ctx_t *ctx, const char *import_file, int argc, char *argv[]); + +int keymgr_import_pkcs11(kdnssec_ctx_t *ctx, char *key_id, int argc, char *argv[]); + +int keymgr_nsec3_salt_print(kdnssec_ctx_t *ctx); + +int keymgr_nsec3_salt_set(kdnssec_ctx_t *ctx, const char *new_salt); + +int keymgr_serial_print(kdnssec_ctx_t *ctx, kaspdb_serial_t type); + +int keymgr_serial_set(kdnssec_ctx_t *ctx, kaspdb_serial_t type, uint32_t new_serial); + +int keymgr_generate_tsig(const char *tsig_name, const char *alg_name, int bits); + +int keymgr_get_key(kdnssec_ctx_t *ctx, const char *key_spec, knot_kasp_key_t **key); + +int keymgr_foreign_key_id(char *argv[], knot_lmdb_db_t *kaspdb, knot_dname_t **key_zone, char **key_id); + +int keymgr_set_timing(knot_kasp_key_t *key, int argc, char *argv[]); + +int keymgr_list_keys(kdnssec_ctx_t *ctx, keymgr_list_params_t *params); + +int keymgr_generate_ds(const knot_dname_t *dname, const knot_kasp_key_t *key); + +int keymgr_generate_dnskey(const knot_dname_t *dname, const knot_kasp_key_t *key); + +int keymgr_list_zones(knot_lmdb_db_t *kaspdb, bool json); diff --git a/src/utils/keymgr/main.c b/src/utils/keymgr/main.c new file mode 100644 index 0000000..355fd3a --- /dev/null +++ b/src/utils/keymgr/main.c @@ -0,0 +1,416 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <getopt.h> +#include <stdlib.h> +#include <unistd.h> + +#include "contrib/strtonum.h" +#include "knot/dnssec/zone-keys.h" +#include "libknot/libknot.h" +#include "utils/common/msg.h" +#include "utils/common/params.h" +#include "utils/common/signal.h" +#include "utils/common/util_conf.h" +#include "utils/keymgr/functions.h" +#include "utils/keymgr/offline_ksk.h" + +#define PROGRAM_NAME "keymgr" + +signal_ctx_t signal_ctx = { 0 }; // global, needed by signal handler + +static void print_help(void) +{ + printf("Usage:\n" + " %s [-c | -C | -D <path>] [options] <zone_name> <command>\n" + " %s [-c | -C | -D <path>] [-j] -l\n" + " %s -t <tsig_name> [<algorithm> [<bits>]]\n" + "\n" + "Config options:\n" + " -c, --config <file> Path to a textual configuration file.\n" + " (default %s)\n" + " -C, --confdb <dir> Path to a configuration database directory.\n" + " (default %s)\n" + " -D, --dir <path> Path to a KASP database directory, use default configuration.\n" + "\n" + "Options:\n" + " -t, --tsig <name> [alg] Generate a TSIG key.\n" + " -e, --extended Extended output (listing of keys with full description).\n" + " -j, --json Print the zones or keys in JSON format.\n" + " -l, --list List all zones that have at least one key in KASP database.\n" + " -x, --mono Don't color the output.\n" + " -X, --color Force output colorization in the normal mode.\n" + " -h, --help Print the program help.\n" + " -V, --version Print the program version.\n" + "\n" + "Commands:\n" + " list List all zone's DNSSEC keys.\n" + " generate Generate new DNSSEC key.\n" + " (syntax: generate <attribute_name>=<value>...)\n" + " import-bind Import BIND-style key file pair (.key + .private).\n" + " (syntax: import-bind <key_file_name>)\n" + " import-pub Import public-only key to be published in the zone (in BIND .key format).\n" + " (syntax: import-pub <key_file_name>)\n" + " import-pem Import key in PEM format. Specify its parameters manually.\n" + " (syntax: import-pem <pem_file_path> <attribute_name>=<value>...)\n" + " import-pkcs11 Import key stored in PKCS11 storage. Specify its parameters manually.\n" + " (syntax: import-pkcs11 <key_id> <attribute_name>=<value>...)\n" + " nsec3-salt Print current NSEC3 salt. If a parameter is specified, set new salt.\n" + " (syntax: nsec3-salt [<new_salt>])\n" + " local-serial Print SOA serial stored in KASP database when using on-slave signing.\n" + " If a parameter is specified, set new serial.\n" + " (syntax: local-serial <new_serial>)\n" + " master-serial Print SOA serial of the remote master stored in KASP database when using on-slave signing.\n" + " If a parameter is specified, set new master serial.\n" + " (syntax: master-serial <new_serial>)\n" + " ds Generate DS record(s) for specified key.\n" + " (syntax: ds <key_spec>)\n" + " dnskey Generate DNSKEY record for specified key.\n" + " (syntax: dnskey <key_spec>)\n" + " share Share an existing key of another zone with the specified zone.\n" + " (syntax: share <full_key_ID> <zone2share_from>\n" + " delete Remove the specified key from zone.\n" + " (syntax: delete <key_spec>)\n" + " set Set existing key's timing attribute.\n" + " (syntax: set <key_spec> <attribute_name>=<value>...)\n" + "\n" + "Commands related to Offline KSK feature:\n" + " pregenerate Pre-generate ZSKs for later rollovers with offline KSK.\n" + " (syntax: pregenerate [<from>] <to>)\n" + " show-offline Print pre-generated offline key-related records for specified time interval (possibly to infinity).\n" + " (syntax: show-offline [<from>] [<to>])\n" + " del-offline Delete pre-generated offline key-related records in specified time interval.\n" + " (syntax: del-offline <from> <to>)\n" + " del-all-old Delete old keys that are in state 'removed'.\n" + " generate-ksr Print to stdout KeySigningRequest based on pre-generated ZSKS.\n" + " (syntax: generate-ksr [<from>] <to>)\n" + " sign-ksr Read KeySigningRequest from a file, sign it and print SignedKeyResponse to stdout.\n" + " (syntax: sign-ksr <ksr_file>)\n" + " validate-skr Validate RRSIGs in a SignedKeyResponse (if not corrupt).\n" + " (syntax: validate-skr <skr_file>)\n" + " import-skr Import DNSKEY record signatures from a SignedKeyResponse.\n" + " (syntax: import-skr <skr_file>)\n" + "\n" + "Key specification:\n" + " either the key tag (number) or [a prefix of] key ID, with an optional\n" + " [id=|keytag=] prefix.\n" + "\n" + "Key attributes:\n" + " algorithm The key cryptographic algorithm: either name (e.g. RSASHA256) or\n" + " number.\n" + " size The key size in bits.\n" + " ksk Whether the generated/imported key shall be Key Signing Key.\n" + " created/publish/ready/active/retire/remove The timestamp of the key\n" + " lifetime event (e.g. published=+1d active=1499770874)\n", + PROGRAM_NAME, PROGRAM_NAME, PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR); +} + +static int key_command(int argc, char *argv[], int opt_ind, knot_lmdb_db_t *kaspdb, + keymgr_list_params_t *list_params) +{ + if (argc < opt_ind + 2) { + ERR2("zone name or command not specified"); + print_help(); + return KNOT_EINVAL; + } + argc -= opt_ind; + argv += opt_ind; + + knot_dname_t *zone_name = knot_dname_from_str_alloc(argv[0]); + if (zone_name == NULL) { + return KNOT_ENOMEM; + } + knot_dname_to_lower(zone_name); + + kdnssec_ctx_t kctx = { 0 }; + + int ret = kdnssec_ctx_init(conf(), &kctx, zone_name, kaspdb, NULL); + if (ret != KNOT_EOK) { + ERR2("failed to initialize KASP (%s)", knot_strerror(ret)); + goto main_end; + } + +#define CHECK_MISSING_ARG(msg) \ + if (argc < 3) { \ + ERR2("%s", (msg)); \ + ret = KNOT_EINVAL; \ + goto main_end; \ + } + +#define CHECK_MISSING_ARG2(msg) \ + if (argc < 4) { \ + ERR2("%s", (msg)); \ + ret = KNOT_EINVAL; \ + goto main_end; \ + } + + bool print_ok_on_succes = true; + if (strcmp(argv[1], "generate") == 0) { + ret = keymgr_generate_key(&kctx, argc - 2, argv + 2); + print_ok_on_succes = false; + } else if (strcmp(argv[1], "import-bind") == 0) { + CHECK_MISSING_ARG("BIND-style key to import not specified"); + ret = keymgr_import_bind(&kctx, argv[2], false); + } else if (strcmp(argv[1], "import-pub") == 0) { + CHECK_MISSING_ARG("BIND-style key to import not specified"); + ret = keymgr_import_bind(&kctx, argv[2], true); + } else if (strcmp(argv[1], "import-pem") == 0) { + CHECK_MISSING_ARG("PEM file to import not specified"); + ret = keymgr_import_pem(&kctx, argv[2], argc - 3, argv + 3); + } else if (strcmp(argv[1], "import-pkcs11") == 0) { + CHECK_MISSING_ARG("Key ID to import not specified"); + ret = keymgr_import_pkcs11(&kctx, argv[2], argc - 3, argv + 3); + } else if (strcmp(argv[1], "nsec3-salt") == 0) { + if (argc > 2) { + ret = keymgr_nsec3_salt_set(&kctx, argv[2]); + } else { + ret = keymgr_nsec3_salt_print(&kctx); + print_ok_on_succes = false; + } + } else if (strcmp(argv[1], "local-serial") == 0 || strcmp(argv[1], "master-serial") == 0 ) { + kaspdb_serial_t type = (argv[1][0] == 'm' ? KASPDB_SERIAL_MASTER : KASPDB_SERIAL_LASTSIGNED); + if (argc > 2) { + uint32_t new_serial = 0; + if ((ret = str_to_u32(argv[2], &new_serial)) == KNOT_EOK) { + ret = keymgr_serial_set(&kctx, type, new_serial); + } + } else { + ret = keymgr_serial_print(&kctx, type); + print_ok_on_succes = false; + } + } else if (strcmp(argv[1], "set") == 0) { + CHECK_MISSING_ARG("Key is not specified"); + knot_kasp_key_t *key2set; + ret = keymgr_get_key(&kctx, argv[2], &key2set); + if (ret == KNOT_EOK) { + ret = keymgr_set_timing(key2set, argc - 3, argv + 3); + if (ret == KNOT_EOK) { + ret = kdnssec_ctx_commit(&kctx); + } + } + } else if (strcmp(argv[1], "list") == 0) { + list_params->format = TIME_PRINT_UNIX; + if (argc > 2 && strcmp(argv[2], "human") == 0) { + list_params->format = TIME_PRINT_HUMAN_MIXED; + } else if (argc > 2 && strcmp(argv[2], "iso") == 0) { + list_params->format = TIME_PRINT_ISO8601; + } + ret = keymgr_list_keys(&kctx, list_params); + print_ok_on_succes = false; + } else if (strcmp(argv[1], "ds") == 0 || strcmp(argv[1], "dnskey") == 0) { + int (*generate_rr)(const knot_dname_t *, const knot_kasp_key_t *) = keymgr_generate_dnskey; + if (strcmp(argv[1], "ds") == 0) { + generate_rr = keymgr_generate_ds; + } + if (argc < 3) { + for (int i = 0; i < kctx.zone->num_keys && ret == KNOT_EOK; i++) { + if (kctx.zone->keys[i].is_ksk) { + ret = generate_rr(zone_name, &kctx.zone->keys[i]); + } + } + } else { + knot_kasp_key_t *key2rr; + ret = keymgr_get_key(&kctx, argv[2], &key2rr); + if (ret == KNOT_EOK) { + ret = generate_rr(zone_name, key2rr); + } + } + print_ok_on_succes = false; + } else if (strcmp(argv[1], "share") == 0) { + CHECK_MISSING_ARG("Key to be shared is not specified"); + CHECK_MISSING_ARG2("Zone to be shared from not specified"); + knot_dname_t *other_zone = NULL; + char *key_to_share = NULL; + ret = keymgr_foreign_key_id(argv, kaspdb, &other_zone, &key_to_share); + if (ret == KNOT_EOK) { + ret = kasp_db_share_key(kctx.kasp_db, other_zone, kctx.zone->dname, key_to_share); + } + free(other_zone); + free(key_to_share); + } else if (strcmp(argv[1], "delete") == 0) { + CHECK_MISSING_ARG("Key is not specified"); + knot_kasp_key_t *key2del; + ret = keymgr_get_key(&kctx, argv[2], &key2del); + if (ret == KNOT_EOK) { + ret = kdnssec_delete_key(&kctx, key2del); + } + } else if (strcmp(argv[1], "pregenerate") == 0) { + CHECK_MISSING_ARG("Timestamp to not specified"); + ret = keymgr_pregenerate_zsks(&kctx, argc > 3 ? argv[2] : NULL, + argc > 3 ? argv[3] : argv[2]); + } else if (strcmp(argv[1], "show-offline") == 0) { + ret = keymgr_print_offline_records(&kctx, argc > 2 ? argv[2] : NULL, + argc > 3 ? argv[3] : NULL); + print_ok_on_succes = false; + } else if (strcmp(argv[1], "del-offline") == 0) { + CHECK_MISSING_ARG2("Timestamps from-to not specified"); + ret = keymgr_delete_offline_records(&kctx, argv[2], argv[3]); + } else if (strcmp(argv[1], "del-all-old") == 0) { + ret = keymgr_del_all_old(&kctx); + } else if (strcmp(argv[1], "generate-ksr") == 0) { + CHECK_MISSING_ARG("Timestamps to not specified"); + ret = keymgr_print_ksr(&kctx, argc > 3 ? argv[2] : NULL, + argc > 3 ? argv[3] : argv[2]); + print_ok_on_succes = false; + } else if (strcmp(argv[1], "sign-ksr") == 0) { + CHECK_MISSING_ARG("Input file not specified"); + ret = keymgr_sign_ksr(&kctx, argv[2]); + print_ok_on_succes = false; + } else if (strcmp(argv[1], "validate-skr") == 0) { + CHECK_MISSING_ARG("Input file not specified"); + ret = keymgr_validate_skr(&kctx, argv[2]); + } else if (strcmp(argv[1], "import-skr") == 0) { + CHECK_MISSING_ARG("Input file not specified"); + ret = keymgr_import_skr(&kctx, argv[2]); + } else { + ERR2("invalid command '%s'", argv[1]); + goto main_end; + } + +#undef CHECK_MISSING_ARG + + if (ret == KNOT_EOK) { + printf("%s", print_ok_on_succes ? "OK\n" : ""); + } else { + ERR2("%s", knot_strerror(ret)); + } + +main_end: + kdnssec_ctx_deinit(&kctx); + free(zone_name); + + return ret; +} + +int main(int argc, char *argv[]) +{ + knot_lmdb_db_t kaspdb = { 0 }; + + struct option opts[] = { + { "config", required_argument, NULL, 'c' }, + { "confdb", required_argument, NULL, 'C' }, + { "dir", required_argument, NULL, 'D' }, + { "tsig", required_argument, NULL, 't' }, + { "extended", no_argument, NULL, 'e' }, + { "list", no_argument, NULL, 'l' }, + { "brief", no_argument, NULL, 'b' }, // Legacy. + { "mono", no_argument, NULL, 'x' }, + { "color", no_argument, NULL, 'X' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "json", no_argument, NULL, 'j' }, + { NULL } + }; + + tzset(); + + signal_ctx.close_db = &kaspdb; + signal_init_std(); + + int ret; + bool just_list = false; + keymgr_list_params_t list_params = { 0 }; + + list_params.color = isatty(STDOUT_FILENO); + + int opt = 0, parm = 0; + while ((opt = getopt_long(argc, argv, "c:C:D:t:ejlbxXhV", opts, NULL)) != -1) { + switch (opt) { + case 'c': + if (util_conf_init_file(optarg) != KNOT_EOK) { + goto failure; + } + break; + case 'C': + if (util_conf_init_confdb(optarg) != KNOT_EOK) { + goto failure; + } + break; + case 'D': + if (util_conf_init_justdb("kasp-db", optarg) != KNOT_EOK) { + goto failure; + } + break; + case 't': + if (argc > optind + 1) { + (void)str_to_int(argv[optind + 1], &parm, 0, 65536); + } + ret = keymgr_generate_tsig(optarg, (argc > optind ? argv[optind] : "hmac-sha256"), parm); + if (ret != KNOT_EOK) { + ERR2("failed to generate TSIG (%s)", knot_strerror(ret)); + goto failure; + } + goto success; + case 'e': + list_params.extended = true; + break; + case 'j': + list_params.json = true; + break; + case 'l': + just_list = true; + break; + case 'b': + WARN2("option '--brief' is deprecated and enabled by default"); + break; + case 'x': + list_params.color = false; + break; + case 'X': + list_params.color = true; + break; + case 'h': + print_help(); + goto success; + case 'V': + print_version(PROGRAM_NAME); + goto success; + default: + print_help(); + goto failure; + } + } + + signal_ctx.color = list_params.color; + + if (util_conf_init_default(true) != KNOT_EOK) { + goto failure; + } + + util_update_privileges(); + + conf_val_t mapsize = conf_db_param(conf(), C_KASP_DB_MAX_SIZE); + char *kasp_dir = conf_db(conf(), C_KASP_DB); + knot_lmdb_init(&kaspdb, kasp_dir, conf_int(&mapsize), 0, "keys_db"); + free(kasp_dir); + + if (just_list) { + ret = keymgr_list_zones(&kaspdb, list_params.json); + } else { + ret = key_command(argc, argv, optind, &kaspdb, &list_params); + } + knot_lmdb_deinit(&kaspdb); + if (ret != KNOT_EOK) { + goto failure; + } + +success: + util_conf_deinit(); + return EXIT_SUCCESS; +failure: + util_conf_deinit(); + return EXIT_FAILURE; +} diff --git a/src/utils/keymgr/offline_ksk.c b/src/utils/keymgr/offline_ksk.c new file mode 100644 index 0000000..253199f --- /dev/null +++ b/src/utils/keymgr/offline_ksk.c @@ -0,0 +1,582 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <string.h> +#include <stdio.h> +#include <time.h> + +#include "utils/keymgr/offline_ksk.h" +#include "contrib/strtonum.h" +#include "knot/dnssec/kasp/policy.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/key_records.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-events.h" +#include "knot/dnssec/zone-keys.h" +#include "knot/dnssec/zone-sign.h" +#include "libzscanner/scanner.h" +#include "utils/common/msg.h" +#include "utils/keymgr/functions.h" + +#define KSR_SKR_VER "1.0" + +static int pregenerate_once(kdnssec_ctx_t *ctx, knot_time_t *next) +{ + zone_sign_reschedule_t resch = { 0 }; + + // generate ZSKs + int ret = knot_dnssec_key_rollover(ctx, KEY_ROLL_ALLOW_ZSK_ROLL, &resch); + if (ret != KNOT_EOK) { + ERR2("key rollover failed"); + return ret; + } + // we don't need to do anything explicitly with the generated ZSKs + // they're simply stored in KASP db + + *next = resch.next_rollover; + return KNOT_EOK; +} + +// please free *_dnskey and keyset even if returned error +static int load_dnskey_rrset(kdnssec_ctx_t *ctx, knot_rrset_t **_dnskey, zone_keyset_t *keyset) +{ + // prepare the DNSKEY rrset to be signed + knot_rrset_t *dnskey = knot_rrset_new(ctx->zone->dname, KNOT_RRTYPE_DNSKEY, + KNOT_CLASS_IN, ctx->policy->dnskey_ttl, NULL); + if (dnskey == NULL) { + return KNOT_ENOMEM; + } + *_dnskey = dnskey; + + int ret = load_zone_keys(ctx, keyset, false); + if (ret != KNOT_EOK) { + ERR2("failed to load keys"); + return ret; + } + + for (int i = 0; i < keyset->count; i++) { + zone_key_t *key = &keyset->keys[i]; + if (key->is_public) { + ret = rrset_add_zone_key(dnskey, key); + if (ret != KNOT_EOK) { + ERR2("failed to add zone key"); + return ret; + } + } + } + + return KNOT_EOK; +} + +int keymgr_pregenerate_zsks(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to) +{ + knot_time_t from = 0, to; + int ret = parse_timestamp(arg_to, &to); + if (ret != KNOT_EOK) { + return ret; + } + if (arg_from != NULL) { + ret = parse_timestamp(arg_from, &from); + if (ret != KNOT_EOK) { + return ret; + } + } + + knot_time_t next = (from == 0 ? ctx->now : from); + ret = KNOT_EOK; + + ctx->keep_deleted_keys = true; + ctx->policy->manual = false; + + if (ctx->policy->dnskey_ttl == UINT32_MAX || + ctx->policy->zone_maximal_ttl == UINT32_MAX) { + ERR2("dnskey-ttl or zone-max-ttl not configured"); + return KNOT_ESEMCHECK; + } + + while (ret == KNOT_EOK && knot_time_cmp(next, to) <= 0) { + ctx->now = next; + ret = pregenerate_once(ctx, &next); + } + + return ret; +} + +static int dump_rrset_to_buf(const knot_rrset_t *rrset, char **buf, size_t *buf_size) +{ + if (*buf == NULL) { + *buf = malloc(*buf_size); + if (*buf == NULL) { + return KNOT_ENOMEM; + } + } + + knot_dump_style_t style = { + .wrap = true, + .show_ttl = true, + .verbose = true, + .original_ttl = true, + .human_timestamp = true + }; + return knot_rrset_txt_dump(rrset, buf, buf_size, &style); +} + +static void print_header(const char *of_what, knot_time_t timestamp, const char *contents) +{ + char date[64] = { 0 }; + (void)knot_time_print(TIME_PRINT_ISO8601, timestamp, date, sizeof(date)); + printf(";; %s %"PRIu64" (%s) =========\n%s", of_what, + timestamp, date, contents); +} + +int keymgr_print_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to) +{ + knot_time_t from = 0, to = 0; + if (arg_from != NULL) { + int ret = parse_timestamp(arg_from, &from); + if (ret != KNOT_EOK) { + return ret; + } + } + if (arg_to != NULL) { + int ret = parse_timestamp(arg_to, &to); + if (ret != KNOT_EOK) { + return ret; + } + } + + bool empty = true; + char *buf = NULL; + size_t buf_size = 512; + while (true) { + if (arg_to != NULL && knot_time_cmp(from, to) > 0) { + break; + } + knot_time_t next; + key_records_t r = { { 0 } }; + int ret = kasp_db_load_offline_records(ctx->kasp_db, ctx->zone->dname, + &from, &next, &r); + if (ret == KNOT_ENOENT) { + break; + } else if (ret != KNOT_EOK) { + free(buf); + return ret; + } + + ret = key_records_dump(&buf, &buf_size, &r, true); + key_records_clear(&r); + if (ret != KNOT_EOK) { + free(buf); + return ret; + } + print_header("Offline records for", from, buf); + empty = false; + + if (next == 0) { + break; + } + from = next; + } + free(buf); + + /* If from is lower than the first record's timestamp, try to start + from the first one's instead of empty output. */ + if (empty && from > 0) { + knot_time_t last = 0; + int ret = key_records_last_timestamp(ctx, &last); + if (ret == KNOT_EOK && knot_time_cmp(last, from) > 0) { + return keymgr_print_offline_records(ctx, 0, arg_to); + } + } + return KNOT_EOK; +} + +int keymgr_delete_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to) +{ + knot_time_t from, to; + int ret = parse_timestamp(arg_from, &from); + if (ret != KNOT_EOK) { + return ret; + } + ret = parse_timestamp(arg_to, &to); + if (ret != KNOT_EOK) { + return ret; + } + return kasp_db_delete_offline_records(ctx->kasp_db, ctx->zone->dname, from, to); +} + +int keymgr_del_all_old(kdnssec_ctx_t *ctx) +{ + for (size_t i = 0; i < ctx->zone->num_keys; ) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + if (knot_time_cmp(key->timing.remove, ctx->now) < 0) { + int ret = kdnssec_delete_key(ctx, key); + if (ret != KNOT_EOK) { + return ret; + } + } else { + i++; + } + } + return kdnssec_ctx_commit(ctx); +} + +static void print_generated_message(void) +{ + char buf[64] = { 0 }; + knot_time_print(TIME_PRINT_ISO8601, knot_time(), buf, sizeof(buf)); + printf("generated at %s by Knot DNS %s\n", buf, VERSION); +} + +static int ksr_once(kdnssec_ctx_t *ctx, char **buf, size_t *buf_size, knot_time_t *next_ksr) +{ + knot_rrset_t *dnskey = NULL; + zone_keyset_t keyset = { 0 }; + int ret = load_dnskey_rrset(ctx, &dnskey, &keyset); + if (ret != KNOT_EOK) { + goto done; + } + ret = dump_rrset_to_buf(dnskey, buf, buf_size); + if (ret >= 0) { + print_header("KeySigningRequest "KSR_SKR_VER, ctx->now, *buf); + ret = KNOT_EOK; + } + +done: + if (ret == KNOT_EOK && next_ksr != NULL) { + *next_ksr = knot_get_next_zone_key_event(&keyset); + } + knot_rrset_free(dnskey, NULL); + free_zone_keys(&keyset); + return ret; +} + +#define OFFLINE_KSK_CONF_CHECK \ + if (!ctx->policy->offline_ksk || !ctx->policy->manual) { \ + ERR2("offline-ksk and manual must be enabled in configuration"); \ + return KNOT_ESEMCHECK; \ + } + +int keymgr_print_ksr(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to) +{ + OFFLINE_KSK_CONF_CHECK + + knot_time_t from, to; + int ret = parse_timestamp(arg_to, &to); + if (ret != KNOT_EOK) { + return ret; + } + if (arg_from == NULL) { + ret = key_records_last_timestamp(ctx, &from); + } else { + ret = parse_timestamp(arg_from, &from); + } + if (ret != KNOT_EOK) { + return ret; + } + + char *buf = NULL; + size_t buf_size = 4096; + while (ret == KNOT_EOK && knot_time_cmp(from, to) < 0) { + ctx->now = from; + ret = ksr_once(ctx, &buf, &buf_size, &from); + } + if (ret != KNOT_EOK) { + free(buf); + return ret; + } + ctx->now = to; + // force end of period as a KSR timestamp + ret = ksr_once(ctx, &buf, &buf_size, NULL); + + printf(";; KeySigningRequest %s ", KSR_SKR_VER); + print_generated_message(); + + free(buf); + return ret; +} + +typedef struct { + int ret; + key_records_t r; + knot_time_t timestamp; + kdnssec_ctx_t *kctx; +} ksr_sign_ctx_t; + +static int ksr_sign_dnskey(kdnssec_ctx_t *ctx, knot_rrset_t *zsk, knot_time_t now, + knot_time_t *next_sign) +{ + zone_keyset_t keyset = { 0 }; + char *buf = NULL; + size_t buf_size = 4096; + knot_time_t rrsigs_expire = 0; + + ctx->now = now; + ctx->policy->dnskey_ttl = zsk->ttl; + + knot_timediff_t rrsig_refresh = ctx->policy->rrsig_refresh_before; + if (rrsig_refresh == UINT32_MAX) { // not setting rrsig-refresh prohibited by documentation, but we need to do something + rrsig_refresh = ctx->policy->dnskey_ttl + ctx->policy->propagation_delay; + } + + int ret = load_zone_keys(ctx, &keyset, false); + if (ret != KNOT_EOK) { + return ret; + } + + key_records_t r; + key_records_init(ctx, &r); + + ret = knot_zone_sign_add_dnskeys(&keyset, ctx, &r, NULL, NULL); + if (ret != KNOT_EOK) { + goto done; + } + + ret = knot_rdataset_merge(&r.dnskey.rrs, &zsk->rrs, NULL); + if (ret != KNOT_EOK) { + goto done; + } + + // no check if the KSK used for signing (in keyset) is contained in DNSKEY record being signed (in KSR) ! + for (int i = 0; i < keyset.count; i++) { + ret = key_records_sign(&keyset.keys[i], &r, ctx, &rrsigs_expire); + if (ret != KNOT_EOK) { + goto done; + } + } + ret = key_records_dump(&buf, &buf_size, &r, true); + if (ret == KNOT_EOK) { + print_header("SignedKeyResponse "KSR_SKR_VER, ctx->now, buf); + *next_sign = knot_time_min( + knot_get_next_zone_key_event(&keyset), + knot_time_add(rrsigs_expire, -rrsig_refresh) + ); + } + +done: + free(buf); + key_records_clear(&r); + free_zone_keys(&keyset); + return ret; +} + +static int process_skr_between_ksrs(ksr_sign_ctx_t *ctx, knot_time_t from, knot_time_t to) +{ + for (knot_time_t t = from; t < to /* if (t == infinity) stop */; ) { + int ret = ksr_sign_dnskey(ctx->kctx, &ctx->r.dnskey, t, &t); + if (ret != KNOT_EOK) { + return ret; + } + } + return KNOT_EOK; +} + +static void ksr_sign_header(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + + // parse header + _unused_ float header_ver; + char next_str[21] = { 0 }; + if (sc->error.code != 0 || ctx->ret != KNOT_EOK || + sscanf((const char *)sc->buffer, "; KeySigningRequest %f %20s", + &header_ver, next_str) < 1) { + return; + } + + knot_time_t next_timestamp; + if (str_to_u64(next_str, &next_timestamp) != KNOT_EOK) { + // trailing header without timestamp + next_timestamp = 0; + } + + // sign previous KSR and inbetween KSK changes + if (ctx->timestamp > 0) { + knot_time_t inbetween_from; + ctx->ret = ksr_sign_dnskey(ctx->kctx, &ctx->r.dnskey, ctx->timestamp, + &inbetween_from); + if (next_timestamp > 0 && ctx->ret == KNOT_EOK) { + ctx->ret = process_skr_between_ksrs(ctx, inbetween_from, + next_timestamp); + } + key_records_clear_rdatasets(&ctx->r); + } + + // start new KSR + ctx->timestamp = next_timestamp; +} + +static void ksr_sign_once(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + if (sc->error.code == 0 && ctx->ret == KNOT_EOK) { + ctx->ret = knot_rrset_add_rdata(&ctx->r.dnskey, sc->r_data, sc->r_data_length, NULL); + ctx->r.dnskey.ttl = sc->r_ttl; + } +} + +static void skr_import_header(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + + // parse header + _unused_ float header_ver; + char next_str[21] = { 0 }; + if (sc->error.code != 0 || ctx->ret != KNOT_EOK || + sscanf((const char *)sc->buffer, "; SignedKeyResponse %f %20s", + &header_ver, next_str) < 1) { + return; + } + + knot_time_t next_timestamp; + if (str_to_u64(next_str, &next_timestamp) != KNOT_EOK) { + // trailing header without timestamp + next_timestamp = 0; + } + + // delete possibly existing conflicting offline records + ctx->ret = kasp_db_delete_offline_records( + ctx->kctx->kasp_db, ctx->kctx->zone->dname, next_timestamp, 0 + ); + + // store previous SKR + if (ctx->timestamp > 0 && ctx->ret == KNOT_EOK) { + ctx->ret = key_records_verify(&ctx->r, ctx->kctx, ctx->timestamp); + if (ctx->ret != KNOT_EOK) { + return; + } + if (next_timestamp > 0) { + ctx->ret = key_records_verify(&ctx->r, ctx->kctx, next_timestamp - 1); + if (ctx->ret != KNOT_EOK) { + return; + } + } + ctx->ret = kasp_db_store_offline_records(ctx->kctx->kasp_db, + ctx->timestamp, &ctx->r); + key_records_clear_rdatasets(&ctx->r); + } + + // start new SKR + ctx->timestamp = next_timestamp; +} + +static void skr_validate_header(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + + _unused_ float header_ver; + char next_str[21] = { 0 }; + if (sc->error.code != 0 || ctx->ret != KNOT_EOK || + sscanf((const char *)sc->buffer, "; SignedKeyResponse %f %20s", + &header_ver, next_str) < 1) { + return; + } + + knot_time_t next_timestamp; + if (str_to_u64(next_str, &next_timestamp) != KNOT_EOK) { + // trailing header without timestamp + next_timestamp = 0; + } + + if (ctx->timestamp > 0 && ctx->ret == KNOT_EOK) { + int ret = key_records_verify(&ctx->r, ctx->kctx, ctx->timestamp); + if (ret != KNOT_EOK) { // ctx->ret untouched + ERR2("invalid SignedKeyResponse for %"KNOT_TIME_PRINTF" (%s)", + ctx->timestamp, knot_strerror(ret)); + } + if (next_timestamp > 0) { + ret = key_records_verify(&ctx->r, ctx->kctx, next_timestamp - 1); + if (ret != KNOT_EOK) { // ctx->ret untouched + ERR2("invalid SignedKeyResponse for %"KNOT_TIME_PRINTF" (%s)", + next_timestamp - 1, knot_strerror(ret)); + } + } + key_records_clear_rdatasets(&ctx->r); + } + + ctx->timestamp = next_timestamp; +} + +static void skr_import_once(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + if (sc->error.code == 0 && ctx->ret == KNOT_EOK) { + ctx->ret = key_records_add_rdata(&ctx->r, sc->r_type, sc->r_data, + sc->r_data_length, sc->r_ttl); + } +} + +static int read_ksr_skr(kdnssec_ctx_t *ctx, const char *infile, + void (*cb_header)(zs_scanner_t *), void (*cb_record)(zs_scanner_t *)) +{ + zs_scanner_t sc = { 0 }; + int ret = zs_init(&sc, "", KNOT_CLASS_IN, 0); + if (ret < 0) { + return KNOT_ERROR; + } + + ret = zs_set_input_file(&sc, infile); + if (ret < 0) { + zs_deinit(&sc); + return (sc.error.code == ZS_FILE_ACCESS) ? KNOT_EFACCES : KNOT_EFILE; + } + + ksr_sign_ctx_t pctx = { 0 }; + key_records_init(ctx, &pctx.r); + pctx.kctx = ctx; + ret = zs_set_processing(&sc, cb_record, NULL, &pctx); + if (ret < 0) { + zs_deinit(&sc); + return KNOT_EBUSY; + } + sc.process.comment = cb_header; + + ret = zs_parse_all(&sc); + + if (sc.error.code != 0) { + ret = KNOT_EMALF; + } else if (pctx.ret != KNOT_EOK) { + ret = pctx.ret; + } else if (ret < 0 || pctx.r.dnskey.rrs.count > 0 || pctx.r.cdnskey.rrs.count > 0 || + pctx.r.cds.rrs.count > 0 || pctx.r.rrsig.rrs.count > 0) { + ret = KNOT_EMALF; + } + key_records_clear(&pctx.r); + zs_deinit(&sc); + return ret; +} + +int keymgr_sign_ksr(kdnssec_ctx_t *ctx, const char *ksr_file) +{ + OFFLINE_KSK_CONF_CHECK + + int ret = read_ksr_skr(ctx, ksr_file, ksr_sign_header, ksr_sign_once); + printf(";; SignedKeyResponse %s ", KSR_SKR_VER); + print_generated_message(); + return ret; +} + +int keymgr_import_skr(kdnssec_ctx_t *ctx, const char *skr_file) +{ + OFFLINE_KSK_CONF_CHECK + + return read_ksr_skr(ctx, skr_file, skr_import_header, skr_import_once); +} + +int keymgr_validate_skr(kdnssec_ctx_t *ctx, const char *skr_file) +{ + return read_ksr_skr(ctx, skr_file, skr_validate_header, skr_import_once); +} diff --git a/src/utils/keymgr/offline_ksk.h b/src/utils/keymgr/offline_ksk.h new file mode 100644 index 0000000..bf0e085 --- /dev/null +++ b/src/utils/keymgr/offline_ksk.h @@ -0,0 +1,35 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/dnssec/context.h" + +int keymgr_pregenerate_zsks(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to); + +int keymgr_print_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to); + +int keymgr_delete_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to); + +int keymgr_del_all_old(kdnssec_ctx_t *ctx); + +int keymgr_print_ksr(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to); + +int keymgr_sign_ksr(kdnssec_ctx_t *ctx, const char *ksr_file); + +int keymgr_import_skr(kdnssec_ctx_t *ctx, const char *skr_file); + +int keymgr_validate_skr(kdnssec_ctx_t *ctx, const char *skr_file); diff --git a/src/utils/khost/khost_main.c b/src/utils/khost/khost_main.c new file mode 100644 index 0000000..75b0db7 --- /dev/null +++ b/src/utils/khost/khost_main.c @@ -0,0 +1,45 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdlib.h> + +#include "libdnssec/crypto.h" +#include "utils/khost/khost_params.h" +#include "utils/kdig/kdig_exec.h" +#include "libknot/libknot.h" + +int main(int argc, char *argv[]) +{ + int ret = EXIT_SUCCESS; + + tzset(); + + kdig_params_t params; + if (khost_parse(¶ms, argc, argv) == KNOT_EOK) { + if (!params.stop) { + dnssec_crypto_init(); + if (kdig_exec(¶ms) != KNOT_EOK) { + ret = EXIT_FAILURE; + } + dnssec_crypto_cleanup(); + } + } else { + ret = EXIT_FAILURE; + } + + khost_clean(¶ms); + return ret; +} diff --git a/src/utils/khost/khost_params.c b/src/utils/khost/khost_params.c new file mode 100644 index 0000000..1423e09 --- /dev/null +++ b/src/utils/khost/khost_params.c @@ -0,0 +1,365 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <getopt.h> +#include <locale.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "utils/khost/khost_params.h" +#include "utils/kdig/kdig_params.h" +#include "utils/common/msg.h" +#include "utils/common/params.h" +#include "utils/common/resolv.h" +#include "libknot/libknot.h" +#include "contrib/strtonum.h" +#include "contrib/ucw/lists.h" + +#define PROGRAM_NAME "khost" + +#define DEFAULT_RETRIES_HOST 1 +#define DEFAULT_TIMEOUT_HOST 2 + +static const style_t DEFAULT_STYLE_HOST = { + .format = FORMAT_HOST, + .style = { + .wrap = false, + .show_class = true, + .show_ttl = true, + .verbose = false, + .original_ttl = false, + .empty_ttl = false, + .human_ttl = false, + .human_timestamp = true, + .generic = false, + .ascii_to_idn = name_to_idn + }, + .show_query = false, + .show_header = false, + .show_edns = false, + .show_question = true, + .show_answer = true, + .show_authority = true, + .show_additional = true, + .show_tsig = false, + .show_footer = false +}; + +static int khost_init(kdig_params_t *params) +{ + // Initialize params with kdig defaults. + int ret = kdig_init(params); + + if (ret != KNOT_EOK) { + return ret; + } + + // Set khost specific defaults. + free(params->config->port); + params->config->port = strdup(DEFAULT_DNS_PORT); + params->config->retries = DEFAULT_RETRIES_HOST; + params->config->wait = DEFAULT_TIMEOUT_HOST; + params->config->class_num = KNOT_CLASS_IN; + params->config->style = DEFAULT_STYLE_HOST; + params->config->idn = true; + + // Check port. + if (params->config->port == NULL) { + query_free(params->config); + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +void khost_clean(kdig_params_t *params) +{ + if (params == NULL) { + DBG_NULL; + return; + } + + kdig_clean(params); +} + +static int parse_server(const char *value, list_t *servers, const char *def_port) +{ + if (params_parse_server(value, servers, def_port) != KNOT_EOK) { + ERR("invalid server %s", value); + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +static int parse_name(const char *value, list_t *queries, const query_t *conf) +{ + char *reverse = get_reverse_name(value); + char *ascii_name = (char *)value; + query_t *query; + + if (conf->idn) { + ascii_name = name_from_idn(value); + if (ascii_name == NULL) { + free(reverse); + return KNOT_EINVAL; + } + } + + // If name is not FQDN, append trailing dot. + char *fqd_name = get_fqd_name(ascii_name); + + if (conf->idn) { + free(ascii_name); + } + + // RR type is known. + if (conf->type_num >= 0) { + if (conf->type_num == KNOT_RRTYPE_PTR) { + free(fqd_name); + + // Check for correct address. + if (reverse == NULL) { + ERR("invalid IPv4/IPv6 address %s", value); + return KNOT_EINVAL; + } + + // Add reverse query for address. + query = query_create(reverse, conf); + free(reverse); + if (query == NULL) { + return KNOT_ENOMEM; + } + add_tail(queries, (node_t *)query); + } else { + free(reverse); + + // Add query for name and specified type. + query = query_create(fqd_name, conf); + free(fqd_name); + if (query == NULL) { + return KNOT_ENOMEM; + } + add_tail(queries, (node_t *)query); + } + // RR type is unknown, use defaults. + } else { + if (reverse == NULL) { + // Add query for name and type A. + query = query_create(fqd_name, conf); + if (query == NULL) { + free(fqd_name); + return KNOT_ENOMEM; + } + query->type_num = KNOT_RRTYPE_A; + add_tail(queries, (node_t *)query); + + // Add query for name and type AAAA. + query = query_create(fqd_name, conf); + if (query == NULL) { + free(fqd_name); + return KNOT_ENOMEM; + } + query->type_num = KNOT_RRTYPE_AAAA; + query->style.hide_cname = true; + add_tail(queries, (node_t *)query); + + // Add query for name and type MX. + query = query_create(fqd_name, conf); + if (query == NULL) { + free(fqd_name); + return KNOT_ENOMEM; + } + free(fqd_name); + query->type_num = KNOT_RRTYPE_MX; + query->style.hide_cname = true; + add_tail(queries, (node_t *)query); + } else { + free(fqd_name); + + // Add reverse query for address. + query = query_create(reverse, conf); + free(reverse); + if (query == NULL) { + return KNOT_ENOMEM; + } + query->type_num = KNOT_RRTYPE_PTR; + add_tail(queries, (node_t *)query); + } + } + + return KNOT_EOK; +} + +static void print_help(void) +{ + printf("Usage: %s [-4] [-6] [-adhrsTvVw] [-c class] [-t type]\n" + " [-R retries] [-W time] name [server]\n\n" + " -4 Use IPv4 protocol only.\n" + " -6 Use IPv6 protocol only.\n" + " -a Same as -t ANY -v.\n" + " -d Allow debug messages.\n" + " -h, --help Print the program help.\n" + " -r Disable recursion.\n" + " -T Use TCP protocol.\n" + " -v Verbose output.\n" + " -V, --version Print the program version.\n" + " -w Wait forever.\n" + " -c Set query class.\n" + " -t Set query type.\n" + " -R Set number of UDP retries.\n" + " -W Set wait interval.\n", + PROGRAM_NAME); +} + +int khost_parse(kdig_params_t *params, int argc, char *argv[]) +{ + if (params == NULL || argv == NULL) { + DBG_NULL; + return KNOT_EINVAL; + } + + if (khost_init(params) != KNOT_EOK) { + return KNOT_ERROR; + } + +#ifdef LIBIDN + // Set up localization. + if (setlocale(LC_CTYPE, "") == NULL) { + params->config->idn = false; + } +#endif + + query_t *conf = params->config; + uint16_t rclass, rtype; + int64_t serial; + bool notify; + + // Long options. + struct option opts[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { NULL } + }; + + // Command line options processing. + int opt = 0; + while ((opt = getopt_long(argc, argv, "46adhrsTvVwc:t:R:W:", opts, NULL)) + != -1) { + switch (opt) { + case '4': + conf->ip = IP_4; + break; + case '6': + conf->ip = IP_6; + break; + case 'a': + conf->type_num = KNOT_RRTYPE_ANY; + conf->style.format = FORMAT_FULL; + conf->style.show_header = true; + conf->style.show_edns = true; + conf->style.show_footer = true; + break; + case 'd': + msg_enable_debug(1); + break; + case 'h': + print_help(); + params->stop = false; + return KNOT_EOK; + case 'r': + conf->flags.rd_flag = false; + break; + case 'T': + conf->protocol = PROTO_TCP; + break; + case 'v': + conf->style.format = FORMAT_FULL; + conf->style.show_header = true; + conf->style.show_edns = true; + conf->style.show_footer = true; + break; + case 'V': + print_version(PROGRAM_NAME); + params->stop = false; + return KNOT_EOK; + case 'w': + conf->wait = -1; + break; + case 'c': + if (params_parse_class(optarg, &rclass) != KNOT_EOK) { + ERR("invalid class '%s'", optarg); + return KNOT_EINVAL; + } + conf->class_num = rclass; + break; + case 't': + if (params_parse_type(optarg, &rtype, &serial, ¬ify) + != KNOT_EOK) { + ERR("invalid type '%s'", optarg); + return KNOT_EINVAL; + } + conf->type_num = rtype; + conf->serial = serial; + conf->notify = notify; + + // If NOTIFY, reset default RD flag. + if (conf->notify) { + conf->flags.rd_flag = false; + } + break; + case 'R': + if (str_to_u32(optarg, &conf->retries) != KNOT_EOK) { + ERR("invalid retries '%s'", optarg); + return KNOT_EINVAL; + } + break; + case 'W': + if (params_parse_wait(optarg, &conf->wait) != KNOT_EOK) { + ERR("invalid wait '%s'", optarg); + return KNOT_EINVAL; + } + break; + default: + print_help(); + return KNOT_ENOTSUP; + } + } + + // Process non-option parameters. + switch (argc - optind) { + case 2: + if (parse_server(argv[optind + 1], &conf->servers, conf->port) + != KNOT_EOK) { + return KNOT_EINVAL; + } + case 1: // Fall through. + if (parse_name(argv[optind], ¶ms->queries, conf) + != KNOT_EOK) { + return KNOT_EINVAL; + } + break; + default: + print_help(); + return KNOT_ENOTSUP; + } + + // Complete missing data in queries based on defaults. + complete_queries(¶ms->queries, params->config); + + return KNOT_EOK; +} diff --git a/src/utils/khost/khost_params.h b/src/utils/khost/khost_params.h new file mode 100644 index 0000000..6f019fe --- /dev/null +++ b/src/utils/khost/khost_params.h @@ -0,0 +1,22 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "utils/kdig/kdig_params.h" + +int khost_parse(kdig_params_t *params, int argc, char *argv[]); +void khost_clean(kdig_params_t *params); diff --git a/src/utils/kjournalprint/main.c b/src/utils/kjournalprint/main.c new file mode 100644 index 0000000..3ba0019 --- /dev/null +++ b/src/utils/kjournalprint/main.c @@ -0,0 +1,491 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <getopt.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "libknot/libknot.h" +#include "knot/journal/journal_basic.h" +#include "knot/journal/journal_metadata.h" +#include "knot/journal/journal_read.h" +#include "knot/journal/serialization.h" +#include "knot/zone/zone-dump.h" +#include "utils/common/msg.h" +#include "utils/common/params.h" +#include "utils/common/signal.h" +#include "utils/common/util_conf.h" +#include "contrib/color.h" +#include "contrib/strtonum.h" +#include "contrib/string.h" +#include "contrib/time.h" + +#define PROGRAM_NAME "kjournalprint" + +knot_lmdb_db_t journal_db = { 0 }; +signal_ctx_t signal_ctx = { 0 }; // global, needed by signal handler + +static void print_help(void) +{ + printf("Usage:\n" + " %s [-c | -C | -D <path>] [options] <zone_name>\n" + " %s [-c | -C | -D <path>] -z\n" + "\n" + "Config options:\n" + " -c, --config <file> Path to a textual configuration file.\n" + " (default %s)\n" + " -C, --confdb <dir> Path to a configuration database directory.\n" + " (default %s)\n" + " -D, --dir <path> Path to a journal database directory, use default\n" + " configuration.\n" + "Options:\n" + " -z, --zone-list Instead of reading the journal, display the list\n" + " of zones in the DB.\n" + " -l, --limit <num> Read only <num> newest changes.\n" + " -s, --serial <soa> Start with a specific SOA serial.\n" + " -H, --check Additional journal semantic checks.\n" + " -d, --debug Debug mode output.\n" + " -x, --mono Get output without coloring.\n" + " -n, --no-color An alias for -x, deprecated.\n" + " -X, --color Force output coloring.\n" + " -h, --help Print the program help.\n" + " -V, --version Print the program version.\n", + PROGRAM_NAME, PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR); +} + +typedef struct { + bool debug; + bool color; + bool check; + int limit; + int counter; + uint32_t serial; + bool from_serial; + size_t changes; +} print_params_t; + +static void print_changeset(const changeset_t *chs, uint64_t timestamp, print_params_t *params) +{ + char time_buf[64] = { 0 }; + (void)knot_time_print(TIME_PRINT_UNIX, timestamp, time_buf, sizeof(time_buf)); + + static size_t count = 1; + if (chs->soa_from == NULL) { + printf("%s;; Zone-in-journal, serial: %u, changeset: %zu, timestamp: %s%s\n", + COL_YELW(params->color), + knot_soa_serial(chs->soa_to->rrs.rdata), + count++, + time_buf, + COL_RST(params->color)); + } else { + printf("%s;; Changes between zone versions: %u -> %u, changeset: %zu, timestamp: %s%s\n", + COL_YELW(params->color), + knot_soa_serial(chs->soa_from->rrs.rdata), + knot_soa_serial(chs->soa_to->rrs.rdata), + count++, + time_buf, + COL_RST(params->color)); + } + changeset_print(chs, stdout, params->color); +} + +knot_dynarray_declare(rrtype, uint16_t, DYNARRAY_VISIBILITY_STATIC, 100) +knot_dynarray_define(rrtype, uint16_t, DYNARRAY_VISIBILITY_STATIC) + +typedef struct { + rrtype_dynarray_t *arr; + size_t *counter; +} rrtypelist_ctx_t; + +static void rrtypelist_add(rrtype_dynarray_t *arr, uint16_t add_type) +{ + bool already_present = false; + knot_dynarray_foreach(rrtype, uint16_t, i, *arr) { + if (*i == add_type) { + already_present = true; + break; + } + } + if (!already_present) { + rrtype_dynarray_add(arr, &add_type); + } +} + +static int rrtypelist_callback(zone_node_t *node, void *data) +{ + rrtypelist_ctx_t *ctx = data; + for (int i = 0; i < node->rrset_count; i++) { + knot_rrset_t rrset = node_rrset_at(node, i); + rrtypelist_add(ctx->arr, rrset.type); + *ctx->counter += rrset.rrs.count; + } + return KNOT_EOK; +} + +static void print_changeset_debugmode(const changeset_t *chs, uint64_t timestamp) +{ + char time_buf[64] = { 0 }; + (void)knot_time_print(TIME_PRINT_HUMAN_MIXED, timestamp, time_buf, sizeof(time_buf)); + + // detect all types + rrtype_dynarray_t types = { 0 }; + size_t count_minus = 1, count_plus = 1; // 1 for SOA which is always present but not iterated + rrtypelist_ctx_t ctx_minus = { &types, &count_minus }, ctx_plus = { &types, &count_plus }; + (void)zone_contents_apply(chs->remove, rrtypelist_callback, &ctx_minus); + (void)zone_contents_nsec3_apply(chs->remove, rrtypelist_callback, &ctx_minus); + (void)zone_contents_apply(chs->add, rrtypelist_callback, &ctx_plus); + (void)zone_contents_nsec3_apply(chs->add, rrtypelist_callback, &ctx_plus); + + if (chs->soa_from == NULL) { + printf("Zone-in-journal %u +++: %zu\t size: %zu\t timestamp: %s\t", knot_soa_serial(chs->soa_to->rrs.rdata), + count_plus, changeset_serialized_size(chs), time_buf); + } else { + printf("%u -> %u ---: %zu\t +++: %zu\t size: %zu\t timestamp: %s\t", knot_soa_serial(chs->soa_from->rrs.rdata), + knot_soa_serial(chs->soa_to->rrs.rdata), count_minus, count_plus, changeset_serialized_size(chs), time_buf); + } + + char temp[100]; + knot_dynarray_foreach(rrtype, uint16_t, i, types) { + (void)knot_rrtype_to_string(*i, temp, sizeof(temp)); + printf(" %s", temp); + } + printf("\n"); +} + +static int count_changeset_cb(_unused_ bool special, const changeset_t *ch, _unused_ uint64_t timestamp, void *ctx) +{ + print_params_t *params = ctx; + if (ch != NULL) { + params->counter++; + } + return KNOT_EOK; +} + +static int print_changeset_cb(bool special, const changeset_t *ch, uint64_t timestamp, void *ctx) +{ + print_params_t *params = ctx; + if (ch != NULL && params->counter++ >= params->limit) { + if (params->debug) { + print_changeset_debugmode(ch, timestamp); + params->changes++; + } else { + print_changeset(ch, timestamp, params); + } + if (special && params->debug) { + printf("---------------------------------------------\n"); + } + } + return KNOT_EOK; +} + +int print_journal(char *path, knot_dname_t *name, print_params_t *params) +{ + zone_journal_t j = { &journal_db, name }; + bool exists; + uint64_t occupied, occupied_all; + + knot_lmdb_init(&journal_db, path, 0, journal_env_flags(JOURNAL_MODE_ROBUST, true), NULL); + int ret = knot_lmdb_exists(&journal_db); + if (ret == KNOT_EOK) { + ret = knot_lmdb_open(&journal_db); + } + if (ret != KNOT_EOK) { + knot_lmdb_deinit(&journal_db); + return ret; + } + + ret = journal_info(j, &exists, NULL, NULL, NULL, NULL, NULL, &occupied, &occupied_all); + if (ret != KNOT_EOK || !exists) { + ERR2("zone not exists in the journal DB %s", path); + knot_lmdb_deinit(&journal_db); + return ret == KNOT_EOK ? KNOT_ENOENT : ret; + } + + if (params->check) { + ret = journal_sem_check(j); + if (ret > 0) { + ERR2("semantic check failed with code %d", ret); + } else if (ret != KNOT_EOK) { + ERR2("semantic check failed (%s)", knot_strerror(ret)); + } + } + + if (params->limit >= 0 && ret == KNOT_EOK) { + if (params->from_serial) { + ret = journal_walk_from(j, params->serial, count_changeset_cb, params); + } else { + ret = journal_walk(j, count_changeset_cb, params); + } + } + if (ret == KNOT_EOK) { + if (params->limit < 0 || params->counter <= params->limit) { + params->limit = 0; + } else { + params->limit = params->counter - params->limit; + } + params->counter = 0; + if (params->from_serial) { + ret = journal_walk_from(j, params->serial, print_changeset_cb, params); + } else { + ret = journal_walk(j, print_changeset_cb, params); + } + } + + if (params->debug && ret == KNOT_EOK) { + printf("Total number of changesets: %zu\n", params->changes); + printf("Occupied this zone (approx): %"PRIu64" KiB\n", occupied / 1024); + printf("Occupied all zones together: %"PRIu64" KiB\n", occupied_all / 1024); + } + + knot_lmdb_deinit(&journal_db); + return ret; +} + +static int add_zone_to_list(const knot_dname_t *zone, void *list) +{ + knot_dname_t *copy = knot_dname_copy(zone, NULL); + if (copy == NULL) { + return KNOT_ENOMEM; + } + return ptrlist_add(list, copy, NULL) == NULL ? KNOT_ENOMEM : KNOT_EOK; +} + +static int list_zone(const knot_dname_t *zone, bool detailed, knot_lmdb_db_t *jdb, uint64_t *occupied_all) +{ + knot_dname_txt_storage_t zone_str; + if (knot_dname_to_str(zone_str, zone, sizeof(zone_str)) == NULL) { + return KNOT_EINVAL; + } + + if (detailed) { + zone_journal_t j = { jdb, zone }; + uint32_t first_serial = 0, last_serial = 0; + bool exists = false, zij = false; + uint64_t occupied; + + int ret = journal_info(j, &exists, &first_serial, &zij, &last_serial, + NULL, NULL, &occupied, occupied_all); + if (ret != KNOT_EOK) { + return ret; + } + assert(exists); + printf("%-28s %8"PRIu64" %10u %10u %3s\n", + zone_str, occupied / 1024, first_serial, last_serial, zij ? "yes" : "no"); + } else { + printf("%s\n", zone_str); + } + return KNOT_EOK; +} + +int list_zones(char *path, bool detailed) +{ + knot_lmdb_init(&journal_db, path, 0, journal_env_flags(JOURNAL_MODE_ROBUST, true), NULL); + + list_t zones; + init_list(&zones); + ptrnode_t *zone; + uint64_t occupied_all = 0; + bool first = detailed; + + int ret = journals_walk(&journal_db, add_zone_to_list, &zones); + WALK_LIST(zone, zones) { + if (ret != KNOT_EOK) { + break; + } else if (first) { + printf(";; <zone name> <occupied KiB> <first serial> <last serial> <full zone>\n"); + first = false; + } + ret = list_zone(zone->d, detailed, &journal_db, &occupied_all); + } + + knot_lmdb_deinit(&journal_db); + ptrlist_deep_free(&zones, NULL); + + if (detailed && ret == KNOT_EOK) { + printf(";; Occupied all zones together: %"PRIu64" KiB\n", occupied_all / 1024); + } + return ret; +} + +int main(int argc, char *argv[]) +{ + bool justlist = false; + + print_params_t params = { + .debug = false, + .color = isatty(STDOUT_FILENO), + .check = false, + .limit = -1, + .from_serial = false, + }; + + struct option opts[] = { + { "config", required_argument, NULL, 'c' }, + { "confdb", required_argument, NULL, 'C' }, + { "dir", required_argument, NULL, 'D' }, + { "limit", required_argument, NULL, 'l' }, + { "serial", required_argument, NULL, 's' }, + { "zone-list", no_argument, NULL, 'z' }, + { "check", no_argument, NULL, 'H' }, + { "debug", no_argument, NULL, 'd' }, + { "no-color", no_argument, NULL, 'n' }, + { "mono", no_argument, NULL, 'x' }, + { "color", no_argument, NULL, 'X' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { NULL } + }; + + signal_ctx.close_db = &journal_db; + signal_init_std(); + + int opt = 0; + while ((opt = getopt_long(argc, argv, "c:C:D:l:s:zHdnxXhV", opts, NULL)) != -1) { + switch (opt) { + case 'c': + if (util_conf_init_file(optarg) != KNOT_EOK) { + goto failure; + } + break; + case 'C': + if (util_conf_init_confdb(optarg) != KNOT_EOK) { + goto failure; + } + break; + case 'D': + if (util_conf_init_justdb("journal-db", optarg) != KNOT_EOK) { + goto failure; + } + break; + case 'l': + if (str_to_int(optarg, ¶ms.limit, 0, INT_MAX) != KNOT_EOK) { + print_help(); + goto failure; + } + break; + case 's': + if (str_to_u32(optarg, ¶ms.serial) != KNOT_EOK) { + print_help(); + goto failure; + } + params.from_serial = true; + break; + case 'z': + justlist = true; + break; + case 'H': + params.check = true; + break; + case 'd': + params.debug = true; + break; + case 'n': + case 'x': + params.color = false; + break; + case 'X': + params.color = true; + break; + case 'h': + print_help(); + goto success; + case 'V': + print_version(PROGRAM_NAME); + goto success; + default: + print_help(); + goto failure; + } + } + + // Backward compatibility. + if ((justlist && (argc - optind > 0)) || (!justlist && (argc - optind > 1))) { + WARN2("obsolete parameter specified"); + if (util_conf_init_justdb("journal-db", argv[optind]) != KNOT_EOK) { + goto failure; + } + optind++; + } + + signal_ctx.color = params.color; + + if (util_conf_init_default(true) != KNOT_EOK) { + goto failure; + } + + char *db = conf_db(conf(), C_JOURNAL_DB); + + if (justlist) { + int ret = list_zones(db, params.debug); + free(db); + switch (ret) { + case KNOT_ENOENT: + INFO2("No zones in journal DB"); + // FALLTHROUGH + case KNOT_EOK: + goto success; + case KNOT_ENODB: + ERR2("the journal DB does not exist"); + goto failure; + case KNOT_EMALF: + ERR2("the journal DB is broken"); + goto failure; + default: + ERR2("failed to load zone list (%s)", knot_strerror(ret)); + goto failure; + } + } else { + if (argc - optind != 1) { + print_help(); + free(db); + goto failure; + } + knot_dname_t *name = knot_dname_from_str_alloc(argv[optind]); + knot_dname_to_lower(name); + + int ret = print_journal(db, name, ¶ms); + free(name); + free(db); + switch (ret) { + case KNOT_ENOENT: + if (params.from_serial) { + INFO2("The journal is empty or the serial not present"); + } else { + INFO2("The journal is empty"); + } + break; + case KNOT_ENODB: + ERR2("the journal DB does not exist"); + goto failure; + case KNOT_EOUTOFZONE: + ERR2("the journal DB does not contain the specified zone"); + goto failure; + case KNOT_EOK: + break; + default: + ERR2("failed to load changesets (%s)", knot_strerror(ret)); + goto failure; + } + } + +success: + util_conf_deinit(); + return EXIT_SUCCESS; +failure: + util_conf_deinit(); + return EXIT_FAILURE; +} diff --git a/src/utils/knotc/commands.c b/src/utils/knotc/commands.c new file mode 100644 index 0000000..e5cb455 --- /dev/null +++ b/src/utils/knotc/commands.c @@ -0,0 +1,1377 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "libknot/libknot.h" +#include "knot/common/log.h" +#include "knot/ctl/commands.h" +#include "knot/conf/conf.h" +#include "knot/conf/confdb.h" +#include "knot/conf/module.h" +#include "knot/conf/tools.h" +#include "knot/zone/zonefile.h" +#include "knot/zone/zone-load.h" +#include "contrib/color.h" +#include "contrib/macros.h" +#include "contrib/string.h" +#include "contrib/strtonum.h" +#include "contrib/openbsd/strlcat.h" +#include "utils/knotc/commands.h" + +#define CMD_EXIT "exit" + +#define CMD_STATUS "status" +#define CMD_STOP "stop" +#define CMD_RELOAD "reload" +#define CMD_STATS "stats" + +#define CMD_ZONE_CHECK "zone-check" +#define CMD_ZONE_STATUS "zone-status" +#define CMD_ZONE_RELOAD "zone-reload" +#define CMD_ZONE_REFRESH "zone-refresh" +#define CMD_ZONE_RETRANSFER "zone-retransfer" +#define CMD_ZONE_NOTIFY "zone-notify" +#define CMD_ZONE_FLUSH "zone-flush" +#define CMD_ZONE_BACKUP "zone-backup" +#define CMD_ZONE_RESTORE "zone-restore" +#define CMD_ZONE_SIGN "zone-sign" +#define CMD_ZONE_KEYS_LOAD "zone-keys-load" +#define CMD_ZONE_KEY_ROLL "zone-key-rollover" +#define CMD_ZONE_KSK_SBM "zone-ksk-submitted" +#define CMD_ZONE_FREEZE "zone-freeze" +#define CMD_ZONE_THAW "zone-thaw" +#define CMD_ZONE_XFR_FREEZE "zone-xfr-freeze" +#define CMD_ZONE_XFR_THAW "zone-xfr-thaw" + +#define CMD_ZONE_READ "zone-read" +#define CMD_ZONE_BEGIN "zone-begin" +#define CMD_ZONE_COMMIT "zone-commit" +#define CMD_ZONE_ABORT "zone-abort" +#define CMD_ZONE_DIFF "zone-diff" +#define CMD_ZONE_GET "zone-get" +#define CMD_ZONE_SET "zone-set" +#define CMD_ZONE_UNSET "zone-unset" +#define CMD_ZONE_PURGE "zone-purge" +#define CMD_ZONE_STATS "zone-stats" + +#define CMD_CONF_INIT "conf-init" +#define CMD_CONF_CHECK "conf-check" +#define CMD_CONF_IMPORT "conf-import" +#define CMD_CONF_EXPORT "conf-export" +#define CMD_CONF_LIST "conf-list" +#define CMD_CONF_READ "conf-read" +#define CMD_CONF_BEGIN "conf-begin" +#define CMD_CONF_COMMIT "conf-commit" +#define CMD_CONF_ABORT "conf-abort" +#define CMD_CONF_DIFF "conf-diff" +#define CMD_CONF_GET "conf-get" +#define CMD_CONF_SET "conf-set" +#define CMD_CONF_UNSET "conf-unset" + +#define CTL_LOG_STR "failed to control" + +#define CTL_SEND(type, data) \ + ret = knot_ctl_send(args->ctl, (type), (data)); \ + if (ret != KNOT_EOK) { \ + log_error(CTL_LOG_STR" (%s)", knot_strerror(ret)); \ + return ret; \ + } + +#define CTL_SEND_DATA CTL_SEND(KNOT_CTL_TYPE_DATA, &data) +#define CTL_SEND_BLOCK CTL_SEND(KNOT_CTL_TYPE_BLOCK, NULL) + +static int check_args(cmd_args_t *args, int min, int max) +{ + if (max == 0 && args->argc > 0) { + log_error("command doesn't take arguments"); + return KNOT_EINVAL; + } else if (min == max && args->argc != min) { + log_error("command requires %i arguments", min); + return KNOT_EINVAL; + } else if (args->argc < min) { + log_error("command requires at least %i arguments", min); + return KNOT_EINVAL; + } else if (max > 0 && args->argc > max) { + log_error("command takes at most %i arguments", max); + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +static int check_conf_args(cmd_args_t *args) +{ + // Mask relevant flags. + cmd_flag_t flags = args->desc->flags; + flags &= CMD_FOPT_ITEM | CMD_FREQ_ITEM | CMD_FOPT_DATA; + + switch (args->argc) { + case 0: + if (flags == CMD_FNONE || (flags & CMD_FOPT_ITEM)) { + return KNOT_EOK; + } + break; + case 1: + if (flags & (CMD_FOPT_ITEM | CMD_FREQ_ITEM)) { + return KNOT_EOK; + } + break; + default: + if (flags != CMD_FNONE) { + return KNOT_EOK; + } + break; + } + + log_error("invalid number of arguments"); + + return KNOT_EINVAL; +} + +static int get_conf_key(const char *key, knot_ctl_data_t *data) +{ + // Get key0. + const char *key0 = key; + + // Check for id. + char *id = strchr(key, '['); + if (id != NULL) { + // Separate key0 and id. + *id++ = '\0'; + + // Check for id end. + char *id_end = id; + while ((id_end = strchr(id_end, ']')) != NULL) { + // Check for escaped character. + if (*(id_end - 1) != '\\') { + break; + } + id_end++; + } + + // Check for unclosed id. + if (id_end == NULL) { + log_error("(missing bracket after identifier) %s", id); + return KNOT_EINVAL; + } + + // Separate id and key1. + *id_end = '\0'; + + key = id_end + 1; + + // Key1 or nothing must follow. + if (*key != '.' && *key != '\0') { + log_error("(unexpected token) %s", key); + return KNOT_EINVAL; + } + } + + // Check for key1. + char *key1 = strchr(key, '.'); + if (key1 != NULL) { + // Separate key0/id and key1. + *key1++ = '\0'; + + if (*key1 == '\0') { + log_error("(missing item specification)"); + return KNOT_EINVAL; + } + } + + (*data)[KNOT_CTL_IDX_SECTION] = key0; + (*data)[KNOT_CTL_IDX_ITEM] = key1; + (*data)[KNOT_CTL_IDX_ID] = id; + + return KNOT_EOK; +} + +static void format_data(cmd_args_t *args, knot_ctl_type_t data_type, + knot_ctl_data_t *data, bool *empty) +{ + const char *error = (*data)[KNOT_CTL_IDX_ERROR]; + const char *flags = (*data)[KNOT_CTL_IDX_FLAGS]; + const char *key0 = (*data)[KNOT_CTL_IDX_SECTION]; + const char *key1 = (*data)[KNOT_CTL_IDX_ITEM]; + const char *id = (*data)[KNOT_CTL_IDX_ID]; + const char *zone = (*data)[KNOT_CTL_IDX_ZONE]; + const char *owner = (*data)[KNOT_CTL_IDX_OWNER]; + const char *ttl = (*data)[KNOT_CTL_IDX_TTL]; + const char *type = (*data)[KNOT_CTL_IDX_TYPE]; + const char *value = (*data)[KNOT_CTL_IDX_DATA]; + + bool col = false; + char status_col[32] = ""; + + static bool first_status_item = true; + + const char *sign = NULL; + if (ctl_has_flag(flags, CTL_FLAG_DIFF_ADD)) { + sign = CTL_FLAG_DIFF_ADD; + } else if (ctl_has_flag(flags, CTL_FLAG_DIFF_REM)) { + sign = CTL_FLAG_DIFF_REM; + } + + switch (args->desc->cmd) { + case CTL_STATUS: + if (error != NULL) { + printf("error: (%s)%s%s", error, + (type != NULL) ? " " : "", + (type != NULL) ? type : ""); + } else if (value != NULL) { + printf("%s", value); + *empty = false; + } + break; + case CTL_STOP: + case CTL_RELOAD: + case CTL_CONF_BEGIN: + case CTL_CONF_ABORT: + // Only error message is expected here. + if (error != NULL) { + printf("error: (%s)", error); + } + break; + case CTL_ZONE_STATUS: + if (error == NULL) { + col = args->extended ? args->color_force : args->color; + } + if (!ctl_has_flag(flags, CTL_FLAG_STATUS_EMPTY)) { + strlcat(status_col, COL_BOLD(col), sizeof(status_col)); + } + if (ctl_has_flag(flags, CTL_FLAG_STATUS_SLAVE)) { + strlcat(status_col, COL_RED(col), sizeof(status_col)); + } else { + strlcat(status_col, COL_GRN(col), sizeof(status_col)); + } + if (ctl_has_flag(flags, CTL_FLAG_STATUS_MEMBER)) { + strlcat(status_col, COL_UNDR(col), sizeof(status_col)); + } + // FALLTHROUGH + case CTL_ZONE_RELOAD: + case CTL_ZONE_REFRESH: + case CTL_ZONE_RETRANSFER: + case CTL_ZONE_NOTIFY: + case CTL_ZONE_FLUSH: + case CTL_ZONE_BACKUP: + case CTL_ZONE_RESTORE: + case CTL_ZONE_SIGN: + case CTL_ZONE_KEYS_LOAD: + case CTL_ZONE_KEY_ROLL: + case CTL_ZONE_KSK_SBM: + case CTL_ZONE_FREEZE: + case CTL_ZONE_THAW: + case CTL_ZONE_BEGIN: + case CTL_ZONE_COMMIT: + case CTL_ZONE_ABORT: + case CTL_ZONE_PURGE: + if (data_type == KNOT_CTL_TYPE_DATA) { + printf("%s%s%s%s%s%s%s%s%s%s", + (!(*empty) ? "\n" : ""), + (error != NULL ? "error: " : ""), + (zone != NULL ? "[" : ""), + (zone != NULL ? status_col : ""), + (zone != NULL ? zone : ""), + (zone != NULL ? COL_RST(col) : ""), + (zone != NULL ? "]" : ""), + (error != NULL ? " (" : ""), + (error != NULL ? error : ""), + (error != NULL ? ")" : "")); + *empty = false; + } + if (args->desc->cmd == CTL_ZONE_STATUS && type != NULL) { + if (data_type == KNOT_CTL_TYPE_DATA) { + first_status_item = true; + } + if (!args->extended && + (value == 0 || strcmp(value, STATUS_EMPTY) == 0) && + strcmp(type, "serial") != 0) { + return; + } + + printf("%s %s: %s%s%s", + (first_status_item ? "" : " |"), + type, COL_BOLD(col), value, COL_RST(col)); + first_status_item = false; + } + break; + case CTL_CONF_COMMIT: // Can return a check error context. + case CTL_CONF_LIST: + case CTL_CONF_READ: + case CTL_CONF_DIFF: + case CTL_CONF_GET: + case CTL_CONF_SET: + case CTL_CONF_UNSET: + if (data_type == KNOT_CTL_TYPE_DATA) { + printf("%s%s%s%s%s%s%s%s%s%s%s%s", + (!(*empty) ? "\n" : ""), + (error != NULL ? "error: (" : ""), + (error != NULL ? error : ""), + (error != NULL ? ") " : ""), + (sign != NULL ? sign : ""), + (key0 != NULL ? key0 : ""), + (id != NULL ? "[" : ""), + (id != NULL ? id : ""), + (id != NULL ? "]" : ""), + (key1 != NULL ? "." : ""), + (key1 != NULL ? key1 : ""), + (value != NULL ? " =" : "")); + *empty = false; + } + if (value != NULL) { + printf(" %s", value); + } + break; + case CTL_ZONE_READ: + case CTL_ZONE_DIFF: + case CTL_ZONE_GET: + case CTL_ZONE_SET: + case CTL_ZONE_UNSET: + printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", + (!(*empty) ? "\n" : ""), + (error != NULL ? "error: (" : ""), + (error != NULL ? error : ""), + (error != NULL ? ") " : ""), + (zone != NULL ? "[" : ""), + (zone != NULL ? zone : ""), + (zone != NULL ? "] " : ""), + (sign != NULL ? sign : ""), + (owner != NULL ? owner : ""), + (ttl != NULL ? " " : ""), + (ttl != NULL ? ttl : ""), + (type != NULL ? " " : ""), + (type != NULL ? type : ""), + (value != NULL ? " " : ""), + (value != NULL ? value : "")); + *empty = false; + break; + case CTL_STATS: + case CTL_ZONE_STATS: + printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", + (!(*empty) ? "\n" : ""), + (error != NULL ? "error: (" : ""), + (error != NULL ? error : ""), + (error != NULL ? ") " : ""), + (zone != NULL ? "[" : ""), + (zone != NULL ? zone : ""), + (zone != NULL ? "] " : ""), + (key0 != NULL ? key0 : ""), + (key1 != NULL ? "." : ""), + (key1 != NULL ? key1 : ""), + (id != NULL ? "[" : ""), + (id != NULL ? id : ""), + (id != NULL ? "]" : ""), + (value != NULL ? " = " : ""), + (value != NULL ? value : "")); + *empty = false; + break; + default: + assert(0); + } +} + +static void format_block(ctl_cmd_t cmd, bool failed, bool empty) +{ + switch (cmd) { + case CTL_STATUS: + printf("%s\n", (failed || !empty) ? "" : "Running"); + break; + case CTL_STOP: + printf("%s\n", failed ? "" : "Stopped"); + break; + case CTL_RELOAD: + printf("%s\n", failed ? "" : "Reloaded"); + break; + case CTL_CONF_BEGIN: + case CTL_CONF_COMMIT: + case CTL_CONF_ABORT: + case CTL_CONF_SET: + case CTL_CONF_UNSET: + case CTL_ZONE_RELOAD: + case CTL_ZONE_REFRESH: + case CTL_ZONE_RETRANSFER: + case CTL_ZONE_NOTIFY: + case CTL_ZONE_FLUSH: + case CTL_ZONE_BACKUP: + case CTL_ZONE_RESTORE: + case CTL_ZONE_SIGN: + case CTL_ZONE_KEYS_LOAD: + case CTL_ZONE_KEY_ROLL: + case CTL_ZONE_KSK_SBM: + case CTL_ZONE_FREEZE: + case CTL_ZONE_THAW: + case CTL_ZONE_XFR_FREEZE: + case CTL_ZONE_XFR_THAW: + case CTL_ZONE_BEGIN: + case CTL_ZONE_COMMIT: + case CTL_ZONE_ABORT: + case CTL_ZONE_SET: + case CTL_ZONE_UNSET: + case CTL_ZONE_PURGE: + printf("%s\n", failed ? "" : "OK"); + break; + case CTL_ZONE_STATUS: + case CTL_ZONE_READ: + case CTL_ZONE_DIFF: + case CTL_ZONE_GET: + case CTL_CONF_LIST: + case CTL_CONF_READ: + case CTL_CONF_DIFF: + case CTL_CONF_GET: + case CTL_ZONE_STATS: + case CTL_STATS: + printf("%s", empty ? "" : "\n"); + break; + default: + assert(0); + } +} + +static int ctl_receive(cmd_args_t *args) +{ + bool failed = false; + bool empty = true; + + while (true) { + knot_ctl_type_t type; + knot_ctl_data_t data; + + int ret = knot_ctl_receive(args->ctl, &type, &data); + if (ret != KNOT_EOK) { + log_error(CTL_LOG_STR" (%s)", knot_strerror(ret)); + return ret; + } + + switch (type) { + case KNOT_CTL_TYPE_END: + log_error(CTL_LOG_STR" (%s)", knot_strerror(KNOT_EMALF)); + return KNOT_EMALF; + case KNOT_CTL_TYPE_BLOCK: + format_block(args->desc->cmd, failed, empty); + return failed ? KNOT_ERROR : KNOT_EOK; + case KNOT_CTL_TYPE_DATA: + case KNOT_CTL_TYPE_EXTRA: + format_data(args, type, &data, &empty); + break; + default: + assert(0); + return KNOT_EINVAL; + } + + if (data[KNOT_CTL_IDX_ERROR] != NULL) { + failed = true; + } + } + + return KNOT_EOK; +} + +static int cmd_ctl(cmd_args_t *args) +{ + int ret = check_args(args, 0, (args->desc->cmd == CTL_STATUS ? 1 : 0)); + if (ret != KNOT_EOK) { + return ret; + } + + knot_ctl_data_t data = { + [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd), + [KNOT_CTL_IDX_FLAGS] = args->flags, + [KNOT_CTL_IDX_TYPE] = args->argc > 0 ? args->argv[0] : NULL + }; + + CTL_SEND_DATA + CTL_SEND_BLOCK + + return ctl_receive(args); +} + +static int set_stats_items(cmd_args_t *args, knot_ctl_data_t *data) +{ + int min_args, max_args; + switch (args->desc->cmd) { + case CTL_STATS: min_args = 0; max_args = 1; break; + case CTL_ZONE_STATS: min_args = 1; max_args = 2; break; + default: + assert(0); + return KNOT_EINVAL; + } + + // Check the number of arguments. + int ret = check_args(args, min_args, max_args); + if (ret != KNOT_EOK) { + return ret; + } + + int idx = 0; + + // Set ZONE name. + if (args->argc > idx && args->desc->cmd == CTL_ZONE_STATS) { + if (strcmp(args->argv[idx], "--") != 0) { + (*data)[KNOT_CTL_IDX_ZONE] = args->argv[idx]; + } + idx++; + } + + if (args->argc > idx) { + (*data)[KNOT_CTL_IDX_SECTION] = args->argv[idx]; + + char *item = strchr(args->argv[idx], '.'); + if (item != NULL) { + // Separate section and item. + *item++ = '\0'; + (*data)[KNOT_CTL_IDX_ITEM] = item; + } + } + + return KNOT_EOK; +} + +static int cmd_stats_ctl(cmd_args_t *args) +{ + knot_ctl_data_t data = { + [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd), + [KNOT_CTL_IDX_FLAGS] = args->flags, + }; + + int ret = set_stats_items(args, &data); + if (ret != KNOT_EOK) { + return ret; + } + + CTL_SEND_DATA + CTL_SEND_BLOCK + + return ctl_receive(args); +} + +static int zone_exec(cmd_args_t *args, int (*fcn)(const knot_dname_t *, void *), + void *data) +{ + bool failed = false; + + // Process specified zones. + if (args->argc > 0) { + knot_dname_storage_t id; + + for (int i = 0; i < args->argc; i++) { + if (knot_dname_from_str(id, args->argv[i], sizeof(id)) == NULL) { + log_zone_str_error(args->argv[i], "invalid name"); + failed = true; + continue; + } + knot_dname_to_lower(id); + + if (!conf_rawid_exists(conf(), C_ZONE, id, knot_dname_size(id))) { + log_zone_error(id, "%s", knot_strerror(KNOT_ENOZONE)); + failed = true; + continue; + } + + if (fcn(id, data) != KNOT_EOK) { + failed = true; + } + } + // Process all configured zones. + } else { + for (conf_iter_t iter = conf_iter(conf(), C_ZONE); + iter.code == KNOT_EOK; conf_iter_next(conf(), &iter)) { + conf_val_t val = conf_iter_id(conf(), &iter); + const knot_dname_t *id = conf_dname(&val); + + if (fcn(id, data) != KNOT_EOK) { + failed = true; + } + } + } + + return failed ? KNOT_ERROR : KNOT_EOK; +} + +static int zone_check(const knot_dname_t *dname, void *data) +{ + cmd_args_t *args = data; + + conf_val_t load = conf_zone_get(conf(), C_ZONEFILE_LOAD, dname); + if (conf_opt(&load) == ZONEFILE_LOAD_NONE) { + return KNOT_EOK; + } + + zone_contents_t *contents = NULL; + conf_val_t mode = conf_zone_get(conf(), C_SEM_CHECKS, dname); + int ret = zone_load_contents(conf(), dname, &contents, conf_opt(&mode), args->force); + zone_contents_deep_free(contents); + if (ret != KNOT_EOK && ret != KNOT_ESEMCHECK) { + knot_dname_txt_storage_t name; + (void)knot_dname_to_str(name, dname, sizeof(name)); + log_error("[%s] failed to check zone file (%s)", name, knot_strerror(ret)); + } + + return ret; +} + +static int cmd_zone_check(cmd_args_t *args) +{ + return zone_exec(args, zone_check, args); +} + +static int cmd_zone_key_roll_ctl(cmd_args_t *args) +{ + int ret = check_args(args, 2, 2); + if (ret != KNOT_EOK) { + return ret; + } + + knot_ctl_data_t data = { + [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd), + [KNOT_CTL_IDX_FLAGS] = args->flags, + [KNOT_CTL_IDX_ZONE] = args->argv[0], + [KNOT_CTL_IDX_TYPE] = args->argv[1], + }; + + CTL_SEND_DATA + CTL_SEND_BLOCK + + return ctl_receive(args); +} + +static int cmd_zone_ctl(cmd_args_t *args) +{ + knot_ctl_data_t data = { + [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd), + [KNOT_CTL_IDX_FLAGS] = args->flags, + }; + + // Check the number of arguments. + int ret = check_args(args, (args->desc->flags & CMD_FREQ_ZONE) ? 1 : 0, -1); + if (ret != KNOT_EOK) { + return ret; + } + + if (args->desc->cmd == CTL_ZONE_PURGE && !args->force) { + log_error("force option required!"); + return KNOT_EDENIED; + } + + // Ignore all zones argument. + if (args->argc == 1 && strcmp(args->argv[0], "--") == 0) { + args->argc = 0; + } + + if (args->argc == 0) { + CTL_SEND_DATA + } + for (int i = 0; i < args->argc; i++) { + data[KNOT_CTL_IDX_ZONE] = args->argv[i]; + + CTL_SEND_DATA + } + + CTL_SEND_BLOCK + + return ctl_receive(args); +} + +#define FILTER_IMPORT_NOPURGE "+nopurge" + +typedef struct { + const char *name; + char id; + bool with_data; // Only ONE filter of each filter_desc_t may have data! +} filter_desc_t; + +const filter_desc_t zone_flush_filters[] = { + { "+outdir", CTL_FILTER_FLUSH_OUTDIR, true }, + { NULL }, +}; + +const filter_desc_t zone_backup_filters[] = { + { "+backupdir", CTL_FILTER_BACKUP_OUTDIR, true }, + { "+zonefile", CTL_FILTER_BACKUP_ZONEFILE, false }, + { "+nozonefile", CTL_FILTER_BACKUP_NOZONEFILE, false }, + { "+journal", CTL_FILTER_BACKUP_JOURNAL, false }, + { "+nojournal", CTL_FILTER_BACKUP_NOJOURNAL, false }, + { "+timers", CTL_FILTER_BACKUP_TIMERS, false }, + { "+notimers", CTL_FILTER_BACKUP_NOTIMERS, false }, + { "+kaspdb", CTL_FILTER_BACKUP_KASPDB, false }, + { "+nokaspdb", CTL_FILTER_BACKUP_NOKASPDB, false }, + { "+keysonly", CTL_FILTER_BACKUP_KEYSONLY, false }, + { "+nokeysonly", CTL_FILTER_BACKUP_NOKEYSONLY, false }, + { "+catalog", CTL_FILTER_BACKUP_CATALOG, false }, + { "+nocatalog", CTL_FILTER_BACKUP_NOCATALOG, false }, + { "+quic", CTL_FILTER_BACKUP_QUIC, false }, + { "+noquic", CTL_FILTER_BACKUP_NOQUIC, false }, + { NULL }, +}; + +const filter_desc_t zone_status_filters[] = { + { "+role", CTL_FILTER_STATUS_ROLE }, + { "+serial", CTL_FILTER_STATUS_SERIAL }, + { "+transaction", CTL_FILTER_STATUS_TRANSACTION }, + { "+freeze", CTL_FILTER_STATUS_FREEZE }, + { "+catalog", CTL_FILTER_STATUS_CATALOG }, + { "+events", CTL_FILTER_STATUS_EVENTS }, + { NULL }, +}; + +const filter_desc_t zone_purge_filters[] = { + { "+expire", CTL_FILTER_PURGE_EXPIRE }, + { "+zonefile", CTL_FILTER_PURGE_ZONEFILE }, + { "+journal", CTL_FILTER_PURGE_JOURNAL }, + { "+timers", CTL_FILTER_PURGE_TIMERS }, + { "+kaspdb", CTL_FILTER_PURGE_KASPDB }, + { "+catalog", CTL_FILTER_PURGE_CATALOG }, + { "+orphan", CTL_FILTER_PURGE_ORPHAN }, + { NULL }, +}; + +const filter_desc_t null_filter = { NULL }; + +#define MAX_FILTERS sizeof(zone_backup_filters) / sizeof(filter_desc_t) - 1 + +static const filter_desc_t *get_filter(ctl_cmd_t cmd, const char *filter_name) +{ + const filter_desc_t *fd = NULL; + switch (cmd) { + case CTL_ZONE_FLUSH: + fd = zone_flush_filters; + break; + case CTL_ZONE_BACKUP: + case CTL_ZONE_RESTORE: + fd = zone_backup_filters; + break; + case CTL_ZONE_STATUS: + fd = zone_status_filters; + break; + case CTL_ZONE_PURGE: + fd = zone_purge_filters; + break; + default: + return &null_filter; + } + for (size_t i = 0; fd[i].name != NULL; i++) { + if (strcmp(fd[i].name, filter_name) == 0) { + return &fd[i]; + } + } + return &null_filter; +} + +static int cmd_zone_filter_ctl(cmd_args_t *args) +{ + knot_ctl_data_t data = { + [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd), + [KNOT_CTL_IDX_FLAGS] = args->flags, + }; + + if (args->desc->cmd == CTL_ZONE_PURGE && !args->force) { + log_error("force option required!"); + return KNOT_EDENIED; + } + + char filter_buff[MAX_FILTERS + 1] = { 0 }; + + // First, process the filters. + for (int i = 0; i < args->argc; i++) { + if (args->argv[i][0] == '+') { + if (data[KNOT_CTL_IDX_FILTER] == NULL) { + data[KNOT_CTL_IDX_FILTER] = filter_buff; + } + char filter_id[2] = { get_filter(args->desc->cmd, args->argv[i])->id, 0 }; + if (filter_id[0] == '\0') { + log_error("unknown filter: %s", args->argv[i]); + return KNOT_EINVAL; + } + if (strchr(filter_buff, filter_id[0]) == NULL) { + assert(strlen(filter_buff) < MAX_FILTERS); + strlcat(filter_buff, filter_id, sizeof(filter_buff)); + } + if (get_filter(args->desc->cmd, args->argv[i])->with_data) { + data[KNOT_CTL_IDX_DATA] = args->argv[++i]; + } + } + } + + // Second, process zones. + int ret; + int sentzones = 0; + bool twodash = false; + for (int i = 0; i < args->argc; i++) { + // Skip filters. + if (args->argv[i][0] == '+') { + if (get_filter(args->desc->cmd, args->argv[i])->with_data) { + i++; + } + continue; + } + + if (strcmp(args->argv[i], "--") != 0) { + data[KNOT_CTL_IDX_ZONE] = args->argv[i]; + CTL_SEND_DATA + sentzones++; + } else { + twodash = true; + } + } + + if ((args->desc->flags & CMD_FREQ_ZONE) && sentzones == 0 && !twodash) { + log_error("zone must be specified (or -- for all zones)"); + return KNOT_EDENIED; + } + + if (sentzones == 0) { + CTL_SEND_DATA + } + CTL_SEND_BLOCK + + return ctl_receive(args); +} + +static int set_rdata(cmd_args_t *args, int pos, char *rdata, size_t rdata_len) +{ + rdata[0] = '\0'; + + for (int i = pos; i < args->argc; i++) { + if (i > pos && strlcat(rdata, " ", rdata_len) >= rdata_len) { + return KNOT_ESPACE; + } + if (strlcat(rdata, args->argv[i], rdata_len) >= rdata_len) { + return KNOT_ESPACE; + } + } + + return KNOT_EOK; +} + +static int set_node_items(cmd_args_t *args, knot_ctl_data_t *data, char *rdata, + size_t rdata_len) +{ + int min_args, max_args; + switch (args->desc->cmd) { + case CTL_ZONE_READ: + case CTL_ZONE_GET: min_args = 1; max_args = 3; break; + case CTL_ZONE_DIFF: min_args = 1; max_args = 1; break; + case CTL_ZONE_SET: min_args = 3; max_args = -1; break; + case CTL_ZONE_UNSET: min_args = 2; max_args = -1; break; + default: + assert(0); + return KNOT_EINVAL; + } + + // Check the number of arguments. + int ret = check_args(args, min_args, max_args); + if (ret != KNOT_EOK) { + return ret; + } + + int idx = 0; + + // Set ZONE name. + assert(args->argc > idx); + if (strcmp(args->argv[idx], "--") != 0) { + (*data)[KNOT_CTL_IDX_ZONE] = args->argv[idx]; + } + idx++; + + // Set OWNER name if specified. + if (args->argc > idx) { + (*data)[KNOT_CTL_IDX_OWNER] = args->argv[idx]; + idx++; + } + + // Set TTL only with an editing operation. + if (args->argc > idx) { + uint32_t num; + uint16_t type; + if (knot_rrtype_from_string(args->argv[idx], &type) != 0 && + str_to_u32(args->argv[idx], &num) == KNOT_EOK) { + switch (args->desc->cmd) { + case CTL_ZONE_SET: + case CTL_ZONE_UNSET: + (*data)[KNOT_CTL_IDX_TTL] = args->argv[idx]; + idx++; + break; + default: + break; + } + } + } + + // Set record TYPE if specified. + if (args->argc > idx) { + (*data)[KNOT_CTL_IDX_TYPE] = args->argv[idx]; + idx++; + } + + // Set record DATA if specified. + if (args->argc > idx) { + ret = set_rdata(args, idx, rdata, rdata_len); + if (ret != KNOT_EOK) { + return ret; + } + (*data)[KNOT_CTL_IDX_DATA] = rdata; + } + + return KNOT_EOK; +} + +static int cmd_zone_node_ctl(cmd_args_t *args) +{ + knot_ctl_data_t data = { + [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd), + [KNOT_CTL_IDX_FLAGS] = args->flags, + }; + + char rdata[65536]; // Maximum item size in libknot control interface. + + int ret = set_node_items(args, &data, rdata, sizeof(rdata)); + if (ret != KNOT_EOK) { + return ret; + } + + CTL_SEND_DATA + CTL_SEND_BLOCK + + return ctl_receive(args); +} + +static int cmd_conf_init(cmd_args_t *args) +{ + int ret = check_args(args, 0, 0); + if (ret != KNOT_EOK) { + return ret; + } + + ret = conf_db_check(conf(), &conf()->read_txn); + if ((ret >= KNOT_EOK || ret == KNOT_CONF_EVERSION)) { + if (ret != KNOT_EOK && !args->force) { + log_error("use force option to overwrite the existing " + "destination and ensure the server is not running!"); + return KNOT_EDENIED; + } + + ret = conf_import(conf(), "", 0); + } + + if (ret == KNOT_EOK) { + log_info("OK"); + } else { + log_error("init (%s)", knot_strerror(ret)); + } + + return ret; +} + +static int conf_check_group(const yp_item_t *group, const uint8_t *id, size_t id_len) +{ + knotd_conf_check_extra_t extra = { + .conf = conf(), + .txn = &conf()->read_txn, + .check = true + }; + knotd_conf_check_args_t args = { + .id = id, + .id_len = id_len, + .extra = &extra + }; + + bool non_empty = false; + bool error = false; + + // Check the group sub-items. + for (yp_item_t *item = group->sub_items; item->name != NULL; item++) { + args.item = item; + + conf_val_t bin; + conf_db_get(conf(), &conf()->read_txn, group->name, item->name, + id, id_len, &bin); + if (bin.code == KNOT_ENOENT) { + continue; + } else if (bin.code != KNOT_EOK) { + log_error("failed to read the configuration DB (%s)", + knot_strerror(bin.code)); + return bin.code; + } + + non_empty = true; + + // Check the item value(s). + size_t values = conf_val_count(&bin); + for (size_t i = 1; i <= values; i++) { + conf_val(&bin); + args.data = bin.data; + args.data_len = bin.len; + + int ret = conf_exec_callbacks(&args); + if (ret != KNOT_EOK) { + log_error("config, item '%s%s%s%s.%s' (%s)", + group->name + 1, + (id != NULL ? "[" : ""), + (id != NULL ? (const char *)id : ""), + (id != NULL ? "]" : ""), + item->name + 1, + args.err_str); + error = true; + } + if (values > 1) { + conf_val_next(&bin); + } + } + } + + // Check the group item itself. + if (id != NULL || non_empty) { + args.item = group; + args.data = NULL; + args.data_len = 0; + + int ret = conf_exec_callbacks(&args); + if (ret != KNOT_EOK) { + log_error("config, section '%s%s%s%s' (%s)", + group->name + 1, + (id != NULL ? "[" : ""), + (id != NULL ? (const char *)id : ""), + (id != NULL ? "]" : ""), + args.err_str); + error = true; + } + } + + return error ? KNOT_ESEMCHECK : KNOT_EOK; +} + +static int cmd_conf_check(cmd_args_t *args) // Similar to conf_io_check(). +{ + int ret = check_args(args, 0, 0); + if (ret != KNOT_EOK) { + return ret; + } + + if (conf()->filename == NULL) { // Config file already checked. + for (yp_item_t *item = conf()->schema; item->name != NULL; item++) { + // Skip include item. + if (item->type != YP_TGRP) { + continue; + } + + // Group without identifiers. + if (!(item->flags & YP_FMULTI)) { + ret = conf_check_group(item, NULL, 0); + if (ret != KNOT_EOK) { + return ret; + } + continue; + } + + conf_iter_t iter; + ret = conf_db_iter_begin(conf(), &conf()->read_txn, item->name, &iter); + if (ret == KNOT_ENOENT) { + continue; + } else if (ret != KNOT_EOK) { + log_error("failed to read the configuration DB (%s)", + knot_strerror(ret)); + return ret; + } + + while (ret == KNOT_EOK) { + const uint8_t *id; + size_t id_len; + ret = conf_db_iter_id(conf(), &iter, &id, &id_len); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + log_error("failed to read the configuration DB (%s)", + knot_strerror(ret)); + return ret; + } + + // Check the group with this identifier. + ret = conf_check_group(item, id, id_len); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + return ret; + } + + ret = conf_db_iter_next(conf(), &iter); + } + if (ret != KNOT_EOF) { + log_error("failed to read the configuration DB (%s)", + knot_strerror(ret)); + return ret; + } + } + } + + log_info("Configuration is valid"); + + return KNOT_EOK; +} + +static int cmd_conf_import(cmd_args_t *args) +{ + int ret = check_args(args, 1, 2); + if (ret != KNOT_EOK) { + return ret; + } + + import_flag_t flags = IMPORT_FILE; + if (args->argc == 2) { + const char *filter = args->argv[1]; + if (strcmp(filter, FILTER_IMPORT_NOPURGE) == 0) { + flags |= IMPORT_NO_PURGE; + } else { + log_error("unknown filter: %s", filter); + return KNOT_EINVAL; + } + } + + ret = conf_db_check(conf(), &conf()->read_txn); + if ((ret >= KNOT_EOK || ret == KNOT_CONF_EVERSION)) { + if (ret != KNOT_EOK && !args->force) { + log_error("use force option to modify/overwrite the existing " + "destination and ensure the server is not running!"); + return KNOT_EDENIED; + } + + // Import to a cloned conf to avoid external module conflict. + conf_t *new_conf = NULL; + ret = conf_clone(&new_conf); + if (ret == KNOT_EOK) { + yp_schema_purge_dynamic(new_conf->schema); + log_debug("loading modules for imported configuration"); + ret = conf_mod_load_common(new_conf); + if (ret == KNOT_EOK) { + log_debug("importing confdb from file '%s'", args->argv[0]); + ret = conf_import(new_conf, args->argv[0], flags); + } + conf_free(new_conf); + } + } + + if (ret == KNOT_EOK) { + log_info("OK"); + } else { + log_error("import (%s)", knot_strerror(ret)); + } + + return ret; +} + +static int cmd_conf_export(cmd_args_t *args) +{ + int ret = check_args(args, 0, 1); + if (ret != KNOT_EOK) { + return ret; + } + + // Stdout is the default output file. + const char *file_name = NULL; + if (args->argc > 0) { + file_name = args->argv[0]; + log_debug("exporting confdb into file '%s'", file_name); + } + + ret = conf_export(conf(), file_name, YP_SNONE); + + if (ret == KNOT_EOK) { + if (args->argc > 0) { + log_info("OK"); + } + } else { + log_error("export (%s)", knot_strerror(ret)); + } + + return ret; +} + +static int cmd_conf_ctl(cmd_args_t *args) +{ + // Check the number of arguments. + int ret = check_conf_args(args); + if (ret != KNOT_EOK) { + return ret; + } + + char flags[16] = ""; + strlcat(flags, args->flags, sizeof(flags)); + if (args->desc->flags & CMD_FLIST_SCHEMA) { + strlcat(flags, CTL_FLAG_LIST_SCHEMA, sizeof(flags)); + } + + knot_ctl_data_t data = { + [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd), + [KNOT_CTL_IDX_FLAGS] = flags, + }; + + // Send the command without parameters. + if (args->argc == 0) { + CTL_SEND_DATA + // Set the first item argument. + } else { + ret = get_conf_key(args->argv[0], &data); + if (ret != KNOT_EOK) { + return ret; + } + + // Send if only one argument or item without values. + if (args->argc == 1 || !(args->desc->flags & CMD_FOPT_DATA)) { + CTL_SEND_DATA + } + } + + // Send the item values or the other items. + for (int i = 1; i < args->argc; i++) { + if (args->desc->flags & CMD_FOPT_DATA) { + data[KNOT_CTL_IDX_DATA] = args->argv[i]; + } else { + ret = get_conf_key(args->argv[i], &data); + if (ret != KNOT_EOK) { + return ret; + } + } + + CTL_SEND_DATA + } + + CTL_SEND_BLOCK + + return ctl_receive(args); +} + +const cmd_desc_t cmd_table[] = { + { CMD_EXIT, NULL, CTL_NONE }, + + { CMD_STATUS, cmd_ctl, CTL_STATUS, CMD_FOPT_DATA}, + { CMD_STOP, cmd_ctl, CTL_STOP }, + { CMD_RELOAD, cmd_ctl, CTL_RELOAD }, + { CMD_STATS, cmd_stats_ctl, CTL_STATS }, + + { CMD_ZONE_CHECK, cmd_zone_check, CTL_NONE, CMD_FOPT_ZONE | CMD_FREAD }, + { CMD_ZONE_STATUS, cmd_zone_filter_ctl, CTL_ZONE_STATUS, CMD_FOPT_ZONE }, + { CMD_ZONE_RELOAD, cmd_zone_ctl, CTL_ZONE_RELOAD, CMD_FOPT_ZONE }, + { CMD_ZONE_REFRESH, cmd_zone_ctl, CTL_ZONE_REFRESH, CMD_FOPT_ZONE }, + { CMD_ZONE_RETRANSFER, cmd_zone_ctl, CTL_ZONE_RETRANSFER, CMD_FOPT_ZONE }, + { CMD_ZONE_NOTIFY, cmd_zone_ctl, CTL_ZONE_NOTIFY, CMD_FOPT_ZONE }, + { CMD_ZONE_FLUSH, cmd_zone_filter_ctl, CTL_ZONE_FLUSH, CMD_FOPT_ZONE }, + { CMD_ZONE_BACKUP, cmd_zone_filter_ctl, CTL_ZONE_BACKUP, CMD_FOPT_ZONE }, + { CMD_ZONE_RESTORE, cmd_zone_filter_ctl, CTL_ZONE_RESTORE, CMD_FOPT_ZONE }, + { CMD_ZONE_SIGN, cmd_zone_ctl, CTL_ZONE_SIGN, CMD_FOPT_ZONE }, + { CMD_ZONE_KEYS_LOAD, cmd_zone_ctl, CTL_ZONE_KEYS_LOAD, CMD_FOPT_ZONE }, + { CMD_ZONE_KEY_ROLL, cmd_zone_key_roll_ctl, CTL_ZONE_KEY_ROLL, CMD_FREQ_ZONE }, + { CMD_ZONE_KSK_SBM, cmd_zone_ctl, CTL_ZONE_KSK_SBM, CMD_FREQ_ZONE | CMD_FOPT_ZONE }, + { CMD_ZONE_FREEZE, cmd_zone_ctl, CTL_ZONE_FREEZE, CMD_FOPT_ZONE }, + { CMD_ZONE_THAW, cmd_zone_ctl, CTL_ZONE_THAW, CMD_FOPT_ZONE }, + { CMD_ZONE_XFR_FREEZE, cmd_zone_ctl, CTL_ZONE_XFR_FREEZE, CMD_FOPT_ZONE }, + { CMD_ZONE_XFR_THAW, cmd_zone_ctl, CTL_ZONE_XFR_THAW, CMD_FOPT_ZONE }, + + { CMD_ZONE_READ, cmd_zone_node_ctl, CTL_ZONE_READ, CMD_FREQ_ZONE }, + { CMD_ZONE_BEGIN, cmd_zone_ctl, CTL_ZONE_BEGIN, CMD_FREQ_ZONE | CMD_FOPT_ZONE }, + { CMD_ZONE_COMMIT, cmd_zone_ctl, CTL_ZONE_COMMIT, CMD_FREQ_ZONE | CMD_FOPT_ZONE }, + { CMD_ZONE_ABORT, cmd_zone_ctl, CTL_ZONE_ABORT, CMD_FREQ_ZONE | CMD_FOPT_ZONE }, + { CMD_ZONE_DIFF, cmd_zone_node_ctl, CTL_ZONE_DIFF, CMD_FREQ_ZONE }, + { CMD_ZONE_GET, cmd_zone_node_ctl, CTL_ZONE_GET, CMD_FREQ_ZONE }, + { CMD_ZONE_SET, cmd_zone_node_ctl, CTL_ZONE_SET, CMD_FREQ_ZONE }, + { CMD_ZONE_UNSET, cmd_zone_node_ctl, CTL_ZONE_UNSET, CMD_FREQ_ZONE }, + { CMD_ZONE_PURGE, cmd_zone_filter_ctl, CTL_ZONE_PURGE, CMD_FREQ_ZONE | CMD_FOPT_ZONE }, + { CMD_ZONE_STATS, cmd_stats_ctl, CTL_ZONE_STATS, CMD_FREQ_ZONE }, + + { CMD_CONF_INIT, cmd_conf_init, CTL_NONE, CMD_FWRITE }, + { CMD_CONF_CHECK, cmd_conf_check, CTL_NONE, CMD_FREAD | CMD_FREQ_MOD }, + { CMD_CONF_IMPORT, cmd_conf_import, CTL_NONE, CMD_FWRITE | CMD_FOPT_MOD }, + { CMD_CONF_EXPORT, cmd_conf_export, CTL_NONE, CMD_FREAD | CMD_FOPT_MOD }, + { CMD_CONF_LIST, cmd_conf_ctl, CTL_CONF_LIST, CMD_FOPT_ITEM | CMD_FLIST_SCHEMA }, + { CMD_CONF_READ, cmd_conf_ctl, CTL_CONF_READ, CMD_FOPT_ITEM }, + { CMD_CONF_BEGIN, cmd_conf_ctl, CTL_CONF_BEGIN }, + { CMD_CONF_COMMIT, cmd_conf_ctl, CTL_CONF_COMMIT }, + { CMD_CONF_ABORT, cmd_conf_ctl, CTL_CONF_ABORT }, + { CMD_CONF_DIFF, cmd_conf_ctl, CTL_CONF_DIFF, CMD_FOPT_ITEM | CMD_FREQ_TXN }, + { CMD_CONF_GET, cmd_conf_ctl, CTL_CONF_GET, CMD_FOPT_ITEM | CMD_FREQ_TXN }, + { CMD_CONF_SET, cmd_conf_ctl, CTL_CONF_SET, CMD_FREQ_ITEM | CMD_FOPT_DATA | CMD_FREQ_TXN | CMD_FLIST_SCHEMA}, + { CMD_CONF_UNSET, cmd_conf_ctl, CTL_CONF_UNSET, CMD_FOPT_ITEM | CMD_FOPT_DATA | CMD_FREQ_TXN }, + { NULL } +}; + +static const cmd_help_t cmd_help_table[] = { + { CMD_EXIT, "", "Exit interactive mode." }, + { "", "", "" }, + { CMD_STATUS, "[<detail>]", "Check if the server is running." }, + { CMD_STOP, "", "Stop the server if running." }, + { CMD_RELOAD, "", "Reload the server configuration and modified zones." }, + { CMD_STATS, "[<module>[.<counter>]]", "Show global statistics counter(s)." }, + { "", "", "" }, + { CMD_ZONE_CHECK, "[<zone>...]", "Check if the zone can be loaded. (*)" }, + { CMD_ZONE_STATUS, "[<zone>...] [<filter>...]", "Show the zone status." }, + { CMD_ZONE_RELOAD, "[<zone>...]", "Reload a zone from a disk. (#)" }, + { CMD_ZONE_REFRESH, "[<zone>...]", "Force slave zone refresh. (#)" }, + { CMD_ZONE_NOTIFY, "[<zone>...]", "Send a NOTIFY message to all configured remotes. (#)" }, + { CMD_ZONE_RETRANSFER, "[<zone>...]", "Force slave zone retransfer (no serial check). (#)" }, + { CMD_ZONE_FLUSH, "[<zone>...] [<filter>...]", "Flush zone journal into the zone file. (#)" }, + { CMD_ZONE_BACKUP, "[<zone>...] [<filter>...] +backupdir <dir>", "Backup zone data and metadata. (#)" }, + { CMD_ZONE_RESTORE, "[<zone>...] [<filter>...] +backupdir <dir>", "Restore zone data and metadata. (#)" }, + { CMD_ZONE_SIGN, "[<zone>...]", "Re-sign the automatically signed zone. (#)" }, + { CMD_ZONE_KEYS_LOAD, "[<zone>...]", "Re-load keys from KASP database, sign the zone. (#)" }, + { CMD_ZONE_KEY_ROLL, " <zone> ksk|zsk", "Trigger immediate key rollover. (#)" }, + { CMD_ZONE_KSK_SBM, " <zone>...", "When KSK submission, confirm parent's DS presence. (#)" }, + { CMD_ZONE_FREEZE, "[<zone>...]", "Temporarily postpone automatic zone-changing events. (#)" }, + { CMD_ZONE_THAW, "[<zone>...]", "Dismiss zone freeze. (#)" }, + { CMD_ZONE_XFR_FREEZE, "[<zone>...]", "Temporarily disable outgoing AXFR/IXFR. (#)" }, + { CMD_ZONE_XFR_THAW, "[<zone>...]", "Dismiss outgoing XFR freeze. (#)" }, + { "", "", "" }, + { CMD_ZONE_READ, "<zone> [<owner> [<type>]]", "Get zone data that are currently being presented." }, + { CMD_ZONE_BEGIN, "<zone>...", "Begin a zone transaction." }, + { CMD_ZONE_COMMIT, "<zone>...", "Commit the zone transaction." }, + { CMD_ZONE_ABORT, "<zone>...", "Abort the zone transaction." }, + { CMD_ZONE_DIFF, "<zone>", "Get zone changes within the transaction." }, + { CMD_ZONE_GET, "<zone> [<owner> [<type>]]", "Get zone data within the transaction." }, + { CMD_ZONE_SET, "<zone> <owner> [<ttl>] <type> <rdata>", "Add zone record within the transaction." }, + { CMD_ZONE_UNSET, "<zone> <owner> [<type> [<rdata>]]", "Remove zone data within the transaction." }, + { CMD_ZONE_PURGE, "<zone>... [<filter>...]", "Purge zone data, zone file, journal, timers, and KASP data. (#)" }, + { CMD_ZONE_STATS, "<zone> [<module>[.<counter>]]", "Show zone statistics counter(s)."}, + { "", "", "" }, + { CMD_CONF_INIT, "", "Initialize the confdb. (*)" }, + { CMD_CONF_CHECK, "", "Check the server configuration. (*)" }, + { CMD_CONF_IMPORT, " <filename> [+nopurge]", "Import a config file into the confdb. (*)" }, + { CMD_CONF_EXPORT, "[<filename>]", "Export the confdb into a config file or stdout. (*)" }, + { CMD_CONF_LIST, "[<item>...]", "List the confdb sections or section items." }, + { CMD_CONF_READ, "[<item>...]", "Get the item from the active confdb." }, + { CMD_CONF_BEGIN, "", "Begin a writing confdb transaction." }, + { CMD_CONF_COMMIT, "", "Commit the confdb transaction." }, + { CMD_CONF_ABORT, "", "Rollback the confdb transaction." }, + { CMD_CONF_DIFF, "[<item>...]", "Get the item difference within the transaction." }, + { CMD_CONF_GET, "[<item>...]", "Get the item data within the transaction." }, + { CMD_CONF_SET, " <item> [<data>...]", "Set the item data within the transaction." }, + { CMD_CONF_UNSET, "[<item>] [<data>...]", "Unset the item data within the transaction." }, + { NULL } +}; + +void print_commands(void) +{ + printf("\nActions:\n"); + + for (const cmd_help_t *cmd = cmd_help_table; cmd->name != NULL; cmd++) { + printf(" %-18s %-38s %s\n", cmd->name, cmd->params, cmd->desc); + } + + printf("\n" + "Note:\n" + " Use @ owner to denote the zone name.\n" + " Empty or '--' <zone> parameter means all zones or all zones with a transaction.\n" + " Type <item> parameter in the form of <section>[<identifier>].<name>.\n" + " (*) indicates a local operation which requires a configuration.\n" + " (#) indicates an optionally blocking operation.\n" + " The '-b' and '-f' options can be placed right after the command name.\n"); +} diff --git a/src/utils/knotc/commands.h b/src/utils/knotc/commands.h new file mode 100644 index 0000000..22c3035 --- /dev/null +++ b/src/utils/knotc/commands.h @@ -0,0 +1,74 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/control/control.h" +#include "knot/ctl/commands.h" + +/*! \brief Command condition flags. */ +typedef enum { + CMD_FNONE = 0, /*!< Empty flag. */ + CMD_FREAD = 1 << 0, /*!< Required read access to config or confdb. */ + CMD_FWRITE = 1 << 1, /*!< Required write access to confdb. */ + CMD_FOPT_ITEM = 1 << 2, /*!< Optional item argument. */ + CMD_FREQ_ITEM = 1 << 3, /*!< Required item argument. */ + CMD_FOPT_DATA = 1 << 4, /*!< Optional item data argument. */ + CMD_FOPT_ZONE = 1 << 5, /*!< Optional zone name argument. */ + CMD_FREQ_ZONE = 1 << 6, /*!< Required zone name argument. */ + CMD_FREQ_TXN = 1 << 7, /*!< Required open confdb transaction. */ + CMD_FOPT_MOD = 1 << 8, /*!< Optional configured modules dependency. */ + CMD_FREQ_MOD = 1 << 9, /*!< Required configured modules dependency. */ + CMD_FLIST_SCHEMA = 1 << 10, /*!< List schema or possible option values. */ +} cmd_flag_t; + +struct cmd_desc; +typedef struct cmd_desc cmd_desc_t; + +/*! \brief Command callback arguments. */ +typedef struct { + const cmd_desc_t *desc; + knot_ctl_t *ctl; + int argc; + const char **argv; + char flags[4]; + bool force; + bool extended; + bool color; + bool color_force; + bool blocking; +} cmd_args_t; + +/*! \brief Command callback description. */ +struct cmd_desc { + const char *name; + int (*fcn)(cmd_args_t *); + ctl_cmd_t cmd; + cmd_flag_t flags; +}; + +/*! \brief Command description. */ +typedef struct { + const char *name; + const char *params; + const char *desc; +} cmd_help_t; + +/*! \brief Table of commands. */ +extern const cmd_desc_t cmd_table[]; + +/*! \brief Prints commands help. */ +void print_commands(void); diff --git a/src/utils/knotc/interactive.c b/src/utils/knotc/interactive.c new file mode 100644 index 0000000..a03b8d3 --- /dev/null +++ b/src/utils/knotc/interactive.c @@ -0,0 +1,450 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdio.h> +#include <histedit.h> + +#include "knot/common/log.h" +#include "utils/common/lookup.h" +#include "utils/knotc/interactive.h" +#include "utils/knotc/commands.h" +#include "contrib/openbsd/strlcat.h" +#include "contrib/string.h" + +#define PROGRAM_NAME "knotc" +#define HISTORY_FILE ".knotc_history" + +extern params_t params; + +typedef struct { + const char **args; + int count; + bool dname; +} dup_check_ctx_t; + +static void cmds_lookup(EditLine *el, const char *str, size_t str_len) +{ + lookup_t lookup; + int ret = lookup_init(&lookup); + if (ret != KNOT_EOK) { + return; + } + + // Fill the lookup with command names. + for (const cmd_desc_t *desc = cmd_table; desc->name != NULL; desc++) { + ret = lookup_insert(&lookup, desc->name, NULL); + if (ret != KNOT_EOK) { + goto cmds_lookup_finish; + } + } + + (void)lookup_complete(&lookup, str, str_len, el, true); + +cmds_lookup_finish: + lookup_deinit(&lookup); +} + +static void remove_duplicates(lookup_t *lookup, dup_check_ctx_t *check_ctx) +{ + if (check_ctx == NULL) { + return; + } + + knot_dname_txt_storage_t dname = ""; + for (int i = 0; i < check_ctx->count; i++) { + const char *arg = (check_ctx->args)[i]; + size_t len = strlen(arg); + if (check_ctx->dname && len > 1 && arg[len - 1] != '.') { + strlcat(dname, arg, sizeof(dname)); + strlcat(dname, ".", sizeof(dname)); + arg = dname; + } + (void)lookup_remove(lookup, arg); + } +} + +static void local_zones_lookup(EditLine *el, const char *str, size_t str_len, + dup_check_ctx_t *check_ctx) +{ + lookup_t lookup; + int ret = lookup_init(&lookup); + if (ret != KNOT_EOK) { + return; + } + + knot_dname_txt_storage_t buff; + + // Fill the lookup with local zone names. + for (conf_iter_t iter = conf_iter(conf(), C_ZONE); + iter.code == KNOT_EOK; conf_iter_next(conf(), &iter)) { + conf_val_t val = conf_iter_id(conf(), &iter); + char *name = knot_dname_to_str(buff, conf_dname(&val), sizeof(buff)); + + ret = lookup_insert(&lookup, name, NULL); + if (ret != KNOT_EOK) { + conf_iter_finish(conf(), &iter); + goto local_zones_lookup_finish; + } + } + + remove_duplicates(&lookup, check_ctx); + (void)lookup_complete(&lookup, str, str_len, el, true); + +local_zones_lookup_finish: + lookup_deinit(&lookup); +} + +static void list_separators(EditLine *el, const char *separators) +{ + lookup_t lookup; + if (lookup_init(&lookup) != KNOT_EOK) { + return; + } + + size_t count = strlen(separators); + for (int i = 0; i < count; i++) { + char sep[2] = { separators[i] }; + (void)lookup_insert(&lookup, sep, NULL); + } + (void)lookup_complete(&lookup, "", 0, el, false); + + lookup_deinit(&lookup); +} + +static bool rmt_lookup(EditLine *el, const char *str, size_t str_len, + const char *section, const char *item, const char *id, + dup_check_ctx_t *check_ctx, bool add_space, const char *flags, + knot_ctl_idx_t idx) +{ + const cmd_desc_t *desc = cmd_table; + while (desc->name != NULL && desc->cmd != CTL_CONF_LIST) { + desc++; + } + assert(desc->name != NULL); + + knot_ctl_data_t query = { + [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(desc->cmd), + [KNOT_CTL_IDX_SECTION] = section, + [KNOT_CTL_IDX_ITEM] = item, + [KNOT_CTL_IDX_ID] = id, + [KNOT_CTL_IDX_FLAGS] = flags + }; + + lookup_t lookup; + knot_ctl_t *ctl = NULL; + bool found = false; + + if (set_ctl(&ctl, params.socket, DEFAULT_CTL_TIMEOUT_MS, desc) != KNOT_EOK || + knot_ctl_send(ctl, KNOT_CTL_TYPE_DATA, &query) != KNOT_EOK || + knot_ctl_send(ctl, KNOT_CTL_TYPE_BLOCK, NULL) != KNOT_EOK || + lookup_init(&lookup) != KNOT_EOK) { + unset_ctl(ctl); + return found; + } + + while (true) { + knot_ctl_type_t type; + knot_ctl_data_t reply; + + if (knot_ctl_receive(ctl, &type, &reply) != KNOT_EOK) { + goto rmt_lookup_finish; + } + + if (type != KNOT_CTL_TYPE_DATA && type != KNOT_CTL_TYPE_EXTRA) { + break; + } + + const char *error = reply[KNOT_CTL_IDX_ERROR]; + if (error != NULL) { + printf("\nnotice: (%s)\n", error); + goto rmt_lookup_finish; + } + + // Insert the received name into the lookup. + if (lookup_insert(&lookup, reply[idx], NULL) != KNOT_EOK) { + goto rmt_lookup_finish; + } + } + + remove_duplicates(&lookup, check_ctx); + if (lookup_complete(&lookup, str, str_len, el, add_space) == KNOT_EOK && + str != NULL && strcmp(lookup.found.key, str) == 0) { + found = true; + } + +rmt_lookup_finish: + lookup_deinit(&lookup); + unset_ctl(ctl); + + return found; +} + +static bool id_lookup(EditLine *el, const char *str, size_t str_len, + const char *section, const cmd_desc_t *cmd_desc, + dup_check_ctx_t *ctx, bool add_space, bool zones) +{ + char flags[4] = ""; + if (zones) { + strlcat(flags, CTL_FLAG_LIST_ZONES, sizeof(flags)); + } else if (cmd_desc->flags & CMD_FREQ_TXN) { + strlcat(flags, CTL_FLAG_LIST_TXN, sizeof(flags)); + } + + return rmt_lookup(el, str, str_len, section, NULL, NULL, ctx, add_space, + flags, KNOT_CTL_IDX_ID); +} + +static void val_lookup(EditLine *el, const char *str, size_t str_len, + const char *section, const char *item, const char *id, + dup_check_ctx_t *ctx, bool list_schema) +{ + char flags[4] = CTL_FLAG_LIST_TXN; + if (list_schema) { + strlcat(flags, CTL_FLAG_LIST_SCHEMA, sizeof(flags)); + } + + (void)rmt_lookup(el, str, str_len, section, item, id, ctx, true, + flags, KNOT_CTL_IDX_DATA); +} + +static bool list_lookup(EditLine *el, const char *str, const char *section) +{ + const char *flags = CTL_FLAG_LIST_SCHEMA; + knot_ctl_idx_t idx = (section == NULL) ? KNOT_CTL_IDX_SECTION : KNOT_CTL_IDX_ITEM; + + return rmt_lookup(el, str, strlen(str), section, NULL, NULL, NULL, + section != NULL, flags, idx); +} + +static void item_lookup(EditLine *el, const char *str, const cmd_desc_t *cmd_desc) +{ + // List all sections. + if (str == NULL) { + (void)list_lookup(el, "", NULL); + return; + } + + // Check for id specification. + char *id = (strchr(str, '[')); + if (id != NULL) { + char *section = strndup(str, id - str); + + // Check for completed id specification. + char *id_stop = (strchr(id, ']')); + if (id_stop != NULL) { + // Complete the item name. + if (*(id_stop + 1) == '.') { + (void)list_lookup(el, id_stop + 2, section); + } else { + list_separators(el, "."); + } + } else { + // Complete the section id. + if (id_lookup(el, id + 1, strlen(id + 1), section, cmd_desc, + NULL, false, false)) { + list_separators(el, "]"); + } + } + + free(section); + } else { + // Check for item specification. + char *dot = (strchr(str, '.')); + if (dot != NULL) { + // Complete the item name. + char *section = strndup(str, dot - str); + (void)list_lookup(el, dot + 1, section); + free(section); + } else { + // Complete the section name. + if (list_lookup(el, str, NULL)) { + list_separators(el, "[."); + } + } + } +} + +static unsigned char complete(EditLine *el, int ch) +{ + int argc, token, pos; + const char **argv; + + const LineInfo *li = el_line(el); + Tokenizer *tok = tok_init(NULL); + + // Parse the line. + int ret = tok_line(tok, li, &argc, &argv, &token, &pos); + if (ret != 0) { + goto complete_exit; + } + + // Show possible commands. + if (argc == 0) { + print_commands(); + goto complete_exit; + } + + // Complete the command name. + if (token == 0) { + cmds_lookup(el, argv[0], pos); + goto complete_exit; + } + + // Find the command descriptor. + const cmd_desc_t *desc = cmd_table; + while (desc->name != NULL && strcmp(desc->name, argv[0]) != 0) { + desc++; + } + if (desc->name == NULL) { + goto complete_exit; + } + + // Finish if a command with no or unsupported arguments. + if (desc->flags == CMD_FNONE || desc->flags == CMD_FREAD || + desc->flags == CMD_FWRITE) { + goto complete_exit; + } + + ret = set_config(desc, ¶ms); + if (ret != KNOT_EOK) { + goto complete_exit; + } + + // Complete the zone name. + if (desc->flags & (CMD_FREQ_ZONE | CMD_FOPT_ZONE)) { + if (token > 1 && !(desc->flags & CMD_FOPT_ZONE)) { + goto complete_exit; + } + + dup_check_ctx_t ctx = { &argv[1], token - 1, true }; + if (desc->flags & CMD_FREAD) { + local_zones_lookup(el, argv[token], pos, &ctx); + } else { + id_lookup(el, argv[token], pos, "zone", desc, &ctx, true, true); + } + goto complete_exit; + // Complete the section/id/item name or item value. + } else if (desc->flags & (CMD_FOPT_ITEM | CMD_FREQ_ITEM)) { + if (token == 1) { + item_lookup(el, argv[1], desc); + } else if (desc->flags & CMD_FOPT_DATA) { + char section[YP_MAX_TXT_KEY_LEN + 1] = ""; + char item[YP_MAX_TXT_KEY_LEN + 1] = ""; + char id[KNOT_DNAME_TXT_MAXLEN + 1] = ""; + + assert(YP_MAX_TXT_KEY_LEN == 127); + assert(KNOT_DNAME_TXT_MAXLEN == 1004); + if (sscanf(argv[1], "%127[^[][%1004[^]]].%127s", section, id, item) == 3 || + sscanf(argv[1], "%127[^.].%127s", section, item) == 2) { + dup_check_ctx_t ctx = { &argv[2], token - 2 }; + val_lookup(el, argv[token], pos, section, item, id, + &ctx, desc->flags & CMD_FLIST_SCHEMA); + } + } + goto complete_exit; + } + +complete_exit: + conf_update(NULL, CONF_UPD_FNONE); + tok_reset(tok); + tok_end(tok); + + return CC_REDISPLAY; +} + +static char *prompt(EditLine *el) +{ + return PROGRAM_NAME"> "; +} + +int interactive_loop(params_t *process_params) +{ + char *hist_file = NULL; + const char *home = getenv("HOME"); + if (home != NULL) { + hist_file = sprintf_alloc("%s/%s", home, HISTORY_FILE); + } + if (hist_file == NULL) { + log_notice("failed to get home directory"); + } + + EditLine *el = el_init(PROGRAM_NAME, stdin, stdout, stderr); + if (el == NULL) { + log_error("interactive mode not available"); + free(hist_file); + return KNOT_ERROR; + } + + History *hist = history_init(); + if (hist == NULL) { + log_error("interactive mode not available"); + el_end(el); + free(hist_file); + return KNOT_ERROR; + } + + HistEvent hev = { 0 }; + history(hist, &hev, H_SETSIZE, 1000); + history(hist, &hev, H_SETUNIQUE, 1); + el_set(el, EL_HIST, history, hist); + history(hist, &hev, H_LOAD, hist_file); + + el_set(el, EL_TERMINAL, NULL); + el_set(el, EL_EDITOR, "emacs"); + el_set(el, EL_PROMPT, prompt); + el_set(el, EL_SIGNAL, 1); + el_source(el, NULL); + + // Warning: these two el_sets()'s always leak -- in libedit2 library! + // For more details see this commit's message. + el_set(el, EL_ADDFN, PROGRAM_NAME"-complete", + "Perform "PROGRAM_NAME" completion.", complete); + el_set(el, EL_BIND, "^I", PROGRAM_NAME"-complete", NULL); + + int count; + const char *line; + while ((line = el_gets(el, &count)) != NULL && count > 0) { + Tokenizer *tok = tok_init(NULL); + + // Tokenize the current line. + int argc; + const char **argv; + const LineInfo *li = el_line(el); + int ret = tok_line(tok, li, &argc, &argv, NULL, NULL); + if (ret == 0 && argc != 0) { + history(hist, &hev, H_ENTER, line); + history(hist, &hev, H_SAVE, hist_file); + + // Process the command. + ret = process_cmd(argc, argv, process_params); + } + + tok_reset(tok); + tok_end(tok); + + // Check for the exit command. + if (ret == KNOT_CTL_ESTOP) { + break; + } + } + + history_end(hist); + free(hist_file); + + el_end(el); + + return KNOT_EOK; +} diff --git a/src/utils/knotc/interactive.h b/src/utils/knotc/interactive.h new file mode 100644 index 0000000..59690c7 --- /dev/null +++ b/src/utils/knotc/interactive.h @@ -0,0 +1,26 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "utils/knotc/process.h" + +/*! + * Executes an interactive processing loop. + * + * \param[in] params Utility parameters. + */ +int interactive_loop(params_t *params); diff --git a/src/utils/knotc/main.c b/src/utils/knotc/main.c new file mode 100644 index 0000000..dad3671 --- /dev/null +++ b/src/utils/knotc/main.c @@ -0,0 +1,181 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <getopt.h> +#include <stdio.h> +#include <unistd.h> + +#include "contrib/strtonum.h" +#include "knot/common/log.h" +#include "utils/common/params.h" +#include "utils/common/signal.h" +#include "utils/knotc/commands.h" +#include "utils/knotc/interactive.h" +#include "utils/knotc/process.h" + +#define PROGRAM_NAME "knotc" +#define SPACE " " + +signal_ctx_t signal_ctx = { 0 }; // global, needed by signal handler + +static void print_help(void) +{ + printf("Usage: %s [-c | -C <path>] [options] <action>\n" + "\n" + "Config options:\n" + " -c, --config <file> "SPACE"Use a textual configuration file.\n" + " "SPACE" (default %s)\n" + " -C, --confdb <dir> "SPACE"Use a binary configuration database directory.\n" + " "SPACE" (default %s)\n" + "Options:\n" + " -m, --max-conf-size <MiB>"SPACE"Set maximum size of the configuration database (max 10000 MiB).\n" + " "SPACE" (default %d MiB)\n" + " -s, --socket <path> "SPACE"Use a control UNIX socket path.\n" + " "SPACE" (default %s)\n" + " -t, --timeout <sec> "SPACE"Use a control socket timeout (max 86400 seconds).\n" + " "SPACE" (default %u seconds)\n" + " -b, --blocking "SPACE"Zone event trigger commands wait until the event is finished.\n" + " -e, --extended "SPACE"Show extended output.\n" + " -f, --force "SPACE"Forced operation. Overrides some checks.\n" + " -x, --mono "SPACE"Don't color the output.\n" + " -X, --color "SPACE"Force output colorization.\n" + " -v, --verbose "SPACE"Enable debug output.\n" + " -h, --help "SPACE"Print the program help.\n" + " -V, --version "SPACE"Print the program version.\n", + PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR, + CONF_MAPSIZE, RUN_DIR "/knot.sock", DEFAULT_CTL_TIMEOUT_MS / 1000); + + print_commands(); +} + +params_t params = { + .max_conf_size = (size_t)CONF_MAPSIZE * 1024 * 1024, + .timeout = -1 +}; + +int main(int argc, char **argv) +{ + /* Long options. */ + struct option opts[] = { + { "config", required_argument, NULL, 'c' }, + { "confdb", required_argument, NULL, 'C' }, + { "max-conf-size", required_argument, NULL, 'm' }, + { "socket", required_argument, NULL, 's' }, + { "timeout", required_argument, NULL, 't' }, + { "blocking", no_argument, NULL, 'b' }, + { "extended", no_argument, NULL, 'e' }, + { "force", no_argument, NULL, 'f' }, + { "mono", no_argument, NULL, 'x' }, + { "color", no_argument, NULL, 'X' }, + { "verbose", no_argument, NULL, 'v' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { NULL } + }; + + /* Set the time zone. */ + tzset(); + + /* Inititalize the termination signals handler. */ + signal_init_std(); + + params.color = isatty(STDOUT_FILENO); + params.color_force = false; + + /* Parse command line arguments */ + int opt = 0; + while ((opt = getopt_long(argc, argv, "+c:C:m:s:t:befxXvhV", opts, NULL)) != -1) { + switch (opt) { + case 'c': + params.orig_config = optarg; + break; + case 'C': + params.orig_confdb = optarg; + break; + case 'm': + if (str_to_size(optarg, ¶ms.max_conf_size, 1, 10000) != KNOT_EOK) { + print_help(); + return EXIT_FAILURE; + } + /* Convert to bytes. */ + params.max_conf_size *= 1024 * 1024; + break; + case 's': + params.socket = optarg; + break; + case 't': + if (str_to_int(optarg, ¶ms.timeout, 0, 86400) != KNOT_EOK) { + print_help(); + return EXIT_FAILURE; + } + /* Convert to milliseconds. */ + params.timeout *= 1000; + break; + case 'b': + params.blocking = true; + break; + case 'e': + params.extended = true; + break; + case 'f': + params.force = true; + break; + case 'v': + params.verbose = true; + break; + case 'x': + params.color = false; + break; + case 'X': + params.color = true; + params.color_force = true; + break; + case 'h': + print_help(); + return EXIT_SUCCESS; + case 'V': + print_version(PROGRAM_NAME); + return EXIT_SUCCESS; + default: + print_help(); + return EXIT_FAILURE; + } + } + + signal_ctx.color = params.color; + + /* Set up simplified logging just to stdout/stderr. */ + log_init(); + log_levels_set(LOG_TARGET_STDOUT, LOG_SOURCE_ANY, + LOG_MASK(LOG_INFO) | LOG_MASK(LOG_NOTICE)); + log_levels_set(LOG_TARGET_STDERR, LOG_SOURCE_ANY, LOG_UPTO(LOG_WARNING)); + log_levels_set(LOG_TARGET_SYSLOG, LOG_SOURCE_ANY, 0); + log_flag_set(LOG_FLAG_NOTIMESTAMP | LOG_FLAG_NOINFO); + if (params.verbose) { + log_levels_add(LOG_TARGET_STDOUT, LOG_SOURCE_ANY, LOG_MASK(LOG_DEBUG)); + } + + int ret; + if (argc - optind < 1) { + ret = interactive_loop(¶ms); + } else { + ret = process_cmd(argc - optind, (const char **)argv + optind, ¶ms); + } + + log_close(); + + return (ret == KNOT_EOK) ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/src/utils/knotc/process.c b/src/utils/knotc/process.c new file mode 100644 index 0000000..5bfc07c --- /dev/null +++ b/src/utils/knotc/process.c @@ -0,0 +1,291 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stddef.h> +#include <sys/stat.h> + +#include "contrib/openbsd/strlcat.h" +#include "knot/conf/conf.h" +#include "knot/common/log.h" +#include "utils/knotc/commands.h" +#include "utils/knotc/process.h" + +static const cmd_desc_t *get_cmd_desc(const char *command) +{ + /* Find requested command. */ + const cmd_desc_t *desc = cmd_table; + while (desc->name != NULL) { + if (strcmp(desc->name, command) == 0) { + break; + } + desc++; + } + if (desc->name == NULL) { + log_error("invalid command '%s'", command); + return NULL; + } + + return desc; +} + +static bool get_cmd_force_flag(const char *arg) +{ + if (strcmp(arg, "-f") == 0 || strcmp(arg, "--force") == 0) { + return true; + } + return false; +} + +static bool get_cmd_blocking_flag(const char *arg) +{ + if (strcmp(arg, "-b") == 0 || strcmp(arg, "--blocking") == 0) { + return true; + } + return false; +} + +int set_config(const cmd_desc_t *desc, params_t *params) +{ + /* Reset the configuration paths (needed in the interactive mode). */ + params->config = params->orig_config; + params->confdb = params->orig_confdb; + + if (params->config != NULL && params->confdb != NULL) { + log_error("ambiguous configuration source"); + return KNOT_EINVAL; + } + + /* Mask relevant flags. */ + cmd_flag_t flags = desc->flags & (CMD_FREAD | CMD_FWRITE); + cmd_flag_t mod_flags = desc->flags & (CMD_FOPT_MOD | CMD_FREQ_MOD); + + /* Choose the optimal config source. */ + struct stat st; + bool import = false; + if (flags == CMD_FNONE && params->socket != NULL) { + /* Control operation, known socket, skip configuration. */ + return KNOT_EOK; + } else if (params->confdb != NULL) { + import = false; + } else if (flags == CMD_FWRITE) { + import = false; + params->confdb = CONF_DEFAULT_DBDIR; + } else if (params->config != NULL){ + import = true; + } else if (conf_db_exists(CONF_DEFAULT_DBDIR)) { + import = false; + params->confdb = CONF_DEFAULT_DBDIR; + } else if (stat(CONF_DEFAULT_FILE, &st) == 0) { + import = true; + params->config = CONF_DEFAULT_FILE; + } else if (flags != CMD_FNONE) { + log_error("no configuration source available"); + return KNOT_EINVAL; + } + + const char *src = import ? params->config : params->confdb; + log_debug("%s '%s'", import ? "config" : "confdb", + (src != NULL) ? src : "empty"); + + /* Prepare config flags. */ + conf_flag_t conf_flags = CONF_FNOHOSTNAME; + if (params->confdb != NULL && !(flags & CMD_FWRITE)) { + conf_flags |= CONF_FREADONLY; + } + if (import || mod_flags & CMD_FOPT_MOD) { + conf_flags |= CONF_FOPTMODULES; + } else if (mod_flags & CMD_FREQ_MOD) { + conf_flags |= CONF_FREQMODULES; + } + + /* Open confdb. */ + conf_t *new_conf = NULL; + int ret = conf_new(&new_conf, conf_schema, params->confdb, + params->max_conf_size, conf_flags); + if (ret != KNOT_EOK) { + log_error("failed to open configuration database '%s' (%s)", + (params->confdb != NULL) ? params->confdb : "", + knot_strerror(ret)); + return ret; + } + + /* Import the config file. */ + if (import) { + ret = conf_import(new_conf, params->config, IMPORT_FILE); + if (ret != KNOT_EOK) { + log_error("failed to load configuration file '%s' (%s)", + params->config, knot_strerror(ret)); + conf_free(new_conf); + return ret; + } + } + + /* Update to the new config. */ + conf_update(new_conf, CONF_UPD_FNONE); + + return KNOT_EOK; +} + +int set_ctl(knot_ctl_t **ctl, const char *socket, int timeout, const cmd_desc_t *desc) +{ + if (desc == NULL) { + *ctl = NULL; + return KNOT_EINVAL; + } + + /* Mask relevant flags. */ + cmd_flag_t flags = desc->flags & (CMD_FREAD | CMD_FWRITE); + + /* Check if control socket is required. */ + if (flags != CMD_FNONE) { + *ctl = NULL; + return KNOT_EOK; + } + + /* Get control socket path. */ + char *path = NULL; + if (socket != NULL) { + path = strdup(socket); + } else { + conf_val_t listen_val = conf_get(conf(), C_CTL, C_LISTEN); + conf_val_t rundir_val = conf_get(conf(), C_SRV, C_RUNDIR); + char *rundir = conf_abs_path(&rundir_val, NULL); + path = conf_abs_path(&listen_val, rundir); + free(rundir); + } + if (path == NULL) { + log_error("empty control socket path"); + return KNOT_EINVAL; + } + + log_debug("socket '%s'", path); + + *ctl = knot_ctl_alloc(); + if (*ctl == NULL) { + free(path); + return KNOT_ENOMEM; + } + + knot_ctl_set_timeout(*ctl, timeout); + + int ret = knot_ctl_connect(*ctl, path); + if (ret != KNOT_EOK) { + knot_ctl_free(*ctl); + *ctl = NULL; + log_error("failed to connect to socket '%s' (%s)", path, + knot_strerror(ret)); + free(path); + return ret; + } + + free(path); + + return KNOT_EOK; +} + +void unset_ctl(knot_ctl_t *ctl) +{ + if (ctl == NULL) { + return; + } + + int ret = knot_ctl_send(ctl, KNOT_CTL_TYPE_END, NULL); + if (ret != KNOT_EOK && ret != KNOT_ECONN) { + log_error("failed to finish control (%s)", knot_strerror(ret)); + } + + knot_ctl_close(ctl); + knot_ctl_free(ctl); +} + +int process_cmd(int argc, const char **argv, params_t *params) +{ + if (argc == 0) { + return KNOT_ENOTSUP; + } + + /* Check the command name. */ + const cmd_desc_t *desc = get_cmd_desc(argv[0]); + if (desc == NULL) { + return KNOT_ENOENT; + } + + /* Check for exit. */ + if (desc->fcn == NULL) { + return KNOT_CTL_ESTOP; + } + + /* Set up the configuration. */ + int ret = set_config(desc, params); + if (ret != KNOT_EOK) { + return ret; + } + + /* Prepare command parameters. */ + cmd_args_t args = { + .desc = desc, + .argc = argc - 1, + .argv = argv + 1, + .force = params->force, + .extended = params->extended, + .color = params->color, + .color_force = params->color_force, + .blocking = params->blocking + }; + + /* Check for special flags after command. */ + while (args.argc > 0) { + if (get_cmd_force_flag(args.argv[0])) { + args.force = true; + args.argc--; + args.argv++; + } else if (get_cmd_blocking_flag(args.argv[0])) { + args.blocking = true; + args.argc--; + args.argv++; + } else { + break; + } + } + + /* Prepare flags parameter. */ + if (args.force) { + strlcat(args.flags, CTL_FLAG_FORCE, sizeof(args.flags)); + } + if (args.blocking) { + strlcat(args.flags, CTL_FLAG_BLOCKING, sizeof(args.flags)); + } + + /* Set control interface if necessary. */ + int cmd_timeout = params->timeout != -1 ? params->timeout : DEFAULT_CTL_TIMEOUT_MS; + if (args.blocking && params->timeout == -1) { + cmd_timeout = 0; + } + ret = set_ctl(&args.ctl, params->socket, cmd_timeout, desc); + if (ret != KNOT_EOK) { + conf_update(NULL, CONF_UPD_FNONE); + return ret; + } + + /* Execute the command. */ + ret = desc->fcn(&args); + + /* Cleanup */ + unset_ctl(args.ctl); + conf_update(NULL, CONF_UPD_FNONE); + + return ret; +} diff --git a/src/utils/knotc/process.h b/src/utils/knotc/process.h new file mode 100644 index 0000000..3946131 --- /dev/null +++ b/src/utils/knotc/process.h @@ -0,0 +1,78 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "utils/knotc/commands.h" + +#define DEFAULT_CTL_TIMEOUT_MS (60 * 1000) + +/*! Utility command line parameters. */ +typedef struct { + const char *orig_config; + const char *orig_confdb; + const char *config; + const char *confdb; + size_t max_conf_size; + const char *socket; + bool verbose; + bool extended; + bool force; + bool blocking; + int timeout; + bool color; + bool color_force; +} params_t; + +/*! + * Prepares a proper configuration according to the specified command. + * + * \param[in] desc Utility command descriptor. + * \param[in] params Utility parameters. + * + * \return Error code, KNOT_EOK if successful. + */ +int set_config(const cmd_desc_t *desc, params_t *params); + +/*! + * Establishes a control interface if necessary. + * + * \param[in] ctl Control context. + * \param[in] socket Control socket path. + * \param[in] timeout Control socket timeout. + * \param[in] desc Utility command descriptor. + * + * \return Error code, KNOT_EOK if successful. + */ +int set_ctl(knot_ctl_t **ctl, const char *socket, int timeout, const cmd_desc_t *desc); + +/*! + * Cleans up the control context. + * + * \param[in] ctl Control context. + */ +void unset_ctl(knot_ctl_t *ctl); + +/*! + * Processes the given utility command. + * + * \param[in] argc Number of command arguments. + * \param[in] argv Command arguments. + * \param[in] params Utility parameters. + * + * \return Error code, KNOT_EOK if successful. + */ +int process_cmd(int argc, const char **argv, params_t *params); diff --git a/src/utils/knotd/main.c b/src/utils/knotd/main.c new file mode 100644 index 0000000..2355d9e --- /dev/null +++ b/src/utils/knotd/main.c @@ -0,0 +1,670 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <dirent.h> +#include <fcntl.h> +#include <poll.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <getopt.h> +#include <signal.h> +#include <sys/stat.h> +#include <urcu.h> + +#ifdef ENABLE_CAP_NG +#include <cap-ng.h> +#endif + +#include "libdnssec/crypto.h" +#include "libknot/libknot.h" +#include "contrib/strtonum.h" +#include "contrib/time.h" +#include "knot/ctl/process.h" +#include "knot/conf/conf.h" +#include "knot/conf/migration.h" +#include "knot/conf/module.h" +#include "knot/common/log.h" +#include "knot/common/process.h" +#include "knot/common/stats.h" +#include "knot/common/systemd.h" +#include "knot/server/server.h" +#include "knot/server/tcp-handler.h" + +#define PROGRAM_NAME "knotd" + +/* Signal flags. */ +static volatile bool sig_req_stop = false; +static volatile bool sig_req_reload = false; +static volatile bool sig_req_zones_reload = false; + +static int make_daemon(int nochdir, int noclose) +{ + int ret; + + switch (fork()) { + case -1: + /* Error */ + return -1; + case 0: + /* Forked */ + break; + default: + /* Exit the main process */ + _exit(0); + } + + if (setsid() == -1) { + return -1; + } + + if (!nochdir) { + ret = chdir("/"); + if (ret == -1) + return errno; + } + + if (!noclose) { + ret = close(STDIN_FILENO); + ret += close(STDOUT_FILENO); + ret += close(STDERR_FILENO); + if (ret < 0) { + return errno; + } + + int fd = open("/dev/null", O_RDWR); + if (fd == -1) { + return errno; + } + + if (dup2(fd, STDIN_FILENO) < 0) { + close(fd); + return errno; + } + if (dup2(fd, STDOUT_FILENO) < 0) { + close(fd); + return errno; + } + if (dup2(fd, STDERR_FILENO) < 0) { + close(fd); + return errno; + } + close(fd); + } + + return 0; +} + +struct signal { + int signum; + bool handle; +}; + +/*! \brief Signals used by the server. */ +static const struct signal SIGNALS[] = { + { SIGHUP, true }, /* Reload server. */ + { SIGUSR1, true }, /* Reload zones. */ + { SIGINT, true }, /* Terminate server. */ + { SIGTERM, true }, /* Terminate server. */ + { SIGALRM, false }, /* Internal thread synchronization. */ + { SIGPIPE, false }, /* Ignored. Some I/O errors. */ + { 0 } +}; + +/*! \brief Server signal handler. */ +static void handle_signal(int signum) +{ + switch (signum) { + case SIGHUP: + sig_req_reload = true; + break; + case SIGUSR1: + sig_req_zones_reload = true; + break; + case SIGINT: + case SIGTERM: + if (sig_req_stop) { + exit(EXIT_FAILURE); + } + sig_req_stop = true; + break; + default: + /* ignore */ + break; + } +} + +/*! \brief Setup signal handlers and blocking mask. */ +static void setup_signals(void) +{ + /* Block all signals. */ + static sigset_t all; + sigfillset(&all); + sigdelset(&all, SIGPROF); + sigdelset(&all, SIGQUIT); + sigdelset(&all, SIGILL); + sigdelset(&all, SIGABRT); + sigdelset(&all, SIGBUS); + sigdelset(&all, SIGFPE); + sigdelset(&all, SIGSEGV); + pthread_sigmask(SIG_SETMASK, &all, NULL); + + /* Setup handlers. */ + struct sigaction action = { .sa_handler = handle_signal }; + for (const struct signal *s = SIGNALS; s->signum > 0; s++) { + sigaction(s->signum, &action, NULL); + } +} + +/*! \brief Unblock server control signals. */ +static void enable_signals(void) +{ + sigset_t mask; + sigemptyset(&mask); + + for (const struct signal *s = SIGNALS; s->signum > 0; s++) { + if (s->handle) { + sigaddset(&mask, s->signum); + } + } + + pthread_sigmask(SIG_UNBLOCK, &mask, NULL); +} + +/*! \brief Drop POSIX 1003.1e capabilities. */ +static void drop_capabilities(void) +{ +#ifdef ENABLE_CAP_NG + /* Drop all capabilities. */ + if (capng_have_capability(CAPNG_EFFECTIVE, CAP_SETPCAP)) { + capng_clear(CAPNG_SELECT_BOTH); + + /* Apply. */ + if (capng_apply(CAPNG_SELECT_BOTH) < 0) { + log_error("failed to set process capabilities (%s)", + strerror(errno)); + } + } else { + log_info("process not allowed to set capabilities, skipping"); + } +#endif /* ENABLE_CAP_NG */ +} + +static void check_loaded(server_t *server) +{ + static bool finished = false; + if (finished) { + return; + } + + /* Avoid traversing the zonedb too frequently. */ + static struct timespec last = { 0 }; + struct timespec now = time_now(); + if (last.tv_sec == now.tv_sec) { + return; + } + last = now; + + if (!(conf()->cache.srv_dbus_event & DBUS_EVENT_RUNNING)) { + finished = true; + return; + } + + knot_zonedb_iter_t *it = knot_zonedb_iter_begin(server->zone_db); + while (!knot_zonedb_iter_finished(it)) { + zone_t *zone = (zone_t *)knot_zonedb_iter_val(it); + if (zone->contents == NULL) { + knot_zonedb_iter_free(it); + return; + } + knot_zonedb_iter_next(it); + } + knot_zonedb_iter_free(it); + + finished = true; + systemd_emit_running(true); +} + +/*! \brief Event loop listening for signals and remote commands. */ +static void event_loop(server_t *server, const char *socket, bool daemonize, + unsigned long pid) +{ + knot_ctl_t *ctl = knot_ctl_alloc(); + if (ctl == NULL) { + log_fatal("control, failed to initialize (%s)", + knot_strerror(KNOT_ENOMEM)); + return; + } + + // Set control timeout. + knot_ctl_set_timeout(ctl, conf()->cache.ctl_timeout); + + /* Get control socket configuration. */ + char *listen; + if (socket == NULL) { + conf_val_t listen_val = conf_get(conf(), C_CTL, C_LISTEN); + conf_val_t rundir_val = conf_get(conf(), C_SRV, C_RUNDIR); + char *rundir = conf_abs_path(&rundir_val, NULL); + listen = conf_abs_path(&listen_val, rundir); + free(rundir); + } else { + listen = strdup(socket); + } + if (listen == NULL) { + knot_ctl_free(ctl); + log_fatal("control, empty socket path"); + return; + } + + log_info("control, binding to '%s'", listen); + + /* Bind the control socket. */ + int ret = knot_ctl_bind(ctl, listen); + if (ret != KNOT_EOK) { + knot_ctl_free(ctl); + log_fatal("control, failed to bind socket '%s' (%s)", + listen, knot_strerror(ret)); + free(listen); + return; + } + free(listen); + + enable_signals(); + + /* Notify systemd about successful start. */ + systemd_ready_notify(); + if (daemonize) { + log_info("server started as a daemon, PID %lu", pid); + } else { + log_info("server started in the foreground, PID %lu", pid); + } + + /* Run event loop. */ + for (;;) { + /* Interrupts. */ + if (sig_req_reload && !sig_req_stop) { + sig_req_reload = false; + server_reload(server, RELOAD_FULL); + } + if (sig_req_zones_reload && !sig_req_stop) { + sig_req_zones_reload = false; + reload_t mode = server->catalog_upd_signal ? RELOAD_CATALOG : RELOAD_ZONES; + server->catalog_upd_signal = false; + server_update_zones(conf(), server, mode); + } + if (sig_req_stop) { + break; + } + + // Update control timeout. + knot_ctl_set_timeout(ctl, conf()->cache.ctl_timeout); + + if (sig_req_reload || sig_req_zones_reload) { + continue; + } + + check_loaded(server); + + ret = knot_ctl_accept(ctl); + if (ret != KNOT_EOK) { + continue; + } + + ret = ctl_process(ctl, server); + knot_ctl_close(ctl); + if (ret == KNOT_CTL_ESTOP) { + break; + } + } + + if (conf()->cache.srv_dbus_event & DBUS_EVENT_RUNNING) { + systemd_emit_running(false); + } + + /* Unbind the control socket. */ + knot_ctl_unbind(ctl); + knot_ctl_free(ctl); +} + +static void print_help(void) +{ + printf("Usage: %s [-c | -C <path>] [options]\n" + "\n" + "Config options:\n" + " -c, --config <file> Use a textual configuration file.\n" + " (default %s)\n" + " -C, --confdb <dir> Use a binary configuration database directory.\n" + " (default %s)\n" + "Options:\n" + " -m, --max-conf-size <MiB> Set maximum size of the configuration database (max 10000 MiB).\n" + " (default %d MiB)\n" + " -s, --socket <path> Use a remote control UNIX socket path.\n" + " (default %s)\n" + " -d, --daemonize=[dir] Run the server as a daemon (with new root directory).\n" + " -v, --verbose Enable debug output.\n" + " -h, --help Print the program help.\n" + " -V, --version Print the program version.\n", + PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR, + CONF_MAPSIZE, RUN_DIR "/knot.sock"); +} + +static void print_version(void) +{ + printf("%s (Knot DNS), version %s\n", PROGRAM_NAME, PACKAGE_VERSION); +} + +static int set_config(const char *confdb, const char *config, size_t max_conf_size) +{ + if (config != NULL && confdb != NULL) { + log_fatal("ambiguous configuration source"); + return KNOT_EINVAL; + } + + /* Choose the optimal config source. */ + bool import = false; + if (confdb != NULL) { + import = false; + } else if (config != NULL){ + import = true; + } else if (conf_db_exists(CONF_DEFAULT_DBDIR)) { + import = false; + confdb = CONF_DEFAULT_DBDIR; + } else { + import = true; + config = CONF_DEFAULT_FILE; + } + + /* Open confdb. */ + conf_t *new_conf = NULL; + int ret = conf_new(&new_conf, conf_schema, confdb, max_conf_size, CONF_FREQMODULES); + if (ret != KNOT_EOK) { + log_fatal("failed to open configuration database '%s' (%s)", + (confdb != NULL) ? confdb : "", knot_strerror(ret)); + return ret; + } + + /* Import the config file. */ + if (import) { + ret = conf_import(new_conf, config, IMPORT_FILE | IMPORT_REINIT_CACHE); + if (ret != KNOT_EOK) { + log_fatal("failed to load configuration file '%s' (%s)", + config, knot_strerror(ret)); + conf_free(new_conf); + return ret; + } + } + + // Migrate from old schema. + ret = conf_migrate(new_conf); + if (ret != KNOT_EOK) { + log_error("failed to migrate configuration (%s)", knot_strerror(ret)); + } + + /* Update to the new config. */ + conf_update(new_conf, CONF_UPD_FNONE); + + return KNOT_EOK; +} + +int main(int argc, char **argv) +{ + bool daemonize = false; + const char *config = NULL; + const char *confdb = NULL; + size_t max_conf_size = (size_t)CONF_MAPSIZE * 1024 * 1024; + const char *daemon_root = "/"; + char *socket = NULL; + bool verbose = false; + + /* Long options. */ + struct option opts[] = { + { "config", required_argument, NULL, 'c' }, + { "confdb", required_argument, NULL, 'C' }, + { "max-conf-size", required_argument, NULL, 'm' }, + { "socket", required_argument, NULL, 's' }, + { "daemonize", optional_argument, NULL, 'd' }, + { "verbose", no_argument, NULL, 'v' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { NULL } + }; + + /* Set the time zone. */ + tzset(); + + /* Parse command line arguments. */ + int opt = 0; + while ((opt = getopt_long(argc, argv, "c:C:m:s:dvhV", opts, NULL)) != -1) { + switch (opt) { + case 'c': + config = optarg; + break; + case 'C': + confdb = optarg; + break; + case 'm': + if (str_to_size(optarg, &max_conf_size, 1, 10000) != KNOT_EOK) { + print_help(); + return EXIT_FAILURE; + } + /* Convert to bytes. */ + max_conf_size *= 1024 * 1024; + break; + case 's': + socket = optarg; + break; + case 'd': + daemonize = true; + if (optarg) { + daemon_root = optarg; + } + break; + case 'v': + verbose = true; + break; + case 'h': + print_help(); + return EXIT_SUCCESS; + case 'V': + print_version(); + return EXIT_SUCCESS; + default: + print_help(); + return EXIT_FAILURE; + } + } + + /* Check for non-option parameters. */ + if (argc - optind > 0) { + print_help(); + return EXIT_FAILURE; + } + + /* Set file creation mask to remove all permissions for others. */ + umask(S_IROTH | S_IWOTH | S_IXOTH); + + /* Now check if we want to daemonize. */ + if (daemonize) { + if (make_daemon(1, 0) != 0) { + fprintf(stderr, "Daemonization failed, shutting down...\n"); + return EXIT_FAILURE; + } + } + + /* Setup base signal handling. */ + setup_signals(); + + /* Initialize cryptographic backend. */ + dnssec_crypto_init(); + + /* Initialize pseudorandom number generator. */ + srand(time(NULL)); + + /* Initialize logging subsystem. */ + log_init(); + if (verbose) { + log_levels_add(LOG_TARGET_STDOUT, LOG_SOURCE_ANY, LOG_MASK(LOG_DEBUG)); + } + + /* Set up the configuration */ + int ret = set_config(confdb, config, max_conf_size); + if (ret != KNOT_EOK) { + log_close(); + dnssec_crypto_cleanup(); + return EXIT_FAILURE; + } + + /* Reconfigure logging. */ + log_reconfigure(conf()); + + /* Initialize server. */ + server_t server; + ret = server_init(&server, conf()->cache.srv_bg_threads); + if (ret != KNOT_EOK) { + log_fatal("failed to initialize server (%s)", knot_strerror(ret)); + conf_free(conf()); + log_close(); + dnssec_crypto_cleanup(); + return EXIT_FAILURE; + } + + /* Reconfigure server workers, interfaces, and databases. + * @note This MUST be done before we drop privileges. */ + ret = server_reconfigure(conf(), &server); + if (ret != KNOT_EOK) { + log_fatal("failed to configure server"); + server_wait(&server); + server_deinit(&server); + conf_free(conf()); + log_close(); + dnssec_crypto_cleanup(); + return EXIT_FAILURE; + } + + /* Alter privileges. */ + int uid, gid; + if (conf_user(conf(), &uid, &gid) != KNOT_EOK || + log_update_privileges(uid, gid) != KNOT_EOK || + proc_update_privileges(uid, gid) != KNOT_EOK) { + log_fatal("failed to drop privileges"); + server_wait(&server); + server_deinit(&server); + conf_free(conf()); + log_close(); + dnssec_crypto_cleanup(); + return EXIT_FAILURE; + } + + if (conf()->cache.srv_dbus_event != DBUS_EVENT_NONE) { + ret = systemd_dbus_open(); + if (ret != KNOT_EOK) { + log_error("d-bus: failed to open system bus (%s)", + knot_strerror(ret)); + } else { + log_info("d-bus: connected to system bus"); + } + int64_t delay = conf_get_int(conf(), C_SRV, C_DBUS_INIT_DELAY); + sleep(delay); + } + + /* Drop POSIX capabilities. */ + drop_capabilities(); + + /* Activate global query modules. */ + conf_activate_modules(conf(), &server, NULL, conf()->query_modules, + &conf()->query_plan); + + /* Check and create PID file. */ + unsigned long pid = pid_check_and_create(); + if (pid == 0) { + server_wait(&server); + server_deinit(&server); + conf_free(conf()); + systemd_dbus_close(); + log_close(); + dnssec_crypto_cleanup(); + return EXIT_FAILURE; + } + + if (daemonize) { + if (chdir(daemon_root) != 0) { + log_warning("failed to change working directory to %s", + daemon_root); + } else { + log_info("changed directory to %s", daemon_root); + } + } + + /* Now we're going multithreaded. */ + rcu_register_thread(); + + /* Populate zone database. */ + log_info("loading %zu zones", conf_id_count(conf(), C_ZONE)); + server_update_zones(conf(), &server, RELOAD_ZONES); + + /* Check number of loaded zones. */ + if (knot_zonedb_size(server.zone_db) == 0) { + log_warning("no zones loaded"); + } + + stats_reconfigure(conf(), &server); + + /* Start it up. */ + log_info("starting server"); + conf_val_t async_val = conf_get(conf(), C_SRV, C_ASYNC_START); + ret = server_start(&server, conf_bool(&async_val)); + if (ret != KNOT_EOK) { + log_fatal("failed to start server (%s)", knot_strerror(ret)); + server_wait(&server); + stats_deinit(); + server_deinit(&server); + rcu_unregister_thread(); + pid_cleanup(); + conf_free(conf()); + systemd_dbus_close(); + log_close(); + dnssec_crypto_cleanup(); + return EXIT_FAILURE; + } + + /* Start the event loop. */ + event_loop(&server, socket, daemonize, pid); + + /* Teardown server. */ + server_stop(&server); + server_wait(&server); + stats_deinit(); + + /* Cleanup PID file. */ + pid_cleanup(); + + /* Free server and configuration. */ + server_deinit(&server); + conf_free(conf()); + + /* Unhook from RCU. */ + rcu_unregister_thread(); + + systemd_dbus_close(); + + log_info("shutting down"); + log_close(); + + dnssec_crypto_cleanup(); + + return EXIT_SUCCESS; +} diff --git a/src/utils/knsec3hash/knsec3hash.c b/src/utils/knsec3hash/knsec3hash.c new file mode 100644 index 0000000..a7bac97 --- /dev/null +++ b/src/utils/knsec3hash/knsec3hash.c @@ -0,0 +1,187 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <getopt.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> + +#include "contrib/base32hex.h" +#include "contrib/string.h" +#include "contrib/strtonum.h" +#include "libdnssec/error.h" +#include "libdnssec/nsec.h" +#include "libknot/libknot.h" +#include "utils/common/msg.h" +#include "utils/common/params.h" + +#define PROGRAM_NAME "knsec3hash" + +/*! + * \brief Print program help (and example). + */ +static void print_help(void) +{ + printf("Usage: " PROGRAM_NAME " <salt> <algorithm> <iterations> <domain-name>\n"); + printf("Example: " PROGRAM_NAME " c01dcafe 1 10 knot-dns.cz\n"); + printf("Alternative usage: "PROGRAM_NAME " <algorithm> <flags> <iterations> <salt> <domain-name>\n"); + printf("Example: " PROGRAM_NAME " 1 0 10 c01dcafe knot-dns.cz\n"); +} + +/*! + * \brief Parse NSEC3 salt. + */ +static int str_to_salt(const char *str, dnssec_binary_t *salt) +{ + if (strcmp(str, "-") == 0) { + salt->size = 0; + return DNSSEC_EOK; + } else { + salt->data = hex_to_bin(str, &salt->size); + return (salt->data != NULL ? DNSSEC_EOK : DNSSEC_EINVAL); + } +} + +/*! + * \brief Parse NSEC3 parameters and fill structure with NSEC3 parameters. + */ +static bool parse_nsec3_params(dnssec_nsec3_params_t *params, const char *salt_str, + const char *algorithm_str, const char *iterations_str) +{ + uint8_t algorithm = 0; + int r = str_to_u8(algorithm_str, &algorithm); + if (r != KNOT_EOK) { + ERR2("invalid algorithm number"); + return false; + } + + uint16_t iterations = 0; + r = str_to_u16(iterations_str, &iterations); + if (r != KNOT_EOK) { + ERR2("invalid iteration count"); + return false; + } + + dnssec_binary_t salt = { 0 }; + r = str_to_salt(salt_str, &salt); + if (r != DNSSEC_EOK) { + ERR2("invalid salt (%s)", knot_strerror(r)); + return false; + } + + if (salt.size > UINT8_MAX) { + ERR2("invalid salt, maximum length is %d bytes", UINT8_MAX); + dnssec_binary_free(&salt); + return false; + } + + params->algorithm = algorithm; + params->iterations = iterations; + params->salt = salt; + params->flags = 0; + + return true; +} + +/*! + * \brief Entry point of 'knsec3hash'. + */ +int main(int argc, char *argv[]) +{ + struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { NULL } + }; + + int opt = 0; + while ((opt = getopt_long(argc, argv, "hV", options, NULL)) != -1) { + switch(opt) { + case 'h': + print_help(); + return EXIT_SUCCESS; + case 'V': + print_version(PROGRAM_NAME); + return EXIT_SUCCESS; + default: + print_help(); + return EXIT_FAILURE; + } + } + + bool new_params = false; + if (argc == 6) { + // knsec3hash <algorithm> <flags> <iterations> <salt> <domain> + new_params = true; + } else if (argc != 5) { + // knsec3hash <salt> <algorithm> <iterations> <domain> + print_help(); + return EXIT_FAILURE; + } + + int exit_code = EXIT_FAILURE; + dnssec_nsec3_params_t nsec3_params = { 0 }; + + dnssec_binary_t dname = { 0 }; + dnssec_binary_t digest = { 0 }; + dnssec_binary_t digest_print = { 0 }; + + if (new_params) { + if (!parse_nsec3_params(&nsec3_params, argv[4], argv[1], argv[3])) { + goto fail; + } + } else { + if (!parse_nsec3_params(&nsec3_params, argv[1], argv[2], argv[3])) { + goto fail; + } + } + + dname.data = knot_dname_from_str_alloc(argv[new_params ? 5 : 4]); + if (dname.data == NULL) { + ERR2("cannot parse domain name"); + goto fail; + } + knot_dname_to_lower(dname.data); + dname.size = knot_dname_size(dname.data); + + int r = dnssec_nsec3_hash(&dname, &nsec3_params, &digest); + if (r != DNSSEC_EOK) { + ERR2("cannot compute NSEC3 hash (%s)", knot_strerror(r)); + goto fail; + } + + r = knot_base32hex_encode_alloc(digest.data, digest.size, &digest_print.data); + if (r < 0) { + ERR2("cannot encode computed hash (%s)", knot_strerror(r)); + goto fail; + } + digest_print.size = r; + + exit_code = EXIT_SUCCESS; + + printf("%.*s (salt=%s, hash=%d, iterations=%d)\n", (int)digest_print.size, + digest_print.data, argv[1], nsec3_params.algorithm, + nsec3_params.iterations); + +fail: + dnssec_nsec3_params_free(&nsec3_params); + dnssec_binary_free(&dname); + dnssec_binary_free(&digest); + dnssec_binary_free(&digest_print); + + return exit_code; +} diff --git a/src/utils/knsupdate/knsupdate_exec.c b/src/utils/knsupdate/knsupdate_exec.c new file mode 100644 index 0000000..e201711 --- /dev/null +++ b/src/utils/knsupdate/knsupdate_exec.c @@ -0,0 +1,1059 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/socket.h> +#include <unistd.h> + +#include "libdnssec/random.h" +#include "utils/knsupdate/knsupdate_exec.h" +#include "utils/knsupdate/knsupdate_interactive.h" +#include "utils/common/exec.h" +#include "utils/common/msg.h" +#include "utils/common/netio.h" +#include "utils/common/params.h" +#include "utils/common/sign.h" +#include "utils/common/token.h" +#include "libknot/libknot.h" +#include "contrib/ctype.h" +#include "contrib/getline.h" +#include "contrib/string.h" +#include "contrib/strtonum.h" +#include "contrib/openbsd/strlcpy.h" + +/* Declarations of cmd parse functions. */ +typedef int (*cmd_handle_f)(const char *lp, knsupdate_params_t *params); +int cmd_add(const char* lp, knsupdate_params_t *params); +int cmd_answer(const char* lp, knsupdate_params_t *params); +int cmd_class(const char* lp, knsupdate_params_t *params); +int cmd_debug(const char* lp, knsupdate_params_t *params); +int cmd_del(const char* lp, knsupdate_params_t *params); +int cmd_gsstsig(const char* lp, knsupdate_params_t *params); +int cmd_key(const char* lp, knsupdate_params_t *params); +int cmd_local(const char* lp, knsupdate_params_t *params); +int cmd_nxdomain(const char *lp, knsupdate_params_t *params); +int cmd_nxrrset(const char *lp, knsupdate_params_t *params); +int cmd_oldgsstsig(const char* lp, knsupdate_params_t *params); +int cmd_origin(const char* lp, knsupdate_params_t *params); +int cmd_prereq(const char* lp, knsupdate_params_t *params); +int cmd_exit(const char* lp, knsupdate_params_t *params); +int cmd_realm(const char* lp, knsupdate_params_t *params); +int cmd_send(const char* lp, knsupdate_params_t *params); +int cmd_server(const char* lp, knsupdate_params_t *params); +int cmd_show(const char* lp, knsupdate_params_t *params); +int cmd_ttl(const char* lp, knsupdate_params_t *params); +int cmd_update(const char* lp, knsupdate_params_t *params); +int cmd_yxdomain(const char *lp, knsupdate_params_t *params); +int cmd_yxrrset(const char *lp, knsupdate_params_t *params); +int cmd_zone(const char* lp, knsupdate_params_t *params); + +/* Sorted list of commands. + * This way we could identify command byte-per-byte and + * cancel early if the next is lexicographically greater. + */ +const char* knsupdate_cmd_array[] = { + "\x3" "add", + "\x6" "answer", + "\x5" "class", /* {classname} */ + "\x5" "debug", + "\x3" "del", + "\x6" "delete", + "\x4" "exit", + "\x7" "gsstsig", + "\x3" "key", /* {[alg:]name} {secret} */ + "\x5" "local", /* {address} [port] */ + "\x8" "nxdomain", + "\x7" "nxrrset", + "\xa" "oldgsstsig", + "\x6" "origin", /* {name} */ + "\x6" "prereq", /* (nx|yx)(domain|rrset) {domain-name} ... */ + "\x4" "quit", + "\x5" "realm", /* {[realm_name]} */ + "\x4" "send", + "\x6" "server", /* {servername} [port] */ + "\x4" "show", + "\x3" "ttl", /* {seconds} */ + "\x6" "update", /* (add|delete) {domain-name} ... */ + "\x8" "yxdomain", + "\x7" "yxrrset", + "\x4" "zone", /* {zonename} */ + NULL +}; + +cmd_handle_f cmd_handle[] = { + cmd_add, + cmd_answer, + cmd_class, + cmd_debug, + cmd_del, + cmd_del, /* delete/del synonyms */ + cmd_exit, + cmd_gsstsig, + cmd_key, + cmd_local, + cmd_nxdomain, + cmd_nxrrset, + cmd_oldgsstsig, + cmd_origin, + cmd_prereq, + cmd_exit, /* exit/quit synonyms */ + cmd_realm, + cmd_send, + cmd_server, + cmd_show, + cmd_ttl, + cmd_update, + cmd_yxdomain, + cmd_yxrrset, + cmd_zone, +}; + +/* {prereq} command table. */ +const char* pq_array[] = { + "\x8" "nxdomain", + "\x7" "nxrrset", + "\x8" "yxdomain", + "\x7" "yxrrset", + NULL +}; + +enum { + PQ_NXDOMAIN = 0, + PQ_NXRRSET, + PQ_YXDOMAIN, + PQ_YXRRSET +}; + +/* RR parser flags */ +enum { + PARSE_NODEFAULT = 1 << 0, /* Do not fill defaults. */ + PARSE_NAMEONLY = 1 << 1, /* Parse only name. */ + PARSE_NOTTL = 1 << 2 /* Ignore TTL item. */ +}; + +static bool dname_isvalid(const char *lp) +{ + knot_dname_t *dn = knot_dname_from_str_alloc(lp); + if (dn == NULL) { + return false; + } + knot_dname_free(dn, NULL); + return true; +} + +/* This is probably redundant, but should be a bit faster so let's keep it. */ +static int parse_full_rr(zs_scanner_t *s, const char* lp) +{ + if (zs_set_input_string(s, lp, strlen(lp)) != 0 || + zs_parse_all(s) != 0) { + ERR("invalid record (%s)", zs_strerror(s->error.code)); + return KNOT_EPARSEFAIL; + } + + /* Class must not differ from specified. */ + if (s->r_class != s->default_class) { + char cls_s[16] = ""; + knot_rrclass_to_string(s->default_class, cls_s, sizeof(cls_s)); + ERR("class mismatch '%s'", cls_s); + return KNOT_EPARSEFAIL; + } + + return KNOT_EOK; +} + +static int parse_partial_rr(zs_scanner_t *s, const char *lp, unsigned flags) +{ + knot_dname_txt_storage_t owner_str = { 0 }; + + /* Extract owner. */ + size_t len = strcspn(lp, SEP_CHARS); + memcpy(owner_str, lp, len); + + /* Check if ORIGIN (@) or FQDN. */ + bool origin = false; + bool fqdn = true; + if (len == 1 && owner_str[0] == '@') { + origin = true; + fqdn = false; + } else if (owner_str[len - 1] != '.') { + fqdn = false; + } + + /* Convert textual owner to dname. */ + if (!origin) { + knot_dname_storage_t owner; + if (knot_dname_from_str(owner, owner_str, sizeof(owner)) == NULL) { + return KNOT_EINVAL; + } + + s->r_owner_length = knot_dname_size(owner); + memcpy(s->r_owner, owner, s->r_owner_length); + } else { + s->r_owner_length = 0; + } + + /* Append origin if not FQDN. */ + if (!fqdn) { + if (!origin) { + s->r_owner_length--; + } + memcpy(s->r_owner + s->r_owner_length, s->zone_origin, + s->zone_origin_length); + s->r_owner_length += s->zone_origin_length; + } + + lp = tok_skipspace(lp + len); + + /* Initialize */ + s->r_type = KNOT_RRTYPE_ANY; + s->r_class = s->default_class; + s->r_data_length = 0; + if (flags & PARSE_NODEFAULT) { + s->r_ttl = 0; + } else { + s->r_ttl = s->default_ttl; + } + + /* Parse only name? */ + if (flags & PARSE_NAMEONLY) { + if (*lp != '\0') { + WARN("ignoring input data '%s'", lp); + } + return KNOT_EOK; + } + + /* Now there could be [ttl] [class] [type [data...]]. */ + char *np = NULL; + long ttl = strtol(lp, &np, 10); + if (ttl >= 0 && np && (*np == '\0' || is_space(*np))) { + DBG("%s: parsed ttl=%lu", __func__, ttl); + if (flags & PARSE_NOTTL) { + WARN("ignoring TTL value '%ld'", ttl); + } else { + s->r_ttl = ttl; + } + lp = tok_skipspace(np); + } + + uint16_t num; + char *buff = NULL; + char *cls = NULL; + char *type = NULL; + + /* Try to find class. */ + len = strcspn(lp, SEP_CHARS); + if (len > 0) { + buff = strndup(lp, len); + } + + if (knot_rrclass_from_string(buff, &num) == 0) { + /* Class must not differ from specified. */ + if (num != s->default_class) { + ERR("class mismatch '%s'", buff); + free(buff); + return KNOT_EPARSEFAIL; + } + cls = buff; + buff = NULL; + s->r_class = num; + DBG("%s: parsed class=%u '%s'", __func__, s->r_class, cls); + lp = tok_skipspace(lp + len); + } + + /* Try to parser type. */ + if (cls != NULL) { + len = strcspn(lp, SEP_CHARS); + if (len > 0) { + buff = strndup(lp, len); + } + } + if (knot_rrtype_from_string(buff, &num) == 0) { + type = buff; + buff = NULL; + s->r_type = num; + DBG("%s: parsed type=%u '%s'", __func__, s->r_type, type); + lp = tok_skipspace(lp + len); + } + + free(buff); + + /* Remainder */ + if (*lp == '\0') { + free(cls); + free(type); + return KNOT_EOK; + } + + /* Need to parse rdata, synthetize input. */ + char *rr = sprintf_alloc(" %u IN %s %s\n", s->r_ttl, type, lp); + free(cls); + free(type); + if (rr == NULL) { + return KNOT_ENOMEM; + } + if (zs_set_input_string(s, rr, strlen(rr)) != 0 || + zs_parse_all(s) != 0) { + ERR("invalid rdata (%s)", zs_strerror(s->error.code)); + return KNOT_EPARSEFAIL; + } + free(rr); + + return KNOT_EOK; +} + +static srv_info_t *parse_host(const char *lp, const char* default_port) +{ + /* Extract server address. */ + srv_info_t *srv = NULL; + size_t len = strcspn(lp, SEP_CHARS); + char *addr = strndup(lp, len); + if (!addr) return NULL; + DBG("%s: parsed addr: %s", __func__, addr); + + /* Store port/service if present. */ + lp = tok_skipspace(lp + len); + if (*lp == '\0') { + srv = srv_info_create(addr, default_port); + free(addr); + return srv; + } + + len = strcspn(lp, SEP_CHARS); + char *port = strndup(lp, len); + if (!port) { + free(addr); + return NULL; + } + DBG("%s: parsed port: %s", __func__, port); + + /* Create server struct. */ + srv = srv_info_create(addr, port); + free(addr); + free(port); + return srv; +} + +/* Append parsed RRSet to list. */ +static int rr_list_append(zs_scanner_t *s, list_t *target_list, knot_mm_t *mm) +{ + knot_rrset_t *rr = knot_rrset_new(s->r_owner, s->r_type, s->r_class, + s->r_ttl, NULL); + if (!rr) { + DBG("%s: failed to create rrset", __func__); + return KNOT_ENOMEM; + } + + /* Create RDATA. */ + int ret = knot_rrset_add_rdata(rr, s->r_data, s->r_data_length, NULL); + if (ret != KNOT_EOK) { + DBG("%s: failed to set rrset from wire (%s)", + __func__, knot_strerror(ret)); + knot_rrset_free(rr, NULL); + return ret; + } + + if (ptrlist_add(target_list, rr, mm) == NULL) { + knot_rrset_free(rr, NULL); + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +/*! \brief Write RRSet list to packet section. */ +static int rr_list_to_packet(knot_pkt_t *dst, list_t *list) +{ + assert(dst != NULL); + assert(list != NULL); + + ptrnode_t *node; + WALK_LIST(node, *list) { + int ret = knot_pkt_put(dst, KNOT_COMPR_HINT_NONE, + (knot_rrset_t *)node->d, 0); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +/*! \brief Build UPDATE query. */ +static int build_query(knsupdate_params_t *params) +{ + /* Clear old query. */ + knot_pkt_t *query = params->query; + knot_pkt_clear(query); + + /* Write question. */ + knot_wire_set_id(query->wire, dnssec_random_uint16_t()); + knot_wire_set_opcode(query->wire, KNOT_OPCODE_UPDATE); + knot_dname_t *qname = knot_dname_from_str_alloc(params->zone); + int ret = knot_pkt_put_question(query, qname, params->class_num, + KNOT_RRTYPE_SOA); + knot_dname_free(qname, NULL); + if (ret != KNOT_EOK) { + return ret; + } + + /* Now, PREREQ => ANSWER section. */ + ret = knot_pkt_begin(query, KNOT_ANSWER); + if (ret != KNOT_EOK) { + return ret; + } + + /* Write PREREQ. */ + ret = rr_list_to_packet(query, ¶ms->prereq_list); + if (ret != KNOT_EOK) { + return ret; + } + + /* Now, UPDATE data => AUTHORITY section. */ + ret = knot_pkt_begin(query, KNOT_AUTHORITY); + if (ret != KNOT_EOK) { + return ret; + } + + /* Write UPDATE data. */ + return rr_list_to_packet(query, ¶ms->update_list); +} + +static int pkt_sendrecv(knsupdate_params_t *params) +{ + net_t net; + int ret; + + ret = net_init(params->srcif, + params->server, + get_iptype(params->ip, params->server), + get_socktype(params->protocol, KNOT_RRTYPE_SOA), + params->wait, + NET_FLAGS_NONE, + NULL, + NULL, + &net); + if (ret != KNOT_EOK) { + return -1; + } + + ret = net_connect(&net); + DBG("%s: send_msg = %d", __func__, net.sockfd); + if (ret != KNOT_EOK) { + net_clean(&net); + return -1; + } + + ret = net_send(&net, params->query->wire, params->query->size); + if (ret != KNOT_EOK) { + net_close(&net); + net_clean(&net); + return -1; + } + + /* Clear response buffer. */ + knot_pkt_clear(params->answer); + + /* Wait for reception. */ + int rb = net_receive(&net, params->answer->wire, params->answer->max_size); + DBG("%s: receive_msg = %d", __func__, rb); + if (rb <= 0) { + net_close(&net); + net_clean(&net); + return -1; + } else { + params->answer->size = rb; + } + + net_close(&net); + net_clean(&net); + + return rb; +} + +int knsupdate_process_line(const char *line, knsupdate_params_t *params) +{ + /* Check for empty line or comment. */ + if (line[0] == '\0' || line[0] == ';') { + return KNOT_EOK; + } + + int ret = tok_find(line, knsupdate_cmd_array); + if (ret < 0) { + return ret; /* Syntax error - do nothing. */ + } + + const char *cmd = knsupdate_cmd_array[ret]; + const char *val = tok_skipspace(line + TOK_L(cmd)); + params->parser.error.counter = 0; /* Reset possible previous error. */ + ret = cmd_handle[ret](val, params); + if (ret != KNOT_EOK) { + DBG("operation '%s' failed (%s) on line '%s'", + TOK_S(cmd), knot_strerror(ret), line); + } + + return ret; +} + +static bool is_terminal(FILE *file) +{ + int fd = fileno(file); + assert(fd >= 0); + return isatty(fd); +} + +static int process_lines(knsupdate_params_t *params, FILE *input) +{ + char *buf = NULL; + size_t buflen = 0; + if(is_terminal(input)) { + return interactive_loop(params); + } + int ret = KNOT_EOK; + + /* Process lines. */ + while (!params->stop && knot_getline(&buf, &buflen, input) != -1) { + /* Remove leading and trailing white space. */ + char *line = strstrip(buf); + ret = knsupdate_process_line(line, params); + memset(line, 0, strlen(line)); + free(line); + if (ret != KNOT_EOK) { + break; + } + } + + if (buf != NULL) { + memset(buf, 0, buflen); + free(buf); + } + + return ret; +} + +int knsupdate_exec(knsupdate_params_t *params) +{ + if (!params) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + + /* If no file specified, enter the interactive mode. */ + if (EMPTY_LIST(params->qfiles)) { + return process_lines(params, stdin); + } + + /* Read from each specified file. */ + ptrnode_t *n; + WALK_LIST(n, params->qfiles) { + const char *filename = (const char*)n->d; + if (strcmp(filename, "-") == 0) { + ret = process_lines(params, stdin); + if (ret != KNOT_EOK) { + break; + } + continue; + } + + FILE *fp = fopen(filename, "r"); + if (!fp) { + ERR("failed to open '%s' (%s)", + filename, strerror(errno)); + ret = KNOT_EFILE; + break; + } + ret = process_lines(params, fp); + fclose(fp); + if (ret != KNOT_EOK) { + break; + } + } + + return ret; +} + +int cmd_update(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + /* update is optional token, next add|del|delete */ + int bp = tok_find(lp, knsupdate_cmd_array); + if (bp < 0) return bp; /* Syntax error. */ + + /* allow only specific tokens */ + cmd_handle_f *h = cmd_handle; + if (h[bp] != cmd_add && h[bp] != cmd_del) { + ERR("unexpected token '%s' after 'update', allowed: '%s'", + lp, "{add|del|delete}"); + return KNOT_EPARSEFAIL; + } + + return h[bp](tok_skipspace(lp + TOK_L(knsupdate_cmd_array[bp])), params); +} + +int cmd_add(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + if (parse_full_rr(¶ms->parser, lp) != KNOT_EOK) { + return KNOT_EPARSEFAIL; + } + + return rr_list_append(¶ms->parser, ¶ms->update_list, ¶ms->mm); +} + +int cmd_del(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + zs_scanner_t *rrp = ¶ms->parser; + int ret = parse_partial_rr(rrp, lp, PARSE_NODEFAULT); + if (ret != KNOT_EOK) { + return ret; + } + + /* Check owner name. */ + if (rrp->r_owner_length == 0) { + ERR("failed to parse owner name '%s'", lp); + return KNOT_EPARSEFAIL; + } + + rrp->r_ttl = 0; /* Set TTL = 0 when deleting. */ + + /* When deleting whole RRSet, use ANY class */ + if (rrp->r_data_length == 0) { + rrp->r_class = KNOT_CLASS_ANY; + } else { + rrp->r_class = KNOT_CLASS_NONE; + } + + return rr_list_append(rrp, ¶ms->update_list, ¶ms->mm); +} + +int cmd_class(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + uint16_t cls; + + if (knot_rrclass_from_string(lp, &cls) != 0) { + ERR("failed to parse class '%s'", lp); + return KNOT_EPARSEFAIL; + } + + params->class_num = cls; + params->parser.default_class = params->class_num; + + return KNOT_EOK; +} + +int cmd_ttl(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + uint32_t ttl = 0; + + if (str_to_u32(lp, &ttl) != KNOT_EOK) { + ERR("failed to parse ttl '%s'", lp); + return KNOT_EPARSEFAIL; + } + + return knsupdate_set_ttl(params, ttl); +} + +int cmd_debug(const char* lp, _unused_ knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + msg_enable_debug(1); + + return KNOT_EOK; +} + +int cmd_nxdomain(const char *lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + zs_scanner_t *s = ¶ms->parser; + int ret = parse_partial_rr(s, lp, PARSE_NODEFAULT | PARSE_NAMEONLY); + if (ret != KNOT_EOK) { + return ret; + } + + s->r_ttl = 0; + s->r_class = KNOT_CLASS_NONE; + + return rr_list_append(s, ¶ms->prereq_list, ¶ms->mm); +} + +int cmd_yxdomain(const char *lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + zs_scanner_t *s = ¶ms->parser; + int ret = parse_partial_rr(s, lp, PARSE_NODEFAULT | PARSE_NAMEONLY); + if (ret != KNOT_EOK) { + return ret; + } + + s->r_ttl = 0; + s->r_class = KNOT_CLASS_ANY; + + return rr_list_append(s, ¶ms->prereq_list, ¶ms->mm); +} + +int cmd_nxrrset(const char *lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + zs_scanner_t *s = ¶ms->parser; + int ret = parse_partial_rr(s, lp, PARSE_NOTTL); + if (ret != KNOT_EOK) { + return ret; + } + + /* Check owner name. */ + if (s->r_owner_length == 0) { + ERR("failed to parse prereq owner name '%s'", lp); + return KNOT_EPARSEFAIL; + } + + s->r_ttl = 0; + s->r_class = KNOT_CLASS_NONE; + + return rr_list_append(s, ¶ms->prereq_list, ¶ms->mm); +} + +int cmd_yxrrset(const char *lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + zs_scanner_t *s = ¶ms->parser; + int ret = parse_partial_rr(s, lp, PARSE_NOTTL); + if (ret != KNOT_EOK) { + return ret; + } + + /* Check owner name. */ + if (s->r_owner_length == 0) { + ERR("failed to parse prereq owner name '%s'", lp); + return KNOT_EPARSEFAIL; + } + + s->r_ttl = 0; + if (s->r_data_length > 0) { + s->r_class = KNOT_CLASS_IN; + } else { + s->r_class = KNOT_CLASS_ANY; + } + + return rr_list_append(s, ¶ms->prereq_list, ¶ms->mm); +} + +int cmd_prereq(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + /* Scan prereq specifier ([ny]xrrset|[ny]xdomain) */ + int prereq_type = tok_find(lp, pq_array); + if (prereq_type < 0) { + return prereq_type; + } + + const char *tok = pq_array[prereq_type]; + DBG("%s: type %s", __func__, TOK_S(tok)); + lp = tok_skipspace(lp + TOK_L(tok)); + if (strlen(lp) == 0) { + ERR("missing prerequisite owner name"); + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + switch(prereq_type) { + case PQ_NXDOMAIN: + ret = cmd_nxdomain(lp, params); + break; + case PQ_YXDOMAIN: + ret = cmd_yxdomain(lp, params); + break; + case PQ_NXRRSET: + ret = cmd_nxrrset(lp, params); + break; + case PQ_YXRRSET: + ret = cmd_yxrrset(lp, params); + break; + default: + ret = KNOT_ERROR; + } + + return ret; +} + +int cmd_exit(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + params->stop = true; + + return KNOT_EOK; +} + +int cmd_send(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + DBG("sending packet"); + + if (params->zone == NULL) { + ERR("no zone specified"); + return KNOT_EINVAL; + } + + /* Build query packet. */ + int ret = build_query(params); + if (ret != KNOT_EOK) { + ERR("failed to build UPDATE message (%s)", knot_strerror(ret)); + return ret; + } + + /* Sign if key specified. */ + sign_context_t sign_ctx = { 0 }; + if (params->tsig_key.name) { + ret = sign_context_init_tsig(&sign_ctx, ¶ms->tsig_key); + if (ret != KNOT_EOK) { + ERR("failed to initialize signing context (%s)", + knot_strerror(ret)); + return ret; + } + + ret = sign_packet(params->query, &sign_ctx); + if (ret != KNOT_EOK) { + ERR("failed to sign UPDATE message (%s)", + knot_strerror(ret)); + sign_context_deinit(&sign_ctx); + return ret; + } + } + + int rb = 0; + /* Send/recv message (1 try + N retries). */ + int tries = 1 + params->retries; + for (; tries > 0; --tries) { + rb = pkt_sendrecv(params); + if (rb > 0) { + break; + } + } + + /* Check Send/recv result. */ + if (rb <= 0) { + sign_context_deinit(&sign_ctx); + return KNOT_ECONNREFUSED; + } + + /* Parse response. */ + ret = knot_pkt_parse(params->answer, KNOT_PF_NOCANON); + if (ret != KNOT_EOK) { + ERR("failed to parse response (%s)", knot_strerror(ret)); + sign_context_deinit(&sign_ctx); + return ret; + } + + /* Check signature if expected. */ + if (params->tsig_key.name) { + ret = verify_packet(params->answer, &sign_ctx); + sign_context_deinit(&sign_ctx); + if (ret != KNOT_EOK) { + print_packet(params->answer, NULL, 0, -1, 0, true, + ¶ms->style); + ERR("reply verification (%s)", knot_strerror(ret)); + return ret; + } + } + + /* Free RRSet lists. */ + knsupdate_reset(params); + + /* Check return code. */ + if (knot_pkt_ext_rcode(params->answer) != KNOT_RCODE_NOERROR) { + print_packet(params->answer, NULL, 0, -1, 0, true, ¶ms->style); + ERR("update failed with error '%s'", + knot_pkt_ext_rcode_name(params->answer)); + ret = KNOT_ERROR; + } else { + DBG("update success"); + } + + return ret; +} + +int cmd_zone(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + /* Check zone name. */ + if (!dname_isvalid(lp)) { + ERR("failed to parse zone '%s'", lp); + return KNOT_EPARSEFAIL; + } + + free(params->zone); + params->zone = strdup(lp); + + return KNOT_EOK; +} + +int cmd_server(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + /* Parse host. */ + srv_info_t *srv = parse_host(lp, params->server->service); + if (!srv) { + ERR("failed to parse server '%s'", lp); + return KNOT_ENOMEM; + } + + srv_info_free(params->server); + params->server = srv; + + return KNOT_EOK; +} + +int cmd_local(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + /* Parse host. */ + srv_info_t *srv = parse_host(lp, "0"); + if (!srv) { + ERR("failed to parse local '%s'", lp); + return KNOT_ENOMEM; + } + + srv_info_free(params->srcif); + params->srcif = srv; + + return KNOT_EOK; +} + +int cmd_show(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + if (!params->query) { + return KNOT_EOK; + } + + printf("Update query:\n"); + build_query(params); + print_packet(params->query, NULL, 0, -1, 0, false, ¶ms->style); + printf("\n"); + + return KNOT_EOK; +} + +int cmd_answer(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + if (!params->answer) { + return KNOT_EOK; + } + + printf("Answer:\n"); + print_packet(params->answer, NULL, 0, -1, 0, true, ¶ms->style); + + return KNOT_EOK; +} + +int cmd_key(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + /* Convert to default format. */ + char *kstr = strdup(lp); + if (!kstr) { + return KNOT_ENOMEM; + } + + int ret = KNOT_EOK; + + /* Search for the name secret separation. Allow also alg:name:key form. */ + char *sep = strchr(kstr, ' '); + if (sep != NULL) { + /* Replace ' ' with ':'. More spaces are ignored in base64. */ + *sep = ':'; + } + + /* Override existing key. */ + knot_tsig_key_deinit(¶ms->tsig_key); + + ret = knot_tsig_key_init_str(¶ms->tsig_key, kstr); + if (ret != KNOT_EOK) { + ERR("invalid key specification"); + } + + free(kstr); + + return ret; +} + +int cmd_origin(const char* lp, knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + /* Check zone name. */ + if (!dname_isvalid(lp)) { + ERR("failed to parse zone '%s'", lp); + return KNOT_EPARSEFAIL; + } + + return knsupdate_set_origin(params, lp); +} + +/* + * Not implemented. + */ + +int cmd_gsstsig(const char* lp, _unused_ knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + ERR("gsstsig not supported"); + + return KNOT_ENOTSUP; +} + +int cmd_oldgsstsig(const char* lp, _unused_ knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + ERR("oldgsstsig not supported"); + + return KNOT_ENOTSUP; +} + +int cmd_realm(const char* lp, _unused_ knsupdate_params_t *params) +{ + DBG("%s: lp='%s'", __func__, lp); + + ERR("realm not supported"); + + return KNOT_ENOTSUP; +} diff --git a/src/utils/knsupdate/knsupdate_exec.h b/src/utils/knsupdate/knsupdate_exec.h new file mode 100644 index 0000000..36ca43b --- /dev/null +++ b/src/utils/knsupdate/knsupdate_exec.h @@ -0,0 +1,25 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "utils/knsupdate/knsupdate_params.h" + +extern const char* knsupdate_cmd_array[]; + +int knsupdate_exec(knsupdate_params_t *params); + +int knsupdate_process_line(const char *line, knsupdate_params_t *params); diff --git a/src/utils/knsupdate/knsupdate_interactive.c b/src/utils/knsupdate/knsupdate_interactive.c new file mode 100644 index 0000000..0e4ed66 --- /dev/null +++ b/src/utils/knsupdate/knsupdate_interactive.c @@ -0,0 +1,177 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <histedit.h> + +#include "contrib/string.h" +#include "utils/common/lookup.h" +#include "utils/common/msg.h" +#include "utils/knsupdate/knsupdate_exec.h" +#include "utils/knsupdate/knsupdate_interactive.h" + +#define HISTORY_FILE ".knsupdate_history" + +static char *prompt(EditLine *el) +{ + return PROGRAM_NAME"> "; +} + +static void print_commands(void) +{ + printf("\n"); + + for (const char **cmd = knsupdate_cmd_array; *cmd != NULL; cmd++) { + printf(" %-18s\n", (*cmd) + 1); + } +} + +static void cmds_lookup(EditLine *el, const char *str, size_t str_len) +{ + lookup_t lookup; + int ret = lookup_init(&lookup); + if (ret != KNOT_EOK) { + return; + } + + // Fill the lookup with command names. + for (const char **desc = knsupdate_cmd_array; *desc != NULL; desc++) { + ret = lookup_insert(&lookup, (*desc) + 1, NULL); + if (ret != KNOT_EOK) { + goto cmds_lookup_finish; + } + } + + (void)lookup_complete(&lookup, str, str_len, el, true); + +cmds_lookup_finish: + lookup_deinit(&lookup); +} + +static unsigned char complete(EditLine *el, int ch) +{ + int argc, token, pos; + const char **argv; + + const LineInfo *li = el_line(el); + Tokenizer *tok = tok_init(NULL); + + // Parse the line. + int ret = tok_line(tok, li, &argc, &argv, &token, &pos); + if (ret != 0) { + goto complete_exit; + } + + // Show possible commands. + if (argc == 0) { + print_commands(); + goto complete_exit; + } + + // Complete the command name. + if (token == 0) { + cmds_lookup(el, argv[0], pos); + goto complete_exit; + } + + // Find the command descriptor. + const char **desc = knsupdate_cmd_array; + while (*desc != NULL && strcmp((*desc) + 1, argv[0]) != 0) { + desc++; + } + if (*desc == NULL) { + goto complete_exit; + } + +complete_exit: + tok_reset(tok); + tok_end(tok); + + return CC_REDISPLAY; +} + +int interactive_loop(knsupdate_params_t *params) +{ + char *hist_file = NULL; + const char *home = getenv("HOME"); + if (home != NULL) { + hist_file = sprintf_alloc("%s/%s", home, HISTORY_FILE); + } + if (hist_file == NULL) { + INFO("failed to get home directory"); + } + + EditLine *el = el_init(PROGRAM_NAME, stdin, stdout, stderr); + if (el == NULL) { + ERR("interactive mode not available"); + free(hist_file); + return KNOT_ERROR; + } + + History *hist = history_init(); + if (hist == NULL) { + ERR("interactive mode not available"); + el_end(el); + free(hist_file); + return KNOT_ERROR; + } + + HistEvent hev = { 0 }; + history(hist, &hev, H_SETSIZE, 1000); + history(hist, &hev, H_SETUNIQUE, 1); + el_set(el, EL_HIST, history, hist); + history(hist, &hev, H_LOAD, hist_file); + + el_set(el, EL_TERMINAL, NULL); + el_set(el, EL_EDITOR, "emacs"); + el_set(el, EL_PROMPT, prompt); + el_set(el, EL_SIGNAL, 1); + el_source(el, NULL); + + // Warning: these two el_sets()'s always leak -- in libedit2 library! + // For more details see this commit's message. + el_set(el, EL_ADDFN, PROGRAM_NAME"-complete", + "Perform "PROGRAM_NAME" completion.", complete); + el_set(el, EL_BIND, "^I", PROGRAM_NAME"-complete", NULL); + + int count; + const char *line; + while ((line = el_gets(el, &count)) != NULL && count > 0) { + char command[count + 1]; + memcpy(command, line, count); + command[count] = '\0'; + // Removes trailing newline + size_t cmd_len = strcspn(command, "\n"); + command[cmd_len] = '\0'; + + if (cmd_len > 0) { + history(hist, &hev, H_ENTER, command); + history(hist, &hev, H_SAVE, hist_file); + } + + // Process the command. + (void)knsupdate_process_line(command, params); + if (params->stop) { + break; + } + } + + history_end(hist); + free(hist_file); + + el_end(el); + + return KNOT_EOK; +} diff --git a/src/utils/knsupdate/knsupdate_interactive.h b/src/utils/knsupdate/knsupdate_interactive.h new file mode 100644 index 0000000..e2a2428 --- /dev/null +++ b/src/utils/knsupdate/knsupdate_interactive.h @@ -0,0 +1,26 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "utils/knsupdate/knsupdate_params.h" + +/*! + * Executes an interactive processing loop. + * + * \param[in] params Utility parameters. + */ +int interactive_loop(knsupdate_params_t *params); diff --git a/src/utils/knsupdate/knsupdate_main.c b/src/utils/knsupdate/knsupdate_main.c new file mode 100644 index 0000000..ab65c1e --- /dev/null +++ b/src/utils/knsupdate/knsupdate_main.c @@ -0,0 +1,46 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdlib.h> + +#include "libdnssec/crypto.h" +#include "utils/knsupdate/knsupdate_exec.h" +#include "utils/knsupdate/knsupdate_params.h" +#include "libknot/libknot.h" + +int main(int argc, char *argv[]) +{ + + int ret = EXIT_SUCCESS; + + tzset(); + + knsupdate_params_t params; + if (knsupdate_parse(¶ms, argc, argv) == KNOT_EOK) { + if (!params.stop) { + dnssec_crypto_init(); + if (knsupdate_exec(¶ms) != KNOT_EOK) { + ret = EXIT_FAILURE; + } + dnssec_crypto_cleanup(); + } + } else { + ret = EXIT_FAILURE; + } + + knsupdate_clean(¶ms); + return ret; +} diff --git a/src/utils/knsupdate/knsupdate_params.c b/src/utils/knsupdate/knsupdate_params.c new file mode 100644 index 0000000..f9fa41f --- /dev/null +++ b/src/utils/knsupdate/knsupdate_params.c @@ -0,0 +1,304 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <getopt.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "utils/knsupdate/knsupdate_params.h" +#include "utils/common/msg.h" +#include "utils/common/netio.h" +#include "libknot/libknot.h" +#include "libknot/tsig.h" +#include "contrib/mempattern.h" +#include "contrib/strtonum.h" +#include "contrib/ucw/mempool.h" + +#define DEFAULT_RETRIES_NSUPDATE 3 +#define DEFAULT_TIMEOUT_NSUPDATE 12 + +static const style_t DEFAULT_STYLE_NSUPDATE = { + .format = FORMAT_NSUPDATE, + .style = { + .wrap = false, + .show_class = true, + .show_ttl = true, + .verbose = false, + .original_ttl = false, + .empty_ttl = false, + .human_ttl = false, + .human_timestamp = true, + .generic = false, + .ascii_to_idn = NULL + }, + .show_query = false, + .show_header = true, + .show_section = true, + .show_edns = false, + .show_question = true, + .show_answer = true, + .show_authority = true, + .show_additional = true, + .show_tsig = true, + .show_footer = false +}; + +static int parser_set_default(zs_scanner_t *s, const char *fmt, ...) +{ + /* Format string. */ + char buf[512]; /* Must suffice for domain name and TTL. */ + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + + if (n < 0 || (size_t)n >= sizeof(buf)) { + return ZS_EINVAL; + } + + /* Buffer must contain newline */ + if (zs_set_input_string(s, buf, n) != 0 || + zs_parse_all(s) != 0) { + return s->error.code; + } + + return KNOT_EOK; +} + +static int knsupdate_init(knsupdate_params_t *params) +{ + memset(params, 0, sizeof(knsupdate_params_t)); + + /* Initialize lists. */ + init_list(¶ms->qfiles); + init_list(¶ms->update_list); + init_list(¶ms->prereq_list); + + /* Initialize memory context. */ + mm_ctx_mempool(¶ms->mm, MM_DEFAULT_BLKSIZE); + + /* Default server. */ + params->server = srv_info_create(DEFAULT_IPV4_NAME, DEFAULT_DNS_PORT); + if (!params->server) + return KNOT_ENOMEM; + + /* Default settings. */ + params->ip = IP_ALL; + params->protocol = PROTO_ALL; + params->class_num = KNOT_CLASS_IN; + params->retries = DEFAULT_RETRIES_NSUPDATE; + params->wait = DEFAULT_TIMEOUT_NSUPDATE; + + /* Initialize RR parser. */ + if (zs_init(¶ms->parser, ".", params->class_num, 3600) != 0 || + zs_set_processing(¶ms->parser, NULL, NULL, NULL) != 0) { + zs_deinit(¶ms->parser); + return KNOT_ENOMEM; + } + + /* Default style. */ + params->style = DEFAULT_STYLE_NSUPDATE; + + /* Create query/answer packets. */ + params->query = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, ¶ms->mm); + params->answer = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, ¶ms->mm); + + return KNOT_EOK; +} + +void knsupdate_clean(knsupdate_params_t *params) +{ + if (params == NULL) { + return; + } + + /* Clear current query. */ + knsupdate_reset(params); + + /* Free qfiles. */ + ptrlist_free(¶ms->qfiles, ¶ms->mm); + + srv_info_free(params->server); + srv_info_free(params->srcif); + free(params->zone); + zs_deinit(¶ms->parser); + knot_pkt_free(params->query); + knot_pkt_free(params->answer); + knot_tsig_key_deinit(¶ms->tsig_key); + + /* Clean up the structure. */ + mp_delete(params->mm.ctx); + memset(params, 0, sizeof(*params)); +} + +/*! \brief Free RRSet list. */ +static void rr_list_free(list_t *list, knot_mm_t *mm) +{ + assert(list != NULL); + assert(mm != NULL); + + ptrnode_t *node; + WALK_LIST(node, *list) { + knot_rrset_t *rrset = (knot_rrset_t *)node->d; + knot_rrset_free(rrset, NULL); + } + ptrlist_free(list, mm); +} + +void knsupdate_reset(knsupdate_params_t *params) +{ + /* Free ADD/REMOVE RRSets. */ + rr_list_free(¶ms->update_list, ¶ms->mm); + + /* Free PREREQ RRSets. */ + rr_list_free(¶ms->prereq_list, ¶ms->mm); +} + +static void print_help(void) +{ + printf("Usage: %s [-d] [-v] [-k keyfile | -y [hmac:]name:key]\n" + " [-p port] [-t timeout] [-r retries] [filename]\n", + PROGRAM_NAME); +} + +int knsupdate_parse(knsupdate_params_t *params, int argc, char *argv[]) +{ + if (params == NULL || argv == NULL) { + return KNOT_EINVAL; + } + + int ret = knsupdate_init(params); + if (ret != KNOT_EOK) { + return ret; + } + + // Long options. + struct option opts[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { NULL } + }; + + /* Command line options processing. */ + int opt = 0; + while ((opt = getopt_long(argc, argv, "dhDvVp:t:r:y:k:", opts, NULL)) + != -1) { + switch (opt) { + case 'd': + case 'D': /* Extra debugging. */ + msg_enable_debug(1); + break; + case 'h': + print_help(); + params->stop = true; + return KNOT_EOK; + case 'v': + params->protocol = PROTO_TCP; + break; + case 'V': + print_version(PROGRAM_NAME); + params->stop = true; + return KNOT_EOK; + case 'p': + free(params->server->service); + params->server->service = strdup(optarg); + if (!params->server->service) { + ERR("failed to set default port '%s'", optarg); + return KNOT_ENOMEM; + } + break; + case 'r': + ret = str_to_u32(optarg, ¶ms->retries); + if (ret != KNOT_EOK) { + ERR("invalid retries '%s'", optarg); + return ret; + } + break; + case 't': + ret = params_parse_wait(optarg, ¶ms->wait); + if (ret != KNOT_EOK) { + ERR("invalid timeout '%s'", optarg); + return ret; + } + break; + case 'y': + knot_tsig_key_deinit(¶ms->tsig_key); + ret = knot_tsig_key_init_str(¶ms->tsig_key, optarg); + if (ret != KNOT_EOK) { + ERR("failed to parse key '%s'", optarg); + return ret; + } + break; + case 'k': + knot_tsig_key_deinit(¶ms->tsig_key); + ret = knot_tsig_key_init_file(¶ms->tsig_key, optarg); + if (ret != KNOT_EOK) { + ERR("failed to parse keyfile '%s'", optarg); + return ret; + } + break; + default: + print_help(); + return KNOT_ENOTSUP; + } + } + + /* No retries for TCP. */ + if (params->protocol == PROTO_TCP) { + params->retries = 0; + } else { + /* If wait/tries < 1 s, set 1 second for each try. */ + if (params->wait > 0 && + (uint32_t)params->wait < ( 1 + params->retries)) { + params->wait = 1; + } else { + params->wait /= (1 + params->retries); + } + } + + /* Process non-option parameters. */ + for (; optind < argc; ++optind) { + ptrlist_add(¶ms->qfiles, argv[optind], ¶ms->mm); + } + + return ret; +} + +int knsupdate_set_ttl(knsupdate_params_t *params, const uint32_t ttl) +{ + int ret = parser_set_default(¶ms->parser, "$TTL %u\n", ttl); + if (ret != KNOT_EOK) { + ERR("failed to set default TTL, %s", zs_strerror(ret)); + } + return ret; +} + +int knsupdate_set_origin(knsupdate_params_t *params, const char *origin) +{ + char *fqdn = get_fqd_name(origin); + + int ret = parser_set_default(¶ms->parser, "$ORIGIN %s\n", fqdn); + + free(fqdn); + + if (ret != KNOT_EOK) { + ERR("failed to set default origin, %s", zs_strerror(ret)); + } + return ret; +} diff --git a/src/utils/knsupdate/knsupdate_params.h b/src/utils/knsupdate/knsupdate_params.h new file mode 100644 index 0000000..1933244 --- /dev/null +++ b/src/utils/knsupdate/knsupdate_params.h @@ -0,0 +1,72 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> + +#include "utils/common/netio.h" +#include "utils/common/params.h" +#include "utils/common/sign.h" +#include "libknot/libknot.h" +#include "libzscanner/scanner.h" +#include "contrib/ucw/lists.h" + +#define PROGRAM_NAME "knsupdate" + +/*! \brief knsupdate-specific params data. */ +typedef struct { + /*!< Stop processing - just print help, version,... */ + bool stop; + /*!< List of files with query data. */ + list_t qfiles; + /*!< List of nameservers to query to. */ + srv_info_t *server; + /*!< Local interface (optional). */ + srv_info_t *srcif; + /*!< Version of ip protocol to use. */ + ip_t ip; + /*!< Type (TCP, UDP) protocol to use. */ + protocol_t protocol; + /*!< Default class number. */ + uint16_t class_num; + /*!< Number of UDP retries. */ + uint32_t retries; + /*!< Wait for network response in seconds (-1 means forever). */ + int32_t wait; + /*!< Current zone. */ + char *zone; + /*!< RR parser. */ + zs_scanner_t parser; + /*!< Current packet. */ + knot_pkt_t *query; + /*!< Current response. */ + knot_pkt_t *answer; + /*< Lists of RRSets. */ + list_t update_list, prereq_list; + /*!< Transaction signature context. */ + knot_tsig_key_t tsig_key; + /*!< Default output settings. */ + style_t style; + /*!< Memory context. */ + knot_mm_t mm; +} knsupdate_params_t; + +int knsupdate_parse(knsupdate_params_t *params, int argc, char *argv[]); +int knsupdate_set_ttl(knsupdate_params_t *params, const uint32_t ttl); +int knsupdate_set_origin(knsupdate_params_t *params, const char *origin); +void knsupdate_clean(knsupdate_params_t *params); +void knsupdate_reset(knsupdate_params_t *params); diff --git a/src/utils/kxdpgun/ip_route.c b/src/utils/kxdpgun/ip_route.c new file mode 100644 index 0000000..003c764 --- /dev/null +++ b/src/utils/kxdpgun/ip_route.c @@ -0,0 +1,358 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <time.h> + +#include <arpa/inet.h> +#include <libmnl/libmnl.h> +#include <linux/if_ether.h> +#include <linux/if_link.h> +#include <linux/rtnetlink.h> +#include <net/if.h> + +#include "utils/kxdpgun/ip_route.h" +#include "contrib/sockaddr.h" + +#define ROUTE_LOOKUP_LOOP_LIMIT 10000 + +static size_t addr_len(int family) +{ + switch (family) { + case AF_INET: + return sizeof(struct in_addr); + case AF_INET6: + return sizeof(struct in6_addr); + default: + return 0; + } +} + +static int send_dummy_pkt(const struct sockaddr_storage *ip) +{ + static const uint8_t dummy_pkt[] = { + // dummy data + 0x08, 0x00, 0xec, 0x72, 0x0b, 0x87, 0x00, 0x06, + + //padding + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + + int fd = socket(ip->ss_family, SOCK_RAW, + ip->ss_family == AF_INET6 ? IPPROTO_ICMPV6 : IPPROTO_ICMP); + if (fd < 0) { + return -errno; + } + int ret = sendto(fd, dummy_pkt, sizeof(dummy_pkt), 0, (const struct sockaddr *)ip, + ip->ss_family == AF_INET6 ? sizeof(struct sockaddr_in6) : + sizeof(struct sockaddr_in)); + if (ret < 0) { + ret = -errno; + } + close(fd); + return ret; +} + +static int netlink_query(int family, uint16_t type, mnl_cb_t cb, void *data, + void *qextra, size_t qextra_len, uint16_t qextra_type) +{ + // open and bind NETLINK socket + struct mnl_socket *nl = mnl_socket_open(NETLINK_ROUTE); + if (nl == NULL) { + return -errno; + } + if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) { + mnl_socket_close(nl); + return -errno; + } + unsigned portid = mnl_socket_get_portid(nl); + int ret = 0; + + // allocate request + char buf[MNL_SOCKET_BUFFER_SIZE]; + struct nlmsghdr *nlh = mnl_nlmsg_put_header(buf); + if (nlh == NULL) { + ret = -ENOMEM; + goto end; + } + unsigned seq = time(NULL); + nlh->nlmsg_seq = seq; + nlh->nlmsg_type = type; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; + struct rtmsg *rtm = mnl_nlmsg_put_extra_header(nlh, sizeof(*rtm)); + if (rtm == NULL) { + ret = -ENOMEM; + goto end; + } + + if (qextra_len > 0) { + nlh->nlmsg_flags = NLM_F_REQUEST; + rtm->rtm_dst_len = qextra_len * 8; // 8 bits per byte + mnl_attr_put(nlh, qextra_type, qextra_len, qextra); + } + + // send request + rtm->rtm_family = family; + ret = mnl_socket_sendto(nl, nlh, nlh->nlmsg_len); + if (ret < 0) { + ret = -errno; + goto end; + } + + // collect replies with callback + while ((ret = mnl_socket_recvfrom(nl, buf, sizeof(buf))) > 0) { + ret = mnl_cb_run(buf, ret, seq, portid, cb, data); + if (ret <= MNL_CB_STOP) { + break; + } + if (qextra_len > 0) { + break; + } + } + ret = ret < 0 ? -errno : 0; + +end: + mnl_socket_close(nl); + return ret; +} + +typedef struct { + const struct sockaddr_storage *ip; + struct sockaddr_storage *via; + struct sockaddr_storage *src; + char *dev; + uint64_t priority; // top 32 bits: unmatched address bits; bottom 32 bits: route metric priority + unsigned match; + + // intermediate callback data + const struct nlattr *tb[RTA_MAX+1]; +} ip_route_get_ctx_t; + +static int validate_attr_route(const struct nlattr *attr, void *data) +{ + /* skip unsupported attribute in user-space */ + if (mnl_attr_type_valid(attr, RTA_MAX) < 0) { + return MNL_CB_OK; + } + + ip_route_get_ctx_t *ctx = data; + + int type = mnl_attr_get_type(attr); + switch(type) { + case RTA_TABLE: + case RTA_OIF: + case RTA_FLOW: + case RTA_PRIORITY: + if (mnl_attr_validate(attr, MNL_TYPE_U32) < 0) { + return MNL_CB_ERROR; + } + break; + case RTA_DST: + case RTA_SRC: + case RTA_PREFSRC: + case RTA_GATEWAY: + if (mnl_attr_validate2(attr, MNL_TYPE_BINARY, addr_len(ctx->ip->ss_family)) < 0) { + return MNL_CB_ERROR; + } + break; + case RTA_METRICS: + if (mnl_attr_validate(attr, MNL_TYPE_NESTED) < 0) { + return MNL_CB_ERROR; + } + break; + } + ctx->tb[type] = attr; + return MNL_CB_OK; +} + +static void attr2addr(const struct nlattr *attr, int family, struct sockaddr_storage *out) +{ + if (attr == NULL) { + out->ss_family = AF_UNSPEC; + return; + } + out->ss_family = family; + if (family == AF_INET6) { + struct in6_addr *addr = mnl_attr_get_payload(attr); + memcpy(&((struct sockaddr_in6 *)out)->sin6_addr, addr, sizeof(*addr)); + } else { + struct in_addr *addr = mnl_attr_get_payload(attr); + memcpy(&((struct sockaddr_in *)out)->sin_addr, addr, sizeof(*addr)); + } +} + +static void attr2dev(const struct nlattr *attr, char *out) // out must have IFNAMSIZ length +{ + *out = '\0'; + + if (attr != NULL) { + if_indextoname(mnl_attr_get_u32(attr), out); + } +} + +static uint32_t attr2prio(const struct nlattr *attr) +{ + if (attr == NULL) { + return 0; // 0 is the default metric priority in linux + } + return mnl_attr_get_u32(attr); +} + +static int ip_route_get_cb(const struct nlmsghdr *nlh, void *data) +{ + ip_route_get_ctx_t *ctx = data; + struct rtmsg *rm = mnl_nlmsg_get_payload(nlh); + if (rm->rtm_family != ctx->ip->ss_family) { + return MNL_CB_ERROR; + } + + mnl_attr_parse(nlh, sizeof(*rm), validate_attr_route, data); + + uint64_t new_metric = addr_len(rm->rtm_family) * 8 - rm->rtm_dst_len; + new_metric = (new_metric << 32) + attr2prio(ctx->tb[RTA_PRIORITY]); + if (new_metric >= ctx->priority) { + return MNL_CB_OK; + } + + struct sockaddr_storage dst; + attr2addr(ctx->tb[RTA_DST], rm->rtm_family, &dst); + + if (rm->rtm_dst_len == 0 || + sockaddr_net_match(&dst, ctx->ip, rm->rtm_dst_len)) { + attr2addr(ctx->tb[RTA_PREFSRC], rm->rtm_family, ctx->src); + attr2addr(ctx->tb[RTA_GATEWAY], rm->rtm_family, ctx->via); + attr2dev(ctx->tb[RTA_OIF], ctx->dev); + ctx->match++; + ctx->priority = new_metric; + } + + memset(ctx->tb, 0, sizeof(void *) * (RTA_MAX+1)); + return MNL_CB_OK; +} + +int ip_route_get(const struct sockaddr_storage *ip, + struct sockaddr_storage *via, + struct sockaddr_storage *src, + char *dev) +{ + struct sockaddr_storage last_via = { 0 }; + ip_route_get_ctx_t ctx = { ip, &last_via, src, dev, 0, 0 }; + do { + ctx.priority = UINT64_MAX; + + size_t qextra_len; + void *qextra = sockaddr_raw(ip, &qextra_len); + int ret = netlink_query(ip->ss_family, RTM_GETROUTE, + ip_route_get_cb, &ctx, qextra, + qextra_len, IFA_ADDRESS); + if (ret != 0) { + return ret; + } + if (last_via.ss_family == ip->ss_family) { // not AF_UNSPEC + memcpy(via, &last_via, sizeof(*via)); + } + + // next loop will search for path to "via" + ctx.ip = via; + } while (last_via.ss_family != AF_UNSPEC && + ctx.priority != UINT64_MAX && // avoid loop when nothing found + ctx.match < ROUTE_LOOKUP_LOOP_LIMIT); // avoid loop when looped route + + return src->ss_family == ip->ss_family ? 0 : -ENOENT; +} + +typedef struct { + const struct sockaddr_storage *ip; + uint8_t *mac; + unsigned match; + + // intermediate callback data + const struct nlattr *tb[RTA_MAX+1]; +} ip_neigh_ctx_t; + +static int validate_attr_neigh(const struct nlattr *attr, void *data) +{ + /* skip unsupported attribute in user-space */ + if (mnl_attr_type_valid(attr, NDA_MAX) < 0) { + return MNL_CB_OK; + } + + ip_neigh_ctx_t *ctx = data; + + int type = mnl_attr_get_type(attr); + switch (type) { + case NDA_DST: + if (mnl_attr_validate2(attr, MNL_TYPE_BINARY, addr_len(ctx->ip->ss_family)) < 0) { + return MNL_CB_ERROR; + } + break; + case NDA_LLADDR: + if (mnl_attr_validate2(attr, MNL_TYPE_BINARY, ETH_ALEN) < 0) { + return MNL_CB_ERROR; + } + break; + } + + ctx->tb[type] = attr; + return MNL_CB_OK; +} + +static int ip_neigh_cb(const struct nlmsghdr *nlh, void *data) +{ + ip_neigh_ctx_t *ctx = data; + struct rtmsg *rm = mnl_nlmsg_get_payload(nlh); + if (rm->rtm_family != ctx->ip->ss_family) { + return MNL_CB_ERROR; + } + + mnl_attr_parse(nlh, sizeof(*rm), validate_attr_neigh, data); + + struct sockaddr_storage dst; + attr2addr(ctx->tb[NDA_DST], rm->rtm_family, &dst); + + if (sockaddr_cmp((struct sockaddr_storage *)&dst, (struct sockaddr_storage *)ctx->ip, true) == 0 && + ctx->tb[NDA_LLADDR] != NULL) { + memcpy(ctx->mac, mnl_attr_get_payload(ctx->tb[NDA_LLADDR]), ETH_ALEN); + ctx->match++; + } + + memset(ctx->tb, 0, sizeof(void *) * (RTA_MAX+1)); + return MNL_CB_OK; +} + +int ip_neigh_get(const struct sockaddr_storage *ip, bool dummy_sendto, uint8_t *mac) +{ + if (dummy_sendto) { + int ret = send_dummy_pkt(ip); + if (ret < 0) { + return ret; + } + usleep(10000); + } + ip_neigh_ctx_t ctx = { ip, mac, 0 }; + int ret = netlink_query(ip->ss_family, RTM_GETNEIGH, ip_neigh_cb, &ctx, + NULL, 0, 0); + if (ret == 0 && ctx.match == 0) { + return -ENOENT; + } + return ret; +} diff --git a/src/utils/kxdpgun/ip_route.h b/src/utils/kxdpgun/ip_route.h new file mode 100644 index 0000000..c35c25b --- /dev/null +++ b/src/utils/kxdpgun/ip_route.h @@ -0,0 +1,46 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> +#include <sys/socket.h> + +/*! + * \brief Get route to an IPv4/6 from system routing table. + * + * \param ip IPv4 or IPv6 to search route to. + * \param via Out: gateway (first hop) on the route, or AF_UNSPEC if same subnet. + * \param src Out: local outgoing IP address on the route. + * \param dev Out: local network interface on the route (you must pre-allocate IFNAMSIZ bytes!). + * + * \return 0 on success, negative errno otherwise. + */ +int ip_route_get(const struct sockaddr_storage *ip, + struct sockaddr_storage *via, + struct sockaddr_storage *src, + char *dev); + +/*! + * \brief Obtain neighbour's MAC addr from system neighbour table. + * + * \param ip IPv4 or IPv6 of the neighbour in question. + * \param dummy_sendto Attempt sendto() to target IP in order to let the system fill the neighbour table. + * \param mac Out: MAC address of the neighbour (you must pre-allocate ETH_ALEN bytes!). + * + * \return 0 on success, -ENOENT if neighbour not found, negative errno otherwise. + */ +int ip_neigh_get(const struct sockaddr_storage *ip, bool dummy_sendto, uint8_t *mac); diff --git a/src/utils/kxdpgun/load_queries.c b/src/utils/kxdpgun/load_queries.c new file mode 100644 index 0000000..8ecac48 --- /dev/null +++ b/src/utils/kxdpgun/load_queries.c @@ -0,0 +1,164 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "load_queries.h" +#include "libknot/libknot.h" +#include "utils/common/msg.h" + +#define ERR_PREFIX "failed loading queries " + +enum qflags { + QFLAG_EDNS = 1, + QFLAG_DO = 2, +}; + +struct pkt_payload *global_payloads = NULL; + +void free_global_payloads(void) +{ + struct pkt_payload *g_payloads_p = global_payloads, *tmp; + while (g_payloads_p != NULL) { + tmp = g_payloads_p; + g_payloads_p = tmp->next; + free(tmp); + } + global_payloads = NULL; +} + +bool load_queries(const char *filename, uint16_t edns_size, uint16_t msgid, size_t maxcount) +{ + size_t read = 0; + FILE *f = fopen(filename, "r"); + if (f == NULL) { + ERR2(ERR_PREFIX "file '%s' (%s)", filename, strerror(errno)); + return false; + } + struct pkt_payload *g_payloads_top = NULL; + + struct { + char line[KNOT_DNAME_TXT_MAXLEN + 256]; + char dname_txt[KNOT_DNAME_TXT_MAXLEN + 1]; + uint8_t dname[KNOT_DNAME_MAXLEN]; + char type_txt[128]; + char flags_txt[128]; + } *bufs; + bufs = malloc(sizeof(*bufs)); // avoiding too much stuff on stack + if (bufs == NULL) { + ERR2(ERR_PREFIX "(out of memory)"); + goto fail; + } + + while (read < maxcount) { + if (fgets(bufs->line, sizeof(bufs->line), f) == NULL) { + break; + } + bufs->flags_txt[0] = '\0'; + int ret = sscanf(bufs->line, "%s%s%s", bufs->dname_txt, bufs->type_txt, bufs->flags_txt); + if (ret < 2) { + ERR2(ERR_PREFIX "(faulty line): '%.*s'", + (int)strcspn(bufs->line, "\n"), bufs->line); + goto fail; + } + + void *pret = knot_dname_from_str(bufs->dname, bufs->dname_txt, sizeof(bufs->dname)); + if (pret == NULL) { + ERR2(ERR_PREFIX "(faulty dname): '%s'", bufs->dname_txt); + goto fail; + } + + uint16_t type; + ret = knot_rrtype_from_string(bufs->type_txt, &type); + if (ret < 0) { + ERR2(ERR_PREFIX "(faulty type): '%s'", bufs->type_txt); + goto fail; + } + + enum qflags flags = 0; + switch (bufs->flags_txt[0]) { + case '\0': + break; + case 'e': + case 'E': + flags |= QFLAG_EDNS; + break; + case 'd': + case 'D': + flags |= QFLAG_EDNS | QFLAG_DO; + break; + default: + ERR2(ERR_PREFIX "(faulty flag): '%s'", bufs->flags_txt); + goto fail; + } + + size_t dname_len = knot_dname_size(bufs->dname); + size_t pkt_len = KNOT_WIRE_HEADER_SIZE + 2 * sizeof(uint16_t) + dname_len; + if (flags & QFLAG_EDNS) { + pkt_len += KNOT_EDNS_MIN_SIZE; + } + + struct pkt_payload *pkt = calloc(1, sizeof(struct pkt_payload) + pkt_len); + if (pkt == NULL) { + ERR2(ERR_PREFIX "(out of memory)"); + goto fail; + } + pkt->len = pkt_len; + memcpy(pkt->payload, &msgid, sizeof(msgid)); + pkt->payload[2] = 0x01; // RD bit + pkt->payload[5] = 0x01; // 1 question + pkt->payload[11] = (flags & QFLAG_EDNS) ? 0x01 : 0x00; + memcpy(pkt->payload + 12, bufs->dname, dname_len); + pkt->payload[dname_len + 12] = type >> 8; + pkt->payload[dname_len + 13] = type & 0xff; + pkt->payload[dname_len + 15] = KNOT_CLASS_IN; + if (flags & QFLAG_EDNS) { + pkt->payload[dname_len + 18] = KNOT_RRTYPE_OPT; + pkt->payload[dname_len + 19] = edns_size >> 8; + pkt->payload[dname_len + 20] = edns_size & 0xff; + pkt->payload[dname_len + 23] = (flags & QFLAG_DO) ? 0x80 : 0x00; + } + + // add pkt to list global_payloads + if (g_payloads_top == NULL) { + global_payloads = pkt; + g_payloads_top = pkt; + } else { + g_payloads_top->next = pkt; + g_payloads_top = pkt; + } + read++; + } + + if (global_payloads == NULL) { + ERR2(ERR_PREFIX "(no queries in file)"); + goto fail; + } + + free(bufs); + fclose(f); + return true; + +fail: + free_global_payloads(); + free(bufs); + fclose(f); + return false; +} diff --git a/src/utils/kxdpgun/load_queries.h b/src/utils/kxdpgun/load_queries.h new file mode 100644 index 0000000..3d7bace --- /dev/null +++ b/src/utils/kxdpgun/load_queries.h @@ -0,0 +1,32 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdbool.h> +#include <stdint.h> + +struct pkt_payload { + struct pkt_payload *next; + size_t len; + uint8_t payload[]; +}; + +extern struct pkt_payload *global_payloads; + +bool load_queries(const char *filename, uint16_t edns_size, uint16_t msgid, size_t maxcount); + +void free_global_payloads(void); diff --git a/src/utils/kxdpgun/main.c b/src/utils/kxdpgun/main.c new file mode 100644 index 0000000..c9db312 --- /dev/null +++ b/src/utils/kxdpgun/main.c @@ -0,0 +1,1502 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <errno.h> +#include <getopt.h> +#include <ifaddrs.h> +#include <inttypes.h> +#include <net/if.h> +#include <poll.h> +#include <pthread.h> +#include <signal.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include <arpa/inet.h> +#include <netinet/in.h> +#include <net/if.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/resource.h> + +#include "libknot/libknot.h" +#include "libknot/xdp.h" +#include "libknot/xdp/tcp_iobuf.h" +#ifdef ENABLE_QUIC +#include <gnutls/gnutls.h> +#include "libknot/quic/quic.h" +#endif // ENABLE_QUIC +#include "contrib/macros.h" +#include "contrib/mempattern.h" +#include "contrib/openbsd/strlcat.h" +#include "contrib/openbsd/strlcpy.h" +#include "contrib/os.h" +#include "contrib/sockaddr.h" +#include "contrib/toeplitz.h" +#include "contrib/ucw/mempool.h" +#include "utils/common/msg.h" +#include "utils/common/params.h" +#include "utils/kxdpgun/ip_route.h" +#include "utils/kxdpgun/load_queries.h" + +#define PROGRAM_NAME "kxdpgun" +#define SPACE " " + +enum { + KXDPGUN_WAIT, + KXDPGUN_START, + KXDPGUN_STOP, +}; + +volatile int xdp_trigger = KXDPGUN_WAIT; + +volatile unsigned stats_trigger = 0; + +unsigned global_cpu_aff_start = 0; +unsigned global_cpu_aff_step = 1; + +#define REMOTE_PORT_DEFAULT 53 +#define REMOTE_PORT_DOQ_DEFAULT 853 +#define LOCAL_PORT_MIN 2000 +#define LOCAL_PORT_MAX 65535 +#define QUIC_THREAD_PORTS 100 + +#define RCODE_MAX (0x0F + 1) + +typedef struct { + size_t collected; + uint64_t duration; + uint64_t qry_sent; + uint64_t synack_recv; + uint64_t ans_recv; + uint64_t finack_recv; + uint64_t rst_recv; + uint64_t size_recv; + uint64_t wire_recv; + uint64_t rcodes_recv[RCODE_MAX]; + pthread_mutex_t mutex; +} kxdpgun_stats_t; + +static kxdpgun_stats_t global_stats = { 0 }; + +typedef enum { + KXDPGUN_IGNORE_NONE = 0, + KXDPGUN_IGNORE_QUERY = (1 << 0), + KXDPGUN_IGNORE_LASTBYTE = (1 << 1), + KXDPGUN_IGNORE_CLOSE = (1 << 2), + KXDPGUN_REUSE_CONN = (1 << 3), +} xdp_gun_ignore_t; + +typedef struct { + union { + struct sockaddr_in local_ip4; + struct sockaddr_in6 local_ip; + struct sockaddr_storage local_ip_ss; + }; + union { + struct sockaddr_in target_ip4; + struct sockaddr_in6 target_ip; + struct sockaddr_storage target_ip_ss; + }; + char dev[IFNAMSIZ]; + uint64_t qps, duration; + unsigned at_once; + uint16_t msgid; + uint16_t edns_size; + uint16_t vlan_tci; + uint8_t local_mac[6], target_mac[6]; + uint8_t local_ip_range; + bool ipv6; + bool tcp; + bool quic; + bool quic_full_handshake; + const char *qlog_dir; + const char *sending_mode; + xdp_gun_ignore_t ignore1; + knot_tcp_ignore_t ignore2; + uint16_t target_port; + knot_xdp_filter_flag_t flags; + unsigned n_threads, thread_id; + knot_eth_rss_conf_t *rss_conf; + knot_xdp_config_t xdp_config; +} xdp_gun_ctx_t; + +const static xdp_gun_ctx_t ctx_defaults = { + .dev[0] = '\0', + .edns_size = 1232, + .qps = 1000, + .duration = 5000000UL, // usecs + .at_once = 10, + .sending_mode = "", + .target_port = 0, + .flags = KNOT_XDP_FILTER_UDP | KNOT_XDP_FILTER_PASS, + .xdp_config = { .extra_frames = true }, +}; + +static void sigterm_handler(int signo) +{ + assert(signo == SIGTERM || signo == SIGINT); + xdp_trigger = KXDPGUN_STOP; +} + +static void sigusr_handler(int signo) +{ + assert(signo == SIGUSR1); + if (global_stats.collected == 0) { + stats_trigger++; + } +} + +static void clear_stats(kxdpgun_stats_t *st) +{ + pthread_mutex_lock(&st->mutex); + st->duration = 0; + st->qry_sent = 0; + st->synack_recv = 0; + st->ans_recv = 0; + st->finack_recv = 0; + st->rst_recv = 0; + st->size_recv = 0; + st->wire_recv = 0; + st->collected = 0; + memset(st->rcodes_recv, 0, sizeof(st->rcodes_recv)); + pthread_mutex_unlock(&st->mutex); +} + +static size_t collect_stats(kxdpgun_stats_t *into, const kxdpgun_stats_t *what) +{ + pthread_mutex_lock(&into->mutex); + into->duration = MAX(into->duration, what->duration); + into->qry_sent += what->qry_sent; + into->synack_recv += what->synack_recv; + into->ans_recv += what->ans_recv; + into->finack_recv += what->finack_recv; + into->rst_recv += what->rst_recv; + into->size_recv += what->size_recv; + into->wire_recv += what->wire_recv; + for (int i = 0; i < RCODE_MAX; i++) { + into->rcodes_recv[i] += what->rcodes_recv[i]; + } + size_t res = ++into->collected; + pthread_mutex_unlock(&into->mutex); + return res; +} + +static void print_stats(kxdpgun_stats_t *st, bool tcp, bool quic, bool recv, uint64_t qps) +{ + pthread_mutex_lock(&st->mutex); + +#define ps(counter) ((counter) * 1000 / (st->duration / 1000)) +#define pct(counter) ((counter) * 100.0 / st->qry_sent) + + const char *name = tcp ? "SYNs: " : quic ? "initials:" : "queries: "; + printf("total %s %"PRIu64" (%"PRIu64" pps) (%f%%)\n", name, + st->qry_sent, ps(st->qry_sent), 100.0 * st->qry_sent / (st->duration / 1000000.0 * qps)); + if (st->qry_sent > 0 && recv) { + if (tcp || quic) { + name = tcp ? "established:" : "handshakes: "; + printf("total %s %"PRIu64" (%"PRIu64" pps) (%f%%)\n", name, + st->synack_recv, ps(st->synack_recv), pct(st->synack_recv)); + } + printf("total replies: %"PRIu64" (%"PRIu64" pps) (%f%%)\n", + st->ans_recv, ps(st->ans_recv), pct(st->ans_recv)); + if (tcp) { + printf("total closed: %"PRIu64" (%"PRIu64" pps) (%f%%)\n", + st->finack_recv, ps(st->finack_recv), pct(st->finack_recv)); + } + if (st->rst_recv > 0) { + printf("total reset: %"PRIu64" (%"PRIu64" pps) (%f%%)\n", + st->rst_recv, ps(st->rst_recv), pct(st->rst_recv)); + } + printf("average DNS reply size: %"PRIu64" B\n", + st->ans_recv > 0 ? st->size_recv / st->ans_recv : 0); + printf("average Ethernet reply rate: %"PRIu64" bps (%.2f Mbps)\n", + ps(st->wire_recv * 8), ps((float)st->wire_recv * 8 / (1000 * 1000))); + + for (int i = 0; i < RCODE_MAX; i++) { + if (st->rcodes_recv[i] > 0) { + const knot_lookup_t *rcode = knot_lookup_by_id(knot_rcode_names, i); + const char *rcname = rcode == NULL ? "unknown" : rcode->name; + int space = MAX(9 - strlen(rcname), 0); + printf("responded %s: %.*s%"PRIu64"\n", + rcname, space, " ", st->rcodes_recv[i]); + } + } + } + printf("duration: %"PRIu64" s\n", (st->duration / (1000 * 1000))); + + pthread_mutex_unlock(&st->mutex); +} + +inline static void timer_start(struct timespec *timesp) +{ + clock_gettime(CLOCK_MONOTONIC, timesp); +} + +inline static uint64_t timer_end(struct timespec *timesp) +{ + struct timespec end; + clock_gettime(CLOCK_MONOTONIC, &end); + uint64_t res = (end.tv_sec - timesp->tv_sec) * (uint64_t)1000000; + res += ((int64_t)end.tv_nsec - timesp->tv_nsec) / 1000; + return res; +} + +static unsigned addr_bits(bool ipv6) +{ + return ipv6 ? 128 : 32; +} + +static void shuffle_sockaddr4(struct sockaddr_in *dst, struct sockaddr_in *src, uint64_t increment) +{ + memcpy(&dst->sin_addr, &src->sin_addr, sizeof(dst->sin_addr)); + if (increment > 0) { + dst->sin_addr.s_addr = htobe32(be32toh(src->sin_addr.s_addr) + increment); + } +} + +static void shuffle_sockaddr6(struct sockaddr_in6 *dst, struct sockaddr_in6 *src, uint64_t increment) +{ + memcpy(&dst->sin6_addr, &src->sin6_addr, sizeof(dst->sin6_addr)); + if (increment > 0) { + uint64_t *dst_addr = (uint64_t *)&dst->sin6_addr; + uint64_t *src_addr = (uint64_t *)&src->sin6_addr; + dst_addr[1] = htobe64(be64toh(src_addr[1]) + increment); + } +} + +static void shuffle_sockaddr(struct sockaddr_in6 *dst, struct sockaddr_in6 *src, + uint16_t port, uint64_t increment) +{ + dst->sin6_family = src->sin6_family; + dst->sin6_port = htobe16(port); + if (src->sin6_family == AF_INET6) { + shuffle_sockaddr6(dst, src, increment); + } else { + shuffle_sockaddr4((struct sockaddr_in *)dst, (struct sockaddr_in *)src, increment); + } +} + +static void next_payload(struct pkt_payload **payload, int increment) +{ + if (*payload == NULL) { + *payload = global_payloads; + } + for (int i = 0; i < increment; i++) { + if ((*payload)->next == NULL) { + *payload = global_payloads; + } else { + *payload = (*payload)->next; + } + } +} + +static void put_dns_payload(struct iovec *put_into, bool zero_copy, xdp_gun_ctx_t *ctx, struct pkt_payload **payl) +{ + if (zero_copy) { + put_into->iov_base = (*payl)->payload; + } else { + memcpy(put_into->iov_base, (*payl)->payload, (*payl)->len); + } + put_into->iov_len = (*payl)->len; + next_payload(payl, ctx->n_threads); +} + +#ifdef ENABLE_QUIC +static uint16_t get_rss_id(xdp_gun_ctx_t *ctx, uint16_t local_port) +{ + assert(ctx->rss_conf); + + const uint8_t *key = (const uint8_t *)&(ctx->rss_conf->data[ctx->rss_conf->table_size]); + const size_t key_len = ctx->rss_conf->key_size; + uint8_t data[2 * sizeof(struct in6_addr) + 2 * sizeof(uint16_t)]; + + size_t addr_len; + if (ctx->ipv6) { + addr_len = sizeof(struct in6_addr); + memcpy(data, &ctx->target_ip.sin6_addr, addr_len); + memcpy(data + addr_len, &ctx->local_ip.sin6_addr, addr_len); + } else { + addr_len = sizeof(struct in_addr); + memcpy(data, &ctx->target_ip4.sin_addr, addr_len); + memcpy(data + addr_len, &ctx->local_ip4.sin_addr, addr_len); + } + + uint16_t src_port = htobe16(ctx->target_port); + memcpy(data + 2 * addr_len, &src_port, sizeof(src_port)); + uint16_t dst_port = htobe16(local_port); + memcpy(data + 2 * addr_len + sizeof(uint16_t), &dst_port, sizeof(dst_port)); + + size_t data_len = 2 * addr_len + 2 * sizeof(uint16_t); + uint16_t hash = toeplitz_hash(key, key_len, data, data_len); + + return ctx->rss_conf->data[hash & ctx->rss_conf->mask]; +} + +static uint16_t adjust_port(xdp_gun_ctx_t *ctx, uint16_t local_port) +{ + assert(UINT16_MAX == LOCAL_PORT_MAX); + + if (local_port < LOCAL_PORT_MIN) { + local_port = LOCAL_PORT_MIN; + } + + if (ctx->rss_conf == NULL) { + return local_port; + } + + for (int i = 0; i < UINT16_MAX; i++) { + if (ctx->thread_id == get_rss_id(ctx, local_port)) { + break; + } + local_port++; + if (local_port < LOCAL_PORT_MIN) { + local_port = LOCAL_PORT_MIN; + } + } + + return local_port; +} +#endif // ENABLE_QUIC + +static unsigned alloc_pkts(knot_xdp_msg_t *pkts, struct knot_xdp_socket *xsk, + xdp_gun_ctx_t *ctx, uint64_t tick) +{ + uint64_t unique = (tick * ctx->n_threads + ctx->thread_id) * ctx->at_once; + + knot_xdp_msg_flag_t flags = ctx->ipv6 ? KNOT_XDP_MSG_IPV6 : 0; + if (ctx->tcp) { + flags |= (KNOT_XDP_MSG_TCP | KNOT_XDP_MSG_SYN | KNOT_XDP_MSG_MSS); + } else if (ctx->quic) { + return ctx->at_once; // NOOP + } + if (ctx->vlan_tci != 0) { + flags |= KNOT_XDP_MSG_VLAN; + } + + for (unsigned i = 0; i < ctx->at_once; i++) { + int ret = knot_xdp_send_alloc(xsk, flags, &pkts[i]); + if (ret != KNOT_EOK) { + return i; + } + + uint16_t port_range = LOCAL_PORT_MAX - LOCAL_PORT_MIN + 1; + uint16_t local_port = LOCAL_PORT_MIN + unique % port_range; + uint64_t ip_incr = (unique / port_range) % (1 << (addr_bits(ctx->ipv6) - ctx->local_ip_range)); + shuffle_sockaddr(&pkts[i].ip_from, &ctx->local_ip, local_port, ip_incr); + shuffle_sockaddr(&pkts[i].ip_to, &ctx->target_ip, ctx->target_port, 0); + + memcpy(pkts[i].eth_from, ctx->local_mac, 6); + memcpy(pkts[i].eth_to, ctx->target_mac, 6); + + pkts[i].vlan_tci = ctx->vlan_tci; + + unique++; + } + return ctx->at_once; +} + +inline static bool check_dns_payload(struct iovec *payl, xdp_gun_ctx_t *ctx, + kxdpgun_stats_t *st) +{ + if (payl->iov_len < KNOT_WIRE_HEADER_SIZE || + memcmp(payl->iov_base, &ctx->msgid, sizeof(ctx->msgid)) != 0) { + return false; + } + st->rcodes_recv[((uint8_t *)payl->iov_base)[3] & 0x0F]++; + st->size_recv += payl->iov_len; + st->ans_recv++; + return true; +} + +#ifdef ENABLE_QUIC +static int quic_alloc_cb(knot_quic_reply_t *rpl) +{ + xdp_gun_ctx_t *ctx = rpl->in_ctx; + knot_xdp_msg_t *msg = rpl->out_ctx; + + unsigned flags = ctx->ipv6 ? KNOT_XDP_MSG_IPV6 : 0; + if (ctx->vlan_tci != 0) { + flags |= KNOT_XDP_MSG_VLAN; + } + + int ret = knot_xdp_send_alloc(rpl->sock, flags, msg); + if (ret != KNOT_EOK) { + return ret; + } + + memcpy(msg->eth_from, ctx->local_mac, sizeof(ctx->local_mac)); + memcpy(msg->eth_to, ctx->target_mac, sizeof(ctx->target_mac)); + memcpy(&msg->ip_from, &ctx->local_ip, sizeof(msg->ip_from)); + memcpy(&msg->ip_to, &ctx->target_ip, sizeof(msg->ip_to)); + + msg->vlan_tci = ctx->vlan_tci; + + return KNOT_EOK; +} + +static int quic_reply_alloc_cb(knot_quic_reply_t *rpl) +{ + return knot_xdp_reply_alloc(rpl->sock, rpl->in_ctx, rpl->out_ctx); +} + +static int quic_send_cb(knot_quic_reply_t *rpl) +{ + uint32_t sent = 0; + return knot_xdp_send(rpl->sock, rpl->out_ctx, 1, &sent); +} + +static void quic_free_cb(knot_quic_reply_t *rpl) +{ + knot_xdp_send_free(rpl->sock, rpl->out_ctx, 1); +} +#endif // ENABLE_QUIC + +void *xdp_gun_thread(void *_ctx) +{ + xdp_gun_ctx_t *ctx = _ctx; + struct knot_xdp_socket *xsk; + struct timespec timer; + knot_xdp_msg_t pkts[ctx->at_once]; + uint64_t errors = 0, lost = 0, duration = 0; + kxdpgun_stats_t local_stats = { 0 }; + unsigned stats_triggered = 0; + knot_tcp_table_t *tcp_table = NULL; +#ifdef ENABLE_QUIC + knot_quic_table_t *quic_table = NULL; + struct knot_quic_creds *quic_creds = NULL; + list_t quic_sessions; + init_list(&quic_sessions); +#endif // ENABLE_QUIC + list_t reuse_conns; + init_list(&reuse_conns); + const uint64_t extra_wait = ctx->quic ? 4000000 : 1000000; + + if (ctx->tcp) { + tcp_table = knot_tcp_table_new(ctx->qps, NULL); + if (tcp_table == NULL) { + ERR2("failed to allocate TCP connection table"); + return NULL; + } + } + if (ctx->quic) { +#ifdef ENABLE_QUIC + quic_creds = knot_quic_init_creds_peer(NULL, NULL, 0); + if (quic_creds == NULL) { + ERR2("failed to initialize QUIC context"); + return NULL; + } + quic_table = knot_quic_table_new(ctx->qps * 100, SIZE_MAX, SIZE_MAX, 1232, quic_creds); + if (quic_table == NULL) { + ERR2("failed to allocate QUIC connection table"); + return NULL; + } + quic_table->qlog_dir = ctx->qlog_dir; +#else + assert(0); +#endif // ENABLE_QUIC + } + + knot_xdp_load_bpf_t mode = (ctx->thread_id == 0 ? + KNOT_XDP_LOAD_BPF_ALWAYS : KNOT_XDP_LOAD_BPF_NEVER); + /* + * This mutex prevents libbpf from logging: + * 'libbpf: can't get link by id (5535): Resource temporarily unavailable' + */ + pthread_mutex_lock(&global_stats.mutex); + int ret = knot_xdp_init(&xsk, ctx->dev, ctx->thread_id, ctx->flags, + LOCAL_PORT_MIN, LOCAL_PORT_MIN, mode, &ctx->xdp_config); + pthread_mutex_unlock(&global_stats.mutex); + if (ret != KNOT_EOK) { + ERR2("failed to initialize XDP socket#%u on interface %s (%s)", + ctx->thread_id, ctx->dev, knot_strerror(ret)); + knot_tcp_table_free(tcp_table); + return NULL; + } + + if (ctx->thread_id == 0) { + INFO2("using interface %s, XDP threads %u, %s%s%s, %s mode", + ctx->dev, ctx->n_threads, + (ctx->tcp ? "TCP" : ctx->quic ? "QUIC" : "UDP"), + (ctx->sending_mode[0] != '\0' ? " mode " : ""), + (ctx->sending_mode[0] != '\0' ? ctx->sending_mode : ""), + (knot_eth_xdp_mode(if_nametoindex(ctx->dev)) == KNOT_XDP_MODE_FULL ? + "native" : "emulated")); + } + + struct pollfd pfd = { knot_xdp_socket_fd(xsk), POLLIN, 0 }; + + while (xdp_trigger == KXDPGUN_WAIT) { + usleep(1000); + } + + uint64_t tick = 0; + struct pkt_payload *payload_ptr = NULL; + next_payload(&payload_ptr, ctx->thread_id); + +#ifdef ENABLE_QUIC + knot_xdp_msg_t msg_out; + knot_quic_reply_t quic_send_reply = { + .out_payload = &msg_out.payload, + .in_ctx = ctx, + .out_ctx = &msg_out, + .sock = xsk, + .alloc_reply = quic_alloc_cb, + .send_reply = quic_send_cb, + .free_reply = quic_free_cb, + }; + knot_quic_reply_t quic_reply = { + .out_payload = &msg_out.payload, + .out_ctx = &msg_out, + .sock = xsk, + .alloc_reply = quic_reply_alloc_cb, + .send_reply = quic_send_cb, + .free_reply = quic_free_cb, + }; + + ctx->target_ip.sin6_port = htobe16(ctx->target_port); + knot_sweep_stats_t sweep_stats = { 0 }; + + uint16_t local_ports[QUIC_THREAD_PORTS]; + uint16_t port = LOCAL_PORT_MIN; + for (int i = 0; ctx->quic && i < QUIC_THREAD_PORTS; ++i) { + local_ports[i] = adjust_port(ctx, port); + port = local_ports[i] + 1; + assert(port >= LOCAL_PORT_MIN); + } + size_t local_ports_it = 0; +#endif // ENABLE_QUIC + + timer_start(&timer); + + while (duration < ctx->duration + extra_wait) { + + // sending part + if (duration < ctx->duration) { + while (1) { + knot_xdp_send_prepare(xsk); + unsigned alloced = alloc_pkts(pkts, xsk, ctx, tick); + if (alloced < ctx->at_once) { + lost += ctx->at_once - alloced; + if (alloced == 0) { + break; + } + } + + if (ctx->tcp) { + for (int i = 0; i < alloced; i++) { + pkts[i].payload.iov_len = 0; + + if (!EMPTY_LIST(reuse_conns)) { + ptrnode_t *n = HEAD(reuse_conns); + knot_tcp_relay_t *rl = n->d; + rem_node(&n->n); + free(n); + struct iovec payl; + put_dns_payload(&payl, true, ctx, &payload_ptr); + ret = knot_tcp_reply_data(rl, tcp_table, + (ctx->ignore1 & KXDPGUN_IGNORE_LASTBYTE), + payl.iov_base, payl.iov_len); + if (ret == KNOT_EOK) { + ret = knot_tcp_send(xsk, rl, 1, ctx->at_once); + } + if (ret == KNOT_EOK) { + pkts[i].flags &= ~KNOT_XDP_MSG_SYN; // skip sending respective packet + local_stats.qry_sent++; + } + free(rl); + } + } + } else if (ctx->quic) { +#ifdef ENABLE_QUIC + uint16_t local_port = local_ports[local_ports_it++ % QUIC_THREAD_PORTS]; + ctx->local_ip.sin6_port = htobe16(local_port); + + for (unsigned i = 0; i < ctx->at_once; i++) { + knot_quic_conn_t *newconn = NULL; + if (!EMPTY_LIST(reuse_conns)) { + ptrnode_t *n = HEAD(reuse_conns); + newconn = n->d; + rem_node(&n->n); + assert(HEAD(reuse_conns) != n); + free(n); + if (newconn->streams_count < 1) { + newconn = NULL; // un-re-usable conn + } else { + ctx->local_ip.sin6_port = knot_quic_conn_local_port(newconn); + ret = KNOT_EOK; + } + } + if (newconn == NULL) { + ret = knot_quic_client(quic_table, &ctx->target_ip, &ctx->local_ip, + NULL, &newconn); + } + if (ret == KNOT_EOK) { + struct iovec tmp = { + knot_quic_stream_add_data(newconn, (newconn->streams_first + newconn->streams_count) * 4, + NULL, payload_ptr->len), + 0 + }; + put_dns_payload(&tmp, false, ctx, &payload_ptr); + if (newconn->streams_count < 2) { + if (EMPTY_LIST(quic_sessions)) { + newconn->streams_count = -1; + } else { + void *session = HEAD(quic_sessions); + rem_node(session); + (void)knot_quic_session_load(newconn, session); + } + } + ret = knot_quic_send(quic_table, newconn, &quic_send_reply, 1, + (ctx->ignore1 & KXDPGUN_IGNORE_LASTBYTE) ? KNOT_QUIC_SEND_IGNORE_LASTBYTE : 0); + } + if (ret == KNOT_EOK) { + local_stats.qry_sent++; + } + } + (void)knot_xdp_send_finish(xsk); +#endif // ENABLE_QUIC + break; + } else { + for (int i = 0; i < alloced; i++) { + put_dns_payload(&pkts[i].payload, false, + ctx, &payload_ptr); + } + } + + uint32_t really_sent = 0; + if (knot_xdp_send(xsk, pkts, alloced, &really_sent) != KNOT_EOK) { + lost += alloced; + } + local_stats.qry_sent += really_sent; + (void)knot_xdp_send_finish(xsk); + + break; + } + } + + // receiving part + if (!(ctx->flags & KNOT_XDP_FILTER_DROP)) { + while (1) { + ret = poll(&pfd, 1, 0); + if (ret < 0) { + errors++; + break; + } + if (!pfd.revents) { + break; + } + + uint32_t recvd = 0; + size_t wire = 0; + (void)knot_xdp_recv(xsk, pkts, ctx->at_once, &recvd, &wire); + if (recvd == 0) { + break; + } + if (ctx->tcp) { + knot_tcp_relay_t relays[recvd]; + ret = knot_tcp_recv(relays, pkts, recvd, tcp_table, NULL, ctx->ignore2); + if (ret != KNOT_EOK) { + errors++; + break; + } + + for (size_t i = 0; i < recvd; i++) { + knot_tcp_relay_t *rl = &relays[i]; + struct iovec payl; + switch (rl->action) { + case XDP_TCP_ESTABLISH: + local_stats.synack_recv++; + if (ctx->ignore1 & KXDPGUN_IGNORE_QUERY) { + break; + } + put_dns_payload(&payl, true, ctx, &payload_ptr); + ret = knot_tcp_reply_data(rl, tcp_table, + (ctx->ignore1 & KXDPGUN_IGNORE_LASTBYTE), + payl.iov_base, payl.iov_len); + if (ret != KNOT_EOK) { + errors++; + } + break; + case XDP_TCP_CLOSE: + local_stats.finack_recv++; + break; + case XDP_TCP_RESET: + local_stats.rst_recv++; + break; + default: + break; + } + for (size_t j = 0; rl->inbf != NULL && j < rl->inbf->n_inbufs; j++) { + if (check_dns_payload(&rl->inbf->inbufs[j], ctx, &local_stats)) { + if (!(ctx->ignore1 & KXDPGUN_IGNORE_CLOSE)) { + rl->answer = XDP_TCP_CLOSE; + } else if ((ctx->ignore1 & KXDPGUN_REUSE_CONN)) { + knot_tcp_relay_t *rl_copy = malloc(sizeof(*rl)); + memcpy(rl_copy, rl, sizeof(*rl)); + ptrlist_add(&reuse_conns, rl_copy, NULL); + rl_copy->answer = XDP_TCP_NOOP; + rl_copy->auto_answer = 0; + } + } + } + } + + ret = knot_tcp_send(xsk, relays, recvd, ctx->at_once); + if (ret != KNOT_EOK) { + errors++; + } + (void)knot_xdp_send_finish(xsk); + + knot_tcp_cleanup(tcp_table, relays, recvd); + } else if (ctx->quic) { +#ifdef ENABLE_QUIC + for (size_t i = 0; i < recvd; i++) { + knot_xdp_msg_t *msg_in = &pkts[i]; + knot_quic_conn_t *conn; + + quic_reply.ip_rem = (struct sockaddr_storage *)&msg_in->ip_from; + quic_reply.ip_loc = (struct sockaddr_storage *)&msg_in->ip_to; + quic_reply.in_payload = &msg_in->payload; + quic_reply.in_ctx = msg_in; + + ret = knot_quic_handle(quic_table, &quic_reply, 5000000000L, &conn); + if (ret == KNOT_ECONN) { + local_stats.rst_recv++; + knot_quic_cleanup(&conn, 1); + continue; + } else if (ret != 0) { + errors++; + knot_quic_cleanup(&conn, 1); + break; + } + + if (conn == NULL || conn->conn == NULL) { + knot_quic_cleanup(&conn, 1); + continue; + } + + if (!ctx->quic_full_handshake && knot_quic_session_available(conn)) { + void *session = knot_quic_session_save(conn); + if (session != NULL) { + add_tail(&quic_sessions, session); + } + } + + if ((conn->flags & KNOT_QUIC_CONN_HANDSHAKE_DONE) && conn->streams_count == -1) { + conn->streams_count = 1; + + local_stats.synack_recv++; + if ((ctx->ignore1 & KXDPGUN_IGNORE_QUERY)) { + knot_quic_table_rem(conn, quic_table); + knot_quic_cleanup(&conn, 1); + continue; + } + } + if (!(conn->flags & KNOT_QUIC_CONN_HANDSHAKE_DONE) && conn->streams_count == -1) { + knot_quic_table_rem(conn, quic_table); + knot_quic_cleanup(&conn, 1); + continue; + } + assert(conn->streams_count > 0); + + if ((ctx->ignore2 & XDP_TCP_IGNORE_ESTABLISH)) { + knot_quic_table_rem(conn, quic_table); + knot_quic_cleanup(&conn, 1); + local_stats.synack_recv++; + continue; + } + + int64_t s0id; + knot_quic_stream_t *stream0 = knot_quic_stream_get_process(conn, &s0id); + if (stream0 != NULL && stream0->inbufs != NULL && stream0->inbufs->n_inbufs > 0) { + check_dns_payload(&stream0->inbufs->inbufs[0], ctx, &local_stats); + stream0->inbufs->n_inbufs = 0; // signal that data have been read out + + if ((ctx->ignore2 & XDP_TCP_IGNORE_DATA_ACK)) { + knot_quic_table_rem(conn, quic_table); + knot_quic_cleanup(&conn, 1); + continue; + } else if ((ctx->ignore1 & KXDPGUN_REUSE_CONN)) { + if (conn->streams_count > 1) { // keep the number of outstanding streams below MAX_STREAMS_PER_CONN, while preserving at least one at all times + knot_quic_conn_stream_free(conn, conn->streams_first * 4); + } + ptrlist_add(&reuse_conns, conn, NULL); + } + } + ret = knot_quic_send(quic_table, conn, &quic_reply, 4, + (ctx->ignore1 & KXDPGUN_IGNORE_LASTBYTE) ? KNOT_QUIC_SEND_IGNORE_LASTBYTE : 0); + if (ret != KNOT_EOK) { + errors++; + } + + if (!(ctx->ignore1 & KXDPGUN_IGNORE_CLOSE) && (conn->flags & KNOT_QUIC_CONN_SESSION_TAKEN) && + stream0 != NULL && stream0->inbufs != NULL && stream0->inbufs->n_inbufs == 0) { + assert(!(ctx->ignore2 & XDP_TCP_IGNORE_DATA_ACK)); + quic_reply.handle_ret = KNOT_QUIC_HANDLE_RET_CLOSE; + ret = knot_quic_send(quic_table, conn, &quic_reply, 1, 0); + knot_quic_table_rem(conn, quic_table); + knot_quic_cleanup(&conn, 1); + if (ret != KNOT_EOK) { + errors++; + } + } + } + (void)knot_xdp_send_finish(xsk); +#endif // ENABLE_QUIC + } else { + for (int i = 0; i < recvd; i++) { + (void)check_dns_payload(&pkts[i].payload, ctx, + &local_stats); + } + } + local_stats.wire_recv += wire; + knot_xdp_recv_finish(xsk, pkts, recvd); + pfd.revents = 0; + } + } + +#ifdef ENABLE_QUIC + if (ctx->quic) { + (void)knot_quic_table_sweep(quic_table, NULL, &sweep_stats); + } +#endif // ENABLE_QUIC + + // speed and signal part + uint64_t dura_exp = (local_stats.qry_sent * 1000000) / ctx->qps; + duration = timer_end(&timer); + if (xdp_trigger == KXDPGUN_STOP && ctx->duration > duration) { + ctx->duration = duration; + } + if (stats_trigger > stats_triggered) { + assert(stats_trigger == stats_triggered + 1); + stats_triggered++; + + local_stats.duration = duration; + size_t collected = collect_stats(&global_stats, &local_stats); + assert(collected <= ctx->n_threads); + if (collected == ctx->n_threads) { + print_stats(&global_stats, ctx->tcp, ctx->quic, + !(ctx->flags & KNOT_XDP_FILTER_DROP), + ctx->qps * ctx->n_threads); + clear_stats(&global_stats); + } + } + if (dura_exp > duration) { + usleep(dura_exp - duration); + } + if (duration > ctx->duration) { + usleep(1000); + } + tick++; + } + + knot_xdp_deinit(xsk); + + if (ctx->tcp) { + ptrlist_deep_free(&reuse_conns, NULL); + } else if (ctx->quic) { + ptrlist_free(&reuse_conns, NULL); // stored conns get freed as part of xyz_table_free + } else { + assert(EMPTY_LIST(reuse_conns)); + } + knot_tcp_table_free(tcp_table); + +#ifdef ENABLE_QUIC + knot_quic_table_free(quic_table); + struct knot_quic_session *n, *nxt; + WALK_LIST_DELSAFE(n, nxt, quic_sessions) { + knot_quic_session_load(NULL, n); + } + knot_quic_free_creds(quic_creds); +#endif // ENABLE_QUIC + + char recv_str[40] = "", lost_str[40] = "", err_str[40] = ""; + if (!(ctx->flags & KNOT_XDP_FILTER_DROP)) { + (void)snprintf(recv_str, sizeof(recv_str), ", received %"PRIu64, local_stats.ans_recv); + } + if (lost > 0) { + (void)snprintf(lost_str, sizeof(lost_str), ", lost %"PRIu64, lost); + } + if (errors > 0) { + (void)snprintf(err_str, sizeof(err_str), ", errors %"PRIu64, errors); + } + INFO2("thread#%02u: sent %"PRIu64"%s%s%s", + ctx->thread_id, local_stats.qry_sent, recv_str, lost_str, err_str); + local_stats.duration = ctx->duration; + collect_stats(&global_stats, &local_stats); + + return NULL; +} + +static int dev2mac(const char *dev, uint8_t *mac) +{ + struct ifreq ifr; + memset(&ifr, 0, sizeof(ifr)); + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) { + return -errno; + } + strlcpy(ifr.ifr_name, dev, IFNAMSIZ); + + int ret = ioctl(fd, SIOCGIFHWADDR, &ifr); + if (ret >= 0) { + memcpy(mac, ifr.ifr_hwaddr.sa_data, 6); + } else { + ret = -errno; + } + close(fd); + return ret; +} + +static bool mac_empty(const uint8_t *mac) +{ + static const uint8_t unset_mac[6] = { 0 }; + return (memcmp(mac, unset_mac, sizeof(unset_mac)) == 0); +} + +static int mac_sscan(const char *src, uint8_t *dst) +{ + int tmp[6]; + if (6 != sscanf(src, "%2x:%2x:%2x:%2x:%2x:%2x", + &tmp[0], &tmp[1], &tmp[2], &tmp[3], &tmp[4], &tmp[5])) { + return KNOT_EINVAL; + } + + for (int i = 0; i < 6; i++) { + dst[i] = (uint8_t)tmp[i]; + } + + return KNOT_EOK; +} + +static bool configure_target(char *target_str, char *local_ip, xdp_gun_ctx_t *ctx) +{ + int val; + char *at = strrchr(target_str, '@'); + if (at != NULL && (val = atoi(at + 1)) > 0 && val <= 0xffff) { + ctx->target_port = val; + *at = '\0'; + } + + ctx->ipv6 = false; + if (inet_pton(AF_INET, target_str, &ctx->target_ip4.sin_addr) <= 0) { + ctx->ipv6 = true; + ctx->target_ip.sin6_family = AF_INET6; + if (inet_pton(AF_INET6, target_str, &ctx->target_ip.sin6_addr) <= 0) { + ERR2("invalid target IP"); + return false; + } + } else { + ctx->target_ip.sin6_family = AF_INET; + } + + struct sockaddr_storage via = { 0 }; + if (local_ip == NULL || ctx->dev[0] == '\0' || mac_empty(ctx->target_mac)) { + char auto_dev[IFNAMSIZ]; + int ret = ip_route_get(&ctx->target_ip_ss, + &via, + &ctx->local_ip_ss, + (ctx->dev[0] == '\0') ? ctx->dev : auto_dev); + if (ret < 0) { + ERR2("can't find route to '%s' (%s)", target_str, strerror(-ret)); + return false; + } + } + + ctx->local_ip_range = addr_bits(ctx->ipv6); // by default use one IP + if (local_ip != NULL) { + at = strrchr(local_ip, '/'); + if (at != NULL && (val = atoi(at + 1)) > 0 && val <= ctx->local_ip_range) { + ctx->local_ip_range = val; + *at = '\0'; + } + if (ctx->ipv6) { + if (ctx->local_ip_range < 64 || + inet_pton(AF_INET6, local_ip, &ctx->local_ip.sin6_addr) <= 0) { + ERR2("invalid local IPv6 or unsupported prefix length"); + return false; + } + } else { + if (inet_pton(AF_INET, local_ip, &ctx->local_ip4.sin_addr) <= 0) { + ERR2("invalid local IPv4"); + return false; + } + } + } + + if (mac_empty(ctx->target_mac)) { + const struct sockaddr_storage *neigh = (via.ss_family == AF_UNSPEC) ? + &ctx->target_ip_ss : &via; + int ret = ip_neigh_get(neigh, true, ctx->target_mac); + if (ret < 0) { + char neigh_str[256] = { 0 }; + sockaddr_tostr(neigh_str, sizeof(neigh_str), (struct sockaddr_storage *)neigh); + ERR2("failed to get remote MAC of target/gateway '%s' (%s)", + neigh_str, strerror(-ret)); + return false; + } + } + + if (mac_empty(ctx->local_mac)) { + int ret = dev2mac(ctx->dev, ctx->local_mac); + if (ret < 0) { + ERR2("failed to get MAC of device '%s' (%s)", ctx->dev, strerror(-ret)); + return false; + } + } + + int ret = knot_eth_queues(ctx->dev); + if (ret >= 0) { + ctx->n_threads = ret; + } else { + ERR2("unable to get number of queues for '%s' (%s)", ctx->dev, + knot_strerror(ret)); + return false; + } + + if (ctx->n_threads > 1 && ctx->quic) { + ret = knot_eth_rss(ctx->dev, &ctx->rss_conf); + if (ret != 0) { + WARN2("unable to read NIC RSS configuration for '%s' (%s)", + ctx->dev, knot_strerror(ret)); + } + } + + return true; +} + +static void print_help(void) +{ + printf("Usage: %s [options] -i <queries_file> <dest_ip>\n" + "\n" + "Options:\n" + " -t, --duration <sec> "SPACE"Duration of traffic generation.\n" + " "SPACE" (default is %"PRIu64" seconds)\n" + " -T, --tcp[=debug_mode] "SPACE"Send queries over TCP.\n" + " -U, --quic[=debug_mode] "SPACE"Send queries over QUIC.\n" + " -Q, --qps <qps> "SPACE"Number of queries-per-second (approximately) to be sent.\n" + " "SPACE" (default is %"PRIu64" qps)\n" + " -b, --batch <size> "SPACE"Send queries in a batch of defined size.\n" + " "SPACE" (default is %d for UDP, %d for TCP)\n" + " -r, --drop "SPACE"Drop incoming responses (disables response statistics).\n" + " -p, --port <port> "SPACE"Remote destination port.\n" + " "SPACE" (default is %d for UDP/TCP, %u for QUIC)\n" + " -F, --affinity <spec> "SPACE"CPU affinity in the format [<cpu_start>][s<cpu_step>].\n" + " "SPACE" (default is %s)\n" + " -i, --infile <file> "SPACE"Path to a file with query templates.\n" + " -I, --interface <ifname> "SPACE"Override auto-detected interface for outgoing communication.\n" + " -l, --local <ip[/prefix]>"SPACE"Override auto-detected source IP address or subnet.\n" + " -L, --local-mac <MAC> "SPACE"Override auto-detected local MAC address.\n" + " -R, --remote-mac <MAC> "SPACE"Override auto-detected remote MAC address.\n" + " -v, --vlan <id> "SPACE"Add VLAN 802.1Q header with the given id.\n" + " -e, --edns-size <size> "SPACE"EDNS UDP payload size, range 512-4096 (default 1232)\n" + " -m, --mode <mode> "SPACE"Set XDP mode (auto, copy, generic).\n" + " -G, --qlog <path> "SPACE"Output directory for qlog (useful for QUIC only).\n" + " -h, --help "SPACE"Print the program help.\n" + " -V, --version "SPACE"Print the program version.\n" + "\n" + "Parameters:\n" + " <dest_ip> "SPACE"IPv4 or IPv6 address of the remote destination.\n", + PROGRAM_NAME, ctx_defaults.duration / 1000000, ctx_defaults.qps, + ctx_defaults.at_once, 1, REMOTE_PORT_DEFAULT, REMOTE_PORT_DOQ_DEFAULT, "0s1"); +} + +static bool sending_mode(const char *arg, xdp_gun_ctx_t *ctx) +{ + if (arg == NULL) { + ctx->sending_mode = ""; + return true; + } else if (strlen(arg) != 1) { + goto mode_invalid; + } + ctx->sending_mode = arg; + + switch (ctx->sending_mode[0]) { + case '0': + if (!ctx->quic) { + goto mode_unavailable; + } + ctx->quic_full_handshake = true; + break; + case '1': + ctx->ignore1 = KXDPGUN_IGNORE_QUERY; + ctx->ignore2 = XDP_TCP_IGNORE_ESTABLISH | XDP_TCP_IGNORE_FIN; + break; + case '2': + ctx->ignore1 = KXDPGUN_IGNORE_QUERY; + break; + case '3': + ctx->ignore1 = KXDPGUN_IGNORE_QUERY; + ctx->ignore2 = XDP_TCP_IGNORE_FIN; + break; + case '5': + ctx->ignore1 = KXDPGUN_IGNORE_LASTBYTE; + ctx->ignore2 = XDP_TCP_IGNORE_FIN; + break; + case '7': + ctx->ignore1 = KXDPGUN_IGNORE_CLOSE; + ctx->ignore2 = XDP_TCP_IGNORE_DATA_ACK | XDP_TCP_IGNORE_FIN; + break; + case '8': + ctx->ignore1 = KXDPGUN_IGNORE_CLOSE; + ctx->ignore2 = XDP_TCP_IGNORE_FIN; + break; + case '9': + if (!ctx->tcp) { + goto mode_unavailable; + } + ctx->ignore2 = XDP_TCP_IGNORE_FIN; + break; + case 'R': + ctx->ignore1 = KXDPGUN_IGNORE_CLOSE | KXDPGUN_REUSE_CONN; + ctx->quic_full_handshake = true; + break; + default: + goto mode_invalid; + } + + return true; +mode_unavailable: + ERR2("mode '%s' not available", optarg); + return false; +mode_invalid: + ERR2("invalid mode '%s'", optarg); + return false; +} + +static int set_mode(const char *arg, knot_xdp_config_t *config) +{ + assert(arg != NULL); + assert(config != NULL); + + if (strcmp(arg, "auto") == 0) { + config->force_copy = false; + config->force_generic = false; + return KNOT_EOK; + } + + if (strcmp(arg, "copy") == 0) { + config->force_copy = true; + config->force_generic = false; + return KNOT_EOK; + } + + if (strcmp(arg, "generic") == 0) { + config->force_copy = false; + config->force_generic = true; + return KNOT_EOK; + } + + return KNOT_EINVAL; +} + +static bool get_opts(int argc, char *argv[], xdp_gun_ctx_t *ctx) +{ + struct option opts[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "duration", required_argument, NULL, 't' }, + { "qps", required_argument, NULL, 'Q' }, + { "batch", required_argument, NULL, 'b' }, + { "drop", no_argument, NULL, 'r' }, + { "port", required_argument, NULL, 'p' }, + { "tcp", optional_argument, NULL, 'T' }, + { "quic", optional_argument, NULL, 'U' }, + { "affinity", required_argument, NULL, 'F' }, + { "interface", required_argument, NULL, 'I' }, + { "local", required_argument, NULL, 'l' }, + { "infile", required_argument, NULL, 'i' }, + { "local-mac", required_argument, NULL, 'L' }, + { "remote-mac", required_argument, NULL, 'R' }, + { "vlan", required_argument, NULL, 'v' }, + { "edns-size", required_argument, NULL, 'e' }, + { "mode", required_argument, NULL, 'm' }, + { "qlog", required_argument, NULL, 'G' }, + { NULL } + }; + + int opt = 0, arg; + bool default_at_once = true; + double argf; + char *argcp, *local_ip = NULL, *filename = NULL; + while ((opt = getopt_long(argc, argv, "hVt:Q:b:rp:T::U::F:I:l:i:L:R:v:e:m:G:", opts, NULL)) != -1) { + switch (opt) { + case 'h': + print_help(); + exit(EXIT_SUCCESS); + case 'V': + print_version(PROGRAM_NAME); + exit(EXIT_SUCCESS); + case 't': + assert(optarg); + argf = atof(optarg); + if (argf > 0) { + ctx->duration = argf * 1000000.0; + assert(ctx->duration >= 1000); + } else { + ERR2("invalid duration '%s'", optarg); + return false; + } + break; + case 'Q': + assert(optarg); + arg = atoi(optarg); + if (arg > 0) { + ctx->qps = arg; + } else { + ERR2("invalid QPS '%s'", optarg); + return false; + } + break; + case 'b': + assert(optarg); + arg = atoi(optarg); + if (arg > 0) { + default_at_once = false; + ctx->at_once = arg; + } else { + ERR2("invalid batch size '%s'", optarg); + return false; + } + break; + case 'r': + ctx->flags &= ~KNOT_XDP_FILTER_PASS; + ctx->flags |= KNOT_XDP_FILTER_DROP; + break; + case 'p': + assert(optarg); + arg = atoi(optarg); + if (arg > 0 && arg <= 0xffff) { + ctx->target_port = arg; + } else { + ERR2("invalid port '%s'", optarg); + return false; + } + break; + case 'T': + ctx->tcp = true; + ctx->quic = false; + ctx->flags &= ~(KNOT_XDP_FILTER_UDP | KNOT_XDP_FILTER_QUIC); + ctx->flags |= KNOT_XDP_FILTER_TCP; + if (default_at_once) { + ctx->at_once = 1; + } + if (!sending_mode(optarg, ctx)) { + return false; + } + break; + case 'U': +#ifdef ENABLE_QUIC + ctx->quic = true; + ctx->tcp = false; + ctx->flags &= ~(KNOT_XDP_FILTER_UDP | KNOT_XDP_FILTER_TCP); + ctx->flags |= KNOT_XDP_FILTER_QUIC; + if (ctx->target_port == 0) { + ctx->target_port = REMOTE_PORT_DOQ_DEFAULT; + } + if (default_at_once) { + ctx->at_once = 1; + } + if (!sending_mode(optarg, ctx)) { + return false; + } +#else + ERR2("QUIC not available"); + return false; +#endif // ENABLE_QUIC + break; + case 'F': + assert(optarg); + if ((arg = atoi(optarg)) > 0) { + global_cpu_aff_start = arg; + } + argcp = strchr(optarg, 's'); + if (argcp != NULL && (arg = atoi(argcp + 1)) > 0) { + global_cpu_aff_step = arg; + } + break; + case 'I': + strlcpy(ctx->dev, optarg, IFNAMSIZ); + break; + case 'l': + local_ip = optarg; + break; + case 'i': + filename = optarg; + break; + case 'L': + if (mac_sscan(optarg, ctx->local_mac) != KNOT_EOK) { + ERR2("invalid local MAC address '%s'", optarg); + return false; + } + break; + case 'R': + if (mac_sscan(optarg, ctx->target_mac) != KNOT_EOK) { + ERR2("invalid remote MAC address '%s'", optarg); + return false; + } + break; + case 'v': + assert(optarg); + arg = atoi(optarg); + if (arg > 0 && arg < 4095) { + uint16_t id = arg; + ctx->vlan_tci = htobe16(id); + } else { + ERR2("invalid VLAN id '%s'", optarg); + return false; + } + break; + case 'e': + assert(optarg); + arg = atoi(optarg); + if (arg >= 512 && arg <= 4096) { + ctx->edns_size = arg; + } else { + ERR2("invalid edns size '%s'", optarg); + return false; + } + break; + case 'm': + assert(optarg); + if (set_mode(optarg, &ctx->xdp_config) != KNOT_EOK) { + ERR2("invalid mode '%s'", optarg); + return false; + } + break; + case 'G': + ctx->qlog_dir = optarg; + break; + default: + print_help(); + return false; + } + } + if (filename == NULL) { + print_help(); + return false; + } + size_t qcount = ctx->duration / 1000000 * ctx->qps; + if (!load_queries(filename, ctx->edns_size, ctx->msgid, qcount)) { + return false; + } + if (global_payloads == NULL || argc - optind != 1) { + print_help(); + return false; + } + + if (ctx->target_port == 0) { + ctx->target_port = REMOTE_PORT_DEFAULT; + } + + if (!configure_target(argv[optind], local_ip, ctx)) { + return false; + } + + if (ctx->qps < ctx->n_threads) { + WARN2("QPS increased to the number of threads/queues: %u", ctx->n_threads); + ctx->qps = ctx->n_threads; + } + ctx->qps /= ctx->n_threads; + + return true; +} + +int main(int argc, char *argv[]) +{ + xdp_gun_ctx_t ctx = ctx_defaults, *thread_ctxs = NULL; + ctx.msgid = time(NULL) % UINT16_MAX; + pthread_t *threads = NULL; + + if (!get_opts(argc, argv, &ctx)) { + free_global_payloads(); + return EXIT_FAILURE; + } + + thread_ctxs = calloc(ctx.n_threads, sizeof(*thread_ctxs)); + threads = calloc(ctx.n_threads, sizeof(*threads)); + if (thread_ctxs == NULL || threads == NULL) { + ERR2("out of memory"); + free(thread_ctxs); + free(threads); + free_global_payloads(); + return EXIT_FAILURE; + } + for (int i = 0; i < ctx.n_threads; i++) { + thread_ctxs[i] = ctx; + thread_ctxs[i].thread_id = i; + } + + if (!linux_at_least(5, 11)) { + struct rlimit min_limit = { RLIM_INFINITY, RLIM_INFINITY }, cur_limit = { 0 }; + if (getrlimit(RLIMIT_MEMLOCK, &cur_limit) != 0 || + cur_limit.rlim_cur != min_limit.rlim_cur || + cur_limit.rlim_max != min_limit.rlim_max) { + int ret = setrlimit(RLIMIT_MEMLOCK, &min_limit); + if (ret != 0) { + WARN2("unable to increase RLIMIT_MEMLOCK: %s", + strerror(errno)); + } + } + } + + pthread_mutex_init(&global_stats.mutex, NULL); + + struct sigaction stop_action = { .sa_handler = sigterm_handler }; + struct sigaction stats_action = { .sa_handler = sigusr_handler }; + sigaction(SIGINT, &stop_action, NULL); + sigaction(SIGTERM, &stop_action, NULL); + sigaction(SIGUSR1, &stats_action, NULL); + + for (size_t i = 0; i < ctx.n_threads; i++) { + unsigned affinity = global_cpu_aff_start + i * global_cpu_aff_step; + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET(affinity, &set); + (void)pthread_create(&threads[i], NULL, xdp_gun_thread, &thread_ctxs[i]); + int ret = pthread_setaffinity_np(threads[i], sizeof(cpu_set_t), &set); + if (ret != 0) { + WARN2("failed to set affinity of thread#%zu to CPU#%u", i, affinity); + } + usleep(20000); + } + usleep(1000000); + + xdp_trigger = KXDPGUN_START; + usleep(1000000); + + for (size_t i = 0; i < ctx.n_threads; i++) { + pthread_join(threads[i], NULL); + } + if (global_stats.duration > 0 && global_stats.qry_sent > 0) { + print_stats(&global_stats, ctx.tcp, ctx.quic, !(ctx.flags & KNOT_XDP_FILTER_DROP), ctx.qps * ctx.n_threads); + } + pthread_mutex_destroy(&global_stats.mutex); + + free(ctx.rss_conf); + free(thread_ctxs); + free(threads); + free_global_payloads(); + + return EXIT_SUCCESS; +} diff --git a/src/utils/kzonecheck/main.c b/src/utils/kzonecheck/main.c new file mode 100644 index 0000000..3a2b620 --- /dev/null +++ b/src/utils/kzonecheck/main.c @@ -0,0 +1,185 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <getopt.h> +#include <libgen.h> +#include <stdio.h> + +#include "contrib/time.h" +#include "contrib/tolower.h" +#include "libknot/libknot.h" +#include "knot/common/log.h" +#include "knot/zone/semantic-check.h" +#include "utils/common/msg.h" +#include "utils/common/params.h" +#include "utils/kzonecheck/zone_check.h" + +#define PROGRAM_NAME "kzonecheck" + +#define STDIN_SUBST "-" +#define STDIN_REPL "/dev/stdin" + +static void print_help(void) +{ + printf("Usage: %s [options] <filename>\n" + "\n" + "Options:\n" + " -o, --origin <zone_origin> Zone name.\n" + " (default filename without .zone)\n" + " -d, --dnssec <on|off> Also check DNSSEC-related records.\n" + " -t, --time <timestamp> Current time specification.\n" + " (default current UNIX time)\n" + " -p, --print Print the zone on stdout.\n" + " -v, --verbose Enable debug output.\n" + " -h, --help Print the program help.\n" + " -V, --version Print the program version.\n", + PROGRAM_NAME); +} + +static bool str2bool(const char *s) +{ + switch (knot_tolower(s[0])) { + case '1': + case 'y': + case 't': + return true; + case 'o': + return knot_tolower(s[1]) == 'n'; + default: + return false; + } +} + +int main(int argc, char *argv[]) +{ + const char *origin = NULL; + bool verbose = false, print = false; + semcheck_optional_t optional = SEMCHECK_DNSSEC_AUTO; // default value for --dnssec + knot_time_t check_time = (knot_time_t)time(NULL); + + /* Long options. */ + struct option opts[] = { + { "origin", required_argument, NULL, 'o' }, + { "time", required_argument, NULL, 't' }, + { "dnssec", required_argument, NULL, 'd' }, + { "print", no_argument, NULL, 'p' }, + { "verbose", no_argument, NULL, 'v' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { NULL } + }; + + /* Set the time zone. */ + tzset(); + + /* Parse command line arguments */ + int opt = 0; + while ((opt = getopt_long(argc, argv, "o:t:d:pvVh", opts, NULL)) != -1) { + switch (opt) { + case 'o': + origin = optarg; + break; + case 'p': + print = true; + break; + case 'v': + verbose = true; + break; + case 'h': + print_help(); + return EXIT_SUCCESS; + case 'V': + print_version(PROGRAM_NAME); + return EXIT_SUCCESS; + case 'd': + optional = str2bool(optarg) ? SEMCHECK_DNSSEC_ON : SEMCHECK_DNSSEC_OFF; + break; + case 't': + if (knot_time_parse("YMDhms|#|+-#U|+-#", + optarg, &check_time) != KNOT_EOK) { + ERR2("unknown time format"); + return EXIT_FAILURE; + } + break; + default: + print_help(); + return EXIT_FAILURE; + } + } + + /* Check if there's at least one remaining non-option. */ + if (optind >= argc) { + ERR2("expected zone file name"); + print_help(); + return EXIT_FAILURE; + } + + char *filename = argv[optind]; + if (strncmp(filename, STDIN_SUBST, sizeof(STDIN_SUBST)) == 0) { + filename = STDIN_REPL; + } + + char *zonename; + if (origin == NULL) { + /* Get zone name from file name. */ + const char *ext = ".zone"; + zonename = basename(filename); + if (strcmp(zonename + strlen(zonename) - strlen(ext), ext) == 0) { + zonename = strndup(zonename, strlen(zonename) - strlen(ext)); + } else { + zonename = strdup(zonename); + } + } else { + zonename = strdup(origin); + } + + log_init(); + log_levels_set(LOG_TARGET_STDOUT, LOG_SOURCE_ANY, 0); + log_levels_set(LOG_TARGET_STDERR, LOG_SOURCE_ANY, 0); + log_levels_set(LOG_TARGET_SYSLOG, LOG_SOURCE_ANY, 0); + log_flag_set(LOG_FLAG_NOTIMESTAMP | LOG_FLAG_NOINFO); + if (verbose) { + log_levels_add(LOG_TARGET_STDOUT, LOG_SOURCE_ANY, LOG_UPTO(LOG_DEBUG)); + } + + knot_dname_t *dname = knot_dname_from_str_alloc(zonename); + knot_dname_to_lower(dname); + free(zonename); + int ret = zone_check(filename, dname, optional, (time_t)check_time, print); + knot_dname_free(dname, NULL); + + log_close(); + + switch (ret) { + case KNOT_EOK: + if (verbose) { + INFO2("No semantic error found"); + } + return EXIT_SUCCESS; + case KNOT_EZONEINVAL: + ERR2("serious semantic error detected"); + // FALLTHROUGH + case KNOT_ESEMCHECK: + return EXIT_FAILURE; + case KNOT_EACCES: + case KNOT_EFILE: + ERR2("failed to load the zone file"); + return EXIT_FAILURE; + default: + ERR2("failed to run semantic checks (%s)", knot_strerror(ret)); + return EXIT_FAILURE; + } +} diff --git a/src/utils/kzonecheck/zone_check.c b/src/utils/kzonecheck/zone_check.c new file mode 100644 index 0000000..542e152 --- /dev/null +++ b/src/utils/kzonecheck/zone_check.c @@ -0,0 +1,101 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdio.h> +#include <assert.h> + +#include "utils/kzonecheck/zone_check.h" + +#include "knot/zone/contents.h" +#include "knot/zone/zonefile.h" +#include "knot/zone/zone-dump.h" +#include "utils/common/msg.h" + +typedef struct { + sem_handler_t handler; + unsigned errors[SEM_ERR_UNKNOWN + 1]; /*!< Counting errors by type. */ + unsigned error_count; /*!< Total error count. */ +} err_handler_stats_t; + +static void err_callback(sem_handler_t *handler, const zone_contents_t *zone, + const knot_dname_t *node, sem_error_t error, const char *data) +{ + assert(handler != NULL); + assert(zone != NULL); + err_handler_stats_t *stats = (err_handler_stats_t *)handler; + + knot_dname_txt_storage_t buff; + char *owner = knot_dname_to_str(buff, (node != NULL ? node : zone->apex->owner), + sizeof(buff)); + if (owner == NULL) { + owner = ""; + } + + fprintf(stderr, "[%s] %s%s%s\n", owner, sem_error_msg(error), + (data != NULL ? " " : ""), + (data != NULL ? data : "")); + + stats->errors[error]++; + stats->error_count++; +} + +static void print_statistics(err_handler_stats_t *stats) +{ + fprintf(stderr, "\nError summary:\n"); + for (sem_error_t i = 0; i <= SEM_ERR_UNKNOWN; ++i) { + if (stats->errors[i] > 0) { + fprintf(stderr, "%4u\t%s\n", stats->errors[i], sem_error_msg(i)); + } + } +} + +int zone_check(const char *zone_file, const knot_dname_t *zone_name, + semcheck_optional_t optional, time_t time, bool print) +{ + err_handler_stats_t stats = { + .handler = { .cb = err_callback }, + }; + + zloader_t zl; + int ret = zonefile_open(&zl, zone_file, zone_name, optional, time); + if (ret != KNOT_EOK) { + return ret; + } + zl.err_handler = (sem_handler_t *)&stats; + zl.creator->master = true; + + zone_contents_t *contents = zonefile_load(&zl); + zonefile_close(&zl); + if (contents == NULL && !stats.handler.error) { + return KNOT_ERROR; + } + + if (stats.error_count > 0) { + print_statistics(&stats); + ret = stats.handler.error ? KNOT_EZONEINVAL : KNOT_ESEMCHECK; + if (print) { + fprintf(stderr, "\n"); + } + } + + if (print) { + printf(";; Zone dump (Knot DNS %s)\n", PACKAGE_VERSION); + zone_dump_text(contents, stdout, false, NULL); + } + zone_contents_deep_free(contents); + + return ret; +} diff --git a/src/utils/kzonecheck/zone_check.h b/src/utils/kzonecheck/zone_check.h new file mode 100644 index 0000000..7039f16 --- /dev/null +++ b/src/utils/kzonecheck/zone_check.h @@ -0,0 +1,23 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/zone/semantic-check.h" +#include "libknot/libknot.h" + +int zone_check(const char *zone_file, const knot_dname_t *zone_name, + semcheck_optional_t optional, time_t time, bool print); diff --git a/src/utils/kzonesign/main.c b/src/utils/kzonesign/main.c new file mode 100644 index 0000000..e70abb6 --- /dev/null +++ b/src/utils/kzonesign/main.c @@ -0,0 +1,283 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <getopt.h> +#include <stdlib.h> + +#include "knot/dnssec/zone-events.h" +#include "knot/updates/zone-update.h" +#include "knot/server/server.h" +#include "knot/zone/adjust.h" +#include "knot/zone/zone-load.h" +#include "knot/zone/zonefile.h" +#include "utils/common/msg.h" +#include "utils/common/params.h" +#include "utils/common/signal.h" +#include "utils/common/util_conf.h" +#include "contrib/strtonum.h" + +#define PROGRAM_NAME "kzonesign" + +signal_ctx_t signal_ctx = { 0 }; // global, needed by signal handler + +static void print_help(void) +{ + printf("Usage: %s [-c | -C <path>] [options] <zone_name>\n" + "\n" + "Config options:\n" + " -c, --config <file> Path to a textual configuration file.\n" + " (default %s)\n" + " -C, --confdb <dir> Path to a configuration database directory.\n" + " (default %s)\n" + "Options:\n" + " -o, --outdir <dir_name> Output directory.\n" + " -r, --rollover Allow key rollovers and NSEC3 re-salt.\n" + " -v, --verify Only verify if zone is signed correctly.\n" + " -t, --time <timestamp> Current time specification.\n" + " (default current UNIX time)\n" + " -h, --help Print the program help.\n" + " -V, --version Print the program version.\n", + PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR); +} + +typedef struct { + const char *zone_name_str; + knot_dname_storage_t zone_name; + const char *outdir; + zone_sign_roll_flags_t rollover; + int64_t timestamp; + bool verify; +} sign_params_t; + +static int zonesign(sign_params_t *params) +{ + char *zonefile = NULL; + zone_contents_t *unsigned_conts = NULL; + zone_t *zone_struct = NULL; + zone_update_t up = { 0 }; + server_t fake_server = { 0 }; + zone_sign_reschedule_t next_sign = { 0 }; + int ret = KNOT_ERROR; + + // set the kaspdb for close in emergency + signal_ctx.close_db = &fake_server.kaspdb; + + conf_val_t val = conf_zone_get(conf(), C_DOMAIN, params->zone_name); + if (val.code != KNOT_EOK) { + ERR2("zone '%s' not configured", params->zone_name_str); + ret = KNOT_ENOENT; + goto fail; + } + val = conf_zone_get(conf(), C_DNSSEC_POLICY, params->zone_name); + if (val.code != KNOT_EOK) { + WARN2("DNSSEC policy not configured for zone '%s', taking defaults", + params->zone_name_str); + } + + zone_struct = zone_new(params->zone_name); + if (zone_struct == NULL) { + ERR2("out of memory"); + ret = KNOT_ENOMEM; + goto fail; + } + + ret = zone_load_contents(conf(), params->zone_name, &unsigned_conts, + SEMCHECK_MANDATORY_SOFT, false); + if (ret != KNOT_EOK) { + ERR2("failed to load zone contents (%s)", knot_strerror(ret)); + goto fail; + } + + ret = zone_update_from_contents(&up, zone_struct, unsigned_conts, UPDATE_FULL); + if (ret != KNOT_EOK) { + ERR2("failed to initialize zone update (%s)", knot_strerror(ret)); + zone_contents_deep_free(unsigned_conts); + goto fail; + } + + if (params->verify) { + val = conf_zone_get(conf(), C_ADJUST_THR, params->zone_name); + ret = zone_adjust_full(up.new_cont, conf_int(&val)); + if (ret != KNOT_EOK) { + ERR2("failed to adjust the zone (%s)", knot_strerror(ret)); + zone_update_clear(&up); + goto fail; + } + + ret = knot_dnssec_validate_zone(&up, conf(), params->timestamp, false); + if (ret != KNOT_EOK) { + ERR2("DNSSEC validation failed (%s)", knot_strerror(ret)); + char type_str[16]; + knot_dname_txt_storage_t name_str; + if (knot_dname_to_str(name_str, up.validation_hint.node, sizeof(name_str)) != NULL && + knot_rrtype_to_string(up.validation_hint.rrtype, type_str, sizeof(type_str)) >= 0) { + ERR2("affected node: '%s' type '%s'", name_str, type_str); + } + } else { + INFO2("DNSSEC validation successful"); + } + zone_update_clear(&up); + goto fail; + } + + kasp_db_ensure_init(&fake_server.kaspdb, conf()); + zone_struct->server = &fake_server; + + ret = knot_dnssec_zone_sign(&up, conf(), 0, params->rollover, + params->timestamp, &next_sign); + if (ret == KNOT_DNSSEC_ENOKEY) { // exception: allow generating initial keys + params->rollover = KEY_ROLL_ALLOW_ALL; + ret = knot_dnssec_zone_sign(&up, conf(), 0, params->rollover, + params->timestamp, &next_sign); + } + if (ret != KNOT_EOK) { + ERR2("failed to sign the zone (%s)", knot_strerror(ret)); + zone_update_clear(&up); + goto fail; + } + + if (params->outdir == NULL) { + zonefile = conf_zonefile(conf(), params->zone_name); + ret = zonefile_write(zonefile, up.new_cont); + } else { + zone_contents_t *temp = zone_struct->contents; + zone_struct->contents = up.new_cont; + ret = zone_dump_to_dir(conf(), zone_struct, params->outdir); + zone_struct->contents = temp; + } + zone_update_clear(&up); + if (ret != KNOT_EOK) { + if (params->outdir == NULL) { + ERR2("failed to update zone file '%s' (%s)", + zonefile, knot_strerror(ret)); + } else { + ERR2("failed to flush signed zone to '%s' file (%s)", + params->outdir, knot_strerror(ret)); + + } + goto fail; + } + + INFO2("Next signing: %"KNOT_TIME_PRINTF, next_sign.next_sign); + if (params->rollover) { + INFO2("Next roll-over: %"KNOT_TIME_PRINTF, next_sign.next_rollover); + if (next_sign.next_nsec3resalt) { + INFO2("Next NSEC3 re-salt: %"KNOT_TIME_PRINTF, next_sign.next_nsec3resalt); + } + if (next_sign.plan_ds_check) { + INFO2("KSK submission to parent zone needed"); + } + } + +fail: + if (fake_server.kaspdb.path != NULL) { + knot_lmdb_deinit(&fake_server.kaspdb); + } + zone_free(&zone_struct); + free(zonefile); + + return ret; +} + +int main(int argc, char *argv[]) +{ + sign_params_t params = { 0 }; + + struct option opts[] = { + { "config", required_argument, NULL, 'c' }, + { "confdb", required_argument, NULL, 'C' }, + { "outdir", required_argument, NULL, 'o' }, + { "rollover", no_argument, NULL, 'r' }, + { "verify" , no_argument, NULL, 'v' }, + { "time", required_argument, NULL, 't' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { NULL } + }; + + tzset(); + signal_init_std(); + + int opt = 0; + while ((opt = getopt_long(argc, argv, "c:C:o:rvt:hV", opts, NULL)) != -1) { + switch (opt) { + case 'c': + if (util_conf_init_file(optarg) != KNOT_EOK) { + goto failure; + } + break; + case 'C': + if (util_conf_init_confdb(optarg) != KNOT_EOK) { + goto failure; + } + break; + case 'o': + params.outdir = optarg; + break; + case 'r': + params.rollover = KEY_ROLL_ALLOW_ALL; + break; + case 'v': + params.verify = true; + break; + case 't': + ; uint32_t num = 0; + if (str_to_u32(optarg, &num) != KNOT_EOK || num == 0) { + print_help(); + goto failure; + } + params.timestamp = num; + break; + case 'h': + print_help(); + goto success; + case 'V': + print_version(PROGRAM_NAME); + goto success; + default: + print_help(); + goto failure; + } + } + if (argc - optind != 1) { + ERR2("missing zone name"); + print_help(); + goto failure; + } + params.zone_name_str = argv[optind]; + if (knot_dname_from_str(params.zone_name, params.zone_name_str, + sizeof(params.zone_name)) == NULL) { + ERR2("invalid zone name '%s'", params.zone_name_str); + print_help(); + goto failure; + } + knot_dname_to_lower(params.zone_name); + + if (util_conf_init_default(false) != KNOT_EOK) { + goto failure; + } + + if (zonesign(¶ms) != KNOT_EOK) { + goto failure; + } + +success: + util_conf_deinit(); + return EXIT_SUCCESS; +failure: + util_conf_deinit(); + return EXIT_FAILURE; +} |