From e26b3863b7fd5fa1b7e6a4f45d18710cef16a039 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sat, 5 May 2012 16:41:52 -0700 Subject: [PATCH] Initial import --- .gitignore | 17 +-- COPYING | 15 ++ Makefile | 21 +++ README.markdown | 105 ++++++++++++++ README.md | 4 - dns.h | 33 +++++ dnsblast.c | 379 ++++++++++++++++++++++++++++++++++++++++++++++++ dnsblast.h | 74 ++++++++++ 8 files changed, 633 insertions(+), 15 deletions(-) create mode 100644 COPYING create mode 100644 Makefile create mode 100644 README.markdown delete mode 100644 README.md create mode 100644 dns.h create mode 100644 dnsblast.c create mode 100644 dnsblast.h diff --git a/.gitignore b/.gitignore index 8df9393..ee70c52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,7 @@ -# Compiled Object files -*.slo -*.lo +*.dSYM +*.log *.o - -# Compiled Dynamic libraries -*.so - -# Compiled Static libraries -*.lai -*.la -*.a +*.s +*~ +.DS_Store +dnsblast diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..fc19352 --- /dev/null +++ b/COPYING @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2012 Frank Denis + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..031f861 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ + +OPTIMIZATION ?= -O2 +STDFLAGS ?= -std=c99 +DEBUGFLAGS ?= -Waggregate-return -Wcast-align -Wcast-qual \ +-Wchar-subscripts -Wcomment -Wimplicit -Wmissing-declarations \ +-Wmissing-prototypes -Wnested-externs -Wparentheses -Wwrite-strings \ +-Wformat=2 -Wall -Wextra + +CFLAGS ?= $(OPTIMIZATION) $(STDFLAGS) $(DEBUGFLAGS) + +all: dnsblast + +dnsblast: Makefile dnsblast.o + $(CC) dnsblast.o -o dnsblast $(LDFLAGS) + +dnsblast.o: Makefile dnsblast.c dns.h dnsblast.h + $(CC) -c dnsblast.c -o dnsblast.o $(CFLAGS) + +clean: + rm -f dnsblast *.a *.d *.o + rm -rf *.dSYM diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..b543010 --- /dev/null +++ b/README.markdown @@ -0,0 +1,105 @@ +DNSBlast +======== + +`dnsblast` is a simple and really stupid load testing tool for DNS resolvers. + +Give it the IP address of a resolver, the total number of queries you +want to send, the rate (number of packets per second), and `dnsblast` +will tell you how well the resolver is able to keep up. + +What it is: +----------- + +- a tool to spot bugs in DNS resolvers. +- a tool to help you tune and tweak DNS resolver code in order to +improve it in some way. +- a tool to help you tune and tweak the operating system so that it +can properly cope with a slew of UDP packets. +- a tool to test a resolver with real queries sent to the real and +scary interwebz, not to a sandbox. + +What it is not: +--------------- + +- a tool for DoS'ing resolvers. There are way more efficient ways to +achieve this. +- a benchmarking tool. +- a tool for testing anything but how the server behaves under load. +If you need a serious test suite, take a look at what Unbound +provides. + +What it does: +------------- + +It sends queries for names like +`.com`. + +Yes, that's 4 random characters dot com. Doing that achieves a +NXDOMAIN vs "oh cool, we got a reply" ratio that is surprisingly close +to the one you get from real queries made by real users. + +Different query types are sent. Namely SOA, A, AAA, MX and TXT, and +the probability that a query type gets picked is also close to its +probability in the real world. + +Names are occasionally repeated, also to get closer to what happens in +the real world. That triggers resolver code responsible for queuing +and merging queries. + +The test is deterministic: the exact same sequence of packets is sent +every time you fire up `dnsblast`. The magic resides in the power of +the `rand()` function with a fixed seed. + +What it does not: +----------------- + +It doesn't support DNSSec, it doesn't send anything using TCP, it +doesn't pay attention to the content the resolver sents. + +Fuzzing: +-------- + +In addition, `dnsblast` can send malformed queries. + +Most resolvers just ignore these, so don't expect a high +replies/queries ratio. But this feature can also help spotting bugs. + +The fuzzer is really, really, really simple, though. It just changes +some random bytes. It doesn't even pay attention to the server's +behavior. + +How do I compile it? +-------------------- + +Type: `make`. + +The code it trivial and should be fairly portable, although it only +gets tested on OSX and OpenBSD. + +How do I use it? +---------------- + +To send a shitload of queries to 127.0.0.1: + dnsblast 127.0.0.1 + +To send 50,000 queries to 127.0.0.1: + + dnsblast 127.0.0.1 50000 + +To send 50,000 queries at a rate of 100 queries per second: + + dnsblast 127.0.0.1 50000 100 + +To send 50,000 queries at a rate of 100 qps to a non standard-port, like 5353: + + dnsblast 127.0.0.1 50000 100 5353 + +To send malformed packets, prepend "fuzz": + + dnsblast fuzz 127.0.0.1 + dnsblast fuzz 127.0.0.1 50000 + dnsblast fuzz 127.0.0.1 50000 100 + dnsblast fuzz 127.0.0.1 50000 100 5353 + +If you think that it desperately cries for `getopt()`, you're absolutely correct. + diff --git a/README.md b/README.md deleted file mode 100644 index 3fe905b..0000000 --- a/README.md +++ /dev/null @@ -1,4 +0,0 @@ -dnsblast -======== - -A simple and stupid load testing tool for DNS resolvers \ No newline at end of file diff --git a/dns.h b/dns.h new file mode 100644 index 0000000..359af41 --- /dev/null +++ b/dns.h @@ -0,0 +1,33 @@ + +#ifndef __DNS_H__ +#define __DNS_H__ + +#include +#include + +#define TYPE_A 1U +#define TYPE_SOA 6U +#define TYPE_MX 15U +#define TYPE_TXT 16U +#define TYPE_AAAA 28U + +#define FLAGS_OPCODE_QUERY 0x0 +#define FLAGS_RECURSION_DESIRED 0x100 + +#define CLASS_IN 1U + +typedef struct { + uint16_t id; + uint16_t flags; + uint16_t qdcount; + uint16_t ancount; + uint16_t nscount; + uint16_t arcount; +} __attribute__((__packed__)) DNS_Header; + +#define PUT_HTONS(dst, val) do { \ + *dst++ = val >> 8; \ + *dst++ = val & 0xff; \ +} while (0) + +#endif diff --git a/dnsblast.c b/dnsblast.c new file mode 100644 index 0000000..18c6d5c --- /dev/null +++ b/dnsblast.c @@ -0,0 +1,379 @@ + +#include "dnsblast.h" + +static unsigned long long +get_nanoseconds(void) +{ + struct timeval tv; + gettimeofday(&tv, NULL); + + return tv.tv_sec * 1000000000LL + tv.tv_usec * 1000LL; +} + +static int +init_context(Context * const context, const int sock, + const struct addrinfo * const ai, const _Bool fuzz) +{ + const unsigned long long now = get_nanoseconds(); + *context = (Context) { + .received_packets = 0UL, .sent_packets = 0UL, + .last_status_update = now, .startup_date = now, + .sock = sock, .ai = ai, .fuzz = fuzz, .sending = 1 + }; + + DNS_Header * const question_header = (DNS_Header *) context->question; + *question_header = (DNS_Header) { + .flags = htons(FLAGS_OPCODE_QUERY | FLAGS_RECURSION_DESIRED), + .qdcount = htons(1U), .ancount = 0U, .nscount = 0U, .arcount = 0U + }; + + return 0; +} + +static int +find_name_component_len(const char *name) +{ + int name_pos = 0; + + while (name[name_pos] != '.' && name[name_pos] != 0) { + if (name_pos >= UCHAR_MAX) { + return EOF; + } + name_pos++; + } + return name_pos; +} + +static int +encode_name(unsigned char ** const encoded_ptr, size_t encoded_size, + const char * const name) +{ + unsigned char *encoded = *encoded_ptr; + const char *name_current = name; + int name_current_pos; + + assert(encoded_size > (size_t) 0U); + encoded_size--; + for (;;) { + name_current_pos = find_name_component_len(name); + if (name_current_pos == EOF || + encoded_size <= (size_t) name_current_pos) { + return -1; + } + *encoded++ = (unsigned char) name_current_pos; + memcpy(encoded, name_current, name_current_pos); + encoded_size -= name_current_pos - (size_t) 1U; + encoded += name_current_pos; + if (name_current[name_current_pos] == 0) { + break; + } + name_current += name_current_pos + 1U; + } + *encoded++ = 0; + *encoded_ptr = encoded; + + return 0; +} + +static int +fuzz(unsigned char * const question, const size_t packet_size) +{ + int p = REFUZZ_PROBABILITY; + + do { + question[rand() % packet_size] = rand() % 0xff; + } while (rand() < p && (p = p / 2) > 0); + + return 0; +} + +static int +blast(Context * const context, const char * const name, const uint16_t type) +{ + unsigned char * const question = context->question; + DNS_Header * const question_header = (DNS_Header *) question; + unsigned char * const question_data = question + sizeof *question_header; + const size_t sizeof_question_data = + sizeof question - sizeof *question_header; + + question_header->id = context->id++; + unsigned char *msg = question_data; + assert(sizeof_question_data > (size_t) 2U); + encode_name(&msg, sizeof_question_data - (size_t) 2U, name); + PUT_HTONS(msg, type); + PUT_HTONS(msg, CLASS_IN); + const size_t packet_size = (size_t) (msg - question); + + if (context->fuzz != 0) { + fuzz(question, packet_size); + } + while (sendto(context->sock, question, packet_size, 0, + context->ai->ai_addr, context->ai->ai_addrlen) + != (ssize_t) packet_size) { + if (errno != EAGAIN && errno != EINTR) { + perror("sendto"); + exit(EXIT_FAILURE); + } + } + context->sent_packets++; + + return 0; +} + +static void +usage(void) { + puts("\nUsage: dnsblast [fuzz] [] [] []\n"); + exit(EXIT_SUCCESS); +} + +static struct addrinfo * +resolve(const char * const host, const char * const port) +{ + struct addrinfo *ai, hints; + + memset(&hints, 0, sizeof hints); + hints = (struct addrinfo) { + .ai_family = AF_UNSPEC, .ai_flags = 0, .ai_socktype = SOCK_DGRAM, + .ai_protocol = IPPROTO_UDP + }; + const int gai_err = getaddrinfo(host, port, &hints, &ai); + if (gai_err != 0) { + fprintf(stderr, "[%s:%s]: [%s]\n", host, port, gai_strerror(gai_err)); + exit(EXIT_FAILURE); + } + return ai; +} + +static int +get_random_name(char * const name, size_t name_size) +{ + const char charset_alnum[36] = "abcdefghijklmnopqrstuvwxyz0123456789"; + + assert(name_size > (size_t) 8U); + const int r1 = rand(), r2 = rand(); + name[0] = charset_alnum[(r1) % sizeof charset_alnum]; + name[1] = charset_alnum[(r1 >> 16) % sizeof charset_alnum]; + name[2] = charset_alnum[(r2) % sizeof charset_alnum]; + name[3] = charset_alnum[(r2 >> 16) % sizeof charset_alnum]; + name[4] = '.'; name[5] = 'c'; name[6] = 'o'; name[7] = 'm'; + name[8] = 0; + + return 0; +} + +static uint16_t +get_random_type(void) +{ + const size_t weighted_types_len = + sizeof weighted_types / sizeof weighted_types[0]; + size_t i = 0U; + const int rnd = rand(); + int pos = RAND_MAX; + + do { + pos -= weighted_types[i].weight; + if (rnd > pos) { + return weighted_types[i].type; + } + } while (++i < weighted_types_len); + + return weighted_types[rand() % weighted_types_len].type; +} + +static int +get_sock(const char * const host, const char * const port, + struct addrinfo ** const ai_ref) +{ + int flag = 1; + int sock; + + *ai_ref = resolve(host, port); + sock = socket((*ai_ref)->ai_family, (*ai_ref)->ai_socktype, + (*ai_ref)->ai_protocol); + if (sock == -1) { + return -1; + } + setsockopt(sock, SOL_SOCKET, SO_RCVBUFFORCE, + &(int[]) { MAX_UDP_BUFFER_SIZE }, sizeof (int)); + setsockopt(sock, SOL_SOCKET, SO_SNDBUFFORCE, + &(int[]) { MAX_UDP_BUFFER_SIZE }, sizeof (int)); +#if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DONT) + setsockopt(sock, IPPROTO_IP, IP_MTU_DISCOVER, + &(int[]) { IP_PMTUDISC_DONT }, sizeof (int)); +#elif defined(IP_DONTFRAG) + setsockopt(sock, IPPROTO_IP, IP_DONTFRAG, &(int[]) { 0 }, sizeof (int)); +#endif + assert(ioctl(sock, FIONBIO, &flag) == 0); + + return sock; +} + +static int +receive(Context * const context) +{ + unsigned char buf[MAX_UDP_DATA_SIZE]; + + while (recv(context->sock, buf, sizeof buf, 0) == (ssize_t) -1) { + if (errno == EAGAIN) { + return 1; + } + assert(errno == EINTR); + } + context->received_packets++; + + return 0; +} + +static int +update_status(const Context * const context) +{ + const unsigned long long now = get_nanoseconds(); + const unsigned long long elapsed = now - context->startup_date; + unsigned long long rate = + context->received_packets * 1000000000ULL / elapsed; + if (rate > context->pps) { + rate = context->pps; + } + printf("Sent: [%lu] - Received: [%lu] - Reply rate: [%llu pps] - " + "Ratio: [%.2f%%] \r", + context->sent_packets, context->received_packets, rate, + (double) context->received_packets * 100.0 / + (double) context->sent_packets); + fflush(stdout); + + return 0; +} + +static int +periodically_update_status(Context * const context) +{ + unsigned long long now = get_nanoseconds(); + + if (now - context->last_status_update < UPDATE_STATUS_PERIOD) { + return 1; + } + update_status(context); + context->last_status_update = now; + + return 0; +} + +static int +empty_receive_queue(Context * const context) +{ + while (receive(context) == 0) + ; + periodically_update_status(context); + + return 0; +} + +static int +throttled_receive(Context * const context) +{ + unsigned long long now = get_nanoseconds(), now2; + const unsigned long long elapsed = now - context->startup_date; + const unsigned long long max_packets = + context->pps * elapsed / 1000000000UL; + + if (context->sending == 1 && context->sent_packets <= max_packets) { + empty_receive_queue(context); + } + const unsigned long long excess = context->sent_packets - max_packets; + const unsigned long long time_to_wait = excess / context->pps; + int remaining_time = (int) (time_to_wait * 1000ULL); + int ret; + struct pollfd pfd = { .fd = context->sock, + .events = POLLIN | POLLERR }; + if (context->sending == 0) { + remaining_time = -1; + } else if (remaining_time < 0) { + remaining_time = 0; + } + do { + ret = poll(&pfd, (nfds_t) 1, remaining_time); + if (ret == 0) { + periodically_update_status(context); + return 0; + } + if (ret == -1) { + if (errno != EAGAIN && errno != EINTR) { + perror("poll"); + exit(EXIT_FAILURE); + } + continue; + } + assert(ret == 1); + empty_receive_queue(context); + now2 = get_nanoseconds(); + remaining_time -= (now2 - now) / 1000; + now = now2; + } while (remaining_time > 0); + + return 0; +} + +int +main(int argc, char *argv[]) +{ + char name[100U] = "."; + Context context; + struct addrinfo *ai; + const char *host; + const char *port = "domain"; + unsigned long pps = ULONG_MAX; + unsigned long send_count = ULONG_MAX; + int sock; + uint16_t type; + _Bool fuzz = 0; + + if (argc < 2 || argc > 6) { + usage(); + } + if (strcasecmp(argv[1], "fuzz") == 0) { + fuzz = 1; + argv++; + argc--; + } + if (argc < 1) { + usage(); + } + host = argv[1]; + if (argc > 2) { + send_count = strtoul(argv[2], NULL, 10); + } + if (argc > 3) { + pps = strtoul(argv[3], NULL, 10); + } + if (argc > 4) { + port = argv[4]; + } + if ((sock = get_sock(host, port, &ai)) == -1) { + perror("Oops"); + exit(EXIT_FAILURE); + } + init_context(&context, sock, ai, fuzz); + context.pps = pps; + srand(0U); + assert(send_count > 0UL); + do { + if (rand() > REPEATED_NAME_PROBABILITY) { + get_random_name(name, sizeof name); + } + type = get_random_type(); + blast(&context, name, type); + throttled_receive(&context); + } while (--send_count > 0UL); + update_status(&context); + + context.sending = 0; + while (context.sent_packets != context.received_packets) { + throttled_receive(&context); + } + freeaddrinfo(ai); + assert(close(sock) == 0); + update_status(&context); + putchar('\n'); + + return 0; +} diff --git a/dnsblast.h b/dnsblast.h new file mode 100644 index 0000000..2863832 --- /dev/null +++ b/dnsblast.h @@ -0,0 +1,74 @@ + +#ifndef __DNSBLAST_H__ +#define __DNSBLAST_H__ 1 + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dns.h" + +#define MAX_UDP_DATA_SIZE (0xffff - 20U - 8U) + +#ifndef UPDATE_STATUS_PERIOD +# define UPDATE_STATUS_PERIOD 500000000ULL +#endif + +#ifndef MAX_UDP_BUFFER_SIZE +# define MAX_UDP_BUFFER_SIZE 2097152 +#endif + +#define REPEATED_NAME_PROBABILITY (int) ((RAND_MAX * 13854LL) / 100000LL) +#define REFUZZ_PROBABILITY (int) ((RAND_MAX * 500LL) / 100000LL) + +typedef struct Context_ { + unsigned char question[MAX_UDP_DATA_SIZE]; + const struct addrinfo *ai; + unsigned long long last_status_update; + unsigned long long startup_date; + unsigned long pps; + unsigned long received_packets; + unsigned long sent_packets; + int sock; + uint16_t id; + _Bool fuzz; + _Bool sending; +} Context; + +typedef struct WeightedType_ { + int weight; + uint16_t type; +} WeightedType; + +const WeightedType weighted_types[] = { + { .type = TYPE_A, .weight = (int) ((RAND_MAX * 77662LL) / 100000LL) }, + { .type = TYPE_SOA, .weight = (int) ((RAND_MAX * 803LL) / 100000LL) }, + { .type = TYPE_MX, .weight = (int) ((RAND_MAX * 5073LL) / 100000LL) }, + { .type = TYPE_TXT, .weight = (int) ((RAND_MAX * 2604LL) / 100000LL) }, + { .type = TYPE_AAAA, .weight = (int) ((RAND_MAX * 13858LL) / 100000LL) } +}; + +#ifndef SO_RCVBUFFORCE +# define SO_RCVBUFFORCE SO_RCVBUF +#endif +#ifndef SO_SNDBUFFORCE +# define SO_SNDBUFFORCE SO_SNDBUF +#endif + +#endif