dnsperf/src/dns.c
Daniel Baumann 510d3566ee
Merging upstream version 2.11.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-02-09 09:06:16 +01:00

546 lines
16 KiB
C

/*
* Copyright 2019-2023 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, at;
ssize_t max = 255;
const char* orig_str = str;
bool is_quoted;
if (perf_buffer_availablelength(target) < len) {
return PERF_R_NOSPACE;
}
while (len) {
is_quoted = false;
for (label_len = 0, at = 0; at < len;) {
if (*(str + at) == '\\') {
is_quoted = true;
at++;
if (at >= len)
return PERF_R_FAILURE;
if (*(str + at) >= '0' && *(str + at) <= '9') {
at++;
if (at >= len)
return PERF_R_FAILURE;
if (*(str + at) < '0' || *(str + at) > '9')
return PERF_R_FAILURE;
at++;
if (at >= len)
return PERF_R_FAILURE;
if (*(str + at) < '0' || *(str + at) > '9')
return PERF_R_FAILURE;
}
} else if (*(str + at) == '.') {
break;
}
label_len++;
at++;
}
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;
}
max -= label_len + 1;
if (max < 0) {
return PERF_R_FAILURE;
}
perf_buffer_putuint8(target, label_len);
if (is_quoted) {
for (at = 0; at < len; at++) {
if (*(str + at) == '\\') {
at++;
if (*(str + at) >= '0' && *(str + at) <= '9') {
char b[4];
long v;
memcpy(b, str + at, 3);
at += 2;
b[3] = 0;
v = strtol(b, 0, 7);
if (v < 0 || v > 255)
return PERF_R_FAILURE;
perf_buffer_putuint8(target, (uint8_t)v);
continue;
}
} else if (*(str + at) == '.') {
break;
}
perf_buffer_putmem(target, str + at, 1);
}
str += at;
len -= at;
} else {
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;
if (len > 4 && !strncasecmp(str, "TYPE", 4)) {
char* endptr = 0;
unsigned long int u = strtoul(str + 4, &endptr, 10);
if (endptr != str + len || u == ULONG_MAX || u > 65535) {
return PERF_R_FAILURE;
}
perf_buffer_putuint16(target, u);
return PERF_R_SUCCESS;
}
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;
}