diff options
Diffstat (limited to '')
-rw-r--r-- | src/output/dnssim.c | 502 | ||||
-rw-r--r-- | src/output/dnssim.h | 31 | ||||
-rw-r--r-- | src/output/dnssim.hh | 123 | ||||
-rw-r--r-- | src/output/dnssim.lua | 433 | ||||
-rw-r--r-- | src/output/dnssim/CHANGELOG.md | 16 | ||||
-rw-r--r-- | src/output/dnssim/common.c | 384 | ||||
-rw-r--r-- | src/output/dnssim/connection.c | 471 | ||||
-rw-r--r-- | src/output/dnssim/https2.c | 592 | ||||
-rw-r--r-- | src/output/dnssim/internal.h | 343 | ||||
-rw-r--r-- | src/output/dnssim/ll.h | 83 | ||||
-rw-r--r-- | src/output/dnssim/tcp.c | 356 | ||||
-rw-r--r-- | src/output/dnssim/tls.c | 475 | ||||
-rw-r--r-- | src/output/dnssim/udp.c | 156 |
13 files changed, 3965 insertions, 0 deletions
diff --git a/src/output/dnssim.c b/src/output/dnssim.c new file mode 100644 index 0000000..acd0a05 --- /dev/null +++ b/src/output/dnssim.c @@ -0,0 +1,502 @@ +/* + * Copyright (c) 2019-2020, CZ.NIC, z.s.p.o. + * All rights reserved. + * + * This file is part of dnsjit. + * + * dnsjit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dnsjit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dnsjit. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include "output/dnssim.h" +#include "output/dnssim/internal.h" +#include "output/dnssim/ll.h" +#include "core/assert.h" +#include "core/object/ip.h" +#include "core/object/ip6.h" + +#include <gnutls/gnutls.h> +#include <string.h> + +static core_log_t _log = LOG_T_INIT("output.dnssim"); +static output_dnssim_t _defaults = { LOG_T_INIT_OBJ("output.dnssim") }; + +static uint64_t _now_ms() +{ +#if HAVE_CLOCK_NANOSLEEP + struct timespec ts; + uint64_t now_ms; + if (clock_gettime(CLOCK_REALTIME, &ts)) { + mlfatal("clock_gettime()"); + } + now_ms = ts.tv_sec * 1000; + now_ms += ts.tv_nsec / 1000000; + return now_ms; +#else + mlfatal("clock_gettime() not available"); + return 0; +#endif +} + +core_log_t* output_dnssim_log() +{ + return &_log; +} + +output_dnssim_t* output_dnssim_new(size_t max_clients) +{ + output_dnssim_t* self; + int ret, i; + + mlfatal_oom(self = calloc(1, sizeof(_output_dnssim_t))); + *self = _defaults; + self->handshake_timeout_ms = 5000; + self->idle_timeout_ms = 10000; + output_dnssim_timeout_ms(self, 2000); + + _self->source = NULL; + _self->transport = OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY; + _self->h2_zero_out_msgid = false; + + self->max_clients = max_clients; + lfatal_oom(_self->client_arr = calloc(max_clients, sizeof(_output_dnssim_client_t))); + + for (i = 0; i < max_clients; ++i) { + _self->client_arr[i].dnssim = self; + } + + ret = gnutls_certificate_allocate_credentials(&_self->tls_cred); + if (ret < 0) + lfatal("failed to allocated TLS credentials (%s)", gnutls_strerror(ret)); + + ret = uv_loop_init(&_self->loop); + if (ret < 0) + lfatal("failed to initialize uv_loop (%s)", uv_strerror(ret)); + ldebug("initialized uv_loop"); + + return self; +} + +void output_dnssim_free(output_dnssim_t* self) +{ + mlassert_self(); + int ret, i; + _output_dnssim_source_t* source; + _output_dnssim_source_t* first = _self->source; + output_dnssim_stats_t* stats_prev; + + free(self->stats_sum->latency); + free(self->stats_sum); + do { + stats_prev = self->stats_current->prev; + free(self->stats_current->latency); + free(self->stats_current); + self->stats_current = stats_prev; + } while (self->stats_current != NULL); + + if (_self->source != NULL) { + // free cilcular linked list + do { + source = _self->source->next; + free(_self->source); + _self->source = source; + } while (_self->source != first); + } + + for (i = 0; i < self->max_clients; ++i) { + if (_self->client_arr[i].tls_ticket.size != 0) { + gnutls_free(_self->client_arr[i].tls_ticket.data); + } + } + free(_self->client_arr); + + ret = uv_loop_close(&_self->loop); + if (ret < 0) { + lcritical("failed to close uv_loop (%s)", uv_strerror(ret)); + } else { + ldebug("closed uv_loop"); + } + + gnutls_certificate_free_credentials(_self->tls_cred); + if (_self->tls_priority != NULL) { + gnutls_priority_deinit(*_self->tls_priority); + free(_self->tls_priority); + } + + free(self); +} + +void output_dnssim_log_name(output_dnssim_t* self, const char* name) +{ + mlassert_self(); + lassert(name, "name is nil"); + + strncpy(self->_log.name, name, sizeof(self->_log.name) - 1); + self->_log.name[sizeof(self->_log.name) - 1] = 0; + self->_log.is_obj = false; +} + +static uint32_t _extract_client(const core_object_t* obj) +{ + uint32_t client; + uint8_t* ip; + + switch (obj->obj_type) { + case CORE_OBJECT_IP: + ip = ((core_object_ip_t*)obj)->dst; + break; + case CORE_OBJECT_IP6: + ip = ((core_object_ip6_t*)obj)->dst; + break; + default: + return -1; + } + + memcpy(&client, ip, sizeof(client)); + return client; +} + +static void _receive(output_dnssim_t* self, const core_object_t* obj) +{ + mlassert_self(); + core_object_t* current = (core_object_t*)obj; + core_object_payload_t* payload; + uint32_t client; + + self->processed++; + + /* get payload from packet */ + for (;;) { + if (current->obj_type == CORE_OBJECT_PAYLOAD) { + payload = (core_object_payload_t*)current; + break; + } + if (current->obj_prev == NULL) { + self->discarded++; + lwarning("packet discarded (missing payload object)"); + return; + } + current = (core_object_t*)current->obj_prev; + } + + /* extract client information from IP/IP6 layer */ + for (;;) { + if (current->obj_type == CORE_OBJECT_IP || current->obj_type == CORE_OBJECT_IP6) { + client = _extract_client(current); + break; + } + if (current->obj_prev == NULL) { + self->discarded++; + lwarning("packet discarded (missing ip/ip6 object)"); + return; + } + current = (core_object_t*)current->obj_prev; + } + + if (self->free_after_use) { + /* free all objects except payload */ + current = (core_object_t*)obj; + core_object_t* parent = current; + while (current != NULL) { + parent = current; + current = (core_object_t*)current->obj_prev; + if (parent->obj_type != CORE_OBJECT_PAYLOAD) { + core_object_free(parent); + } + } + } + + if (_self->h2_zero_out_msgid) { + lassert(_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2, "must use HTTP/2 to zero-out msgid"); + if (payload->len < 2) { + self->discarded++; + lwarning("packet discarded (payload len < 2)"); + return; + } + uint8_t* data = (uint8_t*)payload->payload; + data[0] = 0x00; + data[1] = 0x00; + } + + if (client >= self->max_clients) { + self->discarded++; + lwarning("packet discarded (client exceeded max_clients)"); + return; + } + + ldebug("client(c): %d", client); + _output_dnssim_create_request(self, &_self->client_arr[client], payload); +} + +core_receiver_t output_dnssim_receiver() +{ + return (core_receiver_t)_receive; +} + +void output_dnssim_set_transport(output_dnssim_t* self, output_dnssim_transport_t tr) +{ + mlassert_self(); + + switch (tr) { + case OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY: + lnotice("transport set to UDP (no TCP fallback)"); + break; + case OUTPUT_DNSSIM_TRANSPORT_TCP: + lnotice("transport set to TCP"); + break; + case OUTPUT_DNSSIM_TRANSPORT_TLS: +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + lnotice("transport set to TLS"); +#else + lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + break; + case OUTPUT_DNSSIM_TRANSPORT_HTTPS2: +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + lnotice("transport set to HTTP/2 over TLS"); + if (&_self->h2_uri_authority[0]) + lnotice("set uri authority to: %s", _self->h2_uri_authority); +#else + lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + break; + case OUTPUT_DNSSIM_TRANSPORT_UDP: + lfatal("UDP transport with TCP fallback is not supported yet."); + break; + default: + lfatal("unknown or unsupported transport"); + break; + } + + _self->transport = tr; +} + +int output_dnssim_target(output_dnssim_t* self, const char* ip, uint16_t port) +{ + int ret; + mlassert_self(); + lassert(ip, "ip is nil"); + lassert(port, "port is nil"); + + ret = uv_ip6_addr(ip, port, (struct sockaddr_in6*)&_self->target); + if (ret != 0) { + ret = uv_ip4_addr(ip, port, (struct sockaddr_in*)&_self->target); + if (ret != 0) { + lfatal("failed to parse IPv4 or IPv6 from \"%s\"", ip); + } else { + ret = snprintf(_self->h2_uri_authority, _MAX_URI_LEN, "%s:%d", ip, port); + } + } else { + ret = snprintf(_self->h2_uri_authority, _MAX_URI_LEN, "[%s]:%d", ip, port); + } + + if (ret > 0) { + if (_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2) + lnotice("set uri authority to: %s", _self->h2_uri_authority); + } else { + _self->h2_uri_authority[0] = '\0'; + if (_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2) + lfatal("failed to set authority"); + } + + lnotice("set target to %s port %d", ip, port); + return 0; +} + +int output_dnssim_bind(output_dnssim_t* self, const char* ip) +{ + int ret; + mlassert_self(); + lassert(ip, "ip is nil"); + + _output_dnssim_source_t* source; + lfatal_oom(source = malloc(sizeof(_output_dnssim_source_t))); + + ret = uv_ip6_addr(ip, 0, (struct sockaddr_in6*)&source->addr); + if (ret != 0) { + ret = uv_ip4_addr(ip, 0, (struct sockaddr_in*)&source->addr); + if (ret != 0) { + lfatal("failed to parse IPv4 or IPv6 from \"%s\"", ip); + } + } + + if (_self->source == NULL) { + source->next = source; + _self->source = source; + } else { + source->next = _self->source->next; + _self->source->next = source; + } + + lnotice("bind to source address %s", ip); + return 0; +} + +int output_dnssim_tls_priority(output_dnssim_t* self, const char* priority) +{ + mlassert_self(); + lassert(priority, "priority is nil"); + + if (_self->tls_priority != NULL) { + gnutls_priority_deinit(*_self->tls_priority); + free(_self->tls_priority); + } + lfatal_oom(_self->tls_priority = malloc(sizeof(gnutls_priority_t))); + + int ret = gnutls_priority_init(_self->tls_priority, priority, NULL); + if (ret < 0) { + lfatal("failed to initialize TLS priority cache: %s", gnutls_strerror(ret)); + } else { + lnotice("GnuTLS priority set: %s", priority); + } + + return 0; +} + +int output_dnssim_run_nowait(output_dnssim_t* self) +{ + mlassert_self(); + + return uv_run(&_self->loop, UV_RUN_NOWAIT); +} + +void output_dnssim_timeout_ms(output_dnssim_t* self, uint64_t timeout_ms) +{ + mlassert_self(); + lassert(timeout_ms > 0, "timeout must be greater than 0"); + + if (self->stats_sum != NULL) { + free(self->stats_sum->latency); + free(self->stats_sum); + self->stats_sum = 0; + } + if (self->stats_current != NULL) { + output_dnssim_stats_t* stats_prev; + do { + stats_prev = self->stats_current->prev; + free(self->stats_current->latency); + free(self->stats_current); + self->stats_current = stats_prev; + } while (self->stats_current != NULL); + } + + self->timeout_ms = timeout_ms; + + lfatal_oom(self->stats_sum = calloc(1, sizeof(output_dnssim_stats_t))); + lfatal_oom(self->stats_sum->latency = calloc(self->timeout_ms + 1, sizeof(uint64_t))); + + lfatal_oom(self->stats_current = calloc(1, sizeof(output_dnssim_stats_t))); + lfatal_oom(self->stats_current->latency = calloc(self->timeout_ms + 1, sizeof(uint64_t))); + + self->stats_first = self->stats_current; +} + +void output_dnssim_h2_uri_path(output_dnssim_t* self, const char* uri_path) +{ + mlassert_self(); + lassert(uri_path, "uri_path is nil"); + lassert(strlen(uri_path) < _MAX_URI_LEN, "uri_path too long"); + + strncpy(_self->h2_uri_path, uri_path, _MAX_URI_LEN - 1); + _self->h2_uri_path[_MAX_URI_LEN - 1] = 0; + lnotice("http2: set uri path to: %s", _self->h2_uri_path); +} + +void output_dnssim_h2_method(output_dnssim_t* self, const char* method) +{ + mlassert_self(); + lassert(method, "method is nil"); + + if (strcmp("GET", method) == 0) { + _self->h2_method = OUTPUT_DNSSIM_H2_GET; + } else if (strcmp("POST", method) == 0) { + _self->h2_method = OUTPUT_DNSSIM_H2_POST; + } else { + lfatal("http2: unsupported method: \"%s\"", method); + } + + lnotice("http2: set method to %s", method); +} + +void output_dnssim_h2_zero_out_msgid(output_dnssim_t* self, bool zero_out_msgid) +{ + mlassert_self(); + + if (zero_out_msgid) { + lassert(_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2, "transport must be set to HTTP/2 to set zero_out_msgid"); + _self->h2_zero_out_msgid = zero_out_msgid; + } +} + +static void _on_stats_timer_tick(uv_timer_t* handle) +{ + uint64_t now_ms = _now_ms(); + output_dnssim_t* self; + mlassert(handle, "handle is nil"); + self = (output_dnssim_t*)handle->data; + mlassert_self(); + lassert(self->stats_sum, "stats_sum is nil"); + lassert(self->stats_current, "stats_current is nil"); + + lnotice("total processed:%10ld; answers:%10ld; discarded:%10ld; ongoing:%10ld", + self->processed, self->stats_sum->answers, self->discarded, self->ongoing); + + output_dnssim_stats_t* stats_next; + lfatal_oom(stats_next = calloc(1, sizeof(output_dnssim_stats_t))); + lfatal_oom(stats_next->latency = calloc(self->timeout_ms + 1, sizeof(uint64_t))); + + self->stats_current->until_ms = now_ms; + stats_next->since_ms = now_ms; + stats_next->conn_active = self->stats_current->conn_active; + + stats_next->ongoing = self->ongoing; + stats_next->prev = self->stats_current; + self->stats_current->next = stats_next; + self->stats_current = stats_next; +} + +void output_dnssim_stats_collect(output_dnssim_t* self, uint64_t interval_ms) +{ + uint64_t now_ms = _now_ms(); + mlassert_self(); + lassert(self->stats_sum, "stats_sum is nil"); + lassert(self->stats_current, "stats_current is nil"); + + if (self->stats_interval_ms != 0) { + lfatal("statistics collection has already started!"); + } + self->stats_interval_ms = interval_ms; + + self->stats_sum->since_ms = now_ms; + self->stats_current->since_ms = now_ms; + + _self->stats_timer.data = (void*)self; + uv_timer_init(&_self->loop, &_self->stats_timer); + uv_timer_start(&_self->stats_timer, _on_stats_timer_tick, interval_ms, interval_ms); +} + +void output_dnssim_stats_finish(output_dnssim_t* self) +{ + uint64_t now_ms = _now_ms(); + mlassert_self(); + lassert(self->stats_sum, "stats_sum is nil"); + lassert(self->stats_current, "stats_current is nil"); + + self->stats_sum->until_ms = now_ms; + self->stats_current->until_ms = now_ms; + + uv_timer_stop(&_self->stats_timer); + uv_close((uv_handle_t*)&_self->stats_timer, NULL); +} diff --git a/src/output/dnssim.h b/src/output/dnssim.h new file mode 100644 index 0000000..f843000 --- /dev/null +++ b/src/output/dnssim.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019, CZ.NIC, z.s.p.o. + * All rights reserved. + * + * This file is part of dnsjit. + * + * dnsjit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dnsjit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dnsjit. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "core/log.h" +#include "core/receiver.h" + +#ifndef __dnsjit_output_dnssim_h +#define __dnsjit_output_dnssim_h + +#include <stdbool.h> + +#include "output/dnssim.hh" + +#endif diff --git a/src/output/dnssim.hh b/src/output/dnssim.hh new file mode 100644 index 0000000..a17125d --- /dev/null +++ b/src/output/dnssim.hh @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2018-2020, CZ.NIC, z.s.p.o. + * All rights reserved. + * + * This file is part of dnsjit. + * + * dnsjit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dnsjit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dnsjit. If not, see <http://www.gnu.org/licenses/>. + */ + +//lua:require("dnsjit.core.log") +//lua:require("dnsjit.core.receiver_h") + +typedef enum output_dnssim_transport { + OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY, + OUTPUT_DNSSIM_TRANSPORT_UDP, + OUTPUT_DNSSIM_TRANSPORT_TCP, + OUTPUT_DNSSIM_TRANSPORT_TLS, + OUTPUT_DNSSIM_TRANSPORT_HTTPS2 +} output_dnssim_transport_t; + +typedef enum output_dnssim_h2_method { + OUTPUT_DNSSIM_H2_GET, + OUTPUT_DNSSIM_H2_POST +} output_dnssim_h2_method_t; + +typedef struct output_dnssim_stats output_dnssim_stats_t; +struct output_dnssim_stats { + output_dnssim_stats_t* prev; + output_dnssim_stats_t* next; + + uint64_t* latency; + + uint64_t since_ms; + uint64_t until_ms; + + uint64_t requests; + uint64_t ongoing; + uint64_t answers; + + /* Number of connections that are open at the end of the stats interval. */ + uint64_t conn_active; + + /* Number of connection handshake attempts during the stats interval. */ + uint64_t conn_handshakes; + + /* Number of connection that have been resumed with TLS session resumption. */ + uint64_t conn_resumed; + + /* Number of timed out connection handshakes during the stats interval. */ + uint64_t conn_handshakes_failed; + + uint64_t rcode_noerror; + uint64_t rcode_formerr; + uint64_t rcode_servfail; + uint64_t rcode_nxdomain; + uint64_t rcode_notimp; + uint64_t rcode_refused; + uint64_t rcode_yxdomain; + uint64_t rcode_yxrrset; + uint64_t rcode_nxrrset; + uint64_t rcode_notauth; + uint64_t rcode_notzone; + uint64_t rcode_badvers; + uint64_t rcode_badkey; + uint64_t rcode_badtime; + uint64_t rcode_badmode; + uint64_t rcode_badname; + uint64_t rcode_badalg; + uint64_t rcode_badtrunc; + uint64_t rcode_badcookie; + uint64_t rcode_other; +}; + +typedef struct output_dnssim { + core_log_t _log; + + uint64_t processed; + uint64_t discarded; + uint64_t ongoing; + + output_dnssim_stats_t* stats_sum; + output_dnssim_stats_t* stats_current; + output_dnssim_stats_t* stats_first; + + size_t max_clients; + bool free_after_use; + + uint64_t timeout_ms; + uint64_t idle_timeout_ms; + uint64_t handshake_timeout_ms; + uint64_t stats_interval_ms; +} output_dnssim_t; + +core_log_t* output_dnssim_log(); + +output_dnssim_t* output_dnssim_new(size_t max_clients); +void output_dnssim_free(output_dnssim_t* self); + +void output_dnssim_log_name(output_dnssim_t* self, const char* name); +void output_dnssim_set_transport(output_dnssim_t* self, output_dnssim_transport_t tr); +int output_dnssim_target(output_dnssim_t* self, const char* ip, uint16_t port); +int output_dnssim_bind(output_dnssim_t* self, const char* ip); +int output_dnssim_tls_priority(output_dnssim_t* self, const char* priority); +int output_dnssim_run_nowait(output_dnssim_t* self); +void output_dnssim_timeout_ms(output_dnssim_t* self, uint64_t timeout_ms); +void output_dnssim_h2_uri_path(output_dnssim_t* self, const char* uri_path); +void output_dnssim_h2_method(output_dnssim_t* self, const char* method); +void output_dnssim_h2_zero_out_msgid(output_dnssim_t* self, bool zero_out_msgid); +void output_dnssim_stats_collect(output_dnssim_t* self, uint64_t interval_ms); +void output_dnssim_stats_finish(output_dnssim_t* self); + +core_receiver_t output_dnssim_receiver(); diff --git a/src/output/dnssim.lua b/src/output/dnssim.lua new file mode 100644 index 0000000..25193c4 --- /dev/null +++ b/src/output/dnssim.lua @@ -0,0 +1,433 @@ +-- Copyright (c) 2018-2021, CZ.NIC, z.s.p.o. +-- All rights reserved. +-- +-- This file is part of dnsjit. +-- +-- dnsjit is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- dnsjit is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with dnsjit. If not, see <http://www.gnu.org/licenses/>. + +-- dnsjit.output.dnssim +-- Simulate independent DNS clients over various transports +-- output = require("dnsjit.output.dnssim").new() +-- .SS Usage +-- output:udp() +-- output:target("::1", 53) +-- recv, rctx = output:receive() +-- -- pass in objects using recv(rctx, obj) +-- -- repeatedly call output:run_nowait() until it returns 0 +-- .SS DNS-over-TLS example configuration +-- output:tls("NORMAL:-VERS-ALL:+VERS-TLS1.3") -- enforce TLS 1.3 +-- .SS DNS-over-HTTPS/2 example configuration +-- output:https2({ method = "POST", uri_path = "/doh" }) +-- +-- Output module for simulating traffic from huge number of independent, +-- individual DNS clients. +-- Uses libuv for asynchronous communication. +-- There may only be a single DnsSim in a thread. +-- Use +-- .I dnsjit.core.thread +-- to have multiple DnsSim instances. +-- .P +-- With proper use of this component, it is possible to simulate hundreds of +-- thousands of clients when using a high-performance server. +-- This also applies for state-full transports. +-- The complete set-up is quite complex and requires other components. +-- See DNS Shotgun +-- .RI ( https://gitlab.nic.cz/knot/shotgun ) +-- for dnsjit scripts ready for use for high-performance +-- benchmarking. +module(...,package.seeall) + +require("dnsjit.output.dnssim_h") +local bit = require("bit") +local object = require("dnsjit.core.objects") +local ffi = require("ffi") +local C = ffi.C + +local DnsSim = {} + +local _DNSSIM_VERSION = 20210129 +local _DNSSIM_JSON_VERSION = 20200527 + +-- Create a new DnsSim output for up to max_clients. +function DnsSim.new(max_clients) + local self = { + obj = C.output_dnssim_new(max_clients), + max_clients = max_clients, + } + ffi.gc(self.obj, C.output_dnssim_free) + return setmetatable(self, { __index = DnsSim }) +end + +local function _check_version(version, req_version) + if req_version == nil then + return version + end + local min_version = tonumber(req_version) + if min_version == nil then + C.output_dnssim_log():fatal("invalid version number: "..req_version) + return nil + end + if version >= min_version then + return version + end + return nil +end + +-- Check that version of dnssim is at minimum the one passed as +-- .B req_version +-- and return the actual version number. +-- Return nil if the condition is not met. +-- +-- If no +-- .B req_version +-- is specified no check is done and only the version number is returned. +function DnsSim.check_version(req_version) + return _check_version(_DNSSIM_VERSION, req_version) +end + +-- Check that version of dnssim's JSON data format is at minimum the one passed as +-- .B req_version +-- and return the actual version number. +-- Return nil if the condition is not met. +-- +-- If no +-- .B req_version +-- is specified no check is done and only the version number is returned. +function DnsSim.check_json_version(req_version) + return _check_version(_DNSSIM_JSON_VERSION, req_version) +end + +-- Return the Log object to control logging of this instance or module. +-- Optionally, set the instance's log name. +-- Unique name should be used for each instance. +function DnsSim:log(name) + if self == nil then + return C.output_dnssim_log() + end + if name ~= nil then + C.output_dnssim_log_name(self.obj, name) + end + return self.obj._log +end + +-- Set the target IPv4/IPv6 address where queries will be sent to. +function DnsSim:target(ip, port) + local nport = tonumber(port) + if nport == nil then + self.obj._log:fatal("invalid port: "..port) + return -1 + end + if nport <= 0 or nport > 65535 then + self.obj._log:fatal("invalid port number: "..nport) + return -1 + end + return C.output_dnssim_target(self.obj, ip, nport) +end + +-- Specify source IPv4/IPv6 address for sending queries. +-- Can be set multiple times. +-- Addresses are selected round-robin when sending. +function DnsSim:bind(ip) + return C.output_dnssim_bind(self.obj, ip) +end + +-- Set the preferred transport to UDP. +-- +-- When the optional argument +-- .B tcp_fallback +-- is set to true, individual queries are re-tried over TCP when TC bit is set in the answer. +-- Defaults to +-- .B false +-- (aka only UDP is used). +function DnsSim:udp(tcp_fallback) + if tcp_fallback == true then + C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_UDP) + else + C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY) + end +end + +-- Set the transport to TCP. +function DnsSim:tcp() + C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_TCP) +end + +-- Set the transport to TLS. +-- +-- The optional argument +-- .B tls_priority +-- is a GnuTLS priority string, which can be used to select TLS versions, cipher suites etc. +-- For example: +-- +-- .RB "- """ NORMAL:%NO_TICKETS """" +-- will use defaults without TLS session resumption. +-- +-- .RB "- """ SECURE128:-VERS-ALL:+VERS-TLS1.3 """" +-- will use only TLS 1.3 with 128-bit secure ciphers. +-- +-- Refer to: +-- .I https://gnutls.org/manual/html_node/Priority-Strings.html +function DnsSim:tls(tls_priority) + if tls_priority ~= nil then + C.output_dnssim_tls_priority(self.obj, tls_priority) + end + C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_TLS) +end + +-- Set the transport to HTTP/2 over TLS. +-- +-- .B http2_options +-- is a lua table which supports the following keys: +-- +-- .B method: +-- .B GET +-- (default) +-- or +-- .B POST +-- +-- .B uri_path: +-- where queries will be sent. +-- Defaults to +-- .B /dns-query +-- +-- .B zero_out_msgid: +-- when +-- .B true +-- (default), query ID is always set to 0 +-- +-- See tls() method for +-- .B tls_priority +-- documentation. +function DnsSim:https2(http2_options, tls_priority) + if tls_priority ~= nil then + C.output_dnssim_tls_priority(self.obj, tls_priority) + end + + uri_path = "/dns-query" + zero_out_msgid = true + method = "GET" + + if http2_options ~= nil then + if type(http2_options) ~= "table" then + self.obj._log:fatal("http2_options must be a table") + else + if http2_options["uri_path"] ~= nil then + uri_path = http2_options["uri_path"] + end + if http2_options["zero_out_msgid"] ~= nil and http2_options["zero_out_msgid"] ~= true then + zero_out_msgid = false + end + if http2_options["method"] ~= nil then + method = http2_options["method"] + end + end + end + + C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_HTTPS2) + C.output_dnssim_h2_uri_path(self.obj, uri_path) + C.output_dnssim_h2_method(self.obj, method) + C.output_dnssim_h2_zero_out_msgid(self.obj, zero_out_msgid) +end + +-- Set timeout for the individual requests in seconds (default 2s). +-- +-- .BR Beware : +-- increasing this value while the target resolver isn't very responsive +-- (cold cache, heavy load) may degrade DnsSim's performance and skew +-- the results. +function DnsSim:timeout(seconds) + if seconds == nil then + seconds = 2 + end + timeout_ms = math.floor(seconds * 1000) + C.output_dnssim_timeout_ms(self.obj, timeout_ms) +end + +-- Set TCP connection idle timeout for connection reuse according to RFC7766, +-- Section 6.2.3 (defaults to 10s). +-- When set to zero, connections are closed immediately after there are no +-- more pending queries. +function DnsSim:idle_timeout(seconds) + if seconds == nil then + seconds = 10 + end + self.obj.idle_timeout_ms = math.floor(seconds * 1000) +end + +-- Set TCP connection handshake timeout (defaults to 5s). +-- During heavy load, the server may no longer accept new connections. +-- This parameter ensures such connection attempts are aborted after the +-- timeout expires. +function DnsSim:handshake_timeout(seconds) + if seconds == nil then + seconds = 5 + end + self.obj.handshake_timeout_ms = math.floor(seconds * 1000) +end + +-- Run the libuv loop once without blocking when there is no I/O. +-- This should be called repeatedly until 0 is returned and no more data +-- is expected to be received by DnsSim. +function DnsSim:run_nowait() + return C.output_dnssim_run_nowait(self.obj) +end + +-- Set this to true if DnsSim should free the memory of passed-in objects +-- (useful when using +-- .I dnsjit.filter.copy +-- to pass objects from different thread). +function DnsSim:free_after_use(free_after_use) + self.obj.free_after_use = free_after_use +end + +-- Number of input packets discarded due to various reasons. +-- To investigate causes, run with increased logging level. +function DnsSim:discarded() + return tonumber(self.obj.discarded) +end + +-- Number of valid requests (input packets) processed. +function DnsSim:requests() + return tonumber(self.obj.stats_sum.requests) +end + +-- Number of requests that received an answer +function DnsSim:answers() + return tonumber(self.obj.stats_sum.answers) +end + +-- Number of requests that received a NOERROR response +function DnsSim:noerror() + return tonumber(self.obj.stats_sum.rcode_noerror) +end + +-- Configure statistics to be collected every N seconds. +function DnsSim:stats_collect(seconds) + if seconds == nil then + self.obj._log:fatal("number of seconds must be set for stats_collect()") + end + interval_ms = math.floor(seconds * 1000) + C.output_dnssim_stats_collect(self.obj, interval_ms) +end + +-- Stop the collection of statistics. +function DnsSim:stats_finish() + C.output_dnssim_stats_finish(self.obj) +end + +-- Export the results to a JSON file. +function DnsSim:export(filename) + local file = io.open(filename, "w") + if file == nil then + self.obj._log:fatal("export failed: no filename") + return + end + + local function write_stats(file, stats) + file:write( + "{ ", + '"since_ms":', tonumber(stats.since_ms), ',', + '"until_ms":', tonumber(stats.until_ms), ',', + '"requests":', tonumber(stats.requests), ',', + '"ongoing":', tonumber(stats.ongoing), ',', + '"answers":', tonumber(stats.answers), ',', + '"conn_active":', tonumber(stats.conn_active), ',', + '"conn_handshakes":', tonumber(stats.conn_handshakes), ',', + '"conn_resumed":', tonumber(stats.conn_resumed), ',', + '"conn_handshakes_failed":', tonumber(stats.conn_handshakes_failed), ',', + '"rcode_noerror":', tonumber(stats.rcode_noerror), ',', + '"rcode_formerr":', tonumber(stats.rcode_formerr), ',', + '"rcode_servfail":', tonumber(stats.rcode_servfail), ',', + '"rcode_nxdomain":', tonumber(stats.rcode_nxdomain), ',', + '"rcode_notimp":', tonumber(stats.rcode_notimp), ',', + '"rcode_refused":', tonumber(stats.rcode_refused), ',', + '"rcode_yxdomain":', tonumber(stats.rcode_yxdomain), ',', + '"rcode_yxrrset":', tonumber(stats.rcode_yxrrset), ',', + '"rcode_nxrrset":', tonumber(stats.rcode_nxrrset), ',', + '"rcode_notauth":', tonumber(stats.rcode_notauth), ',', + '"rcode_notzone":', tonumber(stats.rcode_notzone), ',', + '"rcode_badvers":', tonumber(stats.rcode_badvers), ',', + '"rcode_badkey":', tonumber(stats.rcode_badkey), ',', + '"rcode_badtime":', tonumber(stats.rcode_badtime), ',', + '"rcode_badmode":', tonumber(stats.rcode_badmode), ',', + '"rcode_badname":', tonumber(stats.rcode_badname), ',', + '"rcode_badalg":', tonumber(stats.rcode_badalg), ',', + '"rcode_badtrunc":', tonumber(stats.rcode_badtrunc), ',', + '"rcode_badcookie":', tonumber(stats.rcode_badcookie), ',', + '"rcode_other":', tonumber(stats.rcode_other), ',', + '"latency":[') + file:write(tonumber(stats.latency[0])) + for i=1,tonumber(self.obj.timeout_ms) do + file:write(',', tonumber(stats.latency[i])) + end + file:write("]}") + end + + file:write( + "{ ", + '"version":', _DNSSIM_JSON_VERSION, ',', + '"merged":false,', + '"stats_interval_ms":', tonumber(self.obj.stats_interval_ms), ',', + '"timeout_ms":', tonumber(self.obj.timeout_ms), ',', + '"idle_timeout_ms":', tonumber(self.obj.idle_timeout_ms), ',', + '"handshake_timeout_ms":', tonumber(self.obj.handshake_timeout_ms), ',', + '"discarded":', self:discarded(), ',', + '"stats_sum":') + write_stats(file, self.obj.stats_sum) + file:write( + ',', + '"stats_periodic":[') + + local stats = self.obj.stats_first + write_stats(file, stats) + + while (stats.next ~= nil) do + stats = stats.next + file:write(',') + write_stats(file, stats) + end + + file:write(']}') + file:close() + self.obj._log:notice("results exported to "..filename) +end + +-- Return the C function and context for receiving objects. +-- Only +-- .I dnsjit.filter.core.object.ip +-- or +-- .I dnsjit.filter.core.object.ip6 +-- objects are supported. +-- The component expects a 32bit integer (in host order) ranging from 0 +-- to max_clients written to first 4 bytes of destination IP. +-- See +-- .IR dnsjit.filter.ipsplit . +function DnsSim:receive() + local receive = C.output_dnssim_receiver() + return receive, self.obj +end + +-- Deprecated: use udp() instead. +-- +-- Set the transport to UDP (without any TCP fallback). +function DnsSim:udp_only() + C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY) +end + +-- dnsjit.filter.copy (3), +-- dnsjit.filter.ipsplit (3), +-- dnsjit.filter.core.object.ip (3), +-- dnsjit.filter.core.object.ip6 (3), +-- https://gitlab.nic.cz/knot/shotgun +return DnsSim diff --git a/src/output/dnssim/CHANGELOG.md b/src/output/dnssim/CHANGELOG.md new file mode 100644 index 0000000..9cbfa55 --- /dev/null +++ b/src/output/dnssim/CHANGELOG.md @@ -0,0 +1,16 @@ +dnssim v20210129 +================ + +- Added DNS-over-HTTPS support with https2() +- Added IPv4 support +- Abort operation on insufficient file descriptors +- Match QUESTION section of received responses +- Improvements in connection state handling +- Deprecate udp_only() in favor of udp() +- Allow setting logger name with log(name) +- Added check_version() and check_json_version() + +dnssim v20200723 +================ + +- First released dnssim version with UDP, TCP and DoT support diff --git a/src/output/dnssim/common.c b/src/output/dnssim/common.c new file mode 100644 index 0000000..e170aec --- /dev/null +++ b/src/output/dnssim/common.c @@ -0,0 +1,384 @@ +/* + * Copyright (c) 2019-2020, CZ.NIC, z.s.p.o. + * All rights reserved. + * + * This file is part of dnsjit. + * + * dnsjit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dnsjit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dnsjit. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include "output/dnssim.h" +#include "output/dnssim/internal.h" +#include "output/dnssim/ll.h" +#include "core/assert.h" + +#include <string.h> + +#define MAX_LABELS 127 + +static core_log_t _log = LOG_T_INIT("output.dnssim"); + +static void _close_request(_output_dnssim_request_t* req); + +static void _on_request_timeout(uv_timer_t* handle) +{ + _close_request((_output_dnssim_request_t*)handle->data); +} + +static ssize_t parse_qsection(core_object_dns_t* dns) +{ + core_object_dns_q_t q; + static core_object_dns_label_t labels[MAX_LABELS]; + const uint8_t* start; + int i; + int ret; + + if (!dns || !dns->have_qdcount) + return -1; + + start = dns->at; + + for (i = 0; i < dns->qdcount; i++) { + ret = core_object_dns_parse_q(dns, &q, labels, MAX_LABELS); + if (ret < 0) + return -1; + } + + return (dns->at - start); +} + +int _output_dnssim_answers_request(_output_dnssim_request_t* req, core_object_dns_t* response) +{ + const uint8_t* question; + ssize_t len; + + if (!response->have_id || !response->have_qdcount) + return _ERR_MALFORMED; + + if (req->dns_q->id != response->id) + return _ERR_MSGID; + + if (req->dns_q->qdcount != response->qdcount) + return _ERR_QUESTION; + + question = response->at; + len = parse_qsection(response); + + if (req->question_len != len) + return _ERR_QUESTION; + + if (memcmp(req->question, question, len) != 0) + return _ERR_QUESTION; + + return 0; +} + +void _output_dnssim_create_request(output_dnssim_t* self, _output_dnssim_client_t* client, core_object_payload_t* payload) +{ + int ret; + _output_dnssim_request_t* req; + mlassert_self(); + lassert(client, "client is nil"); + lassert(payload, "payload is nil"); + + lfatal_oom(req = calloc(1, sizeof(_output_dnssim_request_t))); + req->dnssim = self; + req->client = client; + req->payload = payload; + req->dns_q = core_object_dns_new(); + req->dns_q->obj_prev = (core_object_t*)req->payload; + req->dnssim->ongoing++; + req->state = _OUTPUT_DNSSIM_REQ_ONGOING; + req->stats = self->stats_current; + + ret = core_object_dns_parse_header(req->dns_q); + if (ret != 0) { + ldebug("discarded malformed dns query: couldn't parse header"); + goto failure; + } + + req->question = req->dns_q->at; + req->question_len = parse_qsection(req->dns_q); + if (req->question_len < 0) { + ldebug("discarded malformed dns query: invalid question"); + goto failure; + } + + req->dnssim->stats_sum->requests++; + req->stats->requests++; + + switch (_self->transport) { + case OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY: + case OUTPUT_DNSSIM_TRANSPORT_UDP: + ret = _output_dnssim_create_query_udp(self, req); + break; + case OUTPUT_DNSSIM_TRANSPORT_TCP: + ret = _output_dnssim_create_query_tcp(self, req); + break; + case OUTPUT_DNSSIM_TRANSPORT_TLS: +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + ret = _output_dnssim_create_query_tls(self, req); +#else + lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + break; + case OUTPUT_DNSSIM_TRANSPORT_HTTPS2: +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + ret = _output_dnssim_create_query_https2(self, req); +#else + lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + break; + default: + lfatal("unsupported dnssim transport"); + break; + } + if (ret < 0) { + goto failure; + } + + req->created_at = uv_now(&_self->loop); + req->ended_at = req->created_at + self->timeout_ms; + lfatal_oom(req->timer = malloc(sizeof(uv_timer_t))); + uv_timer_init(&_self->loop, req->timer); + req->timer->data = req; + uv_timer_start(req->timer, _on_request_timeout, self->timeout_ms, 0); + + return; +failure: + self->discarded++; + _close_request(req); + return; +} + +/* Bind before connect to be able to send from different source IPs. */ +int _output_dnssim_bind_before_connect(output_dnssim_t* self, uv_handle_t* handle) +{ + mlassert_self(); + lassert(handle, "handle is nil"); + + if (_self->source != NULL) { + struct sockaddr* addr = (struct sockaddr*)&_self->source->addr; + struct sockaddr* dest = (struct sockaddr*)&_self->target; + int ret = -1; + if (addr->sa_family != dest->sa_family) { + lfatal("failed to bind: source/desitnation address family mismatch"); + } + switch (handle->type) { + case UV_UDP: + ret = uv_udp_bind((uv_udp_t*)handle, addr, 0); + break; + case UV_TCP: + ret = uv_tcp_bind((uv_tcp_t*)handle, addr, 0); + break; + default: + lfatal("failed to bind: unsupported handle type"); + break; + } + if (ret < 0) { + /* This typically happens when we run out of file descriptors. + * Quit to prevent skewed results or unexpected behaviour. */ + lfatal("failed to bind: %s", uv_strerror(ret)); + return ret; + } + _self->source = _self->source->next; + } + return 0; +} + +void _output_dnssim_maybe_free_request(_output_dnssim_request_t* req) +{ + mlassert(req, "req is nil"); + + if (req->qry == NULL && req->timer == NULL) { + if (req->dnssim->free_after_use) { + core_object_payload_free(req->payload); + } + core_object_dns_free(req->dns_q); + free(req); + } +} + +static void _close_query(_output_dnssim_query_t* qry) +{ + mlassert(qry, "qry is nil"); + + switch (qry->transport) { + case OUTPUT_DNSSIM_TRANSPORT_UDP: + _output_dnssim_close_query_udp((_output_dnssim_query_udp_t*)qry); + break; + case OUTPUT_DNSSIM_TRANSPORT_TCP: + _output_dnssim_close_query_tcp((_output_dnssim_query_tcp_t*)qry); + break; + case OUTPUT_DNSSIM_TRANSPORT_TLS: +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + _output_dnssim_close_query_tls((_output_dnssim_query_tcp_t*)qry); +#else + mlfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + break; + case OUTPUT_DNSSIM_TRANSPORT_HTTPS2: +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + _output_dnssim_close_query_https2((_output_dnssim_query_tcp_t*)qry); +#else + mlfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + break; + default: + mlfatal("invalid query transport"); + break; + } +} + +static void _on_request_timer_closed(uv_handle_t* handle) +{ + _output_dnssim_request_t* req = (_output_dnssim_request_t*)handle->data; + mlassert(req, "req is nil"); + free(handle); + req->timer = NULL; + _output_dnssim_maybe_free_request(req); +} + +static void _close_request(_output_dnssim_request_t* req) +{ + if (req == NULL || req->state == _OUTPUT_DNSSIM_REQ_CLOSING) + return; + mlassert(req->state == _OUTPUT_DNSSIM_REQ_ONGOING, "request to be closed must be ongoing"); + req->state = _OUTPUT_DNSSIM_REQ_CLOSING; + req->dnssim->ongoing--; + + /* Calculate latency. */ + uint64_t latency; + req->ended_at = uv_now(&((_output_dnssim_t*)req->dnssim)->loop); + latency = req->ended_at - req->created_at; + if (latency > req->dnssim->timeout_ms) { + req->ended_at = req->created_at + req->dnssim->timeout_ms; + latency = req->dnssim->timeout_ms; + } + req->stats->latency[latency]++; + req->dnssim->stats_sum->latency[latency]++; + + if (req->timer != NULL) { + uv_timer_stop(req->timer); + uv_close((uv_handle_t*)req->timer, _on_request_timer_closed); + } + + /* Finish any queries in flight. */ + _output_dnssim_query_t* qry = req->qry; + if (qry != NULL) + _close_query(qry); + + _output_dnssim_maybe_free_request(req); +} + +void _output_dnssim_request_answered(_output_dnssim_request_t* req, core_object_dns_t* msg) +{ + mlassert(req, "req is nil"); + mlassert(msg, "msg is nil"); + + req->dnssim->stats_sum->answers++; + req->stats->answers++; + + switch (msg->rcode) { + case CORE_OBJECT_DNS_RCODE_NOERROR: + req->dnssim->stats_sum->rcode_noerror++; + req->stats->rcode_noerror++; + break; + case CORE_OBJECT_DNS_RCODE_FORMERR: + req->dnssim->stats_sum->rcode_formerr++; + req->stats->rcode_formerr++; + break; + case CORE_OBJECT_DNS_RCODE_SERVFAIL: + req->dnssim->stats_sum->rcode_servfail++; + req->stats->rcode_servfail++; + break; + case CORE_OBJECT_DNS_RCODE_NXDOMAIN: + req->dnssim->stats_sum->rcode_nxdomain++; + req->stats->rcode_nxdomain++; + break; + case CORE_OBJECT_DNS_RCODE_NOTIMP: + req->dnssim->stats_sum->rcode_notimp++; + req->stats->rcode_notimp++; + break; + case CORE_OBJECT_DNS_RCODE_REFUSED: + req->dnssim->stats_sum->rcode_refused++; + req->stats->rcode_refused++; + break; + case CORE_OBJECT_DNS_RCODE_YXDOMAIN: + req->dnssim->stats_sum->rcode_yxdomain++; + req->stats->rcode_yxdomain++; + break; + case CORE_OBJECT_DNS_RCODE_YXRRSET: + req->dnssim->stats_sum->rcode_yxrrset++; + req->stats->rcode_yxrrset++; + break; + case CORE_OBJECT_DNS_RCODE_NXRRSET: + req->dnssim->stats_sum->rcode_nxrrset++; + req->stats->rcode_nxrrset++; + break; + case CORE_OBJECT_DNS_RCODE_NOTAUTH: + req->dnssim->stats_sum->rcode_notauth++; + req->stats->rcode_notauth++; + break; + case CORE_OBJECT_DNS_RCODE_NOTZONE: + req->dnssim->stats_sum->rcode_notzone++; + req->stats->rcode_notzone++; + break; + case CORE_OBJECT_DNS_RCODE_BADVERS: + req->dnssim->stats_sum->rcode_badvers++; + req->stats->rcode_badvers++; + break; + case CORE_OBJECT_DNS_RCODE_BADKEY: + req->dnssim->stats_sum->rcode_badkey++; + req->stats->rcode_badkey++; + break; + case CORE_OBJECT_DNS_RCODE_BADTIME: + req->dnssim->stats_sum->rcode_badtime++; + req->stats->rcode_badtime++; + break; + case CORE_OBJECT_DNS_RCODE_BADMODE: + req->dnssim->stats_sum->rcode_badmode++; + req->stats->rcode_badmode++; + break; + case CORE_OBJECT_DNS_RCODE_BADNAME: + req->dnssim->stats_sum->rcode_badname++; + req->stats->rcode_badname++; + break; + case CORE_OBJECT_DNS_RCODE_BADALG: + req->dnssim->stats_sum->rcode_badalg++; + req->stats->rcode_badalg++; + break; + case CORE_OBJECT_DNS_RCODE_BADTRUNC: + req->dnssim->stats_sum->rcode_badtrunc++; + req->stats->rcode_badtrunc++; + break; + case CORE_OBJECT_DNS_RCODE_BADCOOKIE: + req->dnssim->stats_sum->rcode_badcookie++; + req->stats->rcode_badcookie++; + break; + default: + req->dnssim->stats_sum->rcode_other++; + req->stats->rcode_other++; + } + + _close_request(req); +} + +void _output_dnssim_on_uv_alloc(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) +{ + mlfatal_oom(buf->base = malloc(suggested_size)); + buf->len = suggested_size; +} diff --git a/src/output/dnssim/connection.c b/src/output/dnssim/connection.c new file mode 100644 index 0000000..eeb1ce8 --- /dev/null +++ b/src/output/dnssim/connection.c @@ -0,0 +1,471 @@ +/* + * Copyright (c) 2020, CZ.NIC, z.s.p.o. + * All rights reserved. + * + * This file is part of dnsjit. + * + * dnsjit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dnsjit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dnsjit. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include "output/dnssim.h" +#include "output/dnssim/internal.h" +#include "output/dnssim/ll.h" +#include "core/assert.h" + +#include <string.h> + +static core_log_t _log = LOG_T_INIT("output.dnssim"); + +static bool _conn_is_connecting(_output_dnssim_connection_t* conn) +{ + return (conn->state >= _OUTPUT_DNSSIM_CONN_TCP_HANDSHAKE && conn->state <= _OUTPUT_DNSSIM_CONN_ACTIVE); +} + +void _output_dnssim_conn_maybe_free(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn can't be nil"); + mlassert(conn->client, "conn must belong to a client"); + if (conn->handle == NULL && conn->handshake_timer == NULL && conn->idle_timer == NULL) { + _ll_try_remove(conn->client->conn, conn); + if (conn->tls != NULL) { + free(conn->tls); + conn->tls = NULL; + } + if (conn->http2 != NULL) { + free(conn->http2); + conn->http2 = NULL; + } + free(conn); + } +} + +static void _on_handshake_timer_closed(uv_handle_t* handle) +{ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)handle->data; + mlassert(conn, "conn is nil"); + mlassert(conn->handshake_timer, "conn must have handshake timer when closing it"); + free(conn->handshake_timer); + conn->handshake_timer = NULL; + _output_dnssim_conn_maybe_free(conn); +} + +static void _on_idle_timer_closed(uv_handle_t* handle) +{ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)handle->data; + mlassert(conn, "conn is nil"); + mlassert(conn->idle_timer, "conn must have idle timer when closing it"); + free(conn->idle_timer); + conn->is_idle = false; + conn->idle_timer = NULL; + _output_dnssim_conn_maybe_free(conn); +} + +void _output_dnssim_conn_close(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn can't be nil"); + mlassert(conn->stats, "conn must have stats"); + mlassert(conn->client, "conn must have client"); + mlassert(conn->client->dnssim, "client must have dnssim"); + + output_dnssim_t* self = conn->client->dnssim; + + switch (conn->state) { + case _OUTPUT_DNSSIM_CONN_CLOSING: + case _OUTPUT_DNSSIM_CONN_CLOSED: + return; + case _OUTPUT_DNSSIM_CONN_TCP_HANDSHAKE: + case _OUTPUT_DNSSIM_CONN_TLS_HANDSHAKE: + conn->stats->conn_handshakes_failed++; + self->stats_sum->conn_handshakes_failed++; + break; + case _OUTPUT_DNSSIM_CONN_ACTIVE: + case _OUTPUT_DNSSIM_CONN_CONGESTED: + self->stats_current->conn_active--; + break; + case _OUTPUT_DNSSIM_CONN_INITIALIZED: + case _OUTPUT_DNSSIM_CONN_CLOSE_REQUESTED: + break; + default: + lfatal("unknown conn state: %d", conn->state); + } + if (conn->prevent_close) { + lassert(conn->state <= _OUTPUT_DNSSIM_CONN_CLOSE_REQUESTED, "conn already closing"); + conn->state = _OUTPUT_DNSSIM_CONN_CLOSE_REQUESTED; + return; + } + conn->state = _OUTPUT_DNSSIM_CONN_CLOSING; + + if (conn->handshake_timer != NULL) { + uv_timer_stop(conn->handshake_timer); + uv_close((uv_handle_t*)conn->handshake_timer, _on_handshake_timer_closed); + } + if (conn->idle_timer != NULL) { + conn->is_idle = false; + uv_timer_stop(conn->idle_timer); + uv_close((uv_handle_t*)conn->idle_timer, _on_idle_timer_closed); + } + + switch (_self->transport) { + case OUTPUT_DNSSIM_TRANSPORT_TCP: + _output_dnssim_tcp_close(conn); + break; + case OUTPUT_DNSSIM_TRANSPORT_TLS: +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + _output_dnssim_tls_close(conn); +#else + lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + break; + case OUTPUT_DNSSIM_TRANSPORT_HTTPS2: +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + _output_dnssim_https2_close(conn); +#else + lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + break; + default: + lfatal("unsupported transport"); + break; + } +} + +/* Close connection or run idle timer when there are no more outstanding queries. */ +void _output_dnssim_conn_idle(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn can't be nil"); + + if (conn->queued == NULL && conn->sent == NULL) { + if (conn->idle_timer == NULL) + _output_dnssim_conn_close(conn); + else if (!conn->is_idle) { + conn->is_idle = true; + uv_timer_again(conn->idle_timer); + } + } +} + +static void _send_pending_queries(_output_dnssim_connection_t* conn) +{ + _output_dnssim_query_tcp_t* qry; + mlassert(conn, "conn is nil"); + mlassert(conn->client, "conn->client is nil"); + qry = (_output_dnssim_query_tcp_t*)conn->client->pending; + + while (qry != NULL && conn->state == _OUTPUT_DNSSIM_CONN_ACTIVE) { + _output_dnssim_query_tcp_t* next = (_output_dnssim_query_tcp_t*)qry->qry.next; + if (qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_WRITE) { + switch (qry->qry.transport) { + case OUTPUT_DNSSIM_TRANSPORT_TCP: + _output_dnssim_tcp_write_query(conn, qry); + break; + case OUTPUT_DNSSIM_TRANSPORT_TLS: +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + _output_dnssim_tls_write_query(conn, qry); +#else + mlfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + break; + case OUTPUT_DNSSIM_TRANSPORT_HTTPS2: +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + _output_dnssim_https2_write_query(conn, qry); +#else + mlfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + break; + default: + mlfatal("unsupported protocol"); + break; + } + } + qry = next; + } +} + +int _output_dnssim_handle_pending_queries(_output_dnssim_client_t* client) +{ + int ret = 0; + mlassert(client, "client is nil"); + + if (client->pending == NULL) + return ret; + + output_dnssim_t* self = client->dnssim; + mlassert(self, "client must belong to dnssim"); + + /* Get active connection or find out whether new connection has to be opened. */ + bool is_connecting = false; + _output_dnssim_connection_t* conn = client->conn; + while (conn != NULL) { + if (conn->state == _OUTPUT_DNSSIM_CONN_ACTIVE) + break; + else if (_conn_is_connecting(conn)) + is_connecting = true; + conn = conn->next; + } + + if (conn != NULL) { /* Send data right away over active connection. */ + _send_pending_queries(conn); + } else if (!is_connecting) { /* No active or connecting connection -> open a new one. */ + lfatal_oom(conn = calloc(1, sizeof(_output_dnssim_connection_t))); + conn->state = _OUTPUT_DNSSIM_CONN_INITIALIZED; + conn->client = client; + conn->stats = self->stats_current; + if (_self->transport == OUTPUT_DNSSIM_TRANSPORT_TLS) { +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + ret = _output_dnssim_tls_init(conn); + if (ret < 0) { + free(conn); + return ret; + } +#else + lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + } else if (_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2) { +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + ret = _output_dnssim_https2_init(conn); + if (ret < 0) { + free(conn); + return ret; + } +#else + lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + } + ret = _output_dnssim_tcp_connect(self, conn); + if (ret < 0) + return ret; + _ll_append(client->conn, conn); + } /* Otherwise, pending queries wil be sent after connected callback. */ + + return ret; +} + +void _output_dnssim_conn_activate(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn is nil"); + mlassert(conn->client, "conn must be associated with a client"); + mlassert(conn->client->dnssim, "client must be associated with dnssim"); + + uv_timer_stop(conn->handshake_timer); + + conn->state = _OUTPUT_DNSSIM_CONN_ACTIVE; + conn->client->dnssim->stats_current->conn_active++; + conn->read_state = _OUTPUT_DNSSIM_READ_STATE_DNSLEN; + conn->dnsbuf_len = 2; + conn->dnsbuf_pos = 0; + conn->dnsbuf_free_after_use = false; + + _send_pending_queries(conn); + _output_dnssim_conn_idle(conn); +} + +int _process_dnsmsg(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn can't be nil"); + mlassert(conn->client, "conn must have client"); + mlassert(conn->client->dnssim, "client must have dnssim"); + + output_dnssim_t* self = conn->client->dnssim; + + core_object_payload_t payload = CORE_OBJECT_PAYLOAD_INIT(NULL); + core_object_dns_t dns_a = CORE_OBJECT_DNS_INIT(&payload); + + payload.payload = (uint8_t*)conn->dnsbuf_data; + payload.len = conn->dnsbuf_len; + + dns_a.obj_prev = (core_object_t*)&payload; + int ret = core_object_dns_parse_header(&dns_a); + if (ret != 0) { + lwarning("tcp response malformed"); + return _ERR_MALFORMED; + } + ldebug("tcp recv dnsmsg id: %04x", dns_a.id); + + _output_dnssim_query_t* qry; + + if (_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2) { + lassert(conn->http2, "conn must have http2 ctx"); + lassert(conn->http2->current_qry, "http2 has no current_qry"); + lassert(conn->http2->current_qry->qry.req, "current_qry has no req"); + lassert(conn->http2->current_qry->qry.req->dns_q, "req has no dns_q"); + + ret = _output_dnssim_answers_request(conn->http2->current_qry->qry.req, &dns_a); + switch (ret) { + case 0: + _output_dnssim_request_answered(conn->http2->current_qry->qry.req, &dns_a); + break; + case _ERR_MSGID: + lwarning("https2 QID mismatch: request=0x%04x, response=0x%04x", + conn->http2->current_qry->qry.req->dns_q->id, dns_a.id); + break; + case _ERR_QUESTION: + default: + lwarning("https2 response question mismatch"); + break; + } + } else { + qry = conn->sent; + while (qry != NULL) { + if (qry->req->dns_q->id == dns_a.id) { + ret = _output_dnssim_answers_request(qry->req, &dns_a); + if (ret != 0) { + lwarning("response question mismatch"); + } else { + _output_dnssim_request_answered(qry->req, &dns_a); + } + break; + } + qry = qry->next; + } + } + + return 0; +} + +static int _parse_dnsbuf_data(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn can't be nil"); + mlassert(conn->dnsbuf_pos == conn->dnsbuf_len, "attempt to parse incomplete dnsbuf_data"); + int ret = 0; + + switch (conn->read_state) { + case _OUTPUT_DNSSIM_READ_STATE_DNSLEN: { + uint16_t* p_dnslen = (uint16_t*)conn->dnsbuf_data; + conn->dnsbuf_len = ntohs(*p_dnslen); + if (conn->dnsbuf_len == 0) { + mlwarning("invalid dnslen received: 0"); + conn->dnsbuf_len = 2; + conn->read_state = _OUTPUT_DNSSIM_READ_STATE_DNSLEN; + } else if (conn->dnsbuf_len < 12) { + mldebug("invalid dnslen received: %d", conn->dnsbuf_len); + ret = -1; + } else { + mldebug("dnslen: %d", conn->dnsbuf_len); + conn->read_state = _OUTPUT_DNSSIM_READ_STATE_DNSMSG; + } + break; + } + case _OUTPUT_DNSSIM_READ_STATE_DNSMSG: + ret = _process_dnsmsg(conn); + if (ret) { + conn->read_state = _OUTPUT_DNSSIM_READ_STATE_INVALID; + } else { + conn->dnsbuf_len = 2; + conn->read_state = _OUTPUT_DNSSIM_READ_STATE_DNSLEN; + } + break; + default: + mlfatal("tcp invalid connection read_state"); + break; + } + + conn->dnsbuf_pos = 0; + if (conn->dnsbuf_free_after_use) { + conn->dnsbuf_free_after_use = false; + free(conn->dnsbuf_data); + } + conn->dnsbuf_data = NULL; + + return ret; +} + +static unsigned int _read_dns_stream_chunk(_output_dnssim_connection_t* conn, size_t len, const char* data) +{ + mlassert(conn, "conn can't be nil"); + mlassert(data, "data can't be nil"); + mlassert(len > 0, "no data to read"); + mlassert((conn->read_state == _OUTPUT_DNSSIM_READ_STATE_DNSLEN || conn->read_state == _OUTPUT_DNSSIM_READ_STATE_DNSMSG), + "connection has invalid read_state"); + + int ret = 0; + unsigned int nread; + size_t expected = conn->dnsbuf_len - conn->dnsbuf_pos; + mlassert(expected > 0, "no data expected"); + + if (conn->dnsbuf_free_after_use == false && expected > len) { + /* Start of partial read. */ + mlassert(conn->dnsbuf_pos == 0, "conn->dnsbuf_pos must be 0 at start of partial read"); + mlassert(conn->dnsbuf_len > 0, "conn->dnsbuf_len must be set at start of partial read"); + mlfatal_oom(conn->dnsbuf_data = malloc(conn->dnsbuf_len * sizeof(char))); + conn->dnsbuf_free_after_use = true; + } + + if (conn->dnsbuf_free_after_use) { /* Partial read is in progress. */ + char* dest = conn->dnsbuf_data + conn->dnsbuf_pos; + if (expected < len) + len = expected; + memcpy(dest, data, len); + conn->dnsbuf_pos += len; + nread = len; + } else { /* Complete and clean read. */ + mlassert(expected <= len, "not enough data to perform complete read"); + conn->dnsbuf_data = (char*)data; + conn->dnsbuf_pos = conn->dnsbuf_len; + nread = expected; + } + + /* If entire dnslen/dnsmsg was read, attempt to parse it. */ + if (conn->dnsbuf_len == conn->dnsbuf_pos) { + ret = _parse_dnsbuf_data(conn); + if (ret < 0) + return ret; + } + + return nread; +} + +void _output_dnssim_read_dns_stream(_output_dnssim_connection_t* conn, size_t len, const char* data) +{ + int pos = 0; + int chunk = 0; + while (pos < len) { + chunk = _read_dns_stream_chunk(conn, len - pos, data + pos); + if (chunk < 0) { + mlwarning("lost orientation in DNS stream, closing"); + _output_dnssim_conn_close(conn); + break; + } else { + pos += chunk; + } + } + mlassert((pos == len) || (chunk < 0), "dns stream read invalid, pos != len"); +} + +void _output_dnssim_read_dnsmsg(_output_dnssim_connection_t* conn, size_t len, const char* data) +{ + mlassert(conn, "conn is nil"); + mlassert(len > 0, "len is zero"); + mlassert(data, "no data"); + mlassert(conn->dnsbuf_pos == 0, "dnsbuf not empty"); + mlassert(conn->dnsbuf_free_after_use == false, "dnsbuf read in progress"); + + /* Read dnsmsg of given length from input data. */ + conn->dnsbuf_len = len; + conn->read_state = _OUTPUT_DNSSIM_READ_STATE_DNSMSG; + int nread = _read_dns_stream_chunk(conn, len, data); + + if (nread != len) { + mlwarning("failed to read received dnsmsg"); + if (conn->dnsbuf_free_after_use) + free(conn->dnsbuf_data); + } + + /* Clean state afterwards. */ + conn->read_state = _OUTPUT_DNSSIM_READ_STATE_DNSLEN; + conn->dnsbuf_len = 2; + conn->dnsbuf_pos = 0; + conn->dnsbuf_free_after_use = false; +} diff --git a/src/output/dnssim/https2.c b/src/output/dnssim/https2.c new file mode 100644 index 0000000..72fcdaf --- /dev/null +++ b/src/output/dnssim/https2.c @@ -0,0 +1,592 @@ +/* + * Copyright (c) 2020, CZ.NIC, z.s.p.o. + * All rights reserved. + * + * This file is part of dnsjit. + * + * dnsjit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dnsjit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dnsjit. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include "output/dnssim.h" +#include "output/dnssim/internal.h" +#include "output/dnssim/ll.h" +#include "core/assert.h" +#include "lib/base64url.h" + +#include <gnutls/gnutls.h> +#include <string.h> + +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + +#define OUTPUT_DNSSIM_MAKE_NV(NAME, VALUE, VALUELEN) \ + { \ + (uint8_t*)NAME, (uint8_t*)VALUE, sizeof(NAME) - 1, VALUELEN, \ + NGHTTP2_NV_FLAG_NONE \ + } + +#define OUTPUT_DNSSIM_MAKE_NV2(NAME, VALUE) \ + { \ + (uint8_t*)NAME, (uint8_t*)VALUE, sizeof(NAME) - 1, sizeof(VALUE) - 1, \ + NGHTTP2_NV_FLAG_NONE \ + } + +#define OUTPUT_DNSSIM_HTTP_GET_TEMPLATE "?dns=" +#define OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN (sizeof(OUTPUT_DNSSIM_HTTP_GET_TEMPLATE) - 1) +#define OUTPUT_DNSSIM_HTTP2_INITIAL_MAX_CONCURRENT_STREAMS 100 +#define OUTPUT_DNSSIM_HTTP2_DEFAULT_MAX_CONCURRENT_STREAMS 0xffffffffu + +static core_log_t _log = LOG_T_INIT("output.dnssim"); + +static ssize_t _http2_send(nghttp2_session* session, const uint8_t* data, size_t length, int flags, void* user_data) +{ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data; + mlassert(conn, "conn can't be null"); + mlassert(conn->tls, "conn must have tls ctx"); + mlassert(conn->tls->session, "conn must have tls session"); + + mldebug("http2 (%p): sending data, len=%ld", session, length); + + ssize_t len = 0; + if ((len = gnutls_record_send(conn->tls->session, data, length)) < 0) { + mlwarning("gnutls_record_send failed: %s", gnutls_strerror(len)); + len = NGHTTP2_ERR_CALLBACK_FAILURE; + } + + return len; +} + +static ssize_t _http2_on_data_provider_read(nghttp2_session* session, int32_t stream_id, uint8_t* buf, size_t length, uint32_t* data_flags, nghttp2_data_source* source, void* user_data) +{ + _output_dnssim_https2_data_provider_t* buffer = source->ptr; + mlassert(buffer, "no data provider"); + mlassert(buffer->len <= MAX_DNSMSG_SIZE, "invalid dnsmsg size: %zu B", buffer->len); + + ssize_t sent = (length < buffer->len) ? length : buffer->len; + mlassert(sent >= 0, "negative length of bytes to send"); + + memcpy(buf, buffer->buf, sent); + buffer->buf += sent; + buffer->len -= sent; + if (buffer->len == 0) + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + + return sent; +} + +static _output_dnssim_query_tcp_t* _http2_get_stream_qry(_output_dnssim_connection_t* conn, int32_t stream_id) +{ + mlassert(conn, "conn is nil"); + mlassert(stream_id >= 0, "invalid stream_id"); + + _output_dnssim_query_tcp_t* qry = (_output_dnssim_query_tcp_t*)conn->sent; + while (qry != NULL && qry->stream_id != stream_id) { + qry = (_output_dnssim_query_tcp_t*)qry->qry.next; + } + + return qry; +} + +static int _http2_on_header(nghttp2_session* session, const nghttp2_frame* frame, const uint8_t* name, size_t namelen, const uint8_t* value, size_t valuelen, uint8_t flags, void* user_data) +{ + if (frame->hd.type == NGHTTP2_HEADERS && frame->headers.cat == NGHTTP2_HCAT_RESPONSE) { + if (namelen == 7 && strncmp((char*)name, ":status", 7) == 0) { + if (valuelen != 3 || (value[0] != '1' && value[0] != '2')) { + /* When reponse code isn't 1xx or 2xx, close the query. + * This will result in request timeout, which currently seems + * slightly better than mocking SERVFAIL for statistics. */ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data; + mlassert(conn, "conn is nil"); + _output_dnssim_query_tcp_t* qry = _http2_get_stream_qry(conn, frame->hd.stream_id); + + if (qry != NULL) { + _output_dnssim_close_query_https2(qry); + mlinfo("http response %s, closing query", value); + } + } + } + } + return 0; +} + +static int _http2_on_data_recv(nghttp2_session* session, uint8_t flags, int32_t stream_id, const uint8_t* data, size_t len, void* user_data) +{ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data; + mlassert(conn, "conn is nil"); + + _output_dnssim_query_tcp_t* qry = _http2_get_stream_qry(conn, stream_id); + + mldebug("http2: data chunk recv, session=%p, len=%d", session, len); + + if (qry) { + if (qry->recv_buf_len == 0) { + if (len > MAX_DNSMSG_SIZE) { + mlwarning("http response exceeded maximum size of dns message"); + return -1; + } + mlfatal_oom(qry->recv_buf = malloc(len)); + memcpy(qry->recv_buf, data, len); + qry->recv_buf_len = len; + } else { + size_t total_len = qry->recv_buf_len + len; + if (total_len > MAX_DNSMSG_SIZE) { + mlwarning("http response exceeded maximum size of dns message"); + return -1; + } + mlfatal_oom(qry->recv_buf = realloc(qry->recv_buf, total_len)); + memcpy(qry->recv_buf + qry->recv_buf_len, data, len); + qry->recv_buf_len = total_len; + } + } else { + mldebug("no query associated with this stream id, ignoring"); + } + + return 0; +} + +static void _http2_check_max_streams(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn can't be null"); + mlassert(conn->http2, "conn must have http2 ctx"); + + switch (conn->state) { + case _OUTPUT_DNSSIM_CONN_ACTIVE: + if (conn->http2->open_streams >= conn->http2->max_concurrent_streams) { + mlinfo("http2 (%p): reached maximum number of concurrent streams (%ld)", + conn->http2->session, conn->http2->max_concurrent_streams); + conn->state = _OUTPUT_DNSSIM_CONN_CONGESTED; + } + break; + case _OUTPUT_DNSSIM_CONN_CONGESTED: + if (conn->http2->open_streams < conn->http2->max_concurrent_streams) + conn->state = _OUTPUT_DNSSIM_CONN_ACTIVE; + break; + default: + break; + } +} + +static int _http2_on_stream_close(nghttp2_session* session, int32_t stream_id, uint32_t error_code, void* user_data) +{ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data; + mlassert(conn, "conn can't be null"); + mlassert(conn->http2, "conn must have http2 ctx"); + mlassert(conn->http2->open_streams > 0, "conn has no open streams"); + + conn->http2->open_streams--; + _http2_check_max_streams(conn); + return 0; +} + +static int _http2_on_frame_recv(nghttp2_session* session, const nghttp2_frame* frame, void* user_data) +{ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data; + mlassert(conn, "conn can't be null"); + mlassert(conn->tls, "conn must have tls ctx"); + mlassert(conn->tls->session, "conn must have tls session"); + mlassert(conn->http2, "conn must have http2 ctx"); + + switch (frame->hd.type) { + case NGHTTP2_DATA: + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + mldebug("http2 (%p): final DATA frame recv", session); + _output_dnssim_query_tcp_t* qry = _http2_get_stream_qry(conn, frame->hd.stream_id); + + if (qry != NULL) { + conn->http2->current_qry = qry; + _output_dnssim_read_dnsmsg(conn, qry->recv_buf_len, (char*)qry->recv_buf); + } + } + break; + case NGHTTP2_SETTINGS: + if (!conn->http2->remote_settings_received) { + /* On the first SETTINGS frame, set concurrent streams to unlimited, same as nghttp2. */ + conn->http2->remote_settings_received = true; + conn->http2->max_concurrent_streams = OUTPUT_DNSSIM_HTTP2_DEFAULT_MAX_CONCURRENT_STREAMS; + _http2_check_max_streams(conn); + } + nghttp2_settings* settings = (nghttp2_settings*)frame; + int i; + for (i = 0; i < settings->niv; i++) { + switch (settings->iv[i].settings_id) { + case NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: + conn->http2->max_concurrent_streams = settings->iv[i].value; + _http2_check_max_streams(conn); + break; + default: + break; + } + } + break; + default: + break; + } + return 0; +} + +int _output_dnssim_https2_init(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn is nil"); + mlassert(conn->tls == NULL, "conn already has tls context"); + mlassert(conn->http2 == NULL, "conn already has http2 context"); + mlassert(conn->client, "conn must be associated with a client"); + mlassert(conn->client->dnssim, "client must have dnssim"); + + int ret = -1; + nghttp2_session_callbacks* callbacks; + nghttp2_option* option; + output_dnssim_t* self = conn->client->dnssim; + + /* Initialize TLS session. */ + ret = _output_dnssim_tls_init(conn); + if (ret < 0) + return ret; + + /* Configure ALPN to negotiate HTTP/2. */ + const gnutls_datum_t protos[] = { + { (unsigned char*)"h2", 2 } + }; + ret = gnutls_alpn_set_protocols(conn->tls->session, protos, 1, 0); + if (ret < 0) { + lwarning("failed to set ALPN protocol: %s", gnutls_strerror(ret)); + return ret; + } + + lfatal_oom(conn->http2 = calloc(1, sizeof(_output_dnssim_http2_ctx_t))); + conn->http2->max_concurrent_streams = OUTPUT_DNSSIM_HTTP2_INITIAL_MAX_CONCURRENT_STREAMS; + + /* Set up HTTP/2 callbacks and client. */ + lassert(nghttp2_session_callbacks_new(&callbacks) == 0, "out of memory"); + nghttp2_session_callbacks_set_send_callback(callbacks, _http2_send); + nghttp2_session_callbacks_set_on_header_callback(callbacks, _http2_on_header); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, _http2_on_data_recv); + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, _http2_on_frame_recv); + nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, _http2_on_stream_close); + + lassert(nghttp2_option_new(&option) == 0, "out of memory"); + nghttp2_option_set_peer_max_concurrent_streams(option, conn->http2->max_concurrent_streams); + + ret = nghttp2_session_client_new2(&conn->http2->session, callbacks, conn, option); + + nghttp2_session_callbacks_del(callbacks); + nghttp2_option_del(option); + + if (ret < 0) { + free(conn->http2); + conn->http2 = NULL; + } + + return ret; +} + +int _output_dnssim_https2_setup(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn is nil"); + mlassert(conn->tls, "conn must have tls ctx"); + mlassert(conn->tls->session, "conn must have tls session"); + mlassert(conn->http2, "conn must have http2 ctx"); + mlassert(conn->http2->session, "conn must have http2 session"); + + int ret = -1; + + /* Check "h2" protocol was negotiated with ALPN. */ + gnutls_datum_t proto; + ret = gnutls_alpn_get_selected_protocol(conn->tls->session, &proto); + if (ret < 0) { + mlwarning("http2: failed to get negotiated protocol: %s", gnutls_strerror(ret)); + return ret; + } + if (proto.size != 2 || memcmp("h2", proto.data, 2) != 0) { + mlwarning("http2: protocol is not negotiated"); + return ret; + } + + /* Submit SETTIGNS frame. */ + static const nghttp2_settings_entry iv[] = { + { NGHTTP2_SETTINGS_MAX_FRAME_SIZE, MAX_DNSMSG_SIZE }, + { NGHTTP2_SETTINGS_ENABLE_PUSH, 0 }, /* Only we can initiate streams. */ + }; + ret = nghttp2_submit_settings(conn->http2->session, NGHTTP2_FLAG_NONE, iv, sizeof(iv) / sizeof(*iv)); + if (ret < 0) { + mlwarning("http2: failed to submit SETTINGS: %s", nghttp2_strerror(ret)); + return ret; + } + + ret = 0; + return ret; +} + +void _output_dnssim_https2_process_input_data(_output_dnssim_connection_t* conn, size_t len, const char* data) +{ + mlassert(conn, "conn is nil"); + mlassert(conn->http2, "conn must have http2 ctx"); + mlassert(conn->http2->session, "conn must have http2 session"); + + /* Process incoming frames. */ + ssize_t ret = 0; + conn->prevent_close = true; + ret = nghttp2_session_mem_recv(conn->http2->session, (uint8_t*)data, len); + conn->prevent_close = false; + if (ret < 0) { + mlwarning("failed nghttp2_session_mem_recv: %s", nghttp2_strerror(ret)); + _output_dnssim_conn_close(conn); + return; + } else if (conn->state == _OUTPUT_DNSSIM_CONN_CLOSE_REQUESTED) { + _output_dnssim_conn_close(conn); + return; + } + mlassert(ret == len, "nghttp2_session_mem_recv didn't process all data"); + + /* Send any frames the read might have triggered. */ + ret = nghttp2_session_send(conn->http2->session); + if (ret < 0) { + mlwarning("failed nghttp2_session_send: %s", nghttp2_strerror(ret)); + _output_dnssim_conn_close(conn); + return; + } +} + +int _output_dnssim_create_query_https2(output_dnssim_t* self, _output_dnssim_request_t* req) +{ + mlassert_self(); + lassert(req, "req is nil"); + lassert(req->client, "request must have a client associated with it"); + + _output_dnssim_query_tcp_t* qry; + + lfatal_oom(qry = calloc(1, sizeof(_output_dnssim_query_tcp_t))); + + qry->qry.transport = OUTPUT_DNSSIM_TRANSPORT_HTTPS2; + qry->qry.req = req; + qry->qry.state = _OUTPUT_DNSSIM_QUERY_PENDING_WRITE; + qry->stream_id = -1; + req->qry = &qry->qry; // TODO change when adding support for multiple Qs for req + _ll_append(req->client->pending, &qry->qry); + + return _output_dnssim_handle_pending_queries(req->client); +} + +void _output_dnssim_close_query_https2(_output_dnssim_query_tcp_t* qry) +{ + mlassert(qry, "qry can't be null"); + mlassert(qry->qry.req, "query must be part of a request"); + _output_dnssim_request_t* req = qry->qry.req; + mlassert(req->client, "request must belong to a client"); + + _ll_try_remove(req->client->pending, &qry->qry); + if (qry->conn) { + _output_dnssim_connection_t* conn = qry->conn; + _ll_try_remove(conn->sent, &qry->qry); + qry->conn = NULL; + _output_dnssim_conn_idle(conn); + } + + if (qry->recv_buf != NULL) + free(qry->recv_buf); + + _ll_remove(req->qry, &qry->qry); + free(qry); +} + +void _output_dnssim_https2_close(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn can't be nil"); + mlassert(conn->http2, "conn must have http2 ctx"); + + nghttp2_session_del(conn->http2->session); + _output_dnssim_tls_close(conn); +} + +static int _http2_send_query_get(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry) +{ + mlassert(conn, "conn can't be null"); + mlassert(qry, "qry can't be null"); + mlassert(qry->qry.req, "req can't be null"); + mlassert(qry->qry.req->payload, "payload can't be null"); + mlassert(qry->qry.req->payload->len <= MAX_DNSMSG_SIZE, "payload too big"); + mlassert(conn->client, "conn must be associated with client"); + mlassert(conn->client->dnssim, "client must have dnssim"); + + output_dnssim_t* self = conn->client->dnssim; + core_object_payload_t* content = qry->qry.req->payload; + + const size_t uri_path_len = strlen(_self->h2_uri_path); + const size_t path_len = uri_path_len + + OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN + + (content->len * 4) / 3 + 3 /* upper limit of base64 encoding */ + + 1; /* terminating null byte */ + if (path_len >= _MAX_URI_LEN) { + self->discarded++; + linfo("http2: uri path with query too long, query discarded"); + return 0; + } + char path[path_len]; + memcpy(path, _self->h2_uri_path, uri_path_len); + memcpy(&path[uri_path_len], OUTPUT_DNSSIM_HTTP_GET_TEMPLATE, OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN); + + int32_t ret = base64url_encode(content->payload, content->len, + (uint8_t*)&path[uri_path_len + OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN], + sizeof(path) - uri_path_len - OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN - 1); + if (ret < 0) { + self->discarded++; + linfo("http2: base64url encode of query failed, query discarded"); + return 0; + } + + nghttp2_nv hdrs[] = { + OUTPUT_DNSSIM_MAKE_NV2(":method", "GET"), + OUTPUT_DNSSIM_MAKE_NV2(":scheme", "https"), + OUTPUT_DNSSIM_MAKE_NV(":authority", _self->h2_uri_authority, strlen(_self->h2_uri_authority)), + OUTPUT_DNSSIM_MAKE_NV(":path", path, uri_path_len + sizeof(OUTPUT_DNSSIM_HTTP_GET_TEMPLATE) - 1 + ret), + OUTPUT_DNSSIM_MAKE_NV2("accept", "application/dns-message"), + }; + + qry->stream_id = nghttp2_submit_request(conn->http2->session, NULL, hdrs, sizeof(hdrs) / sizeof(nghttp2_nv), NULL, NULL); + + if (qry->stream_id < 0) { + mldebug("http2 (%p): failed to submit request: %s", conn->http2->session, nghttp2_strerror(qry->stream_id)); + return -1; + } + mldebug("http2 (%p): GET %s", conn->http2->session, path); + conn->http2->open_streams++; + _http2_check_max_streams(conn); + + ret = nghttp2_session_send(conn->http2->session); + if (ret < 0) { + mldebug("http2 (%p): failed session send: %s", conn->http2->session, nghttp2_strerror(ret)); + return -1; + } + + return 0; +} + +static int _http2_send_query_post(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry) +{ + mlassert(conn, "conn can't be null"); + mlassert(qry, "qry can't be null"); + mlassert(qry->qry.req, "req can't be null"); + mlassert(qry->qry.req->payload, "payload can't be null"); + mlassert(qry->qry.req->payload->len <= MAX_DNSMSG_SIZE, "payload too big"); + mlassert(conn->client, "conn must be associated with client"); + mlassert(conn->client->dnssim, "client must have dnssim"); + + output_dnssim_t* self = conn->client->dnssim; + + core_object_payload_t* content = qry->qry.req->payload; + + int window_size = nghttp2_session_get_remote_window_size(conn->http2->session); + if (content->len > window_size) { + mldebug("http2 (%p): insufficient remote window size, deferring", conn->http2->session); + return 0; + } + + char content_length[6]; /* max dnslen "65535" */ + int content_length_len = snprintf(content_length, 6, "%zd", content->len); + + nghttp2_nv hdrs[] = { + OUTPUT_DNSSIM_MAKE_NV2(":method", "POST"), + OUTPUT_DNSSIM_MAKE_NV2(":scheme", "https"), + OUTPUT_DNSSIM_MAKE_NV(":authority", _self->h2_uri_authority, strlen(_self->h2_uri_authority)), + OUTPUT_DNSSIM_MAKE_NV(":path", _self->h2_uri_path, strlen(_self->h2_uri_path)), + OUTPUT_DNSSIM_MAKE_NV2("accept", "application/dns-message"), + OUTPUT_DNSSIM_MAKE_NV2("content-type", "application/dns-message"), + OUTPUT_DNSSIM_MAKE_NV("content-length", content_length, content_length_len) + }; + + _output_dnssim_https2_data_provider_t data = { + .buf = content->payload, + .len = content->len + }; + + nghttp2_data_provider data_provider = { + .source.ptr = &data, + .read_callback = _http2_on_data_provider_read + }; + + qry->stream_id = nghttp2_submit_request(conn->http2->session, NULL, hdrs, sizeof(hdrs) / sizeof(nghttp2_nv), &data_provider, NULL); + + if (qry->stream_id < 0) { + mldebug("http2 (%p): failed to submit request: %s", conn->http2->session, nghttp2_strerror(qry->stream_id)); + return -1; + } + mldebug("http2 (%p): POST payload len=%ld", conn->http2->session, content->len); + conn->http2->open_streams++; + _http2_check_max_streams(conn); + + window_size = nghttp2_session_get_stream_remote_window_size(conn->http2->session, qry->stream_id); + mlassert(content->len <= window_size, + "unsupported: http2 stream window size (%ld B) is smaller than dns payload (%ld B)", + window_size, content->len); + + int ret = nghttp2_session_send(conn->http2->session); + if (ret < 0) { + mldebug("http2 (%p): failed session send: %s", conn->http2->session, nghttp2_strerror(ret)); + return -1; + } + + return 0; +} + +void _output_dnssim_https2_write_query(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry) +{ + mlassert(qry, "qry can't be null"); + mlassert(qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_WRITE, "qry must be pending write"); + mlassert(conn, "conn can't be null"); + mlassert(conn->state == _OUTPUT_DNSSIM_CONN_ACTIVE, "connection state != ACTIVE"); + mlassert(conn->http2, "conn must have http2 ctx"); + mlassert(conn->http2->session, "conn must have http2 session"); + mlassert(conn->client, "conn must be associated with client"); + mlassert(conn->client->pending, "conn has no pending queries"); + mlassert(conn->client->dnssim, "client must have dnssim"); + + int ret = 0; + output_dnssim_t* self = conn->client->dnssim; + + if (!nghttp2_session_check_request_allowed(conn->http2->session)) { + mldebug("http2 (%p): request not allowed", conn->http2->session); + _output_dnssim_conn_close(conn); + return; + } + + switch (_self->h2_method) { + case OUTPUT_DNSSIM_H2_POST: + ret = _http2_send_query_post(conn, qry); + break; + case OUTPUT_DNSSIM_H2_GET: + ret = _http2_send_query_get(conn, qry); + break; + default: + lfatal("http2: unsupported method"); + } + + if (ret < 0) { + _output_dnssim_conn_close(conn); + return; + } + + qry->conn = conn; + _ll_remove(conn->client->pending, &qry->qry); + _ll_append(conn->sent, &qry->qry); + + /* Stop idle timer, since there are queries to answer now. */ + if (conn->idle_timer != NULL) { + conn->is_idle = false; + uv_timer_stop(conn->idle_timer); + } + + qry->qry.state = _OUTPUT_DNSSIM_QUERY_SENT; +} + +#endif diff --git a/src/output/dnssim/internal.h b/src/output/dnssim/internal.h new file mode 100644 index 0000000..b9feddf --- /dev/null +++ b/src/output/dnssim/internal.h @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2019-2020, CZ.NIC, z.s.p.o. + * All rights reserved. + * + * This file is part of dnsjit. + * + * dnsjit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dnsjit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dnsjit. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __dnsjit_output_dnssim_internal_h +#define __dnsjit_output_dnssim_internal_h + +#include <gnutls/gnutls.h> +#include <nghttp2/nghttp2.h> +#include <uv.h> +#include "core/object/dns.h" +#include "core/object/payload.h" + +#define DNSSIM_MIN_GNUTLS_VERSION 0x030603 +#define DNSSIM_MIN_GNUTLS_ERRORMSG "dnssim tls/https2 transport requires GnuTLS >= 3.6.3" + +#define _self ((_output_dnssim_t*)self) +#define _ERR_MALFORMED -2 +#define _ERR_MSGID -3 +#define _ERR_TC -4 +#define _ERR_QUESTION -5 + +#define _MAX_URI_LEN 65536 +#define MAX_DNSMSG_SIZE 65535 +#define WIRE_BUF_SIZE (MAX_DNSMSG_SIZE + 2 + 16384) /** max tcplen + 2b tcplen + 16kb tls record */ + +typedef struct _output_dnssim_request _output_dnssim_request_t; +typedef struct _output_dnssim_connection _output_dnssim_connection_t; +typedef struct _output_dnssim_client _output_dnssim_client_t; + +/* + * Query-related structures. + */ + +typedef struct _output_dnssim_query _output_dnssim_query_t; +struct _output_dnssim_query { + /* + * Next query in the list. + * + * Currently, next is used for TCP clients/connection, which makes it + * impossible to use for tracking multiple queries of a single request. + * + * TODO: refactor the linked lists to allow query to be part of multiple lists + */ + _output_dnssim_query_t* next; + + output_dnssim_transport_t transport; + _output_dnssim_request_t* req; + + /* Query state, currently used only for TCP. */ + enum { + _OUTPUT_DNSSIM_QUERY_PENDING_WRITE, + _OUTPUT_DNSSIM_QUERY_PENDING_WRITE_CB, + _OUTPUT_DNSSIM_QUERY_PENDING_CLOSE, + _OUTPUT_DNSSIM_QUERY_WRITE_FAILED, + _OUTPUT_DNSSIM_QUERY_SENT, + _OUTPUT_DNSSIM_QUERY_ORPHANED + } state; +}; + +typedef struct _output_dnssim_query_udp _output_dnssim_query_udp_t; +struct _output_dnssim_query_udp { + _output_dnssim_query_t qry; + + uv_udp_t* handle; + uv_buf_t buf; +}; + +typedef struct _output_dnssim_query_tcp _output_dnssim_query_tcp_t; +struct _output_dnssim_query_tcp { + _output_dnssim_query_t qry; + + /* Connection this query is assigned to. */ + _output_dnssim_connection_t* conn; + + uv_write_t write_req; + + /* Send buffers for libuv; 0 is for dnslen, 1 is for dnsmsg. */ + uv_buf_t bufs[2]; + + /* HTTP/2 stream id that was used to send this query. */ + int32_t stream_id; + + /* HTTP/2 expected content length. */ + int32_t content_len; + + /* Receive buffer (currently used only by HTTP/2). */ + uint8_t* recv_buf; + ssize_t recv_buf_len; +}; + +struct _output_dnssim_request { + /* List of queries associated with this request. */ + _output_dnssim_query_t* qry; + + /* Client this request belongs to. */ + _output_dnssim_client_t* client; + + /* The DNS question to be resolved. */ + core_object_payload_t* payload; + core_object_dns_t* dns_q; + const uint8_t* question; + ssize_t question_len; + + /* Timestamps for latency calculation. */ + uint64_t created_at; + uint64_t ended_at; + + /* Timer for tracking timeout of the request. */ + uv_timer_t* timer; + + /* The output component of this request. */ + output_dnssim_t* dnssim; + + /* State of the request. */ + enum { + _OUTPUT_DNSSIM_REQ_ONGOING, + _OUTPUT_DNSSIM_REQ_CLOSING + } state; + + /* Statistics interval in which this request is tracked. */ + output_dnssim_stats_t* stats; +}; + +/* + * Connection-related structures. + */ + +/* Read-state of connection's data stream. */ +typedef enum _output_dnssim_read_state { + _OUTPUT_DNSSIM_READ_STATE_CLEAN, + _OUTPUT_DNSSIM_READ_STATE_DNSLEN, /* Expecting bytes of dnslen. */ + _OUTPUT_DNSSIM_READ_STATE_DNSMSG, /* Expecting bytes of dnsmsg. */ + _OUTPUT_DNSSIM_READ_STATE_INVALID +} _output_dnssim_read_state_t; + +/* TLS-related data for a single connection. */ +typedef struct _output_dnssim_tls_ctx { + gnutls_session_t session; + uint8_t* buf; + ssize_t buf_len; + ssize_t buf_pos; + size_t write_queue_size; +} _output_dnssim_tls_ctx_t; + +/* HTTP2 context for a single connection. */ +typedef struct _output_dnssim_http2_ctx { + nghttp2_session* session; + + /* Query to which the dnsbuf currently being processed belongs to. */ + _output_dnssim_query_tcp_t* current_qry; + + /* Maximum number of concurrent and currently open streams. */ + uint32_t max_concurrent_streams; + uint32_t open_streams; + + /* Flag indicating whether we received the peer's initial SETTINGS frame. */ + bool remote_settings_received; +} _output_dnssim_http2_ctx_t; + +struct _output_dnssim_connection { + _output_dnssim_connection_t* next; + + uv_tcp_t* handle; + + /* Timeout timer for establishing the connection. */ + uv_timer_t* handshake_timer; + + /* Idle timer for connection reuse. rfc7766#section-6.2.3 */ + uv_timer_t* idle_timer; + bool is_idle; + + /* List of queries that have been queued (pending write callback). */ + _output_dnssim_query_t* queued; + + /* List of queries that have been sent over this connection. */ + _output_dnssim_query_t* sent; + + /* Client this connection belongs to. */ + _output_dnssim_client_t* client; + + /* State of the connection. + * Numeric ordering of constants is significant and follows the typical connection lifecycle. + * Ensure new states are added to a proper place. */ + enum { + _OUTPUT_DNSSIM_CONN_INITIALIZED = 0, + _OUTPUT_DNSSIM_CONN_TCP_HANDSHAKE = 10, + _OUTPUT_DNSSIM_CONN_TLS_HANDSHAKE = 20, + _OUTPUT_DNSSIM_CONN_ACTIVE = 30, + _OUTPUT_DNSSIM_CONN_CONGESTED = 35, + _OUTPUT_DNSSIM_CONN_CLOSE_REQUESTED = 38, + _OUTPUT_DNSSIM_CONN_CLOSING = 40, + _OUTPUT_DNSSIM_CONN_CLOSED = 50 + } state; + + /* State of the data stream read. */ + _output_dnssim_read_state_t read_state; + + /* Total length of the expected dns data (either 2 for dnslen, or dnslen itself). */ + size_t dnsbuf_len; + + /* Current position in the receive dns buffer. */ + size_t dnsbuf_pos; + + /* Receive buffer used for incomplete messages or dnslen. */ + char* dnsbuf_data; + bool dnsbuf_free_after_use; + + /* Statistics interval in which the handshake is tracked. */ + output_dnssim_stats_t* stats; + + /* TLS-related data. */ + _output_dnssim_tls_ctx_t* tls; + + /* HTTP/2-related data. */ + _output_dnssim_http2_ctx_t* http2; + + /* Prevents immediate closure of connection. Instead, connection is moved + * to CLOSE_REQUESTED state and setter of this flag is responsible for + * closing the connection when clearing this flag. */ + bool prevent_close; +}; + +/* + * Client structure. + */ + +struct _output_dnssim_client { + /* Dnssim component this client belongs to. */ + output_dnssim_t* dnssim; + + /* List of connections. + * Multiple connections may be used (e.g. some are already closed for writing). + */ + _output_dnssim_connection_t* conn; + + /* List of queries that are pending to be sent over any available connection. */ + _output_dnssim_query_t* pending; + + /* TLS-ticket for session resumption. */ + gnutls_datum_t tls_ticket; +}; + +/* + * DnsSim-related structures. + */ + +typedef struct _output_dnssim_source _output_dnssim_source_t; +struct _output_dnssim_source { + _output_dnssim_source_t* next; + struct sockaddr_storage addr; +}; + +typedef struct _output_dnssim _output_dnssim_t; +struct _output_dnssim { + output_dnssim_t pub; + + uv_loop_t loop; + uv_timer_t stats_timer; + + struct sockaddr_storage target; + _output_dnssim_source_t* source; + output_dnssim_transport_t transport; + + char h2_uri_authority[_MAX_URI_LEN]; + char h2_uri_path[_MAX_URI_LEN]; + bool h2_zero_out_msgid; + output_dnssim_h2_method_t h2_method; + + /* Array of clients, mapped by client ID (ranges from 0 to max_clients). */ + _output_dnssim_client_t* client_arr; + + gnutls_priority_t* tls_priority; + gnutls_certificate_credentials_t tls_cred; + char wire_buf[WIRE_BUF_SIZE]; /* thread-local buffer for processing tls input */ +}; + +/* Provides data for HTTP/2 data frames. */ +typedef struct { + const uint8_t* buf; + size_t len; +} _output_dnssim_https2_data_provider_t; + +/* + * Forward function declarations. + */ + +int _output_dnssim_bind_before_connect(output_dnssim_t* self, uv_handle_t* handle); +int _output_dnssim_create_query_udp(output_dnssim_t* self, _output_dnssim_request_t* req); +int _output_dnssim_create_query_tcp(output_dnssim_t* self, _output_dnssim_request_t* req); +void _output_dnssim_close_query_udp(_output_dnssim_query_udp_t* qry); +void _output_dnssim_close_query_tcp(_output_dnssim_query_tcp_t* qry); +int _output_dnssim_answers_request(_output_dnssim_request_t* req, core_object_dns_t* response); +void _output_dnssim_request_answered(_output_dnssim_request_t* req, core_object_dns_t* msg); +void _output_dnssim_maybe_free_request(_output_dnssim_request_t* req); +void _output_dnssim_on_uv_alloc(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf); +void _output_dnssim_create_request(output_dnssim_t* self, _output_dnssim_client_t* client, core_object_payload_t* payload); +int _output_dnssim_handle_pending_queries(_output_dnssim_client_t* client); +int _output_dnssim_tcp_connect(output_dnssim_t* self, _output_dnssim_connection_t* conn); +void _output_dnssim_tcp_close(_output_dnssim_connection_t* conn); +void _output_dnssim_tcp_write_query(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry); +void _output_dnssim_conn_close(_output_dnssim_connection_t* conn); +void _output_dnssim_conn_idle(_output_dnssim_connection_t* conn); +int _output_dnssim_handle_pending_queries(_output_dnssim_client_t* client); +void _output_dnssim_conn_activate(_output_dnssim_connection_t* conn); +void _output_dnssim_conn_maybe_free(_output_dnssim_connection_t* conn); +void _output_dnssim_read_dns_stream(_output_dnssim_connection_t* conn, size_t len, const char* data); +void _output_dnssim_read_dnsmsg(_output_dnssim_connection_t* conn, size_t len, const char* data); + +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION +int _output_dnssim_create_query_tls(output_dnssim_t* self, _output_dnssim_request_t* req); +void _output_dnssim_close_query_tls(_output_dnssim_query_tcp_t* qry); +int _output_dnssim_tls_init(_output_dnssim_connection_t* conn); +void _output_dnssim_tls_process_input_data(_output_dnssim_connection_t* conn); +void _output_dnssim_tls_close(_output_dnssim_connection_t* conn); +void _output_dnssim_tls_write_query(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry); + +int _output_dnssim_create_query_https2(output_dnssim_t* self, _output_dnssim_request_t* req); +void _output_dnssim_close_query_https2(_output_dnssim_query_tcp_t* qry); +int _output_dnssim_https2_init(_output_dnssim_connection_t* conn); +int _output_dnssim_https2_setup(_output_dnssim_connection_t* conn); +void _output_dnssim_https2_process_input_data(_output_dnssim_connection_t* conn, size_t len, const char* data); +void _output_dnssim_https2_close(_output_dnssim_connection_t* conn); +void _output_dnssim_https2_write_query(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry); +#endif + +#endif diff --git a/src/output/dnssim/ll.h b/src/output/dnssim/ll.h new file mode 100644 index 0000000..8e0b07a --- /dev/null +++ b/src/output/dnssim/ll.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019-2020, CZ.NIC, z.s.p.o. + * All rights reserved. + * + * This file is part of dnsjit. + * + * dnsjit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dnsjit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dnsjit. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __dnsjit_output_dnssim_ll_h +#define __dnsjit_output_dnssim_ll_h + +#include "core/assert.h" + +/* Utility macros for linked list structures. + * + * - "list" is the pointer to the first node of the linked list + * - "list" can be NULL if there are no nodes + * - every node has "next", which points to the next node (can be NULL) + */ + +/* Append a node to the list. + * + * Only a single node can be appended - node->next must be NULL. + */ +#define _ll_append(list, node) \ + { \ + glassert((node)->next == NULL, "node->next must be null when appending"); \ + if ((list) == NULL) \ + (list) = (node); \ + else if ((node) != NULL) { \ + typeof(list) _current = (list); \ + while (_current->next != NULL) \ + _current = _current->next; \ + _current->next = node; \ + } \ + } + +/* Remove a node from the list. + * + * In strict mode, the node must be present in the list. + */ +#define _ll_remove_template(list, node, strict) \ + { \ + if (strict) \ + glassert((list), "list can't be null when removing nodes"); \ + if ((list) != NULL && (node) != NULL) { \ + if ((list) == (node)) { \ + (list) = (node)->next; \ + (node)->next = NULL; \ + } else { \ + typeof(list) _current = (list); \ + while (_current != NULL && _current->next != (node)) { \ + if (strict) \ + glassert((_current->next), "list doesn't contain the node to be removed"); \ + _current = _current->next; \ + } \ + if (_current != NULL) { \ + _current->next = (node)->next; \ + (node)->next = NULL; \ + } \ + } \ + } \ + } + +/* Remove a node from the list. */ +#define _ll_remove(list, node) _ll_remove_template((list), (node), true) + +/* Remove a node from the list if it's present. */ +#define _ll_try_remove(list, node) _ll_remove_template((list), (node), false) + +#endif diff --git a/src/output/dnssim/tcp.c b/src/output/dnssim/tcp.c new file mode 100644 index 0000000..1f2b619 --- /dev/null +++ b/src/output/dnssim/tcp.c @@ -0,0 +1,356 @@ +/* + * Copyright (c) 2019-2020, CZ.NIC, z.s.p.o. + * All rights reserved. + * + * This file is part of dnsjit. + * + * dnsjit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dnsjit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dnsjit. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include "output/dnssim.h" +#include "output/dnssim/internal.h" +#include "output/dnssim/ll.h" +#include "core/assert.h" + +#include <string.h> + +static core_log_t _log = LOG_T_INIT("output.dnssim"); + +static void _move_queries_to_pending(_output_dnssim_query_tcp_t* qry) +{ + _output_dnssim_query_tcp_t* qry_tmp; + while (qry != NULL) { + mlassert(qry->conn, "query must be associated with conn"); + mlassert(qry->conn->state == _OUTPUT_DNSSIM_CONN_CLOSED, "conn must be closed"); + mlassert(qry->conn->client, "conn must be associated with client"); + qry_tmp = (_output_dnssim_query_tcp_t*)qry->qry.next; + qry->qry.next = NULL; + _ll_append(qry->conn->client->pending, &qry->qry); + qry->conn = NULL; + qry->qry.state = _OUTPUT_DNSSIM_QUERY_ORPHANED; + qry->stream_id = -1; + qry->recv_buf_len = 0; + if (qry->recv_buf != NULL) { + free(qry->recv_buf); + qry->recv_buf = NULL; + } + qry = qry_tmp; + } +} + +static void _on_tcp_closed(uv_handle_t* handle) +{ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)handle->data; + mlassert(conn, "conn is nil"); + conn->state = _OUTPUT_DNSSIM_CONN_CLOSED; + + /* Orphan any queries that are still unresolved. */ + _move_queries_to_pending((_output_dnssim_query_tcp_t*)conn->queued); + conn->queued = NULL; + _move_queries_to_pending((_output_dnssim_query_tcp_t*)conn->sent); + conn->sent = NULL; + + /* TODO Improve client re-connect behavior in case the connection fails to + * establish. Currently, queries are orphaned and attempted to be re-sent + * along with the next query that triggers a new connection. + * + * Attempting to establish new connection immediately leads to performance + * issues if the number of these attempts doesn't have upper limit. */ + ///* Ensure orhpaned queries are re-sent over a different connection. */ + //if (_output_dnssim_handle_pending_queries(conn->client) != 0) + // mlinfo("tcp: orphaned queries failed to be re-sent"); + + mlassert(conn->handle, "conn must have tcp handle when closing it"); + free(conn->handle); + conn->handle = NULL; + _output_dnssim_conn_maybe_free(conn); +} + +static void _on_tcp_query_written(uv_write_t* wr_req, int status) +{ + _output_dnssim_query_tcp_t* qry = (_output_dnssim_query_tcp_t*)wr_req->data; + mlassert(qry, "qry/wr_req->data is nil"); + mlassert(qry->conn, "query must be associated with connection"); + _output_dnssim_connection_t* conn = qry->conn; + + free(((_output_dnssim_query_tcp_t*)qry)->bufs[0].base); + + if (qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_CLOSE) { + qry->qry.state = status < 0 ? _OUTPUT_DNSSIM_QUERY_WRITE_FAILED : _OUTPUT_DNSSIM_QUERY_SENT; + _output_dnssim_request_t* req = qry->qry.req; + _output_dnssim_close_query_tcp(qry); + _output_dnssim_maybe_free_request(req); + qry = NULL; + } + + if (status < 0) { + if (status != UV_ECANCELED) + mlinfo("tcp write failed: %s", uv_strerror(status)); + if (qry != NULL) + qry->qry.state = _OUTPUT_DNSSIM_QUERY_WRITE_FAILED; + _output_dnssim_conn_close(conn); + return; + } + + if (qry == NULL) + return; + + /* Mark query as sent and assign it to connection. */ + mlassert(qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_WRITE_CB, "invalid query state"); + qry->qry.state = _OUTPUT_DNSSIM_QUERY_SENT; + if (qry->conn->state == _OUTPUT_DNSSIM_CONN_ACTIVE) { + mlassert(qry->conn->queued, "conn has no queued queries"); + _ll_remove(qry->conn->queued, &qry->qry); + _ll_append(qry->conn->sent, &qry->qry); + } +} + +void _output_dnssim_tcp_write_query(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry) +{ + mlassert(qry, "qry can't be null"); + mlassert(qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_WRITE, "qry must be pending write"); + mlassert(qry->qry.req, "req can't be null"); + mlassert(qry->qry.req->dns_q, "dns_q can't be null"); + mlassert(qry->qry.req->dns_q->obj_prev, "payload can't be null"); + mlassert(conn, "conn can't be null"); + mlassert(conn->state == _OUTPUT_DNSSIM_CONN_ACTIVE, "connection state != ACTIVE"); + mlassert(conn->client, "conn must be associated with client"); + mlassert(conn->client->pending, "conn has no pending queries"); + + mldebug("tcp write dnsmsg id: %04x", qry->qry.req->dns_q->id); + + core_object_payload_t* payload = (core_object_payload_t*)qry->qry.req->dns_q->obj_prev; + uint16_t* len; + mlfatal_oom(len = malloc(sizeof(uint16_t))); + *len = htons(payload->len); + qry->bufs[0] = uv_buf_init((char*)len, 2); + qry->bufs[1] = uv_buf_init((char*)payload->payload, payload->len); + + qry->conn = conn; + _ll_remove(conn->client->pending, &qry->qry); + _ll_append(conn->queued, &qry->qry); + + /* Stop idle timer, since there are queries to answer now. */ + if (conn->idle_timer != NULL) { + conn->is_idle = false; + uv_timer_stop(conn->idle_timer); + } + + qry->write_req.data = (void*)qry; + uv_write(&qry->write_req, (uv_stream_t*)conn->handle, qry->bufs, 2, _on_tcp_query_written); + qry->qry.state = _OUTPUT_DNSSIM_QUERY_PENDING_WRITE_CB; +} + +static void _on_tcp_read(uv_stream_t* handle, ssize_t nread, const uv_buf_t* buf) +{ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)handle->data; + output_dnssim_t* self = conn->client->dnssim; + + if (nread > 0) { + mldebug("tcp nread: %d", nread); + switch (_self->transport) { + case OUTPUT_DNSSIM_TRANSPORT_TCP: + _output_dnssim_read_dns_stream(conn, nread, buf->base); + break; + case OUTPUT_DNSSIM_TRANSPORT_TLS: + case OUTPUT_DNSSIM_TRANSPORT_HTTPS2: +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + mlassert(conn->tls, "con must have tls ctx"); + conn->tls->buf = (uint8_t*)buf->base; + conn->tls->buf_pos = 0; + conn->tls->buf_len = nread; + _output_dnssim_tls_process_input_data(conn); +#else + mlfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + break; + default: + mlfatal("unsupported transport"); + break; + } + } else if (nread < 0) { + if (nread != UV_EOF) + mlinfo("tcp conn unexpected close: %s", uv_strerror(nread)); + _output_dnssim_conn_close(conn); + } + + if (buf->base != NULL) + free(buf->base); +} + +static void _on_tcp_connected(uv_connect_t* conn_req, int status) +{ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)conn_req->handle->data; + mlassert(conn, "conn is nil"); + + free(conn_req); + + if (status < 0) { + mldebug("tcp connect failed: %s", uv_strerror(status)); + _output_dnssim_conn_close(conn); + return; + } + + mlassert(conn->state == _OUTPUT_DNSSIM_CONN_TCP_HANDSHAKE, "connection state != TCP_HANDSHAKE"); + int ret = uv_read_start((uv_stream_t*)conn->handle, _output_dnssim_on_uv_alloc, _on_tcp_read); + if (ret < 0) { + mlwarning("tcp uv_read_start() failed: %s", uv_strerror(ret)); + _output_dnssim_conn_close(conn); + return; + } + + mldebug("tcp connected"); + mlassert(conn->client, "conn must be associated with a client"); + mlassert(conn->client->dnssim, "client must be associated with dnssim"); + output_dnssim_t* self = conn->client->dnssim; + switch (_self->transport) { + case OUTPUT_DNSSIM_TRANSPORT_TCP: + _output_dnssim_conn_activate(conn); + break; + case OUTPUT_DNSSIM_TRANSPORT_TLS: + case OUTPUT_DNSSIM_TRANSPORT_HTTPS2: +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + mldebug("init tls handshake"); + _output_dnssim_tls_process_input_data(conn); /* Initiate TLS handshake. */ +#else + mlfatal(DNSSIM_MIN_GNUTLS_ERRORMSG); +#endif + break; + default: + lfatal("unsupported transport protocol"); + break; + } +} + +static void _on_connection_timeout(uv_timer_t* handle) +{ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)handle->data; + _output_dnssim_conn_close(conn); +} + +int _output_dnssim_tcp_connect(output_dnssim_t* self, _output_dnssim_connection_t* conn) +{ + mlassert_self(); + lassert(conn, "connection can't be null"); + lassert(conn->handle == NULL, "connection already has a handle"); + lassert(conn->handshake_timer == NULL, "connection already has a handshake timer"); + lassert(conn->idle_timer == NULL, "connection already has idle timer"); + lassert(conn->state == _OUTPUT_DNSSIM_CONN_INITIALIZED, "connection state != INITIALIZED"); + + lfatal_oom(conn->handle = malloc(sizeof(uv_tcp_t))); + conn->handle->data = (void*)conn; + int ret = uv_tcp_init(&_self->loop, conn->handle); + if (ret < 0) { + lwarning("failed to init uv_tcp_t"); + goto failure; + } + + ret = _output_dnssim_bind_before_connect(self, (uv_handle_t*)conn->handle); + if (ret < 0) + goto failure; + + /* Set connection parameters. */ + ret = uv_tcp_nodelay(conn->handle, 1); + if (ret < 0) + lwarning("tcp: failed to set TCP_NODELAY: %s", uv_strerror(ret)); + + /* Set connection handshake timeout. */ + lfatal_oom(conn->handshake_timer = malloc(sizeof(uv_timer_t))); + uv_timer_init(&_self->loop, conn->handshake_timer); + conn->handshake_timer->data = (void*)conn; + uv_timer_start(conn->handshake_timer, _on_connection_timeout, self->handshake_timeout_ms, 0); + + /* Set idle connection timer. */ + if (self->idle_timeout_ms > 0) { + lfatal_oom(conn->idle_timer = malloc(sizeof(uv_timer_t))); + uv_timer_init(&_self->loop, conn->idle_timer); + conn->idle_timer->data = (void*)conn; + + /* Start and stop the timer to set the repeat value without running the timer. */ + uv_timer_start(conn->idle_timer, _on_connection_timeout, self->idle_timeout_ms, self->idle_timeout_ms); + uv_timer_stop(conn->idle_timer); + } + + mldebug("tcp connecting"); + uv_connect_t* conn_req; + lfatal_oom(conn_req = malloc(sizeof(uv_connect_t))); + ret = uv_tcp_connect(conn_req, conn->handle, (struct sockaddr*)&_self->target, _on_tcp_connected); + if (ret < 0) + goto failure; + + conn->stats->conn_handshakes++; + conn->client->dnssim->stats_sum->conn_handshakes++; + conn->state = _OUTPUT_DNSSIM_CONN_TCP_HANDSHAKE; + return 0; +failure: + _output_dnssim_conn_close(conn); + return ret; +} + +void _output_dnssim_tcp_close(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn can't be nil"); + + if (conn->handle != NULL) { + uv_read_stop((uv_stream_t*)conn->handle); + uv_close((uv_handle_t*)conn->handle, _on_tcp_closed); + } +} + +int _output_dnssim_create_query_tcp(output_dnssim_t* self, _output_dnssim_request_t* req) +{ + mlassert_self(); + lassert(req, "req is nil"); + lassert(req->client, "request must have a client associated with it"); + + _output_dnssim_query_tcp_t* qry; + + lfatal_oom(qry = calloc(1, sizeof(_output_dnssim_query_tcp_t))); + + qry->qry.transport = OUTPUT_DNSSIM_TRANSPORT_TCP; + qry->qry.req = req; + qry->qry.state = _OUTPUT_DNSSIM_QUERY_PENDING_WRITE; + req->qry = &qry->qry; // TODO change when adding support for multiple Qs for req + _ll_append(req->client->pending, &qry->qry); + + return _output_dnssim_handle_pending_queries(req->client); +} + +void _output_dnssim_close_query_tcp(_output_dnssim_query_tcp_t* qry) +{ + mlassert(qry, "qry can't be null"); + mlassert(qry->qry.req, "query must be part of a request"); + _output_dnssim_request_t* req = qry->qry.req; + mlassert(req->client, "request must belong to a client"); + + if ((qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_WRITE_CB || qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_CLOSE)) { + /* Query can't be freed until uv callback is called. */ + qry->qry.state = _OUTPUT_DNSSIM_QUERY_PENDING_CLOSE; + return; + } + + _ll_try_remove(req->client->pending, &qry->qry); + if (qry->conn) { + _output_dnssim_connection_t* conn = qry->conn; + _ll_try_remove(conn->queued, &qry->qry); /* edge-case of cancelled queries */ + _ll_try_remove(conn->sent, &qry->qry); + qry->conn = NULL; + _output_dnssim_conn_idle(conn); + } + + _ll_remove(req->qry, &qry->qry); + free(qry); +} diff --git a/src/output/dnssim/tls.c b/src/output/dnssim/tls.c new file mode 100644 index 0000000..e87ca47 --- /dev/null +++ b/src/output/dnssim/tls.c @@ -0,0 +1,475 @@ +/* + * Copyright (c) 2020, CZ.NIC, z.s.p.o. + * All rights reserved. + * + * This file is part of dnsjit. + * + * dnsjit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dnsjit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dnsjit. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include "output/dnssim.h" +#include "output/dnssim/internal.h" +#include "output/dnssim/ll.h" +#include "core/assert.h" + +#include <gnutls/gnutls.h> +#include <string.h> + +#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION + +#ifndef MIN +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) /** Minimum of two numbers **/ +#endif + +static core_log_t _log = LOG_T_INIT("output.dnssim"); + +struct async_write_ctx { + uv_write_t write_req; + _output_dnssim_connection_t* conn; + char buf[]; +}; + +static int _tls_handshake(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn is nil"); + mlassert(conn->tls, "conn must have tls context"); + mlassert(conn->client, "conn must belong to a client"); + mlassert(conn->state <= _OUTPUT_DNSSIM_CONN_TLS_HANDSHAKE, "conn in invalid state"); + + /* Set TLS session resumption ticket if available. */ + if (conn->state < _OUTPUT_DNSSIM_CONN_TLS_HANDSHAKE && conn->client->tls_ticket.size != 0) { + gnutls_datum_t* ticket = &conn->client->tls_ticket; + gnutls_session_set_data(conn->tls->session, ticket->data, ticket->size); + } + conn->state = _OUTPUT_DNSSIM_CONN_TLS_HANDSHAKE; + + return gnutls_handshake(conn->tls->session); +} + +void _output_dnssim_tls_process_input_data(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn is nil"); + mlassert(conn->client, "conn must have client"); + mlassert(conn->client->dnssim, "client must have dnssim"); + mlassert(conn->tls, "conn must have tls ctx"); + + if (conn->state >= _OUTPUT_DNSSIM_CONN_CLOSING) + return; + + output_dnssim_t* self = conn->client->dnssim; + + /* Ensure TLS handshake is performed before receiving data. + * See https://www.gnutls.org/manual/html_node/TLS-handshake.html */ + while (conn->state <= _OUTPUT_DNSSIM_CONN_TLS_HANDSHAKE) { + int err = _tls_handshake(conn); + mldebug("tls handshake returned: %s", gnutls_strerror(err)); + if (err == GNUTLS_E_SUCCESS) { + if (gnutls_session_is_resumed(conn->tls->session)) + conn->stats->conn_resumed++; + if (_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2) { + if (_output_dnssim_https2_setup(conn) < 0) { + _output_dnssim_conn_close(conn); + return; + } + } + _output_dnssim_conn_activate(conn); + break; + } else if (err == GNUTLS_E_AGAIN) { + return; /* Wait for more data */ + } else if (err == GNUTLS_E_FATAL_ALERT_RECEIVED) { + gnutls_alert_description_t alert = gnutls_alert_get(conn->tls->session); + mlwarning("gnutls_handshake failed: %s", gnutls_alert_get_name(alert)); + _output_dnssim_conn_close(conn); + return; + } else if (gnutls_error_is_fatal(err)) { + mlwarning("gnutls_handshake failed: %s", gnutls_strerror_name(err)); + _output_dnssim_conn_close(conn); + return; + } + } + + /* See https://gnutls.org/manual/html_node/Data-transfer-and-termination.html#Data-transfer-and-termination */ + while (true) { + /* Connection might have been closed due to an error, don't try to use it. */ + if (conn->state < _OUTPUT_DNSSIM_CONN_ACTIVE || conn->state >= _OUTPUT_DNSSIM_CONN_CLOSING) + return; + + ssize_t count = gnutls_record_recv(conn->tls->session, _self->wire_buf, WIRE_BUF_SIZE); + if (count > 0) { + switch (_self->transport) { + case OUTPUT_DNSSIM_TRANSPORT_TLS: + _output_dnssim_read_dns_stream(conn, count, _self->wire_buf); + break; + case OUTPUT_DNSSIM_TRANSPORT_HTTPS2: + _output_dnssim_https2_process_input_data(conn, count, _self->wire_buf); + break; + default: + lfatal("unsupported transport layer"); + break; + } + } else if (count == GNUTLS_E_AGAIN) { + if (conn->tls->buf_pos == conn->tls->buf_len) { + /* See https://www.gnutls.org/manual/html_node/Asynchronous-operation.html */ + break; /* No more data available in this libuv buffer */ + } + continue; + } else if (count == GNUTLS_E_INTERRUPTED) { + continue; + } else if (count == GNUTLS_E_REHANDSHAKE) { + continue; /* Ignore rehandshake request. */ + } else if (count < 0) { + mlwarning("gnutls_record_recv failed: %s", gnutls_strerror_name(count)); + _output_dnssim_conn_close(conn); + return; + } else if (count == 0) { + break; + } + } + mlassert(conn->tls->buf_len == conn->tls->buf_pos, "tls didn't read the entire buffer"); +} + +static ssize_t _tls_pull(gnutls_transport_ptr_t ptr, void* buf, size_t len) +{ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)ptr; + mlassert(conn != NULL, "conn is null"); + mlassert(conn->tls != NULL, "conn must have tls ctx"); + + ssize_t avail = conn->tls->buf_len - conn->tls->buf_pos; + if (avail <= 0) { + mldebug("tls pull: no more data"); + errno = EAGAIN; + return -1; + } + + ssize_t transfer = MIN(avail, len); + memcpy(buf, conn->tls->buf + conn->tls->buf_pos, transfer); + conn->tls->buf_pos += transfer; + return transfer; +} + +static void _tls_on_write_complete(uv_write_t* req, int status) +{ + mlassert(req->data != NULL, "uv_write req has no data pointer"); + struct async_write_ctx* async_ctx = (struct async_write_ctx*)req->data; + _output_dnssim_connection_t* conn = async_ctx->conn; + mlassert(conn, "conn is nil"); + mlassert(conn->tls, "conn must have tls ctx"); + mlassert(conn->tls->write_queue_size > 0, "invalid write_queue_size: %d", conn->tls->write_queue_size); + conn->tls->write_queue_size -= 1; + free(req->data); + + if (status < 0) + _output_dnssim_conn_close(conn); +} + +static ssize_t _tls_vec_push(gnutls_transport_ptr_t ptr, const giovec_t* iov, int iovcnt) +{ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)ptr; + mlassert(conn != NULL, "conn is null"); + mlassert(conn->tls != NULL, "conn must have tls ctx"); + + if (iovcnt == 0) + return 0; + + /* + * This is a little bit complicated. There are two different writes: + * 1. Immediate, these don't need to own the buffered data and return immediately + * 2. Asynchronous, these need to own the buffers until the write completes + * In order to avoid copying the buffer, an immediate write is tried first if possible. + * If it isn't possible to write the data without queueing, an asynchronous write + * is created (with copied buffered data). + */ + + size_t total_len = 0; + uv_buf_t uv_buf[iovcnt]; + int i; + for (i = 0; i < iovcnt; ++i) { + uv_buf[i].base = iov[i].iov_base; + uv_buf[i].len = iov[i].iov_len; + total_len += iov[i].iov_len; + } + + /* Try to perform the immediate write first to avoid copy */ + int ret = 0; + if (conn->tls->write_queue_size == 0) { + ret = uv_try_write((uv_stream_t*)conn->handle, uv_buf, iovcnt); + /* from libuv documentation - + uv_try_write will return either: + > 0: number of bytes written (can be less than the supplied buffer size). + < 0: negative error code (UV_EAGAIN is returned if no data can be sent immediately). + */ + if (ret == total_len) { + /* All the data were buffered by libuv. + * Return. */ + return ret; + } + + if (ret < 0 && ret != UV_EAGAIN) { + /* uv_try_write() has returned error code other then UV_EAGAIN. + * Return. */ + errno = EIO; + return -1; + } + /* Since we are here expression below is true + * (ret != total_len) && (ret >= 0 || ret == UV_EAGAIN) + * or the same + * (ret != total_len && ret >= 0) || (ret != total_len && ret == UV_EAGAIN) + * i.e. either occurs partial write or UV_EAGAIN. + * Proceed and copy data amount to owned memory and perform async write. + */ + if (ret == UV_EAGAIN) { + /* No data were buffered, so we must buffer all the data. */ + ret = 0; + } + } + + /* Fallback when the queue is full, and it's not possible to do an immediate write */ + char* p = malloc(sizeof(struct async_write_ctx) + total_len - ret); + if (p != NULL) { + struct async_write_ctx* async_ctx = (struct async_write_ctx*)p; + async_ctx->conn = conn; + char* buf = async_ctx->buf; + /* Skip data written in the partial write */ + size_t to_skip = ret; + /* Copy the buffer into owned memory */ + size_t off = 0; + int i; + for (i = 0; i < iovcnt; ++i) { + if (to_skip > 0) { + /* Ignore current buffer if it's all skipped */ + if (to_skip >= uv_buf[i].len) { + to_skip -= uv_buf[i].len; + continue; + } + /* Skip only part of the buffer */ + uv_buf[i].base += to_skip; + uv_buf[i].len -= to_skip; + to_skip = 0; + } + memcpy(buf + off, uv_buf[i].base, uv_buf[i].len); + off += uv_buf[i].len; + } + uv_buf[0].base = buf; + uv_buf[0].len = off; + + /* Create an asynchronous write request */ + uv_write_t* write_req = &async_ctx->write_req; + memset(write_req, 0, sizeof(uv_write_t)); + write_req->data = p; + + /* Perform an asynchronous write with a callback */ + if (uv_write(write_req, (uv_stream_t*)conn->handle, uv_buf, 1, _tls_on_write_complete) == 0) { + ret = total_len; + conn->tls->write_queue_size += 1; + } else { + free(p); + errno = EIO; + ret = -1; + } + } else { + errno = ENOMEM; + ret = -1; + } + + return ret; +} + +int _tls_pull_timeout(gnutls_transport_ptr_t ptr, unsigned int ms) +{ + _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)ptr; + mlassert(conn != NULL, "conn is null"); + mlassert(conn->tls != NULL, "conn must have tls ctx"); + + ssize_t avail = conn->tls->buf_len - conn->tls->buf_pos; + if (avail <= 0) { + errno = EAGAIN; + return -1; + } + return avail; +} + +int _output_dnssim_tls_init(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn is nil"); + mlassert(conn->tls == NULL, "conn already has tls context"); + + int ret; + mlfatal_oom(conn->tls = malloc(sizeof(_output_dnssim_tls_ctx_t))); + conn->tls->buf = NULL; + conn->tls->buf_len = 0; + conn->tls->buf_pos = 0; + conn->tls->write_queue_size = 0; + + ret = gnutls_init(&conn->tls->session, GNUTLS_CLIENT | GNUTLS_NONBLOCK); + if (ret < 0) { + mldebug("failed gnutls_init() (%s)", gnutls_strerror(ret)); + free(conn->tls); + conn->tls = 0; + return ret; + } + + output_dnssim_t* self = conn->client->dnssim; + if (_self->tls_priority == NULL) { + ret = gnutls_set_default_priority(conn->tls->session); + if (ret < 0) { + mldebug("failed gnutls_set_default_priority() (%s)", gnutls_strerror(ret)); + gnutls_deinit(conn->tls->session); + free(conn->tls); + conn->tls = 0; + return ret; + } + } else { + ret = gnutls_priority_set(conn->tls->session, *_self->tls_priority); + if (ret < 0) { + mldebug("failed gnutls_priority_set() (%s)", gnutls_strerror(ret)); + gnutls_deinit(conn->tls->session); + free(conn->tls); + conn->tls = 0; + return ret; + } + } + + ret = gnutls_credentials_set(conn->tls->session, GNUTLS_CRD_CERTIFICATE, _self->tls_cred); + if (ret < 0) { + mldebug("failed gnutls_credentials_set() (%s)", gnutls_strerror(ret)); + gnutls_deinit(conn->tls->session); + free(conn->tls); + conn->tls = 0; + return ret; + } + + gnutls_transport_set_pull_function(conn->tls->session, _tls_pull); + gnutls_transport_set_pull_timeout_function(conn->tls->session, _tls_pull_timeout); + gnutls_transport_set_vec_push_function(conn->tls->session, _tls_vec_push); + gnutls_transport_set_ptr(conn->tls->session, conn); + + return 0; +} + +int _output_dnssim_create_query_tls(output_dnssim_t* self, _output_dnssim_request_t* req) +{ + mlassert_self(); + lassert(req, "req is nil"); + lassert(req->client, "request must have a client associated with it"); + + _output_dnssim_query_tcp_t* qry; + + lfatal_oom(qry = calloc(1, sizeof(_output_dnssim_query_tcp_t))); + + qry->qry.transport = OUTPUT_DNSSIM_TRANSPORT_TLS; + qry->qry.req = req; + qry->qry.state = _OUTPUT_DNSSIM_QUERY_PENDING_WRITE; + req->qry = &qry->qry; // TODO change when adding support for multiple Qs for req + _ll_append(req->client->pending, &qry->qry); + + return _output_dnssim_handle_pending_queries(req->client); +} + +void _output_dnssim_close_query_tls(_output_dnssim_query_tcp_t* qry) +{ + mlassert(qry, "qry can't be null"); + mlassert(qry->qry.req, "query must be part of a request"); + _output_dnssim_request_t* req = qry->qry.req; + mlassert(req->client, "request must belong to a client"); + + _ll_try_remove(req->client->pending, &qry->qry); + if (qry->conn) { + _output_dnssim_connection_t* conn = qry->conn; + _ll_try_remove(conn->sent, &qry->qry); + qry->conn = NULL; + _output_dnssim_conn_idle(conn); + } + + _ll_remove(req->qry, &qry->qry); + free(qry); +} + +void _output_dnssim_tls_close(_output_dnssim_connection_t* conn) +{ + mlassert(conn, "conn can't be nil"); + mlassert(conn->tls, "conn must have tls ctx"); + mlassert(conn->client, "conn must belong to a client"); + + /* Try and get a TLS session ticket for potential resumption. */ + int ret; + if (gnutls_session_get_flags(conn->tls->session) & GNUTLS_SFLAGS_SESSION_TICKET) { + if (conn->client->tls_ticket.size != 0) { + gnutls_free(conn->client->tls_ticket.data); + } + ret = gnutls_session_get_data2(conn->tls->session, &conn->client->tls_ticket); + if (ret < 0) { + mldebug("gnutls_session_get_data2 failed: %s", gnutls_strerror(ret)); + conn->client->tls_ticket.size = 0; + } + } + + gnutls_deinit(conn->tls->session); + _output_dnssim_tcp_close(conn); +} + +void _output_dnssim_tls_write_query(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry) +{ + mlassert(qry, "qry can't be null"); + mlassert(qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_WRITE, "qry must be pending write"); + mlassert(qry->qry.req, "req can't be null"); + mlassert(qry->qry.req->dns_q, "dns_q can't be null"); + mlassert(qry->qry.req->dns_q->obj_prev, "payload can't be null"); + mlassert(conn, "conn can't be null"); + mlassert(conn->state == _OUTPUT_DNSSIM_CONN_ACTIVE, "connection state != ACTIVE"); + mlassert(conn->tls, "conn must have tls ctx"); + mlassert(conn->client, "conn must be associated with client"); + mlassert(conn->client->pending, "conn has no pending queries"); + + core_object_payload_t* payload = (core_object_payload_t*)qry->qry.req->dns_q->obj_prev; + uint16_t len = htons(payload->len); + + gnutls_record_cork(conn->tls->session); + ssize_t count = 0; + if ((count = gnutls_record_send(conn->tls->session, &len, sizeof(len)) < 0) || (count = gnutls_record_send(conn->tls->session, payload->payload, payload->len) < 0)) { + mlwarning("gnutls_record_send failed: %s", gnutls_strerror_name(count)); + _output_dnssim_conn_close(conn); + return; + } + + const ssize_t submitted = sizeof(len) + payload->len; + + int ret = gnutls_record_uncork(conn->tls->session, GNUTLS_RECORD_WAIT); + if (gnutls_error_is_fatal(ret)) { + mlinfo("gnutls_record_uncorck failed: %s", gnutls_strerror_name(ret)); + _output_dnssim_conn_close(conn); + return; + } + + if (ret != submitted) { + mlwarning("gnutls_record_uncork didn't send all data"); + _output_dnssim_conn_close(conn); + return; + } + + qry->conn = conn; + _ll_remove(conn->client->pending, &qry->qry); + _ll_append(conn->sent, &qry->qry); + + /* Stop idle timer, since there are queries to answer now. */ + if (conn->idle_timer != NULL) { + conn->is_idle = false; + uv_timer_stop(conn->idle_timer); + } + + qry->qry.state = _OUTPUT_DNSSIM_QUERY_SENT; +} + +#endif diff --git a/src/output/dnssim/udp.c b/src/output/dnssim/udp.c new file mode 100644 index 0000000..74f8569 --- /dev/null +++ b/src/output/dnssim/udp.c @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2019-2020, CZ.NIC, z.s.p.o. + * All rights reserved. + * + * This file is part of dnsjit. + * + * dnsjit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dnsjit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dnsjit. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include "output/dnssim.h" +#include "output/dnssim/internal.h" +#include "output/dnssim/ll.h" +#include "core/assert.h" + +static core_log_t _log = LOG_T_INIT("output.dnssim"); + +static int _process_udp_response(uv_udp_t* handle, ssize_t nread, const uv_buf_t* buf) +{ + _output_dnssim_query_udp_t* qry = (_output_dnssim_query_udp_t*)handle->data; + _output_dnssim_request_t* req; + core_object_payload_t payload = CORE_OBJECT_PAYLOAD_INIT(NULL); + core_object_dns_t dns_a = CORE_OBJECT_DNS_INIT(&payload); + mlassert(qry, "qry is nil"); + mlassert(qry->qry.req, "query must be part of a request"); + req = qry->qry.req; + + payload.payload = (uint8_t*)buf->base; + payload.len = nread; + + dns_a.obj_prev = (core_object_t*)&payload; + int ret = core_object_dns_parse_header(&dns_a); + if (ret != 0) { + mldebug("udp response malformed"); + return _ERR_MALFORMED; + } + if (dns_a.id != req->dns_q->id) { + mldebug("udp response msgid mismatch %x(q) != %x(a)", req->dns_q->id, dns_a.id); + return _ERR_MSGID; + } + if (dns_a.tc == 1) { + mldebug("udp response has TC=1"); + return _ERR_TC; + } + ret = _output_dnssim_answers_request(req, &dns_a); + if (ret != 0) { + mlwarning("udp reponse question mismatch"); + return _ERR_QUESTION; + } + + _output_dnssim_request_answered(req, &dns_a); + return 0; +} + +static void _on_udp_query_recv(uv_udp_t* handle, ssize_t nread, const uv_buf_t* buf, const struct sockaddr* addr, unsigned flags) +{ + if (nread > 0) { + mldebug("udp recv: %d", nread); + + // TODO handle TC=1 + _process_udp_response(handle, nread, buf); + } + + if (buf->base != NULL) { + free(buf->base); + } +} + +static void _on_query_udp_closed(uv_handle_t* handle) +{ + _output_dnssim_query_udp_t* qry = (_output_dnssim_query_udp_t*)handle->data; + _output_dnssim_request_t* req; + mlassert(qry, "qry is nil"); + mlassert(qry->qry.req, "query must be part of a request"); + req = qry->qry.req; + + free(qry->handle); + + _ll_remove(req->qry, &qry->qry); + free(qry); + + if (req->qry == NULL) + _output_dnssim_maybe_free_request(req); +} + +void _output_dnssim_close_query_udp(_output_dnssim_query_udp_t* qry) +{ + int ret; + mlassert(qry, "qry is nil"); + + ret = uv_udp_recv_stop(qry->handle); + if (ret < 0) { + mldebug("failed uv_udp_recv_stop(): %s", uv_strerror(ret)); + } + + uv_close((uv_handle_t*)qry->handle, _on_query_udp_closed); +} + +int _output_dnssim_create_query_udp(output_dnssim_t* self, _output_dnssim_request_t* req) +{ + int ret; + _output_dnssim_query_udp_t* qry; + core_object_payload_t* payload; + mlassert_self(); + lassert(req, "req is nil"); + payload = (core_object_payload_t*)req->dns_q->obj_prev; + + lfatal_oom(qry = calloc(1, sizeof(_output_dnssim_query_udp_t))); + lfatal_oom(qry->handle = malloc(sizeof(uv_udp_t))); + + qry->qry.transport = OUTPUT_DNSSIM_TRANSPORT_UDP; + qry->qry.req = req; + qry->buf = uv_buf_init((char*)payload->payload, payload->len); + qry->handle->data = (void*)qry; + ret = uv_udp_init(&_self->loop, qry->handle); + if (ret < 0) { + lwarning("failed to init uv_udp_t"); + goto failure; + } + _ll_append(req->qry, &qry->qry); + + ret = _output_dnssim_bind_before_connect(self, (uv_handle_t*)qry->handle); + if (ret < 0) + return ret; + + ret = uv_udp_try_send(qry->handle, &qry->buf, 1, (struct sockaddr*)&_self->target); + if (ret < 0) { + lwarning("failed to send udp packet: %s", uv_strerror(ret)); + return ret; + } + + // listen for reply + ret = uv_udp_recv_start(qry->handle, _output_dnssim_on_uv_alloc, _on_udp_query_recv); + if (ret < 0) { + lwarning("failed uv_udp_recv_start(): %s", uv_strerror(ret)); + return ret; + } + + return 0; +failure: + free(qry->handle); + free(qry); + return ret; +} |