diff options
Diffstat (limited to 'src/dns.c')
-rw-r--r-- | src/dns.c | 484 |
1 files changed, 484 insertions, 0 deletions
diff --git a/src/dns.c b/src/dns.c new file mode 100644 index 0000000..548a1a9 --- /dev/null +++ b/src/dns.c @@ -0,0 +1,484 @@ +/* + * Copyright 2019-2021 OARC, Inc. + * Copyright 2017-2018 Akamai Technologies + * Copyright 2006-2016 Nominum, Inc. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "config.h" + +#include "dns.h" + +#include "log.h" +#include "opt.h" +#include "qtype.h" + +#include <ctype.h> +#include <time.h> +#include <inttypes.h> +#include <stdbool.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <limits.h> + +#ifdef HAVE_LDNS +#include <ldns/ldns.h> +#endif + +#define WHITESPACE " \t\n" + +#define MAX_RDATA_LENGTH 65535 +#define EDNSLEN 11 + +const char* perf_dns_rcode_strings[] = { + "NOERROR", "FORMERR", "SERVFAIL", "NXDOMAIN", + "NOTIMP", "REFUSED", "YXDOMAIN", "YXRRSET", + "NXRRSET", "NOTAUTH", "NOTZONE", "rcode11", + "rcode12", "rcode13", "rcode14", "rcode15" +}; + +perf_result_t perf_dname_fromstring(const char* str, size_t len, perf_buffer_t* target) +{ + size_t label_len; + const char* orig_str = str; + + if (perf_buffer_availablelength(target) < len) { + return PERF_R_NOSPACE; + } + + while (len) { + for (label_len = 0; label_len < len; label_len++) { + if (*(str + label_len) == '.') { + break; + } + } + if (!label_len) { + // Just a dot + if (len > 1) { + // a dot but with labels after it + return PERF_R_FAILURE; + } else if (str != orig_str) { + // a dot but with labels before it + return PERF_R_FAILURE; + } + perf_buffer_putuint8(target, 0); + break; + } + if (label_len > 63) { + return PERF_R_FAILURE; + } + perf_buffer_putuint8(target, label_len); + perf_buffer_putmem(target, str, label_len); + str += label_len; + len -= label_len; + if (len < 2) { + // Last label/dot + perf_buffer_putuint8(target, 0); + break; + } + // advance past dot + str++; + len--; + } + + return PERF_R_SUCCESS; +} + +perf_result_t perf_qtype_fromstring(const char* str, size_t len, perf_buffer_t* target) +{ + const perf_qtype_t* q = qtype_table; + + while (q->type) { + if (!strncasecmp(q->type, str, len)) { + perf_buffer_putuint16(target, q->value); + return PERF_R_SUCCESS; + } + q++; + } + + return PERF_R_FAILURE; +} + +static perf_result_t build_query(const perf_region_t* line, perf_buffer_t* msg) +{ + char * domain_str, *qtype_str; + size_t domain_len, qtype_len; + perf_result_t result; + + domain_str = line->base; + domain_len = strcspn(line->base, WHITESPACE); + + if (!domain_len) { + perf_log_warning("invalid query input format: %s", (char*)line->base); + return PERF_R_FAILURE; + } + + qtype_str = line->base + domain_len; + while (isspace(*qtype_str)) + qtype_str++; + qtype_len = strcspn(qtype_str, WHITESPACE); + + /* Create the question section */ + result = perf_dname_fromstring(domain_str, domain_len, msg); + if (result != PERF_R_SUCCESS) { + perf_log_warning("invalid domain name (or out of space): %.*s", (int)domain_len, domain_str); + return result; + } + + if (!qtype_len) { + perf_log_warning("invalid query input format: %s", (char*)line->base); + return PERF_R_FAILURE; + } + + result = perf_qtype_fromstring(qtype_str, qtype_len, msg); + if (result != PERF_R_SUCCESS) { + perf_log_warning("invalid qtype: %.*s", (int)qtype_len, qtype_str); + return result; + } + + perf_buffer_putuint16(msg, 1); // class IN + + return PERF_R_SUCCESS; +} + +#ifdef HAVE_LDNS +static bool token_equals(const perf_region_t* token, const char* str) +{ + return (strlen(str) == token->length && strncasecmp(str, token->base, token->length) == 0); +} + +/* + * Reads one line containing an individual update for a dynamic update message. + */ +static perf_result_t +read_update_line(char* str, const ldns_rdf* origin, + bool want_ttl, bool need_type, bool want_rdata, bool need_rdata, + ldns_rr** rr, const char** errstr) +{ + char tmp[256], *str2; + size_t len; + + while (isspace(*str & 0xff)) + str++; + str2 = str; + + /* + * Read the owner name + */ + len = strcspn(str, WHITESPACE); + if (len > sizeof(tmp) - 1) { + *errstr = "domain name too large"; + return PERF_R_NOSPACE; + } + memcpy(tmp, str, len); + tmp[len] = 0; + + ldns_rdf* owner; + if (!(owner = ldns_dname_new_frm_str(tmp))) { + *errstr = "invalid name or out of memory"; + return PERF_R_FAILURE; + } + ldns_rr_set_owner(*rr, owner); + if (!ldns_dname_str_absolute(tmp) && origin) { + if (ldns_dname_cat(ldns_rr_owner(*rr), origin) != LDNS_STATUS_OK) { + return PERF_R_FAILURE; + } + } + + str += len; + while (isspace(*str & 0xff)) + str++; + + /* + * Read the ttl + */ + if (want_ttl) { + len = strcspn(str, WHITESPACE); + if (len > sizeof(tmp) - 1) { + *errstr = "TTL string too large"; + return PERF_R_NOSPACE; + } + memcpy(tmp, str, len); + tmp[len] = 0; + + char* endptr = 0; + unsigned long int u = strtoul(tmp, &endptr, 10); + if (*endptr || u == ULONG_MAX) { + *errstr = "TTL invalid"; + return PERF_R_INVALIDUPDATE; + } + + ldns_rr_set_ttl(*rr, u); + + str += len; + while (isspace(*str & 0xff)) + str++; + } + + /* + * Read the type + */ + len = strcspn(str, WHITESPACE); + if (!len) { + if (!need_type) + return PERF_R_SUCCESS; + + *errstr = "TYPE required"; + return PERF_R_INVALIDUPDATE; + } + if (len > sizeof(tmp) - 1) { + *errstr = "TYPE string too large"; + return PERF_R_NOSPACE; + } + memcpy(tmp, str, len); + tmp[len] = 0; + + ldns_rr_type type = ldns_get_rr_type_by_name(tmp); + if (!type) { + *errstr = "TYPE invalid"; + return PERF_R_INVALIDUPDATE; + } + ldns_rr_set_type(*rr, type); + + str += len; + while (isspace(*str & 0xff)) + str++; + + if (!want_rdata) + return PERF_R_SUCCESS; + + /* + * Read the rdata + */ + if (*str == 0) { + if (!need_rdata) + return PERF_R_SUCCESS; + + *errstr = "RDATA required"; + return PERF_R_INVALIDUPDATE; + } + + // Need to recreate ldns_rr because there is no new_frm_str function to + // correctly parse RDATA (quotes etc) for a RDF + ldns_rr* rr2 = 0; + if (ldns_rr_new_frm_str(&rr2, str2, 0, origin, 0) != LDNS_STATUS_OK) { + *errstr = "invalid RDATA or out of memory"; + return PERF_R_INVALIDUPDATE; + } + + // Force set TTL since if its missing in the input it will get the default + // 3600 and not 0 as it should + ldns_rr_set_ttl(rr2, ldns_rr_ttl(*rr)); + + ldns_rr_free(*rr); + *rr = rr2; + + return PERF_R_SUCCESS; +} + +static void compression_free(ldns_rbnode_t* node, void* arg) +{ + (void)arg; + ldns_rdf_deep_free((ldns_rdf*)node->key); + LDNS_FREE(node); +} + +/* + * Reads a complete dynamic update message and sends it. + */ +static perf_result_t build_update(const perf_region_t* record, perf_buffer_t* msg) +{ + perf_region_t input, token; + char * msgbase, *str; + bool is_update; + int updates = 0; + int prereqs = 0; + perf_result_t result = PERF_R_FAILURE; + ldns_rdf* origin = 0; + ldns_rr* rr = 0; + ldns_buffer* lmsg = 0; + ldns_rbtree_t compression; + const char* errstr; + + input = *record; + msgbase = perf_buffer_base(msg); + ldns_rbtree_init(&compression, ldns_dname_compare_v); + + // Fill LDNS buffer with current message (DNS headers) + if (!(lmsg = ldns_buffer_new(perf_buffer_length(msg)))) { + perf_log_fatal("unable to create LDNS buffer for DNS message"); + goto done; // for scan-build / sonarcloud + } + ldns_buffer_write(lmsg, perf_buffer_base(msg), perf_buffer_usedlength(msg)); + + if (!(origin = ldns_rdf_new_frm_str(LDNS_RDF_TYPE_DNAME, input.base))) { + perf_log_warning("Unable to parse domain name %s", (char*)input.base); + goto done; + } + if (ldns_dname2buffer_wire_compress(lmsg, origin, &compression) != LDNS_STATUS_OK) { + perf_log_warning("Unable to write domain name %s to wire format", (char*)input.base); + goto done; + } + + ldns_buffer_write_u16(lmsg, 6); // SOA + ldns_buffer_write_u16(lmsg, 1); // IN + + while (true) { + input.base += strlen(input.base) + 1; + if (input.base >= record->base + record->length) { + perf_log_warning("incomplete update: %s", (char*)record->base); + result = PERF_R_FAILURE; + goto done; + } + + is_update = false; + token.base = input.base; + token.length = strcspn(token.base, WHITESPACE); + str = input.base + token.length; + errstr = 0; + if (token_equals(&token, "send")) { + break; + } + + rr = ldns_rr_new(); + ldns_rr_set_ttl(rr, 0); + ldns_rr_set_type(rr, LDNS_RR_TYPE_ANY); + ldns_rr_set_class(rr, LDNS_RR_CLASS_IN); + + if (token_equals(&token, "add")) { + result = read_update_line(str, origin, true, true, true, true, &rr, &errstr); + ldns_rr_set_class(rr, LDNS_RR_CLASS_IN); + is_update = true; + } else if (token_equals(&token, "delete")) { + result = read_update_line(str, origin, false, false, true, false, &rr, &errstr); + if (ldns_rr_rd_count(rr)) { + ldns_rr_set_class(rr, LDNS_RR_CLASS_NONE); + } else { + ldns_rr_set_class(rr, LDNS_RR_CLASS_ANY); + } + is_update = true; + } else if (token_equals(&token, "require")) { + result = read_update_line(str, origin, false, false, true, false, &rr, &errstr); + if (ldns_rr_rd_count(rr)) { + ldns_rr_set_class(rr, LDNS_RR_CLASS_IN); + } else { + ldns_rr_set_class(rr, LDNS_RR_CLASS_ANY); + } + is_update = false; + } else if (token_equals(&token, "prohibit")) { + result = read_update_line(str, origin, false, false, false, false, &rr, &errstr); + ldns_rr_set_class(rr, LDNS_RR_CLASS_NONE); + is_update = false; + } else { + perf_log_warning("invalid update command: %s", (char*)input.base); + result = PERF_R_FAILURE; + } + + if (result != PERF_R_SUCCESS) { + if (errstr) { + perf_log_warning("invalid update command, %s: %s", errstr, (char*)input.base); + } else if (result == PERF_R_INVALIDUPDATE) { + perf_log_warning("invalid update command: %s", (char*)input.base); + } else { + perf_log_warning("error processing update command: %s", (char*)input.base); + } + ldns_rr_free(rr); + goto done; + } + + if (!is_update && updates > 0) { + perf_log_warning("prereqs must precede updates"); + result = PERF_R_FAILURE; + ldns_rr_free(rr); + goto done; + } + + if (ldns_rr2buffer_wire_compress(lmsg, rr, LDNS_SECTION_ANSWER, &compression) != LDNS_STATUS_OK) { + perf_log_warning("Unable to write update message to wire format"); + ldns_rr_free(rr); + goto done; + } + ldns_rr_free(rr); + + if (is_update) + updates++; + else + prereqs++; + } + + if (ldns_buffer_position(lmsg) - perf_buffer_usedlength(msg) > perf_buffer_availablelength(msg)) { + perf_log_warning("out of space in message buffer"); + result = PERF_R_NOSPACE; + goto done; + } + uint8_t* p = ldns_buffer_begin(lmsg) + perf_buffer_usedlength(msg); + perf_buffer_putmem(msg, p, ldns_buffer_position(lmsg) - perf_buffer_usedlength(msg)); + + msgbase[7] = prereqs; /* ANCOUNT = number of prereqs */ + msgbase[9] = updates; /* AUCOUNT = number of updates */ + + result = PERF_R_SUCCESS; + +done: + ldns_buffer_free(lmsg); + ldns_rdf_deep_free(origin); + ldns_traverse_postorder(&compression, compression_free, 0); + + return result; +} +#endif + +perf_result_t perf_dns_buildrequest(const perf_region_t* record, uint16_t qid, + bool edns, bool dnssec, bool is_update, + perf_tsigkey_t* tsigkey, perf_ednsoption_t* edns_option, + perf_buffer_t* msg) +{ + unsigned int flags; + perf_result_t result; + + if (is_update) + flags = 5 << 11; // opcode UPDATE + else + flags = 0x0100U; // flag RD + + /* Create the DNS packet header */ + perf_buffer_putuint16(msg, qid); + perf_buffer_putuint16(msg, flags); /* flags */ + perf_buffer_putuint16(msg, 1); /* qdcount */ + perf_buffer_putuint16(msg, 0); /* ancount */ + perf_buffer_putuint16(msg, 0); /* aucount */ + perf_buffer_putuint16(msg, 0); /* arcount */ + + if (is_update) { +#ifdef HAVE_LDNS + result = build_update(record, msg); +#else + result = PERF_R_FAILURE; +#endif + } else { + result = build_query(record, msg); + } + + if (result == PERF_R_SUCCESS && edns) { + result = perf_add_edns(msg, dnssec, edns_option); + } + + if (result == PERF_R_SUCCESS && tsigkey) { + result = perf_add_tsig(msg, tsigkey); + } + + return result; +} |