summaryrefslogtreecommitdiffstats
path: root/src/output/dnssim
diff options
context:
space:
mode:
Diffstat (limited to 'src/output/dnssim')
-rw-r--r--src/output/dnssim/CHANGELOG.md16
-rw-r--r--src/output/dnssim/common.c384
-rw-r--r--src/output/dnssim/connection.c471
-rw-r--r--src/output/dnssim/https2.c592
-rw-r--r--src/output/dnssim/internal.h343
-rw-r--r--src/output/dnssim/ll.h83
-rw-r--r--src/output/dnssim/tcp.c356
-rw-r--r--src/output/dnssim/tls.c475
-rw-r--r--src/output/dnssim/udp.c156
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;
+}