diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2021-03-04 18:39:15 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2021-03-04 18:39:15 +0000 |
commit | 513a07abc0a06eea6bea417e20b3d4bc3a0d546f (patch) | |
tree | 258198120db2d799da09fdcf1f60f260fa2942a2 /src | |
parent | Initial commit. (diff) | |
download | dnsperf-513a07abc0a06eea6bea417e20b3d4bc3a0d546f.tar.xz dnsperf-513a07abc0a06eea6bea417e20b3d4bc3a0d546f.zip |
Adding upstream version 2.4.2+debian.upstream/2.4.2+debian
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src')
-rw-r--r-- | src/Makefile.am | 66 | ||||
-rw-r--r-- | src/buffer.h | 116 | ||||
-rw-r--r-- | src/datafile.c | 246 | ||||
-rw-r--r-- | src/datafile.h | 52 | ||||
-rw-r--r-- | src/dns.c | 484 | ||||
-rw-r--r-- | src/dns.h | 43 | ||||
-rw-r--r-- | src/dnsperf.1.in | 366 | ||||
-rw-r--r-- | src/dnsperf.c | 1220 | ||||
-rw-r--r-- | src/edns.c | 158 | ||||
-rw-r--r-- | src/edns.h | 41 | ||||
-rwxr-xr-x | src/gen-qtype.c.py | 46 | ||||
-rw-r--r-- | src/list.h | 91 | ||||
-rw-r--r-- | src/log.c | 72 | ||||
-rw-r--r-- | src/log.h | 28 | ||||
-rw-r--r-- | src/net.c | 664 | ||||
-rw-r--r-- | src/net.h | 84 | ||||
-rw-r--r-- | src/opt.c | 234 | ||||
-rw-r--r-- | src/opt.h | 39 | ||||
-rw-r--r-- | src/os.c | 150 | ||||
-rw-r--r-- | src/os.h | 44 | ||||
-rw-r--r-- | src/qtype.c | 117 | ||||
-rw-r--r-- | src/qtype.h | 32 | ||||
-rwxr-xr-x | src/resperf-report | 112 | ||||
-rw-r--r-- | src/resperf.1.in | 647 | ||||
-rw-r--r-- | src/resperf.c | 816 | ||||
-rw-r--r-- | src/result.h | 38 | ||||
-rw-r--r-- | src/strerror.c | 37 | ||||
-rw-r--r-- | src/strerror.h | 27 | ||||
-rw-r--r-- | src/test/Makefile.am | 9 | ||||
-rw-r--r-- | src/test/datafile | 2 | ||||
-rw-r--r-- | src/test/datafile2 | 560 | ||||
-rw-r--r-- | src/test/datafile3 | 2 | ||||
-rwxr-xr-x | src/test/test1.sh | 4 | ||||
-rwxr-xr-x | src/test/test2.sh | 108 | ||||
-rwxr-xr-x | src/test/test3.sh | 46 | ||||
-rw-r--r-- | src/test/updatefile | 11 | ||||
-rw-r--r-- | src/tsig.c | 326 | ||||
-rw-r--r-- | src/tsig.h | 41 | ||||
-rw-r--r-- | src/util.h | 146 |
39 files changed, 7325 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..35d10a3 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,66 @@ +# 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. + +MAINTAINERCLEANFILES = $(srcdir)/Makefile.in $(srcdir)/config.h.in +CLEANFILES = dnsperf.1 resperf.1 *.gcda *.gcno *.gcov + +SUBDIRS = test + +AM_CFLAGS = -I$(srcdir) \ + -I$(top_srcdir) \ + $(PTHREAD_CFLAGS) $(libssl_CFLAGS) $(libcrypto_CFLAGS) $(libldns_CFLAGS) + +EXTRA_DIST = dnsperf.1.in resperf-report resperf.1.in + +bin_PROGRAMS = dnsperf resperf +dist_bin_SCRIPTS = resperf-report + +_libperf_sources = datafile.c dns.c log.c net.c opt.c os.c strerror.c qtype.c \ + edns.c tsig.c +_libperf_headers = datafile.h dns.h log.h net.h opt.h os.h util.h strerror.h \ + list.h result.h buffer.h qtype.h edns.h tsig.h + +dnsperf_SOURCES = $(_libperf_sources) dnsperf.c +dist_dnsperf_SOURCES = $(_libperf_headers) +dnsperf_LDADD = $(PTHREAD_LIBS) $(libssl_LIBS) $(libcrypto_LIBS) \ + $(libldns_LIBS) + +resperf_SOURCES = $(_libperf_sources) resperf.c +dist_resperf_SOURCES = $(_libperf_headers) +resperf_LDADD = $(PTHREAD_LIBS) $(libssl_LIBS) $(libcrypto_LIBS) \ + $(libldns_LIBS) + +man1_MANS = dnsperf.1 resperf.1 + +dnsperf.1: dnsperf.1.in Makefile + sed -e 's,[@]PACKAGE_VERSION[@],$(PACKAGE_VERSION),g' \ +-e 's,[@]PACKAGE_URL[@],$(PACKAGE_URL),g' \ +-e 's,[@]PACKAGE_BUGREPORT[@],$(PACKAGE_BUGREPORT),g' \ +< $(srcdir)/dnsperf.1.in > dnsperf.1 + +resperf.1: resperf.1.in Makefile + sed -e 's,[@]PACKAGE_VERSION[@],$(PACKAGE_VERSION),g' \ +-e 's,[@]PACKAGE_URL[@],$(PACKAGE_URL),g' \ +-e 's,[@]PACKAGE_BUGREPORT[@],$(PACKAGE_BUGREPORT),g' \ +< $(srcdir)/resperf.1.in > resperf.1 + +if ENABLE_GCOV +gcov-local: + for src in $(_libperf_sources) dnsperf.c resperf.c; do \ + gcov -l -r -s "$(srcdir)" "$$src"; \ + done +endif diff --git a/src/buffer.h b/src/buffer.h new file mode 100644 index 0000000..ab46f9b --- /dev/null +++ b/src/buffer.h @@ -0,0 +1,116 @@ +/* + * 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. + */ + +#ifndef PERF_BUFFER_H +#define PERF_BUFFER_H 1 + +#include <stddef.h> + +typedef struct perf_region { + void* base; + size_t length; +} perf_region_t; + +typedef struct perf_buffer { + void* base; + size_t length, used, current, active; +} perf_buffer_t; + +#include <assert.h> + +#define perf_buffer_init(b, _base, _length) \ + { \ + (b)->base = _base; \ + (b)->length = _length; \ + (b)->used = 0; \ + (b)->current = 0; \ + (b)->active = 0; \ + } + +#define perf_buffer_add(b, n) \ + { \ + assert((b)->used + n <= (b)->length); \ + (b)->used += (n); \ + } + +#define perf_buffer_length(b) ((b)->length) + +#define perf_buffer_availablelength(b) ((b)->length - (b)->used) + +#define perf_buffer_base(b) ((b)->base) + +#define perf_buffer_clear(b) \ + { \ + (b)->used = 0; \ + (b)->current = 0; \ + (b)->active = 0; \ + } + +#define perf_buffer_putmem(b, base, length) \ + { \ + assert(perf_buffer_availablelength(b) >= length); \ + memcpy(perf_buffer_used(b), base, length); \ + perf_buffer_add(b, length); \ + } + +#define perf_buffer_putuint8(b, _val) \ + { \ + unsigned char* _cp; \ + uint8_t _val2 = (_val); \ + assert(perf_buffer_availablelength(b) >= 1U); \ + _cp = perf_buffer_used(b); \ + (b)->used += 1U; \ + _cp[0] = _val2; \ + } + +#define perf_buffer_putuint16(b, _val) \ + { \ + unsigned char* _cp; \ + uint16_t _val2 = (_val); \ + assert(perf_buffer_availablelength(b) >= 2U); \ + _cp = perf_buffer_used(b); \ + (b)->used += 2U; \ + _cp[0] = _val2 >> 8; \ + _cp[1] = _val2; \ + } + +#define perf_buffer_putuint32(b, _val) \ + { \ + unsigned char* _cp; \ + uint32_t _val2 = (_val); \ + assert(perf_buffer_availablelength(b) >= 4U); \ + _cp = perf_buffer_used(b); \ + (b)->used += 4U; \ + _cp[0] = _val2 >> 24; \ + _cp[1] = _val2 >> 16; \ + _cp[2] = _val2 >> 8; \ + _cp[3] = _val2; \ + } + +#define perf_buffer_copyregion(b, r) perf_buffer_putmem(b, (r)->base, (r)->length) + +#define perf_buffer_used(b) ((void*)((unsigned char*)(b)->base + (b)->used)) +#define perf_buffer_usedlength(b) ((b)->used) +#define perf_buffer_usedregion(b, r) \ + { \ + (r)->base = (b)->base; \ + (r)->length = (b)->used; \ + } + +#endif diff --git a/src/datafile.c b/src/datafile.c new file mode 100644 index 0000000..da2acac --- /dev/null +++ b/src/datafile.c @@ -0,0 +1,246 @@ +/* + * 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 "datafile.h" + +#include "log.h" +#include "os.h" +#include "util.h" + +#include <fcntl.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/stat.h> +#include <assert.h> + +perf_datafile_t* perf_datafile_open(const char* filename) +{ + perf_datafile_t* dfile; + struct stat buf; + + dfile = calloc(1, sizeof(perf_datafile_t)); + if (!dfile) { + perf_log_fatal("out of memory"); + return 0; // fix clang scan-build + } + + PERF_MUTEX_INIT(&dfile->lock); + dfile->pipe_fd = -1; + dfile->is_file = false; + dfile->size = 0; + dfile->cached = false; + dfile->maxruns = 1; + dfile->nruns = 0; + dfile->read_any = false; + if (!filename) { + dfile->fd = STDIN_FILENO; + } else { + dfile->fd = open(filename, O_RDONLY); + if (dfile->fd < 0) + perf_log_fatal("unable to open file: %s", filename); + if (fstat(dfile->fd, &buf) == 0 && S_ISREG(buf.st_mode)) { + dfile->is_file = true; + dfile->size = buf.st_size; + } + } + + return dfile; +} + +void perf_datafile_close(perf_datafile_t** dfilep) +{ + perf_datafile_t* dfile; + + assert(dfilep); + assert(*dfilep); + + dfile = *dfilep; + *dfilep = 0; + + if (dfile->fd >= 0 && dfile->fd != STDIN_FILENO) { + close(dfile->fd); + } + PERF_MUTEX_DESTROY(&dfile->lock); + free(dfile); +} + +void perf_datafile_setpipefd(perf_datafile_t* dfile, int pipe_fd) +{ + dfile->pipe_fd = pipe_fd; +} + +void perf_datafile_setmaxruns(perf_datafile_t* dfile, unsigned int maxruns) +{ + dfile->maxruns = maxruns; +} + +static void reopen_file(perf_datafile_t* dfile) +{ + if (dfile->cached) { + dfile->at = 0; + } else { + if (lseek(dfile->fd, 0L, SEEK_SET) < 0) { + perf_log_fatal("cannot reread input"); + } + dfile->at = 0; + dfile->have = 0; + dfile->databuf[0] = 0; + } +} + +static perf_result_t read_more(perf_datafile_t* dfile) +{ + ssize_t n; + perf_result_t result; + struct perf_net_socket sock = { .mode = sock_file, .fd = dfile->fd }; + + if (!dfile->is_file && dfile->pipe_fd >= 0) { + result = perf_os_waituntilreadable(&sock, dfile->pipe_fd, -1); + if (result != PERF_R_SUCCESS) + return (result); + } + + if (dfile->at && dfile->at < dfile->have) { + memmove(dfile->databuf, &dfile->databuf[dfile->at], dfile->have - dfile->at); + dfile->have -= dfile->at; + dfile->at = 0; + } else if (dfile->at == dfile->have) { + dfile->have = 0; + dfile->at = 0; + } + + n = read(dfile->fd, &dfile->databuf[dfile->have], sizeof(dfile->databuf) - dfile->have - 1); + if (n < 0) { + return (PERF_R_FAILURE); + } + + dfile->have += n; + dfile->databuf[dfile->have] = 0; + + if (dfile->is_file && dfile->have == dfile->size) { + dfile->cached = true; + } + + return (PERF_R_SUCCESS); +} + +static perf_result_t read_one_line(perf_datafile_t* dfile, perf_buffer_t* lines) +{ + const char* cur; + size_t length, curlen, nrem; + perf_result_t result; + + while (true) { + /* Get the current line */ + cur = &dfile->databuf[dfile->at]; + curlen = strcspn(cur, "\n"); + + /* + * If the current line contains the rest of the buffer, + * we need to read more (unless the full file is cached). + */ + nrem = dfile->have - dfile->at; + if (curlen == nrem) { + if (!dfile->cached) { + result = read_more(dfile); + if (result != PERF_R_SUCCESS) + return (result); + } + if (dfile->have - dfile->at == 0) { + dfile->nruns++; + return (PERF_R_EOF); + } + if (dfile->have - dfile->at > nrem) + continue; + } + + /* We now have a line. Advance the buffer past it. */ + dfile->at += curlen; + if (dfile->have - dfile->at > 0) { + dfile->at += 1; + } + + /* If the line is empty or a comment, we need to try again. */ + if (curlen > 0 && cur[0] != ';') + break; + } + + length = perf_buffer_availablelength(lines); + if (curlen > length - 1) + curlen = length - 1; + perf_buffer_putmem(lines, (unsigned char*)cur, curlen); + perf_buffer_putuint8(lines, 0); + + return (PERF_R_SUCCESS); +} + +perf_result_t perf_datafile_next(perf_datafile_t* dfile, perf_buffer_t* lines, bool is_update) +{ + const char* current; + perf_result_t result; + + PERF_LOCK(&dfile->lock); + + if (dfile->maxruns > 0 && dfile->maxruns == dfile->nruns) { + result = PERF_R_EOF; + goto done; + } + + result = read_one_line(dfile, lines); + if (result == PERF_R_EOF) { + if (!dfile->read_any) { + result = PERF_R_INVALIDFILE; + goto done; + } + if (dfile->maxruns != dfile->nruns) { + reopen_file(dfile); + result = read_one_line(dfile, lines); + } + } + if (result != PERF_R_SUCCESS) { + goto done; + } + dfile->read_any = true; + + if (is_update) { + while (true) { + current = perf_buffer_used(lines); + result = read_one_line(dfile, lines); + if (result == PERF_R_EOF && dfile->maxruns != dfile->nruns) { + reopen_file(dfile); + } + if (result != PERF_R_SUCCESS || strcasecmp(current, "send") == 0) + break; + } + } + + result = PERF_R_SUCCESS; +done: + PERF_UNLOCK(&dfile->lock); + return (result); +} + +unsigned int perf_datafile_nruns(const perf_datafile_t* dfile) +{ + return dfile->nruns; +} diff --git a/src/datafile.h b/src/datafile.h new file mode 100644 index 0000000..7b6caf3 --- /dev/null +++ b/src/datafile.h @@ -0,0 +1,52 @@ +/* + * 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 "result.h" +#include "buffer.h" + +#ifndef PERF_DATAFILE_H +#define PERF_DATAFILE_H 1 + +#include <pthread.h> +#include <stdbool.h> + +typedef struct perf_datafile { + pthread_mutex_t lock; + int pipe_fd; + int fd; + bool is_file; + size_t size, at, have; + bool cached; + char databuf[(64 * 1024) + 1]; + unsigned int maxruns; + unsigned int nruns; + bool read_any; +} perf_datafile_t; + +perf_datafile_t* perf_datafile_open(const char* filename); + +void perf_datafile_close(perf_datafile_t** dfilep); +void perf_datafile_setmaxruns(perf_datafile_t* dfile, unsigned int maxruns); +void perf_datafile_setpipefd(perf_datafile_t* dfile, int pipe_fd); + +perf_result_t perf_datafile_next(perf_datafile_t* dfile, perf_buffer_t* lines, bool is_update); + +unsigned int perf_datafile_nruns(const perf_datafile_t* dfile); + +#endif 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; +} diff --git a/src/dns.h b/src/dns.h new file mode 100644 index 0000000..cb8cb49 --- /dev/null +++ b/src/dns.h @@ -0,0 +1,43 @@ +/* + * 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 "result.h" +#include "buffer.h" +#include "edns.h" +#include "tsig.h" + +#ifndef PERF_DNS_H +#define PERF_DNS_H 1 + +#include <stdint.h> +#include <stdbool.h> + +#define MAX_UDP_PACKET 512 + +extern const char* perf_dns_rcode_strings[]; + +perf_result_t perf_dname_fromstring(const char* str, size_t len, perf_buffer_t* target); +perf_result_t perf_qtype_fromstring(const char* str, size_t len, perf_buffer_t* target); + +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); + +#endif diff --git a/src/dnsperf.1.in b/src/dnsperf.1.in new file mode 100644 index 0000000..f6def3b --- /dev/null +++ b/src/dnsperf.1.in @@ -0,0 +1,366 @@ +.\" 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. +.TH dnsperf 1 "@PACKAGE_VERSION@" "dnsperf" +.SH NAME +dnsperf \- test the performance of a DNS server +.SH SYNOPSIS +.hy 0 +.ad l +\fBdnsperf\fR\ [\fB\-a\ \fIlocal_addr\fB\fR] +[\fB\-b\ \fIbufsize\fB\fR] +[\fB\-c\ \fIclients\fB\fR] +[\fB\-d\ \fIdatafile\fB\fR] +[\fB\-D\fR] +[\fB\-e\fR] +[\fB\-E\ \fIcode:secret\fB\fR] +[\fB\-f\ \fIfamily\fB\fR] +[\fB\-h\fR] +[\fB\-l\ \fIlimit\fB\fR] +[\fB\-m\ \fImode\fB\fR] +[\fB\-n\ \fIruns_through_file\fB\fR] +[\fB\-p\ \fIport\fB\fR] +[\fB\-q\ \fInum_queries\fB\fR] +[\fB\-Q\ \fImax_qps\fB\fR] +[\fB\-s\ \fIserver_addr\fB\fR] +[\fB\-S\ \fIstats_interval\fB\fR] +[\fB\-t\ \fItimeout\fB\fR] +[\fB\-T\ \fIthreads\fB\fR] +[\fB\-u\fR] +[\fB\-v\fR] +[\fB\-x\ \fIlocal_port\fB\fR] +[\fB\-y\ \fI[alg:]name:secret\fB\fR] +.ad +.hy +.SH DESCRIPTION +\fBdnsperf\fR is a DNS server performance testing tool. It is primarily +intended for measuring the performance of authoritative DNS servers, but it +can also be used for measuring caching server performance in a closed +laboratory environment. For testing caching servers resolving against the +live Internet, the \fBresperf\fR program is preferred. + +It is recommended that \fBdnsperf\fR and the name server under test be run +on separate machines, so that the CPU usage of \fBdnsperf\fR itself does not +slow down the name server. The two machines should be connected with a fast +network, preferably a dedicated Gigabit Ethernet segment. Testing through a +router or firewall is not advisable. +.SS "Configuring the name server" +If using \fBdnsperf\fR to test an authoritative server, the name server +under test should be set up to serve one or more zones similar in size and +number to what the server is expected to serve in production. + +Also, be sure to turn off recursion in the server's configuration (in BIND +8/9, specify "recursion no;" in the options block). In BIND 8, you should +also specify "fetch-glue no;"; otherwise the server may attempt to retrieve +glue information from the Internet during the test, slowing it down by an +unpredictable factor. +.SS "Constructing a query input file" +A \fBdnsperf\fR input file should contain a large and realistic set of +queries, on the order of ten thousand to a million. The input file contains +one line per query, consisting of a domain name and an RR type name +separated by a space. The class of the query is implicitly IN. + +When measuring the performance serving non-terminal zones such as the root +zone or TLDs, note that such servers spend most of their time providing +referral responses, not authoritative answers. Therefore, a realistic input +file might consist mostly of queries for type A for names *below*, not at, +the delegations present in the zone. For example, when testing the +performance of a server configured to be authoritative for the top-level +domain "fi.", which contains delegations for domains like "helsinki.fi" and +"turku.fi", the input file could contain lines like +.RS +.hy 0 + +.nf +www.turku.fi A +www.helsinki.fi A +.fi +.hy +.RE + +where the "www" prefix ensures that the server will respond with a referral. +Ideally, a realistic proportion of queries for nonexistent domains should be +mixed in with those for existing ones, and the lines of the input file +should be in a random order. +.SS "Constructing a dynamic update input file" +To test dynamic update performance, \fBdnsperf\fR is run with the \fB\-u\fR +option, and the input file is constructed of blocks of lines describing +dynamic update messages. The first line in a block contains the zone name: +.RS +.hy 0 + +.nf +example.com +.fi +.hy +.RE + +Subsequent lines contain prerequisites, if there are any. Prerequisites can +specify that a name may or may not exist, an rrset may or may not exist, or +an rrset exists and its rdata matches all specified rdata for that name and +type. The keywords "require" and "prohibit" are followed by the appropriate +information. All relative names are considered to be relative to the zone +name. The following lines show the 5 types of prerequisites. +.RS +.hy 0 + +.nf +require a +require a A +require a A 1.2.3.4 +prohibit x +prohibit x A +.fi +.hy +.RE + +Subsequent lines contain records to be added, records to be deleted, rrsets +to be deleted, or names to be deleted. The keywords "add" or "delete" are +followed by the appropriate information. All relative names are considered +to be relative to the zone name. The following lines show the 4 types of +updates. +.RS +.hy 0 + +.nf +add x 3600 A 10.1.2.3 +delete y A 10.1.2.3 +delete z A +delete w +.fi +.hy +.RE + +Each update message is terminated by a line containing the command: +.RS +.hy 0 + +.nf +send +.fi +.hy +.RE +.SS "Running the tests" +When running \fBdnsperf\fR, a data file (the \fB\-d\fR option) and server +(the \fB\-s\fR option) will normally be specified. The output of dnsperf is +mostly self-explanatory. Pay attention to the number of dropped packets +reported - when running the test over a local Ethernet connection, it should +be zero. If one or more packets has been dropped, there may be a problem +with the network connection. In that case, the results should be considered +suspect and the test repeated. +.SH OPTIONS + +\fB-a \fIlocal_addr\fB\fR +.br +.RS +Specifies the local address from which to send requests. The default is the +wildcard address. +.RE + +\fB-b \fIbufsize\fB\fR +.br +.RS +Sets the size of the socket's send and receive buffers, in kilobytes. If not +specified, the operating system's default is used. +.RE + +\fB-c \fIclients\fB\fR +.br +.RS +Act as multiple clients. Requests are sent from multiple sockets. The +default is to act as 1 client. +.RE + +\fB-d \fIdatafile\fB\fR +.br +.RS +Specifies the input data file. If not specified, \fBdnsperf\fR will read +from standard input. +.RE + +\fB-D\fR +.br +.RS +Sets the DO (DNSSEC OK) bit [RFC3225] in all packets sent. This also enables +EDNS0, which is required for DNSSEC. +.RE + +\fB-e\fR +.br +.RS +Enables EDNS0 [RFC2671], by adding an OPT record to all packets sent. +.RE + +\fB-E \fIcode:value\fB\fR +.br +.RS +Add an EDNS [RFC2671] option to all packets sent, using the specified +numeric option code and value expressed as a a hex-encoded string. This also +enables EDNS0. +.RE + +\fB-f \fIfamily\fB\fR +.br +.RS +Specifies the address family used for sending DNS packets. The possible +values are "inet", "inet6", or "any". If "any" (the default value) is +specified, \fBdnsperf\fR will use whichever address family is appropriate +for the server it is sending packets to. +.RE + +\fB-h\fR +.br +.RS +Print a usage statement and exit. +.RE + +\fB-l \fIlimit\fB\fR +.br +.RS +Specifies a time limit for the run, in seconds. This may cause the input to +be read multiple times, or only some of the input to be read. The default +behavior is to read the input once, and have no specific time limit. +.RE + +\fB-n \fIruns_through_file\fB\fR +.br +.RS +Run through the input file at most this many times. If no time limit is set, +the file will be read exactly this number of times; if a time limit is set, +the file may be read fewer times. +.RE + +\fB-p \fIport\fB\fR +.br +.RS +Sets the port on which the DNS packets are sent. If not specified, the +standard DNS port (udp/tcp 53, dot/tls 853) is used. +.RE + +\fB-q \fInum_queries\fB\fR +.br +.RS +Sets the maximum number of outstanding requests. When this value is reached, +\fBdnsperf\fR will not send any more requests until either responses are +received or requests time out. The default value is 100. +.RE + +\fB-Q \fImax_qps\fB\fR +.br +.RS +Limits the number of requests per second. There is no default limit. +.RE + +\fB-m \fImode\fB\fR +.br +.RS +Specifies the transport mode to use, "udp", "tcp" or "dot"/"tls". +Default is "udp". +.RE + +\fB-s \fIserver_addr\fB\fR +.br +.RS +Specifies the name or address of the server to which requests will be sent. +The default is the loopback address, 127.0.0.1. +.RE + +\fB-S \fIstats_interval\fB\fR +.br +.RS +If this parameter is specified, a count of the number of queries per second +during the interval will be printed out every stats_interval seconds. +.RE + +\fB-t \fItimeout\fB\fR +.br +.RS +Specifies the request timeout value, in seconds. \fBdnsperf\fR will no +longer wait for a response to a particular request after this many seconds +have elapsed. The default is 5 seconds. +.RE + +\fB-T \fIthreads\fB\fR +.br +.RS +Run multiple client threads. By default, \fBdnsperf\fR uses one thread for +sending requests and one thread for receiving responses. If this option is +specified, \fBdnsperf\fR will instead use N pairs of send/receive threads. +.RE + +\fB-u\fR +.br +.RS +Instructs \fBdnsperf\fR to send DNS dynamic update messages, rather than +queries. The format of the input file is different in this case; see the +"Constructing a dynamic update input file" section for more details. +.RE + +\fB-v\fR +.br +.RS +Enables verbose mode. The DNS RCODE of each response will be reported to +standard output when the response is received, as will the latency. If a +query times out, it will be reported with the special string "T" instead of +a normal DNS RCODE. If a query is interrupted, it will be reported with the +special string "I". Additional information regarding network readiness and +congestion will also be reported. +.RE + +\fB-x \fIlocal_port\fB\fR +.br +.RS +Specifies the local port from which to send requests. The default is the +wildcard port (0). + +If acting as multiple clients and the wildcard port is used, each client +will use a different random port. If a port is specified, the clients will +use a range of ports starting with the specified one. +.RE + +\fB-y \fI[alg:]name:secret\fB\fR +.br +.RS +Add a TSIG record [RFC2845] to all packets sent, using the specified TSIG +key algorithm, name and secret, where the algorithm defaults to hmac-md5 and +the secret is expressed as a base-64 encoded string. +Available algorithms are: hmac-md5, hmac-sha1, hmac-sha224, hmac-sha256, +hmac-sha384 and hmac-sha512. +.RE +.SH "SEE ALSO" +\fBresperf\fR(1) +.SH AUTHOR +Nominum, Inc. +.LP +Maintained by DNS-OARC +.LP +.RS +.I https://www.dns-oarc.net/ +.RE +.LP +.SH BUGS +For issues and feature requests please use: +.LP +.RS +\fI@PACKAGE_URL@\fP +.RE +.LP +For question and help please use: +.LP +.RS +\fI@PACKAGE_BUGREPORT@\fP +.RE +.LP diff --git a/src/dnsperf.c b/src/dnsperf.c new file mode 100644 index 0000000..f001370 --- /dev/null +++ b/src/dnsperf.c @@ -0,0 +1,1220 @@ +/* + * 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. + */ + +/*** + *** DNS Performance Testing Tool + ***/ + +#include "config.h" + +#include "net.h" +#include "datafile.h" +#include "dns.h" +#include "log.h" +#include "opt.h" +#include "os.h" +#include "util.h" +#include "list.h" +#include "buffer.h" + +#include <inttypes.h> +#include <errno.h> +#include <math.h> +#include <pthread.h> +#include <signal.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <sys/time.h> +#include <openssl/ssl.h> +#include <openssl/conf.h> +#include <openssl/err.h> + +#define DEFAULT_SERVER_NAME "127.0.0.1" +#define DEFAULT_SERVER_PORT 53 +#define DEFAULT_SERVER_TLS_PORT 853 +#define DEFAULT_SERVER_PORTS "udp/tcp 53 or dot/tls 853" +#define DEFAULT_LOCAL_PORT 0 +#define DEFAULT_MAX_OUTSTANDING 100 +#define DEFAULT_TIMEOUT 5 + +#define TIMEOUT_CHECK_TIME 100000 + +#define MAX_INPUT_DATA (64 * 1024) + +#define MAX_SOCKETS 256 + +#define RECV_BATCH_SIZE 16 + +typedef struct { + int argc; + char** argv; + int family; + uint32_t clients; + uint32_t threads; + uint32_t maxruns; + uint64_t timelimit; + perf_sockaddr_t server_addr; + perf_sockaddr_t local_addr; + uint64_t timeout; + uint32_t bufsize; + bool edns; + bool dnssec; + perf_tsigkey_t* tsigkey; + perf_ednsoption_t* edns_option; + uint32_t max_outstanding; + uint32_t max_qps; + uint64_t stats_interval; + bool updates; + bool verbose; + enum perf_net_mode mode; +} config_t; + +typedef struct { + uint64_t start_time; + uint64_t end_time; + uint64_t stop_time; + struct timespec stop_time_ns; +} times_t; + +typedef struct { + uint64_t rcodecounts[16]; + + uint64_t num_sent; + uint64_t num_interrupted; + uint64_t num_timedout; + uint64_t num_completed; + + uint64_t total_request_size; + uint64_t total_response_size; + + uint64_t latency_sum; + uint64_t latency_sum_squares; + uint64_t latency_min; + uint64_t latency_max; +} stats_t; + +typedef perf_list(struct query_info) query_list; + +typedef struct query_info { + uint64_t timestamp; + query_list* list; + char* desc; + struct perf_net_socket* sock; + /* + * This link links the query into the list of outstanding + * queries or the list of available query IDs. + */ + perf_link(struct query_info); +} query_info; + +#define NQIDS 65536 + +typedef struct { + query_info queries[NQIDS]; + query_list outstanding_queries; + query_list unused_queries; + + pthread_t sender; + pthread_t receiver; + + pthread_mutex_t lock; + pthread_cond_t cond; + + unsigned int nsocks; + int current_sock; + struct perf_net_socket* socks; + + bool done_sending; + uint64_t done_send_time; + + const config_t* config; + const times_t* times; + stats_t stats; + + uint32_t max_outstanding; + uint32_t max_qps; + + uint64_t last_recv; +} threadinfo_t; + +static threadinfo_t* threads; + +static pthread_mutex_t start_lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t start_cond = PTHREAD_COND_INITIALIZER; +static bool started; + +static bool interrupted = false; + +static int threadpipe[2]; +static int mainpipe[2]; +static int intrpipe[2]; + +static perf_datafile_t* input; + +const char* progname = "dnsperf"; + +static void +handle_sigint(int sig) +{ + (void)sig; + if (write(intrpipe[1], "", 1)) { // lgtm [cpp/empty-block] + } +} + +static void +print_initial_status(const config_t* config) +{ + time_t now; + char buf[255], ct[32]; + int i; + + printf("[Status] Command line: %s", progname); + for (i = 1; i < config->argc; i++) + printf(" %s", config->argv[i]); + printf("\n"); + + perf_sockaddr_format(&config->server_addr, buf, sizeof(buf)); + printf("[Status] Sending %s (to %s)\n", + config->updates ? "updates" : "queries", buf); + + now = time(NULL); + printf("[Status] Started at: %s", ctime_r(&now, ct)); + + printf("[Status] Stopping after "); + if (config->timelimit) + printf("%u.%06u seconds", + (unsigned int)(config->timelimit / MILLION), + (unsigned int)(config->timelimit % MILLION)); + if (config->timelimit && config->maxruns) + printf(" or "); + if (config->maxruns) + printf("%u run%s through file", config->maxruns, + config->maxruns == 1 ? "" : "s"); + printf("\n"); +} + +static void +print_final_status(const config_t* config) +{ + const char* reason; + + if (interrupted) + reason = "interruption"; + else if (config->maxruns > 0 && perf_datafile_nruns(input) == config->maxruns) + reason = "end of file"; + else + reason = "time limit"; + + printf("[Status] Testing complete (%s)\n", reason); + printf("\n"); +} + +static double +stddev(uint64_t sum_of_squares, uint64_t sum, uint64_t total) +{ + double squared; + + squared = (double)sum * (double)sum; + return sqrt((sum_of_squares - (squared / total)) / (total - 1)); +} + +static void +print_statistics(const config_t* config, const times_t* times, stats_t* stats) +{ + const char* units; + uint64_t run_time; + bool first_rcode; + uint64_t latency_avg; + unsigned int i; + + units = config->updates ? "Updates" : "Queries"; + + run_time = times->end_time - times->start_time; + + printf("Statistics:\n\n"); + + printf(" %s sent: %" PRIu64 "\n", + units, stats->num_sent); + printf(" %s completed: %" PRIu64 " (%.2lf%%)\n", + units, stats->num_completed, + PERF_SAFE_DIV(100.0 * stats->num_completed, stats->num_sent)); + printf(" %s lost: %" PRIu64 " (%.2lf%%)\n", + units, stats->num_timedout, + PERF_SAFE_DIV(100.0 * stats->num_timedout, stats->num_sent)); + if (stats->num_interrupted > 0) + printf(" %s interrupted: %" PRIu64 " (%.2lf%%)\n", + units, stats->num_interrupted, + PERF_SAFE_DIV(100.0 * stats->num_interrupted, stats->num_sent)); + printf("\n"); + + printf(" Response codes: "); + first_rcode = true; + for (i = 0; i < 16; i++) { + if (stats->rcodecounts[i] == 0) + continue; + if (first_rcode) + first_rcode = false; + else + printf(", "); + printf("%s %" PRIu64 " (%.2lf%%)", + perf_dns_rcode_strings[i], stats->rcodecounts[i], + (stats->rcodecounts[i] * 100.0) / stats->num_completed); + } + printf("\n"); + + printf(" Average packet size: request %u, response %u\n", + (unsigned int)PERF_SAFE_DIV(stats->total_request_size, stats->num_sent), + (unsigned int)PERF_SAFE_DIV(stats->total_response_size, + stats->num_completed)); + printf(" Run time (s): %u.%06u\n", + (unsigned int)(run_time / MILLION), + (unsigned int)(run_time % MILLION)); + printf(" %s per second: %.6lf\n", units, + PERF_SAFE_DIV(stats->num_completed, (((double)run_time) / MILLION))); + + printf("\n"); + + latency_avg = PERF_SAFE_DIV(stats->latency_sum, stats->num_completed); + printf(" Average Latency (s): %u.%06u (min %u.%06u, max %u.%06u)\n", + (unsigned int)(latency_avg / MILLION), + (unsigned int)(latency_avg % MILLION), + (unsigned int)(stats->latency_min / MILLION), + (unsigned int)(stats->latency_min % MILLION), + (unsigned int)(stats->latency_max / MILLION), + (unsigned int)(stats->latency_max % MILLION)); + if (stats->num_completed > 1) { + printf(" Latency StdDev (s): %f\n", + stddev(stats->latency_sum_squares, stats->latency_sum, + stats->num_completed) + / MILLION); + } + + printf("\n"); +} + +static void +sum_stats(const config_t* config, stats_t* total) +{ + unsigned int i, j; + + memset(total, 0, sizeof(*total)); + + for (i = 0; i < config->threads; i++) { + stats_t* stats = &threads[i].stats; + + for (j = 0; j < 16; j++) + total->rcodecounts[j] += stats->rcodecounts[j]; + + total->num_sent += stats->num_sent; + total->num_interrupted += stats->num_interrupted; + total->num_timedout += stats->num_timedout; + total->num_completed += stats->num_completed; + + total->total_request_size += stats->total_request_size; + total->total_response_size += stats->total_response_size; + + total->latency_sum += stats->latency_sum; + total->latency_sum_squares += stats->latency_sum_squares; + if (stats->latency_min < total->latency_min || i == 0) + total->latency_min = stats->latency_min; + if (stats->latency_max > total->latency_max) + total->latency_max = stats->latency_max; + } +} + +static char* +stringify(unsigned int value) +{ + static char buf[20]; + + snprintf(buf, sizeof(buf), "%u", value); + return buf; +} + +static void +setup(int argc, char** argv, config_t* config) +{ + const char* family = NULL; + const char* server_name = DEFAULT_SERVER_NAME; + in_port_t server_port = 0; + const char* local_name = NULL; + in_port_t local_port = DEFAULT_LOCAL_PORT; + const char* filename = NULL; + const char* edns_option = NULL; + const char* tsigkey = NULL; + const char* mode = 0; + + memset(config, 0, sizeof(*config)); + config->argc = argc; + config->argv = argv; + + config->family = AF_UNSPEC; + config->clients = 1; + config->threads = 1; + config->timeout = DEFAULT_TIMEOUT * MILLION; + config->max_outstanding = DEFAULT_MAX_OUTSTANDING; + config->mode = sock_udp; + + perf_opt_add('f', perf_opt_string, "family", + "address family of DNS transport, inet or inet6", "any", + &family); + perf_opt_add('m', perf_opt_string, "mode", "set transport mode: udp, tcp or dot/tls", "udp", &mode); + perf_opt_add('s', perf_opt_string, "server_addr", + "the server to query", DEFAULT_SERVER_NAME, &server_name); + perf_opt_add('p', perf_opt_port, "port", + "the port on which to query the server", + DEFAULT_SERVER_PORTS, &server_port); + perf_opt_add('a', perf_opt_string, "local_addr", + "the local address from which to send queries", NULL, + &local_name); + perf_opt_add('x', perf_opt_port, "local_port", + "the local port from which to send queries", + stringify(DEFAULT_LOCAL_PORT), &local_port); + perf_opt_add('d', perf_opt_string, "datafile", + "the input data file", "stdin", &filename); + perf_opt_add('c', perf_opt_uint, "clients", + "the number of clients to act as", NULL, + &config->clients); + perf_opt_add('T', perf_opt_uint, "threads", + "the number of threads to run", NULL, + &config->threads); + perf_opt_add('n', perf_opt_uint, "maxruns", + "run through input at most N times", NULL, + &config->maxruns); + perf_opt_add('l', perf_opt_timeval, "timelimit", + "run for at most this many seconds", NULL, + &config->timelimit); + perf_opt_add('b', perf_opt_uint, "buffer_size", + "socket send/receive buffer size in kilobytes", NULL, + &config->bufsize); + perf_opt_add('t', perf_opt_timeval, "timeout", + "the timeout for query completion in seconds", + stringify(DEFAULT_TIMEOUT), &config->timeout); + perf_opt_add('e', perf_opt_boolean, NULL, + "enable EDNS 0", NULL, &config->edns); + perf_opt_add('E', perf_opt_string, "code:value", + "send EDNS option", NULL, &edns_option); + perf_opt_add('D', perf_opt_boolean, NULL, + "set the DNSSEC OK bit (implies EDNS)", NULL, + &config->dnssec); + perf_opt_add('y', perf_opt_string, "[alg:]name:secret", + "the TSIG algorithm, name and secret (base64)", NULL, + &tsigkey); + perf_opt_add('q', perf_opt_uint, "num_queries", + "the maximum number of queries outstanding", + stringify(DEFAULT_MAX_OUTSTANDING), + &config->max_outstanding); + perf_opt_add('Q', perf_opt_uint, "max_qps", + "limit the number of queries per second", NULL, + &config->max_qps); + perf_opt_add('S', perf_opt_timeval, "stats_interval", + "print qps statistics every N seconds", + NULL, &config->stats_interval); + perf_opt_add('u', perf_opt_boolean, NULL, + "send dynamic updates instead of queries", + NULL, &config->updates); + perf_opt_add('v', perf_opt_boolean, NULL, + "verbose: report each query and additional information to stdout", + NULL, &config->verbose); + bool log_stdout = false; + perf_opt_add('W', perf_opt_boolean, NULL, "log warnings and errors to stdout instead of stderr", NULL, &log_stdout); + + perf_opt_parse(argc, argv); + + if (log_stdout) { + perf_log_tostdout(); + } + + if (mode != 0) + config->mode = perf_net_parsemode(mode); + + if (!server_port) { + server_port = config->mode == sock_tls ? DEFAULT_SERVER_TLS_PORT : DEFAULT_SERVER_PORT; + } + + if (family != NULL) + config->family = perf_net_parsefamily(family); + perf_net_parseserver(config->family, server_name, server_port, + &config->server_addr); + perf_net_parselocal(config->server_addr.sa.sa.sa_family, + local_name, local_port, &config->local_addr); + + input = perf_datafile_open(filename); + + if (config->maxruns == 0 && config->timelimit == 0) + config->maxruns = 1; + perf_datafile_setmaxruns(input, config->maxruns); + + if (config->dnssec || edns_option != NULL) + config->edns = true; + + if (tsigkey != NULL) + config->tsigkey = perf_tsig_parsekey(tsigkey); + + if (edns_option != NULL) + config->edns_option = perf_edns_parseoption(edns_option); + + /* + * If we run more threads than max-qps, some threads will have + * ->max_qps set to 0, and be unlimited. + */ + if (config->max_qps > 0 && config->threads > config->max_qps) + config->threads = config->max_qps; + + /* + * We also can't run more threads than clients. + */ + if (config->threads > config->clients) + config->threads = config->clients; + +#ifndef HAVE_LDNS + if (config->updates) { + perf_log_fatal("Unable to dynamic update, support not built in"); + } +#endif +} + +static void +cleanup(config_t* config) +{ + unsigned int i; + + perf_datafile_close(&input); + for (i = 0; i < 2; i++) { + close(threadpipe[i]); + close(mainpipe[i]); + close(intrpipe[i]); + } + if (config->tsigkey != NULL) + perf_tsig_destroykey(&config->tsigkey); + if (config->edns_option != NULL) + perf_edns_destroyoption(&config->edns_option); +} + +typedef enum { + prepend_unused, + append_unused, + prepend_outstanding, +} query_move_op; + +static inline void +query_move(threadinfo_t* tinfo, query_info* q, query_move_op op) +{ + perf_list_unlink(*q->list, q); + switch (op) { + case prepend_unused: + q->list = &tinfo->unused_queries; + perf_list_prepend(tinfo->unused_queries, q); + break; + case append_unused: + q->list = &tinfo->unused_queries; + perf_list_append(tinfo->unused_queries, q); + break; + case prepend_outstanding: + q->list = &tinfo->outstanding_queries; + perf_list_prepend(tinfo->outstanding_queries, q); + break; + } +} + +static inline uint64_t +num_outstanding(const stats_t* stats) +{ + return stats->num_sent - stats->num_completed - stats->num_timedout; +} + +static void +wait_for_start(void) +{ + PERF_LOCK(&start_lock); + while (!started) + PERF_WAIT(&start_cond, &start_lock); + PERF_UNLOCK(&start_lock); +} + +static void* +do_send(void* arg) +{ + threadinfo_t* tinfo; + const config_t* config; + const times_t* times; + stats_t* stats; + unsigned int max_packet_size; + perf_buffer_t msg; + uint64_t now, run_time, req_time; + char input_data[MAX_INPUT_DATA]; + perf_buffer_t lines; + perf_region_t used; + query_info* q; + int qid; + unsigned char packet_buffer[MAX_EDNS_PACKET]; + unsigned char* base; + unsigned int length; + int n, i, any_inprogress = 0; + perf_result_t result; + + tinfo = (threadinfo_t*)arg; + config = tinfo->config; + times = tinfo->times; + stats = &tinfo->stats; + max_packet_size = config->edns ? MAX_EDNS_PACKET : MAX_UDP_PACKET; + perf_buffer_init(&msg, packet_buffer, max_packet_size); + perf_buffer_init(&lines, input_data, sizeof(input_data)); + + wait_for_start(); + now = perf_get_time(); + while (!interrupted && now < times->stop_time) { + /* Avoid flooding the network too quickly. */ + if (stats->num_sent < tinfo->max_outstanding && stats->num_sent % 2 == 1) { + if (stats->num_completed == 0) + usleep(1000); + else + sleep(0); + now = perf_get_time(); + } + + /* Rate limiting */ + if (tinfo->max_qps > 0) { + run_time = now - times->start_time; + req_time = (MILLION * stats->num_sent) / tinfo->max_qps; + if (req_time > run_time) { + usleep(req_time - run_time); + now = perf_get_time(); + continue; + } + } + + PERF_LOCK(&tinfo->lock); + + /* Limit in-flight queries */ + if (num_outstanding(stats) >= tinfo->max_outstanding) { + PERF_TIMEDWAIT(&tinfo->cond, &tinfo->lock, ×->stop_time_ns, NULL); + PERF_UNLOCK(&tinfo->lock); + now = perf_get_time(); + continue; + } + + q = perf_list_head(tinfo->unused_queries); + query_move(tinfo, q, prepend_outstanding); + q->timestamp = UINT64_MAX; + + i = tinfo->nsocks * 2; + while (i--) { + q->sock = &tinfo->socks[tinfo->current_sock++ % tinfo->nsocks]; + switch (perf_net_sockready(q->sock, threadpipe[0], TIMEOUT_CHECK_TIME)) { + case 0: + if (config->verbose) { + perf_log_warning("socket %p not ready", q->sock); + } + q->sock = 0; + continue; + case -1: + if (errno == EINPROGRESS) { + any_inprogress = 1; + q->sock = 0; + continue; + } + if (config->verbose) { + perf_log_warning("socket %p readiness check timed out", q->sock); + } + default: + break; + } + break; + }; + + if (!q->sock) { + query_move(tinfo, q, prepend_unused); + PERF_UNLOCK(&tinfo->lock); + now = perf_get_time(); + continue; + } + PERF_UNLOCK(&tinfo->lock); + + perf_buffer_clear(&lines); + result = perf_datafile_next(input, &lines, config->updates); + if (result != PERF_R_SUCCESS) { + if (result == PERF_R_INVALIDFILE) + perf_log_fatal("input file contains no data"); + break; + } + + qid = q - tinfo->queries; + perf_buffer_usedregion(&lines, &used); + perf_buffer_clear(&msg); + result = perf_dns_buildrequest(&used, qid, + config->edns, config->dnssec, config->updates, + config->tsigkey, config->edns_option, + &msg); + if (result != PERF_R_SUCCESS) { + PERF_LOCK(&tinfo->lock); + query_move(tinfo, q, prepend_unused); + PERF_UNLOCK(&tinfo->lock); + now = perf_get_time(); + continue; + } + + base = perf_buffer_base(&msg); + length = perf_buffer_usedlength(&msg); + + now = perf_get_time(); + if (config->verbose) { + free(q->desc); + q->desc = strdup(lines.base); + if (q->desc == NULL) + perf_log_fatal("out of memory"); + } + q->timestamp = now; + + n = perf_net_sendto(q->sock, base, length, 0, &config->server_addr.sa.sa, + config->server_addr.length); + if (n < 0) { + if (errno == EINPROGRESS) { + if (config->verbose) { + perf_log_warning("network congested, packet sending in progress"); + } + any_inprogress = 1; + } else { + if (config->verbose) { + char __s[256]; + perf_log_warning("failed to send packet: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + PERF_LOCK(&tinfo->lock); + query_move(tinfo, q, prepend_unused); + PERF_UNLOCK(&tinfo->lock); + continue; + } + } else if ((unsigned int)n != length) { + perf_log_warning("failed to send full packet: only sent %d of %u", n, length); + PERF_LOCK(&tinfo->lock); + query_move(tinfo, q, prepend_unused); + PERF_UNLOCK(&tinfo->lock); + continue; + } + stats->num_sent++; + + stats->total_request_size += length; + } + + while (any_inprogress) { + any_inprogress = 0; + for (i = 0; i < tinfo->nsocks; i++) { + if (perf_net_sockready(&tinfo->socks[i], threadpipe[0], TIMEOUT_CHECK_TIME) == -1 && errno == EINPROGRESS) { + any_inprogress = 1; + } + } + } + + tinfo->done_send_time = perf_get_time(); + tinfo->done_sending = true; + if (write(mainpipe[1], "", 1)) { // lgtm [cpp/empty-block] + } + return NULL; +} + +static void +process_timeouts(threadinfo_t* tinfo, uint64_t now) +{ + struct query_info* q; + const config_t* config; + + config = tinfo->config; + + /* Avoid locking unless we need to. */ + q = perf_list_tail(tinfo->outstanding_queries); + if (q == NULL || q->timestamp > now || now - q->timestamp < config->timeout) + return; + + PERF_LOCK(&tinfo->lock); + + do { + query_move(tinfo, q, append_unused); + + tinfo->stats.num_timedout++; + + if (q->desc != NULL) { + perf_log_printf("> T %s", q->desc); + } else { + perf_log_printf("[Timeout] %s timed out: msg id %u", + config->updates ? "Update" : "Query", + (unsigned int)(q - tinfo->queries)); + } + q = perf_list_tail(tinfo->outstanding_queries); + } while (q != NULL && q->timestamp < now && now - q->timestamp >= config->timeout); + + PERF_UNLOCK(&tinfo->lock); +} + +typedef struct { + struct perf_net_socket* sock; + uint16_t qid; + uint16_t rcode; + unsigned int size; + uint64_t when; + uint64_t sent; + bool unexpected; + bool short_response; + char* desc; +} received_query_t; + +static bool +recv_one(threadinfo_t* tinfo, int which_sock, + unsigned char* packet_buffer, unsigned int packet_size, + received_query_t* recvd, int* saved_errnop) +{ + uint16_t* packet_header; + uint64_t now; + int n; + + packet_header = (uint16_t*)packet_buffer; + + n = perf_net_recv(&tinfo->socks[which_sock], packet_buffer, packet_size, 0); + now = perf_get_time(); + if (n < 0) { + *saved_errnop = errno; + return false; + } + if (!n) { + // Treat connection closed like try again until reconnection features are in + *saved_errnop = EAGAIN; + return false; + } + recvd->sock = &tinfo->socks[which_sock]; + recvd->qid = ntohs(packet_header[0]); + recvd->rcode = ntohs(packet_header[1]) & 0xF; + recvd->size = n; + recvd->when = now; + recvd->sent = 0; + recvd->unexpected = false; + recvd->short_response = (n < 4); + recvd->desc = NULL; + return true; +} + +static inline void +bit_set(unsigned char* bits, unsigned int bit) +{ + unsigned int shift, mask; + + shift = 7 - (bit % 8); + mask = 1 << shift; + + bits[bit / 8] |= mask; +} + +static inline bool +bit_check(unsigned char* bits, unsigned int bit) +{ + unsigned int shift; + + shift = 7 - (bit % 8); + + if ((bits[bit / 8] >> shift) & 0x01) + return true; + return false; +} + +static void* +do_recv(void* arg) +{ + threadinfo_t* tinfo; + stats_t* stats; + unsigned char packet_buffer[MAX_EDNS_PACKET]; + received_query_t recvd[RECV_BATCH_SIZE] = { { 0, 0, 0, 0, 0, 0, false, false, 0 } }; + unsigned int nrecvd; + int saved_errno; + unsigned char socketbits[MAX_SOCKETS / 8]; + uint64_t now, latency; + query_info* q; + unsigned int current_socket, last_socket; + unsigned int i, j; + + tinfo = (threadinfo_t*)arg; + stats = &tinfo->stats; + + wait_for_start(); + now = perf_get_time(); + last_socket = 0; + while (!interrupted) { + process_timeouts(tinfo, now); + + /* + * If we're done sending and either all responses have been + * received, stop. + */ + if (tinfo->done_sending && num_outstanding(stats) == 0) + break; + + /* + * Try to receive a few packets, so that we can process them + * atomically. + */ + saved_errno = 0; + memset(socketbits, 0, sizeof(socketbits)); + for (i = 0; i < RECV_BATCH_SIZE; i++) { + for (j = 0; j < tinfo->nsocks; j++) { + current_socket = (j + last_socket) % tinfo->nsocks; + if (bit_check(socketbits, current_socket)) + continue; + if (recv_one(tinfo, current_socket, packet_buffer, + sizeof(packet_buffer), &recvd[i], &saved_errno)) { + last_socket = (current_socket + 1); + break; + } + bit_set(socketbits, current_socket); + if (saved_errno != EAGAIN) + break; + } + if (j == tinfo->nsocks) + break; + } + nrecvd = i; + + /* Do all of the processing that requires the lock */ + PERF_LOCK(&tinfo->lock); + for (i = 0; i < nrecvd; i++) { + if (recvd[i].short_response) + continue; + + q = &tinfo->queries[recvd[i].qid]; + if (q->list != &tinfo->outstanding_queries || q->timestamp == UINT64_MAX || !perf_net_sockeq(q->sock, recvd[i].sock)) { + recvd[i].unexpected = true; + continue; + } + query_move(tinfo, q, append_unused); + recvd[i].sent = q->timestamp; + recvd[i].desc = q->desc; + q->desc = NULL; + } + PERF_SIGNAL(&tinfo->cond); + PERF_UNLOCK(&tinfo->lock); + + /* Now do the rest of the processing unlocked */ + for (i = 0; i < nrecvd; i++) { + if (recvd[i].short_response) { + perf_log_warning("received short response"); + continue; + } + if (recvd[i].unexpected) { + perf_log_warning("received a response with an " + "unexpected (maybe timed out) " + "id: %u", + recvd[i].qid); + continue; + } + latency = recvd[i].when - recvd[i].sent; + if (recvd[i].desc != NULL) { + perf_log_printf( + "> %s %s %u.%06u", + perf_dns_rcode_strings[recvd[i].rcode], + recvd[i].desc, + (unsigned int)(latency / MILLION), + (unsigned int)(latency % MILLION)); + free(recvd[i].desc); + } + + stats->num_completed++; + stats->total_response_size += recvd[i].size; + stats->rcodecounts[recvd[i].rcode]++; + stats->latency_sum += latency; + stats->latency_sum_squares += (latency * latency); + if (latency < stats->latency_min || stats->num_completed == 1) + stats->latency_min = latency; + if (latency > stats->latency_max) + stats->latency_max = latency; + } + + if (nrecvd > 0) + tinfo->last_recv = recvd[nrecvd - 1].when; + + /* + * If there was an error, handle it (by either ignoring it, + * blocking, or exiting). + */ + if (nrecvd < RECV_BATCH_SIZE) { + if (saved_errno == EINTR) { + continue; + } else if (saved_errno == EAGAIN) { + perf_os_waituntilanyreadable(tinfo->socks, tinfo->nsocks, + threadpipe[0], TIMEOUT_CHECK_TIME); + now = perf_get_time(); + continue; + } else { + char __s[256]; + perf_log_fatal("failed to receive packet: %s", perf_strerror_r(saved_errno, __s, sizeof(__s))); + } + } + } + + return NULL; +} + +static void* +do_interval_stats(void* arg) +{ + threadinfo_t* tinfo; + stats_t total; + uint64_t now; + uint64_t last_interval_time; + uint64_t last_completed; + uint64_t interval_time; + uint64_t num_completed; + double qps; + struct perf_net_socket sock = { .mode = sock_pipe, .fd = threadpipe[0] }; + + tinfo = arg; + last_interval_time = tinfo->times->start_time; + last_completed = 0; + + wait_for_start(); + while (perf_os_waituntilreadable(&sock, threadpipe[0], + tinfo->config->stats_interval) + == PERF_R_TIMEDOUT) { + now = perf_get_time(); + sum_stats(tinfo->config, &total); + interval_time = now - last_interval_time; + num_completed = total.num_completed - last_completed; + qps = num_completed / (((double)interval_time) / MILLION); + perf_log_printf("%u.%06u: %.6lf", + (unsigned int)(now / MILLION), + (unsigned int)(now % MILLION), qps); + last_interval_time = now; + last_completed = total.num_completed; + } + + return NULL; +} + +static void +cancel_queries(threadinfo_t* tinfo) +{ + struct query_info* q; + + while (true) { + q = perf_list_tail(tinfo->outstanding_queries); + if (q == NULL) + break; + query_move(tinfo, q, append_unused); + + if (q->timestamp == UINT64_MAX) + continue; + + tinfo->stats.num_interrupted++; + if (q->desc != NULL) { + perf_log_printf("> I %s", q->desc); + free(q->desc); + q->desc = NULL; + } + } +} + +static uint32_t +per_thread(uint32_t total, uint32_t nthreads, unsigned int offset) +{ + uint32_t value, temp_total; + + value = total / nthreads; + + /* + * work out if there's a shortfall and adjust if necessary + */ + temp_total = value * nthreads; + if (temp_total < total && offset < total - temp_total) + value++; + + return value; +} + +static void +threadinfo_init(threadinfo_t* tinfo, const config_t* config, + const times_t* times) +{ + unsigned int offset, socket_offset, i; + + memset(tinfo, 0, sizeof(*tinfo)); + PERF_MUTEX_INIT(&tinfo->lock); + PERF_COND_INIT(&tinfo->cond); + + perf_list_init(tinfo->outstanding_queries); + perf_list_init(tinfo->unused_queries); + for (i = 0; i < NQIDS; i++) { + perf_link_init(&tinfo->queries[i]); + perf_list_append(tinfo->unused_queries, &tinfo->queries[i]); + tinfo->queries[i].list = &tinfo->unused_queries; + } + + offset = tinfo - threads; + + tinfo->config = config; + tinfo->times = times; + + /* + * Compute per-thread limits based on global values. + */ + tinfo->max_outstanding = per_thread(config->max_outstanding, + config->threads, offset); + tinfo->max_qps = per_thread(config->max_qps, config->threads, offset); + tinfo->nsocks = per_thread(config->clients, config->threads, offset); + + /* + * We can't have more than 64k outstanding queries per thread. + */ + if (tinfo->max_outstanding > NQIDS) + tinfo->max_outstanding = NQIDS; + + if (tinfo->nsocks > MAX_SOCKETS) + tinfo->nsocks = MAX_SOCKETS; + + if (!(tinfo->socks = calloc(tinfo->nsocks, sizeof(*tinfo->socks)))) { + perf_log_fatal("out of memory"); + } + socket_offset = 0; + for (i = 0; i < offset; i++) + socket_offset += threads[i].nsocks; + for (i = 0; i < tinfo->nsocks; i++) + tinfo->socks[i] = perf_net_opensocket(config->mode, &config->server_addr, + &config->local_addr, + socket_offset++, + config->bufsize); + tinfo->current_sock = 0; + + PERF_THREAD(&tinfo->receiver, do_recv, tinfo); + PERF_THREAD(&tinfo->sender, do_send, tinfo); +} + +static void +threadinfo_stop(threadinfo_t* tinfo) +{ + PERF_SIGNAL(&tinfo->cond); + PERF_JOIN(tinfo->sender, NULL); + PERF_JOIN(tinfo->receiver, NULL); +} + +static void +threadinfo_cleanup(threadinfo_t* tinfo, times_t* times) +{ + unsigned int i; + + if (interrupted) + cancel_queries(tinfo); + for (i = 0; i < tinfo->nsocks; i++) + perf_net_close(&tinfo->socks[i]); + if (tinfo->last_recv > times->end_time) + times->end_time = tinfo->last_recv; +} + +int main(int argc, char** argv) +{ + config_t config; + times_t times; + stats_t total_stats; + threadinfo_t stats_thread; + unsigned int i; + perf_result_t result; + struct perf_net_socket sock = { .mode = sock_pipe }; + + printf("DNS Performance Testing Tool\n" + "Version " PACKAGE_VERSION "\n\n"); + + (void)SSL_library_init(); +#if OPENSSL_VERSION_NUMBER < 0x10100000L + SSL_load_error_strings(); + OPENSSL_config(0); +#endif + + setup(argc, argv, &config); + + if (pipe(threadpipe) < 0 || pipe(mainpipe) < 0 || pipe(intrpipe) < 0) + perf_log_fatal("creating pipe"); + + perf_datafile_setpipefd(input, threadpipe[0]); + + perf_os_blocksignal(SIGINT, true); + switch (config.mode) { + case sock_tcp: + case sock_tls: + // block SIGPIPE for TCP/TLS mode, if connection is closed it will generate a signal + perf_os_blocksignal(SIGPIPE, true); + break; + default: + break; + } + + print_initial_status(&config); + + if (!(threads = calloc(config.threads, sizeof(threadinfo_t)))) { + perf_log_fatal("out of memory"); + } + for (i = 0; i < config.threads; i++) + threadinfo_init(&threads[i], &config, ×); + if (config.stats_interval > 0) { + stats_thread.config = &config; + stats_thread.times = × + PERF_THREAD(&stats_thread.sender, do_interval_stats, &stats_thread); + } + + times.start_time = perf_get_time(); + if (config.timelimit > 0) + times.stop_time = times.start_time + config.timelimit; + else + times.stop_time = UINT64_MAX; + times.stop_time_ns.tv_sec = times.stop_time / MILLION; + times.stop_time_ns.tv_nsec = (times.stop_time % MILLION) * 1000; + + PERF_LOCK(&start_lock); + started = true; + PERF_BROADCAST(&start_cond); + PERF_UNLOCK(&start_lock); + + perf_os_handlesignal(SIGINT, handle_sigint); + perf_os_blocksignal(SIGINT, false); + sock.fd = mainpipe[0]; + result = perf_os_waituntilreadable(&sock, intrpipe[0], + times.stop_time - times.start_time); + if (result == PERF_R_CANCELED) + interrupted = true; + + times.end_time = perf_get_time(); + + if (write(threadpipe[1], "", 1)) { // lgtm [cpp/empty-block] + } + for (i = 0; i < config.threads; i++) + threadinfo_stop(&threads[i]); + if (config.stats_interval > 0) + PERF_JOIN(stats_thread.sender, NULL); + + for (i = 0; i < config.threads; i++) + threadinfo_cleanup(&threads[i], ×); + + print_final_status(&config); + + sum_stats(&config, &total_stats); + print_statistics(&config, ×, &total_stats); + + cleanup(&config); +#if OPENSSL_VERSION_NUMBER < 0x10100000L + ERR_free_strings(); +#endif + + return (0); +} diff --git a/src/edns.c b/src/edns.c new file mode 100644 index 0000000..9c04f9b --- /dev/null +++ b/src/edns.c @@ -0,0 +1,158 @@ +/* + * 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 "edns.h" + +#include "log.h" +#include "opt.h" + +#include <stdint.h> +#include <stdlib.h> +#include <limits.h> +#include <string.h> + +#define EDNSLEN 11 + +perf_ednsoption_t* perf_edns_parseoption(const char* arg) +{ + char * copy, *sep, *value, *endptr, hex[3]; + perf_ednsoption_t* option; + size_t data_len; + unsigned long int u; + perf_buffer_t save; + + copy = strdup(arg); + if (!copy) { + perf_log_fatal("out of memory"); + return 0; // fix clang scan-build + } + + sep = strchr(copy, ':'); + if (!sep) { + perf_log_warning("invalid EDNS Option, must be code:value"); + perf_opt_usage(); + exit(1); + } + *sep = '\0'; + value = sep + 1; + + data_len = strlen(value); + if (!data_len) { + perf_log_warning("invalid EDNS Option, value is empty"); + perf_opt_usage(); + exit(1); + } + if (data_len & 1) { + perf_log_warning("invalid EDNS Option, value must hex string (even number of characters)"); + perf_opt_usage(); + exit(1); + } + data_len /= 2; + data_len += 4; // code, len, data... + + option = calloc(1, sizeof(perf_ednsoption_t) + data_len); + if (!option) { + perf_log_fatal("out of memory"); + free(copy); // fix clang scan-build + return 0; // fix clang scan-build + } + perf_buffer_init(&option->buffer, &option->data[0], data_len); + + endptr = 0; + u = strtoul(copy, &endptr, 10); + if (*endptr || u == ULONG_MAX) { + perf_log_warning("invalid EDNS Option code '%s'", copy); + perf_opt_usage(); + exit(1); + } + perf_buffer_putuint16(&option->buffer, u & 0xffff); + + save = option->buffer; + perf_buffer_add(&option->buffer, 2); + hex[2] = 0; + while (*value) { + memcpy(hex, value, 2); + endptr = 0; + u = strtoul(hex, &endptr, 16); + if (*endptr || u == ULONG_MAX) { + perf_log_warning("invalid EDNS Option hex value '%.*s'", 2, value); + perf_opt_usage(); + exit(1); + } + perf_buffer_putuint8(&option->buffer, u & 0xff); + value += 2; + } + perf_buffer_putuint16(&save, perf_buffer_usedlength(&option->buffer) - 4); + + free(copy); + + return option; +} + +void perf_edns_destroyoption(perf_ednsoption_t** optionp) +{ + assert(optionp); + assert(*optionp); + + free(*optionp); + *optionp = 0; +} + +/* + * Appends an OPT record to the packet. + */ +perf_result_t perf_add_edns(perf_buffer_t* packet, bool dnssec, perf_ednsoption_t* option) +{ + unsigned char* base; + size_t option_length = 0, total_length; + + if (option) { + option_length = perf_buffer_usedlength(&option->buffer); + } + total_length = EDNSLEN + option_length; + + if (perf_buffer_availablelength(packet) < total_length) { + perf_log_warning("failed to add OPT to query packet"); + return PERF_R_NOSPACE; + } + + base = perf_buffer_base(packet); + + perf_buffer_putuint8(packet, 0); /* root name */ + perf_buffer_putuint16(packet, 41); /* OPT record */ + perf_buffer_putuint16(packet, MAX_EDNS_PACKET); /* class */ + perf_buffer_putuint8(packet, 0); /* xrcode */ + perf_buffer_putuint8(packet, 0); /* version */ + if (dnssec) { + /* flags */ + perf_buffer_putuint16(packet, 0x8000); + } else { + perf_buffer_putuint16(packet, 0); + } + perf_buffer_putuint16(packet, option_length); /* rdlen */ + if (option) { + perf_buffer_putmem(packet, perf_buffer_base(&option->buffer), option_length); + } + + base[11]++; /* increment additional record count */ + + return PERF_R_SUCCESS; +} diff --git a/src/edns.h b/src/edns.h new file mode 100644 index 0000000..300d462 --- /dev/null +++ b/src/edns.h @@ -0,0 +1,41 @@ +/* + * 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 "result.h" +#include "buffer.h" + +#ifndef PERF_EDNS_H +#define PERF_EDNS_H 1 + +#include <stdbool.h> + +#define MAX_EDNS_PACKET 4096 + +typedef struct perf_ednsoption { + perf_buffer_t buffer; + char data[]; +} perf_ednsoption_t; + +perf_ednsoption_t* perf_edns_parseoption(const char* arg); + +void perf_edns_destroyoption(perf_ednsoption_t** optionp); + +perf_result_t perf_add_edns(perf_buffer_t* packet, bool dnssec, perf_ednsoption_t* option); + +#endif diff --git a/src/gen-qtype.c.py b/src/gen-qtype.c.py new file mode 100755 index 0000000..f9735e5 --- /dev/null +++ b/src/gen-qtype.c.py @@ -0,0 +1,46 @@ +#!/usr/bin/python3 + +import csv +from urllib.request import Request, urlopen +from io import StringIO + +qtype = {} + +for row in csv.reader(StringIO(urlopen(Request('https://www.iana.org/assignments/dns-parameters/dns-parameters-4.csv')).read().decode('utf-8'))): + if row[0] == 'TYPE': + continue + try: + qtype[row[0]] = int(row[1]) + except Exception: + continue + +print("""/* + * 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 "qtype.h" + +const perf_qtype_t qtype_table[] = {""") + +for k, v in qtype.items(): + print(" { \"%s\", %d }," % (k, v)) + +print(""" { 0, 0 } +};""") diff --git a/src/list.h b/src/list.h new file mode 100644 index 0000000..f110b79 --- /dev/null +++ b/src/list.h @@ -0,0 +1,91 @@ +/* + * 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. + */ + +#ifndef PERF_LIST_H +#define PERF_LIST_H 1 + +#include <assert.h> + +#define perf_link(type) \ + struct { \ + type *prev, *next; \ + } _link +#define perf_link_init(link) \ + { \ + (link)->_link.prev = 0; \ + (link)->_link.next = 0; \ + } + +#define perf_list(type) \ + struct { \ + type *head, *tail; \ + } +#define perf_list_init(list) \ + { \ + (list).head = 0; \ + (list).tail = 0; \ + } + +#define perf_list_head(list) ((list).head) +#define perf_list_tail(list) ((list).tail) +#define perf_list_empty(list) (!(list).head) + +#define perf_list_append(list, link) \ + { \ + if ((list).tail) { \ + (list).tail->_link.next = (link); \ + } else { \ + (list).head = (link); \ + } \ + (link)->_link.prev = (list).tail; \ + (link)->_link.next = 0; \ + (list).tail = (link); \ + } +#define perf_list_prepend(list, link) \ + { \ + if ((list).head) { \ + (list).head->_link.prev = (link); \ + } else { \ + (list).tail = (link); \ + } \ + (link)->_link.prev = 0; \ + (link)->_link.next = (list).head; \ + (list).head = (link); \ + } +#define perf_list_unlink(list, link) \ + { \ + if ((link)->_link.next) { \ + (link)->_link.next->_link.prev = (link)->_link.prev; \ + } else { \ + assert((list).tail == (link)); \ + (list).tail = (link)->_link.prev; \ + } \ + if ((link)->_link.prev) { \ + (link)->_link.prev->_link.next = (link)->_link.next; \ + } else { \ + assert((list).head == (link)); \ + (list).head = (link)->_link.next; \ + } \ + (link)->_link.next = 0; \ + (link)->_link.prev = 0; \ + assert((list).head != (link)); \ + assert((list).tail != (link)); \ + } + +#endif diff --git a/src/log.c b/src/log.c new file mode 100644 index 0000000..bba5ba6 --- /dev/null +++ b/src/log.c @@ -0,0 +1,72 @@ +/* + * 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 "log.h" + +#include "util.h" + +#include <pthread.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> + +static bool log_err_stdout = false; + +pthread_mutex_t log_lock = PTHREAD_MUTEX_INITIALIZER; + +static void +vlog(FILE* stream, const char* prefix, const char* fmt, va_list args) +{ + PERF_LOCK(&log_lock); + fflush(stdout); + if (prefix != NULL) + fprintf(stream, "%s: ", prefix); + vfprintf(stream, fmt, args); + fprintf(stream, "\n"); + PERF_UNLOCK(&log_lock); +} + +void perf_log_printf(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + vlog(stdout, NULL, fmt, args); +} + +void perf_log_fatal(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + vlog(log_err_stdout ? stdout : stderr, "Error", fmt, args); + exit(1); +} + +void perf_log_warning(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + vlog(log_err_stdout ? stdout : stderr, "Warning", fmt, args); +} + +void perf_log_tostdout(void) +{ + log_err_stdout = true; +} diff --git a/src/log.h b/src/log.h new file mode 100644 index 0000000..76ad7b7 --- /dev/null +++ b/src/log.h @@ -0,0 +1,28 @@ +/* + * 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. + */ + +#ifndef PERF_LOG_H +#define PERF_LOG_H 1 + +void perf_log_printf(const char* fmt, ...); +void perf_log_fatal(const char* fmt, ...); +void perf_log_warning(const char* fmt, ...); +void perf_log_tostdout(void); + +#endif diff --git a/src/net.c b/src/net.c new file mode 100644 index 0000000..9c4a8e2 --- /dev/null +++ b/src/net.c @@ -0,0 +1,664 @@ +/* + * 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 "net.h" + +#include "log.h" +#include "opt.h" +#include "os.h" +#include "strerror.h" + +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <poll.h> +#include <openssl/err.h> +#include <netdb.h> +#include <arpa/inet.h> + +#define TCP_RECV_BUF_SIZE (16 * 1024) +#define TCP_SEND_BUF_SIZE (4 * 1024) + +static SSL_CTX* ssl_ctx = 0; + +int perf_net_parsefamily(const char* family) +{ + if (family == NULL || strcmp(family, "any") == 0) + return AF_UNSPEC; + else if (strcmp(family, "inet") == 0) + return AF_INET; +#ifdef AF_INET6 + else if (strcmp(family, "inet6") == 0) + return AF_INET6; +#endif + else { + fprintf(stderr, "invalid family %s\n", family); + perf_opt_usage(); + exit(1); + } +} + +void perf_sockaddr_fromin(perf_sockaddr_t* sockaddr, const struct in_addr* in, in_port_t port) +{ + memset(sockaddr, 0, sizeof(*sockaddr)); + sockaddr->sa.sin.sin_family = AF_INET; + sockaddr->sa.sin.sin_addr = *in; + sockaddr->sa.sin.sin_port = htons(port); + sockaddr->length = sizeof(sockaddr->sa.sin); +} + +void perf_sockaddr_fromin6(perf_sockaddr_t* sockaddr, const struct in6_addr* in, in_port_t port) +{ + memset(sockaddr, 0, sizeof(*sockaddr)); + sockaddr->sa.sin6.sin6_family = AF_INET6; + sockaddr->sa.sin6.sin6_addr = *in; + sockaddr->sa.sin6.sin6_port = htons(port); + sockaddr->length = sizeof(sockaddr->sa.sin6); +} + +in_port_t perf_sockaddr_port(const perf_sockaddr_t* sockaddr) +{ + switch (sockaddr->sa.sa.sa_family) { + case AF_INET: + return sockaddr->sa.sin.sin_port; + case AF_INET6: + return sockaddr->sa.sin6.sin6_port; + default: + break; + } + return 0; +} + +void perf_sockaddr_setport(perf_sockaddr_t* sockaddr, in_port_t port) +{ + switch (sockaddr->sa.sa.sa_family) { + case AF_INET: + sockaddr->sa.sin.sin_port = port; + break; + case AF_INET6: + sockaddr->sa.sin6.sin6_port = port; + break; + default: + break; + } +} + +void perf_sockaddr_format(const perf_sockaddr_t* sockaddr, char* buf, size_t len) +{ + const void* src; + + *buf = 0; + + switch (sockaddr->sa.sa.sa_family) { + case AF_INET: + src = &sockaddr->sa.sin.sin_addr; + break; + case AF_INET6: + src = &sockaddr->sa.sin6.sin6_addr; + break; + default: + return; + } + + (void)inet_ntop(sockaddr->sa.sa.sa_family, src, buf, len); +} + +void perf_net_parseserver(int family, const char* name, unsigned int port, perf_sockaddr_t* addr) +{ + struct addrinfo* ai; + + if (getaddrinfo(name, 0, 0, &ai) == 0) { + struct addrinfo* a; + + for (a = ai; a; a = a->ai_next) { + if (a->ai_family == family || family == AF_UNSPEC) { + switch (a->ai_family) { + case AF_INET: + perf_sockaddr_fromin(addr, &((struct sockaddr_in*)a->ai_addr)->sin_addr, port); + break; + case AF_INET6: + perf_sockaddr_fromin6(addr, &((struct sockaddr_in6*)a->ai_addr)->sin6_addr, port); + break; + default: + continue; + } + + freeaddrinfo(ai); + return; + } + } + freeaddrinfo(ai); + } + + fprintf(stderr, "invalid server address %s\n", name); + perf_opt_usage(); + exit(1); +} + +void perf_net_parselocal(int family, const char* name, unsigned int port, + perf_sockaddr_t* addr) +{ + struct in_addr in4a; + struct in6_addr in6a; + + if (name == NULL) { + switch (family) { + case AF_INET: + in4a.s_addr = INADDR_ANY; + perf_sockaddr_fromin(addr, &in4a, port); + return; + case AF_INET6: + perf_sockaddr_fromin6(addr, &in6addr_any, port); + return; + default: + break; + } + } else if (inet_pton(AF_INET, name, &in4a) == 1) { + perf_sockaddr_fromin(addr, &in4a, port); + return; + } else if (inet_pton(AF_INET6, name, &in6a) == 1) { + perf_sockaddr_fromin6(addr, &in6a, port); + return; + } + + fprintf(stderr, "invalid local address %s\n", name); + perf_opt_usage(); + exit(1); +} + +struct perf_net_socket perf_net_opensocket(enum perf_net_mode mode, const perf_sockaddr_t* server, const perf_sockaddr_t* local, unsigned int offset, int bufsize) +{ + int family; + perf_sockaddr_t tmp; + int port; + int ret; + int flags; + struct perf_net_socket sock = { .mode = mode, .is_ready = 1 }; + + family = server->sa.sa.sa_family; + + if (local->sa.sa.sa_family != family) { + perf_log_fatal("server and local addresses have different families"); + } + + switch (mode) { + case sock_udp: + sock.fd = socket(family, SOCK_DGRAM, 0); + break; + case sock_tls: + if (pthread_mutex_init(&sock.lock, 0)) { + perf_log_fatal("pthread_mutex_init() failed"); + } + if ((sock.fd = socket(family, SOCK_STREAM, 0)) < 0) { + char __s[256]; + perf_log_fatal("socket: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + if (!ssl_ctx) { +#ifdef HAVE_TLS_METHOD + if (!(ssl_ctx = SSL_CTX_new(TLS_method()))) { + perf_log_fatal("SSL_CTX_new(): %s", ERR_error_string(ERR_get_error(), 0)); + } + if (!SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_2_VERSION)) { + perf_log_fatal("SSL_CTX_set_min_proto_version(TLS1_2_VERSION): %s", ERR_error_string(ERR_get_error(), 0)); + } +#else + if (!(ssl_ctx = SSL_CTX_new(SSLv23_client_method()))) { + perf_log_fatal("SSL_CTX_new(): %s", ERR_error_string(ERR_get_error(), 0)); + } +#endif + } + if (!(sock.ssl = SSL_new(ssl_ctx))) { + perf_log_fatal("SSL_new(): %s", ERR_error_string(ERR_get_error(), 0)); + } + if (!(ret = SSL_set_fd(sock.ssl, sock.fd))) { + perf_log_fatal("SSL_set_fd(): %s", ERR_error_string(SSL_get_error(sock.ssl, ret), 0)); + } + break; + case sock_tcp: + sock.fd = socket(family, SOCK_STREAM, 0); + break; + default: + perf_log_fatal("perf_net_opensocket(): invalid mode"); + } + + if (sock.fd == -1) { + char __s[256]; + perf_log_fatal("socket: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + +#if defined(AF_INET6) && defined(IPV6_V6ONLY) + if (family == AF_INET6) { + int on = 1; + + if (setsockopt(sock.fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) == -1) { + perf_log_warning("setsockopt(IPV6_V6ONLY) failed"); + } + } +#endif + + tmp = *local; + port = perf_sockaddr_port(&tmp); + if (port != 0 && offset != 0) { + port += offset; + if (port >= 0xFFFF) + perf_log_fatal("port %d out of range", port); + perf_sockaddr_setport(&tmp, port); + } + + if (bind(sock.fd, &tmp.sa.sa, tmp.length) == -1) { + char __s[256]; + perf_log_fatal("bind: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + + if (bufsize > 0) { + bufsize *= 1024; + + ret = setsockopt(sock.fd, SOL_SOCKET, SO_RCVBUF, + &bufsize, sizeof(bufsize)); + if (ret < 0) + perf_log_warning("setsockbuf(SO_RCVBUF) failed"); + + ret = setsockopt(sock.fd, SOL_SOCKET, SO_SNDBUF, + &bufsize, sizeof(bufsize)); + if (ret < 0) + perf_log_warning("setsockbuf(SO_SNDBUF) failed"); + } + + flags = fcntl(sock.fd, F_GETFL, 0); + if (flags < 0) + perf_log_fatal("fcntl(F_GETFL)"); + ret = fcntl(sock.fd, F_SETFL, flags | O_NONBLOCK); + if (ret < 0) + perf_log_fatal("fcntl(F_SETFL)"); + + if (mode == sock_tcp || mode == sock_tls) { + if (connect(sock.fd, &server->sa.sa, server->length)) { + if (errno == EINPROGRESS) { + sock.is_ready = 0; + } else { + char __s[256]; + perf_log_fatal("connect() failed: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + } + sock.recvbuf = malloc(TCP_RECV_BUF_SIZE); + sock.at = 0; + sock.have_more = 0; + sock.sendbuf = malloc(TCP_SEND_BUF_SIZE); + if (!sock.recvbuf || !sock.sendbuf) { + perf_log_fatal("perf_net_opensocket() failed: unable to allocate buffers"); + } + } + + return sock; +} + +ssize_t perf_net_recv(struct perf_net_socket* sock, void* buf, size_t len, int flags) +{ + switch (sock->mode) { + case sock_tls: { + ssize_t n; + uint16_t dnslen, dnslen2; + + if (!sock->have_more) { + if (pthread_mutex_lock(&sock->lock)) { + perf_log_fatal("pthread_mutex_lock() failed"); + } + if (!sock->is_ready) { + if (pthread_mutex_unlock(&sock->lock)) { + perf_log_fatal("pthread_mutex_unlock() failed"); + } + errno = EAGAIN; + return -1; + } + + n = SSL_read(sock->ssl, sock->recvbuf + sock->at, TCP_RECV_BUF_SIZE - sock->at); + if (n < 0) { + int err = SSL_get_error(sock->ssl, n); + if (pthread_mutex_unlock(&sock->lock)) { + perf_log_fatal("pthread_mutex_unlock() failed"); + } + if (err == SSL_ERROR_WANT_READ) { + errno = EAGAIN; + } else { + errno = EBADF; + } + return -1; + } + if (pthread_mutex_unlock(&sock->lock)) { + perf_log_fatal("pthread_mutex_unlock() failed"); + } + + sock->at += n; + if (sock->at < 3) { + errno = EAGAIN; + return -1; + } + } + + memcpy(&dnslen, sock->recvbuf, 2); + dnslen = ntohs(dnslen); + if (sock->at < dnslen + 2) { + errno = EAGAIN; + return -1; + } + memcpy(buf, sock->recvbuf + 2, len < dnslen ? len : dnslen); + memmove(sock->recvbuf, sock->recvbuf + 2 + dnslen, sock->at - 2 - dnslen); + sock->at -= 2 + dnslen; + + if (sock->at > 2) { + memcpy(&dnslen2, sock->recvbuf, 2); + dnslen2 = ntohs(dnslen2); + if (sock->at >= dnslen2 + 2) { + sock->have_more = 1; + return dnslen; + } + } + + sock->have_more = 0; + return dnslen; + } + case sock_tcp: { + ssize_t n; + uint16_t dnslen, dnslen2; + + if (!sock->have_more) { + n = recv(sock->fd, sock->recvbuf + sock->at, TCP_RECV_BUF_SIZE - sock->at, flags); + if (n < 0) { + if (errno == ECONNRESET) { + // Treat connection reset like try again until reconnection features are in + errno = EAGAIN; + } + return n; + } + sock->at += n; + if (sock->at < 3) { + errno = EAGAIN; + return -1; + } + } + + memcpy(&dnslen, sock->recvbuf, 2); + dnslen = ntohs(dnslen); + if (sock->at < dnslen + 2) { + errno = EAGAIN; + return -1; + } + memcpy(buf, sock->recvbuf + 2, len < dnslen ? len : dnslen); + memmove(sock->recvbuf, sock->recvbuf + 2 + dnslen, sock->at - 2 - dnslen); + sock->at -= 2 + dnslen; + + if (sock->at > 2) { + memcpy(&dnslen2, sock->recvbuf, 2); + dnslen2 = ntohs(dnslen2); + if (sock->at >= dnslen2 + 2) { + sock->have_more = 1; + return dnslen; + } + } + + sock->have_more = 0; + return dnslen; + } + default: + break; + } + + return recv(sock->fd, buf, len, flags); +} + +ssize_t perf_net_sendto(struct perf_net_socket* sock, const void* buf, size_t len, int flags, + const struct sockaddr* dest_addr, socklen_t addrlen) +{ + switch (sock->mode) { + case sock_tls: { + size_t send = len < TCP_SEND_BUF_SIZE - 2 ? len : (TCP_SEND_BUF_SIZE - 2); + // TODO: We only send what we can send, because we can't continue sending + uint16_t dnslen = htons(send); + ssize_t n; + + memcpy(sock->sendbuf, &dnslen, 2); + memcpy(sock->sendbuf + 2, buf, send); + if (pthread_mutex_lock(&sock->lock)) { + perf_log_fatal("pthread_mutex_lock() failed"); + } + n = SSL_write(sock->ssl, sock->sendbuf, send + 2); + if (n < 0) { + perf_log_warning("SSL_write(): %s", ERR_error_string(SSL_get_error(sock->ssl, n), 0)); + errno = EBADF; + } + if (pthread_mutex_unlock(&sock->lock)) { + perf_log_fatal("pthread_mutex_unlock() failed"); + } + + if (n > 0 && n < send + 2) { + sock->sending = n; + sock->flags = flags; + memcpy(&sock->dest_addr, dest_addr, addrlen); + sock->addrlen = addrlen; + sock->is_ready = 0; + errno = EINPROGRESS; + return -1; + } + + return n > 0 ? n - 2 : n; + } + case sock_tcp: { + size_t send = len < TCP_SEND_BUF_SIZE - 2 ? len : (TCP_SEND_BUF_SIZE - 2); + // TODO: We only send what we can send, because we can't continue sending + uint16_t dnslen = htons(send); + ssize_t n; + + memcpy(sock->sendbuf, &dnslen, 2); + memcpy(sock->sendbuf + 2, buf, send); + n = sendto(sock->fd, sock->sendbuf, send + 2, flags, dest_addr, addrlen); + + if (n > 0 && n < send + 2) { + sock->sending = n; + sock->flags = flags; + memcpy(&sock->dest_addr, dest_addr, addrlen); + sock->addrlen = addrlen; + sock->is_ready = 0; + errno = EINPROGRESS; + return -1; + } + + return n > 0 ? n - 2 : n; + } + default: + break; + } + return sendto(sock->fd, buf, len, flags, dest_addr, addrlen); +} + +int perf_net_close(struct perf_net_socket* sock) +{ + return close(sock->fd); +} + +int perf_net_sockeq(struct perf_net_socket* sock_a, struct perf_net_socket* sock_b) +{ + return sock_a->fd == sock_b->fd; +} + +enum perf_net_mode perf_net_parsemode(const char* mode) +{ + if (!strcmp(mode, "udp")) { + return sock_udp; + } else if (!strcmp(mode, "tcp")) { + return sock_tcp; + } else if (!strcmp(mode, "tls") || !strcmp(mode, "dot")) { + return sock_tls; + } + + perf_log_warning("invalid socket mode"); + perf_opt_usage(); + exit(1); +} + +int perf_net_sockready(struct perf_net_socket* sock, int pipe_fd, int64_t timeout) +{ + if (sock->is_ready) { + return 1; + } + + switch (sock->mode) { + case sock_tls: { + int ret; + + if (sock->sending) { + uint16_t dnslen; + ssize_t n; + + memcpy(&dnslen, sock->sendbuf, 2); + dnslen = ntohs(dnslen); + if (pthread_mutex_lock(&sock->lock)) { + perf_log_fatal("pthread_mutex_lock() failed"); + } + n = SSL_write(sock->ssl, sock->sendbuf + sock->sending, dnslen + 2 - sock->sending); + if (n < 1) { + if (n < 0) { + perf_log_warning("SSL_write(): %s", ERR_error_string(SSL_get_error(sock->ssl, n), 0)); + errno = EBADF; + } + if (pthread_mutex_unlock(&sock->lock)) { + perf_log_fatal("pthread_mutex_unlock() failed"); + } + return -1; + } + if (pthread_mutex_unlock(&sock->lock)) { + perf_log_fatal("pthread_mutex_unlock() failed"); + } + sock->sending += n; + if (sock->sending < dnslen + 2) { + errno = EINPROGRESS; + return -1; + } + sock->sending = 0; + sock->is_ready = 1; + return 1; + } + + if (!sock->is_ssl_ready) { + switch (perf_os_waituntilanywritable(sock, 1, pipe_fd, timeout)) { + case PERF_R_TIMEDOUT: + return -1; + case PERF_R_SUCCESS: { + int error = 0; + socklen_t len = (socklen_t)sizeof(error); + + getsockopt(sock->fd, SOL_SOCKET, SO_ERROR, (void*)&error, &len); + if (error != 0) { + if (error == EINPROGRESS +#if EWOULDBLOCK != EAGAIN + || error == EWOULDBLOCK +#endif + || error == EAGAIN) { + return 0; + } + return -1; + } + } + } + sock->is_ssl_ready = 1; + } + + if (pthread_mutex_lock(&sock->lock)) { + perf_log_fatal("pthread_mutex_lock() failed"); + } + ret = SSL_connect(sock->ssl); + if (!ret) { + perf_log_warning("SSL_connect(): %s", ERR_error_string(SSL_get_error(sock->ssl, ret), 0)); + if (pthread_mutex_unlock(&sock->lock)) { + perf_log_fatal("pthread_mutex_unlock() failed"); + } + return -1; + } + if (ret < 0) { + int err = SSL_get_error(sock->ssl, ret); + if (pthread_mutex_unlock(&sock->lock)) { + perf_log_fatal("pthread_mutex_unlock() failed"); + } + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + return 0; + } + perf_log_warning("SSL_connect(): %s", ERR_error_string(err, 0)); + return -1; + } + sock->is_ready = 1; + if (pthread_mutex_unlock(&sock->lock)) { + perf_log_fatal("pthread_mutex_unlock() failed"); + } + return 1; + } + case sock_tcp: + if (sock->sending) { + uint16_t dnslen; + ssize_t n; + + memcpy(&dnslen, sock->sendbuf, 2); + dnslen = ntohs(dnslen); + n = sendto(sock->fd, sock->sendbuf + sock->sending, dnslen + 2 - sock->sending, sock->flags, (struct sockaddr*)&sock->dest_addr, sock->addrlen); + if (n < 1) { + return -1; + } + sock->sending += n; + if (sock->sending < dnslen + 2) { + errno = EINPROGRESS; + return -1; + } + sock->sending = 0; + sock->is_ready = 1; + return 1; + } + + switch (perf_os_waituntilanywritable(sock, 1, pipe_fd, timeout)) { + case PERF_R_TIMEDOUT: + return -1; + case PERF_R_SUCCESS: { + int error = 0; + socklen_t len = (socklen_t)sizeof(error); + + getsockopt(sock->fd, SOL_SOCKET, SO_ERROR, (void*)&error, &len); + if (error != 0) { + if (error == EINPROGRESS +#if EWOULDBLOCK != EAGAIN + || error == EWOULDBLOCK +#endif + || error == EAGAIN) { + return 0; + } + return -1; + } + sock->is_ready = 1; + return 1; + } + } + break; + default: + break; + } + + return -1; +} diff --git a/src/net.h b/src/net.h new file mode 100644 index 0000000..1339af6 --- /dev/null +++ b/src/net.h @@ -0,0 +1,84 @@ +/* + * 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. + */ + +#ifndef PERF_NET_H +#define PERF_NET_H 1 + +#include <sys/types.h> +#include <sys/socket.h> +#include <openssl/ssl.h> +#include <pthread.h> +#include <netinet/in.h> + +struct perf_sockaddr { + union { + struct sockaddr sa; + struct sockaddr_in sin; + struct sockaddr_in6 sin6; + } sa; + socklen_t length; +}; +typedef struct perf_sockaddr perf_sockaddr_t; + +enum perf_net_mode { + sock_none, + sock_file, + sock_pipe, + sock_udp, + sock_tcp, + sock_tls +}; + +struct perf_net_socket { + enum perf_net_mode mode; + int fd, have_more, is_ready, flags, is_ssl_ready; + char* recvbuf; + size_t at, sending; + char* sendbuf; + struct sockaddr_storage dest_addr; + socklen_t addrlen; + SSL* ssl; + pthread_mutex_t lock; +}; + +void perf_sockaddr_fromin(perf_sockaddr_t* sockaddr, const struct in_addr* in, in_port_t port); +void perf_sockaddr_fromin6(perf_sockaddr_t* sockaddr, const struct in6_addr* in, in_port_t port); +in_port_t perf_sockaddr_port(const perf_sockaddr_t* sockaddr); +void perf_sockaddr_setport(perf_sockaddr_t* sockaddr, in_port_t port); +void perf_sockaddr_format(const perf_sockaddr_t* sockaddr, char* buf, size_t len); + +ssize_t perf_net_recv(struct perf_net_socket* sock, void* buf, size_t len, int flags); +ssize_t perf_net_sendto(struct perf_net_socket* sock, const void* buf, size_t len, int flags, + const struct sockaddr* dest_addr, socklen_t addrlen); + +int perf_net_close(struct perf_net_socket* sock); +int perf_net_sockeq(struct perf_net_socket* sock_a, struct perf_net_socket* sock_b); + +int perf_net_parsefamily(const char* family); + +void perf_net_parseserver(int family, const char* name, unsigned int port, perf_sockaddr_t* addr); +void perf_net_parselocal(int family, const char* name, unsigned int port, perf_sockaddr_t* addr); + +struct perf_net_socket perf_net_opensocket(enum perf_net_mode mode, const perf_sockaddr_t* server, const perf_sockaddr_t* local, unsigned int offset, int bufsize); + +enum perf_net_mode perf_net_parsemode(const char* mode); + +int perf_net_sockready(struct perf_net_socket* sock, int pipe_fd, int64_t timeout); + +#endif diff --git a/src/opt.c b/src/opt.c new file mode 100644 index 0000000..41dcd5e --- /dev/null +++ b/src/opt.c @@ -0,0 +1,234 @@ +/* + * 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 "opt.h" + +#include "log.h" +#include "util.h" +#include "result.h" + +#include <getopt.h> +#include <stdbool.h> +#include <stdio.h> +#include <inttypes.h> +#include <stdlib.h> +#include <string.h> +#include <stdint.h> +#include <errno.h> +#include <netinet/in.h> + +#define MAX_OPTS 64 +#define LINE_LENGTH 80 + +typedef struct { + char c; + perf_opttype_t type; + const char* desc; + const char* help; + const char* defval; + char defvalbuf[32]; + union { + void* valp; + char** stringp; + bool* boolp; + unsigned int* uintp; + uint64_t* uint64p; + double* doublep; + in_port_t* portp; + } u; +} opt_t; + +static opt_t opts[MAX_OPTS]; +static unsigned int nopts; +static char optstr[MAX_OPTS * 2 + 2 + 1] = { 0 }; +extern const char* progname; + +void perf_opt_add(char c, perf_opttype_t type, const char* desc, const char* help, + const char* defval, void* valp) +{ + opt_t* opt; + + if (nopts == MAX_OPTS) { + perf_log_fatal("too many defined options"); + return; + } + opt = &opts[nopts++]; + opt->c = c; + opt->type = type; + opt->desc = desc; + opt->help = help; + if (defval != NULL) { + opt->defvalbuf[sizeof(opt->defvalbuf) - 1] = 0; + strncpy(opt->defvalbuf, defval, sizeof(opt->defvalbuf)); + if (opt->defvalbuf[sizeof(opt->defvalbuf) - 1]) { + perf_log_fatal("perf_opt_add(): defval too large"); + return; + } + opt->defval = opt->defvalbuf; + } else { + opt->defval = NULL; + } + opt->u.valp = valp; + + char newoptstr[sizeof(optstr) + 2]; + snprintf(newoptstr, sizeof(newoptstr), "%s%c%s", optstr, c, (type == perf_opt_boolean ? "" : ":")); + memcpy(optstr, newoptstr, sizeof(optstr) - 1); + optstr[sizeof(optstr) - 1] = 0; +} + +void perf_opt_usage(void) +{ + unsigned int prefix_len, position, arg_len, i, j; + + prefix_len = fprintf(stderr, "Usage: %s", progname); + position = prefix_len; + for (i = 0; i < nopts; i++) { + arg_len = 6; + if (opts[i].desc != NULL) + arg_len += strlen(opts[i].desc) + 1; + if (LINE_LENGTH - position - 1 < arg_len) { + fprintf(stderr, "\n"); + for (j = 0; j < prefix_len; j++) + fprintf(stderr, " "); + position = prefix_len; + } + fprintf(stderr, " [-%c", opts[i].c); + if (opts[i].desc != NULL) + fprintf(stderr, " %s", opts[i].desc); + fprintf(stderr, "]"); + position += arg_len; + } + fprintf(stderr, "\n"); + + for (i = 0; i < nopts; i++) { + fprintf(stderr, " -%c %s", opts[i].c, opts[i].help); + if (opts[i].defval) + fprintf(stderr, " (default: %s)", opts[i].defval); + fprintf(stderr, "\n"); + } +} + +static uint32_t +parse_uint(const char* desc, const char* str, + unsigned int min, unsigned int max) +{ + unsigned long int val; + uint32_t ret; + char* endptr = 0; + + errno = 0; + val = strtoul(str, &endptr, 10); + if (!errno && str && *str && endptr && !*endptr && val <= UINT32_MAX) { + ret = (uint32_t)val; + if (ret >= min && ret <= max) { + return ret; + } + } + + fprintf(stderr, "invalid %s: %s\n", desc, str); + perf_opt_usage(); + exit(1); +} + +static double +parse_double(const char* desc, const char* str) +{ + const char* s; + char c; + bool seen_dot = false; + + s = str; + while (*s != 0) { + c = *s++; + if (c == '.') { + if (seen_dot) + goto fail; + seen_dot = true; + } else if (c < '0' || c > '9') { + goto fail; + } + } + + return atof(str); + +fail: + fprintf(stderr, "invalid %s: %s\n", desc, str); + perf_opt_usage(); + exit(1); +} + +static uint64_t +parse_timeval(const char* desc, const char* str) +{ + return MILLION * parse_double(desc, str); +} + +void perf_opt_parse(int argc, char** argv) +{ + int c; + opt_t* opt; + unsigned int i; + + perf_opt_add('h', perf_opt_boolean, NULL, "print this help", NULL, NULL); + + while ((c = getopt(argc, argv, optstr)) != -1) { + for (i = 0; i < nopts; i++) { + if (opts[i].c == c) + break; + } + if (i == nopts) { + perf_opt_usage(); + exit(1); + } + if (c == 'h') { + perf_opt_usage(); + exit(0); + } + opt = &opts[i]; + switch (opt->type) { + case perf_opt_string: + *opt->u.stringp = optarg; + break; + case perf_opt_boolean: + *opt->u.boolp = true; + break; + case perf_opt_uint: + *opt->u.uintp = parse_uint(opt->desc, optarg, + 1, 0xFFFFFFFF); + break; + case perf_opt_timeval: + *opt->u.uint64p = parse_timeval(opt->desc, optarg); + break; + case perf_opt_double: + *opt->u.doublep = parse_double(opt->desc, optarg); + break; + case perf_opt_port: + *opt->u.portp = parse_uint(opt->desc, optarg, + 0, 0xFFFF); + break; + } + } + if (optind != argc) { + fprintf(stderr, "unexpected argument %s\n", argv[optind]); + perf_opt_usage(); + exit(1); + } +} diff --git a/src/opt.h b/src/opt.h new file mode 100644 index 0000000..829ba5c --- /dev/null +++ b/src/opt.h @@ -0,0 +1,39 @@ +/* + * 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. + */ + +#ifndef PERF_OPT_H +#define PERF_OPT_H 1 + +typedef enum { + perf_opt_string, + perf_opt_boolean, + perf_opt_uint, + perf_opt_timeval, + perf_opt_double, + perf_opt_port, +} perf_opttype_t; + +void perf_opt_add(char c, perf_opttype_t type, const char* desc, const char* help, + const char* defval, void* valp); + +void perf_opt_usage(void); + +void perf_opt_parse(int argc, char** argv); + +#endif diff --git a/src/os.c b/src/os.c new file mode 100644 index 0000000..addcdac --- /dev/null +++ b/src/os.c @@ -0,0 +1,150 @@ +/* + * 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 "os.h" + +#include "log.h" +#include "util.h" + +#include <errno.h> +#include <signal.h> +#include <inttypes.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <poll.h> + +void perf_os_blocksignal(int sig, bool block) +{ + sigset_t sset; + int op; + + op = block ? SIG_BLOCK : SIG_UNBLOCK; + + if (sigemptyset(&sset) < 0 || sigaddset(&sset, sig) < 0 || pthread_sigmask(op, &sset, NULL) < 0) { + char __s[256]; + perf_log_fatal("pthread_sigmask: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } +} + +void perf_os_handlesignal(int sig, void (*handler)(int)) +{ + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handler; + + if (sigfillset(&sa.sa_mask) < 0 || sigaction(sig, &sa, NULL) < 0) { + char __s[256]; + perf_log_fatal("sigaction: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } +} + +perf_result_t +perf_os_waituntilreadable(struct perf_net_socket* sock, int pipe_fd, int64_t timeout) +{ + return perf_os_waituntilanyreadable(sock, 1, pipe_fd, timeout); +} + +perf_result_t +perf_os_waituntilanyreadable(struct perf_net_socket* socks, unsigned int nfds, int pipe_fd, + int64_t timeout) +{ + struct pollfd fds[nfds + 1]; + size_t i; + int to, n; + + for (i = 0; i < nfds; i++) { + if (socks[i].have_more) + return (PERF_R_SUCCESS); + + fds[i].fd = socks[i].fd; + fds[i].events = POLLIN; + } + + fds[nfds].fd = pipe_fd; + fds[nfds].events = POLLIN; + + if (timeout < 0) { + to = -1; + } else { + to = timeout / 1000; + if (timeout && !to) { + to = 1; + } + } + + n = poll(fds, nfds + 1, to); + if (n < 0) { + if (errno != EINTR) { + char __s[256]; + perf_log_fatal("select(): %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + return (PERF_R_CANCELED); + } else if (n == 0) { + return (PERF_R_TIMEDOUT); + } else if (fds[nfds].revents & POLLIN) { + return (PERF_R_CANCELED); + } else { + return (PERF_R_SUCCESS); + } +} + +perf_result_t +perf_os_waituntilanywritable(struct perf_net_socket* socks, unsigned int nfds, int pipe_fd, + int64_t timeout) +{ + struct pollfd fds[nfds + 1]; + size_t i; + int to, n; + + for (i = 0; i < nfds; i++) { + fds[i].fd = socks[i].fd; + fds[i].events = POLLOUT; + } + + fds[nfds].fd = pipe_fd; + fds[nfds].events = POLLIN; + + if (timeout < 0) { + to = -1; + } else { + to = timeout / 1000; + if (timeout && !to) { + to = 1; + } + } + + n = poll(fds, nfds + 1, to); + if (n < 0) { + if (errno != EINTR) { + char __s[256]; + perf_log_fatal("select(): %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + return (PERF_R_CANCELED); + } else if (n == 0) { + return (PERF_R_TIMEDOUT); + } else if (fds[nfds].revents & POLLIN) { + return (PERF_R_CANCELED); + } else { + return (PERF_R_SUCCESS); + } +} diff --git a/src/os.h b/src/os.h new file mode 100644 index 0000000..90ecfaf --- /dev/null +++ b/src/os.h @@ -0,0 +1,44 @@ +/* + * 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 "net.h" +#include "result.h" + +#ifndef PERF_OS_H +#define PERF_OS_H 1 + +#include <inttypes.h> +#include <stdbool.h> + +void perf_os_blocksignal(int sig, bool block); + +void perf_os_handlesignal(int sig, void (*handler)(int)); + +perf_result_t +perf_os_waituntilreadable(struct perf_net_socket* sock, int pipe_fd, int64_t timeout); + +perf_result_t +perf_os_waituntilanyreadable(struct perf_net_socket* socks, unsigned int nfds, int pipe_fd, + int64_t timeout); + +perf_result_t +perf_os_waituntilanywritable(struct perf_net_socket* socks, unsigned int nfds, int pipe_fd, + int64_t timeout); + +#endif diff --git a/src/qtype.c b/src/qtype.c new file mode 100644 index 0000000..78aadcf --- /dev/null +++ b/src/qtype.c @@ -0,0 +1,117 @@ +/* + * 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 "qtype.h" + +const perf_qtype_t qtype_table[] = { + { "A", 1 }, + { "NS", 2 }, + { "MD", 3 }, + { "MF", 4 }, + { "CNAME", 5 }, + { "SOA", 6 }, + { "MB", 7 }, + { "MG", 8 }, + { "MR", 9 }, + { "NULL", 10 }, + { "WKS", 11 }, + { "PTR", 12 }, + { "HINFO", 13 }, + { "MINFO", 14 }, + { "MX", 15 }, + { "TXT", 16 }, + { "RP", 17 }, + { "AFSDB", 18 }, + { "X25", 19 }, + { "ISDN", 20 }, + { "RT", 21 }, + { "NSAP", 22 }, + { "NSAP-PTR", 23 }, + { "SIG", 24 }, + { "KEY", 25 }, + { "PX", 26 }, + { "GPOS", 27 }, + { "AAAA", 28 }, + { "LOC", 29 }, + { "NXT", 30 }, + { "EID", 31 }, + { "NIMLOC", 32 }, + { "SRV", 33 }, + { "ATMA", 34 }, + { "NAPTR", 35 }, + { "KX", 36 }, + { "CERT", 37 }, + { "A6", 38 }, + { "DNAME", 39 }, + { "SINK", 40 }, + { "OPT", 41 }, + { "APL", 42 }, + { "DS", 43 }, + { "SSHFP", 44 }, + { "IPSECKEY", 45 }, + { "RRSIG", 46 }, + { "NSEC", 47 }, + { "DNSKEY", 48 }, + { "DHCID", 49 }, + { "NSEC3", 50 }, + { "NSEC3PARAM", 51 }, + { "TLSA", 52 }, + { "SMIMEA", 53 }, + { "Unassigned", 54 }, + { "HIP", 55 }, + { "NINFO", 56 }, + { "RKEY", 57 }, + { "TALINK", 58 }, + { "CDS", 59 }, + { "CDNSKEY", 60 }, + { "OPENPGPKEY", 61 }, + { "CSYNC", 62 }, + { "ZONEMD", 63 }, + { "SVCB", 64 }, + { "HTTPS", 65 }, + { "SPF", 99 }, + { "UINFO", 100 }, + { "UID", 101 }, + { "GID", 102 }, + { "UNSPEC", 103 }, + { "NID", 104 }, + { "L32", 105 }, + { "L64", 106 }, + { "LP", 107 }, + { "EUI48", 108 }, + { "EUI64", 109 }, + { "TKEY", 249 }, + { "TSIG", 250 }, + { "IXFR", 251 }, + { "AXFR", 252 }, + { "MAILB", 253 }, + { "MAILA", 254 }, + { "*", 255 }, + { "URI", 256 }, + { "CAA", 257 }, + { "AVC", 258 }, + { "DOA", 259 }, + { "AMTRELAY", 260 }, + { "TA", 32768 }, + { "DLV", 32769 }, + { "Reserved", 65535 }, + { 0, 0 } +}; diff --git a/src/qtype.h b/src/qtype.h new file mode 100644 index 0000000..8c1a17d --- /dev/null +++ b/src/qtype.h @@ -0,0 +1,32 @@ +/* + * 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. + */ + +#ifndef PERF_QTYPE_H +#define PERF_QTYPE_H 1 + +#include <stdint.h> + +typedef struct perf_qtype { + char* type; + uint16_t value; +} perf_qtype_t; + +extern const perf_qtype_t qtype_table[]; + +#endif diff --git a/src/resperf-report b/src/resperf-report new file mode 100755 index 0000000..b3b7697 --- /dev/null +++ b/src/resperf-report @@ -0,0 +1,112 @@ +#!/bin/sh +# +# 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. +# +# Driver script to run resperf and generate an HTML report of +# the results, with graphs. +# + +# Program locations - change these if not in $PATH +resperf=resperf +gnuplot=gnuplot + +# The gnuplot terminal type. This determines the image format for the +# plots; "png" or "gif" will both work as long as the corresponding +# terminal support is compiled into your copy of gnuplot. +terminal=png + +# Create a unique ID for this report +id=`date '+%Y%m%d-%H%M'` + +# Set up file names +reportfile="$id.html" +outputfile="$id.output" +plotfile="$id.gnuplot" +rate_graph="$id.rate.$terminal" +latency_graph="$id.latency.$terminal" + +# Run the test +$resperf -P "$plotfile" "$@" >"$outputfile" 2>&1 || + { echo "`basename $0`: error running resperf:" >&2; + cat $outputfile >&2; + exit 1; + } + +# Create plots + +if + $gnuplot <<EOF +set terminal $terminal +set output "$rate_graph" +set title "Query / response / failure rate" +set key top left +set xlabel "Time (seconds)" +set yrange [0:] +plot \ +"$plotfile" using 1:3 title "Queries sent per second" with lines, \ +"$plotfile" using 1:4 title "Total responses received per second" with lines, \ +"$plotfile" using 1:5 title "Failure responses received per second" with lines +EOF +then + : +else + echo "`basename $0`: error running gnuplot" >&2; exit 1; +fi + +if + $gnuplot <<EOF +set terminal $terminal +set output "$latency_graph" +set title "Latency" +set key top left +set xlabel "Time (seconds)" +set yrange [0:] +plot \ +"$plotfile" using 1:6 title "Average latency (seconds)" with lines +EOF +then + : +else + echo "`basename $0`: error running gnuplot" >&2; exit 1; +fi + +# Generate the report + +exec >"$reportfile" + +cat <<EOF +<html><head></head><body> +<h1>Resperf report $id</h1> +<h2>Resperf output</h2> +<pre> +EOF +cat "$outputfile" +cat <<EOF +</pre> +EOF + +cat <<EOF +<h2>Plots</h2> +<p> +<img src="$rate_graph" /> +<img src="$latency_graph" /> +</p> +</body></html> +EOF + +echo "Done, report is in $reportfile" >&2 diff --git a/src/resperf.1.in b/src/resperf.1.in new file mode 100644 index 0000000..463a5ac --- /dev/null +++ b/src/resperf.1.in @@ -0,0 +1,647 @@ +.\" 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. +.TH resperf 1 "@PACKAGE_VERSION@" "resperf" +.SH NAME +resperf \- test the resolution performance of a caching DNS server +.SH SYNOPSIS +.hy 0 +.ad l +\fBresperf\-report\fR\ [\fB\-a\ \fIlocal_addr\fB\fR] +[\fB\-d\ \fIdatafile\fB\fR] +[\fB\-M\ \fImode\fB\fR] +[\fB\-s\ \fIserver_addr\fB\fR] +[\fB\-p\ \fIport\fB\fR] +[\fB\-x\ \fIlocal_port\fB\fR] +[\fB\-t\ \fItimeout\fB\fR] +[\fB\-b\ \fIbufsize\fB\fR] +[\fB\-f\ \fIfamily\fB\fR] +[\fB\-e\fR] +[\fB\-D\fR] +[\fB\-y\ \fI[alg:]name:secret\fB\fR] +[\fB\-h\fR] +[\fB\-i\ \fIinterval\fB\fR] +[\fB\-m\ \fImax_qps\fB\fR] +[\fB\-r\ \fIrampup_time\fB\fR] +[\fB\-c\ \fIconstant_traffic_time\fB\fR] +[\fB\-L\ \fImax_loss\fB\fR] +[\fB\-C\ \fIclients\fB\fR] +[\fB\-q\ \fImax_outstanding\fB\fR] +[\fB\-v\fR] +.ad +.hy +.hy 0 +.ad l + +\fBresperf\fR\ [\fB\-a\ \fIlocal_addr\fB\fR] +[\fB\-d\ \fIdatafile\fB\fR] +[\fB\-M\ \fImode\fB\fR] +[\fB\-s\ \fIserver_addr\fB\fR] +[\fB\-p\ \fIport\fB\fR] +[\fB\-x\ \fIlocal_port\fB\fR] +[\fB\-t\ \fItimeout\fB\fR] +[\fB\-b\ \fIbufsize\fB\fR] +[\fB\-f\ \fIfamily\fB\fR] +[\fB\-e\fR] +[\fB\-D\fR] +[\fB\-y\ \fI[alg:]name:secret\fB\fR] +[\fB\-h\fR] +[\fB\-i\ \fIinterval\fB\fR] +[\fB\-m\ \fImax_qps\fB\fR] +[\fB\-P\ \fIplot_data_file\fB\fR] +[\fB\-r\ \fIrampup_time\fB\fR] +[\fB\-c\ \fIconstant_traffic_time\fB\fR] +[\fB\-L\ \fImax_loss\fB\fR] +[\fB\-C\ \fIclients\fB\fR] +[\fB\-q\ \fImax_outstanding\fB\fR] +[\fB\-v\fR] +.ad +.hy +.SH DESCRIPTION +\fBresperf\fR is a companion tool to \fBdnsperf\fR. \fBdnsperf\fR was +primarily designed for benchmarking authoritative servers, and it does not +work well with caching servers that are talking to the live Internet. One +reason for this is that dnsperf uses a "self-pacing" approach, which is +based on the assumption that you can keep the server 100% busy simply by +sending it a small burst of back-to-back queries to fill up network buffers, +and then send a new query whenever you get a response back. This approach +works well for authoritative servers that process queries in order and one +at a time; it also works pretty well for a caching server in a closed +laboratory environment talking to a simulated Internet that's all on the +same LAN. Unfortunately, it does not work well with a caching server talking +to the actual Internet, which may need to work on thousands of queries in +parallel to achieve its maximum throughput. There have been numerous +attempts to use dnsperf (or its predecessor, queryperf) for benchmarking +live caching servers, usually with poor results. Therefore, a separate tool +designed specifically for caching servers is needed. +.SS "How resperf works" +Unlike the "self-pacing" approach of dnsperf, resperf works by sending DNS +queries at a controlled, steadily increasing rate. By default, resperf will +send traffic for 60 seconds, linearly increasing the amount of traffic from +zero to 100,000 queries per second. + +During the test, resperf listens for responses from the server and keeps +track of response rates, failure rates, and latencies. It will also continue +listening for responses for an additional 40 seconds after it has stopped +sending traffic, so that there is time for the server to respond to the last +queries sent. This time period was chosen to be longer than the overall +query timeout of both Nominum CacheServe and current versions of BIND. + +If the test is successful, the query rate will at some point exceed the +capacity of the server and queries will be dropped, causing the response +rate to stop growing or even decrease as the query rate increases. + +The result of the test is a set of measurements of the query rate, response +rate, failure response rate, and average query latency as functions of time. +.SS "What you will need" +Benchmarking a live caching server is serious business. A fast caching +server like Nominum CacheServe, resolving a mix of cacheable and +non-cacheable queries typical of ISP customer traffic, is capable of +resolving well over 1,000,000 queries per second. In the process, it will +send more than 40,000 queries per second to authoritative servers on the +Internet, and receive responses to most of them. Assuming an average request +size of 50 bytes and a response size of 150 bytes, this amounts to some 1216 +Mbps of outgoing and 448 Mbps of incoming traffic. If your Internet +connection can't handle the bandwidth, you will end up measuring the speed +of the connection, not the server, and may saturate the connection causing a +degradation in service for other users. + +Make sure there is no stateful firewall between the server and the Internet, +because most of them can't handle the amount of UDP traffic the test will +generate and will end up dropping packets, skewing the test results. Some +will even lock up or crash. + +You should run resperf on a machine separate from the server under test, on +the same LAN. Preferably, this should be a Gigabit Ethernet network. The +machine running resperf should be at least as fast as the machine being +tested; otherwise, it may end up being the bottleneck. + +There should be no other applications running on the machine running +resperf. Performance testing at the traffic levels involved is essentially a +hard real-time application - consider the fact that at a query rate of +100,000 queries per second, if resperf gets delayed by just 1/100 of a +second, 1000 incoming UDP packets will arrive in the meantime. This is more +than most operating systems will buffer, which means packets will be +dropped. + +Because the granularity of the timers provided by operating systems is +typically too coarse to accurately schedule packet transmissions at +sub-millisecond intervals, resperf will busy-wait between packet +transmissions, constantly polling for responses in the meantime. Therefore, +it is normal for resperf to consume 100% CPU during the whole test run, even +during periods where query rates are relatively low. + +You will also need a set of test queries in the \fBdnsperf\fR file format. +See the \fBdnsperf\fR man page for instructions on how to construct this +query file. To make the test as realistic as possible, the queries should be +derived from recorded production client DNS traffic, without removing +duplicate queries or other filtering. With the default settings, resperf +will use up to 3 million queries in each test run. + +If the caching server to be tested has a configurable limit on the number of +simultaneous resolutions, like the \fBmax\-recursive\-clients\fR statement +in Nominum CacheServe or the \fBrecursive\-clients\fR option in BIND 9, you +will probably have to increase it. As a starting point, we recommend a value +of 10000 for Nominum CacheServe and 100000 for BIND 9. Should the limit be +reached, it will show up in the plots as an increase in the number of +failure responses. + +The server being tested should be restarted at the beginning of each test to +make sure it is starting with an empty cache. If the cache already contains +data from a previous test run that used the same set of queries, almost all +queries will be answered from the cache, yielding inflated performance +numbers. + +To use the \fBresperf\-report\fR script, you need to have \fBgnuplot\fR +installed. Make sure your installed version of \fBgnuplot\fR supports the +png terminal driver. If your \fBgnuplot\fR doesn't support png but does +support gif, you can change the line saying terminal=png in the +\fBresperf\-report\fR script to terminal=gif. +.SS "Running the test" +Resperf is typically invoked via the \fBresperf\-report\fR script, which +will run \fBresperf\fR with its output redirected to a file and then +automatically generate an illustrated report in HTML format. Command line +arguments given to resperf-report will be passed on unchanged to resperf. + +When running resperf-report, you will need to specify at least the server IP +address and the query data file. A typical invocation will look like +.RS +.hy 0 + +.nf +resperf\-report \-s 10.0.0.2 \-d queryfile +.fi +.hy +.RE + +With default settings, the test run will take at most 100 seconds (60 +seconds of ramping up traffic and then 40 seconds of waiting for responses), +but in practice, the 60-second traffic phase will usually be cut short. To +be precise, resperf can transition from the traffic-sending phase to the +waiting-for-responses phase in three different ways: +.IP \(bu 2 +Running for the full allotted time and successfully reaching the maximum +query rate (by default, 60 seconds and 100,000 qps, respectively). Since +this is a very high query rate, this will rarely happen (with today's +hardware); one of the other two conditions listed below will usually occur +first. +.IP \(bu 2 +Exceeding 65,536 outstanding queries. This often happens as a result of +(successfully) exceeding the capacity of the server being tested, causing +the excess queries to be dropped. The limit of 65,536 queries comes from the +number of possible values for the ID field in the DNS packet. Resperf needs +to allocate a unique ID for each outstanding query, and is therefore unable +to send further queries if the set of possible IDs is exhausted. +.IP \(bu 2 +When resperf finds itself unable to send queries fast enough. Resperf will +notice if it is falling behind in its scheduled query transmissions, and if +this backlog reaches 1000 queries, it will print a message like "Fell behind +by 1000 queries" (or whatever the actual number is at the time) and stop +sending traffic. +.PP +Regardless of which of the above conditions caused the traffic-sending phase +of the test to end, you should examine the resulting plots to make sure the +server's response rate is flattening out toward the end of the test. If it +is not, then you are not loading the server enough. If you are getting the +"Fell behind" message, make sure that the machine running resperf is fast +enough and has no other applications running. + +You should also monitor the CPU usage of the server under test. It should +reach close to 100% CPU at the point of maximum traffic; if it does not, you +most likely have a bottleneck in some other part of your test setup, for +example, your external Internet connection. + +The report generated by \fBresperf\-report\fR will be stored with a unique +file name based on the current date and time, e.g., +\fI20060812-1550.html\fR. The PNG images of the plots and other auxiliary +files will be stored in separate files beginning with the same date-time +string. To view the report, simply open the \fI.html\fR file in a web +browser. + +If you need to copy the report to a separate machine for viewing, make sure +to copy the .png files along with the .html file (or simply copy all the +files, e.g., using scp 20060812-1550.* host:directory/). +.SS "Interpreting the report" +The \fI.html\fR file produced by \fBresperf\-report\fR consists of two +sections. The first section, "Resperf output", contains output from the +\fBresperf\fR program such as progress messages, a summary of the command +line arguments, and summary statistics. The second section, "Plots", +contains two plots generated by \fBgnuplot\fR: "Query/response/failure rate" +and "Latency". + +The "Query/response/failure rate" plot contains three graphs. The "Queries +sent per second" graph shows the amount of traffic being sent to the server; +this should be very close to a straight diagonal line, reflecting the linear +ramp-up of traffic. + +The "Total responses received per second" graph shows how many of the +queries received a response from the server. All responses are counted, +whether successful (NOERROR or NXDOMAIN) or not (e.g., SERVFAIL). + +The "Failure responses received per second" graph shows how many of the +queries received a failure response. A response is considered to be a +failure if its RCODE is neither NOERROR nor NXDOMAIN. + +By visually inspecting the graphs, you can get an idea of how the server +behaves under increasing load. The "Total responses received per second" +graph will initially closely follow the "Queries sent per second" graph +(often rendering it invisible in the plot as the two graphs are plotted on +top of one another), but when the load exceeds the server's capacity, the +"Total responses received per second" graph may diverge from the "Queries +sent per second" graph and flatten out, indicating that some of the queries +are being dropped. + +The "Failure responses received per second" graph will normally show a +roughly linear ramp close to the bottom of the plot with some random +fluctuation, since typical query traffic will contain some small percentage +of failing queries randomly interspersed with the successful ones. As the +total traffic increases, the number of failures will increase +proportionally. + +If the "Failure responses received per second" graph turns sharply upwards, +this can be another indication that the load has exceeded the server's +capacity. This will happen if the server reacts to overload by sending +SERVFAIL responses rather than by dropping queries. Since Nominum CacheServe +and BIND 9 will both respond with SERVFAIL when they exceed their +\fBmax\-recursive\-clients\fR or \fBrecursive\-clients\fR limit, +respectively, a sudden increase in the number of failures could mean that +the limit needs to be increased. + +The "Latency" plot contains a single graph marked "Average latency". This +shows how the latency varies during the course of the test. Typically, the +latency graph will exhibit a downwards trend because the cache hit rate +improves as ever more responses are cached during the test, and the latency +for a cache hit is much smaller than for a cache miss. The latency graph is +provided as an aid in determining the point where the server gets +overloaded, which can be seen as a sharp upwards turn in the graph. The +latency graph is not intended for making absolute latency measurements or +comparisons between servers; the latencies shown in the graph are not +representative of production latencies due to the initially empty cache and +the deliberate overloading of the server towards the end of the test. + +Note that all measurements are displayed on the plot at the horizontal +position corresponding to the point in time when the query was sent, not +when the response (if any) was received. This makes it it easy to compare +the query and response rates; for example, if no queries are dropped, the +query and response graphs will be identical. As another example, if the plot +shows 10% failure responses at t=5 seconds, this means that 10% of the +queries sent at t=5 seconds eventually failed, not that 10% of the responses +received at t=5 seconds were failures. +.SS "Determining the server's maximum throughput" +Often, the goal of running \fBresperf\fR is to determine the server's +maximum throughput, in other words, the number of queries per second it is +capable of handling. This is not always an easy task, because as a server is +driven into overload, the service it provides may deteriorate gradually, and +this deterioration can manifest itself either as queries being dropped, as +an increase in the number of SERVFAIL responses, or an increase in latency. +The maximum throughput may be defined as the highest level of traffic at +which the server still provides an acceptable level of service, but that +means you first need to decide what an acceptable level of service means in +terms of packet drop percentage, SERVFAIL percentage, and latency. + +The summary statistics in the "Resperf output" section of the report +contains a "Maximum throughput" value which by default is determined from +the maximum rate at which the server was able to return responses, without +regard to the number of queries being dropped or failing at that point. This +method of throughput measurement has the advantage of simplicity, but it may +or may not be appropriate for your needs; the reported value should always +be validated by a visual inspection of the graphs to ensure that service has +not already deteriorated unacceptably before the maximum response rate is +reached. It may also be helpful to look at the "Lost at that point" value in +the summary statistics; this indicates the percentage of the queries that +was being dropped at the point in the test when the maximum throughput was +reached. + +Alternatively, you can make resperf report the throughput at the point in +the test where the percentage of queries dropped exceeds a given limit (or +the maximum as above if the limit is never exceeded). This can be a more +realistic indication of how much the server can be loaded while still +providing an acceptable level of service. This is done using the \fB\-L\fR +command line option; for example, specifying \fB\-L 10\fR makes resperf +report the highest throughput reached before the server starts dropping more +than 10% of the queries. + +There is no corresponding way of automatically constraining results based on +the number of failed queries, because unlike dropped queries, resolution +failures will occur even when the the server is not overloaded, and the +number of such failures is heavily dependent on the query data and network +conditions. Therefore, the plots should be manually inspected to ensure that +there is not an abnormal number of failures. +.SH "GENERATING CONSTANT TRAFFIC" +In addition to ramping up traffic linearly, \fBresperf\fR also has the +capability to send a constant stream of traffic. This can be useful when +using \fBresperf\fR for tasks other than performance measurement; for +example, it can be used to "soak test" a server by subjecting it to a +sustained load for an extended period of time. + +To generate a constant traffic load, use the \fB\-c\fR command line option, +together with the \fB\-m\fR option which specifies the desired constant +query rate. For example, to send 10000 queries per second for an hour, use +\fB\-m 10000 \-c 3600\fR. This will include the usual 30-second gradual +ramp-up of traffic at the beginning, which may be useful to avoid initially +overwhelming a server that is starting with an empty cache. To start the +onslaught of traffic instantly, use \fB\-m 10000 \-c 3600 \-r 0\fR. + +To be precise, \fBresperf\fR will do a linear ramp-up of traffic from 0 to +\fB\-m\fR queries per second over a period of \fB\-r\fR seconds, followed by +a plateau of steady traffic at \fB\-m\fR queries per second lasting for +\fB\-c\fR seconds, followed by waiting for responses for an extra 40 +seconds. Either the ramp-up or the plateau can be suppressed by supplying a +duration of zero seconds with \fB\-r 0\fR and \fB\-c 0\fR, respectively. The +latter is the default. + +Sending traffic at high rates for hours on end will of course require very +large amounts of input data. Also, a long-running test will generate a large +amount of plot data, which is kept in memory for the duration of the test. +To reduce the memory usage and the size of the plot file, consider +increasing the interval between measurements from the default of 0.5 seconds +using the \fB\-i\fR option in long-running tests. + +When using \fBresperf\fR for long-running tests, it is important that the +traffic rate specified using the \fB\-m\fR is one that both \fBresperf\fR +itself and the server under test can sustain. Otherwise, the test is likely +to be cut short as a result of either running out of query IDs (because of +large numbers of dropped queries) or of resperf falling behind its +transmission schedule. +.SH OPTIONS +Because the \fBresperf\-report\fR script passes its command line options +directly to the \fBresperf\fR programs, they both accept the same set of +options, with one exception: \fBresperf\-report\fR automatically adds an +appropriate \fB\-P\fR to the \fBresperf\fR command line, and therefore does +not itself take a \fB\-P\fR option. + +\fB-d \fIdatafile\fB\fR +.br +.RS +Specifies the input data file. If not specified, \fBresperf\fR will read +from standard input. +.RE + +\fB-M \fImode\fB\fR +.br +.RS +Specifies the transport mode to use, "udp", "tcp" or "tls". Default is "udp". +.RE + +\fB-s \fIserver_addr\fB\fR +.br +.RS +Specifies the name or address of the server to which requests will be sent. +The default is the loopback address, 127.0.0.1. +.RE + +\fB-p \fIport\fB\fR +.br +.RS +Sets the port on which the DNS packets are sent. If not specified, the +standard DNS port (udp/tcp 53, tls 853) is used. +.RE + +\fB-a \fIlocal_addr\fB\fR +.br +.RS +Specifies the local address from which to send requests. The default is the +wildcard address. +.RE + +\fB-x \fIlocal_port\fB\fR +.br +.RS +Specifies the local port from which to send requests. The default is the +wildcard port (0). + +If acting as multiple clients and the wildcard port is used, each client +will use a different random port. If a port is specified, the clients will +use a range of ports starting with the specified one. +.RE + +\fB-t \fItimeout\fB\fR +.br +.RS +Specifies the request timeout value, in seconds. \fBresperf\fR will no +longer wait for a response to a particular request after this many seconds +have elapsed. The default is 45 seconds. + +\fBresperf\fR times out unanswered requests in order to reclaim query IDs so +that the query ID space will not be exhausted in a long-running test, such +as when "soak testing" a server for an day with \fB\-m 10000 \-c 86400\fR. +The timeouts and the ability to tune them are of little use in the more +typical use case of a performance test lasting only a minute or two. + +The default timeout of 45 seconds was chosen to be longer than the query +timeout of current caching servers. Note that this is longer than the +corresponding default in \fBdnsperf\fR, because caching servers can take +many orders of magnitude longer to answer a query than authoritative servers +do. + +If a short timeout is used, there is a possibility that \fBresperf\fR will +receive a response after the corresponding request has timed out; in this +case, a message like Warning: Received a response with an unexpected id: 141 +will be printed. +.RE + +\fB-b \fIbufsize\fB\fR +.br +.RS +Sets the size of the socket's send and receive buffers, in kilobytes. If not +specified, the operating system's default is used. +.RE + +\fB-f \fIfamily\fB\fR +.br +.RS +Specifies the address family used for sending DNS packets. The possible +values are "inet", "inet6", or "any". If "any" (the default value) is +specified, \fBresperf\fR will use whichever address family is appropriate +for the server it is sending packets to. +.RE + +\fB-e\fR +.br +.RS +Enables EDNS0 [RFC2671], by adding an OPT record to all packets sent. +.RE + +\fB-D\fR +.br +.RS +Sets the DO (DNSSEC OK) bit [RFC3225] in all packets sent. This also enables +EDNS0, which is required for DNSSEC. +.RE + +\fB-y \fI[alg:]name:secret\fB\fR +.br +.RS +Add a TSIG record [RFC2845] to all packets sent, using the specified TSIG +key algorithm, name and secret, where the algorithm defaults to hmac-md5 and +the secret is expressed as a base-64 encoded string. +.RE + +\fB-h\fR +.br +.RS +Print a usage statement and exit. +.RE + +\fB-i \fIinterval\fB\fR +.br +.RS +Specifies the time interval between data points in the plot file. The +default is 0.5 seconds. +.RE + +\fB-m \fImax_qps\fB\fR +.br +.RS +Specifies the target maximum query rate (in queries per second). This should +be higher than the expected maximum throughput of the server being tested. +Traffic will be ramped up at a linearly increasing rate until this value is +reached, or until one of the other conditions described in the section +"Running the test" occurs. The default is 100000 queries per second. +.RE + +\fB-P \fIplot_data_file\fB\fR +.br +.RS +Specifies the name of the plot data file. The default is +\fIresperf.gnuplot\fR. +.RE + +\fB-r \fIrampup_time\fB\fR +.br +.RS +Specifies the length of time over which traffic will be ramped up. The +default is 60 seconds. +.RE + +\fB-c \fIconstant_traffic_time\fB\fR +.br +.RS +Specifies the length of time for which traffic will be sent at a constant +rate following the initial ramp-up. The default is 0 seconds, meaning no +sending of traffic at a constant rate will be done. +.RE + +\fB-L \fImax_loss\fB\fR +.br +.RS +Specifies the maximum acceptable query loss percentage for purposes of +determining the maximum throughput value. The default is 100%, meaning that +\fBresperf\fR will measure the maximum throughput without regard to query +loss. +.RE + +\fB-C \fIclients\fB\fR +.br +.RS +Act as multiple clients. Requests are sent from multiple sockets. The +default is to act as 1 client. +.RE + +\fB-q \fImax_outstanding\fB\fR +.br +.RS +Sets the maximum number of outstanding requests. \fBresperf\fR will stop +ramping up traffic when this many queries are outstanding. The default is +64k, and the limit is 64k per client. +.RE + +\fB-v\fR +.br +.RS +Enables verbose mode to report about network readiness and congestion. +.RE +.SH "THE PLOT DATA FILE" +The plot data file is written by the \fBresperf\fR program and contains the +data to be plotted using \fBgnuplot\fR. When running \fBresperf\fR via the +\fBresperf\-report\fR script, there is no need for the user to deal with +this file directly, but its format and contents are documented here for +completeness and in case you wish to run \fBresperf\fR directly and use its +output for purposes other than viewing it with \fBgnuplot\fR. + +The first line of the file is a comment identifying the fields. It may be +recognized as a comment by its leading hash sign (#). + +Subsequent lines contain the actual plot data. For purposes of generating +the plot data file, the test run is divided into time intervals of 0.5 +seconds (or some other length of time specified with the \fB\-i\fR command +line option). Each line corresponds to one such interval, and contains the +following values as floating-point numbers: + +\fBTime\fR +.br +.RS +The midpoint of this time interval, in seconds since the beginning of the +run +.RE + +\fBTarget queries per second\fR +.br +.RS +The number of queries per second scheduled to be sent in this time interval +.RE + +\fBActual queries per second\fR +.br +.RS +The number of queries per second actually sent in this time interval +.RE + +\fBResponses per second\fR +.br +.RS +The number of responses received corresponding to queries sent in this time +interval, divided by the length of the interval +.RE + +\fBFailures per second\fR +.br +.RS +The number of responses received corresponding to queries sent in this time +interval and having an RCODE other than NOERROR or NXDOMAIN, divided by the +length of the interval +.RE + +\fBAverage latency\fR +.br +.RS +The average time between sending the query and receiving a response, for +queries sent in this time interval +.RE +.SH "SEE ALSO" +\fBdnsperf\fR(1) +.SH AUTHOR +Nominum, Inc. +.LP +Maintained by DNS-OARC +.LP +.RS +.I https://www.dns-oarc.net/ +.RE +.LP +.SH BUGS +For issues and feature requests please use: +.LP +.RS +\fI@PACKAGE_URL@\fP +.RE +.LP +For question and help please use: +.LP +.RS +\fI@PACKAGE_BUGREPORT@\fP +.RE +.LP diff --git a/src/resperf.c b/src/resperf.c new file mode 100644 index 0000000..97c998d --- /dev/null +++ b/src/resperf.c @@ -0,0 +1,816 @@ +/* + * 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. + */ + +/*** + *** DNS Resolution Performance Testing Tool + ***/ + +#include "config.h" + +#include "datafile.h" +#include "dns.h" +#include "log.h" +#include "net.h" +#include "opt.h" +#include "util.h" +#include "os.h" +#include "list.h" +#include "result.h" +#include "buffer.h" + +#include <errno.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/time.h> +#include <openssl/ssl.h> +#include <openssl/conf.h> +#include <openssl/err.h> +#include <signal.h> + +/* + * Global stuff + */ + +#define DEFAULT_SERVER_NAME "127.0.0.1" +#define DEFAULT_SERVER_PORT 53 +#define DEFAULT_SERVER_TLS_PORT 853 +#define DEFAULT_SERVER_PORTS "udp/tcp 53 or dot/tls 853" +#define DEFAULT_LOCAL_PORT 0 +#define DEFAULT_SOCKET_BUFFER 32 +#define DEFAULT_TIMEOUT 45 +#define DEFAULT_MAX_OUTSTANDING (64 * 1024) + +#define MAX_INPUT_DATA (64 * 1024) + +#define TIMEOUT_CHECK_TIME 5000000 + +#define DNS_RCODE_NOERROR 0 +#define DNS_RCODE_NXDOMAIN 3 + +struct query_info; + +typedef perf_list(struct query_info) query_list; + +typedef struct query_info { + uint64_t sent_timestamp; + /* + * This link links the query into the list of outstanding + * queries or the list of available query IDs. + */ + perf_link(struct query_info); + /* + * The list this query is on. + */ + query_list* list; +} query_info; + +static query_list outstanding_list; +static query_list instanding_list; + +static query_info* queries; + +static perf_sockaddr_t server_addr; +static perf_sockaddr_t local_addr; +static unsigned int nsocks; +static struct perf_net_socket* socks; +static enum perf_net_mode mode; + +static int dummypipe[2]; + +static uint64_t query_timeout; +static bool edns; +static bool dnssec; + +static perf_datafile_t* input; + +/* The target traffic level at the end of the ramp-up */ +double max_qps = 100000.0; + +/* The time period over which we ramp up traffic */ +#define DEFAULT_RAMP_TIME 60 +static uint64_t ramp_time; + +/* How long to send constant traffic after the initial ramp-up */ +#define DEFAULT_SUSTAIN_TIME 0 +static uint64_t sustain_time; + +/* How long to wait for responses after sending traffic */ +static uint64_t wait_time = 40 * MILLION; + +/* Total duration of the traffic-sending part of the test */ +static uint64_t traffic_time; + +/* Total duration of the test */ +static uint64_t end_time; + +/* Interval between plot data points, in microseconds */ +#define DEFAULT_BUCKET_INTERVAL 0.5 +static uint64_t bucket_interval; + +/* The number of plot data points */ +static int n_buckets; + +/* The plot data file */ +static const char* plotfile = "resperf.gnuplot"; + +/* The largest acceptable query loss when reporting max throughput */ +static double max_loss_percent = 100.0; + +/* The maximum number of outstanding queries */ +static unsigned int max_outstanding; + +static uint64_t num_queries_sent; +static uint64_t num_queries_outstanding; +static uint64_t num_responses_received; +static uint64_t num_queries_timed_out; +static uint64_t rcodecounts[16]; + +static uint64_t time_now; +static uint64_t time_of_program_start; +static uint64_t time_of_end_of_run; + +/* + * The last plot data point containing actual data; this can + * be less than than (n_buckets - 1) if the traffic sending + * phase is cut short + */ +static int last_bucket_used; + +/* + * The statistics for queries sent during one bucket_interval + * of the traffic sending phase. + */ +typedef struct { + int queries; + int responses; + int failures; + double latency_sum; +} ramp_bucket; + +/* Pointer to array of n_buckets ramp_bucket structures */ +static ramp_bucket* buckets; + +enum phase { + /* + * The ramp-up phase: we are steadily increasing traffic. + */ + PHASE_RAMP, + /* + * The sustain phase: we are sending traffic at a constant + * rate. + */ + PHASE_SUSTAIN, + /* + * The wait phase: we have stopped sending queries and are + * just waiting for any remaining responses. + */ + PHASE_WAIT +}; +static enum phase phase = PHASE_RAMP; + +/* The time when the sustain/wait phase began */ +static uint64_t sustain_phase_began, wait_phase_began; + +static perf_tsigkey_t* tsigkey; + +static bool verbose; + +const char* progname = "resperf"; + +static char* +stringify(double value, int precision) +{ + static char buf[20]; + + snprintf(buf, sizeof(buf), "%.*f", precision, value); + return buf; +} + +static void +setup(int argc, char** argv) +{ + const char* family = NULL; + const char* server_name = DEFAULT_SERVER_NAME; + in_port_t server_port = 0; + const char* local_name = NULL; + in_port_t local_port = DEFAULT_LOCAL_PORT; + const char* filename = NULL; + const char* tsigkey_str = NULL; + int sock_family; + unsigned int bufsize; + unsigned int i; + const char* _mode = 0; + + sock_family = AF_UNSPEC; + server_port = 0; + local_port = DEFAULT_LOCAL_PORT; + bufsize = DEFAULT_SOCKET_BUFFER; + query_timeout = DEFAULT_TIMEOUT * MILLION; + ramp_time = DEFAULT_RAMP_TIME * MILLION; + sustain_time = DEFAULT_SUSTAIN_TIME * MILLION; + bucket_interval = DEFAULT_BUCKET_INTERVAL * MILLION; + max_outstanding = DEFAULT_MAX_OUTSTANDING; + nsocks = 1; + mode = sock_udp; + verbose = false; + + perf_opt_add('f', perf_opt_string, "family", + "address family of DNS transport, inet or inet6", "any", + &family); + perf_opt_add('M', perf_opt_string, "mode", "set transport mode: udp, tcp or dot/tls", "udp", &_mode); + perf_opt_add('s', perf_opt_string, "server_addr", + "the server to query", DEFAULT_SERVER_NAME, &server_name); + perf_opt_add('p', perf_opt_port, "port", + "the port on which to query the server", + DEFAULT_SERVER_PORTS, &server_port); + perf_opt_add('a', perf_opt_string, "local_addr", + "the local address from which to send queries", NULL, + &local_name); + perf_opt_add('x', perf_opt_port, "local_port", + "the local port from which to send queries", + stringify(DEFAULT_LOCAL_PORT, 0), &local_port); + perf_opt_add('d', perf_opt_string, "datafile", + "the input data file", "stdin", &filename); + perf_opt_add('t', perf_opt_timeval, "timeout", + "the timeout for query completion in seconds", + stringify(DEFAULT_TIMEOUT, 0), &query_timeout); + perf_opt_add('b', perf_opt_uint, "buffer_size", + "socket send/receive buffer size in kilobytes", NULL, + &bufsize); + perf_opt_add('e', perf_opt_boolean, NULL, + "enable EDNS 0", NULL, &edns); + perf_opt_add('D', perf_opt_boolean, NULL, + "set the DNSSEC OK bit (implies EDNS)", NULL, &dnssec); + perf_opt_add('y', perf_opt_string, "[alg:]name:secret", + "the TSIG algorithm, name and secret", NULL, &tsigkey_str); + perf_opt_add('i', perf_opt_timeval, "plot_interval", + "the time interval between plot data points, in seconds", + stringify(DEFAULT_BUCKET_INTERVAL, 1), &bucket_interval); + perf_opt_add('m', perf_opt_double, "max_qps", + "the maximum number of queries per second", + stringify(max_qps, 0), &max_qps); + perf_opt_add('P', perf_opt_string, "plotfile", + "the name of the plot data file", plotfile, &plotfile); + perf_opt_add('r', perf_opt_timeval, "ramp_time", + "the ramp-up time in seconds", + stringify(DEFAULT_RAMP_TIME, 0), &ramp_time); + perf_opt_add('c', perf_opt_timeval, "constant_traffic_time", + "how long to send constant traffic, in seconds", + stringify(DEFAULT_SUSTAIN_TIME, 0), &sustain_time); + perf_opt_add('L', perf_opt_double, "max_query_loss", + "the maximum acceptable query loss, in percent", + stringify(max_loss_percent, 0), &max_loss_percent); + perf_opt_add('C', perf_opt_uint, "clients", + "the number of clients to act as", NULL, &nsocks); + perf_opt_add('q', perf_opt_uint, "num_outstanding", + "the maximum number of queries outstanding", + stringify(DEFAULT_MAX_OUTSTANDING, 0), &max_outstanding); + perf_opt_add('v', perf_opt_boolean, NULL, + "verbose: report additional information to stdout", + NULL, &verbose); + bool log_stdout = false; + perf_opt_add('W', perf_opt_boolean, NULL, "log warnings and errors to stdout instead of stderr", NULL, &log_stdout); + + perf_opt_parse(argc, argv); + + if (log_stdout) { + perf_log_tostdout(); + } + + if (_mode != 0) + mode = perf_net_parsemode(_mode); + + if (!server_port) { + server_port = mode == sock_tls ? DEFAULT_SERVER_TLS_PORT : DEFAULT_SERVER_PORT; + } + + if (max_outstanding > nsocks * DEFAULT_MAX_OUTSTANDING) + perf_log_fatal("number of outstanding packets (%u) must not " + "be more than 64K per client", + max_outstanding); + + if (ramp_time + sustain_time == 0) + perf_log_fatal("rampup_time and constant_traffic_time must not " + "both be 0"); + + perf_list_init(outstanding_list); + perf_list_init(instanding_list); + if (!(queries = calloc(max_outstanding, sizeof(query_info)))) { + perf_log_fatal("out of memory"); + } + for (i = 0; i < max_outstanding; i++) { + perf_link_init(&queries[i]); + perf_list_append(instanding_list, &queries[i]); + queries[i].list = &instanding_list; + } + + if (family != NULL) + sock_family = perf_net_parsefamily(family); + perf_net_parseserver(sock_family, server_name, server_port, &server_addr); + perf_net_parselocal(server_addr.sa.sa.sa_family, local_name, + local_port, &local_addr); + + input = perf_datafile_open(filename); + + if (dnssec) + edns = true; + + if (tsigkey_str != NULL) + tsigkey = perf_tsig_parsekey(tsigkey_str); + + if (!(socks = calloc(nsocks, sizeof(*socks)))) { + perf_log_fatal("out of memory"); + } + for (i = 0; i < nsocks; i++) + socks[i] = perf_net_opensocket(mode, &server_addr, &local_addr, i, bufsize); +} + +static void +cleanup(void) +{ + unsigned int i; + + perf_datafile_close(&input); + for (i = 0; i < nsocks; i++) + (void)perf_net_close(&socks[i]); + close(dummypipe[0]); + close(dummypipe[1]); +} + +/* Find the ramp_bucket for queries sent at time "when" */ + +static ramp_bucket* +find_bucket(uint64_t when) +{ + uint64_t sent_at = when - time_of_program_start; + int i = (int)((n_buckets * sent_at) / traffic_time); + /* + * Guard against array bounds violations due to roundoff + * errors or scheduling jitter + */ + if (i < 0) + i = 0; + if (i > n_buckets - 1) + i = n_buckets - 1; + return &buckets[i]; +} + +/* + * print_statistics: + * Print out statistics based on the results of the test + */ +static void +print_statistics(void) +{ + int i; + double max_throughput; + double loss_at_max_throughput; + bool first_rcode; + uint64_t run_time = time_of_end_of_run - time_of_program_start; + + printf("\nStatistics:\n\n"); + + printf(" Queries sent: %" PRIu64 "\n", + num_queries_sent); + printf(" Queries completed: %" PRIu64 "\n", + num_responses_received); + printf(" Queries lost: %" PRIu64 "\n", + num_queries_sent - num_responses_received); + printf(" Response codes: "); + first_rcode = true; + for (i = 0; i < 16; i++) { + if (rcodecounts[i] == 0) + continue; + if (first_rcode) + first_rcode = false; + else + printf(", "); + printf("%s %" PRIu64 " (%.2lf%%)", + perf_dns_rcode_strings[i], rcodecounts[i], + (rcodecounts[i] * 100.0) / num_responses_received); + } + printf("\n"); + printf(" Run time (s): %u.%06u\n", + (unsigned int)(run_time / MILLION), + (unsigned int)(run_time % MILLION)); + + /* Find the maximum throughput, subject to the -L option */ + max_throughput = 0.0; + loss_at_max_throughput = 0.0; + for (i = 0; i <= last_bucket_used; i++) { + ramp_bucket* b = &buckets[i]; + double responses_per_sec = b->responses / (bucket_interval / (double)MILLION); + double loss = b->queries ? (b->queries - b->responses) / (double)b->queries : 0.0; + double loss_percent = loss * 100.0; + if (loss_percent > max_loss_percent) + break; + if (responses_per_sec > max_throughput) { + max_throughput = responses_per_sec; + loss_at_max_throughput = loss_percent; + } + } + printf(" Maximum throughput: %.6lf qps\n", max_throughput); + printf(" Lost at that point: %.2f%%\n", loss_at_max_throughput); +} + +static ramp_bucket* +init_buckets(int n) +{ + ramp_bucket* p; + int i; + + if (!(p = calloc(n, sizeof(*p)))) { + perf_log_fatal("out of memory"); + return 0; // fix clang scan-build + } + for (i = 0; i < n; i++) { + p[i].queries = p[i].responses = p[i].failures = 0; + p[i].latency_sum = 0.0; + } + return p; +} + +/* + * Send a query based on a line of input. + * Return PERF_R_NOMORE if we ran out of query IDs. + */ +static perf_result_t +do_one_line(perf_buffer_t* lines, perf_buffer_t* msg) +{ + query_info* q; + unsigned int qid; + unsigned int sock; + perf_region_t used; + unsigned char* base; + unsigned int length; + perf_result_t result; + + q = perf_list_head(instanding_list); + if (!q) + return (PERF_R_NOMORE); + qid = (q - queries) / nsocks; + sock = (q - queries) % nsocks; + + if (socks[sock].sending) { + if (perf_net_sockready(&socks[sock], dummypipe[0], TIMEOUT_CHECK_TIME) == -1) { + if (errno == EINPROGRESS) { + if (verbose) { + perf_log_warning("network congested, packet sending in progress"); + } + } else { + if (verbose) { + char __s[256]; + perf_log_warning("failed to send packet: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + } + return (PERF_R_FAILURE); + } + + perf_list_unlink(instanding_list, q); + perf_list_prepend(outstanding_list, q); + q->list = &outstanding_list; + + num_queries_sent++; + num_queries_outstanding++; + + q = perf_list_head(instanding_list); + if (!q) + return (PERF_R_NOMORE); + qid = (q - queries) / nsocks; + sock = (q - queries) % nsocks; + } + + switch (perf_net_sockready(&socks[sock], dummypipe[0], TIMEOUT_CHECK_TIME)) { + case 0: + if (verbose) { + perf_log_warning("failed to send packet: socket %d not ready", sock); + } + return (PERF_R_FAILURE); + case -1: + perf_log_warning("failed to send packet: socket %d readiness check timed out", sock); + return (PERF_R_FAILURE); + default: + break; + } + + perf_buffer_clear(lines); + result = perf_datafile_next(input, lines, false); + if (result != PERF_R_SUCCESS) + perf_log_fatal("ran out of query data"); + perf_buffer_usedregion(lines, &used); + + perf_buffer_clear(msg); + result = perf_dns_buildrequest(&used, qid, + edns, dnssec, false, + tsigkey, 0, + msg); + if (result != PERF_R_SUCCESS) + return (result); + + q->sent_timestamp = time_now; + + base = perf_buffer_base(msg); + length = perf_buffer_usedlength(msg); + if (perf_net_sendto(&socks[sock], base, length, 0, + &server_addr.sa.sa, server_addr.length) + < 1) { + if (errno == EINPROGRESS) { + if (verbose) { + perf_log_warning("network congested, packet sending in progress"); + } + } else { + if (verbose) { + char __s[256]; + perf_log_warning("failed to send packet: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + } + return (PERF_R_FAILURE); + } + + perf_list_unlink(instanding_list, q); + perf_list_prepend(outstanding_list, q); + q->list = &outstanding_list; + + num_queries_sent++; + num_queries_outstanding++; + + return PERF_R_SUCCESS; +} + +static void +enter_sustain_phase(void) +{ + phase = PHASE_SUSTAIN; + if (sustain_time != 0.0) + printf("[Status] Ramp-up done, sending constant traffic\n"); + sustain_phase_began = time_now; +} + +static void +enter_wait_phase(void) +{ + phase = PHASE_WAIT; + printf("[Status] Waiting for more responses\n"); + wait_phase_began = time_now; +} + +/* + * try_process_response: + * + * Receive from the given socket & process an individual response packet. + * Remove it from the list of open queries (status[]) and decrement the + * number of outstanding queries if it matches an open query. + */ +static void +try_process_response(unsigned int sockindex) +{ + unsigned char packet_buffer[MAX_EDNS_PACKET]; + uint16_t* packet_header; + uint16_t qid, rcode; + query_info* q; + double latency; + ramp_bucket* b; + int n; + + packet_header = (uint16_t*)packet_buffer; + n = perf_net_recv(&socks[sockindex], packet_buffer, sizeof(packet_buffer), 0); + if (n < 0) { + if (errno == EAGAIN || errno == EINTR) { + return; + } else { + char __s[256]; + perf_log_fatal("failed to receive packet: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + } else if (!n) { + // Treat connection closed like try again until reconnection features are in + return; + } else if (n < 4) { + perf_log_warning("received short response"); + return; + } + + qid = ntohs(packet_header[0]); + rcode = ntohs(packet_header[1]) & 0xF; + + q = &queries[qid * nsocks + sockindex]; + if (q->list != &outstanding_list) { + perf_log_warning("received a response with an unexpected id: %u", qid); + return; + } + + perf_list_unlink(outstanding_list, q); + perf_list_append(instanding_list, q); + q->list = &instanding_list; + + num_queries_outstanding--; + + latency = (time_now - q->sent_timestamp) / (double)MILLION; + b = find_bucket(q->sent_timestamp); + b->responses++; + if (!(rcode == DNS_RCODE_NOERROR || rcode == DNS_RCODE_NXDOMAIN)) + b->failures++; + b->latency_sum += latency; + num_responses_received++; + rcodecounts[rcode]++; +} + +static void +retire_old_queries(void) +{ + query_info* q; + + while (true) { + q = perf_list_tail(outstanding_list); + if (q == NULL || (time_now - q->sent_timestamp) < query_timeout) + break; + perf_list_unlink(outstanding_list, q); + perf_list_append(instanding_list, q); + q->list = &instanding_list; + + num_queries_outstanding--; + num_queries_timed_out++; + } +} + +static inline int +num_scheduled(uint64_t time_since_start) +{ + if (phase == PHASE_RAMP) { + return 0.5 * max_qps * (double)time_since_start * time_since_start / (ramp_time * MILLION); + } else { /* PHASE_SUSTAIN */ + return 0.5 * max_qps * (ramp_time / (double)MILLION) + max_qps * (time_since_start - ramp_time) / (double)MILLION; + } +} + +static void +handle_sigpipe(int sig) +{ + (void)sig; + switch (mode) { + case sock_tcp: + case sock_tls: + // if connection is closed it will generate a signal + perf_log_fatal("SIGPIPE received, connection(s) likely closed, can't continue"); + break; + default: + break; + } +} + +int main(int argc, char** argv) +{ + int i; + FILE* plotf; + perf_buffer_t lines, msg; + char input_data[MAX_INPUT_DATA]; + unsigned char outpacket_buffer[MAX_EDNS_PACKET]; + unsigned int max_packet_size; + unsigned int current_sock; + perf_result_t result; + + printf("DNS Resolution Performance Testing Tool\n" + "Version " PACKAGE_VERSION "\n\n"); + + (void)SSL_library_init(); +#if OPENSSL_VERSION_NUMBER < 0x10100000L + SSL_load_error_strings(); + OPENSSL_config(0); +#endif + + setup(argc, argv); + + if (pipe(dummypipe) < 0) + perf_log_fatal("creating pipe"); + + perf_os_handlesignal(SIGPIPE, handle_sigpipe); + + perf_buffer_init(&lines, input_data, sizeof(input_data)); + + max_packet_size = edns ? MAX_EDNS_PACKET : MAX_UDP_PACKET; + perf_buffer_init(&msg, outpacket_buffer, max_packet_size); + + traffic_time = ramp_time + sustain_time; + end_time = traffic_time + wait_time; + + n_buckets = (traffic_time + bucket_interval - 1) / bucket_interval; + buckets = init_buckets(n_buckets); + + time_now = perf_get_time(); + time_of_program_start = time_now; + + printf("[Status] Command line: %s", progname); + for (i = 1; i < argc; i++) { + printf(" %s", argv[i]); + } + printf("\n"); + + printf("[Status] Sending\n"); + + current_sock = 0; + for (;;) { + int should_send; + uint64_t time_since_start = time_now - time_of_program_start; + switch (phase) { + case PHASE_RAMP: + if (time_since_start >= ramp_time) + enter_sustain_phase(); + break; + case PHASE_SUSTAIN: + if (time_since_start >= traffic_time) + enter_wait_phase(); + break; + case PHASE_WAIT: + if (time_since_start >= end_time || perf_list_empty(outstanding_list)) + goto end_loop; + break; + } + if (phase != PHASE_WAIT) { + should_send = num_scheduled(time_since_start) - num_queries_sent; + if (should_send >= 1000) { + printf("[Status] Fell behind by %d queries, " + "ending test at %.0f qps\n", + should_send, (max_qps * time_since_start) / ramp_time); + enter_wait_phase(); + } + if (should_send > 0) { + result = do_one_line(&lines, &msg); + if (result == PERF_R_SUCCESS) + find_bucket(time_now)->queries++; + if (result == PERF_R_NOMORE) { + printf("[Status] Reached %u outstanding queries\n", + max_outstanding); + enter_wait_phase(); + } + } + } + try_process_response(current_sock++); + current_sock = current_sock % nsocks; + retire_old_queries(); + time_now = perf_get_time(); + } +end_loop: + time_now = perf_get_time(); + time_of_end_of_run = time_now; + + printf("[Status] Testing complete\n"); + + plotf = fopen(plotfile, "w"); + if (!plotf) { + char __s[256]; + perf_log_fatal("could not open %s: %s", plotfile, perf_strerror_r(errno, __s, sizeof(__s))); + } + + /* Print column headers */ + fprintf(plotf, "# time target_qps actual_qps " + "responses_per_sec failures_per_sec avg_latency\n"); + + /* Don't print unused buckets */ + last_bucket_used = find_bucket(wait_phase_began) - buckets; + + /* Don't print a partial bucket at the end */ + if (last_bucket_used > 0) + --last_bucket_used; + + for (i = 0; i <= last_bucket_used; i++) { + double t = (i + 0.5) * traffic_time / (n_buckets * (double)MILLION); + double ramp_dtime = ramp_time / (double)MILLION; + double target_qps = t <= ramp_dtime ? (t / ramp_dtime) * max_qps : max_qps; + double latency = buckets[i].responses ? buckets[i].latency_sum / buckets[i].responses : 0; + double interval = bucket_interval / (double)MILLION; + fprintf(plotf, "%7.3f %8.2f %8.2f %8.2f %8.2f %8.6f\n", + t, + target_qps, + buckets[i].queries / interval, + buckets[i].responses / interval, + buckets[i].failures / interval, + latency); + } + + fclose(plotf); + print_statistics(); + cleanup(); +#if OPENSSL_VERSION_NUMBER < 0x10100000L + ERR_free_strings(); +#endif + + return 0; +} diff --git a/src/result.h b/src/result.h new file mode 100644 index 0000000..076fcc5 --- /dev/null +++ b/src/result.h @@ -0,0 +1,38 @@ +/* + * 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. + */ + +#ifndef PERF_RESULT_H +#define PERF_RESULT_H 1 + +#include <assert.h> + +typedef unsigned int perf_result_t; + +#define PERF_R_SUCCESS 0 +#define PERF_R_FAILURE 1 +#define PERF_R_CANCELED 2 +#define PERF_R_EOF 3 +#define PERF_R_INVALIDFILE 4 +#define PERF_R_NOMORE 5 +#define PERF_R_NOSPACE 6 +#define PERF_R_TIMEDOUT 7 + +#define PERF_R_INVALIDUPDATE 100 + +#endif diff --git a/src/strerror.c b/src/strerror.c new file mode 100644 index 0000000..a283bc8 --- /dev/null +++ b/src/strerror.c @@ -0,0 +1,37 @@ +/* + * 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 "strerror.h" + +#include <string.h> +#include <stdio.h> + +const char* perf_strerror_r(int errnum, char* str, size_t len) +{ +#if ((_POSIX_C_SOURCE >= 200112L) && !_GNU_SOURCE) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__APPLE__) + if (strerror_r(errnum, str, len)) { + (void)snprintf(str, len, "Error %d", errnum); + } + return str; +#else + return strerror_r(errnum, str, len); +#endif +} diff --git a/src/strerror.h b/src/strerror.h new file mode 100644 index 0000000..72bca6d --- /dev/null +++ b/src/strerror.h @@ -0,0 +1,27 @@ +/* + * 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. + */ + +#ifndef PERF_STRERROR_H +#define PERF_STRERROR_H 1 + +#include <stddef.h> + +const char* perf_strerror_r(int errnum, char* str, size_t len); + +#endif diff --git a/src/test/Makefile.am b/src/test/Makefile.am new file mode 100644 index 0000000..9b3e7c2 --- /dev/null +++ b/src/test/Makefile.am @@ -0,0 +1,9 @@ +MAINTAINERCLEANFILES = $(srcdir)/Makefile.in + +CLEANFILES = test*.log test*.trs \ + test2.out + +TESTS = test1.sh test2.sh test3.sh + +EXTRA_DIST = $(TESTS) \ + datafile datafile2 updatefile diff --git a/src/test/datafile b/src/test/datafile new file mode 100644 index 0000000..883156e --- /dev/null +++ b/src/test/datafile @@ -0,0 +1,2 @@ +google.com A +google.com AAAA diff --git a/src/test/datafile2 b/src/test/datafile2 new file mode 100644 index 0000000..96acd2d --- /dev/null +++ b/src/test/datafile2 @@ -0,0 +1,560 @@ +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA +google.com A +google.com AAAA diff --git a/src/test/datafile3 b/src/test/datafile3 new file mode 100644 index 0000000..ce49d49 --- /dev/null +++ b/src/test/datafile3 @@ -0,0 +1,2 @@ +. A +google.com. A diff --git a/src/test/test1.sh b/src/test/test1.sh new file mode 100755 index 0000000..945aab3 --- /dev/null +++ b/src/test/test1.sh @@ -0,0 +1,4 @@ +#!/bin/sh -xe + +../dnsperf -h +../resperf -h diff --git a/src/test/test2.sh b/src/test/test2.sh new file mode 100755 index 0000000..a4cfce0 --- /dev/null +++ b/src/test/test2.sh @@ -0,0 +1,108 @@ +#!/bin/sh -xe + +test "$TEST_DNSPERF_WITH_NETWORK" = "1" || exit 0 + +for ip in 1.1.1.1 2606:4700:4700::1111; do + +echo "google.com A" | ../dnsperf -vvv -s $ip -m udp >test2.out +cat test2.out +grep -q "Queries sent: *1" test2.out +echo "google.com A" | ../dnsperf -vvv -s $ip -e -E 12345:0a0a0a0a -m udp >test2.out +cat test2.out +grep -q "Queries sent: *1" test2.out +../dnsperf -vvv -s $ip -d "$srcdir/datafile" -n 2 -m udp >test2.out +cat test2.out +grep -q "Queries sent: *4" test2.out +../dnsperf -s $ip -d "$srcdir/datafile" -n 1 -m tcp >test2.out +cat test2.out +grep -q "Queries sent: *2" test2.out +../dnsperf -s $ip -d "$srcdir/datafile" -n 1 -m tls >test2.out +cat test2.out +grep -q "Queries sent: *2" test2.out +../dnsperf -s $ip -d "$srcdir/datafile" -n 1 -m dot >test2.out +cat test2.out +grep -q "Queries sent: *2" test2.out + +../dnsperf -s $ip -d "$srcdir/datafile3" -n 1 -m dot >test2.out +cat test2.out +grep -q "Queries sent: *2" test2.out + +../dnsperf -s $ip -d "$srcdir/datafile" -n 1 -e >test2.out +cat test2.out +grep -q "Queries sent: *2" test2.out +../dnsperf -s $ip -d "$srcdir/datafile" -n 1 -e -D >test2.out +cat test2.out +grep -q "Queries sent: *2" test2.out + +../dnsperf -d "$srcdir/updatefile" -u -s $ip -y hmac-md5:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= >test2.out +cat test2.out +grep -q "Updates sent: *1" test2.out +../dnsperf -d "$srcdir/updatefile" -u -s $ip -y hmac-sha1:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= >test2.out +cat test2.out +grep -q "Updates sent: *1" test2.out +../dnsperf -d "$srcdir/updatefile" -u -s $ip -y hmac-sha224:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= >test2.out +cat test2.out +grep -q "Updates sent: *1" test2.out +../dnsperf -d "$srcdir/updatefile" -u -s $ip -y hmac-sha256:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= >test2.out +cat test2.out +grep -q "Updates sent: *1" test2.out +../dnsperf -d "$srcdir/updatefile" -u -s $ip -y hmac-sha384:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= >test2.out +cat test2.out +grep -q "Updates sent: *1" test2.out +../dnsperf -d "$srcdir/updatefile" -u -s $ip -y hmac-sha512:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= >test2.out +cat test2.out +grep -q "Updates sent: *1" test2.out + +../resperf -s $ip -m 1 -d "$srcdir/datafile2" -r 2 -c 2 -M udp >test2.out +cat test2.out +grep -q "Queries sent: *2" test2.out +../resperf -s $ip -m 1 -d "$srcdir/datafile2" -r 2 -c 2 -M tcp >test2.out +cat test2.out +grep -q "Queries sent: *2" test2.out + +../resperf -s $ip -m 1 -d "$srcdir/datafile2" -r 2 -c 2 -M udp -D >test2.out +cat test2.out +grep -q "Queries sent: *2" test2.out +# Disabled until https://github.com/DNS-OARC/dnsperf/issues/92 is fixed +../resperf -s $ip -m 1 -d "$srcdir/datafile2" -r 2 -c 2 -M udp -y hmac-sha256:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= >test2.out +cat test2.out +grep -q "Queries sent: *2" test2.out + +# Ignore failure until https://github.com/DNS-OARC/dnsperf/issues/88 is fixed +# May work on slower systems +../resperf -s $ip -m 1 -d "$srcdir/datafile2" -r 2 -c 2 -M tls || true + +done # for ip + +../dnsperf -s 127.66.66.66 -d "$srcdir/datafile" -vvvv -m tcp -n 1 & +sleep 2 +pkill -KILL -u `id -u` dnsperf || true + +../dnsperf -s 127.66.66.66 -d "$srcdir/datafile" -vvvv -m tls -n 1 & +sleep 2 +pkill -KILL -u `id -u` dnsperf || true + +! echo "invalid" | ../dnsperf -s 127.66.66.66 -m tcp +! echo "invalid invalid" | ../dnsperf -s 127.66.66.66 -m tcp +echo "invalid" | ../dnsperf -u -s 127.66.66.66 -m tcp & +sleep 2 +pkill -KILL -u `id -u` dnsperf || true +echo "invalid\ninvalid" | ../dnsperf -u -s 127.66.66.66 -m tcp & +sleep 2 +pkill -KILL -u `id -u` dnsperf || true + +! echo "google.com A" \ + | ../dnsperf -W -s 1.1.1.1 -y tooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= \ + | grep "adding TSIG: invalid owner name" +echo ".google.com A" | ../dnsperf -W -s 1.1.1.1 \ + | grep "invalid domain name" +echo "google.com.. A" | ../dnsperf -W -s 1.1.1.1 \ + | grep "invalid domain name" +echo " A" | ../dnsperf -W -s 1.1.1.1 \ + | grep "invalid query input format" +echo "toooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooolongname" \ + | ../dnsperf -W -s 1.1.1.1 -u \ + | grep "Unable to parse domain name" +echo -e "test\ndelete toooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooolongname" \ + | ../dnsperf -W -s 1.1.1.1 -u \ + | grep "invalid update command, domain name too large" diff --git a/src/test/test3.sh b/src/test/test3.sh new file mode 100755 index 0000000..3c0e44f --- /dev/null +++ b/src/test/test3.sh @@ -0,0 +1,46 @@ +#!/bin/sh -xe + +! ../dnsperf -d does_not_exist +! ../resperf -d does_not_exist +! ../dnsperf -f invalid +! ../dnsperf -f any -s 256.256.256.256 +! ../dnsperf -f inet -s 256.256.256.256 +! ../dnsperf -f inet6 -s 256.256.256.256 +! ../dnsperf -a 127.0.0.1 -d does_not_exist +! ../dnsperf -a ::1 -d does_not_exist +! ../dnsperf -a 256.256.256.256 +! ../dnsperf -m invalid +! ../dnsperf -n 43f8huishfs +! ../dnsperf -p 12345 unexpected argument +! ../dnsperf -p 65536 + +! echo "" | ../dnsperf -y test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= +! echo "" | ../dnsperf -y hmac-md5:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= +! echo "" | ../dnsperf -y hmac-sha1:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= +! echo "" | ../dnsperf -y hmac-sha224:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= +! echo "" | ../dnsperf -y hmac-sha256:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= +! echo "" | ../dnsperf -y hmac-sha384:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= +! echo "" | ../dnsperf -y hmac-sha512:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= +! echo "" | ../dnsperf -y invalid:test:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= +! echo "" | ../dnsperf -y test:invalid +! echo "" | ../dnsperf -y test +echo "" | ../dnsperf -W -y toooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooolongname:Ax42vsuHBjQOKlVHO8yU1zGuQ5hjeSz01LXiNze8pb8= \ + | grep "unable to setup TSIG, name too long" +echo "" | ../dnsperf -W -y test: | grep "unable to setup TSIG, secret empty" + +! ../dnsperf -e -E invalid +! ../dnsperf -e -E 9999999:invalid +! ../dnsperf -e -E 123:invalid +! ../dnsperf -e -E 123:fa0 +../dnsperf -W -E a: | grep "invalid EDNS Option, value is empty" +../dnsperf -W -E a:a | grep "invalid EDNS Option, value must hex string (even number of characters)" +../dnsperf -W -E a:aa | grep "invalid EDNS Option code 'a'" +../dnsperf -W -E 1:xx | grep "invalid EDNS Option hex value 'xx'" + +! ../resperf -d does_not_exist +! ../resperf -r 0 -c 0 +! ../resperf -f invalid +! ../resperf -q 256000 +! ../resperf -m 123.45 unexpected argument +! ../resperf -m 123.. +! ../resperf -m 123a diff --git a/src/test/updatefile b/src/test/updatefile new file mode 100644 index 0000000..1a61edf --- /dev/null +++ b/src/test/updatefile @@ -0,0 +1,11 @@ +example.com +require a +require a A +require a A 1.2.3.4 +prohibit x +prohibit x A +add x 3600 A 10.1.2.3 +delete y A 10.1.2.3 +delete z A +delete w +send diff --git a/src/tsig.c b/src/tsig.c new file mode 100644 index 0000000..0b660c1 --- /dev/null +++ b/src/tsig.c @@ -0,0 +1,326 @@ +/* + * 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 "tsig.h" + +#include "log.h" +#include "opt.h" +#include "dns.h" + +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <limits.h> +#include <openssl/evp.h> +#include <time.h> + +#define TSIG_HMACMD5_NAME "hmac-md5.sig-alg.reg.int" +#define TSIG_HMACSHA1_NAME "hmac-sha1" +#define TSIG_HMACSHA224_NAME "hmac-sha224" +#define TSIG_HMACSHA256_NAME "hmac-sha256" +#define TSIG_HMACSHA384_NAME "hmac-sha384" +#define TSIG_HMACSHA512_NAME "hmac-sha512" + +static unsigned char* decode64(const void* base64, int* len) +{ + unsigned char* out; + + assert(base64); + assert(len); + assert(*len); + + out = calloc(1, *len); + assert(out); + + int olen = *len; + +#if OPENSSL_VERSION_NUMBER < 0x10100000L + EVP_ENCODE_CTX evp; + EVP_DecodeInit(&evp); + if (EVP_DecodeUpdate(&evp, out, &olen, base64, *len)) { + free(out); + return 0; + } +#else + EVP_ENCODE_CTX* evp = EVP_ENCODE_CTX_new(); + if (!evp) { + free(out); + return 0; + } + EVP_DecodeInit(evp); + if (EVP_DecodeUpdate(evp, out, &olen, base64, *len)) { + free(out); + EVP_ENCODE_CTX_free(evp); + return 0; + } + EVP_ENCODE_CTX_free(evp); +#endif + + *len = olen; + + return out; +} + +perf_tsigkey_t* perf_tsig_parsekey(const char* arg) +{ + perf_tsigkey_t* tsigkey; + const char * sep1, *sep2, *alg, *name, *secret; + size_t alglen, namelen, secretlen; + int keylen; + const EVP_MD* md = 0; + + tsigkey = calloc(1, sizeof(*tsigkey)); + if (!tsigkey) { + perf_log_fatal("out of memory"); + return 0; // fix clang scan-build + } + + sep1 = strchr(arg, ':'); + if (sep1 == NULL) { + perf_log_warning("invalid TSIG [alg:]name:secret"); + perf_opt_usage(); + exit(1); + } + + sep2 = strchr(sep1 + 1, ':'); + if (sep2 == NULL) { + /* name:key */ + alg = NULL; + alglen = 0; + name = arg; + namelen = sep1 - arg; + secret = sep1 + 1; + } else { + /* [alg:]name:secret */ + alg = arg; + alglen = sep1 - arg; + name = sep1 + 1; + namelen = sep2 - sep1 - 1; + secret = sep2 + 1; + } + + /* Algorithm */ + + if (!alg || !strncasecmp(alg, "hmac-md5:", 9)) { + md = EVP_md5(); + tsigkey->alg = TSIG_HMACMD5_NAME; + tsigkey->alglen = sizeof(TSIG_HMACMD5_NAME) - 1; + } else if (!strncasecmp(alg, "hmac-sha1:", 10)) { + md = EVP_sha1(); + tsigkey->alg = TSIG_HMACSHA1_NAME; + tsigkey->alglen = sizeof(TSIG_HMACSHA1_NAME) - 1; + } else if (!strncasecmp(alg, "hmac-sha224:", 12)) { + md = EVP_sha224(); + tsigkey->alg = TSIG_HMACSHA224_NAME; + tsigkey->alglen = sizeof(TSIG_HMACSHA224_NAME) - 1; + } else if (!strncasecmp(alg, "hmac-sha256:", 12)) { + md = EVP_sha256(); + tsigkey->alg = TSIG_HMACSHA256_NAME; + tsigkey->alglen = sizeof(TSIG_HMACSHA256_NAME) - 1; + } else if (!strncasecmp(alg, "hmac-sha384:", 12)) { + md = EVP_sha384(); + tsigkey->alg = TSIG_HMACSHA384_NAME; + tsigkey->alglen = sizeof(TSIG_HMACSHA384_NAME) - 1; + } else if (!strncasecmp(alg, "hmac-sha512:", 12)) { + md = EVP_sha512(); + tsigkey->alg = TSIG_HMACSHA512_NAME; + tsigkey->alglen = sizeof(TSIG_HMACSHA512_NAME) - 1; + } else { + perf_log_warning("invalid TSIG algorithm %.*s", (int)alglen, alg); + perf_opt_usage(); + exit(1); + } + + if (namelen > sizeof(tsigkey->name)) { + perf_log_fatal("unable to setup TSIG, name too long"); + // fix clang scan-build / sonarcloud: + free(tsigkey); + return 0; + } + memcpy(tsigkey->name, name, namelen); + tsigkey->namelen = namelen; + for (namelen = 0; namelen < tsigkey->namelen; namelen++) { + tsigkey->name[namelen] = tolower(tsigkey->name[namelen]); + } + + /* Secret */ + + secretlen = strlen(secret); + if (!secretlen) { + perf_log_warning("unable to setup TSIG, secret empty"); + perf_opt_usage(); + exit(1); + } + + keylen = secretlen; + unsigned char* key = decode64(secret, &keylen); + if (!key) { + perf_log_fatal("unable to setup TSIG, invalid base64 secret"); + } + + /* Setup HMAC */ + +#if OPENSSL_VERSION_NUMBER < 0x10100000L + if (!(tsigkey->hmac = calloc(1, sizeof(*tsigkey->hmac)))) { + perf_log_fatal("unable to setup TSIG, OpenSSL HMAC context failed to be created"); + } + HMAC_CTX_init(tsigkey->hmac); +#else + if (!(tsigkey->hmac = HMAC_CTX_new())) { + perf_log_fatal("unable to setup TSIG, OpenSSL HMAC context failed to be created"); + } +#endif + + if (!HMAC_Init_ex(tsigkey->hmac, key, keylen, md, 0)) { + perf_log_fatal("unable to setup TSIG, OpenSSL HMAC init failed"); + } + + free(key); + + return tsigkey; +} + +void perf_tsig_destroykey(perf_tsigkey_t** tsigkeyp) +{ + assert(tsigkeyp); + assert(*tsigkeyp); + +#if OPENSSL_VERSION_NUMBER < 0x10100000L + HMAC_CTX_cleanup((*tsigkeyp)->hmac); + free((*tsigkeyp)->hmac); +#else + HMAC_CTX_free((*tsigkeyp)->hmac); +#endif + + free(*tsigkeyp); + *tsigkeyp = 0; +} + +/* + * Appends a TSIG record to the packet. + */ +perf_result_t perf_add_tsig(perf_buffer_t* packet, perf_tsigkey_t* tsigkey) +{ + unsigned char* base; + size_t rdlen, totallen; + unsigned char tmpdata[512], md[EVP_MAX_MD_SIZE]; + unsigned int mdlen; + perf_buffer_t tmp; + uint32_t now; + perf_result_t result; + + now = time(NULL); + + if (!HMAC_Init_ex(tsigkey->hmac, 0, 0, 0, 0)) { + perf_log_fatal("adding TSIG: OpenSSL HMAC reinit failed"); + } + + if (!HMAC_Update(tsigkey->hmac, perf_buffer_base(packet), perf_buffer_usedlength(packet))) { + perf_log_fatal("adding TSIG: OpenSSL HMAC update failed"); + } + + /* Digest the TSIG record */ + perf_buffer_init(&tmp, tmpdata, sizeof tmpdata); + switch ((result = perf_dname_fromstring(tsigkey->name, tsigkey->namelen, &tmp))) { + case PERF_R_SUCCESS: + break; + case PERF_R_NOSPACE: + perf_log_warning("adding TSIG: out of space in digest record"); + return result; + default: + perf_log_warning("adding TSIG: invalid owner name"); + return result; + } + perf_buffer_putuint16(&tmp, 255); /* class ANY */ + perf_buffer_putuint32(&tmp, 0); /* ttl */ + switch ((result = perf_dname_fromstring(tsigkey->alg, tsigkey->alglen, &tmp))) { + case PERF_R_SUCCESS: + break; + case PERF_R_NOSPACE: + perf_log_warning("adding TSIG: out of space in digest record"); + return result; + default: + perf_log_warning("adding TSIG: invalid algorithm name"); + return result; + } + perf_buffer_putuint16(&tmp, 0); /* time high */ + perf_buffer_putuint32(&tmp, now); /* time low */ + perf_buffer_putuint16(&tmp, 300); /* fudge */ + perf_buffer_putuint16(&tmp, 0); /* error */ + perf_buffer_putuint16(&tmp, 0); /* other length */ + + if (!HMAC_Update(tsigkey->hmac, perf_buffer_base(&tmp), perf_buffer_usedlength(&tmp))) { + perf_log_fatal("adding TSIG: OpenSSL HMAC update failed"); + } + + mdlen = sizeof(md); + if (!HMAC_Final(tsigkey->hmac, md, &mdlen)) { + perf_log_fatal("adding TSIG: OpenSSL HMAC final failed"); + } + + /* Make sure everything will fit */ + rdlen = tsigkey->alglen + 18 + mdlen; + totallen = tsigkey->namelen + 12 + rdlen; + if (totallen > perf_buffer_availablelength(packet)) { + perf_log_warning("adding TSIG: out of space"); + return PERF_R_NOSPACE; + } + + base = perf_buffer_base(packet); + + /* Add the TSIG record. */ + switch ((result = perf_dname_fromstring(tsigkey->name, tsigkey->namelen, packet))) { + case PERF_R_SUCCESS: + break; + case PERF_R_NOSPACE: + perf_log_warning("adding TSIG: out of space"); + return result; + default: + perf_log_warning("adding TSIG: invalid owner name"); + return result; + } + perf_buffer_putuint16(packet, 250); /* type TSIG */ + perf_buffer_putuint16(packet, 255); /* class ANY */ + perf_buffer_putuint32(packet, 0); /* ttl */ + perf_buffer_putuint16(packet, rdlen); /* rdlen */ + switch ((result = perf_dname_fromstring(tsigkey->alg, tsigkey->alglen, packet))) { + case PERF_R_SUCCESS: + break; + case PERF_R_NOSPACE: + perf_log_warning("adding TSIG: out of space"); + return result; + default: + perf_log_warning("adding TSIG: invalid algorithm name"); + return result; + } + perf_buffer_putuint16(packet, 0); /* time high */ + perf_buffer_putuint32(packet, now); /* time low */ + perf_buffer_putuint16(packet, 300); /* fudge */ + perf_buffer_putuint16(packet, mdlen); /* digest len */ + perf_buffer_putmem(packet, md, mdlen); /* digest */ + perf_buffer_putmem(packet, base, 2); /* orig ID */ + perf_buffer_putuint16(packet, 0); /* error */ + perf_buffer_putuint16(packet, 0); /* other len */ + + base[11]++; /* increment additional record count */ + + return PERF_R_SUCCESS; +} diff --git a/src/tsig.h b/src/tsig.h new file mode 100644 index 0000000..ca4cf85 --- /dev/null +++ b/src/tsig.h @@ -0,0 +1,41 @@ +/* + * 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 "result.h" +#include "buffer.h" + +#ifndef PERF_TSIG_H +#define PERF_TSIG_H 1 + +#include <openssl/hmac.h> + +typedef struct perf_tsigkey { + char name[256]; + size_t namelen, alglen; + const char* alg; + HMAC_CTX* hmac; +} perf_tsigkey_t; + +perf_tsigkey_t* perf_tsig_parsekey(const char* arg); + +void perf_tsig_destroykey(perf_tsigkey_t** tsigkeyp); + +perf_result_t perf_add_tsig(perf_buffer_t* packet, perf_tsigkey_t* tsigkey); + +#endif diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..245c984 --- /dev/null +++ b/src/util.h @@ -0,0 +1,146 @@ +/* + * 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 "log.h" +#include "strerror.h" + +#ifndef PERF_UTIL_H +#define PERF_UTIL_H 1 + +#include <pthread.h> +#include <inttypes.h> +#include <stdbool.h> +#include <string.h> +#include <sys/time.h> + +#define MILLION ((uint64_t)1000000) + +#define PERF_THREAD(thread, start, arg) \ + do { \ + int __n = pthread_create((thread), NULL, (start), (arg)); \ + if (__n != 0) { \ + char __s[256]; \ + perf_log_fatal("pthread_create failed: %s", perf_strerror_r(__n, __s, sizeof(__s))); \ + } \ + } while (0) + +#define PERF_JOIN(thread, valuep) \ + do { \ + int __n = pthread_join((thread), (valuep)); \ + if (__n != 0) { \ + char __s[256]; \ + perf_log_fatal("pthread_join failed: %s", perf_strerror_r(__n, __s, sizeof(__s))); \ + } \ + } while (0) + +#define PERF_MUTEX_INIT(mutex) \ + do { \ + int __n = pthread_mutex_init((mutex), NULL); \ + if (__n != 0) { \ + char __s[256]; \ + perf_log_fatal("pthread_mutex_init failed: %s", perf_strerror_r(__n, __s, sizeof(__s))); \ + } \ + } while (0) + +#define PERF_MUTEX_DESTROY(mutex) \ + do { \ + int __n = pthread_mutex_destroy((mutex)); \ + if (__n != 0) { \ + char __s[256]; \ + perf_log_fatal("pthread_mutex_destroy failed: %s", perf_strerror_r(__n, __s, sizeof(__s))); \ + } \ + } while (0) + +#define PERF_LOCK(mutex) \ + do { \ + int __n = pthread_mutex_lock((mutex)); \ + if (__n != 0) { \ + char __s[256]; \ + perf_log_fatal("pthread_mutex_lock failed: %s", perf_strerror_r(__n, __s, sizeof(__s))); \ + } \ + } while (0) + +#define PERF_UNLOCK(mutex) \ + do { \ + int __n = pthread_mutex_unlock((mutex)); \ + if (__n != 0) { \ + char __s[256]; \ + perf_log_fatal("pthread_mutex_unlock failed: %s", perf_strerror_r(__n, __s, sizeof(__s))); \ + } \ + } while (0) + +#define PERF_COND_INIT(cond) \ + do { \ + int __n = pthread_cond_init((cond), NULL); \ + if (__n != 0) { \ + char __s[256]; \ + perf_log_fatal("pthread_cond_init failed: %s", perf_strerror_r(__n, __s, sizeof(__s))); \ + } \ + } while (0) + +#define PERF_SIGNAL(cond) \ + do { \ + int __n = pthread_cond_signal((cond)); \ + if (__n != 0) { \ + char __s[256]; \ + perf_log_fatal("pthread_cond_signal failed: %s", perf_strerror_r(__n, __s, sizeof(__s))); \ + } \ + } while (0) + +#define PERF_BROADCAST(cond) \ + do { \ + int __n = pthread_cond_broadcast((cond)); \ + if (__n != 0) { \ + char __s[256]; \ + perf_log_fatal("pthread_cond_broadcast failed: %s", perf_strerror_r(__n, __s, sizeof(__s))); \ + } \ + } while (0) + +#define PERF_WAIT(cond, mutex) \ + do { \ + int __n = pthread_cond_wait((cond), (mutex)); \ + if (__n != 0) { \ + char __s[256]; \ + perf_log_fatal("pthread_cond_wait failed: %s", perf_strerror_r(__n, __s, sizeof(__s))); \ + } \ + } while (0) + +#define PERF_TIMEDWAIT(cond, mutex, when, timedout) \ + do { \ + int __n = pthread_cond_timedwait((cond), (mutex), (when)); \ + bool* res = (timedout); \ + if (__n != 0 && __n != ETIMEDOUT) { \ + char __s[256]; \ + perf_log_fatal("pthread_cond_timedwait failed: %s", perf_strerror_r(__n, __s, sizeof(__s))); \ + } \ + if (res != NULL) { \ + *res = (__n != 0); \ + } \ + } while (0) + +static __inline__ uint64_t perf_get_time(void) +{ + struct timeval tv; + gettimeofday(&tv, NULL); + return tv.tv_sec * MILLION + tv.tv_usec; +} + +#define PERF_SAFE_DIV(n, d) ((d) == 0 ? 0 : (n) / (d)) + +#endif |