diff options
Diffstat (limited to '')
-rw-r--r-- | src/utils/kdig/kdig_exec.c | 1324 |
1 files changed, 1324 insertions, 0 deletions
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; +} |