diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2021-03-13 07:54:12 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2021-03-13 07:54:12 +0000 |
commit | 4754ed45b607e82450a5e31fea1da3ba61433b04 (patch) | |
tree | 3554490bdc003e6004f605abe41929cdf98b0651 /src/output/dnssim | |
parent | Initial commit. (diff) | |
download | dnsjit-4754ed45b607e82450a5e31fea1da3ba61433b04.tar.xz dnsjit-4754ed45b607e82450a5e31fea1da3ba61433b04.zip |
Adding upstream version 1.1.0+debian.upstream/1.1.0+debian
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/output/dnssim')
-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 |
9 files changed, 2876 insertions, 0 deletions
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; +} |