summaryrefslogtreecommitdiffstats
path: root/src/output
diff options
context:
space:
mode:
Diffstat (limited to 'src/output')
-rw-r--r--src/output/dnscli.c888
-rw-r--r--src/output/dnscli.h38
-rw-r--r--src/output/dnscli.hh72
-rw-r--r--src/output/dnscli.lua187
-rw-r--r--src/output/dnssim.c502
-rw-r--r--src/output/dnssim.h31
-rw-r--r--src/output/dnssim.hh123
-rw-r--r--src/output/dnssim.lua433
-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
-rw-r--r--src/output/null.c87
-rw-r--r--src/output/null.h33
-rw-r--r--src/output/null.hh37
-rw-r--r--src/output/null.lua80
-rw-r--r--src/output/pcap.c111
-rw-r--r--src/output/pcap.h32
-rw-r--r--src/output/pcap.hh41
-rw-r--r--src/output/pcap.lua79
-rw-r--r--src/output/respdiff.c298
-rw-r--r--src/output/respdiff.h31
-rw-r--r--src/output/respdiff.hh36
-rw-r--r--src/output/respdiff.lua94
-rw-r--r--src/output/tcpcli.c381
-rw-r--r--src/output/tcpcli.h32
-rw-r--r--src/output/tcpcli.hh51
-rw-r--r--src/output/tcpcli.lua131
-rw-r--r--src/output/tlscli.c345
-rw-r--r--src/output/tlscli.h34
-rw-r--r--src/output/tlscli.hh52
-rw-r--r--src/output/tlscli.lua103
-rw-r--r--src/output/udpcli.c300
-rw-r--r--src/output/udpcli.h35
-rw-r--r--src/output/udpcli.hh53
-rw-r--r--src/output/udpcli.lua121
41 files changed, 7747 insertions, 0 deletions
diff --git a/src/output/dnscli.c b/src/output/dnscli.c
new file mode 100644
index 0000000..f7c5b5e
--- /dev/null
+++ b/src/output/dnscli.c
@@ -0,0 +1,888 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * 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/dnscli.h"
+#include "core/assert.h"
+#include "core/object/dns.h"
+#include "core/object/payload.h"
+#include "core/object/udp.h"
+#include "core/object/tcp.h"
+
+#include <netdb.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <string.h>
+#ifdef HAVE_ENDIAN_H
+#include <endian.h>
+#else
+#ifdef HAVE_SYS_ENDIAN_H
+#include <sys/endian.h>
+#else
+#ifdef HAVE_MACHINE_ENDIAN_H
+#include <machine/endian.h>
+#endif
+#endif
+#endif
+#ifdef HAVE_BYTESWAP_H
+#include <byteswap.h>
+#endif
+#ifndef bswap_16
+#ifndef bswap16
+#define bswap_16(x) swap16(x)
+#define bswap_32(x) swap32(x)
+#define bswap_64(x) swap64(x)
+#else
+#define bswap_16(x) bswap16(x)
+#define bswap_32(x) bswap32(x)
+#define bswap_64(x) bswap64(x)
+#endif
+#endif
+
+static inline uint16_t _need16(const void* ptr)
+{
+ uint16_t v;
+ memcpy(&v, ptr, sizeof(v));
+ return be16toh(v);
+}
+
+static core_log_t _log = LOG_T_INIT("output.dnscli");
+static output_dnscli_t _defaults = {
+ LOG_T_INIT_OBJ("output.dnscli"),
+ OUTPUT_DNSCLI_MODE_NONE,
+ 0, 0, 0, -1, 0, 0,
+ { 0, 0, 0 }, 0,
+ { 0 }, 0,
+ { 0 }, CORE_OBJECT_PAYLOAD_INIT(0), 0, 0, 0, 0, 0,
+ { 0, 0 },
+ 0, 0
+};
+
+core_log_t* output_dnscli_log()
+{
+ return &_log;
+}
+
+void output_dnscli_init(output_dnscli_t* self, output_dnscli_mode_t mode)
+{
+ mlassert_self();
+
+ *self = _defaults;
+ self->mode = mode;
+ self->pkt.payload = self->recvbuf;
+
+ switch (mode & OUTPUT_DNSCLI_MODE_MODES) {
+ case OUTPUT_DNSCLI_MODE_UDP:
+ case OUTPUT_DNSCLI_MODE_TCP:
+ break;
+ case OUTPUT_DNSCLI_MODE_TLS: {
+ int err;
+ if ((err = gnutls_certificate_allocate_credentials(&self->cred)) != GNUTLS_E_SUCCESS) {
+ lfatal("gnutls_certificate_allocate_credentials() error: %s", gnutls_strerror(err));
+ } else if ((err = gnutls_init(&self->session, GNUTLS_CLIENT | ((mode & OUTPUT_DNSCLI_MODE_NONBLOCKING) ? GNUTLS_NONBLOCK : 0))) != GNUTLS_E_SUCCESS) {
+ lfatal("gnutls_init() error: %s", gnutls_strerror(err));
+ } else if ((err = gnutls_set_default_priority(self->session)) != GNUTLS_E_SUCCESS) {
+ lfatal("gnutls_set_default_priority() error: %s", gnutls_strerror(err));
+ } else if ((err = gnutls_credentials_set(self->session, GNUTLS_CRD_CERTIFICATE, self->cred)) != GNUTLS_E_SUCCESS) {
+ lfatal("gnutls_credentials_set() error: %s", gnutls_strerror(err));
+ }
+
+ gnutls_handshake_set_timeout(self->session, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);
+ break;
+ }
+ default:
+ lfatal("Invalid mode %x", mode);
+ }
+}
+
+void output_dnscli_destroy(output_dnscli_t* self)
+{
+ mlassert_self();
+
+ if (self->fd > -1) {
+ switch (self->mode & OUTPUT_DNSCLI_MODE_MODES) {
+ case OUTPUT_DNSCLI_MODE_UDP:
+ case OUTPUT_DNSCLI_MODE_TCP:
+ shutdown(self->fd, SHUT_RDWR);
+ close(self->fd);
+ break;
+ case OUTPUT_DNSCLI_MODE_TLS:
+ if (self->session) {
+ gnutls_bye(self->session, GNUTLS_SHUT_RDWR);
+ gnutls_deinit(self->session);
+ }
+ shutdown(self->fd, SHUT_RDWR);
+ close(self->fd);
+ if (self->cred) {
+ gnutls_certificate_free_credentials(self->cred);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+int output_dnscli_connect(output_dnscli_t* self, const char* host, const char* port)
+{
+ struct addrinfo* addr;
+ int err;
+ mlassert_self();
+ lassert(host, "host is nil");
+ lassert(port, "port is nil");
+
+ if (self->fd > -1) {
+ lfatal("already connected");
+ }
+
+ if ((err = getaddrinfo(host, port, 0, &addr))) {
+ lcritical("getaddrinfo(%s, %s) error %s", host, port, gai_strerror(err));
+ return -1;
+ }
+ if (!addr) {
+ lcritical("getaddrinfo failed, no address returned");
+ return -1;
+ }
+
+ switch (self->mode & OUTPUT_DNSCLI_MODE_MODES) {
+ case OUTPUT_DNSCLI_MODE_UDP:
+ memcpy(&self->addr, addr->ai_addr, addr->ai_addrlen);
+ self->addr_len = addr->ai_addrlen;
+ freeaddrinfo(addr);
+
+ if ((self->fd = socket(((struct sockaddr*)&self->addr)->sa_family, SOCK_DGRAM, 0)) < 0) {
+ lcritical("socket() error %s", core_log_errstr(errno));
+ return -2;
+ }
+ break;
+ case OUTPUT_DNSCLI_MODE_TCP:
+ case OUTPUT_DNSCLI_MODE_TLS:
+ if ((self->fd = socket(addr->ai_addr->sa_family, SOCK_STREAM, 0)) < 0) {
+ lcritical("socket() error %s", core_log_errstr(errno));
+ freeaddrinfo(addr);
+ return -2;
+ }
+
+ if (connect(self->fd, addr->ai_addr, addr->ai_addrlen)) {
+ lcritical("connect() error %s", core_log_errstr(errno));
+ freeaddrinfo(addr);
+ return -2;
+ }
+
+ freeaddrinfo(addr);
+ break;
+ default:
+ break;
+ }
+
+ switch (self->mode & OUTPUT_DNSCLI_MODE_MODES) {
+ case OUTPUT_DNSCLI_MODE_UDP:
+ case OUTPUT_DNSCLI_MODE_TCP:
+ if (self->mode & OUTPUT_DNSCLI_MODE_NONBLOCKING) {
+ int flags;
+
+ if ((flags = fcntl(self->fd, F_GETFL)) == -1) {
+ lcritical("fcntl(FL_GETFL) error %s", core_log_errstr(errno));
+ return -3;
+ }
+
+ if (fcntl(self->fd, F_SETFL, flags | O_NONBLOCK)) {
+ lcritical("fcntl(FL_SETFL, %x) error %s", flags, core_log_errstr(errno));
+ return -3;
+ }
+ self->nonblocking = 1;
+ }
+ if (self->timeout.sec > 0 || self->timeout.nsec > 0) {
+ self->poll.fd = self->fd;
+ self->poll_timeout = (self->timeout.sec * 1e3) + (self->timeout.nsec / 1e6); //NOSONAR
+ if (!self->poll_timeout) {
+ self->poll_timeout = 1;
+ }
+ }
+ break;
+ case OUTPUT_DNSCLI_MODE_TLS: {
+ unsigned int ms;
+ gnutls_transport_set_int(self->session, self->fd);
+ ms = (self->timeout.sec * 1000) + (self->timeout.nsec / 1000000);
+ if (!ms && self->timeout.nsec) {
+ ms = 1;
+ }
+ gnutls_record_set_timeout(self->session, ms);
+
+ /* Establish TLS */
+ do {
+ err = gnutls_handshake(self->session);
+ } while (err < 0 && gnutls_error_is_fatal(err) == 0);
+ if (err == GNUTLS_E_PREMATURE_TERMINATION) {
+ lcritical("gnutls_handshake() error: %s", gnutls_strerror(err));
+ return -3;
+ } else if (err < 0) {
+ lcritical("gnutls_handshake() failed: %s (%d)\n", gnutls_strerror(err), err);
+ return -3;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ self->conn_ok = 1;
+ return 0;
+}
+
+inline ssize_t _send_udp(output_dnscli_t* self, const uint8_t* payload, size_t len, size_t sent)
+{
+ ssize_t n;
+
+ if (self->poll_timeout) {
+ self->poll.events = POLLOUT;
+ n = poll(&self->poll, 1, self->poll_timeout);
+ if (n != 1 || !(self->poll.revents & POLLOUT)) {
+ if (!n) {
+ self->timeouts++;
+ return -1;
+ }
+ self->errs++;
+ return -2;
+ }
+ }
+ n = sendto(self->fd, payload + sent, len - sent, 0, (struct sockaddr*)&self->addr, self->addr_len);
+ if (n > -1) {
+ return n;
+ }
+ switch (errno) {
+ case EAGAIN:
+#if EAGAIN != EWOULDBLOCK
+ case EWOULDBLOCK:
+#endif
+ case EINTR:
+ return -1;
+ default:
+ break;
+ }
+ return -2;
+}
+
+static void _receive_udp(output_dnscli_t* self, const core_object_t* obj)
+{
+ const uint8_t* payload;
+ size_t len, sent = 0;
+ ssize_t n;
+ mlassert_self();
+
+ switch (obj->obj_type) {
+ case CORE_OBJECT_DNS:
+ payload = ((core_object_dns_t*)obj)->payload;
+ len = ((core_object_dns_t*)obj)->len;
+
+ if (((core_object_dns_t*)obj)->includes_dnslen) {
+ if (len < 2) {
+ return;
+ }
+ payload += 2;
+ len -= 2;
+ }
+ break;
+ case CORE_OBJECT_PAYLOAD:
+ payload = ((core_object_payload_t*)obj)->payload;
+ len = ((core_object_payload_t*)obj)->len;
+ break;
+ default:
+ return;
+ }
+
+ for (;;) {
+ n = _send_udp(self, payload, len, sent);
+ if (n > -1) {
+ sent += n;
+ if (sent < len) {
+ continue;
+ }
+ self->pkts++;
+ return;
+ }
+ if (n == -1) {
+ if (self->nonblocking) {
+ // TODO: warn?
+ return;
+ }
+ continue;
+ }
+ break;
+ }
+ self->errs++;
+}
+
+inline ssize_t _send_tcp(output_dnscli_t* self, const uint8_t* payload, size_t len, size_t sent)
+{
+ ssize_t n;
+
+ if (self->poll_timeout) {
+ self->poll.events = POLLOUT;
+ n = poll(&self->poll, 1, self->poll_timeout);
+ if (n != 1 || !(self->poll.revents & POLLOUT)) {
+ if (!n) {
+ self->timeouts++;
+ return -1;
+ }
+ self->errs++;
+ return -2;
+ }
+ }
+ n = sendto(self->fd, payload + sent, len - sent, 0, 0, 0);
+ if (n > -1) {
+ return n;
+ }
+ switch (errno) {
+ case EAGAIN:
+#if EAGAIN != EWOULDBLOCK
+ case EWOULDBLOCK:
+#endif
+ case EINTR:
+ return -1;
+ default:
+ break;
+ }
+ return -2;
+}
+
+static void _receive_tcp(output_dnscli_t* self, const core_object_t* obj)
+{
+ const uint8_t* payload;
+ size_t len, sent = 0;
+ ssize_t n;
+ mlassert_self();
+
+ switch (obj->obj_type) {
+ case CORE_OBJECT_DNS:
+ if (!((core_object_dns_t*)obj)->includes_dnslen) {
+ uint16_t dnslen = htons(((core_object_dns_t*)obj)->len);
+ payload = (const uint8_t*)&dnslen;
+ len = sizeof(dnslen);
+
+ for (;;) {
+ n = _send_tcp(self, payload, len, sent);
+ if (n > -1) {
+ sent += n;
+ if (sent < len) {
+ continue;
+ }
+ break;
+ }
+ if (n == -1) {
+ if (self->nonblocking) {
+ // TODO: warn?
+ return;
+ }
+ continue;
+ }
+ self->errs++;
+ return;
+ }
+ sent = 0;
+ }
+ payload = ((core_object_dns_t*)obj)->payload;
+ len = ((core_object_dns_t*)obj)->len;
+ break;
+ case CORE_OBJECT_PAYLOAD:
+ payload = ((core_object_payload_t*)obj)->payload;
+ len = ((core_object_payload_t*)obj)->len;
+ break;
+ default:
+ return;
+ }
+
+ for (;;) {
+ n = _send_tcp(self, payload, len, sent);
+ if (n > -1) {
+ sent += n;
+ if (sent < len) {
+ continue;
+ }
+ self->pkts++;
+ return;
+ }
+ if (n == -1) {
+ if (self->nonblocking) {
+ // TODO: warn?
+ return;
+ }
+ continue;
+ }
+ break;
+ }
+ self->errs++;
+}
+
+inline ssize_t _send_tls(output_dnscli_t* self, const uint8_t* payload, size_t len, size_t sent)
+{
+ ssize_t n;
+
+ n = gnutls_record_send(self->session, payload + sent, len - sent);
+ if (n > -1) {
+ return n;
+ }
+ switch (n) {
+ case GNUTLS_E_AGAIN:
+ case GNUTLS_E_TIMEDOUT:
+ case GNUTLS_E_INTERRUPTED:
+ return -1;
+ default:
+ break;
+ }
+ return -2;
+}
+
+static void _receive_tls(output_dnscli_t* self, const core_object_t* obj)
+{
+ const uint8_t* payload;
+ size_t len, sent = 0;
+ ssize_t n;
+ mlassert_self();
+
+ switch (obj->obj_type) {
+ case CORE_OBJECT_DNS:
+ if (!((core_object_dns_t*)obj)->includes_dnslen) {
+ uint16_t dnslen = htons(((core_object_dns_t*)obj)->len);
+ payload = (const uint8_t*)&dnslen;
+ len = sizeof(dnslen);
+
+ for (;;) {
+ n = _send_tls(self, payload, len, sent);
+ if (n > -1) {
+ sent += n;
+ if (sent < len) {
+ continue;
+ }
+ break;
+ }
+ if (n == -1) {
+ if (self->nonblocking) {
+ // TODO: warn?
+ return;
+ }
+ continue;
+ }
+ self->errs++;
+ return;
+ }
+ sent = 0;
+ }
+ payload = ((core_object_dns_t*)obj)->payload;
+ len = ((core_object_dns_t*)obj)->len;
+ break;
+ case CORE_OBJECT_PAYLOAD:
+ payload = ((core_object_payload_t*)obj)->payload;
+ len = ((core_object_payload_t*)obj)->len;
+ break;
+ default:
+ return;
+ }
+
+ for (;;) {
+ n = _send_tls(self, payload, len, sent);
+ if (n > -1) {
+ sent += n;
+ if (sent < len) {
+ continue;
+ }
+ self->pkts++;
+ return;
+ }
+ if (n == -1) {
+ if (self->nonblocking) {
+ // TODO: warn?
+ return;
+ }
+ continue;
+ }
+ break;
+ }
+ self->errs++;
+}
+
+luajit_ssize_t output_dnscli_send(output_dnscli_t* self, const core_object_t* obj, size_t sent)
+{
+ const uint8_t* payload;
+ size_t len;
+ uint16_t dnslen;
+ mlassert_self();
+
+ switch (self->mode & OUTPUT_DNSCLI_MODE_MODES) {
+ case OUTPUT_DNSCLI_MODE_UDP:
+ switch (obj->obj_type) {
+ case CORE_OBJECT_DNS:
+ payload = ((core_object_dns_t*)obj)->payload;
+ len = ((core_object_dns_t*)obj)->len;
+
+ if (((core_object_dns_t*)obj)->includes_dnslen) {
+ if (len < 2) {
+ return -2;
+ }
+ payload += 2;
+ len -= 2;
+ }
+ break;
+ case CORE_OBJECT_PAYLOAD:
+ payload = ((core_object_payload_t*)obj)->payload;
+ len = ((core_object_payload_t*)obj)->len;
+ break;
+ default:
+ return -2;
+ }
+
+ return _send_udp(self, payload, len, sent);
+
+ case OUTPUT_DNSCLI_MODE_TCP:
+ switch (obj->obj_type) {
+ case CORE_OBJECT_DNS:
+ if (!((core_object_dns_t*)obj)->includes_dnslen) {
+ if (sent < sizeof(dnslen)) {
+ dnslen = htons(((core_object_dns_t*)obj)->len);
+ payload = (const uint8_t*)&dnslen;
+ len = sizeof(dnslen);
+
+ return _send_tcp(self, payload, len, sent);
+ }
+ sent -= sizeof(dnslen);
+ }
+ payload = ((core_object_dns_t*)obj)->payload;
+ len = ((core_object_dns_t*)obj)->len;
+ break;
+ case CORE_OBJECT_PAYLOAD:
+ payload = ((core_object_payload_t*)obj)->payload;
+ len = ((core_object_payload_t*)obj)->len;
+ break;
+ default:
+ return -2;
+ }
+
+ return _send_tcp(self, payload, len, sent);
+
+ case OUTPUT_DNSCLI_MODE_TLS:
+ switch (obj->obj_type) {
+ case CORE_OBJECT_DNS:
+ if (!((core_object_dns_t*)obj)->includes_dnslen) {
+ if (sent < sizeof(dnslen)) {
+ dnslen = htons(((core_object_dns_t*)obj)->len);
+ payload = (const uint8_t*)&dnslen;
+ len = sizeof(dnslen);
+
+ return _send_tls(self, payload, len, sent);
+ }
+ sent -= sizeof(dnslen);
+ }
+ payload = ((core_object_dns_t*)obj)->payload;
+ len = ((core_object_dns_t*)obj)->len;
+ break;
+ case CORE_OBJECT_PAYLOAD:
+ payload = ((core_object_payload_t*)obj)->payload;
+ len = ((core_object_payload_t*)obj)->len;
+ break;
+ default:
+ return -2;
+ }
+
+ return _send_tls(self, payload, len, sent);
+
+ default:
+ break;
+ }
+
+ return -2;
+}
+
+core_receiver_t output_dnscli_receiver(output_dnscli_t* self)
+{
+ mlassert_self();
+
+ if (!self->conn_ok) {
+ lfatal("not connected");
+ }
+
+ switch (self->mode & OUTPUT_DNSCLI_MODE_MODES) {
+ case OUTPUT_DNSCLI_MODE_UDP:
+ return (core_receiver_t)_receive_udp;
+ case OUTPUT_DNSCLI_MODE_TCP:
+ return (core_receiver_t)_receive_tcp;
+ case OUTPUT_DNSCLI_MODE_TLS:
+ return (core_receiver_t)_receive_tls;
+ default:
+ break;
+ }
+
+ lfatal("internal error");
+ return 0;
+}
+
+static const core_object_t* _produce_udp(output_dnscli_t* self)
+{
+ ssize_t n;
+ mlassert_self();
+
+ for (;;) {
+ if (self->poll_timeout) {
+ self->poll.events = POLLIN;
+ n = poll(&self->poll, 1, self->poll_timeout);
+ if (n != 1 || !(self->poll.revents & POLLIN)) {
+ if (!n) {
+ self->timeouts++;
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ } else {
+ self->errs++;
+ }
+ return 0;
+ }
+ }
+ n = recvfrom(self->fd, self->recvbuf, sizeof(self->recvbuf), 0, 0, 0);
+ if (n > -1) {
+ break;
+ }
+ switch (errno) {
+ case EAGAIN:
+#if EAGAIN != EWOULDBLOCK
+ case EWOULDBLOCK:
+#endif
+ case EINTR:
+ if (self->nonblocking) {
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ }
+ continue;
+ default:
+ break;
+ }
+ self->errs++;
+ break;
+ }
+
+ if (n < 1) {
+ return 0;
+ }
+
+ self->pkts_recv++;
+ self->pkt.len = n;
+ return (core_object_t*)&self->pkt;
+}
+
+static const core_object_t* _produce_tcp(output_dnscli_t* self)
+{
+ ssize_t n;
+ mlassert_self();
+
+ if (self->have_pkt) {
+ if (self->recv > self->dnslen + sizeof(self->dnslen)) {
+ self->recv -= self->dnslen + sizeof(self->dnslen);
+ memmove(self->recvbuf, self->recvbuf + self->dnslen + sizeof(self->dnslen), self->recv);
+ } else {
+ self->recv = 0;
+ }
+ self->have_pkt = 0;
+ self->have_dnslen = 0;
+ }
+
+ if (!self->have_dnslen && self->recv >= sizeof(self->dnslen)) {
+ self->dnslen = _need16(self->recvbuf);
+ self->have_dnslen = 1;
+ }
+ if (self->have_dnslen && self->recv >= self->dnslen + sizeof(self->dnslen)) {
+ self->pkts_recv++;
+ self->pkt.len = self->dnslen + sizeof(self->dnslen);
+ self->have_pkt = 1;
+ return (core_object_t*)&self->pkt;
+ }
+
+ for (;;) {
+ if (self->poll_timeout) {
+ self->poll.events = POLLIN;
+ n = poll(&self->poll, 1, self->poll_timeout);
+ if (n != 1 || !(self->poll.revents & POLLIN)) {
+ if (!n) {
+ self->timeouts++;
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ } else {
+ self->errs++;
+ }
+ return 0;
+ }
+ }
+ n = recvfrom(self->fd, self->recvbuf + self->recv, sizeof(self->recvbuf) - self->recv, 0, 0, 0);
+ if (n > 0) {
+ self->recv += n;
+
+ if (!self->have_dnslen && self->recv >= sizeof(self->dnslen)) {
+ self->dnslen = _need16(self->recvbuf);
+ self->have_dnslen = 1;
+ }
+ if (self->have_dnslen && self->recv >= self->dnslen + sizeof(self->dnslen)) {
+ self->pkts_recv++;
+ self->pkt.len = self->dnslen + sizeof(self->dnslen);
+ self->have_pkt = 1;
+ return (core_object_t*)&self->pkt;
+ }
+
+ if (self->nonblocking) {
+ break;
+ }
+ continue;
+ }
+ if (!n) {
+ break;
+ }
+ switch (errno) {
+ case EAGAIN:
+#if EAGAIN != EWOULDBLOCK
+ case EWOULDBLOCK:
+#endif
+ case EINTR:
+ if (self->nonblocking) {
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ }
+ continue;
+ default:
+ break;
+ }
+ self->errs++;
+ break;
+ }
+
+ if (n < 1) {
+ return 0;
+ }
+
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+}
+
+static const core_object_t* _produce_tls(output_dnscli_t* self)
+{
+ ssize_t n;
+ mlassert_self();
+
+ if (self->have_pkt) {
+ if (self->recv > self->dnslen + sizeof(self->dnslen)) {
+ self->recv -= self->dnslen + sizeof(self->dnslen);
+ memmove(self->recvbuf, self->recvbuf + self->dnslen + sizeof(self->dnslen), self->recv);
+ } else {
+ self->recv = 0;
+ }
+ self->have_pkt = 0;
+ self->have_dnslen = 0;
+ }
+
+ if (!self->have_dnslen && self->recv >= sizeof(self->dnslen)) {
+ self->dnslen = _need16(self->recvbuf);
+ self->have_dnslen = 1;
+ }
+ if (self->have_dnslen && self->recv >= self->dnslen + sizeof(self->dnslen)) {
+ self->pkts_recv++;
+ self->pkt.len = self->dnslen + sizeof(self->dnslen);
+ self->have_pkt = 1;
+ return (core_object_t*)&self->pkt;
+ }
+
+ for (;;) {
+ if (!gnutls_record_check_pending(self->session) && self->poll_timeout) {
+ self->poll.events = POLLIN;
+ n = poll(&self->poll, 1, self->poll_timeout);
+ if (n != 1 || !(self->poll.revents & POLLIN)) {
+ if (!n) {
+ self->timeouts++;
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ } else {
+ self->errs++;
+ }
+ return 0;
+ }
+ }
+ n = gnutls_record_recv(self->session, self->recvbuf + self->recv, sizeof(self->recvbuf) - self->recv);
+ if (n > 0) {
+ self->recv += n;
+
+ if (!self->have_dnslen && self->recv >= sizeof(self->dnslen)) {
+ self->dnslen = _need16(self->recvbuf);
+ self->have_dnslen = 1;
+ }
+ if (self->have_dnslen && self->recv >= self->dnslen + sizeof(self->dnslen)) {
+ self->pkts_recv++;
+ self->pkt.len = self->dnslen + sizeof(self->dnslen);
+ self->have_pkt = 1;
+ return (core_object_t*)&self->pkt;
+ }
+
+ if (self->nonblocking) {
+ break;
+ }
+ continue;
+ }
+ if (!n) {
+ break;
+ }
+ switch (n) {
+ case GNUTLS_E_AGAIN:
+ case GNUTLS_E_TIMEDOUT:
+ case GNUTLS_E_INTERRUPTED:
+ if (self->nonblocking) {
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ }
+ continue;
+ default:
+ break;
+ }
+ self->errs++;
+ break;
+ }
+
+ if (n < 1) {
+ return 0;
+ }
+
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+}
+
+core_producer_t output_dnscli_producer(output_dnscli_t* self)
+{
+ mlassert_self();
+
+ if (!self->conn_ok) {
+ lfatal("not connected");
+ }
+
+ switch (self->mode & OUTPUT_DNSCLI_MODE_MODES) {
+ case OUTPUT_DNSCLI_MODE_UDP:
+ return (core_producer_t)_produce_udp;
+ case OUTPUT_DNSCLI_MODE_TCP:
+ return (core_producer_t)_produce_tcp;
+ case OUTPUT_DNSCLI_MODE_TLS:
+ return (core_producer_t)_produce_tls;
+ default:
+ break;
+ }
+
+ lfatal("internal error");
+ return 0;
+}
diff --git a/src/output/dnscli.h b/src/output/dnscli.h
new file mode 100644
index 0000000..27f2207
--- /dev/null
+++ b/src/output/dnscli.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "core/log.h"
+#include "core/receiver.h"
+#include "core/producer.h"
+#include "core/object/payload.h"
+#include "core/timespec.h"
+#include "core/compat.h"
+
+#ifndef __dnsjit_output_dnscli_h
+#define __dnsjit_output_dnscli_h
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <gnutls/gnutls.h>
+#include <poll.h>
+
+#include "output/dnscli.hh"
+
+#endif
diff --git a/src/output/dnscli.hh b/src/output/dnscli.hh
new file mode 100644
index 0000000..4126423
--- /dev/null
+++ b/src/output/dnscli.hh
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+//lua:require("dnsjit.core.compat_h")
+//lua:require("dnsjit.core.log")
+//lua:require("dnsjit.core.receiver_h")
+//lua:require("dnsjit.core.producer_h")
+//lua:require("dnsjit.core.object.payload_h")
+//lua:require("dnsjit.core.timespec_h")
+
+typedef enum output_dnscli_mode {
+ OUTPUT_DNSCLI_MODE_NONE = 0,
+ OUTPUT_DNSCLI_MODE_OPTIONS = 0xf,
+ OUTPUT_DNSCLI_MODE_NONBLOCKING = 0x1,
+ OUTPUT_DNSCLI_MODE_MODES = 0xf0,
+ OUTPUT_DNSCLI_MODE_UDP = 0x10,
+ OUTPUT_DNSCLI_MODE_TCP = 0x20,
+ OUTPUT_DNSCLI_MODE_TLS = 0x30,
+} output_dnscli_mode_t;
+
+typedef struct output_dnscli {
+ core_log_t _log;
+
+ output_dnscli_mode_t mode;
+
+ size_t pkts, errs, timeouts;
+ int fd, nonblocking, conn_ok;
+
+ struct pollfd poll;
+ int poll_timeout;
+
+ struct sockaddr_storage addr;
+ size_t addr_len;
+
+ uint8_t recvbuf[(64 * 1024) + 2];
+ core_object_payload_t pkt;
+ uint16_t dnslen;
+ uint8_t have_dnslen, have_pkt;
+ size_t recv, pkts_recv;
+
+ core_timespec_t timeout;
+
+ gnutls_session_t session;
+ gnutls_certificate_credentials_t cred;
+} output_dnscli_t;
+
+core_log_t* output_dnscli_log();
+
+void output_dnscli_init(output_dnscli_t* self, output_dnscli_mode_t mode);
+void output_dnscli_destroy(output_dnscli_t* self);
+int output_dnscli_connect(output_dnscli_t* self, const char* host, const char* port);
+luajit_ssize_t output_dnscli_send(output_dnscli_t* self, const core_object_t* obj, size_t sent);
+
+core_receiver_t output_dnscli_receiver(output_dnscli_t* self);
+core_producer_t output_dnscli_producer(output_dnscli_t* self);
diff --git a/src/output/dnscli.lua b/src/output/dnscli.lua
new file mode 100644
index 0000000..bfdaf36
--- /dev/null
+++ b/src/output/dnscli.lua
@@ -0,0 +1,187 @@
+-- Copyright (c) 2018-2021, OARC, Inc.
+-- All rights reserved.
+--
+-- This file is part of dnsjit.
+--
+-- dnsjit is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- dnsjit is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+
+-- dnsjit.output.dnscli
+-- DNS aware UDP/TCP/TLS client
+-- local dnscli = require("dnsjit.output.dnscli")
+-- .SS UDP Receiver Chain
+-- local output = dnscli.new(dnscli.UDP)
+-- output:connect("127.0.0.1", "53")
+-- input:receiver(output)
+-- .SS TCP Nonblocking
+-- local output = dnscli.new(dnscli.TCP + dnscli.NONBLOCKING)
+-- output:send(object)
+--
+-- The DNS client can a
+-- .I core.object.dns
+-- or a
+-- .I core.object.payload
+-- object via the receiver interface or using
+-- .I send()
+-- and send it as DNS query after which it can receive the response by using
+-- the producer interface.
+-- If the object being sent is a
+-- .I core.object.dns
+-- then it will look at
+-- .I includes_dnslen
+-- attribute and depending on the protocol it will disregard, include or send
+-- the DNS length as an extra packet.
+-- If the object being sent is a
+-- .I core.object.payload
+-- then no special handling will be done and it will be sent as is.
+-- When receiving responses the producer interface will generate
+-- .I core.object.payload
+-- objects which may include the DNS length depending on the protocol used and
+-- must be handled by the caller.
+-- .SS MODES
+-- These transport modes and options are available when creating a new Dnscli
+-- output.
+-- .TP
+-- UDP
+-- Create an output using UDP.
+-- .TP
+-- TCP
+-- Create an output using TCP.
+-- .TP
+-- TLS
+-- Create an output using TCP and encrypt it with TLS.
+-- .TP
+-- NONBLOCKING
+-- Make the client nonblocking, see
+-- .I send()
+-- and
+-- .IR produce() .
+module(...,package.seeall)
+
+require("dnsjit.output.dnscli_h")
+local ffi = require("ffi")
+local C = ffi.C
+
+local t_name = "output_dnscli_t"
+local output_dnscli_t = ffi.typeof(t_name)
+local Dnscli = {
+ NONBLOCKING = 0x1,
+ UDP = 0x10,
+ TCP = 0x20,
+ TLS = 0x30,
+}
+
+-- Create a new Dnscli output.
+function Dnscli.new(mode)
+ local self = {
+ obj = output_dnscli_t(),
+ }
+ C.output_dnscli_init(self.obj, mode)
+ ffi.gc(self.obj, C.output_dnscli_destroy)
+ return setmetatable(self, { __index = Dnscli })
+end
+
+-- Set or return the timeout used for sending and reciving, must be used before
+-- .IR connect() .
+function Dnscli:timeout(seconds, nanoseconds)
+ if seconds == nil and nanoseconds == nil then
+ return self.obj.timeout
+ end
+ if nanoseconds == nil then
+ nanoseconds = 0
+ end
+ self.obj.timeout.sec = seconds
+ self.obj.timeout.nsec = nanoseconds
+end
+
+-- Connect to the
+-- .I host
+-- and
+-- .I port
+-- and return 0 if successful.
+function Dnscli:connect(host, port)
+ return C.output_dnscli_connect(self.obj, host, port)
+end
+
+-- Return if nonblocking mode is on (true) or off (false).
+function Dnscli:nonblocking()
+ if self.obj.nonblocking == 1 then
+ return true
+ end
+ return false
+end
+
+-- Send an object and optionally continue sending after
+-- .I sent
+-- bytes.
+-- Unlike the receive interface this function lets you know if the sending was
+-- successful or not which might be needed on nonblocking connections.
+-- Returns -2 on error, -1 if interrupted, timed out or unable to send due to
+-- nonblocking, or the number of bytes sent.
+-- .B Note
+-- the counters for sent, received, errors and timeouts are not affected by
+-- this function.
+function Dnscli:send(object, sent)
+ if sent == nil then
+ sent = 0
+ end
+ return C.output_dnscli_send(self.obj, object, sent)
+end
+
+-- Return the C functions and context for receiving objects, these objects
+-- will be sent.
+function Dnscli:receive()
+ return C.output_dnscli_receiver(self.obj), self.obj
+end
+
+-- Return the C functions and context for producing objects, these objects
+-- are received.
+-- If nonblocking mode is enabled the producer will return a payload object
+-- with length zero if there was nothing to receive.
+-- If nonblocking mode is disabled the producer will wait for data and if
+-- timed out (see
+-- .IR timeout )
+-- it will return a payload object with length zero.
+-- The producer returns nil on error.
+function Dnscli:produce()
+ return C.output_dnscli_producer(self.obj), self.obj
+end
+
+-- Return the number of "packets" sent, actually the number of completely sent
+-- payloads.
+function Dnscli:packets()
+ return tonumber(self.obj.pkts)
+end
+
+-- Return the number of "packets" received, actually the number of successful
+-- calls to
+-- .IR recvfrom (2)
+-- that returned data.
+function Dnscli:received()
+ return tonumber(self.obj.pkts_recv)
+end
+
+-- Return the number of errors when sending or receiving.
+function Dnscli:errors()
+ return tonumber(self.obj.errs)
+end
+
+-- Return the number of timeouts when sending or receiving.
+function Dnscli:timeouts()
+ return tonumber(self.obj.timeouts)
+end
+
+-- core.object.dns (3),
+-- core.object.payload (3),
+-- core.timespec (3)
+return Dnscli
diff --git a/src/output/dnssim.c b/src/output/dnssim.c
new file mode 100644
index 0000000..acd0a05
--- /dev/null
+++ b/src/output/dnssim.c
@@ -0,0 +1,502 @@
+/*
+ * Copyright (c) 2019-2020, CZ.NIC, z.s.p.o.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "output/dnssim.h"
+#include "output/dnssim/internal.h"
+#include "output/dnssim/ll.h"
+#include "core/assert.h"
+#include "core/object/ip.h"
+#include "core/object/ip6.h"
+
+#include <gnutls/gnutls.h>
+#include <string.h>
+
+static core_log_t _log = LOG_T_INIT("output.dnssim");
+static output_dnssim_t _defaults = { LOG_T_INIT_OBJ("output.dnssim") };
+
+static uint64_t _now_ms()
+{
+#if HAVE_CLOCK_NANOSLEEP
+ struct timespec ts;
+ uint64_t now_ms;
+ if (clock_gettime(CLOCK_REALTIME, &ts)) {
+ mlfatal("clock_gettime()");
+ }
+ now_ms = ts.tv_sec * 1000;
+ now_ms += ts.tv_nsec / 1000000;
+ return now_ms;
+#else
+ mlfatal("clock_gettime() not available");
+ return 0;
+#endif
+}
+
+core_log_t* output_dnssim_log()
+{
+ return &_log;
+}
+
+output_dnssim_t* output_dnssim_new(size_t max_clients)
+{
+ output_dnssim_t* self;
+ int ret, i;
+
+ mlfatal_oom(self = calloc(1, sizeof(_output_dnssim_t)));
+ *self = _defaults;
+ self->handshake_timeout_ms = 5000;
+ self->idle_timeout_ms = 10000;
+ output_dnssim_timeout_ms(self, 2000);
+
+ _self->source = NULL;
+ _self->transport = OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY;
+ _self->h2_zero_out_msgid = false;
+
+ self->max_clients = max_clients;
+ lfatal_oom(_self->client_arr = calloc(max_clients, sizeof(_output_dnssim_client_t)));
+
+ for (i = 0; i < max_clients; ++i) {
+ _self->client_arr[i].dnssim = self;
+ }
+
+ ret = gnutls_certificate_allocate_credentials(&_self->tls_cred);
+ if (ret < 0)
+ lfatal("failed to allocated TLS credentials (%s)", gnutls_strerror(ret));
+
+ ret = uv_loop_init(&_self->loop);
+ if (ret < 0)
+ lfatal("failed to initialize uv_loop (%s)", uv_strerror(ret));
+ ldebug("initialized uv_loop");
+
+ return self;
+}
+
+void output_dnssim_free(output_dnssim_t* self)
+{
+ mlassert_self();
+ int ret, i;
+ _output_dnssim_source_t* source;
+ _output_dnssim_source_t* first = _self->source;
+ output_dnssim_stats_t* stats_prev;
+
+ free(self->stats_sum->latency);
+ free(self->stats_sum);
+ do {
+ stats_prev = self->stats_current->prev;
+ free(self->stats_current->latency);
+ free(self->stats_current);
+ self->stats_current = stats_prev;
+ } while (self->stats_current != NULL);
+
+ if (_self->source != NULL) {
+ // free cilcular linked list
+ do {
+ source = _self->source->next;
+ free(_self->source);
+ _self->source = source;
+ } while (_self->source != first);
+ }
+
+ for (i = 0; i < self->max_clients; ++i) {
+ if (_self->client_arr[i].tls_ticket.size != 0) {
+ gnutls_free(_self->client_arr[i].tls_ticket.data);
+ }
+ }
+ free(_self->client_arr);
+
+ ret = uv_loop_close(&_self->loop);
+ if (ret < 0) {
+ lcritical("failed to close uv_loop (%s)", uv_strerror(ret));
+ } else {
+ ldebug("closed uv_loop");
+ }
+
+ gnutls_certificate_free_credentials(_self->tls_cred);
+ if (_self->tls_priority != NULL) {
+ gnutls_priority_deinit(*_self->tls_priority);
+ free(_self->tls_priority);
+ }
+
+ free(self);
+}
+
+void output_dnssim_log_name(output_dnssim_t* self, const char* name)
+{
+ mlassert_self();
+ lassert(name, "name is nil");
+
+ strncpy(self->_log.name, name, sizeof(self->_log.name) - 1);
+ self->_log.name[sizeof(self->_log.name) - 1] = 0;
+ self->_log.is_obj = false;
+}
+
+static uint32_t _extract_client(const core_object_t* obj)
+{
+ uint32_t client;
+ uint8_t* ip;
+
+ switch (obj->obj_type) {
+ case CORE_OBJECT_IP:
+ ip = ((core_object_ip_t*)obj)->dst;
+ break;
+ case CORE_OBJECT_IP6:
+ ip = ((core_object_ip6_t*)obj)->dst;
+ break;
+ default:
+ return -1;
+ }
+
+ memcpy(&client, ip, sizeof(client));
+ return client;
+}
+
+static void _receive(output_dnssim_t* self, const core_object_t* obj)
+{
+ mlassert_self();
+ core_object_t* current = (core_object_t*)obj;
+ core_object_payload_t* payload;
+ uint32_t client;
+
+ self->processed++;
+
+ /* get payload from packet */
+ for (;;) {
+ if (current->obj_type == CORE_OBJECT_PAYLOAD) {
+ payload = (core_object_payload_t*)current;
+ break;
+ }
+ if (current->obj_prev == NULL) {
+ self->discarded++;
+ lwarning("packet discarded (missing payload object)");
+ return;
+ }
+ current = (core_object_t*)current->obj_prev;
+ }
+
+ /* extract client information from IP/IP6 layer */
+ for (;;) {
+ if (current->obj_type == CORE_OBJECT_IP || current->obj_type == CORE_OBJECT_IP6) {
+ client = _extract_client(current);
+ break;
+ }
+ if (current->obj_prev == NULL) {
+ self->discarded++;
+ lwarning("packet discarded (missing ip/ip6 object)");
+ return;
+ }
+ current = (core_object_t*)current->obj_prev;
+ }
+
+ if (self->free_after_use) {
+ /* free all objects except payload */
+ current = (core_object_t*)obj;
+ core_object_t* parent = current;
+ while (current != NULL) {
+ parent = current;
+ current = (core_object_t*)current->obj_prev;
+ if (parent->obj_type != CORE_OBJECT_PAYLOAD) {
+ core_object_free(parent);
+ }
+ }
+ }
+
+ if (_self->h2_zero_out_msgid) {
+ lassert(_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2, "must use HTTP/2 to zero-out msgid");
+ if (payload->len < 2) {
+ self->discarded++;
+ lwarning("packet discarded (payload len < 2)");
+ return;
+ }
+ uint8_t* data = (uint8_t*)payload->payload;
+ data[0] = 0x00;
+ data[1] = 0x00;
+ }
+
+ if (client >= self->max_clients) {
+ self->discarded++;
+ lwarning("packet discarded (client exceeded max_clients)");
+ return;
+ }
+
+ ldebug("client(c): %d", client);
+ _output_dnssim_create_request(self, &_self->client_arr[client], payload);
+}
+
+core_receiver_t output_dnssim_receiver()
+{
+ return (core_receiver_t)_receive;
+}
+
+void output_dnssim_set_transport(output_dnssim_t* self, output_dnssim_transport_t tr)
+{
+ mlassert_self();
+
+ switch (tr) {
+ case OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY:
+ lnotice("transport set to UDP (no TCP fallback)");
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_TCP:
+ lnotice("transport set to TCP");
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_TLS:
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ lnotice("transport set to TLS");
+#else
+ lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_HTTPS2:
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ lnotice("transport set to HTTP/2 over TLS");
+ if (&_self->h2_uri_authority[0])
+ lnotice("set uri authority to: %s", _self->h2_uri_authority);
+#else
+ lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_UDP:
+ lfatal("UDP transport with TCP fallback is not supported yet.");
+ break;
+ default:
+ lfatal("unknown or unsupported transport");
+ break;
+ }
+
+ _self->transport = tr;
+}
+
+int output_dnssim_target(output_dnssim_t* self, const char* ip, uint16_t port)
+{
+ int ret;
+ mlassert_self();
+ lassert(ip, "ip is nil");
+ lassert(port, "port is nil");
+
+ ret = uv_ip6_addr(ip, port, (struct sockaddr_in6*)&_self->target);
+ if (ret != 0) {
+ ret = uv_ip4_addr(ip, port, (struct sockaddr_in*)&_self->target);
+ if (ret != 0) {
+ lfatal("failed to parse IPv4 or IPv6 from \"%s\"", ip);
+ } else {
+ ret = snprintf(_self->h2_uri_authority, _MAX_URI_LEN, "%s:%d", ip, port);
+ }
+ } else {
+ ret = snprintf(_self->h2_uri_authority, _MAX_URI_LEN, "[%s]:%d", ip, port);
+ }
+
+ if (ret > 0) {
+ if (_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2)
+ lnotice("set uri authority to: %s", _self->h2_uri_authority);
+ } else {
+ _self->h2_uri_authority[0] = '\0';
+ if (_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2)
+ lfatal("failed to set authority");
+ }
+
+ lnotice("set target to %s port %d", ip, port);
+ return 0;
+}
+
+int output_dnssim_bind(output_dnssim_t* self, const char* ip)
+{
+ int ret;
+ mlassert_self();
+ lassert(ip, "ip is nil");
+
+ _output_dnssim_source_t* source;
+ lfatal_oom(source = malloc(sizeof(_output_dnssim_source_t)));
+
+ ret = uv_ip6_addr(ip, 0, (struct sockaddr_in6*)&source->addr);
+ if (ret != 0) {
+ ret = uv_ip4_addr(ip, 0, (struct sockaddr_in*)&source->addr);
+ if (ret != 0) {
+ lfatal("failed to parse IPv4 or IPv6 from \"%s\"", ip);
+ }
+ }
+
+ if (_self->source == NULL) {
+ source->next = source;
+ _self->source = source;
+ } else {
+ source->next = _self->source->next;
+ _self->source->next = source;
+ }
+
+ lnotice("bind to source address %s", ip);
+ return 0;
+}
+
+int output_dnssim_tls_priority(output_dnssim_t* self, const char* priority)
+{
+ mlassert_self();
+ lassert(priority, "priority is nil");
+
+ if (_self->tls_priority != NULL) {
+ gnutls_priority_deinit(*_self->tls_priority);
+ free(_self->tls_priority);
+ }
+ lfatal_oom(_self->tls_priority = malloc(sizeof(gnutls_priority_t)));
+
+ int ret = gnutls_priority_init(_self->tls_priority, priority, NULL);
+ if (ret < 0) {
+ lfatal("failed to initialize TLS priority cache: %s", gnutls_strerror(ret));
+ } else {
+ lnotice("GnuTLS priority set: %s", priority);
+ }
+
+ return 0;
+}
+
+int output_dnssim_run_nowait(output_dnssim_t* self)
+{
+ mlassert_self();
+
+ return uv_run(&_self->loop, UV_RUN_NOWAIT);
+}
+
+void output_dnssim_timeout_ms(output_dnssim_t* self, uint64_t timeout_ms)
+{
+ mlassert_self();
+ lassert(timeout_ms > 0, "timeout must be greater than 0");
+
+ if (self->stats_sum != NULL) {
+ free(self->stats_sum->latency);
+ free(self->stats_sum);
+ self->stats_sum = 0;
+ }
+ if (self->stats_current != NULL) {
+ output_dnssim_stats_t* stats_prev;
+ do {
+ stats_prev = self->stats_current->prev;
+ free(self->stats_current->latency);
+ free(self->stats_current);
+ self->stats_current = stats_prev;
+ } while (self->stats_current != NULL);
+ }
+
+ self->timeout_ms = timeout_ms;
+
+ lfatal_oom(self->stats_sum = calloc(1, sizeof(output_dnssim_stats_t)));
+ lfatal_oom(self->stats_sum->latency = calloc(self->timeout_ms + 1, sizeof(uint64_t)));
+
+ lfatal_oom(self->stats_current = calloc(1, sizeof(output_dnssim_stats_t)));
+ lfatal_oom(self->stats_current->latency = calloc(self->timeout_ms + 1, sizeof(uint64_t)));
+
+ self->stats_first = self->stats_current;
+}
+
+void output_dnssim_h2_uri_path(output_dnssim_t* self, const char* uri_path)
+{
+ mlassert_self();
+ lassert(uri_path, "uri_path is nil");
+ lassert(strlen(uri_path) < _MAX_URI_LEN, "uri_path too long");
+
+ strncpy(_self->h2_uri_path, uri_path, _MAX_URI_LEN - 1);
+ _self->h2_uri_path[_MAX_URI_LEN - 1] = 0;
+ lnotice("http2: set uri path to: %s", _self->h2_uri_path);
+}
+
+void output_dnssim_h2_method(output_dnssim_t* self, const char* method)
+{
+ mlassert_self();
+ lassert(method, "method is nil");
+
+ if (strcmp("GET", method) == 0) {
+ _self->h2_method = OUTPUT_DNSSIM_H2_GET;
+ } else if (strcmp("POST", method) == 0) {
+ _self->h2_method = OUTPUT_DNSSIM_H2_POST;
+ } else {
+ lfatal("http2: unsupported method: \"%s\"", method);
+ }
+
+ lnotice("http2: set method to %s", method);
+}
+
+void output_dnssim_h2_zero_out_msgid(output_dnssim_t* self, bool zero_out_msgid)
+{
+ mlassert_self();
+
+ if (zero_out_msgid) {
+ lassert(_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2, "transport must be set to HTTP/2 to set zero_out_msgid");
+ _self->h2_zero_out_msgid = zero_out_msgid;
+ }
+}
+
+static void _on_stats_timer_tick(uv_timer_t* handle)
+{
+ uint64_t now_ms = _now_ms();
+ output_dnssim_t* self;
+ mlassert(handle, "handle is nil");
+ self = (output_dnssim_t*)handle->data;
+ mlassert_self();
+ lassert(self->stats_sum, "stats_sum is nil");
+ lassert(self->stats_current, "stats_current is nil");
+
+ lnotice("total processed:%10ld; answers:%10ld; discarded:%10ld; ongoing:%10ld",
+ self->processed, self->stats_sum->answers, self->discarded, self->ongoing);
+
+ output_dnssim_stats_t* stats_next;
+ lfatal_oom(stats_next = calloc(1, sizeof(output_dnssim_stats_t)));
+ lfatal_oom(stats_next->latency = calloc(self->timeout_ms + 1, sizeof(uint64_t)));
+
+ self->stats_current->until_ms = now_ms;
+ stats_next->since_ms = now_ms;
+ stats_next->conn_active = self->stats_current->conn_active;
+
+ stats_next->ongoing = self->ongoing;
+ stats_next->prev = self->stats_current;
+ self->stats_current->next = stats_next;
+ self->stats_current = stats_next;
+}
+
+void output_dnssim_stats_collect(output_dnssim_t* self, uint64_t interval_ms)
+{
+ uint64_t now_ms = _now_ms();
+ mlassert_self();
+ lassert(self->stats_sum, "stats_sum is nil");
+ lassert(self->stats_current, "stats_current is nil");
+
+ if (self->stats_interval_ms != 0) {
+ lfatal("statistics collection has already started!");
+ }
+ self->stats_interval_ms = interval_ms;
+
+ self->stats_sum->since_ms = now_ms;
+ self->stats_current->since_ms = now_ms;
+
+ _self->stats_timer.data = (void*)self;
+ uv_timer_init(&_self->loop, &_self->stats_timer);
+ uv_timer_start(&_self->stats_timer, _on_stats_timer_tick, interval_ms, interval_ms);
+}
+
+void output_dnssim_stats_finish(output_dnssim_t* self)
+{
+ uint64_t now_ms = _now_ms();
+ mlassert_self();
+ lassert(self->stats_sum, "stats_sum is nil");
+ lassert(self->stats_current, "stats_current is nil");
+
+ self->stats_sum->until_ms = now_ms;
+ self->stats_current->until_ms = now_ms;
+
+ uv_timer_stop(&_self->stats_timer);
+ uv_close((uv_handle_t*)&_self->stats_timer, NULL);
+}
diff --git a/src/output/dnssim.h b/src/output/dnssim.h
new file mode 100644
index 0000000..f843000
--- /dev/null
+++ b/src/output/dnssim.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2019, CZ.NIC, z.s.p.o.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "core/log.h"
+#include "core/receiver.h"
+
+#ifndef __dnsjit_output_dnssim_h
+#define __dnsjit_output_dnssim_h
+
+#include <stdbool.h>
+
+#include "output/dnssim.hh"
+
+#endif
diff --git a/src/output/dnssim.hh b/src/output/dnssim.hh
new file mode 100644
index 0000000..a17125d
--- /dev/null
+++ b/src/output/dnssim.hh
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2018-2020, CZ.NIC, z.s.p.o.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+//lua:require("dnsjit.core.log")
+//lua:require("dnsjit.core.receiver_h")
+
+typedef enum output_dnssim_transport {
+ OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY,
+ OUTPUT_DNSSIM_TRANSPORT_UDP,
+ OUTPUT_DNSSIM_TRANSPORT_TCP,
+ OUTPUT_DNSSIM_TRANSPORT_TLS,
+ OUTPUT_DNSSIM_TRANSPORT_HTTPS2
+} output_dnssim_transport_t;
+
+typedef enum output_dnssim_h2_method {
+ OUTPUT_DNSSIM_H2_GET,
+ OUTPUT_DNSSIM_H2_POST
+} output_dnssim_h2_method_t;
+
+typedef struct output_dnssim_stats output_dnssim_stats_t;
+struct output_dnssim_stats {
+ output_dnssim_stats_t* prev;
+ output_dnssim_stats_t* next;
+
+ uint64_t* latency;
+
+ uint64_t since_ms;
+ uint64_t until_ms;
+
+ uint64_t requests;
+ uint64_t ongoing;
+ uint64_t answers;
+
+ /* Number of connections that are open at the end of the stats interval. */
+ uint64_t conn_active;
+
+ /* Number of connection handshake attempts during the stats interval. */
+ uint64_t conn_handshakes;
+
+ /* Number of connection that have been resumed with TLS session resumption. */
+ uint64_t conn_resumed;
+
+ /* Number of timed out connection handshakes during the stats interval. */
+ uint64_t conn_handshakes_failed;
+
+ uint64_t rcode_noerror;
+ uint64_t rcode_formerr;
+ uint64_t rcode_servfail;
+ uint64_t rcode_nxdomain;
+ uint64_t rcode_notimp;
+ uint64_t rcode_refused;
+ uint64_t rcode_yxdomain;
+ uint64_t rcode_yxrrset;
+ uint64_t rcode_nxrrset;
+ uint64_t rcode_notauth;
+ uint64_t rcode_notzone;
+ uint64_t rcode_badvers;
+ uint64_t rcode_badkey;
+ uint64_t rcode_badtime;
+ uint64_t rcode_badmode;
+ uint64_t rcode_badname;
+ uint64_t rcode_badalg;
+ uint64_t rcode_badtrunc;
+ uint64_t rcode_badcookie;
+ uint64_t rcode_other;
+};
+
+typedef struct output_dnssim {
+ core_log_t _log;
+
+ uint64_t processed;
+ uint64_t discarded;
+ uint64_t ongoing;
+
+ output_dnssim_stats_t* stats_sum;
+ output_dnssim_stats_t* stats_current;
+ output_dnssim_stats_t* stats_first;
+
+ size_t max_clients;
+ bool free_after_use;
+
+ uint64_t timeout_ms;
+ uint64_t idle_timeout_ms;
+ uint64_t handshake_timeout_ms;
+ uint64_t stats_interval_ms;
+} output_dnssim_t;
+
+core_log_t* output_dnssim_log();
+
+output_dnssim_t* output_dnssim_new(size_t max_clients);
+void output_dnssim_free(output_dnssim_t* self);
+
+void output_dnssim_log_name(output_dnssim_t* self, const char* name);
+void output_dnssim_set_transport(output_dnssim_t* self, output_dnssim_transport_t tr);
+int output_dnssim_target(output_dnssim_t* self, const char* ip, uint16_t port);
+int output_dnssim_bind(output_dnssim_t* self, const char* ip);
+int output_dnssim_tls_priority(output_dnssim_t* self, const char* priority);
+int output_dnssim_run_nowait(output_dnssim_t* self);
+void output_dnssim_timeout_ms(output_dnssim_t* self, uint64_t timeout_ms);
+void output_dnssim_h2_uri_path(output_dnssim_t* self, const char* uri_path);
+void output_dnssim_h2_method(output_dnssim_t* self, const char* method);
+void output_dnssim_h2_zero_out_msgid(output_dnssim_t* self, bool zero_out_msgid);
+void output_dnssim_stats_collect(output_dnssim_t* self, uint64_t interval_ms);
+void output_dnssim_stats_finish(output_dnssim_t* self);
+
+core_receiver_t output_dnssim_receiver();
diff --git a/src/output/dnssim.lua b/src/output/dnssim.lua
new file mode 100644
index 0000000..25193c4
--- /dev/null
+++ b/src/output/dnssim.lua
@@ -0,0 +1,433 @@
+-- Copyright (c) 2018-2021, CZ.NIC, z.s.p.o.
+-- All rights reserved.
+--
+-- This file is part of dnsjit.
+--
+-- dnsjit is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- dnsjit is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+
+-- dnsjit.output.dnssim
+-- Simulate independent DNS clients over various transports
+-- output = require("dnsjit.output.dnssim").new()
+-- .SS Usage
+-- output:udp()
+-- output:target("::1", 53)
+-- recv, rctx = output:receive()
+-- -- pass in objects using recv(rctx, obj)
+-- -- repeatedly call output:run_nowait() until it returns 0
+-- .SS DNS-over-TLS example configuration
+-- output:tls("NORMAL:-VERS-ALL:+VERS-TLS1.3") -- enforce TLS 1.3
+-- .SS DNS-over-HTTPS/2 example configuration
+-- output:https2({ method = "POST", uri_path = "/doh" })
+--
+-- Output module for simulating traffic from huge number of independent,
+-- individual DNS clients.
+-- Uses libuv for asynchronous communication.
+-- There may only be a single DnsSim in a thread.
+-- Use
+-- .I dnsjit.core.thread
+-- to have multiple DnsSim instances.
+-- .P
+-- With proper use of this component, it is possible to simulate hundreds of
+-- thousands of clients when using a high-performance server.
+-- This also applies for state-full transports.
+-- The complete set-up is quite complex and requires other components.
+-- See DNS Shotgun
+-- .RI ( https://gitlab.nic.cz/knot/shotgun )
+-- for dnsjit scripts ready for use for high-performance
+-- benchmarking.
+module(...,package.seeall)
+
+require("dnsjit.output.dnssim_h")
+local bit = require("bit")
+local object = require("dnsjit.core.objects")
+local ffi = require("ffi")
+local C = ffi.C
+
+local DnsSim = {}
+
+local _DNSSIM_VERSION = 20210129
+local _DNSSIM_JSON_VERSION = 20200527
+
+-- Create a new DnsSim output for up to max_clients.
+function DnsSim.new(max_clients)
+ local self = {
+ obj = C.output_dnssim_new(max_clients),
+ max_clients = max_clients,
+ }
+ ffi.gc(self.obj, C.output_dnssim_free)
+ return setmetatable(self, { __index = DnsSim })
+end
+
+local function _check_version(version, req_version)
+ if req_version == nil then
+ return version
+ end
+ local min_version = tonumber(req_version)
+ if min_version == nil then
+ C.output_dnssim_log():fatal("invalid version number: "..req_version)
+ return nil
+ end
+ if version >= min_version then
+ return version
+ end
+ return nil
+end
+
+-- Check that version of dnssim is at minimum the one passed as
+-- .B req_version
+-- and return the actual version number.
+-- Return nil if the condition is not met.
+--
+-- If no
+-- .B req_version
+-- is specified no check is done and only the version number is returned.
+function DnsSim.check_version(req_version)
+ return _check_version(_DNSSIM_VERSION, req_version)
+end
+
+-- Check that version of dnssim's JSON data format is at minimum the one passed as
+-- .B req_version
+-- and return the actual version number.
+-- Return nil if the condition is not met.
+--
+-- If no
+-- .B req_version
+-- is specified no check is done and only the version number is returned.
+function DnsSim.check_json_version(req_version)
+ return _check_version(_DNSSIM_JSON_VERSION, req_version)
+end
+
+-- Return the Log object to control logging of this instance or module.
+-- Optionally, set the instance's log name.
+-- Unique name should be used for each instance.
+function DnsSim:log(name)
+ if self == nil then
+ return C.output_dnssim_log()
+ end
+ if name ~= nil then
+ C.output_dnssim_log_name(self.obj, name)
+ end
+ return self.obj._log
+end
+
+-- Set the target IPv4/IPv6 address where queries will be sent to.
+function DnsSim:target(ip, port)
+ local nport = tonumber(port)
+ if nport == nil then
+ self.obj._log:fatal("invalid port: "..port)
+ return -1
+ end
+ if nport <= 0 or nport > 65535 then
+ self.obj._log:fatal("invalid port number: "..nport)
+ return -1
+ end
+ return C.output_dnssim_target(self.obj, ip, nport)
+end
+
+-- Specify source IPv4/IPv6 address for sending queries.
+-- Can be set multiple times.
+-- Addresses are selected round-robin when sending.
+function DnsSim:bind(ip)
+ return C.output_dnssim_bind(self.obj, ip)
+end
+
+-- Set the preferred transport to UDP.
+--
+-- When the optional argument
+-- .B tcp_fallback
+-- is set to true, individual queries are re-tried over TCP when TC bit is set in the answer.
+-- Defaults to
+-- .B false
+-- (aka only UDP is used).
+function DnsSim:udp(tcp_fallback)
+ if tcp_fallback == true then
+ C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_UDP)
+ else
+ C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY)
+ end
+end
+
+-- Set the transport to TCP.
+function DnsSim:tcp()
+ C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_TCP)
+end
+
+-- Set the transport to TLS.
+--
+-- The optional argument
+-- .B tls_priority
+-- is a GnuTLS priority string, which can be used to select TLS versions, cipher suites etc.
+-- For example:
+--
+-- .RB "- """ NORMAL:%NO_TICKETS """"
+-- will use defaults without TLS session resumption.
+--
+-- .RB "- """ SECURE128:-VERS-ALL:+VERS-TLS1.3 """"
+-- will use only TLS 1.3 with 128-bit secure ciphers.
+--
+-- Refer to:
+-- .I https://gnutls.org/manual/html_node/Priority-Strings.html
+function DnsSim:tls(tls_priority)
+ if tls_priority ~= nil then
+ C.output_dnssim_tls_priority(self.obj, tls_priority)
+ end
+ C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_TLS)
+end
+
+-- Set the transport to HTTP/2 over TLS.
+--
+-- .B http2_options
+-- is a lua table which supports the following keys:
+--
+-- .B method:
+-- .B GET
+-- (default)
+-- or
+-- .B POST
+--
+-- .B uri_path:
+-- where queries will be sent.
+-- Defaults to
+-- .B /dns-query
+--
+-- .B zero_out_msgid:
+-- when
+-- .B true
+-- (default), query ID is always set to 0
+--
+-- See tls() method for
+-- .B tls_priority
+-- documentation.
+function DnsSim:https2(http2_options, tls_priority)
+ if tls_priority ~= nil then
+ C.output_dnssim_tls_priority(self.obj, tls_priority)
+ end
+
+ uri_path = "/dns-query"
+ zero_out_msgid = true
+ method = "GET"
+
+ if http2_options ~= nil then
+ if type(http2_options) ~= "table" then
+ self.obj._log:fatal("http2_options must be a table")
+ else
+ if http2_options["uri_path"] ~= nil then
+ uri_path = http2_options["uri_path"]
+ end
+ if http2_options["zero_out_msgid"] ~= nil and http2_options["zero_out_msgid"] ~= true then
+ zero_out_msgid = false
+ end
+ if http2_options["method"] ~= nil then
+ method = http2_options["method"]
+ end
+ end
+ end
+
+ C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_HTTPS2)
+ C.output_dnssim_h2_uri_path(self.obj, uri_path)
+ C.output_dnssim_h2_method(self.obj, method)
+ C.output_dnssim_h2_zero_out_msgid(self.obj, zero_out_msgid)
+end
+
+-- Set timeout for the individual requests in seconds (default 2s).
+--
+-- .BR Beware :
+-- increasing this value while the target resolver isn't very responsive
+-- (cold cache, heavy load) may degrade DnsSim's performance and skew
+-- the results.
+function DnsSim:timeout(seconds)
+ if seconds == nil then
+ seconds = 2
+ end
+ timeout_ms = math.floor(seconds * 1000)
+ C.output_dnssim_timeout_ms(self.obj, timeout_ms)
+end
+
+-- Set TCP connection idle timeout for connection reuse according to RFC7766,
+-- Section 6.2.3 (defaults to 10s).
+-- When set to zero, connections are closed immediately after there are no
+-- more pending queries.
+function DnsSim:idle_timeout(seconds)
+ if seconds == nil then
+ seconds = 10
+ end
+ self.obj.idle_timeout_ms = math.floor(seconds * 1000)
+end
+
+-- Set TCP connection handshake timeout (defaults to 5s).
+-- During heavy load, the server may no longer accept new connections.
+-- This parameter ensures such connection attempts are aborted after the
+-- timeout expires.
+function DnsSim:handshake_timeout(seconds)
+ if seconds == nil then
+ seconds = 5
+ end
+ self.obj.handshake_timeout_ms = math.floor(seconds * 1000)
+end
+
+-- Run the libuv loop once without blocking when there is no I/O.
+-- This should be called repeatedly until 0 is returned and no more data
+-- is expected to be received by DnsSim.
+function DnsSim:run_nowait()
+ return C.output_dnssim_run_nowait(self.obj)
+end
+
+-- Set this to true if DnsSim should free the memory of passed-in objects
+-- (useful when using
+-- .I dnsjit.filter.copy
+-- to pass objects from different thread).
+function DnsSim:free_after_use(free_after_use)
+ self.obj.free_after_use = free_after_use
+end
+
+-- Number of input packets discarded due to various reasons.
+-- To investigate causes, run with increased logging level.
+function DnsSim:discarded()
+ return tonumber(self.obj.discarded)
+end
+
+-- Number of valid requests (input packets) processed.
+function DnsSim:requests()
+ return tonumber(self.obj.stats_sum.requests)
+end
+
+-- Number of requests that received an answer
+function DnsSim:answers()
+ return tonumber(self.obj.stats_sum.answers)
+end
+
+-- Number of requests that received a NOERROR response
+function DnsSim:noerror()
+ return tonumber(self.obj.stats_sum.rcode_noerror)
+end
+
+-- Configure statistics to be collected every N seconds.
+function DnsSim:stats_collect(seconds)
+ if seconds == nil then
+ self.obj._log:fatal("number of seconds must be set for stats_collect()")
+ end
+ interval_ms = math.floor(seconds * 1000)
+ C.output_dnssim_stats_collect(self.obj, interval_ms)
+end
+
+-- Stop the collection of statistics.
+function DnsSim:stats_finish()
+ C.output_dnssim_stats_finish(self.obj)
+end
+
+-- Export the results to a JSON file.
+function DnsSim:export(filename)
+ local file = io.open(filename, "w")
+ if file == nil then
+ self.obj._log:fatal("export failed: no filename")
+ return
+ end
+
+ local function write_stats(file, stats)
+ file:write(
+ "{ ",
+ '"since_ms":', tonumber(stats.since_ms), ',',
+ '"until_ms":', tonumber(stats.until_ms), ',',
+ '"requests":', tonumber(stats.requests), ',',
+ '"ongoing":', tonumber(stats.ongoing), ',',
+ '"answers":', tonumber(stats.answers), ',',
+ '"conn_active":', tonumber(stats.conn_active), ',',
+ '"conn_handshakes":', tonumber(stats.conn_handshakes), ',',
+ '"conn_resumed":', tonumber(stats.conn_resumed), ',',
+ '"conn_handshakes_failed":', tonumber(stats.conn_handshakes_failed), ',',
+ '"rcode_noerror":', tonumber(stats.rcode_noerror), ',',
+ '"rcode_formerr":', tonumber(stats.rcode_formerr), ',',
+ '"rcode_servfail":', tonumber(stats.rcode_servfail), ',',
+ '"rcode_nxdomain":', tonumber(stats.rcode_nxdomain), ',',
+ '"rcode_notimp":', tonumber(stats.rcode_notimp), ',',
+ '"rcode_refused":', tonumber(stats.rcode_refused), ',',
+ '"rcode_yxdomain":', tonumber(stats.rcode_yxdomain), ',',
+ '"rcode_yxrrset":', tonumber(stats.rcode_yxrrset), ',',
+ '"rcode_nxrrset":', tonumber(stats.rcode_nxrrset), ',',
+ '"rcode_notauth":', tonumber(stats.rcode_notauth), ',',
+ '"rcode_notzone":', tonumber(stats.rcode_notzone), ',',
+ '"rcode_badvers":', tonumber(stats.rcode_badvers), ',',
+ '"rcode_badkey":', tonumber(stats.rcode_badkey), ',',
+ '"rcode_badtime":', tonumber(stats.rcode_badtime), ',',
+ '"rcode_badmode":', tonumber(stats.rcode_badmode), ',',
+ '"rcode_badname":', tonumber(stats.rcode_badname), ',',
+ '"rcode_badalg":', tonumber(stats.rcode_badalg), ',',
+ '"rcode_badtrunc":', tonumber(stats.rcode_badtrunc), ',',
+ '"rcode_badcookie":', tonumber(stats.rcode_badcookie), ',',
+ '"rcode_other":', tonumber(stats.rcode_other), ',',
+ '"latency":[')
+ file:write(tonumber(stats.latency[0]))
+ for i=1,tonumber(self.obj.timeout_ms) do
+ file:write(',', tonumber(stats.latency[i]))
+ end
+ file:write("]}")
+ end
+
+ file:write(
+ "{ ",
+ '"version":', _DNSSIM_JSON_VERSION, ',',
+ '"merged":false,',
+ '"stats_interval_ms":', tonumber(self.obj.stats_interval_ms), ',',
+ '"timeout_ms":', tonumber(self.obj.timeout_ms), ',',
+ '"idle_timeout_ms":', tonumber(self.obj.idle_timeout_ms), ',',
+ '"handshake_timeout_ms":', tonumber(self.obj.handshake_timeout_ms), ',',
+ '"discarded":', self:discarded(), ',',
+ '"stats_sum":')
+ write_stats(file, self.obj.stats_sum)
+ file:write(
+ ',',
+ '"stats_periodic":[')
+
+ local stats = self.obj.stats_first
+ write_stats(file, stats)
+
+ while (stats.next ~= nil) do
+ stats = stats.next
+ file:write(',')
+ write_stats(file, stats)
+ end
+
+ file:write(']}')
+ file:close()
+ self.obj._log:notice("results exported to "..filename)
+end
+
+-- Return the C function and context for receiving objects.
+-- Only
+-- .I dnsjit.filter.core.object.ip
+-- or
+-- .I dnsjit.filter.core.object.ip6
+-- objects are supported.
+-- The component expects a 32bit integer (in host order) ranging from 0
+-- to max_clients written to first 4 bytes of destination IP.
+-- See
+-- .IR dnsjit.filter.ipsplit .
+function DnsSim:receive()
+ local receive = C.output_dnssim_receiver()
+ return receive, self.obj
+end
+
+-- Deprecated: use udp() instead.
+--
+-- Set the transport to UDP (without any TCP fallback).
+function DnsSim:udp_only()
+ C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY)
+end
+
+-- dnsjit.filter.copy (3),
+-- dnsjit.filter.ipsplit (3),
+-- dnsjit.filter.core.object.ip (3),
+-- dnsjit.filter.core.object.ip6 (3),
+-- https://gitlab.nic.cz/knot/shotgun
+return DnsSim
diff --git a/src/output/dnssim/CHANGELOG.md b/src/output/dnssim/CHANGELOG.md
new file mode 100644
index 0000000..9cbfa55
--- /dev/null
+++ b/src/output/dnssim/CHANGELOG.md
@@ -0,0 +1,16 @@
+dnssim v20210129
+================
+
+- Added DNS-over-HTTPS support with https2()
+- Added IPv4 support
+- Abort operation on insufficient file descriptors
+- Match QUESTION section of received responses
+- Improvements in connection state handling
+- Deprecate udp_only() in favor of udp()
+- Allow setting logger name with log(name)
+- Added check_version() and check_json_version()
+
+dnssim v20200723
+================
+
+- First released dnssim version with UDP, TCP and DoT support
diff --git a/src/output/dnssim/common.c b/src/output/dnssim/common.c
new file mode 100644
index 0000000..e170aec
--- /dev/null
+++ b/src/output/dnssim/common.c
@@ -0,0 +1,384 @@
+/*
+ * Copyright (c) 2019-2020, CZ.NIC, z.s.p.o.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "output/dnssim.h"
+#include "output/dnssim/internal.h"
+#include "output/dnssim/ll.h"
+#include "core/assert.h"
+
+#include <string.h>
+
+#define MAX_LABELS 127
+
+static core_log_t _log = LOG_T_INIT("output.dnssim");
+
+static void _close_request(_output_dnssim_request_t* req);
+
+static void _on_request_timeout(uv_timer_t* handle)
+{
+ _close_request((_output_dnssim_request_t*)handle->data);
+}
+
+static ssize_t parse_qsection(core_object_dns_t* dns)
+{
+ core_object_dns_q_t q;
+ static core_object_dns_label_t labels[MAX_LABELS];
+ const uint8_t* start;
+ int i;
+ int ret;
+
+ if (!dns || !dns->have_qdcount)
+ return -1;
+
+ start = dns->at;
+
+ for (i = 0; i < dns->qdcount; i++) {
+ ret = core_object_dns_parse_q(dns, &q, labels, MAX_LABELS);
+ if (ret < 0)
+ return -1;
+ }
+
+ return (dns->at - start);
+}
+
+int _output_dnssim_answers_request(_output_dnssim_request_t* req, core_object_dns_t* response)
+{
+ const uint8_t* question;
+ ssize_t len;
+
+ if (!response->have_id || !response->have_qdcount)
+ return _ERR_MALFORMED;
+
+ if (req->dns_q->id != response->id)
+ return _ERR_MSGID;
+
+ if (req->dns_q->qdcount != response->qdcount)
+ return _ERR_QUESTION;
+
+ question = response->at;
+ len = parse_qsection(response);
+
+ if (req->question_len != len)
+ return _ERR_QUESTION;
+
+ if (memcmp(req->question, question, len) != 0)
+ return _ERR_QUESTION;
+
+ return 0;
+}
+
+void _output_dnssim_create_request(output_dnssim_t* self, _output_dnssim_client_t* client, core_object_payload_t* payload)
+{
+ int ret;
+ _output_dnssim_request_t* req;
+ mlassert_self();
+ lassert(client, "client is nil");
+ lassert(payload, "payload is nil");
+
+ lfatal_oom(req = calloc(1, sizeof(_output_dnssim_request_t)));
+ req->dnssim = self;
+ req->client = client;
+ req->payload = payload;
+ req->dns_q = core_object_dns_new();
+ req->dns_q->obj_prev = (core_object_t*)req->payload;
+ req->dnssim->ongoing++;
+ req->state = _OUTPUT_DNSSIM_REQ_ONGOING;
+ req->stats = self->stats_current;
+
+ ret = core_object_dns_parse_header(req->dns_q);
+ if (ret != 0) {
+ ldebug("discarded malformed dns query: couldn't parse header");
+ goto failure;
+ }
+
+ req->question = req->dns_q->at;
+ req->question_len = parse_qsection(req->dns_q);
+ if (req->question_len < 0) {
+ ldebug("discarded malformed dns query: invalid question");
+ goto failure;
+ }
+
+ req->dnssim->stats_sum->requests++;
+ req->stats->requests++;
+
+ switch (_self->transport) {
+ case OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY:
+ case OUTPUT_DNSSIM_TRANSPORT_UDP:
+ ret = _output_dnssim_create_query_udp(self, req);
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_TCP:
+ ret = _output_dnssim_create_query_tcp(self, req);
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_TLS:
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ ret = _output_dnssim_create_query_tls(self, req);
+#else
+ lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_HTTPS2:
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ ret = _output_dnssim_create_query_https2(self, req);
+#else
+ lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ break;
+ default:
+ lfatal("unsupported dnssim transport");
+ break;
+ }
+ if (ret < 0) {
+ goto failure;
+ }
+
+ req->created_at = uv_now(&_self->loop);
+ req->ended_at = req->created_at + self->timeout_ms;
+ lfatal_oom(req->timer = malloc(sizeof(uv_timer_t)));
+ uv_timer_init(&_self->loop, req->timer);
+ req->timer->data = req;
+ uv_timer_start(req->timer, _on_request_timeout, self->timeout_ms, 0);
+
+ return;
+failure:
+ self->discarded++;
+ _close_request(req);
+ return;
+}
+
+/* Bind before connect to be able to send from different source IPs. */
+int _output_dnssim_bind_before_connect(output_dnssim_t* self, uv_handle_t* handle)
+{
+ mlassert_self();
+ lassert(handle, "handle is nil");
+
+ if (_self->source != NULL) {
+ struct sockaddr* addr = (struct sockaddr*)&_self->source->addr;
+ struct sockaddr* dest = (struct sockaddr*)&_self->target;
+ int ret = -1;
+ if (addr->sa_family != dest->sa_family) {
+ lfatal("failed to bind: source/desitnation address family mismatch");
+ }
+ switch (handle->type) {
+ case UV_UDP:
+ ret = uv_udp_bind((uv_udp_t*)handle, addr, 0);
+ break;
+ case UV_TCP:
+ ret = uv_tcp_bind((uv_tcp_t*)handle, addr, 0);
+ break;
+ default:
+ lfatal("failed to bind: unsupported handle type");
+ break;
+ }
+ if (ret < 0) {
+ /* This typically happens when we run out of file descriptors.
+ * Quit to prevent skewed results or unexpected behaviour. */
+ lfatal("failed to bind: %s", uv_strerror(ret));
+ return ret;
+ }
+ _self->source = _self->source->next;
+ }
+ return 0;
+}
+
+void _output_dnssim_maybe_free_request(_output_dnssim_request_t* req)
+{
+ mlassert(req, "req is nil");
+
+ if (req->qry == NULL && req->timer == NULL) {
+ if (req->dnssim->free_after_use) {
+ core_object_payload_free(req->payload);
+ }
+ core_object_dns_free(req->dns_q);
+ free(req);
+ }
+}
+
+static void _close_query(_output_dnssim_query_t* qry)
+{
+ mlassert(qry, "qry is nil");
+
+ switch (qry->transport) {
+ case OUTPUT_DNSSIM_TRANSPORT_UDP:
+ _output_dnssim_close_query_udp((_output_dnssim_query_udp_t*)qry);
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_TCP:
+ _output_dnssim_close_query_tcp((_output_dnssim_query_tcp_t*)qry);
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_TLS:
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ _output_dnssim_close_query_tls((_output_dnssim_query_tcp_t*)qry);
+#else
+ mlfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_HTTPS2:
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ _output_dnssim_close_query_https2((_output_dnssim_query_tcp_t*)qry);
+#else
+ mlfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ break;
+ default:
+ mlfatal("invalid query transport");
+ break;
+ }
+}
+
+static void _on_request_timer_closed(uv_handle_t* handle)
+{
+ _output_dnssim_request_t* req = (_output_dnssim_request_t*)handle->data;
+ mlassert(req, "req is nil");
+ free(handle);
+ req->timer = NULL;
+ _output_dnssim_maybe_free_request(req);
+}
+
+static void _close_request(_output_dnssim_request_t* req)
+{
+ if (req == NULL || req->state == _OUTPUT_DNSSIM_REQ_CLOSING)
+ return;
+ mlassert(req->state == _OUTPUT_DNSSIM_REQ_ONGOING, "request to be closed must be ongoing");
+ req->state = _OUTPUT_DNSSIM_REQ_CLOSING;
+ req->dnssim->ongoing--;
+
+ /* Calculate latency. */
+ uint64_t latency;
+ req->ended_at = uv_now(&((_output_dnssim_t*)req->dnssim)->loop);
+ latency = req->ended_at - req->created_at;
+ if (latency > req->dnssim->timeout_ms) {
+ req->ended_at = req->created_at + req->dnssim->timeout_ms;
+ latency = req->dnssim->timeout_ms;
+ }
+ req->stats->latency[latency]++;
+ req->dnssim->stats_sum->latency[latency]++;
+
+ if (req->timer != NULL) {
+ uv_timer_stop(req->timer);
+ uv_close((uv_handle_t*)req->timer, _on_request_timer_closed);
+ }
+
+ /* Finish any queries in flight. */
+ _output_dnssim_query_t* qry = req->qry;
+ if (qry != NULL)
+ _close_query(qry);
+
+ _output_dnssim_maybe_free_request(req);
+}
+
+void _output_dnssim_request_answered(_output_dnssim_request_t* req, core_object_dns_t* msg)
+{
+ mlassert(req, "req is nil");
+ mlassert(msg, "msg is nil");
+
+ req->dnssim->stats_sum->answers++;
+ req->stats->answers++;
+
+ switch (msg->rcode) {
+ case CORE_OBJECT_DNS_RCODE_NOERROR:
+ req->dnssim->stats_sum->rcode_noerror++;
+ req->stats->rcode_noerror++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_FORMERR:
+ req->dnssim->stats_sum->rcode_formerr++;
+ req->stats->rcode_formerr++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_SERVFAIL:
+ req->dnssim->stats_sum->rcode_servfail++;
+ req->stats->rcode_servfail++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_NXDOMAIN:
+ req->dnssim->stats_sum->rcode_nxdomain++;
+ req->stats->rcode_nxdomain++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_NOTIMP:
+ req->dnssim->stats_sum->rcode_notimp++;
+ req->stats->rcode_notimp++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_REFUSED:
+ req->dnssim->stats_sum->rcode_refused++;
+ req->stats->rcode_refused++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_YXDOMAIN:
+ req->dnssim->stats_sum->rcode_yxdomain++;
+ req->stats->rcode_yxdomain++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_YXRRSET:
+ req->dnssim->stats_sum->rcode_yxrrset++;
+ req->stats->rcode_yxrrset++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_NXRRSET:
+ req->dnssim->stats_sum->rcode_nxrrset++;
+ req->stats->rcode_nxrrset++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_NOTAUTH:
+ req->dnssim->stats_sum->rcode_notauth++;
+ req->stats->rcode_notauth++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_NOTZONE:
+ req->dnssim->stats_sum->rcode_notzone++;
+ req->stats->rcode_notzone++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_BADVERS:
+ req->dnssim->stats_sum->rcode_badvers++;
+ req->stats->rcode_badvers++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_BADKEY:
+ req->dnssim->stats_sum->rcode_badkey++;
+ req->stats->rcode_badkey++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_BADTIME:
+ req->dnssim->stats_sum->rcode_badtime++;
+ req->stats->rcode_badtime++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_BADMODE:
+ req->dnssim->stats_sum->rcode_badmode++;
+ req->stats->rcode_badmode++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_BADNAME:
+ req->dnssim->stats_sum->rcode_badname++;
+ req->stats->rcode_badname++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_BADALG:
+ req->dnssim->stats_sum->rcode_badalg++;
+ req->stats->rcode_badalg++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_BADTRUNC:
+ req->dnssim->stats_sum->rcode_badtrunc++;
+ req->stats->rcode_badtrunc++;
+ break;
+ case CORE_OBJECT_DNS_RCODE_BADCOOKIE:
+ req->dnssim->stats_sum->rcode_badcookie++;
+ req->stats->rcode_badcookie++;
+ break;
+ default:
+ req->dnssim->stats_sum->rcode_other++;
+ req->stats->rcode_other++;
+ }
+
+ _close_request(req);
+}
+
+void _output_dnssim_on_uv_alloc(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf)
+{
+ mlfatal_oom(buf->base = malloc(suggested_size));
+ buf->len = suggested_size;
+}
diff --git a/src/output/dnssim/connection.c b/src/output/dnssim/connection.c
new file mode 100644
index 0000000..eeb1ce8
--- /dev/null
+++ b/src/output/dnssim/connection.c
@@ -0,0 +1,471 @@
+/*
+ * Copyright (c) 2020, CZ.NIC, z.s.p.o.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "output/dnssim.h"
+#include "output/dnssim/internal.h"
+#include "output/dnssim/ll.h"
+#include "core/assert.h"
+
+#include <string.h>
+
+static core_log_t _log = LOG_T_INIT("output.dnssim");
+
+static bool _conn_is_connecting(_output_dnssim_connection_t* conn)
+{
+ return (conn->state >= _OUTPUT_DNSSIM_CONN_TCP_HANDSHAKE && conn->state <= _OUTPUT_DNSSIM_CONN_ACTIVE);
+}
+
+void _output_dnssim_conn_maybe_free(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn can't be nil");
+ mlassert(conn->client, "conn must belong to a client");
+ if (conn->handle == NULL && conn->handshake_timer == NULL && conn->idle_timer == NULL) {
+ _ll_try_remove(conn->client->conn, conn);
+ if (conn->tls != NULL) {
+ free(conn->tls);
+ conn->tls = NULL;
+ }
+ if (conn->http2 != NULL) {
+ free(conn->http2);
+ conn->http2 = NULL;
+ }
+ free(conn);
+ }
+}
+
+static void _on_handshake_timer_closed(uv_handle_t* handle)
+{
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)handle->data;
+ mlassert(conn, "conn is nil");
+ mlassert(conn->handshake_timer, "conn must have handshake timer when closing it");
+ free(conn->handshake_timer);
+ conn->handshake_timer = NULL;
+ _output_dnssim_conn_maybe_free(conn);
+}
+
+static void _on_idle_timer_closed(uv_handle_t* handle)
+{
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)handle->data;
+ mlassert(conn, "conn is nil");
+ mlassert(conn->idle_timer, "conn must have idle timer when closing it");
+ free(conn->idle_timer);
+ conn->is_idle = false;
+ conn->idle_timer = NULL;
+ _output_dnssim_conn_maybe_free(conn);
+}
+
+void _output_dnssim_conn_close(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn can't be nil");
+ mlassert(conn->stats, "conn must have stats");
+ mlassert(conn->client, "conn must have client");
+ mlassert(conn->client->dnssim, "client must have dnssim");
+
+ output_dnssim_t* self = conn->client->dnssim;
+
+ switch (conn->state) {
+ case _OUTPUT_DNSSIM_CONN_CLOSING:
+ case _OUTPUT_DNSSIM_CONN_CLOSED:
+ return;
+ case _OUTPUT_DNSSIM_CONN_TCP_HANDSHAKE:
+ case _OUTPUT_DNSSIM_CONN_TLS_HANDSHAKE:
+ conn->stats->conn_handshakes_failed++;
+ self->stats_sum->conn_handshakes_failed++;
+ break;
+ case _OUTPUT_DNSSIM_CONN_ACTIVE:
+ case _OUTPUT_DNSSIM_CONN_CONGESTED:
+ self->stats_current->conn_active--;
+ break;
+ case _OUTPUT_DNSSIM_CONN_INITIALIZED:
+ case _OUTPUT_DNSSIM_CONN_CLOSE_REQUESTED:
+ break;
+ default:
+ lfatal("unknown conn state: %d", conn->state);
+ }
+ if (conn->prevent_close) {
+ lassert(conn->state <= _OUTPUT_DNSSIM_CONN_CLOSE_REQUESTED, "conn already closing");
+ conn->state = _OUTPUT_DNSSIM_CONN_CLOSE_REQUESTED;
+ return;
+ }
+ conn->state = _OUTPUT_DNSSIM_CONN_CLOSING;
+
+ if (conn->handshake_timer != NULL) {
+ uv_timer_stop(conn->handshake_timer);
+ uv_close((uv_handle_t*)conn->handshake_timer, _on_handshake_timer_closed);
+ }
+ if (conn->idle_timer != NULL) {
+ conn->is_idle = false;
+ uv_timer_stop(conn->idle_timer);
+ uv_close((uv_handle_t*)conn->idle_timer, _on_idle_timer_closed);
+ }
+
+ switch (_self->transport) {
+ case OUTPUT_DNSSIM_TRANSPORT_TCP:
+ _output_dnssim_tcp_close(conn);
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_TLS:
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ _output_dnssim_tls_close(conn);
+#else
+ lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_HTTPS2:
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ _output_dnssim_https2_close(conn);
+#else
+ lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ break;
+ default:
+ lfatal("unsupported transport");
+ break;
+ }
+}
+
+/* Close connection or run idle timer when there are no more outstanding queries. */
+void _output_dnssim_conn_idle(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn can't be nil");
+
+ if (conn->queued == NULL && conn->sent == NULL) {
+ if (conn->idle_timer == NULL)
+ _output_dnssim_conn_close(conn);
+ else if (!conn->is_idle) {
+ conn->is_idle = true;
+ uv_timer_again(conn->idle_timer);
+ }
+ }
+}
+
+static void _send_pending_queries(_output_dnssim_connection_t* conn)
+{
+ _output_dnssim_query_tcp_t* qry;
+ mlassert(conn, "conn is nil");
+ mlassert(conn->client, "conn->client is nil");
+ qry = (_output_dnssim_query_tcp_t*)conn->client->pending;
+
+ while (qry != NULL && conn->state == _OUTPUT_DNSSIM_CONN_ACTIVE) {
+ _output_dnssim_query_tcp_t* next = (_output_dnssim_query_tcp_t*)qry->qry.next;
+ if (qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_WRITE) {
+ switch (qry->qry.transport) {
+ case OUTPUT_DNSSIM_TRANSPORT_TCP:
+ _output_dnssim_tcp_write_query(conn, qry);
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_TLS:
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ _output_dnssim_tls_write_query(conn, qry);
+#else
+ mlfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_HTTPS2:
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ _output_dnssim_https2_write_query(conn, qry);
+#else
+ mlfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ break;
+ default:
+ mlfatal("unsupported protocol");
+ break;
+ }
+ }
+ qry = next;
+ }
+}
+
+int _output_dnssim_handle_pending_queries(_output_dnssim_client_t* client)
+{
+ int ret = 0;
+ mlassert(client, "client is nil");
+
+ if (client->pending == NULL)
+ return ret;
+
+ output_dnssim_t* self = client->dnssim;
+ mlassert(self, "client must belong to dnssim");
+
+ /* Get active connection or find out whether new connection has to be opened. */
+ bool is_connecting = false;
+ _output_dnssim_connection_t* conn = client->conn;
+ while (conn != NULL) {
+ if (conn->state == _OUTPUT_DNSSIM_CONN_ACTIVE)
+ break;
+ else if (_conn_is_connecting(conn))
+ is_connecting = true;
+ conn = conn->next;
+ }
+
+ if (conn != NULL) { /* Send data right away over active connection. */
+ _send_pending_queries(conn);
+ } else if (!is_connecting) { /* No active or connecting connection -> open a new one. */
+ lfatal_oom(conn = calloc(1, sizeof(_output_dnssim_connection_t)));
+ conn->state = _OUTPUT_DNSSIM_CONN_INITIALIZED;
+ conn->client = client;
+ conn->stats = self->stats_current;
+ if (_self->transport == OUTPUT_DNSSIM_TRANSPORT_TLS) {
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ ret = _output_dnssim_tls_init(conn);
+ if (ret < 0) {
+ free(conn);
+ return ret;
+ }
+#else
+ lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ } else if (_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2) {
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ ret = _output_dnssim_https2_init(conn);
+ if (ret < 0) {
+ free(conn);
+ return ret;
+ }
+#else
+ lfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ }
+ ret = _output_dnssim_tcp_connect(self, conn);
+ if (ret < 0)
+ return ret;
+ _ll_append(client->conn, conn);
+ } /* Otherwise, pending queries wil be sent after connected callback. */
+
+ return ret;
+}
+
+void _output_dnssim_conn_activate(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn is nil");
+ mlassert(conn->client, "conn must be associated with a client");
+ mlassert(conn->client->dnssim, "client must be associated with dnssim");
+
+ uv_timer_stop(conn->handshake_timer);
+
+ conn->state = _OUTPUT_DNSSIM_CONN_ACTIVE;
+ conn->client->dnssim->stats_current->conn_active++;
+ conn->read_state = _OUTPUT_DNSSIM_READ_STATE_DNSLEN;
+ conn->dnsbuf_len = 2;
+ conn->dnsbuf_pos = 0;
+ conn->dnsbuf_free_after_use = false;
+
+ _send_pending_queries(conn);
+ _output_dnssim_conn_idle(conn);
+}
+
+int _process_dnsmsg(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn can't be nil");
+ mlassert(conn->client, "conn must have client");
+ mlassert(conn->client->dnssim, "client must have dnssim");
+
+ output_dnssim_t* self = conn->client->dnssim;
+
+ core_object_payload_t payload = CORE_OBJECT_PAYLOAD_INIT(NULL);
+ core_object_dns_t dns_a = CORE_OBJECT_DNS_INIT(&payload);
+
+ payload.payload = (uint8_t*)conn->dnsbuf_data;
+ payload.len = conn->dnsbuf_len;
+
+ dns_a.obj_prev = (core_object_t*)&payload;
+ int ret = core_object_dns_parse_header(&dns_a);
+ if (ret != 0) {
+ lwarning("tcp response malformed");
+ return _ERR_MALFORMED;
+ }
+ ldebug("tcp recv dnsmsg id: %04x", dns_a.id);
+
+ _output_dnssim_query_t* qry;
+
+ if (_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2) {
+ lassert(conn->http2, "conn must have http2 ctx");
+ lassert(conn->http2->current_qry, "http2 has no current_qry");
+ lassert(conn->http2->current_qry->qry.req, "current_qry has no req");
+ lassert(conn->http2->current_qry->qry.req->dns_q, "req has no dns_q");
+
+ ret = _output_dnssim_answers_request(conn->http2->current_qry->qry.req, &dns_a);
+ switch (ret) {
+ case 0:
+ _output_dnssim_request_answered(conn->http2->current_qry->qry.req, &dns_a);
+ break;
+ case _ERR_MSGID:
+ lwarning("https2 QID mismatch: request=0x%04x, response=0x%04x",
+ conn->http2->current_qry->qry.req->dns_q->id, dns_a.id);
+ break;
+ case _ERR_QUESTION:
+ default:
+ lwarning("https2 response question mismatch");
+ break;
+ }
+ } else {
+ qry = conn->sent;
+ while (qry != NULL) {
+ if (qry->req->dns_q->id == dns_a.id) {
+ ret = _output_dnssim_answers_request(qry->req, &dns_a);
+ if (ret != 0) {
+ lwarning("response question mismatch");
+ } else {
+ _output_dnssim_request_answered(qry->req, &dns_a);
+ }
+ break;
+ }
+ qry = qry->next;
+ }
+ }
+
+ return 0;
+}
+
+static int _parse_dnsbuf_data(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn can't be nil");
+ mlassert(conn->dnsbuf_pos == conn->dnsbuf_len, "attempt to parse incomplete dnsbuf_data");
+ int ret = 0;
+
+ switch (conn->read_state) {
+ case _OUTPUT_DNSSIM_READ_STATE_DNSLEN: {
+ uint16_t* p_dnslen = (uint16_t*)conn->dnsbuf_data;
+ conn->dnsbuf_len = ntohs(*p_dnslen);
+ if (conn->dnsbuf_len == 0) {
+ mlwarning("invalid dnslen received: 0");
+ conn->dnsbuf_len = 2;
+ conn->read_state = _OUTPUT_DNSSIM_READ_STATE_DNSLEN;
+ } else if (conn->dnsbuf_len < 12) {
+ mldebug("invalid dnslen received: %d", conn->dnsbuf_len);
+ ret = -1;
+ } else {
+ mldebug("dnslen: %d", conn->dnsbuf_len);
+ conn->read_state = _OUTPUT_DNSSIM_READ_STATE_DNSMSG;
+ }
+ break;
+ }
+ case _OUTPUT_DNSSIM_READ_STATE_DNSMSG:
+ ret = _process_dnsmsg(conn);
+ if (ret) {
+ conn->read_state = _OUTPUT_DNSSIM_READ_STATE_INVALID;
+ } else {
+ conn->dnsbuf_len = 2;
+ conn->read_state = _OUTPUT_DNSSIM_READ_STATE_DNSLEN;
+ }
+ break;
+ default:
+ mlfatal("tcp invalid connection read_state");
+ break;
+ }
+
+ conn->dnsbuf_pos = 0;
+ if (conn->dnsbuf_free_after_use) {
+ conn->dnsbuf_free_after_use = false;
+ free(conn->dnsbuf_data);
+ }
+ conn->dnsbuf_data = NULL;
+
+ return ret;
+}
+
+static unsigned int _read_dns_stream_chunk(_output_dnssim_connection_t* conn, size_t len, const char* data)
+{
+ mlassert(conn, "conn can't be nil");
+ mlassert(data, "data can't be nil");
+ mlassert(len > 0, "no data to read");
+ mlassert((conn->read_state == _OUTPUT_DNSSIM_READ_STATE_DNSLEN || conn->read_state == _OUTPUT_DNSSIM_READ_STATE_DNSMSG),
+ "connection has invalid read_state");
+
+ int ret = 0;
+ unsigned int nread;
+ size_t expected = conn->dnsbuf_len - conn->dnsbuf_pos;
+ mlassert(expected > 0, "no data expected");
+
+ if (conn->dnsbuf_free_after_use == false && expected > len) {
+ /* Start of partial read. */
+ mlassert(conn->dnsbuf_pos == 0, "conn->dnsbuf_pos must be 0 at start of partial read");
+ mlassert(conn->dnsbuf_len > 0, "conn->dnsbuf_len must be set at start of partial read");
+ mlfatal_oom(conn->dnsbuf_data = malloc(conn->dnsbuf_len * sizeof(char)));
+ conn->dnsbuf_free_after_use = true;
+ }
+
+ if (conn->dnsbuf_free_after_use) { /* Partial read is in progress. */
+ char* dest = conn->dnsbuf_data + conn->dnsbuf_pos;
+ if (expected < len)
+ len = expected;
+ memcpy(dest, data, len);
+ conn->dnsbuf_pos += len;
+ nread = len;
+ } else { /* Complete and clean read. */
+ mlassert(expected <= len, "not enough data to perform complete read");
+ conn->dnsbuf_data = (char*)data;
+ conn->dnsbuf_pos = conn->dnsbuf_len;
+ nread = expected;
+ }
+
+ /* If entire dnslen/dnsmsg was read, attempt to parse it. */
+ if (conn->dnsbuf_len == conn->dnsbuf_pos) {
+ ret = _parse_dnsbuf_data(conn);
+ if (ret < 0)
+ return ret;
+ }
+
+ return nread;
+}
+
+void _output_dnssim_read_dns_stream(_output_dnssim_connection_t* conn, size_t len, const char* data)
+{
+ int pos = 0;
+ int chunk = 0;
+ while (pos < len) {
+ chunk = _read_dns_stream_chunk(conn, len - pos, data + pos);
+ if (chunk < 0) {
+ mlwarning("lost orientation in DNS stream, closing");
+ _output_dnssim_conn_close(conn);
+ break;
+ } else {
+ pos += chunk;
+ }
+ }
+ mlassert((pos == len) || (chunk < 0), "dns stream read invalid, pos != len");
+}
+
+void _output_dnssim_read_dnsmsg(_output_dnssim_connection_t* conn, size_t len, const char* data)
+{
+ mlassert(conn, "conn is nil");
+ mlassert(len > 0, "len is zero");
+ mlassert(data, "no data");
+ mlassert(conn->dnsbuf_pos == 0, "dnsbuf not empty");
+ mlassert(conn->dnsbuf_free_after_use == false, "dnsbuf read in progress");
+
+ /* Read dnsmsg of given length from input data. */
+ conn->dnsbuf_len = len;
+ conn->read_state = _OUTPUT_DNSSIM_READ_STATE_DNSMSG;
+ int nread = _read_dns_stream_chunk(conn, len, data);
+
+ if (nread != len) {
+ mlwarning("failed to read received dnsmsg");
+ if (conn->dnsbuf_free_after_use)
+ free(conn->dnsbuf_data);
+ }
+
+ /* Clean state afterwards. */
+ conn->read_state = _OUTPUT_DNSSIM_READ_STATE_DNSLEN;
+ conn->dnsbuf_len = 2;
+ conn->dnsbuf_pos = 0;
+ conn->dnsbuf_free_after_use = false;
+}
diff --git a/src/output/dnssim/https2.c b/src/output/dnssim/https2.c
new file mode 100644
index 0000000..72fcdaf
--- /dev/null
+++ b/src/output/dnssim/https2.c
@@ -0,0 +1,592 @@
+/*
+ * Copyright (c) 2020, CZ.NIC, z.s.p.o.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "output/dnssim.h"
+#include "output/dnssim/internal.h"
+#include "output/dnssim/ll.h"
+#include "core/assert.h"
+#include "lib/base64url.h"
+
+#include <gnutls/gnutls.h>
+#include <string.h>
+
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+
+#define OUTPUT_DNSSIM_MAKE_NV(NAME, VALUE, VALUELEN) \
+ { \
+ (uint8_t*)NAME, (uint8_t*)VALUE, sizeof(NAME) - 1, VALUELEN, \
+ NGHTTP2_NV_FLAG_NONE \
+ }
+
+#define OUTPUT_DNSSIM_MAKE_NV2(NAME, VALUE) \
+ { \
+ (uint8_t*)NAME, (uint8_t*)VALUE, sizeof(NAME) - 1, sizeof(VALUE) - 1, \
+ NGHTTP2_NV_FLAG_NONE \
+ }
+
+#define OUTPUT_DNSSIM_HTTP_GET_TEMPLATE "?dns="
+#define OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN (sizeof(OUTPUT_DNSSIM_HTTP_GET_TEMPLATE) - 1)
+#define OUTPUT_DNSSIM_HTTP2_INITIAL_MAX_CONCURRENT_STREAMS 100
+#define OUTPUT_DNSSIM_HTTP2_DEFAULT_MAX_CONCURRENT_STREAMS 0xffffffffu
+
+static core_log_t _log = LOG_T_INIT("output.dnssim");
+
+static ssize_t _http2_send(nghttp2_session* session, const uint8_t* data, size_t length, int flags, void* user_data)
+{
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data;
+ mlassert(conn, "conn can't be null");
+ mlassert(conn->tls, "conn must have tls ctx");
+ mlassert(conn->tls->session, "conn must have tls session");
+
+ mldebug("http2 (%p): sending data, len=%ld", session, length);
+
+ ssize_t len = 0;
+ if ((len = gnutls_record_send(conn->tls->session, data, length)) < 0) {
+ mlwarning("gnutls_record_send failed: %s", gnutls_strerror(len));
+ len = NGHTTP2_ERR_CALLBACK_FAILURE;
+ }
+
+ return len;
+}
+
+static ssize_t _http2_on_data_provider_read(nghttp2_session* session, int32_t stream_id, uint8_t* buf, size_t length, uint32_t* data_flags, nghttp2_data_source* source, void* user_data)
+{
+ _output_dnssim_https2_data_provider_t* buffer = source->ptr;
+ mlassert(buffer, "no data provider");
+ mlassert(buffer->len <= MAX_DNSMSG_SIZE, "invalid dnsmsg size: %zu B", buffer->len);
+
+ ssize_t sent = (length < buffer->len) ? length : buffer->len;
+ mlassert(sent >= 0, "negative length of bytes to send");
+
+ memcpy(buf, buffer->buf, sent);
+ buffer->buf += sent;
+ buffer->len -= sent;
+ if (buffer->len == 0)
+ *data_flags |= NGHTTP2_DATA_FLAG_EOF;
+
+ return sent;
+}
+
+static _output_dnssim_query_tcp_t* _http2_get_stream_qry(_output_dnssim_connection_t* conn, int32_t stream_id)
+{
+ mlassert(conn, "conn is nil");
+ mlassert(stream_id >= 0, "invalid stream_id");
+
+ _output_dnssim_query_tcp_t* qry = (_output_dnssim_query_tcp_t*)conn->sent;
+ while (qry != NULL && qry->stream_id != stream_id) {
+ qry = (_output_dnssim_query_tcp_t*)qry->qry.next;
+ }
+
+ return qry;
+}
+
+static int _http2_on_header(nghttp2_session* session, const nghttp2_frame* frame, const uint8_t* name, size_t namelen, const uint8_t* value, size_t valuelen, uint8_t flags, void* user_data)
+{
+ if (frame->hd.type == NGHTTP2_HEADERS && frame->headers.cat == NGHTTP2_HCAT_RESPONSE) {
+ if (namelen == 7 && strncmp((char*)name, ":status", 7) == 0) {
+ if (valuelen != 3 || (value[0] != '1' && value[0] != '2')) {
+ /* When reponse code isn't 1xx or 2xx, close the query.
+ * This will result in request timeout, which currently seems
+ * slightly better than mocking SERVFAIL for statistics. */
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data;
+ mlassert(conn, "conn is nil");
+ _output_dnssim_query_tcp_t* qry = _http2_get_stream_qry(conn, frame->hd.stream_id);
+
+ if (qry != NULL) {
+ _output_dnssim_close_query_https2(qry);
+ mlinfo("http response %s, closing query", value);
+ }
+ }
+ }
+ }
+ return 0;
+}
+
+static int _http2_on_data_recv(nghttp2_session* session, uint8_t flags, int32_t stream_id, const uint8_t* data, size_t len, void* user_data)
+{
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data;
+ mlassert(conn, "conn is nil");
+
+ _output_dnssim_query_tcp_t* qry = _http2_get_stream_qry(conn, stream_id);
+
+ mldebug("http2: data chunk recv, session=%p, len=%d", session, len);
+
+ if (qry) {
+ if (qry->recv_buf_len == 0) {
+ if (len > MAX_DNSMSG_SIZE) {
+ mlwarning("http response exceeded maximum size of dns message");
+ return -1;
+ }
+ mlfatal_oom(qry->recv_buf = malloc(len));
+ memcpy(qry->recv_buf, data, len);
+ qry->recv_buf_len = len;
+ } else {
+ size_t total_len = qry->recv_buf_len + len;
+ if (total_len > MAX_DNSMSG_SIZE) {
+ mlwarning("http response exceeded maximum size of dns message");
+ return -1;
+ }
+ mlfatal_oom(qry->recv_buf = realloc(qry->recv_buf, total_len));
+ memcpy(qry->recv_buf + qry->recv_buf_len, data, len);
+ qry->recv_buf_len = total_len;
+ }
+ } else {
+ mldebug("no query associated with this stream id, ignoring");
+ }
+
+ return 0;
+}
+
+static void _http2_check_max_streams(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn can't be null");
+ mlassert(conn->http2, "conn must have http2 ctx");
+
+ switch (conn->state) {
+ case _OUTPUT_DNSSIM_CONN_ACTIVE:
+ if (conn->http2->open_streams >= conn->http2->max_concurrent_streams) {
+ mlinfo("http2 (%p): reached maximum number of concurrent streams (%ld)",
+ conn->http2->session, conn->http2->max_concurrent_streams);
+ conn->state = _OUTPUT_DNSSIM_CONN_CONGESTED;
+ }
+ break;
+ case _OUTPUT_DNSSIM_CONN_CONGESTED:
+ if (conn->http2->open_streams < conn->http2->max_concurrent_streams)
+ conn->state = _OUTPUT_DNSSIM_CONN_ACTIVE;
+ break;
+ default:
+ break;
+ }
+}
+
+static int _http2_on_stream_close(nghttp2_session* session, int32_t stream_id, uint32_t error_code, void* user_data)
+{
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data;
+ mlassert(conn, "conn can't be null");
+ mlassert(conn->http2, "conn must have http2 ctx");
+ mlassert(conn->http2->open_streams > 0, "conn has no open streams");
+
+ conn->http2->open_streams--;
+ _http2_check_max_streams(conn);
+ return 0;
+}
+
+static int _http2_on_frame_recv(nghttp2_session* session, const nghttp2_frame* frame, void* user_data)
+{
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data;
+ mlassert(conn, "conn can't be null");
+ mlassert(conn->tls, "conn must have tls ctx");
+ mlassert(conn->tls->session, "conn must have tls session");
+ mlassert(conn->http2, "conn must have http2 ctx");
+
+ switch (frame->hd.type) {
+ case NGHTTP2_DATA:
+ if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
+ mldebug("http2 (%p): final DATA frame recv", session);
+ _output_dnssim_query_tcp_t* qry = _http2_get_stream_qry(conn, frame->hd.stream_id);
+
+ if (qry != NULL) {
+ conn->http2->current_qry = qry;
+ _output_dnssim_read_dnsmsg(conn, qry->recv_buf_len, (char*)qry->recv_buf);
+ }
+ }
+ break;
+ case NGHTTP2_SETTINGS:
+ if (!conn->http2->remote_settings_received) {
+ /* On the first SETTINGS frame, set concurrent streams to unlimited, same as nghttp2. */
+ conn->http2->remote_settings_received = true;
+ conn->http2->max_concurrent_streams = OUTPUT_DNSSIM_HTTP2_DEFAULT_MAX_CONCURRENT_STREAMS;
+ _http2_check_max_streams(conn);
+ }
+ nghttp2_settings* settings = (nghttp2_settings*)frame;
+ int i;
+ for (i = 0; i < settings->niv; i++) {
+ switch (settings->iv[i].settings_id) {
+ case NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS:
+ conn->http2->max_concurrent_streams = settings->iv[i].value;
+ _http2_check_max_streams(conn);
+ break;
+ default:
+ break;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ return 0;
+}
+
+int _output_dnssim_https2_init(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn is nil");
+ mlassert(conn->tls == NULL, "conn already has tls context");
+ mlassert(conn->http2 == NULL, "conn already has http2 context");
+ mlassert(conn->client, "conn must be associated with a client");
+ mlassert(conn->client->dnssim, "client must have dnssim");
+
+ int ret = -1;
+ nghttp2_session_callbacks* callbacks;
+ nghttp2_option* option;
+ output_dnssim_t* self = conn->client->dnssim;
+
+ /* Initialize TLS session. */
+ ret = _output_dnssim_tls_init(conn);
+ if (ret < 0)
+ return ret;
+
+ /* Configure ALPN to negotiate HTTP/2. */
+ const gnutls_datum_t protos[] = {
+ { (unsigned char*)"h2", 2 }
+ };
+ ret = gnutls_alpn_set_protocols(conn->tls->session, protos, 1, 0);
+ if (ret < 0) {
+ lwarning("failed to set ALPN protocol: %s", gnutls_strerror(ret));
+ return ret;
+ }
+
+ lfatal_oom(conn->http2 = calloc(1, sizeof(_output_dnssim_http2_ctx_t)));
+ conn->http2->max_concurrent_streams = OUTPUT_DNSSIM_HTTP2_INITIAL_MAX_CONCURRENT_STREAMS;
+
+ /* Set up HTTP/2 callbacks and client. */
+ lassert(nghttp2_session_callbacks_new(&callbacks) == 0, "out of memory");
+ nghttp2_session_callbacks_set_send_callback(callbacks, _http2_send);
+ nghttp2_session_callbacks_set_on_header_callback(callbacks, _http2_on_header);
+ nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, _http2_on_data_recv);
+ nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, _http2_on_frame_recv);
+ nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, _http2_on_stream_close);
+
+ lassert(nghttp2_option_new(&option) == 0, "out of memory");
+ nghttp2_option_set_peer_max_concurrent_streams(option, conn->http2->max_concurrent_streams);
+
+ ret = nghttp2_session_client_new2(&conn->http2->session, callbacks, conn, option);
+
+ nghttp2_session_callbacks_del(callbacks);
+ nghttp2_option_del(option);
+
+ if (ret < 0) {
+ free(conn->http2);
+ conn->http2 = NULL;
+ }
+
+ return ret;
+}
+
+int _output_dnssim_https2_setup(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn is nil");
+ mlassert(conn->tls, "conn must have tls ctx");
+ mlassert(conn->tls->session, "conn must have tls session");
+ mlassert(conn->http2, "conn must have http2 ctx");
+ mlassert(conn->http2->session, "conn must have http2 session");
+
+ int ret = -1;
+
+ /* Check "h2" protocol was negotiated with ALPN. */
+ gnutls_datum_t proto;
+ ret = gnutls_alpn_get_selected_protocol(conn->tls->session, &proto);
+ if (ret < 0) {
+ mlwarning("http2: failed to get negotiated protocol: %s", gnutls_strerror(ret));
+ return ret;
+ }
+ if (proto.size != 2 || memcmp("h2", proto.data, 2) != 0) {
+ mlwarning("http2: protocol is not negotiated");
+ return ret;
+ }
+
+ /* Submit SETTIGNS frame. */
+ static const nghttp2_settings_entry iv[] = {
+ { NGHTTP2_SETTINGS_MAX_FRAME_SIZE, MAX_DNSMSG_SIZE },
+ { NGHTTP2_SETTINGS_ENABLE_PUSH, 0 }, /* Only we can initiate streams. */
+ };
+ ret = nghttp2_submit_settings(conn->http2->session, NGHTTP2_FLAG_NONE, iv, sizeof(iv) / sizeof(*iv));
+ if (ret < 0) {
+ mlwarning("http2: failed to submit SETTINGS: %s", nghttp2_strerror(ret));
+ return ret;
+ }
+
+ ret = 0;
+ return ret;
+}
+
+void _output_dnssim_https2_process_input_data(_output_dnssim_connection_t* conn, size_t len, const char* data)
+{
+ mlassert(conn, "conn is nil");
+ mlassert(conn->http2, "conn must have http2 ctx");
+ mlassert(conn->http2->session, "conn must have http2 session");
+
+ /* Process incoming frames. */
+ ssize_t ret = 0;
+ conn->prevent_close = true;
+ ret = nghttp2_session_mem_recv(conn->http2->session, (uint8_t*)data, len);
+ conn->prevent_close = false;
+ if (ret < 0) {
+ mlwarning("failed nghttp2_session_mem_recv: %s", nghttp2_strerror(ret));
+ _output_dnssim_conn_close(conn);
+ return;
+ } else if (conn->state == _OUTPUT_DNSSIM_CONN_CLOSE_REQUESTED) {
+ _output_dnssim_conn_close(conn);
+ return;
+ }
+ mlassert(ret == len, "nghttp2_session_mem_recv didn't process all data");
+
+ /* Send any frames the read might have triggered. */
+ ret = nghttp2_session_send(conn->http2->session);
+ if (ret < 0) {
+ mlwarning("failed nghttp2_session_send: %s", nghttp2_strerror(ret));
+ _output_dnssim_conn_close(conn);
+ return;
+ }
+}
+
+int _output_dnssim_create_query_https2(output_dnssim_t* self, _output_dnssim_request_t* req)
+{
+ mlassert_self();
+ lassert(req, "req is nil");
+ lassert(req->client, "request must have a client associated with it");
+
+ _output_dnssim_query_tcp_t* qry;
+
+ lfatal_oom(qry = calloc(1, sizeof(_output_dnssim_query_tcp_t)));
+
+ qry->qry.transport = OUTPUT_DNSSIM_TRANSPORT_HTTPS2;
+ qry->qry.req = req;
+ qry->qry.state = _OUTPUT_DNSSIM_QUERY_PENDING_WRITE;
+ qry->stream_id = -1;
+ req->qry = &qry->qry; // TODO change when adding support for multiple Qs for req
+ _ll_append(req->client->pending, &qry->qry);
+
+ return _output_dnssim_handle_pending_queries(req->client);
+}
+
+void _output_dnssim_close_query_https2(_output_dnssim_query_tcp_t* qry)
+{
+ mlassert(qry, "qry can't be null");
+ mlassert(qry->qry.req, "query must be part of a request");
+ _output_dnssim_request_t* req = qry->qry.req;
+ mlassert(req->client, "request must belong to a client");
+
+ _ll_try_remove(req->client->pending, &qry->qry);
+ if (qry->conn) {
+ _output_dnssim_connection_t* conn = qry->conn;
+ _ll_try_remove(conn->sent, &qry->qry);
+ qry->conn = NULL;
+ _output_dnssim_conn_idle(conn);
+ }
+
+ if (qry->recv_buf != NULL)
+ free(qry->recv_buf);
+
+ _ll_remove(req->qry, &qry->qry);
+ free(qry);
+}
+
+void _output_dnssim_https2_close(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn can't be nil");
+ mlassert(conn->http2, "conn must have http2 ctx");
+
+ nghttp2_session_del(conn->http2->session);
+ _output_dnssim_tls_close(conn);
+}
+
+static int _http2_send_query_get(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry)
+{
+ mlassert(conn, "conn can't be null");
+ mlassert(qry, "qry can't be null");
+ mlassert(qry->qry.req, "req can't be null");
+ mlassert(qry->qry.req->payload, "payload can't be null");
+ mlassert(qry->qry.req->payload->len <= MAX_DNSMSG_SIZE, "payload too big");
+ mlassert(conn->client, "conn must be associated with client");
+ mlassert(conn->client->dnssim, "client must have dnssim");
+
+ output_dnssim_t* self = conn->client->dnssim;
+ core_object_payload_t* content = qry->qry.req->payload;
+
+ const size_t uri_path_len = strlen(_self->h2_uri_path);
+ const size_t path_len = uri_path_len
+ + OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN
+ + (content->len * 4) / 3 + 3 /* upper limit of base64 encoding */
+ + 1; /* terminating null byte */
+ if (path_len >= _MAX_URI_LEN) {
+ self->discarded++;
+ linfo("http2: uri path with query too long, query discarded");
+ return 0;
+ }
+ char path[path_len];
+ memcpy(path, _self->h2_uri_path, uri_path_len);
+ memcpy(&path[uri_path_len], OUTPUT_DNSSIM_HTTP_GET_TEMPLATE, OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN);
+
+ int32_t ret = base64url_encode(content->payload, content->len,
+ (uint8_t*)&path[uri_path_len + OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN],
+ sizeof(path) - uri_path_len - OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN - 1);
+ if (ret < 0) {
+ self->discarded++;
+ linfo("http2: base64url encode of query failed, query discarded");
+ return 0;
+ }
+
+ nghttp2_nv hdrs[] = {
+ OUTPUT_DNSSIM_MAKE_NV2(":method", "GET"),
+ OUTPUT_DNSSIM_MAKE_NV2(":scheme", "https"),
+ OUTPUT_DNSSIM_MAKE_NV(":authority", _self->h2_uri_authority, strlen(_self->h2_uri_authority)),
+ OUTPUT_DNSSIM_MAKE_NV(":path", path, uri_path_len + sizeof(OUTPUT_DNSSIM_HTTP_GET_TEMPLATE) - 1 + ret),
+ OUTPUT_DNSSIM_MAKE_NV2("accept", "application/dns-message"),
+ };
+
+ qry->stream_id = nghttp2_submit_request(conn->http2->session, NULL, hdrs, sizeof(hdrs) / sizeof(nghttp2_nv), NULL, NULL);
+
+ if (qry->stream_id < 0) {
+ mldebug("http2 (%p): failed to submit request: %s", conn->http2->session, nghttp2_strerror(qry->stream_id));
+ return -1;
+ }
+ mldebug("http2 (%p): GET %s", conn->http2->session, path);
+ conn->http2->open_streams++;
+ _http2_check_max_streams(conn);
+
+ ret = nghttp2_session_send(conn->http2->session);
+ if (ret < 0) {
+ mldebug("http2 (%p): failed session send: %s", conn->http2->session, nghttp2_strerror(ret));
+ return -1;
+ }
+
+ return 0;
+}
+
+static int _http2_send_query_post(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry)
+{
+ mlassert(conn, "conn can't be null");
+ mlassert(qry, "qry can't be null");
+ mlassert(qry->qry.req, "req can't be null");
+ mlassert(qry->qry.req->payload, "payload can't be null");
+ mlassert(qry->qry.req->payload->len <= MAX_DNSMSG_SIZE, "payload too big");
+ mlassert(conn->client, "conn must be associated with client");
+ mlassert(conn->client->dnssim, "client must have dnssim");
+
+ output_dnssim_t* self = conn->client->dnssim;
+
+ core_object_payload_t* content = qry->qry.req->payload;
+
+ int window_size = nghttp2_session_get_remote_window_size(conn->http2->session);
+ if (content->len > window_size) {
+ mldebug("http2 (%p): insufficient remote window size, deferring", conn->http2->session);
+ return 0;
+ }
+
+ char content_length[6]; /* max dnslen "65535" */
+ int content_length_len = snprintf(content_length, 6, "%zd", content->len);
+
+ nghttp2_nv hdrs[] = {
+ OUTPUT_DNSSIM_MAKE_NV2(":method", "POST"),
+ OUTPUT_DNSSIM_MAKE_NV2(":scheme", "https"),
+ OUTPUT_DNSSIM_MAKE_NV(":authority", _self->h2_uri_authority, strlen(_self->h2_uri_authority)),
+ OUTPUT_DNSSIM_MAKE_NV(":path", _self->h2_uri_path, strlen(_self->h2_uri_path)),
+ OUTPUT_DNSSIM_MAKE_NV2("accept", "application/dns-message"),
+ OUTPUT_DNSSIM_MAKE_NV2("content-type", "application/dns-message"),
+ OUTPUT_DNSSIM_MAKE_NV("content-length", content_length, content_length_len)
+ };
+
+ _output_dnssim_https2_data_provider_t data = {
+ .buf = content->payload,
+ .len = content->len
+ };
+
+ nghttp2_data_provider data_provider = {
+ .source.ptr = &data,
+ .read_callback = _http2_on_data_provider_read
+ };
+
+ qry->stream_id = nghttp2_submit_request(conn->http2->session, NULL, hdrs, sizeof(hdrs) / sizeof(nghttp2_nv), &data_provider, NULL);
+
+ if (qry->stream_id < 0) {
+ mldebug("http2 (%p): failed to submit request: %s", conn->http2->session, nghttp2_strerror(qry->stream_id));
+ return -1;
+ }
+ mldebug("http2 (%p): POST payload len=%ld", conn->http2->session, content->len);
+ conn->http2->open_streams++;
+ _http2_check_max_streams(conn);
+
+ window_size = nghttp2_session_get_stream_remote_window_size(conn->http2->session, qry->stream_id);
+ mlassert(content->len <= window_size,
+ "unsupported: http2 stream window size (%ld B) is smaller than dns payload (%ld B)",
+ window_size, content->len);
+
+ int ret = nghttp2_session_send(conn->http2->session);
+ if (ret < 0) {
+ mldebug("http2 (%p): failed session send: %s", conn->http2->session, nghttp2_strerror(ret));
+ return -1;
+ }
+
+ return 0;
+}
+
+void _output_dnssim_https2_write_query(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry)
+{
+ mlassert(qry, "qry can't be null");
+ mlassert(qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_WRITE, "qry must be pending write");
+ mlassert(conn, "conn can't be null");
+ mlassert(conn->state == _OUTPUT_DNSSIM_CONN_ACTIVE, "connection state != ACTIVE");
+ mlassert(conn->http2, "conn must have http2 ctx");
+ mlassert(conn->http2->session, "conn must have http2 session");
+ mlassert(conn->client, "conn must be associated with client");
+ mlassert(conn->client->pending, "conn has no pending queries");
+ mlassert(conn->client->dnssim, "client must have dnssim");
+
+ int ret = 0;
+ output_dnssim_t* self = conn->client->dnssim;
+
+ if (!nghttp2_session_check_request_allowed(conn->http2->session)) {
+ mldebug("http2 (%p): request not allowed", conn->http2->session);
+ _output_dnssim_conn_close(conn);
+ return;
+ }
+
+ switch (_self->h2_method) {
+ case OUTPUT_DNSSIM_H2_POST:
+ ret = _http2_send_query_post(conn, qry);
+ break;
+ case OUTPUT_DNSSIM_H2_GET:
+ ret = _http2_send_query_get(conn, qry);
+ break;
+ default:
+ lfatal("http2: unsupported method");
+ }
+
+ if (ret < 0) {
+ _output_dnssim_conn_close(conn);
+ return;
+ }
+
+ qry->conn = conn;
+ _ll_remove(conn->client->pending, &qry->qry);
+ _ll_append(conn->sent, &qry->qry);
+
+ /* Stop idle timer, since there are queries to answer now. */
+ if (conn->idle_timer != NULL) {
+ conn->is_idle = false;
+ uv_timer_stop(conn->idle_timer);
+ }
+
+ qry->qry.state = _OUTPUT_DNSSIM_QUERY_SENT;
+}
+
+#endif
diff --git a/src/output/dnssim/internal.h b/src/output/dnssim/internal.h
new file mode 100644
index 0000000..b9feddf
--- /dev/null
+++ b/src/output/dnssim/internal.h
@@ -0,0 +1,343 @@
+/*
+ * Copyright (c) 2019-2020, CZ.NIC, z.s.p.o.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __dnsjit_output_dnssim_internal_h
+#define __dnsjit_output_dnssim_internal_h
+
+#include <gnutls/gnutls.h>
+#include <nghttp2/nghttp2.h>
+#include <uv.h>
+#include "core/object/dns.h"
+#include "core/object/payload.h"
+
+#define DNSSIM_MIN_GNUTLS_VERSION 0x030603
+#define DNSSIM_MIN_GNUTLS_ERRORMSG "dnssim tls/https2 transport requires GnuTLS >= 3.6.3"
+
+#define _self ((_output_dnssim_t*)self)
+#define _ERR_MALFORMED -2
+#define _ERR_MSGID -3
+#define _ERR_TC -4
+#define _ERR_QUESTION -5
+
+#define _MAX_URI_LEN 65536
+#define MAX_DNSMSG_SIZE 65535
+#define WIRE_BUF_SIZE (MAX_DNSMSG_SIZE + 2 + 16384) /** max tcplen + 2b tcplen + 16kb tls record */
+
+typedef struct _output_dnssim_request _output_dnssim_request_t;
+typedef struct _output_dnssim_connection _output_dnssim_connection_t;
+typedef struct _output_dnssim_client _output_dnssim_client_t;
+
+/*
+ * Query-related structures.
+ */
+
+typedef struct _output_dnssim_query _output_dnssim_query_t;
+struct _output_dnssim_query {
+ /*
+ * Next query in the list.
+ *
+ * Currently, next is used for TCP clients/connection, which makes it
+ * impossible to use for tracking multiple queries of a single request.
+ *
+ * TODO: refactor the linked lists to allow query to be part of multiple lists
+ */
+ _output_dnssim_query_t* next;
+
+ output_dnssim_transport_t transport;
+ _output_dnssim_request_t* req;
+
+ /* Query state, currently used only for TCP. */
+ enum {
+ _OUTPUT_DNSSIM_QUERY_PENDING_WRITE,
+ _OUTPUT_DNSSIM_QUERY_PENDING_WRITE_CB,
+ _OUTPUT_DNSSIM_QUERY_PENDING_CLOSE,
+ _OUTPUT_DNSSIM_QUERY_WRITE_FAILED,
+ _OUTPUT_DNSSIM_QUERY_SENT,
+ _OUTPUT_DNSSIM_QUERY_ORPHANED
+ } state;
+};
+
+typedef struct _output_dnssim_query_udp _output_dnssim_query_udp_t;
+struct _output_dnssim_query_udp {
+ _output_dnssim_query_t qry;
+
+ uv_udp_t* handle;
+ uv_buf_t buf;
+};
+
+typedef struct _output_dnssim_query_tcp _output_dnssim_query_tcp_t;
+struct _output_dnssim_query_tcp {
+ _output_dnssim_query_t qry;
+
+ /* Connection this query is assigned to. */
+ _output_dnssim_connection_t* conn;
+
+ uv_write_t write_req;
+
+ /* Send buffers for libuv; 0 is for dnslen, 1 is for dnsmsg. */
+ uv_buf_t bufs[2];
+
+ /* HTTP/2 stream id that was used to send this query. */
+ int32_t stream_id;
+
+ /* HTTP/2 expected content length. */
+ int32_t content_len;
+
+ /* Receive buffer (currently used only by HTTP/2). */
+ uint8_t* recv_buf;
+ ssize_t recv_buf_len;
+};
+
+struct _output_dnssim_request {
+ /* List of queries associated with this request. */
+ _output_dnssim_query_t* qry;
+
+ /* Client this request belongs to. */
+ _output_dnssim_client_t* client;
+
+ /* The DNS question to be resolved. */
+ core_object_payload_t* payload;
+ core_object_dns_t* dns_q;
+ const uint8_t* question;
+ ssize_t question_len;
+
+ /* Timestamps for latency calculation. */
+ uint64_t created_at;
+ uint64_t ended_at;
+
+ /* Timer for tracking timeout of the request. */
+ uv_timer_t* timer;
+
+ /* The output component of this request. */
+ output_dnssim_t* dnssim;
+
+ /* State of the request. */
+ enum {
+ _OUTPUT_DNSSIM_REQ_ONGOING,
+ _OUTPUT_DNSSIM_REQ_CLOSING
+ } state;
+
+ /* Statistics interval in which this request is tracked. */
+ output_dnssim_stats_t* stats;
+};
+
+/*
+ * Connection-related structures.
+ */
+
+/* Read-state of connection's data stream. */
+typedef enum _output_dnssim_read_state {
+ _OUTPUT_DNSSIM_READ_STATE_CLEAN,
+ _OUTPUT_DNSSIM_READ_STATE_DNSLEN, /* Expecting bytes of dnslen. */
+ _OUTPUT_DNSSIM_READ_STATE_DNSMSG, /* Expecting bytes of dnsmsg. */
+ _OUTPUT_DNSSIM_READ_STATE_INVALID
+} _output_dnssim_read_state_t;
+
+/* TLS-related data for a single connection. */
+typedef struct _output_dnssim_tls_ctx {
+ gnutls_session_t session;
+ uint8_t* buf;
+ ssize_t buf_len;
+ ssize_t buf_pos;
+ size_t write_queue_size;
+} _output_dnssim_tls_ctx_t;
+
+/* HTTP2 context for a single connection. */
+typedef struct _output_dnssim_http2_ctx {
+ nghttp2_session* session;
+
+ /* Query to which the dnsbuf currently being processed belongs to. */
+ _output_dnssim_query_tcp_t* current_qry;
+
+ /* Maximum number of concurrent and currently open streams. */
+ uint32_t max_concurrent_streams;
+ uint32_t open_streams;
+
+ /* Flag indicating whether we received the peer's initial SETTINGS frame. */
+ bool remote_settings_received;
+} _output_dnssim_http2_ctx_t;
+
+struct _output_dnssim_connection {
+ _output_dnssim_connection_t* next;
+
+ uv_tcp_t* handle;
+
+ /* Timeout timer for establishing the connection. */
+ uv_timer_t* handshake_timer;
+
+ /* Idle timer for connection reuse. rfc7766#section-6.2.3 */
+ uv_timer_t* idle_timer;
+ bool is_idle;
+
+ /* List of queries that have been queued (pending write callback). */
+ _output_dnssim_query_t* queued;
+
+ /* List of queries that have been sent over this connection. */
+ _output_dnssim_query_t* sent;
+
+ /* Client this connection belongs to. */
+ _output_dnssim_client_t* client;
+
+ /* State of the connection.
+ * Numeric ordering of constants is significant and follows the typical connection lifecycle.
+ * Ensure new states are added to a proper place. */
+ enum {
+ _OUTPUT_DNSSIM_CONN_INITIALIZED = 0,
+ _OUTPUT_DNSSIM_CONN_TCP_HANDSHAKE = 10,
+ _OUTPUT_DNSSIM_CONN_TLS_HANDSHAKE = 20,
+ _OUTPUT_DNSSIM_CONN_ACTIVE = 30,
+ _OUTPUT_DNSSIM_CONN_CONGESTED = 35,
+ _OUTPUT_DNSSIM_CONN_CLOSE_REQUESTED = 38,
+ _OUTPUT_DNSSIM_CONN_CLOSING = 40,
+ _OUTPUT_DNSSIM_CONN_CLOSED = 50
+ } state;
+
+ /* State of the data stream read. */
+ _output_dnssim_read_state_t read_state;
+
+ /* Total length of the expected dns data (either 2 for dnslen, or dnslen itself). */
+ size_t dnsbuf_len;
+
+ /* Current position in the receive dns buffer. */
+ size_t dnsbuf_pos;
+
+ /* Receive buffer used for incomplete messages or dnslen. */
+ char* dnsbuf_data;
+ bool dnsbuf_free_after_use;
+
+ /* Statistics interval in which the handshake is tracked. */
+ output_dnssim_stats_t* stats;
+
+ /* TLS-related data. */
+ _output_dnssim_tls_ctx_t* tls;
+
+ /* HTTP/2-related data. */
+ _output_dnssim_http2_ctx_t* http2;
+
+ /* Prevents immediate closure of connection. Instead, connection is moved
+ * to CLOSE_REQUESTED state and setter of this flag is responsible for
+ * closing the connection when clearing this flag. */
+ bool prevent_close;
+};
+
+/*
+ * Client structure.
+ */
+
+struct _output_dnssim_client {
+ /* Dnssim component this client belongs to. */
+ output_dnssim_t* dnssim;
+
+ /* List of connections.
+ * Multiple connections may be used (e.g. some are already closed for writing).
+ */
+ _output_dnssim_connection_t* conn;
+
+ /* List of queries that are pending to be sent over any available connection. */
+ _output_dnssim_query_t* pending;
+
+ /* TLS-ticket for session resumption. */
+ gnutls_datum_t tls_ticket;
+};
+
+/*
+ * DnsSim-related structures.
+ */
+
+typedef struct _output_dnssim_source _output_dnssim_source_t;
+struct _output_dnssim_source {
+ _output_dnssim_source_t* next;
+ struct sockaddr_storage addr;
+};
+
+typedef struct _output_dnssim _output_dnssim_t;
+struct _output_dnssim {
+ output_dnssim_t pub;
+
+ uv_loop_t loop;
+ uv_timer_t stats_timer;
+
+ struct sockaddr_storage target;
+ _output_dnssim_source_t* source;
+ output_dnssim_transport_t transport;
+
+ char h2_uri_authority[_MAX_URI_LEN];
+ char h2_uri_path[_MAX_URI_LEN];
+ bool h2_zero_out_msgid;
+ output_dnssim_h2_method_t h2_method;
+
+ /* Array of clients, mapped by client ID (ranges from 0 to max_clients). */
+ _output_dnssim_client_t* client_arr;
+
+ gnutls_priority_t* tls_priority;
+ gnutls_certificate_credentials_t tls_cred;
+ char wire_buf[WIRE_BUF_SIZE]; /* thread-local buffer for processing tls input */
+};
+
+/* Provides data for HTTP/2 data frames. */
+typedef struct {
+ const uint8_t* buf;
+ size_t len;
+} _output_dnssim_https2_data_provider_t;
+
+/*
+ * Forward function declarations.
+ */
+
+int _output_dnssim_bind_before_connect(output_dnssim_t* self, uv_handle_t* handle);
+int _output_dnssim_create_query_udp(output_dnssim_t* self, _output_dnssim_request_t* req);
+int _output_dnssim_create_query_tcp(output_dnssim_t* self, _output_dnssim_request_t* req);
+void _output_dnssim_close_query_udp(_output_dnssim_query_udp_t* qry);
+void _output_dnssim_close_query_tcp(_output_dnssim_query_tcp_t* qry);
+int _output_dnssim_answers_request(_output_dnssim_request_t* req, core_object_dns_t* response);
+void _output_dnssim_request_answered(_output_dnssim_request_t* req, core_object_dns_t* msg);
+void _output_dnssim_maybe_free_request(_output_dnssim_request_t* req);
+void _output_dnssim_on_uv_alloc(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf);
+void _output_dnssim_create_request(output_dnssim_t* self, _output_dnssim_client_t* client, core_object_payload_t* payload);
+int _output_dnssim_handle_pending_queries(_output_dnssim_client_t* client);
+int _output_dnssim_tcp_connect(output_dnssim_t* self, _output_dnssim_connection_t* conn);
+void _output_dnssim_tcp_close(_output_dnssim_connection_t* conn);
+void _output_dnssim_tcp_write_query(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry);
+void _output_dnssim_conn_close(_output_dnssim_connection_t* conn);
+void _output_dnssim_conn_idle(_output_dnssim_connection_t* conn);
+int _output_dnssim_handle_pending_queries(_output_dnssim_client_t* client);
+void _output_dnssim_conn_activate(_output_dnssim_connection_t* conn);
+void _output_dnssim_conn_maybe_free(_output_dnssim_connection_t* conn);
+void _output_dnssim_read_dns_stream(_output_dnssim_connection_t* conn, size_t len, const char* data);
+void _output_dnssim_read_dnsmsg(_output_dnssim_connection_t* conn, size_t len, const char* data);
+
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+int _output_dnssim_create_query_tls(output_dnssim_t* self, _output_dnssim_request_t* req);
+void _output_dnssim_close_query_tls(_output_dnssim_query_tcp_t* qry);
+int _output_dnssim_tls_init(_output_dnssim_connection_t* conn);
+void _output_dnssim_tls_process_input_data(_output_dnssim_connection_t* conn);
+void _output_dnssim_tls_close(_output_dnssim_connection_t* conn);
+void _output_dnssim_tls_write_query(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry);
+
+int _output_dnssim_create_query_https2(output_dnssim_t* self, _output_dnssim_request_t* req);
+void _output_dnssim_close_query_https2(_output_dnssim_query_tcp_t* qry);
+int _output_dnssim_https2_init(_output_dnssim_connection_t* conn);
+int _output_dnssim_https2_setup(_output_dnssim_connection_t* conn);
+void _output_dnssim_https2_process_input_data(_output_dnssim_connection_t* conn, size_t len, const char* data);
+void _output_dnssim_https2_close(_output_dnssim_connection_t* conn);
+void _output_dnssim_https2_write_query(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry);
+#endif
+
+#endif
diff --git a/src/output/dnssim/ll.h b/src/output/dnssim/ll.h
new file mode 100644
index 0000000..8e0b07a
--- /dev/null
+++ b/src/output/dnssim/ll.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2019-2020, CZ.NIC, z.s.p.o.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __dnsjit_output_dnssim_ll_h
+#define __dnsjit_output_dnssim_ll_h
+
+#include "core/assert.h"
+
+/* Utility macros for linked list structures.
+ *
+ * - "list" is the pointer to the first node of the linked list
+ * - "list" can be NULL if there are no nodes
+ * - every node has "next", which points to the next node (can be NULL)
+ */
+
+/* Append a node to the list.
+ *
+ * Only a single node can be appended - node->next must be NULL.
+ */
+#define _ll_append(list, node) \
+ { \
+ glassert((node)->next == NULL, "node->next must be null when appending"); \
+ if ((list) == NULL) \
+ (list) = (node); \
+ else if ((node) != NULL) { \
+ typeof(list) _current = (list); \
+ while (_current->next != NULL) \
+ _current = _current->next; \
+ _current->next = node; \
+ } \
+ }
+
+/* Remove a node from the list.
+ *
+ * In strict mode, the node must be present in the list.
+ */
+#define _ll_remove_template(list, node, strict) \
+ { \
+ if (strict) \
+ glassert((list), "list can't be null when removing nodes"); \
+ if ((list) != NULL && (node) != NULL) { \
+ if ((list) == (node)) { \
+ (list) = (node)->next; \
+ (node)->next = NULL; \
+ } else { \
+ typeof(list) _current = (list); \
+ while (_current != NULL && _current->next != (node)) { \
+ if (strict) \
+ glassert((_current->next), "list doesn't contain the node to be removed"); \
+ _current = _current->next; \
+ } \
+ if (_current != NULL) { \
+ _current->next = (node)->next; \
+ (node)->next = NULL; \
+ } \
+ } \
+ } \
+ }
+
+/* Remove a node from the list. */
+#define _ll_remove(list, node) _ll_remove_template((list), (node), true)
+
+/* Remove a node from the list if it's present. */
+#define _ll_try_remove(list, node) _ll_remove_template((list), (node), false)
+
+#endif
diff --git a/src/output/dnssim/tcp.c b/src/output/dnssim/tcp.c
new file mode 100644
index 0000000..1f2b619
--- /dev/null
+++ b/src/output/dnssim/tcp.c
@@ -0,0 +1,356 @@
+/*
+ * Copyright (c) 2019-2020, CZ.NIC, z.s.p.o.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "output/dnssim.h"
+#include "output/dnssim/internal.h"
+#include "output/dnssim/ll.h"
+#include "core/assert.h"
+
+#include <string.h>
+
+static core_log_t _log = LOG_T_INIT("output.dnssim");
+
+static void _move_queries_to_pending(_output_dnssim_query_tcp_t* qry)
+{
+ _output_dnssim_query_tcp_t* qry_tmp;
+ while (qry != NULL) {
+ mlassert(qry->conn, "query must be associated with conn");
+ mlassert(qry->conn->state == _OUTPUT_DNSSIM_CONN_CLOSED, "conn must be closed");
+ mlassert(qry->conn->client, "conn must be associated with client");
+ qry_tmp = (_output_dnssim_query_tcp_t*)qry->qry.next;
+ qry->qry.next = NULL;
+ _ll_append(qry->conn->client->pending, &qry->qry);
+ qry->conn = NULL;
+ qry->qry.state = _OUTPUT_DNSSIM_QUERY_ORPHANED;
+ qry->stream_id = -1;
+ qry->recv_buf_len = 0;
+ if (qry->recv_buf != NULL) {
+ free(qry->recv_buf);
+ qry->recv_buf = NULL;
+ }
+ qry = qry_tmp;
+ }
+}
+
+static void _on_tcp_closed(uv_handle_t* handle)
+{
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)handle->data;
+ mlassert(conn, "conn is nil");
+ conn->state = _OUTPUT_DNSSIM_CONN_CLOSED;
+
+ /* Orphan any queries that are still unresolved. */
+ _move_queries_to_pending((_output_dnssim_query_tcp_t*)conn->queued);
+ conn->queued = NULL;
+ _move_queries_to_pending((_output_dnssim_query_tcp_t*)conn->sent);
+ conn->sent = NULL;
+
+ /* TODO Improve client re-connect behavior in case the connection fails to
+ * establish. Currently, queries are orphaned and attempted to be re-sent
+ * along with the next query that triggers a new connection.
+ *
+ * Attempting to establish new connection immediately leads to performance
+ * issues if the number of these attempts doesn't have upper limit. */
+ ///* Ensure orhpaned queries are re-sent over a different connection. */
+ //if (_output_dnssim_handle_pending_queries(conn->client) != 0)
+ // mlinfo("tcp: orphaned queries failed to be re-sent");
+
+ mlassert(conn->handle, "conn must have tcp handle when closing it");
+ free(conn->handle);
+ conn->handle = NULL;
+ _output_dnssim_conn_maybe_free(conn);
+}
+
+static void _on_tcp_query_written(uv_write_t* wr_req, int status)
+{
+ _output_dnssim_query_tcp_t* qry = (_output_dnssim_query_tcp_t*)wr_req->data;
+ mlassert(qry, "qry/wr_req->data is nil");
+ mlassert(qry->conn, "query must be associated with connection");
+ _output_dnssim_connection_t* conn = qry->conn;
+
+ free(((_output_dnssim_query_tcp_t*)qry)->bufs[0].base);
+
+ if (qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_CLOSE) {
+ qry->qry.state = status < 0 ? _OUTPUT_DNSSIM_QUERY_WRITE_FAILED : _OUTPUT_DNSSIM_QUERY_SENT;
+ _output_dnssim_request_t* req = qry->qry.req;
+ _output_dnssim_close_query_tcp(qry);
+ _output_dnssim_maybe_free_request(req);
+ qry = NULL;
+ }
+
+ if (status < 0) {
+ if (status != UV_ECANCELED)
+ mlinfo("tcp write failed: %s", uv_strerror(status));
+ if (qry != NULL)
+ qry->qry.state = _OUTPUT_DNSSIM_QUERY_WRITE_FAILED;
+ _output_dnssim_conn_close(conn);
+ return;
+ }
+
+ if (qry == NULL)
+ return;
+
+ /* Mark query as sent and assign it to connection. */
+ mlassert(qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_WRITE_CB, "invalid query state");
+ qry->qry.state = _OUTPUT_DNSSIM_QUERY_SENT;
+ if (qry->conn->state == _OUTPUT_DNSSIM_CONN_ACTIVE) {
+ mlassert(qry->conn->queued, "conn has no queued queries");
+ _ll_remove(qry->conn->queued, &qry->qry);
+ _ll_append(qry->conn->sent, &qry->qry);
+ }
+}
+
+void _output_dnssim_tcp_write_query(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry)
+{
+ mlassert(qry, "qry can't be null");
+ mlassert(qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_WRITE, "qry must be pending write");
+ mlassert(qry->qry.req, "req can't be null");
+ mlassert(qry->qry.req->dns_q, "dns_q can't be null");
+ mlassert(qry->qry.req->dns_q->obj_prev, "payload can't be null");
+ mlassert(conn, "conn can't be null");
+ mlassert(conn->state == _OUTPUT_DNSSIM_CONN_ACTIVE, "connection state != ACTIVE");
+ mlassert(conn->client, "conn must be associated with client");
+ mlassert(conn->client->pending, "conn has no pending queries");
+
+ mldebug("tcp write dnsmsg id: %04x", qry->qry.req->dns_q->id);
+
+ core_object_payload_t* payload = (core_object_payload_t*)qry->qry.req->dns_q->obj_prev;
+ uint16_t* len;
+ mlfatal_oom(len = malloc(sizeof(uint16_t)));
+ *len = htons(payload->len);
+ qry->bufs[0] = uv_buf_init((char*)len, 2);
+ qry->bufs[1] = uv_buf_init((char*)payload->payload, payload->len);
+
+ qry->conn = conn;
+ _ll_remove(conn->client->pending, &qry->qry);
+ _ll_append(conn->queued, &qry->qry);
+
+ /* Stop idle timer, since there are queries to answer now. */
+ if (conn->idle_timer != NULL) {
+ conn->is_idle = false;
+ uv_timer_stop(conn->idle_timer);
+ }
+
+ qry->write_req.data = (void*)qry;
+ uv_write(&qry->write_req, (uv_stream_t*)conn->handle, qry->bufs, 2, _on_tcp_query_written);
+ qry->qry.state = _OUTPUT_DNSSIM_QUERY_PENDING_WRITE_CB;
+}
+
+static void _on_tcp_read(uv_stream_t* handle, ssize_t nread, const uv_buf_t* buf)
+{
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)handle->data;
+ output_dnssim_t* self = conn->client->dnssim;
+
+ if (nread > 0) {
+ mldebug("tcp nread: %d", nread);
+ switch (_self->transport) {
+ case OUTPUT_DNSSIM_TRANSPORT_TCP:
+ _output_dnssim_read_dns_stream(conn, nread, buf->base);
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_TLS:
+ case OUTPUT_DNSSIM_TRANSPORT_HTTPS2:
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ mlassert(conn->tls, "con must have tls ctx");
+ conn->tls->buf = (uint8_t*)buf->base;
+ conn->tls->buf_pos = 0;
+ conn->tls->buf_len = nread;
+ _output_dnssim_tls_process_input_data(conn);
+#else
+ mlfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ break;
+ default:
+ mlfatal("unsupported transport");
+ break;
+ }
+ } else if (nread < 0) {
+ if (nread != UV_EOF)
+ mlinfo("tcp conn unexpected close: %s", uv_strerror(nread));
+ _output_dnssim_conn_close(conn);
+ }
+
+ if (buf->base != NULL)
+ free(buf->base);
+}
+
+static void _on_tcp_connected(uv_connect_t* conn_req, int status)
+{
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)conn_req->handle->data;
+ mlassert(conn, "conn is nil");
+
+ free(conn_req);
+
+ if (status < 0) {
+ mldebug("tcp connect failed: %s", uv_strerror(status));
+ _output_dnssim_conn_close(conn);
+ return;
+ }
+
+ mlassert(conn->state == _OUTPUT_DNSSIM_CONN_TCP_HANDSHAKE, "connection state != TCP_HANDSHAKE");
+ int ret = uv_read_start((uv_stream_t*)conn->handle, _output_dnssim_on_uv_alloc, _on_tcp_read);
+ if (ret < 0) {
+ mlwarning("tcp uv_read_start() failed: %s", uv_strerror(ret));
+ _output_dnssim_conn_close(conn);
+ return;
+ }
+
+ mldebug("tcp connected");
+ mlassert(conn->client, "conn must be associated with a client");
+ mlassert(conn->client->dnssim, "client must be associated with dnssim");
+ output_dnssim_t* self = conn->client->dnssim;
+ switch (_self->transport) {
+ case OUTPUT_DNSSIM_TRANSPORT_TCP:
+ _output_dnssim_conn_activate(conn);
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_TLS:
+ case OUTPUT_DNSSIM_TRANSPORT_HTTPS2:
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+ mldebug("init tls handshake");
+ _output_dnssim_tls_process_input_data(conn); /* Initiate TLS handshake. */
+#else
+ mlfatal(DNSSIM_MIN_GNUTLS_ERRORMSG);
+#endif
+ break;
+ default:
+ lfatal("unsupported transport protocol");
+ break;
+ }
+}
+
+static void _on_connection_timeout(uv_timer_t* handle)
+{
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)handle->data;
+ _output_dnssim_conn_close(conn);
+}
+
+int _output_dnssim_tcp_connect(output_dnssim_t* self, _output_dnssim_connection_t* conn)
+{
+ mlassert_self();
+ lassert(conn, "connection can't be null");
+ lassert(conn->handle == NULL, "connection already has a handle");
+ lassert(conn->handshake_timer == NULL, "connection already has a handshake timer");
+ lassert(conn->idle_timer == NULL, "connection already has idle timer");
+ lassert(conn->state == _OUTPUT_DNSSIM_CONN_INITIALIZED, "connection state != INITIALIZED");
+
+ lfatal_oom(conn->handle = malloc(sizeof(uv_tcp_t)));
+ conn->handle->data = (void*)conn;
+ int ret = uv_tcp_init(&_self->loop, conn->handle);
+ if (ret < 0) {
+ lwarning("failed to init uv_tcp_t");
+ goto failure;
+ }
+
+ ret = _output_dnssim_bind_before_connect(self, (uv_handle_t*)conn->handle);
+ if (ret < 0)
+ goto failure;
+
+ /* Set connection parameters. */
+ ret = uv_tcp_nodelay(conn->handle, 1);
+ if (ret < 0)
+ lwarning("tcp: failed to set TCP_NODELAY: %s", uv_strerror(ret));
+
+ /* Set connection handshake timeout. */
+ lfatal_oom(conn->handshake_timer = malloc(sizeof(uv_timer_t)));
+ uv_timer_init(&_self->loop, conn->handshake_timer);
+ conn->handshake_timer->data = (void*)conn;
+ uv_timer_start(conn->handshake_timer, _on_connection_timeout, self->handshake_timeout_ms, 0);
+
+ /* Set idle connection timer. */
+ if (self->idle_timeout_ms > 0) {
+ lfatal_oom(conn->idle_timer = malloc(sizeof(uv_timer_t)));
+ uv_timer_init(&_self->loop, conn->idle_timer);
+ conn->idle_timer->data = (void*)conn;
+
+ /* Start and stop the timer to set the repeat value without running the timer. */
+ uv_timer_start(conn->idle_timer, _on_connection_timeout, self->idle_timeout_ms, self->idle_timeout_ms);
+ uv_timer_stop(conn->idle_timer);
+ }
+
+ mldebug("tcp connecting");
+ uv_connect_t* conn_req;
+ lfatal_oom(conn_req = malloc(sizeof(uv_connect_t)));
+ ret = uv_tcp_connect(conn_req, conn->handle, (struct sockaddr*)&_self->target, _on_tcp_connected);
+ if (ret < 0)
+ goto failure;
+
+ conn->stats->conn_handshakes++;
+ conn->client->dnssim->stats_sum->conn_handshakes++;
+ conn->state = _OUTPUT_DNSSIM_CONN_TCP_HANDSHAKE;
+ return 0;
+failure:
+ _output_dnssim_conn_close(conn);
+ return ret;
+}
+
+void _output_dnssim_tcp_close(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn can't be nil");
+
+ if (conn->handle != NULL) {
+ uv_read_stop((uv_stream_t*)conn->handle);
+ uv_close((uv_handle_t*)conn->handle, _on_tcp_closed);
+ }
+}
+
+int _output_dnssim_create_query_tcp(output_dnssim_t* self, _output_dnssim_request_t* req)
+{
+ mlassert_self();
+ lassert(req, "req is nil");
+ lassert(req->client, "request must have a client associated with it");
+
+ _output_dnssim_query_tcp_t* qry;
+
+ lfatal_oom(qry = calloc(1, sizeof(_output_dnssim_query_tcp_t)));
+
+ qry->qry.transport = OUTPUT_DNSSIM_TRANSPORT_TCP;
+ qry->qry.req = req;
+ qry->qry.state = _OUTPUT_DNSSIM_QUERY_PENDING_WRITE;
+ req->qry = &qry->qry; // TODO change when adding support for multiple Qs for req
+ _ll_append(req->client->pending, &qry->qry);
+
+ return _output_dnssim_handle_pending_queries(req->client);
+}
+
+void _output_dnssim_close_query_tcp(_output_dnssim_query_tcp_t* qry)
+{
+ mlassert(qry, "qry can't be null");
+ mlassert(qry->qry.req, "query must be part of a request");
+ _output_dnssim_request_t* req = qry->qry.req;
+ mlassert(req->client, "request must belong to a client");
+
+ if ((qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_WRITE_CB || qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_CLOSE)) {
+ /* Query can't be freed until uv callback is called. */
+ qry->qry.state = _OUTPUT_DNSSIM_QUERY_PENDING_CLOSE;
+ return;
+ }
+
+ _ll_try_remove(req->client->pending, &qry->qry);
+ if (qry->conn) {
+ _output_dnssim_connection_t* conn = qry->conn;
+ _ll_try_remove(conn->queued, &qry->qry); /* edge-case of cancelled queries */
+ _ll_try_remove(conn->sent, &qry->qry);
+ qry->conn = NULL;
+ _output_dnssim_conn_idle(conn);
+ }
+
+ _ll_remove(req->qry, &qry->qry);
+ free(qry);
+}
diff --git a/src/output/dnssim/tls.c b/src/output/dnssim/tls.c
new file mode 100644
index 0000000..e87ca47
--- /dev/null
+++ b/src/output/dnssim/tls.c
@@ -0,0 +1,475 @@
+/*
+ * Copyright (c) 2020, CZ.NIC, z.s.p.o.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "output/dnssim.h"
+#include "output/dnssim/internal.h"
+#include "output/dnssim/ll.h"
+#include "core/assert.h"
+
+#include <gnutls/gnutls.h>
+#include <string.h>
+
+#if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION
+
+#ifndef MIN
+#define MIN(a, b) (((a) < (b)) ? (a) : (b)) /** Minimum of two numbers **/
+#endif
+
+static core_log_t _log = LOG_T_INIT("output.dnssim");
+
+struct async_write_ctx {
+ uv_write_t write_req;
+ _output_dnssim_connection_t* conn;
+ char buf[];
+};
+
+static int _tls_handshake(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn is nil");
+ mlassert(conn->tls, "conn must have tls context");
+ mlassert(conn->client, "conn must belong to a client");
+ mlassert(conn->state <= _OUTPUT_DNSSIM_CONN_TLS_HANDSHAKE, "conn in invalid state");
+
+ /* Set TLS session resumption ticket if available. */
+ if (conn->state < _OUTPUT_DNSSIM_CONN_TLS_HANDSHAKE && conn->client->tls_ticket.size != 0) {
+ gnutls_datum_t* ticket = &conn->client->tls_ticket;
+ gnutls_session_set_data(conn->tls->session, ticket->data, ticket->size);
+ }
+ conn->state = _OUTPUT_DNSSIM_CONN_TLS_HANDSHAKE;
+
+ return gnutls_handshake(conn->tls->session);
+}
+
+void _output_dnssim_tls_process_input_data(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn is nil");
+ mlassert(conn->client, "conn must have client");
+ mlassert(conn->client->dnssim, "client must have dnssim");
+ mlassert(conn->tls, "conn must have tls ctx");
+
+ if (conn->state >= _OUTPUT_DNSSIM_CONN_CLOSING)
+ return;
+
+ output_dnssim_t* self = conn->client->dnssim;
+
+ /* Ensure TLS handshake is performed before receiving data.
+ * See https://www.gnutls.org/manual/html_node/TLS-handshake.html */
+ while (conn->state <= _OUTPUT_DNSSIM_CONN_TLS_HANDSHAKE) {
+ int err = _tls_handshake(conn);
+ mldebug("tls handshake returned: %s", gnutls_strerror(err));
+ if (err == GNUTLS_E_SUCCESS) {
+ if (gnutls_session_is_resumed(conn->tls->session))
+ conn->stats->conn_resumed++;
+ if (_self->transport == OUTPUT_DNSSIM_TRANSPORT_HTTPS2) {
+ if (_output_dnssim_https2_setup(conn) < 0) {
+ _output_dnssim_conn_close(conn);
+ return;
+ }
+ }
+ _output_dnssim_conn_activate(conn);
+ break;
+ } else if (err == GNUTLS_E_AGAIN) {
+ return; /* Wait for more data */
+ } else if (err == GNUTLS_E_FATAL_ALERT_RECEIVED) {
+ gnutls_alert_description_t alert = gnutls_alert_get(conn->tls->session);
+ mlwarning("gnutls_handshake failed: %s", gnutls_alert_get_name(alert));
+ _output_dnssim_conn_close(conn);
+ return;
+ } else if (gnutls_error_is_fatal(err)) {
+ mlwarning("gnutls_handshake failed: %s", gnutls_strerror_name(err));
+ _output_dnssim_conn_close(conn);
+ return;
+ }
+ }
+
+ /* See https://gnutls.org/manual/html_node/Data-transfer-and-termination.html#Data-transfer-and-termination */
+ while (true) {
+ /* Connection might have been closed due to an error, don't try to use it. */
+ if (conn->state < _OUTPUT_DNSSIM_CONN_ACTIVE || conn->state >= _OUTPUT_DNSSIM_CONN_CLOSING)
+ return;
+
+ ssize_t count = gnutls_record_recv(conn->tls->session, _self->wire_buf, WIRE_BUF_SIZE);
+ if (count > 0) {
+ switch (_self->transport) {
+ case OUTPUT_DNSSIM_TRANSPORT_TLS:
+ _output_dnssim_read_dns_stream(conn, count, _self->wire_buf);
+ break;
+ case OUTPUT_DNSSIM_TRANSPORT_HTTPS2:
+ _output_dnssim_https2_process_input_data(conn, count, _self->wire_buf);
+ break;
+ default:
+ lfatal("unsupported transport layer");
+ break;
+ }
+ } else if (count == GNUTLS_E_AGAIN) {
+ if (conn->tls->buf_pos == conn->tls->buf_len) {
+ /* See https://www.gnutls.org/manual/html_node/Asynchronous-operation.html */
+ break; /* No more data available in this libuv buffer */
+ }
+ continue;
+ } else if (count == GNUTLS_E_INTERRUPTED) {
+ continue;
+ } else if (count == GNUTLS_E_REHANDSHAKE) {
+ continue; /* Ignore rehandshake request. */
+ } else if (count < 0) {
+ mlwarning("gnutls_record_recv failed: %s", gnutls_strerror_name(count));
+ _output_dnssim_conn_close(conn);
+ return;
+ } else if (count == 0) {
+ break;
+ }
+ }
+ mlassert(conn->tls->buf_len == conn->tls->buf_pos, "tls didn't read the entire buffer");
+}
+
+static ssize_t _tls_pull(gnutls_transport_ptr_t ptr, void* buf, size_t len)
+{
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)ptr;
+ mlassert(conn != NULL, "conn is null");
+ mlassert(conn->tls != NULL, "conn must have tls ctx");
+
+ ssize_t avail = conn->tls->buf_len - conn->tls->buf_pos;
+ if (avail <= 0) {
+ mldebug("tls pull: no more data");
+ errno = EAGAIN;
+ return -1;
+ }
+
+ ssize_t transfer = MIN(avail, len);
+ memcpy(buf, conn->tls->buf + conn->tls->buf_pos, transfer);
+ conn->tls->buf_pos += transfer;
+ return transfer;
+}
+
+static void _tls_on_write_complete(uv_write_t* req, int status)
+{
+ mlassert(req->data != NULL, "uv_write req has no data pointer");
+ struct async_write_ctx* async_ctx = (struct async_write_ctx*)req->data;
+ _output_dnssim_connection_t* conn = async_ctx->conn;
+ mlassert(conn, "conn is nil");
+ mlassert(conn->tls, "conn must have tls ctx");
+ mlassert(conn->tls->write_queue_size > 0, "invalid write_queue_size: %d", conn->tls->write_queue_size);
+ conn->tls->write_queue_size -= 1;
+ free(req->data);
+
+ if (status < 0)
+ _output_dnssim_conn_close(conn);
+}
+
+static ssize_t _tls_vec_push(gnutls_transport_ptr_t ptr, const giovec_t* iov, int iovcnt)
+{
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)ptr;
+ mlassert(conn != NULL, "conn is null");
+ mlassert(conn->tls != NULL, "conn must have tls ctx");
+
+ if (iovcnt == 0)
+ return 0;
+
+ /*
+ * This is a little bit complicated. There are two different writes:
+ * 1. Immediate, these don't need to own the buffered data and return immediately
+ * 2. Asynchronous, these need to own the buffers until the write completes
+ * In order to avoid copying the buffer, an immediate write is tried first if possible.
+ * If it isn't possible to write the data without queueing, an asynchronous write
+ * is created (with copied buffered data).
+ */
+
+ size_t total_len = 0;
+ uv_buf_t uv_buf[iovcnt];
+ int i;
+ for (i = 0; i < iovcnt; ++i) {
+ uv_buf[i].base = iov[i].iov_base;
+ uv_buf[i].len = iov[i].iov_len;
+ total_len += iov[i].iov_len;
+ }
+
+ /* Try to perform the immediate write first to avoid copy */
+ int ret = 0;
+ if (conn->tls->write_queue_size == 0) {
+ ret = uv_try_write((uv_stream_t*)conn->handle, uv_buf, iovcnt);
+ /* from libuv documentation -
+ uv_try_write will return either:
+ > 0: number of bytes written (can be less than the supplied buffer size).
+ < 0: negative error code (UV_EAGAIN is returned if no data can be sent immediately).
+ */
+ if (ret == total_len) {
+ /* All the data were buffered by libuv.
+ * Return. */
+ return ret;
+ }
+
+ if (ret < 0 && ret != UV_EAGAIN) {
+ /* uv_try_write() has returned error code other then UV_EAGAIN.
+ * Return. */
+ errno = EIO;
+ return -1;
+ }
+ /* Since we are here expression below is true
+ * (ret != total_len) && (ret >= 0 || ret == UV_EAGAIN)
+ * or the same
+ * (ret != total_len && ret >= 0) || (ret != total_len && ret == UV_EAGAIN)
+ * i.e. either occurs partial write or UV_EAGAIN.
+ * Proceed and copy data amount to owned memory and perform async write.
+ */
+ if (ret == UV_EAGAIN) {
+ /* No data were buffered, so we must buffer all the data. */
+ ret = 0;
+ }
+ }
+
+ /* Fallback when the queue is full, and it's not possible to do an immediate write */
+ char* p = malloc(sizeof(struct async_write_ctx) + total_len - ret);
+ if (p != NULL) {
+ struct async_write_ctx* async_ctx = (struct async_write_ctx*)p;
+ async_ctx->conn = conn;
+ char* buf = async_ctx->buf;
+ /* Skip data written in the partial write */
+ size_t to_skip = ret;
+ /* Copy the buffer into owned memory */
+ size_t off = 0;
+ int i;
+ for (i = 0; i < iovcnt; ++i) {
+ if (to_skip > 0) {
+ /* Ignore current buffer if it's all skipped */
+ if (to_skip >= uv_buf[i].len) {
+ to_skip -= uv_buf[i].len;
+ continue;
+ }
+ /* Skip only part of the buffer */
+ uv_buf[i].base += to_skip;
+ uv_buf[i].len -= to_skip;
+ to_skip = 0;
+ }
+ memcpy(buf + off, uv_buf[i].base, uv_buf[i].len);
+ off += uv_buf[i].len;
+ }
+ uv_buf[0].base = buf;
+ uv_buf[0].len = off;
+
+ /* Create an asynchronous write request */
+ uv_write_t* write_req = &async_ctx->write_req;
+ memset(write_req, 0, sizeof(uv_write_t));
+ write_req->data = p;
+
+ /* Perform an asynchronous write with a callback */
+ if (uv_write(write_req, (uv_stream_t*)conn->handle, uv_buf, 1, _tls_on_write_complete) == 0) {
+ ret = total_len;
+ conn->tls->write_queue_size += 1;
+ } else {
+ free(p);
+ errno = EIO;
+ ret = -1;
+ }
+ } else {
+ errno = ENOMEM;
+ ret = -1;
+ }
+
+ return ret;
+}
+
+int _tls_pull_timeout(gnutls_transport_ptr_t ptr, unsigned int ms)
+{
+ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)ptr;
+ mlassert(conn != NULL, "conn is null");
+ mlassert(conn->tls != NULL, "conn must have tls ctx");
+
+ ssize_t avail = conn->tls->buf_len - conn->tls->buf_pos;
+ if (avail <= 0) {
+ errno = EAGAIN;
+ return -1;
+ }
+ return avail;
+}
+
+int _output_dnssim_tls_init(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn is nil");
+ mlassert(conn->tls == NULL, "conn already has tls context");
+
+ int ret;
+ mlfatal_oom(conn->tls = malloc(sizeof(_output_dnssim_tls_ctx_t)));
+ conn->tls->buf = NULL;
+ conn->tls->buf_len = 0;
+ conn->tls->buf_pos = 0;
+ conn->tls->write_queue_size = 0;
+
+ ret = gnutls_init(&conn->tls->session, GNUTLS_CLIENT | GNUTLS_NONBLOCK);
+ if (ret < 0) {
+ mldebug("failed gnutls_init() (%s)", gnutls_strerror(ret));
+ free(conn->tls);
+ conn->tls = 0;
+ return ret;
+ }
+
+ output_dnssim_t* self = conn->client->dnssim;
+ if (_self->tls_priority == NULL) {
+ ret = gnutls_set_default_priority(conn->tls->session);
+ if (ret < 0) {
+ mldebug("failed gnutls_set_default_priority() (%s)", gnutls_strerror(ret));
+ gnutls_deinit(conn->tls->session);
+ free(conn->tls);
+ conn->tls = 0;
+ return ret;
+ }
+ } else {
+ ret = gnutls_priority_set(conn->tls->session, *_self->tls_priority);
+ if (ret < 0) {
+ mldebug("failed gnutls_priority_set() (%s)", gnutls_strerror(ret));
+ gnutls_deinit(conn->tls->session);
+ free(conn->tls);
+ conn->tls = 0;
+ return ret;
+ }
+ }
+
+ ret = gnutls_credentials_set(conn->tls->session, GNUTLS_CRD_CERTIFICATE, _self->tls_cred);
+ if (ret < 0) {
+ mldebug("failed gnutls_credentials_set() (%s)", gnutls_strerror(ret));
+ gnutls_deinit(conn->tls->session);
+ free(conn->tls);
+ conn->tls = 0;
+ return ret;
+ }
+
+ gnutls_transport_set_pull_function(conn->tls->session, _tls_pull);
+ gnutls_transport_set_pull_timeout_function(conn->tls->session, _tls_pull_timeout);
+ gnutls_transport_set_vec_push_function(conn->tls->session, _tls_vec_push);
+ gnutls_transport_set_ptr(conn->tls->session, conn);
+
+ return 0;
+}
+
+int _output_dnssim_create_query_tls(output_dnssim_t* self, _output_dnssim_request_t* req)
+{
+ mlassert_self();
+ lassert(req, "req is nil");
+ lassert(req->client, "request must have a client associated with it");
+
+ _output_dnssim_query_tcp_t* qry;
+
+ lfatal_oom(qry = calloc(1, sizeof(_output_dnssim_query_tcp_t)));
+
+ qry->qry.transport = OUTPUT_DNSSIM_TRANSPORT_TLS;
+ qry->qry.req = req;
+ qry->qry.state = _OUTPUT_DNSSIM_QUERY_PENDING_WRITE;
+ req->qry = &qry->qry; // TODO change when adding support for multiple Qs for req
+ _ll_append(req->client->pending, &qry->qry);
+
+ return _output_dnssim_handle_pending_queries(req->client);
+}
+
+void _output_dnssim_close_query_tls(_output_dnssim_query_tcp_t* qry)
+{
+ mlassert(qry, "qry can't be null");
+ mlassert(qry->qry.req, "query must be part of a request");
+ _output_dnssim_request_t* req = qry->qry.req;
+ mlassert(req->client, "request must belong to a client");
+
+ _ll_try_remove(req->client->pending, &qry->qry);
+ if (qry->conn) {
+ _output_dnssim_connection_t* conn = qry->conn;
+ _ll_try_remove(conn->sent, &qry->qry);
+ qry->conn = NULL;
+ _output_dnssim_conn_idle(conn);
+ }
+
+ _ll_remove(req->qry, &qry->qry);
+ free(qry);
+}
+
+void _output_dnssim_tls_close(_output_dnssim_connection_t* conn)
+{
+ mlassert(conn, "conn can't be nil");
+ mlassert(conn->tls, "conn must have tls ctx");
+ mlassert(conn->client, "conn must belong to a client");
+
+ /* Try and get a TLS session ticket for potential resumption. */
+ int ret;
+ if (gnutls_session_get_flags(conn->tls->session) & GNUTLS_SFLAGS_SESSION_TICKET) {
+ if (conn->client->tls_ticket.size != 0) {
+ gnutls_free(conn->client->tls_ticket.data);
+ }
+ ret = gnutls_session_get_data2(conn->tls->session, &conn->client->tls_ticket);
+ if (ret < 0) {
+ mldebug("gnutls_session_get_data2 failed: %s", gnutls_strerror(ret));
+ conn->client->tls_ticket.size = 0;
+ }
+ }
+
+ gnutls_deinit(conn->tls->session);
+ _output_dnssim_tcp_close(conn);
+}
+
+void _output_dnssim_tls_write_query(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry)
+{
+ mlassert(qry, "qry can't be null");
+ mlassert(qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_WRITE, "qry must be pending write");
+ mlassert(qry->qry.req, "req can't be null");
+ mlassert(qry->qry.req->dns_q, "dns_q can't be null");
+ mlassert(qry->qry.req->dns_q->obj_prev, "payload can't be null");
+ mlassert(conn, "conn can't be null");
+ mlassert(conn->state == _OUTPUT_DNSSIM_CONN_ACTIVE, "connection state != ACTIVE");
+ mlassert(conn->tls, "conn must have tls ctx");
+ mlassert(conn->client, "conn must be associated with client");
+ mlassert(conn->client->pending, "conn has no pending queries");
+
+ core_object_payload_t* payload = (core_object_payload_t*)qry->qry.req->dns_q->obj_prev;
+ uint16_t len = htons(payload->len);
+
+ gnutls_record_cork(conn->tls->session);
+ ssize_t count = 0;
+ if ((count = gnutls_record_send(conn->tls->session, &len, sizeof(len)) < 0) || (count = gnutls_record_send(conn->tls->session, payload->payload, payload->len) < 0)) {
+ mlwarning("gnutls_record_send failed: %s", gnutls_strerror_name(count));
+ _output_dnssim_conn_close(conn);
+ return;
+ }
+
+ const ssize_t submitted = sizeof(len) + payload->len;
+
+ int ret = gnutls_record_uncork(conn->tls->session, GNUTLS_RECORD_WAIT);
+ if (gnutls_error_is_fatal(ret)) {
+ mlinfo("gnutls_record_uncorck failed: %s", gnutls_strerror_name(ret));
+ _output_dnssim_conn_close(conn);
+ return;
+ }
+
+ if (ret != submitted) {
+ mlwarning("gnutls_record_uncork didn't send all data");
+ _output_dnssim_conn_close(conn);
+ return;
+ }
+
+ qry->conn = conn;
+ _ll_remove(conn->client->pending, &qry->qry);
+ _ll_append(conn->sent, &qry->qry);
+
+ /* Stop idle timer, since there are queries to answer now. */
+ if (conn->idle_timer != NULL) {
+ conn->is_idle = false;
+ uv_timer_stop(conn->idle_timer);
+ }
+
+ qry->qry.state = _OUTPUT_DNSSIM_QUERY_SENT;
+}
+
+#endif
diff --git a/src/output/dnssim/udp.c b/src/output/dnssim/udp.c
new file mode 100644
index 0000000..74f8569
--- /dev/null
+++ b/src/output/dnssim/udp.c
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2019-2020, CZ.NIC, z.s.p.o.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "output/dnssim.h"
+#include "output/dnssim/internal.h"
+#include "output/dnssim/ll.h"
+#include "core/assert.h"
+
+static core_log_t _log = LOG_T_INIT("output.dnssim");
+
+static int _process_udp_response(uv_udp_t* handle, ssize_t nread, const uv_buf_t* buf)
+{
+ _output_dnssim_query_udp_t* qry = (_output_dnssim_query_udp_t*)handle->data;
+ _output_dnssim_request_t* req;
+ core_object_payload_t payload = CORE_OBJECT_PAYLOAD_INIT(NULL);
+ core_object_dns_t dns_a = CORE_OBJECT_DNS_INIT(&payload);
+ mlassert(qry, "qry is nil");
+ mlassert(qry->qry.req, "query must be part of a request");
+ req = qry->qry.req;
+
+ payload.payload = (uint8_t*)buf->base;
+ payload.len = nread;
+
+ dns_a.obj_prev = (core_object_t*)&payload;
+ int ret = core_object_dns_parse_header(&dns_a);
+ if (ret != 0) {
+ mldebug("udp response malformed");
+ return _ERR_MALFORMED;
+ }
+ if (dns_a.id != req->dns_q->id) {
+ mldebug("udp response msgid mismatch %x(q) != %x(a)", req->dns_q->id, dns_a.id);
+ return _ERR_MSGID;
+ }
+ if (dns_a.tc == 1) {
+ mldebug("udp response has TC=1");
+ return _ERR_TC;
+ }
+ ret = _output_dnssim_answers_request(req, &dns_a);
+ if (ret != 0) {
+ mlwarning("udp reponse question mismatch");
+ return _ERR_QUESTION;
+ }
+
+ _output_dnssim_request_answered(req, &dns_a);
+ return 0;
+}
+
+static void _on_udp_query_recv(uv_udp_t* handle, ssize_t nread, const uv_buf_t* buf, const struct sockaddr* addr, unsigned flags)
+{
+ if (nread > 0) {
+ mldebug("udp recv: %d", nread);
+
+ // TODO handle TC=1
+ _process_udp_response(handle, nread, buf);
+ }
+
+ if (buf->base != NULL) {
+ free(buf->base);
+ }
+}
+
+static void _on_query_udp_closed(uv_handle_t* handle)
+{
+ _output_dnssim_query_udp_t* qry = (_output_dnssim_query_udp_t*)handle->data;
+ _output_dnssim_request_t* req;
+ mlassert(qry, "qry is nil");
+ mlassert(qry->qry.req, "query must be part of a request");
+ req = qry->qry.req;
+
+ free(qry->handle);
+
+ _ll_remove(req->qry, &qry->qry);
+ free(qry);
+
+ if (req->qry == NULL)
+ _output_dnssim_maybe_free_request(req);
+}
+
+void _output_dnssim_close_query_udp(_output_dnssim_query_udp_t* qry)
+{
+ int ret;
+ mlassert(qry, "qry is nil");
+
+ ret = uv_udp_recv_stop(qry->handle);
+ if (ret < 0) {
+ mldebug("failed uv_udp_recv_stop(): %s", uv_strerror(ret));
+ }
+
+ uv_close((uv_handle_t*)qry->handle, _on_query_udp_closed);
+}
+
+int _output_dnssim_create_query_udp(output_dnssim_t* self, _output_dnssim_request_t* req)
+{
+ int ret;
+ _output_dnssim_query_udp_t* qry;
+ core_object_payload_t* payload;
+ mlassert_self();
+ lassert(req, "req is nil");
+ payload = (core_object_payload_t*)req->dns_q->obj_prev;
+
+ lfatal_oom(qry = calloc(1, sizeof(_output_dnssim_query_udp_t)));
+ lfatal_oom(qry->handle = malloc(sizeof(uv_udp_t)));
+
+ qry->qry.transport = OUTPUT_DNSSIM_TRANSPORT_UDP;
+ qry->qry.req = req;
+ qry->buf = uv_buf_init((char*)payload->payload, payload->len);
+ qry->handle->data = (void*)qry;
+ ret = uv_udp_init(&_self->loop, qry->handle);
+ if (ret < 0) {
+ lwarning("failed to init uv_udp_t");
+ goto failure;
+ }
+ _ll_append(req->qry, &qry->qry);
+
+ ret = _output_dnssim_bind_before_connect(self, (uv_handle_t*)qry->handle);
+ if (ret < 0)
+ return ret;
+
+ ret = uv_udp_try_send(qry->handle, &qry->buf, 1, (struct sockaddr*)&_self->target);
+ if (ret < 0) {
+ lwarning("failed to send udp packet: %s", uv_strerror(ret));
+ return ret;
+ }
+
+ // listen for reply
+ ret = uv_udp_recv_start(qry->handle, _output_dnssim_on_uv_alloc, _on_udp_query_recv);
+ if (ret < 0) {
+ lwarning("failed uv_udp_recv_start(): %s", uv_strerror(ret));
+ return ret;
+ }
+
+ return 0;
+failure:
+ free(qry->handle);
+ free(qry);
+ return ret;
+}
diff --git a/src/output/null.c b/src/output/null.c
new file mode 100644
index 0000000..9360afd
--- /dev/null
+++ b/src/output/null.c
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * 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/null.h"
+#include "core/assert.h"
+#include "core/object/pcap.h"
+
+static core_log_t _log = LOG_T_INIT("output.null");
+static output_null_t _defaults = {
+ LOG_T_INIT_OBJ("output.null"),
+ 0, 0, 0
+};
+
+core_log_t* output_null_log()
+{
+ return &_log;
+}
+
+void output_null_init(output_null_t* self)
+{
+ mlassert_self();
+
+ *self = _defaults;
+}
+
+void output_null_destroy(output_null_t* self)
+{
+ mlassert_self();
+}
+
+static void _receive(output_null_t* self, const core_object_t* obj)
+{
+ mlassert_self();
+
+ self->pkts++;
+}
+
+core_receiver_t output_null_receiver()
+{
+ return (core_receiver_t)_receive;
+}
+
+void output_null_run(output_null_t* self, int64_t num)
+{
+ mlassert_self();
+
+ if (!self->prod) {
+ lfatal("no producer set");
+ }
+
+ if (num > 0) {
+ while (num--) {
+ const core_object_t* obj = self->prod(self->ctx);
+ if (!obj)
+ break;
+
+ self->pkts++;
+ }
+ } else {
+ for (;;) {
+ const core_object_t* obj = self->prod(self->ctx);
+ if (!obj)
+ break;
+
+ self->pkts++;
+ }
+ }
+}
diff --git a/src/output/null.h b/src/output/null.h
new file mode 100644
index 0000000..b8ccf70
--- /dev/null
+++ b/src/output/null.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "core/log.h"
+#include "core/receiver.h"
+#include "core/producer.h"
+
+#ifndef __dnsjit_output_null_h
+#define __dnsjit_output_null_h
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "output/null.hh"
+
+#endif
diff --git a/src/output/null.hh b/src/output/null.hh
new file mode 100644
index 0000000..3f5cd33
--- /dev/null
+++ b/src/output/null.hh
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+//lua:require("dnsjit.core.log")
+//lua:require("dnsjit.core.receiver_h")
+//lua:require("dnsjit.core.producer_h")
+
+typedef struct output_null {
+ core_log_t _log;
+ core_producer_t prod;
+ void* ctx;
+ size_t pkts;
+} output_null_t;
+
+core_log_t* output_null_log();
+void output_null_init(output_null_t* self);
+void output_null_destroy(output_null_t* self);
+void output_null_run(output_null_t* self, int64_t num);
+
+core_receiver_t output_null_receiver();
diff --git a/src/output/null.lua b/src/output/null.lua
new file mode 100644
index 0000000..b634c56
--- /dev/null
+++ b/src/output/null.lua
@@ -0,0 +1,80 @@
+-- Copyright (c) 2018-2021, OARC, Inc.
+-- All rights reserved.
+--
+-- This file is part of dnsjit.
+--
+-- dnsjit is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- dnsjit is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+
+-- dnsjit.output.null
+-- Output to nothing (/dev/null)
+-- local output = require("dnsjit.output.null").new()
+--
+-- Output module for those that doesn't really like packets.
+module(...,package.seeall)
+
+require("dnsjit.output.null_h")
+local ffi = require("ffi")
+local C = ffi.C
+
+local t_name = "output_null_t"
+local output_null_t = ffi.typeof(t_name)
+local Null = {}
+
+-- Create a new Null output.
+function Null.new()
+ local self = {
+ _producer = nil,
+ obj = output_null_t(),
+ }
+ C.output_null_init(self.obj)
+ ffi.gc(self.obj, C.output_null_destroy)
+ return setmetatable(self, { __index = Null })
+end
+
+-- Return the Log object to control logging of this instance or module.
+function Null:log()
+ if self == nil then
+ return C.output_null_log()
+ end
+ return self.obj._log
+end
+
+-- Return the C functions and context for receiving objects.
+function Null:receive()
+ return C.output_null_receiver(), self.obj
+end
+
+-- Set the producer to get objects from.
+function Null:producer(o)
+ self.obj.prod, self.obj.ctx = o:produce()
+ self._producer = o
+end
+
+-- Retrieve all objects from the producer, if the optional
+-- .I num
+-- is a positive number then stop after that amount of objects have been
+-- retrieved.
+function Null:run(num)
+ if num == nil then
+ num = -1
+ end
+ C.output_null_run(self.obj, num)
+end
+
+-- Return the number of packets we sent into the void.
+function Null:packets()
+ return tonumber(self.obj.pkts)
+end
+
+return Null
diff --git a/src/output/pcap.c b/src/output/pcap.c
new file mode 100644
index 0000000..2b2aaa8
--- /dev/null
+++ b/src/output/pcap.c
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * 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/pcap.h"
+#include "core/assert.h"
+#include "core/object/pcap.h"
+
+static core_log_t _log = LOG_T_INIT("output.pcap");
+static output_pcap_t _defaults = {
+ LOG_T_INIT_OBJ("output.pcap"),
+ 0, 0
+};
+
+core_log_t* output_pcap_log()
+{
+ return &_log;
+}
+
+void output_pcap_init(output_pcap_t* self)
+{
+ mlassert_self();
+
+ *self = _defaults;
+}
+
+void output_pcap_destroy(output_pcap_t* self)
+{
+ mlassert_self();
+}
+
+int output_pcap_open(output_pcap_t* self, const char* file, int linktype, int snaplen)
+{
+ mlassert_self();
+ if (self->dumper) {
+ lfatal("PCAP already opened");
+ }
+
+ if (!(self->pcap = pcap_open_dead(linktype, snaplen))) {
+ lcritical("pcap_open_dead() failed");
+ return -1;
+ }
+
+ if (!(self->dumper = pcap_dump_open(self->pcap, file))) {
+ lcritical("pcap_dump_open() error: %s", pcap_geterr(self->pcap));
+ pcap_close(self->pcap);
+ self->pcap = 0;
+ return -1;
+ }
+
+ return 0;
+}
+
+void output_pcap_close(output_pcap_t* self)
+{
+ mlassert_self();
+ if (self->dumper) {
+ pcap_dump_close(self->dumper);
+ self->dumper = 0;
+ }
+ if (self->pcap) {
+ pcap_close(self->pcap);
+ self->pcap = 0;
+ }
+}
+
+static void _receive(output_pcap_t* self, const core_object_t* obj)
+{
+ struct pcap_pkthdr hdr;
+ mlassert_self();
+
+ while (obj) {
+ if (obj->obj_type == CORE_OBJECT_PCAP) {
+ hdr.ts.tv_sec = ((const core_object_pcap_t*)obj)->ts.sec;
+ hdr.ts.tv_usec = ((const core_object_pcap_t*)obj)->ts.nsec / 1000;
+ hdr.caplen = ((const core_object_pcap_t*)obj)->caplen;
+ hdr.len = ((const core_object_pcap_t*)obj)->len;
+
+ pcap_dump((void*)self->dumper, &hdr, ((const core_object_pcap_t*)obj)->bytes);
+ return;
+ }
+ obj = obj->obj_prev;
+ }
+}
+
+core_receiver_t output_pcap_receiver(output_pcap_t* self)
+{
+ if (!self->dumper) {
+ lfatal("PCAP not opened");
+ }
+
+ return (core_receiver_t)_receive;
+}
diff --git a/src/output/pcap.h b/src/output/pcap.h
new file mode 100644
index 0000000..fccf58d
--- /dev/null
+++ b/src/output/pcap.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "core/log.h"
+#include "core/receiver.h"
+#include "core/producer.h"
+
+#ifndef __dnsjit_output_pcap_h
+#define __dnsjit_output_pcap_h
+
+#include <pcap/pcap.h>
+
+#include "output/pcap.hh"
+
+#endif
diff --git a/src/output/pcap.hh b/src/output/pcap.hh
new file mode 100644
index 0000000..45f5150
--- /dev/null
+++ b/src/output/pcap.hh
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * 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/>.
+ */
+
+#if 0
+typedef struct pcap_dumper {} pcap_dumper_t;
+#endif
+
+//lua:require("dnsjit.core.log")
+//lua:require("dnsjit.core.receiver_h")
+//lua:require("dnsjit.input.pcap_h")
+
+typedef struct output_pcap {
+ core_log_t _log;
+ pcap_t* pcap;
+ pcap_dumper_t* dumper;
+} output_pcap_t;
+
+core_log_t* output_pcap_log();
+void output_pcap_init(output_pcap_t* self);
+void output_pcap_destroy(output_pcap_t* self);
+int output_pcap_open(output_pcap_t* self, const char* file, int linktype, int snaplen);
+void output_pcap_close(output_pcap_t* self);
+
+core_receiver_t output_pcap_receiver(output_pcap_t* self);
diff --git a/src/output/pcap.lua b/src/output/pcap.lua
new file mode 100644
index 0000000..13f0c29
--- /dev/null
+++ b/src/output/pcap.lua
@@ -0,0 +1,79 @@
+-- Copyright (c) 2018-2021, OARC, Inc.
+-- All rights reserved.
+--
+-- This file is part of dnsjit.
+--
+-- dnsjit is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- dnsjit is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+
+-- dnsjit.output.pcap
+-- Output to a PCAP using libpcap
+-- local output = require("dnsjit.output.pcap").new()
+-- output:open("file.pcap")
+-- ...
+-- output:close()
+--
+-- Output module for writing
+-- .I dnsjit.core.object.pcap
+-- objects to a PCAP,
+module(...,package.seeall)
+
+require("dnsjit.output.pcap_h")
+local ffi = require("ffi")
+local C = ffi.C
+
+local t_name = "output_pcap_t"
+local output_pcap_t = ffi.typeof(t_name)
+local Pcap = {}
+
+-- Create a new Pcap output.
+function Pcap.new()
+ local self = {
+ obj = output_pcap_t(),
+ }
+ C.output_pcap_init(self.obj)
+ ffi.gc(self.obj, C.output_pcap_destroy)
+ return setmetatable(self, { __index = Pcap })
+end
+
+-- Return the Log object to control logging of this instance or module.
+function Pcap:log()
+ if self == nil then
+ return C.output_pcap_log()
+ end
+ return self.obj._log
+end
+
+-- Open the PCAP
+-- .I file
+-- to write to using the
+-- .I linktype
+-- and
+-- .IR snaplen .
+-- Returns 0 on success.
+function Pcap:open(file, linktype, snaplen)
+ return C.output_pcap_open(self.obj, file, linktype, snaplen)
+end
+
+-- Close the PCAP.
+function Pcap:close()
+ C.output_pcap_close(self.obj)
+end
+
+-- Return the C functions and context for receiving objects.
+function Pcap:receive()
+ return C.output_pcap_receiver(self.obj), self.obj
+end
+
+-- dnsjit.input.pcap (3)
+return Pcap
diff --git a/src/output/respdiff.c b/src/output/respdiff.c
new file mode 100644
index 0000000..834a264
--- /dev/null
+++ b/src/output/respdiff.c
@@ -0,0 +1,298 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * 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/respdiff.h"
+#include "core/assert.h"
+#include "core/object/payload.h"
+
+#ifdef HAVE_LMDB_H
+#include <lmdb.h>
+#endif
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <string.h>
+
+static core_log_t _log = LOG_T_INIT("output.respdiff");
+static output_respdiff_t _defaults = {
+ LOG_T_INIT_OBJ("output.respdiff"),
+ 0, 0, 0, 0, 0, 0
+};
+
+core_log_t* output_respdiff_log()
+{
+ return &_log;
+}
+
+void output_respdiff_init(output_respdiff_t* self, const char* path, size_t mapsize)
+{
+ mlassert_self();
+
+ if (!path) {
+ lfatal("path is nil");
+ }
+
+ *self = _defaults;
+
+#ifdef HAVE_LMDB_H
+ if (mkdir(path, 0775) && errno != EEXIST) {
+ lfatal("mkdir(%s) error %s", path, core_log_errstr(errno));
+ }
+ if (mdb_env_create((MDB_env**)&self->env)) {
+ lfatal("mdb_env_create failed");
+ }
+ if (mdb_env_set_mapsize((MDB_env*)self->env, mapsize)) {
+ lfatal("mdb_env_set_mapsize(%lu) failed", mapsize);
+ }
+ if (mdb_env_set_maxdbs((MDB_env*)self->env, 3)) {
+ lfatal("mdb_env_set_maxdbs failed");
+ }
+ if (mdb_env_open((MDB_env*)self->env, path, 0, 0664)) {
+ lfatal("mdb_env_open(%s) failed", path);
+ }
+ if (mdb_txn_begin((MDB_env*)self->env, 0, 0, (MDB_txn**)&self->txn)) {
+ lfatal("mdb_txn_begin failed for queries");
+ }
+ lfatal_oom(self->qdb = calloc(1, sizeof(MDB_dbi)));
+ if (mdb_dbi_open((MDB_txn*)self->txn, "queries", MDB_CREATE, (MDB_dbi*)self->qdb)) {
+ lfatal("mdb_dbi_open failed for queries");
+ }
+ lfatal_oom(self->rdb = calloc(1, sizeof(MDB_dbi)));
+ if (mdb_dbi_open((MDB_txn*)self->txn, "answers", MDB_CREATE, (MDB_dbi*)self->rdb)) {
+ lfatal("mdb_dbi_open failed for responses");
+ }
+ lfatal_oom(self->meta = calloc(1, sizeof(MDB_dbi)));
+ if (mdb_dbi_open((MDB_txn*)self->txn, "meta", MDB_CREATE, (MDB_dbi*)self->meta)) {
+ lfatal("mdb_dbi_open failed for meta");
+ }
+#endif
+}
+
+void output_respdiff_destroy(output_respdiff_t* self)
+{
+ mlassert_self();
+
+#ifdef HAVE_LMDB_H
+ if (self->env) {
+ mdb_env_close((MDB_env*)self->env);
+ }
+ free(self->qdb);
+ free(self->rdb);
+ free(self->meta);
+#endif
+}
+
+#ifdef HAVE_LMDB_H
+static const char* _meta_version = "version";
+static const char* _meta_version_val = "2018-05-21";
+static const char* _meta_servers = "servers";
+static const char* _meta_name0 = "name0";
+static const char* _meta_name1 = "name1";
+static const char* _meta_start_time = "start_time";
+static const char* _meta_end_time = "end_time";
+#endif
+
+void output_respdiff_commit(output_respdiff_t* self, const char* origname, const char* recvname, uint64_t start_time, uint64_t end_time)
+{
+#ifdef HAVE_LMDB_H
+ MDB_val k, v;
+ uint32_t i;
+ int err;
+ mlassert_self();
+ lassert(origname, "origname is nil");
+ lassert(recvname, "recvname is nil");
+
+ k.mv_size = strlen(_meta_version);
+ k.mv_data = (void*)_meta_version;
+ v.mv_size = strlen(_meta_version_val);
+ v.mv_data = (void*)_meta_version_val;
+ if ((err = mdb_put((MDB_txn*)self->txn, (MDB_dbi) * ((MDB_dbi*)self->meta), &k, &v, 0))) {
+ if (err == MDB_MAP_FULL) {
+ lfatal("mdb_put meta.version failed, database is full");
+ } else {
+ lfatal("mdb_put meta.version failed (%d)", err);
+ }
+ }
+
+ k.mv_size = strlen(_meta_servers);
+ k.mv_data = (void*)_meta_servers;
+ i = 2;
+ v.mv_size = 4;
+ v.mv_data = (void*)&i;
+ if ((err = mdb_put((MDB_txn*)self->txn, (MDB_dbi) * ((MDB_dbi*)self->meta), &k, &v, 0))) {
+ if (err == MDB_MAP_FULL) {
+ lfatal("mdb_put meta.servers failed, database is full");
+ } else {
+ lfatal("mdb_put meta.servers failed (%d)", err);
+ }
+ }
+
+ k.mv_size = strlen(_meta_name0);
+ k.mv_data = (void*)_meta_name0;
+ v.mv_size = strlen(origname);
+ v.mv_data = (void*)origname;
+ if ((err = mdb_put((MDB_txn*)self->txn, (MDB_dbi) * ((MDB_dbi*)self->meta), &k, &v, 0))) {
+ if (err == MDB_MAP_FULL) {
+ lfatal("mdb_put meta.name0 failed, database is full");
+ } else {
+ lfatal("mdb_put meta.name0 failed (%d)", err);
+ }
+ }
+
+ k.mv_size = strlen(_meta_name1);
+ k.mv_data = (void*)_meta_name1;
+ v.mv_size = strlen(recvname);
+ v.mv_data = (void*)recvname;
+ if ((err = mdb_put((MDB_txn*)self->txn, (MDB_dbi) * ((MDB_dbi*)self->meta), &k, &v, 0))) {
+ if (err == MDB_MAP_FULL) {
+ lfatal("mdb_put meta.name1 failed, database is full");
+ } else {
+ lfatal("mdb_put meta.name1 failed (%d)", err);
+ }
+ }
+
+ k.mv_size = strlen(_meta_start_time);
+ k.mv_data = (void*)_meta_start_time;
+ i = start_time;
+ v.mv_size = 4;
+ v.mv_data = (void*)&i;
+ if ((err = mdb_put((MDB_txn*)self->txn, (MDB_dbi) * ((MDB_dbi*)self->meta), &k, &v, 0))) {
+ if (err == MDB_MAP_FULL) {
+ lfatal("mdb_put meta.start_time failed, database is full");
+ } else {
+ lfatal("mdb_put meta.start_time failed (%d)", err);
+ }
+ }
+
+ k.mv_size = strlen(_meta_end_time);
+ k.mv_data = (void*)_meta_end_time;
+ i = end_time;
+ v.mv_size = 4;
+ v.mv_data = (void*)&i;
+ if ((err = mdb_put((MDB_txn*)self->txn, (MDB_dbi) * ((MDB_dbi*)self->meta), &k, &v, 0))) {
+ if (err == MDB_MAP_FULL) {
+ lfatal("mdb_put meta.end_time failed, database is full");
+ } else {
+ lfatal("mdb_put meta.end_time failed (%d)", err);
+ }
+ }
+
+ if (self->txn) {
+ if (mdb_txn_commit((MDB_txn*)self->txn)) {
+ lfatal("mdb_txn_commit failed");
+ }
+ self->txn = 0;
+ }
+#endif
+}
+
+#ifdef HAVE_LMDB_H
+static void _receive(output_respdiff_t* self, const core_object_t* obj)
+{
+ const core_object_payload_t *query, *original, *response;
+ MDB_val k, v;
+ uint8_t responses[132096];
+ uint32_t msec;
+ uint16_t dnslen;
+ int err;
+ mlassert_self();
+
+ if (!obj || obj->obj_type != CORE_OBJECT_PAYLOAD) {
+ lfatal("invalid first object");
+ }
+ query = (core_object_payload_t*)obj;
+
+ if (!query->obj_prev || query->obj_prev->obj_type != CORE_OBJECT_PAYLOAD) {
+ lfatal("invalid second object");
+ }
+ original = (core_object_payload_t*)query->obj_prev;
+
+ response = (core_object_payload_t*)original->obj_prev;
+ if (response && response->obj_type != CORE_OBJECT_PAYLOAD) {
+ lfatal("invalid third object");
+ }
+
+ if (12 + original->len + (response ? response->len : 0) > sizeof(responses)) {
+ lfatal("not enough buffer space for responses");
+ }
+
+ self->count++;
+
+ k.mv_size = sizeof(self->id);
+ k.mv_data = (void*)&self->id;
+ v.mv_size = query->len;
+ v.mv_data = (void*)query->payload;
+ if ((err = mdb_put((MDB_txn*)self->txn, (MDB_dbi) * ((MDB_dbi*)self->qdb), &k, &v, 0))) {
+ if (err == MDB_MAP_FULL) {
+ lfatal("mdb_put query failed, database is full");
+ } else {
+ lfatal("mdb_put query failed (%d)", err);
+ }
+ }
+
+ msec = 1; // TODO
+ memcpy(responses, &msec, 4);
+ dnslen = original->len;
+ memcpy(&responses[4], &dnslen, 2);
+ memcpy(&responses[6], original->payload, original->len);
+ if (response) {
+ memcpy(&responses[6 + original->len], &msec, 4);
+ dnslen = response->len;
+ memcpy(&responses[10 + original->len], &dnslen, 2);
+ memcpy(&responses[12 + original->len], response->payload, response->len);
+ } else {
+ msec = 0xffffffff;
+ memcpy(&responses[6 + original->len], &msec, 4);
+ dnslen = 0;
+ memcpy(&responses[10 + original->len], &dnslen, 2);
+ }
+
+ v.mv_size = 12 + original->len + (response ? response->len : 0);
+ v.mv_data = (void*)responses;
+ if ((err = mdb_put((MDB_txn*)self->txn, (MDB_dbi) * ((MDB_dbi*)self->rdb), &k, &v, 0))) {
+ if (err == MDB_MAP_FULL) {
+ lfatal("mdb_put answers failed, database is full");
+ } else {
+ lfatal("mdb_put answers failed (%d)", err);
+ }
+ }
+
+ self->id++;
+}
+
+core_receiver_t output_respdiff_receiver(output_respdiff_t* self)
+{
+ mlassert_self();
+
+ if (!self->txn) {
+ lfatal("no LMDB opened");
+ }
+
+ return (core_receiver_t)_receive;
+}
+#else
+core_receiver_t output_respdiff_receiver(output_respdiff_t* self)
+{
+ mlassert_self();
+ lfatal("no LMDB support");
+ return 0;
+}
+#endif
diff --git a/src/output/respdiff.h b/src/output/respdiff.h
new file mode 100644
index 0000000..f375006
--- /dev/null
+++ b/src/output/respdiff.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "core/log.h"
+#include "core/receiver.h"
+
+#ifndef __dnsjit_output_respdiff_h
+#define __dnsjit_output_respdiff_h
+
+#include <stdint.h>
+
+#include "output/respdiff.hh"
+
+#endif
diff --git a/src/output/respdiff.hh b/src/output/respdiff.hh
new file mode 100644
index 0000000..44f597e
--- /dev/null
+++ b/src/output/respdiff.hh
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+//lua:require("dnsjit.core.log")
+//lua:require("dnsjit.core.receiver_h")
+
+typedef struct output_respdiff {
+ core_log_t _log;
+ void * env, *txn, *qdb, *rdb, *meta;
+ uint32_t id;
+ size_t count;
+} output_respdiff_t;
+
+core_log_t* output_respdiff_log();
+void output_respdiff_init(output_respdiff_t* self, const char* path, size_t mapsize);
+void output_respdiff_destroy(output_respdiff_t* self);
+void output_respdiff_commit(output_respdiff_t* self, const char* origname, const char* recvname, uint64_t start_time, uint64_t end_time);
+
+core_receiver_t output_respdiff_receiver(output_respdiff_t* self);
diff --git a/src/output/respdiff.lua b/src/output/respdiff.lua
new file mode 100644
index 0000000..7d17b4a
--- /dev/null
+++ b/src/output/respdiff.lua
@@ -0,0 +1,94 @@
+-- Copyright (c) 2018-2021, OARC, Inc.
+-- All rights reserved.
+--
+-- This file is part of dnsjit.
+--
+-- dnsjit is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- dnsjit is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+
+-- dnsjit.output.respdiff
+-- Output to respdiff LMDB
+-- local output = require("dnsjit.output.respdiff").new("/path/to/lmdb")
+--
+-- Output to an LMDB database (format 2018-05-21) that can be used by respdiff
+-- to compare the responses found in the input data with the responses
+-- received.
+-- The receive function expects to get a chain of 2 or 3
+-- .IR core.object.payload .
+-- For a completed query; The top of the chain is the query, after it the
+-- original response and then the received response.
+-- For a timed out query; The top of the chain is the query, after it the
+-- original response.
+module(...,package.seeall)
+
+require("dnsjit.output.respdiff_h")
+local ffi = require("ffi")
+local C = ffi.C
+
+local t_name = "output_respdiff_t"
+local output_respdiff_t = ffi.typeof(t_name)
+local Respdiff = {}
+
+-- Create a new Respdiff output and created the LMDB database in the directory
+-- .IR path .
+-- The
+-- .I origname
+-- and
+-- .I recvname
+-- are used to populate the meta table, these names should be the same as
+-- what is configured in
+-- .IR respdiff.cfg .
+-- Optional
+-- .I mapsize
+-- can be given to increase the database size beyond the default size of 10MB.
+function Respdiff.new(path, origname, recvname, mapsize)
+ if mapsize == nil then
+ mapsize = 10485760
+ end
+ local self = {
+ obj = output_respdiff_t(),
+ path = path,
+ origname = origname,
+ recvname = recvname,
+ }
+ C.output_respdiff_init(self.obj, path, mapsize)
+ ffi.gc(self.obj, C.output_respdiff_destroy)
+ return setmetatable(self, { __index = Respdiff })
+end
+
+-- Return the Log object to control logging of this instance or module.
+function Respdiff:log()
+ if self == nil then
+ return C.output_respdiff_log()
+ end
+ return self.obj._log
+end
+
+-- Return the C functions and context for receiving objects.
+function Respdiff:receive()
+ return C.output_respdiff_receiver(self.obj), self.obj
+end
+
+-- Commit the LMDB transactions, can not store any more objects after this
+-- call.
+-- The given
+-- .I start_time
+-- and
+-- .I end_time
+-- are used to fill the meta table.
+function Respdiff:commit(start_time, end_time)
+ C.output_respdiff_commit(self.obj, self.origname, self.recvname, start_time, end_time)
+end
+
+-- respdiff " https://gitlab.nic.cz/knot/respdiff"
+return Respdiff
diff --git a/src/output/tcpcli.c b/src/output/tcpcli.c
new file mode 100644
index 0000000..f2b218b
--- /dev/null
+++ b/src/output/tcpcli.c
@@ -0,0 +1,381 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * 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/tcpcli.h"
+#include "core/assert.h"
+#include "core/object/dns.h"
+#include "core/object/payload.h"
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <string.h>
+#include <arpa/inet.h>
+#include <poll.h>
+
+static core_log_t _log = LOG_T_INIT("output.tcpcli");
+static output_tcpcli_t _defaults = {
+ LOG_T_INIT_OBJ("output.tcpcli"),
+ 0, 0, -1,
+ { 0 }, CORE_OBJECT_PAYLOAD_INIT(0),
+ 0, 0, 0, 0,
+ { 5, 0 }, 1
+};
+
+core_log_t* output_tcpcli_log()
+{
+ return &_log;
+}
+
+void output_tcpcli_init(output_tcpcli_t* self)
+{
+ mlassert_self();
+
+ *self = _defaults;
+ self->pkt.payload = self->recvbuf;
+}
+
+void output_tcpcli_destroy(output_tcpcli_t* self)
+{
+ mlassert_self();
+
+ if (self->fd > -1) {
+ shutdown(self->fd, SHUT_RDWR);
+ close(self->fd);
+ }
+}
+
+int output_tcpcli_connect(output_tcpcli_t* self, const char* host, const char* port)
+{
+ struct addrinfo* addr;
+ int err;
+ mlassert_self();
+ lassert(host, "host is nil");
+ lassert(port, "port is nil");
+
+ if (self->fd > -1) {
+ lfatal("already connected");
+ }
+
+ if ((err = getaddrinfo(host, port, 0, &addr))) {
+ lcritical("getaddrinfo(%s, %s) error %s", host, port, gai_strerror(err));
+ return -1;
+ }
+ if (!addr) {
+ lcritical("getaddrinfo failed, no address returned");
+ return -1;
+ }
+
+ if ((self->fd = socket(addr->ai_addr->sa_family, SOCK_STREAM, 0)) < 0) {
+ lcritical("socket() error %s", core_log_errstr(errno));
+ freeaddrinfo(addr);
+ return -2;
+ }
+
+ if (connect(self->fd, addr->ai_addr, addr->ai_addrlen)) {
+ lcritical("connect() error %s", core_log_errstr(errno));
+ freeaddrinfo(addr);
+ return -2;
+ }
+
+ freeaddrinfo(addr);
+ return 0;
+}
+
+int output_tcpcli_nonblocking(output_tcpcli_t* self)
+{
+ int flags;
+ mlassert_self();
+
+ if (self->fd < 0) {
+ lfatal("not connected");
+ }
+
+ flags = fcntl(self->fd, F_GETFL);
+ if (flags != -1) {
+ flags = flags & O_NONBLOCK ? 1 : 0;
+ }
+
+ return flags;
+}
+
+int output_tcpcli_set_nonblocking(output_tcpcli_t* self, int nonblocking)
+{
+ int flags;
+ mlassert_self();
+
+ if (self->fd < 0) {
+ lfatal("not connected");
+ }
+
+ if ((flags = fcntl(self->fd, F_GETFL)) == -1) {
+ lcritical("fcntl(FL_GETFL) error %s", core_log_errstr(errno));
+ return -1;
+ }
+
+ if (nonblocking) {
+ flags |= O_NONBLOCK;
+ self->blocking = 0;
+ } else {
+ flags &= ~O_NONBLOCK;
+ self->blocking = 1;
+ }
+
+ if (fcntl(self->fd, F_SETFL, flags | O_NONBLOCK)) {
+ lcritical("fcntl(FL_SETFL, %x) error %s", flags, core_log_errstr(errno));
+ return -1;
+ }
+
+ return 0;
+}
+
+static void _receive(output_tcpcli_t* self, const core_object_t* obj)
+{
+ const uint8_t* payload;
+ size_t len, sent;
+ uint16_t dnslen;
+ mlassert_self();
+
+ for (; obj;) {
+ switch (obj->obj_type) {
+ case CORE_OBJECT_DNS:
+ obj = obj->obj_prev;
+ continue;
+ case CORE_OBJECT_PAYLOAD:
+ payload = ((core_object_payload_t*)obj)->payload;
+ len = ((core_object_payload_t*)obj)->len;
+ break;
+ default:
+ return;
+ }
+
+ sent = 0;
+ dnslen = htons(len);
+
+ for (;;) {
+ ssize_t ret = sendto(self->fd, ((uint8_t*)&dnslen) + sent, sizeof(dnslen) - sent, 0, 0, 0);
+ if (ret > -1) {
+ sent += ret;
+ if (sent < sizeof(dnslen))
+ continue;
+
+ sent = 0;
+ for (;;) {
+ ssize_t ret = sendto(self->fd, payload + sent, len - sent, 0, 0, 0);
+ if (ret > -1) {
+ sent += ret;
+ if (sent < len)
+ continue;
+ self->pkts++;
+ return;
+ }
+ switch (errno) {
+ case EAGAIN:
+#if EAGAIN != EWOULDBLOCK
+ case EWOULDBLOCK:
+#endif
+ continue;
+ default:
+ break;
+ }
+ break;
+ }
+ self->errs++;
+ return;
+ }
+ switch (errno) {
+ case EAGAIN:
+#if EAGAIN != EWOULDBLOCK
+ case EWOULDBLOCK:
+#endif
+ continue;
+ default:
+ break;
+ }
+ break;
+ }
+ self->errs++;
+ break;
+ }
+}
+
+core_receiver_t output_tcpcli_receiver(output_tcpcli_t* self)
+{
+ mlassert_self();
+
+ if (self->fd < 0) {
+ lfatal("not connected");
+ }
+
+ return (core_receiver_t)_receive;
+}
+
+static const core_object_t* _produce(output_tcpcli_t* self)
+{
+ ssize_t n, recv = 0;
+ uint16_t dnslen;
+ struct pollfd p;
+ int to = 0;
+ mlassert_self();
+
+ // Check if last recvfrom() got more then we needed
+ if (!self->have_dnslen && self->recv > self->dnslen) {
+ recv = self->recv - self->dnslen;
+ if (recv < sizeof(dnslen)) {
+ memcpy(((uint8_t*)&dnslen), self->recvbuf + self->dnslen, recv);
+ } else {
+ memcpy(((uint8_t*)&dnslen), self->recvbuf + self->dnslen, sizeof(dnslen));
+
+ if (recv > sizeof(dnslen)) {
+ self->recv = recv - sizeof(dnslen);
+ memmove(self->recvbuf, self->recvbuf + self->dnslen + sizeof(dnslen), self->recv);
+ } else {
+ self->recv = 0;
+ }
+
+ self->dnslen = ntohs(dnslen);
+ self->have_dnslen = 1;
+
+ if (self->recv > self->dnslen) {
+ self->pkts_recv++;
+ self->pkt.len = self->dnslen;
+ self->have_dnslen = 0;
+ return (core_object_t*)&self->pkt;
+ }
+ }
+ }
+
+ if (self->blocking) {
+ p.fd = self->fd;
+ p.events = POLLIN;
+ p.revents = 0;
+ to = (self->timeout.sec * 1e3) + (self->timeout.nsec / 1e6); //NOSONAR
+ if (!to) {
+ to = 1;
+ }
+ }
+
+ if (!self->have_dnslen) {
+ for (;;) {
+ n = poll(&p, 1, to);
+ if (n < 0 || (p.revents & (POLLERR | POLLHUP | POLLNVAL))) {
+ self->errs++;
+ return 0;
+ }
+ if (!n || !(p.revents & POLLIN)) {
+ if (recv) {
+ self->errs++;
+ return 0;
+ }
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ }
+
+ n = recvfrom(self->fd, ((uint8_t*)&dnslen) + recv, sizeof(dnslen) - recv, 0, 0, 0);
+ if (n > 0) {
+ recv += n;
+ if (recv < sizeof(dnslen))
+ continue;
+ break;
+ }
+ if (!n) {
+ break;
+ }
+ switch (errno) {
+ case EAGAIN:
+#if EAGAIN != EWOULDBLOCK
+ case EWOULDBLOCK:
+#endif
+ continue;
+ default:
+ break;
+ }
+ self->errs++;
+ break;
+ }
+
+ if (n < 1) {
+ return 0;
+ }
+
+ self->dnslen = ntohs(dnslen);
+ self->have_dnslen = 1;
+ self->recv = 0;
+ }
+
+ for (;;) {
+ n = poll(&p, 1, to);
+ if (n < 0 || (p.revents & (POLLERR | POLLHUP | POLLNVAL))) {
+ self->errs++;
+ return 0;
+ }
+ if (!n || !(p.revents & POLLIN)) {
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ }
+
+ n = recvfrom(self->fd, self->recvbuf + self->recv, sizeof(self->recvbuf) - self->recv, 0, 0, 0);
+ if (n > 0) {
+ self->recv += n;
+ if (self->recv < self->dnslen)
+ continue;
+ break;
+ }
+ if (!n) {
+ break;
+ }
+ switch (errno) {
+ case EAGAIN:
+#if EAGAIN != EWOULDBLOCK
+ case EWOULDBLOCK:
+#endif
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ default:
+ break;
+ }
+ self->errs++;
+ break;
+ }
+
+ if (n < 1) {
+ return 0;
+ }
+
+ self->pkts_recv++;
+ self->pkt.len = self->dnslen;
+ self->have_dnslen = 0;
+ return (core_object_t*)&self->pkt;
+}
+
+core_producer_t output_tcpcli_producer(output_tcpcli_t* self)
+{
+ mlassert_self();
+
+ if (self->fd < 0) {
+ lfatal("not connected");
+ }
+
+ return (core_producer_t)_produce;
+}
diff --git a/src/output/tcpcli.h b/src/output/tcpcli.h
new file mode 100644
index 0000000..6eb80f3
--- /dev/null
+++ b/src/output/tcpcli.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "core/log.h"
+#include "core/receiver.h"
+#include "core/producer.h"
+#include "core/object/payload.h"
+#include "core/timespec.h"
+
+#ifndef __dnsjit_output_tcpcli_h
+#define __dnsjit_output_tcpcli_h
+
+#include "output/tcpcli.hh"
+
+#endif
diff --git a/src/output/tcpcli.hh b/src/output/tcpcli.hh
new file mode 100644
index 0000000..277b0dd
--- /dev/null
+++ b/src/output/tcpcli.hh
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+//lua:require("dnsjit.core.log")
+//lua:require("dnsjit.core.receiver_h")
+//lua:require("dnsjit.core.producer_h")
+//lua:require("dnsjit.core.object.payload_h")
+//lua:require("dnsjit.core.timespec_h")
+
+typedef struct output_tcpcli {
+ core_log_t _log;
+ size_t pkts, errs;
+ int fd;
+
+ uint8_t recvbuf[64 * 1024];
+ core_object_payload_t pkt;
+ uint16_t dnslen;
+ uint8_t have_dnslen;
+ size_t recv, pkts_recv;
+
+ core_timespec_t timeout;
+ int8_t blocking;
+} output_tcpcli_t;
+
+core_log_t* output_tcpcli_log();
+
+void output_tcpcli_init(output_tcpcli_t* self);
+void output_tcpcli_destroy(output_tcpcli_t* self);
+int output_tcpcli_connect(output_tcpcli_t* self, const char* host, const char* port);
+int output_tcpcli_nonblocking(output_tcpcli_t* self);
+int output_tcpcli_set_nonblocking(output_tcpcli_t* self, int nonblocking);
+
+core_receiver_t output_tcpcli_receiver(output_tcpcli_t* self);
+core_producer_t output_tcpcli_producer(output_tcpcli_t* self);
diff --git a/src/output/tcpcli.lua b/src/output/tcpcli.lua
new file mode 100644
index 0000000..d57de88
--- /dev/null
+++ b/src/output/tcpcli.lua
@@ -0,0 +1,131 @@
+-- Copyright (c) 2018-2021, OARC, Inc.
+-- All rights reserved.
+--
+-- This file is part of dnsjit.
+--
+-- dnsjit is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- dnsjit is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+
+-- dnsjit.output.tcpcli
+-- Simple, length aware, TCP client
+-- local output = require("dnsjit.output.tcpcli").new("127.0.0.1", "53")
+--
+-- Simple TCP client that takes any payload you give it, sends the length of
+-- the payload as an unsigned 16 bit integer and then sends the payload.
+-- When receiving it will first retrieve the length of the payload as an
+-- unsigned 16 bit integer and it will stall until it gets, even if
+-- nonblocking mode is used.
+-- Then it will retrieve at least that amount of bytes, if nonblocking mode
+-- is used here then it will return a payload object with length zero if
+-- there was nothing to receive or if the full payload have not been received
+-- yet.
+-- Additional calls will continue retrieving the payload.
+-- .SS Attributes
+-- .TP
+-- timeout
+-- A
+-- .I core.timespec
+-- that is used when producing objects.
+module(...,package.seeall)
+
+require("dnsjit.output.tcpcli_h")
+local ffi = require("ffi")
+local C = ffi.C
+
+local t_name = "output_tcpcli_t"
+local output_tcpcli_t = ffi.typeof(t_name)
+local Tcpcli = {}
+
+-- Create a new Tcpcli output.
+function Tcpcli.new()
+ local self = {
+ obj = output_tcpcli_t(),
+ }
+ C.output_tcpcli_init(self.obj)
+ ffi.gc(self.obj, C.output_tcpcli_destroy)
+ return setmetatable(self, { __index = Tcpcli })
+end
+
+-- Set the timeout when producing objects.
+function Tcpcli:timeout(seconds, nanoseconds)
+ self.obj.timeout.sec = seconds
+ self.obj.timeout.nsec = nanoseconds
+end
+
+-- Connect to the
+-- .I host
+-- and
+-- .I port
+-- and return 0 if successful.
+function Tcpcli:connect(host, port)
+ return C.output_tcpcli_connect(self.obj, host, port)
+end
+
+-- Enable (true) or disable (false) nonblocking mode and
+-- return 0 if successful, if
+-- .I bool
+-- is not specified then return if nonblocking mode is on (true) or off (false).
+function Tcpcli:nonblocking(bool)
+ if bool == nil then
+ if C.output_tcpcli_nonblocking(self.obj) == 1 then
+ return true
+ end
+ return false
+ elseif bool == true then
+ return C.output_tcpcli_set_nonblocking(self.obj, 1)
+ else
+ return C.output_tcpcli_set_nonblocking(self.obj, 0)
+ end
+end
+
+-- Return the C functions and context for receiving objects, these objects
+-- will be sent.
+function Tcpcli:receive()
+ return C.output_tcpcli_receiver(self.obj), self.obj
+end
+
+-- Return the C functions and context for producing objects, these objects
+-- are received.
+-- If nonblocking mode is enabled the producer will return a payload object
+-- with length zero if there was nothing to receive or if the full payload
+-- have not been received yet.
+-- If nonblocking mode is disabled the producer will wait for data and if
+-- timed out (see
+-- .IR timeout )
+-- it will return a payload object with length zero.
+-- If a timeout happens during during the first stage, getting the length, it
+-- will fail and return nil.
+-- Additional calls will continue retrieving the payload.
+-- The producer returns nil on error.
+function Tcpcli:produce()
+ return C.output_tcpcli_producer(self.obj), self.obj
+end
+
+-- Return the number of "packets" sent, actually the number of completely sent
+-- payloads.
+function Tcpcli:packets()
+ return tonumber(self.obj.pkts)
+end
+
+-- Return the number of "packets" received, actually the number of completely
+-- received DNS messages.
+function Tcpcli:received()
+ return tonumber(self.obj.pkts_recv)
+end
+
+-- Return the number of errors when sending.
+function Tcpcli:errors()
+ return tonumber(self.obj.errs)
+end
+
+return Tcpcli
diff --git a/src/output/tlscli.c b/src/output/tlscli.c
new file mode 100644
index 0000000..8c5947a
--- /dev/null
+++ b/src/output/tlscli.c
@@ -0,0 +1,345 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * 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/tlscli.h"
+#include "core/assert.h"
+#include "core/object/dns.h"
+#include "core/object/payload.h"
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <string.h>
+#include <arpa/inet.h>
+#include <poll.h>
+
+static core_log_t _log = LOG_T_INIT("output.tlscli");
+static output_tlscli_t _defaults = {
+ LOG_T_INIT_OBJ("output.tlscli"),
+ 0, 0, -1, 0,
+ { 0 }, CORE_OBJECT_PAYLOAD_INIT(0),
+ 0, 0, 0, 0,
+ { 5, 0 },
+ 0, 0
+};
+
+core_log_t* output_tlscli_log()
+{
+ return &_log;
+}
+
+void output_tlscli_init(output_tlscli_t* self)
+{
+ int err;
+ mlassert_self();
+
+ *self = _defaults;
+ self->pkt.payload = self->recvbuf;
+
+ gnutls_global_init();
+ if ((err = gnutls_certificate_allocate_credentials(&self->cred)) != GNUTLS_E_SUCCESS) {
+ lfatal("gnutls_certificate_allocate_credentials() error: %s", gnutls_strerror(err));
+ } else if ((err = gnutls_init(&self->session, GNUTLS_CLIENT)) != GNUTLS_E_SUCCESS) {
+ lfatal("gnutls_init() error: %s", gnutls_strerror(err));
+ } else if ((err = gnutls_set_default_priority(self->session)) != GNUTLS_E_SUCCESS) {
+ lfatal("gnutls_set_default_priority() error: %s", gnutls_strerror(err));
+ } else if ((err = gnutls_credentials_set(self->session, GNUTLS_CRD_CERTIFICATE, self->cred)) != GNUTLS_E_SUCCESS) {
+ lfatal("gnutls_credentials_set() error: %s", gnutls_strerror(err));
+ }
+
+ gnutls_handshake_set_timeout(self->session, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);
+}
+
+void output_tlscli_destroy(output_tlscli_t* self)
+{
+ mlassert_self();
+
+ if (self->fd > -1) {
+ if (self->session) {
+ gnutls_bye(self->session, GNUTLS_SHUT_RDWR);
+ gnutls_deinit(self->session);
+ }
+ shutdown(self->fd, SHUT_RDWR);
+ close(self->fd);
+ if (self->cred) {
+ gnutls_certificate_free_credentials(self->cred);
+ }
+ }
+}
+
+int output_tlscli_connect(output_tlscli_t* self, const char* host, const char* port)
+{
+ struct addrinfo* addr;
+ int err;
+ unsigned int ms;
+ mlassert_self();
+ lassert(host, "host is nil");
+ lassert(port, "port is nil");
+
+ if (self->fd > -1) {
+ lfatal("already connected");
+ }
+ if (self->tls_ok) {
+ lfatal("TLS already established");
+ }
+
+ if ((err = getaddrinfo(host, port, 0, &addr))) {
+ lcritical("getaddrinfo(%s, %s) error %s", host, port, gai_strerror(err));
+ return -1;
+ }
+ if (!addr) {
+ lcritical("getaddrinfo failed, no address returned");
+ return -1;
+ }
+
+ if ((self->fd = socket(addr->ai_addr->sa_family, SOCK_STREAM, 0)) < 0) {
+ lcritical("socket() error %s", core_log_errstr(errno));
+ freeaddrinfo(addr);
+ return -2;
+ }
+
+ if (connect(self->fd, addr->ai_addr, addr->ai_addrlen)) {
+ lcritical("connect() error %s", core_log_errstr(errno));
+ freeaddrinfo(addr);
+ return -2;
+ }
+
+ freeaddrinfo(addr);
+
+ gnutls_transport_set_int(self->session, self->fd);
+ ms = (self->timeout.sec * 1000) + (self->timeout.nsec / 1000000);
+ if (!ms && self->timeout.nsec) {
+ ms = 1;
+ }
+ gnutls_record_set_timeout(self->session, ms);
+
+ /* Establish TLS */
+ do {
+ err = gnutls_handshake(self->session);
+ } while (err < 0 && gnutls_error_is_fatal(err) == 0);
+ if (err == GNUTLS_E_PREMATURE_TERMINATION) {
+ lcritical("gnutls_handshake() error: %s", gnutls_strerror(err));
+ return -3;
+ } else if (err < 0) {
+ lcritical("gnutls_handshake() failed: %s (%d)\n", gnutls_strerror(err), err);
+ return -3;
+ }
+
+ self->tls_ok = 1;
+ return 0;
+}
+
+static void _receive(output_tlscli_t* self, const core_object_t* obj)
+{
+ const uint8_t* payload;
+ size_t len, sent;
+ uint16_t dnslen;
+ ssize_t ret;
+ mlassert_self();
+
+ for (; obj;) {
+ switch (obj->obj_type) {
+ case CORE_OBJECT_DNS:
+ obj = obj->obj_prev;
+ continue;
+ case CORE_OBJECT_PAYLOAD:
+ payload = ((core_object_payload_t*)obj)->payload;
+ len = ((core_object_payload_t*)obj)->len;
+ break;
+ default:
+ return;
+ }
+
+ sent = 0;
+ dnslen = htons(len);
+
+ for (;;) {
+ ret = gnutls_record_send(self->session, ((uint8_t*)&dnslen) + sent, sizeof(dnslen) - sent);
+ if (ret > -1) {
+ sent += ret;
+ if (sent < sizeof(dnslen))
+ continue;
+
+ sent = 0;
+ for (;;) {
+ ret = gnutls_record_send(self->session, payload + sent, len - sent);
+ if (ret > -1) {
+ sent += ret;
+ if (sent < len)
+ continue;
+ self->pkts++;
+ return;
+ }
+ switch (ret) {
+ case GNUTLS_E_AGAIN:
+ case GNUTLS_E_TIMEDOUT:
+ continue;
+ default:
+ break;
+ }
+ break;
+ }
+ self->errs++;
+ return;
+ }
+ switch (ret) {
+ case GNUTLS_E_AGAIN:
+ case GNUTLS_E_TIMEDOUT:
+ continue;
+ default:
+ break;
+ }
+ break;
+ }
+ self->errs++;
+ break;
+ }
+}
+
+core_receiver_t output_tlscli_receiver(output_tlscli_t* self)
+{
+ mlassert_self();
+
+ if (self->fd < 0) {
+ lfatal("not connected");
+ }
+ if (!self->tls_ok) {
+ lfatal("TLS is not established");
+ }
+
+ return (core_receiver_t)_receive;
+}
+
+static const core_object_t* _produce(output_tlscli_t* self)
+{
+ ssize_t n, recv = 0;
+ uint16_t dnslen;
+ mlassert_self();
+
+ // Check if last recvfrom() got more then we needed
+ if (!self->have_dnslen && self->recv > self->dnslen) {
+ recv = self->recv - self->dnslen;
+ if (recv < sizeof(dnslen)) {
+ memcpy(((uint8_t*)&dnslen), self->recvbuf + self->dnslen, recv);
+ } else {
+ memcpy(((uint8_t*)&dnslen), self->recvbuf + self->dnslen, sizeof(dnslen));
+
+ if (recv > sizeof(dnslen)) {
+ self->recv = recv - sizeof(dnslen);
+ memmove(self->recvbuf, self->recvbuf + self->dnslen + sizeof(dnslen), self->recv);
+ } else {
+ self->recv = 0;
+ }
+
+ self->dnslen = ntohs(dnslen);
+ self->have_dnslen = 1;
+
+ if (self->recv > self->dnslen) {
+ self->pkts_recv++;
+ self->pkt.len = self->dnslen;
+ self->have_dnslen = 0;
+ return (core_object_t*)&self->pkt;
+ }
+ }
+ }
+
+ if (!self->have_dnslen) {
+ for (;;) {
+ n = gnutls_record_recv(self->session, ((uint8_t*)&dnslen) + recv, sizeof(dnslen) - recv);
+ if (n > 0) {
+ recv += n;
+ if (recv < sizeof(dnslen))
+ continue;
+ break;
+ }
+ if (!n) {
+ break;
+ }
+ switch (n) {
+ case GNUTLS_E_AGAIN:
+ case GNUTLS_E_TIMEDOUT:
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ default:
+ break;
+ }
+ self->errs++;
+ break;
+ }
+
+ if (n < 1) {
+ return 0;
+ }
+
+ self->dnslen = ntohs(dnslen);
+ self->have_dnslen = 1;
+ self->recv = 0;
+ }
+
+ for (;;) {
+ n = gnutls_record_recv(self->session, self->recvbuf + self->recv, sizeof(self->recvbuf) - self->recv);
+ if (n > 0) {
+ self->recv += n;
+ if (self->recv < self->dnslen)
+ continue;
+ break;
+ }
+ if (!n) {
+ break;
+ }
+ switch (n) {
+ case GNUTLS_E_AGAIN:
+ case GNUTLS_E_TIMEDOUT:
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ default:
+ break;
+ }
+ self->errs++;
+ break;
+ }
+
+ if (n < 1) {
+ return 0;
+ }
+
+ self->pkts_recv++;
+ self->pkt.len = self->dnslen;
+ self->have_dnslen = 0;
+ return (core_object_t*)&self->pkt;
+}
+
+core_producer_t output_tlscli_producer(output_tlscli_t* self)
+{
+ mlassert_self();
+
+ if (self->fd < 0) {
+ lfatal("not connected");
+ }
+ if (!self->tls_ok) {
+ lfatal("TLS is not established");
+ }
+
+ return (core_producer_t)_produce;
+}
diff --git a/src/output/tlscli.h b/src/output/tlscli.h
new file mode 100644
index 0000000..2bdf69f
--- /dev/null
+++ b/src/output/tlscli.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "core/log.h"
+#include "core/receiver.h"
+#include "core/producer.h"
+#include "core/object/payload.h"
+#include "core/timespec.h"
+
+#ifndef __dnsjit_output_tlscli_h
+#define __dnsjit_output_tlscli_h
+
+#include <gnutls/gnutls.h>
+
+#include "output/tlscli.hh"
+
+#endif
diff --git a/src/output/tlscli.hh b/src/output/tlscli.hh
new file mode 100644
index 0000000..4a0c142
--- /dev/null
+++ b/src/output/tlscli.hh
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+//lua:require("dnsjit.core.compat_h")
+//lua:require("dnsjit.core.log")
+//lua:require("dnsjit.core.receiver_h")
+//lua:require("dnsjit.core.producer_h")
+//lua:require("dnsjit.core.object.payload_h")
+//lua:require("dnsjit.core.timespec_h")
+
+typedef struct output_tlscli {
+ core_log_t _log;
+ size_t pkts, errs;
+ int fd, tls_ok;
+
+ uint8_t recvbuf[64 * 1024];
+ core_object_payload_t pkt;
+ uint16_t dnslen;
+ uint8_t have_dnslen;
+ size_t recv, pkts_recv;
+
+ core_timespec_t timeout;
+
+ gnutls_session_t session;
+ gnutls_certificate_credentials_t cred;
+} output_tlscli_t;
+
+core_log_t* output_tlscli_log();
+
+void output_tlscli_init(output_tlscli_t* self);
+void output_tlscli_destroy(output_tlscli_t* self);
+int output_tlscli_connect(output_tlscli_t* self, const char* host, const char* port);
+
+core_receiver_t output_tlscli_receiver(output_tlscli_t* self);
+core_producer_t output_tlscli_producer(output_tlscli_t* self);
diff --git a/src/output/tlscli.lua b/src/output/tlscli.lua
new file mode 100644
index 0000000..e37439b
--- /dev/null
+++ b/src/output/tlscli.lua
@@ -0,0 +1,103 @@
+-- Copyright (c) 2018-2021, OARC, Inc.
+-- All rights reserved.
+--
+-- This file is part of dnsjit.
+--
+-- dnsjit is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- dnsjit is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+
+-- dnsjit.output.tlscli
+-- Simple TLS client
+-- local output = require("dnsjit.output.tlscli").new("127.0.0.1", "853")
+--
+-- Simple TLS client that attempts to do a TLS handshake (without
+-- certificate verification). It behaves the same way as tcpcli, except all
+-- the data is sent over the encrypted channel.
+-- .SS Attributes
+-- .TP
+-- timeout
+-- A
+-- .I core.timespec
+-- that is used when producing objects.
+module(...,package.seeall)
+
+require("dnsjit.output.tlscli_h")
+local ffi = require("ffi")
+local C = ffi.C
+
+local t_name = "output_tlscli_t"
+local output_tlscli_t = ffi.typeof(t_name)
+local Tlscli = {}
+
+-- Create a new Tlscli output.
+function Tlscli.new()
+ local self = {
+ obj = output_tlscli_t(),
+ }
+ C.output_tlscli_init(self.obj)
+ ffi.gc(self.obj, C.output_tlscli_destroy)
+ return setmetatable(self, { __index = Tlscli })
+end
+
+-- Set the timeout when producing objects.
+function Tlscli:timeout(seconds, nanoseconds)
+ self.obj.timeout.sec = seconds
+ self.obj.timeout.nsec = nanoseconds
+end
+
+-- Connect to the
+-- .I host
+-- and
+-- .I port
+-- , perform a TLS handshake and return 0 if successful.
+function Tlscli:connect(host, port)
+ return C.output_tlscli_connect(self.obj, host, port)
+end
+
+-- Return the C functions and context for receiving objects, these objects
+-- will be sent.
+function Tlscli:receive()
+ return C.output_tlscli_receiver(self.obj), self.obj
+end
+
+-- Return the C functions and context for producing objects, these objects
+-- are received.
+-- The producer will wait for data and if timed out (see
+-- .IR timeout )
+-- it will return a payload object with length zero.
+-- If a timeout happens during during the first stage, getting the length, it
+-- will fail and return nil.
+-- Additional calls will continue retrieving the payload.
+-- The producer returns nil on error.
+function Tlscli:produce()
+ return C.output_tlscli_producer(self.obj), self.obj
+end
+
+-- Return the number of "packets" sent, actually the number of completely sent
+-- payloads.
+function Tlscli:packets()
+ return tonumber(self.obj.pkts)
+end
+
+-- Return the number of "packets" received, actually the number of completely
+-- received DNS messages.
+function Tlscli:received()
+ return tonumber(self.obj.pkts_recv)
+end
+
+-- Return the number of errors when sending.
+function Tlscli:errors()
+ return tonumber(self.obj.errs)
+end
+
+return Tlscli
diff --git a/src/output/udpcli.c b/src/output/udpcli.c
new file mode 100644
index 0000000..b207d9c
--- /dev/null
+++ b/src/output/udpcli.c
@@ -0,0 +1,300 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * 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/udpcli.h"
+#include "core/assert.h"
+#include "core/object/dns.h"
+#include "core/object/payload.h"
+
+#include <netdb.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <string.h>
+#include <poll.h>
+
+static core_log_t _log = LOG_T_INIT("output.udpcli");
+static output_udpcli_t _defaults = {
+ LOG_T_INIT_OBJ("output.udpcli"),
+ 0, 0, -1,
+ { 0 }, 0,
+ { 0 }, CORE_OBJECT_PAYLOAD_INIT(0), 0,
+ { 5, 0 }, 1
+};
+
+core_log_t* output_udpcli_log()
+{
+ return &_log;
+}
+
+void output_udpcli_init(output_udpcli_t* self)
+{
+ mlassert_self();
+
+ *self = _defaults;
+ self->pkt.payload = self->recvbuf;
+}
+
+void output_udpcli_destroy(output_udpcli_t* self)
+{
+ mlassert_self();
+
+ if (self->fd > -1) {
+ shutdown(self->fd, SHUT_RDWR);
+ close(self->fd);
+ }
+}
+
+int output_udpcli_connect(output_udpcli_t* self, const char* host, const char* port)
+{
+ struct addrinfo* addr;
+ int err;
+ mlassert_self();
+ lassert(host, "host is nil");
+ lassert(port, "port is nil");
+
+ if (self->fd > -1) {
+ lfatal("already connected");
+ }
+
+ if ((err = getaddrinfo(host, port, 0, &addr))) {
+ lcritical("getaddrinfo(%s, %s) error %s", host, port, gai_strerror(err));
+ return -1;
+ }
+ if (!addr) {
+ lcritical("getaddrinfo failed, no address returned");
+ return -1;
+ }
+
+ memcpy(&self->addr, addr->ai_addr, addr->ai_addrlen);
+ self->addr_len = addr->ai_addrlen;
+ freeaddrinfo(addr);
+
+ if ((self->fd = socket(((struct sockaddr*)&self->addr)->sa_family, SOCK_DGRAM, 0)) < 0) {
+ lcritical("socket() error %s", core_log_errstr(errno));
+ return -2;
+ }
+
+ return 0;
+}
+
+int output_udpcli_nonblocking(output_udpcli_t* self)
+{
+ int flags;
+ mlassert_self();
+
+ if (self->fd < 0) {
+ lfatal("not connected");
+ }
+
+ flags = fcntl(self->fd, F_GETFL);
+ if (flags != -1) {
+ flags = flags & O_NONBLOCK ? 1 : 0;
+ }
+
+ return flags;
+}
+
+int output_udpcli_set_nonblocking(output_udpcli_t* self, int nonblocking)
+{
+ int flags;
+ mlassert_self();
+
+ if (self->fd < 0) {
+ lfatal("not connected");
+ }
+
+ if ((flags = fcntl(self->fd, F_GETFL)) == -1) {
+ lcritical("fcntl(FL_GETFL) error %s", core_log_errstr(errno));
+ return -1;
+ }
+
+ if (nonblocking) {
+ flags |= O_NONBLOCK;
+ self->blocking = 0;
+ } else {
+ flags &= ~O_NONBLOCK;
+ self->blocking = 1;
+ }
+
+ if (fcntl(self->fd, F_SETFL, flags | O_NONBLOCK)) {
+ lcritical("fcntl(FL_SETFL, %x) error %s", flags, core_log_errstr(errno));
+ return -1;
+ }
+
+ return 0;
+}
+
+static void _receive(output_udpcli_t* self, const core_object_t* obj)
+{
+ const uint8_t* payload;
+ size_t len, sent;
+ mlassert_self();
+
+ for (; obj;) {
+ switch (obj->obj_type) {
+ case CORE_OBJECT_DNS:
+ obj = obj->obj_prev;
+ continue;
+ case CORE_OBJECT_PAYLOAD:
+ payload = ((core_object_payload_t*)obj)->payload;
+ len = ((core_object_payload_t*)obj)->len;
+ break;
+ default:
+ return;
+ }
+
+ sent = 0;
+ for (;;) {
+ ssize_t ret = sendto(self->fd, payload + sent, len - sent, 0, (struct sockaddr*)&self->addr, self->addr_len);
+ if (ret > -1) {
+ sent += ret;
+ if (sent < len)
+ continue;
+ self->pkts++;
+ return;
+ }
+ switch (errno) {
+ case EAGAIN:
+#if EAGAIN != EWOULDBLOCK
+ case EWOULDBLOCK:
+#endif
+ continue;
+ default:
+ break;
+ }
+ break;
+ }
+ self->errs++;
+ break;
+ }
+}
+
+core_receiver_t output_udpcli_receiver(output_udpcli_t* self)
+{
+ mlassert_self();
+
+ if (self->fd < 0) {
+ lfatal("not connected");
+ }
+
+ return (core_receiver_t)_receive;
+}
+
+static const core_object_t* _produce(output_udpcli_t* self)
+{
+ ssize_t n;
+ mlassert_self();
+
+ for (;;) {
+ n = recvfrom(self->fd, self->recvbuf, sizeof(self->recvbuf), 0, 0, 0);
+ if (n > -1) {
+ break;
+ }
+ switch (errno) {
+ case EAGAIN:
+#if EAGAIN != EWOULDBLOCK
+ case EWOULDBLOCK:
+#endif
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ default:
+ break;
+ }
+ self->errs++;
+ break;
+ }
+
+ if (n < 1) {
+ return 0;
+ }
+
+ self->pkts_recv++;
+ self->pkt.len = n;
+ return (core_object_t*)&self->pkt;
+}
+
+static const core_object_t* _produce_block(output_udpcli_t* self)
+{
+ ssize_t n;
+ struct pollfd p;
+ int to;
+ mlassert_self();
+
+ p.fd = self->fd;
+ p.events = POLLIN;
+ p.revents = 0;
+ to = (self->timeout.sec * 1e3) + (self->timeout.nsec / 1e6); //NOSONAR
+ if (!to) {
+ to = 1;
+ }
+
+ n = poll(&p, 1, to);
+ if (n < 0 || (p.revents & (POLLERR | POLLHUP | POLLNVAL))) {
+ self->errs++;
+ return 0;
+ }
+ if (!n || !(p.revents & POLLIN)) {
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ }
+
+ for (;;) {
+ n = recvfrom(self->fd, self->recvbuf, sizeof(self->recvbuf), 0, 0, 0);
+ if (n > -1) {
+ break;
+ }
+ switch (errno) {
+ case EAGAIN:
+#if EAGAIN != EWOULDBLOCK
+ case EWOULDBLOCK:
+#endif
+ self->pkt.len = 0;
+ return (core_object_t*)&self->pkt;
+ default:
+ break;
+ }
+ self->errs++;
+ break;
+ }
+
+ if (n < 1) {
+ return 0;
+ }
+
+ self->pkts_recv++;
+ self->pkt.len = n;
+ return (core_object_t*)&self->pkt;
+}
+
+core_producer_t output_udpcli_producer(output_udpcli_t* self)
+{
+ mlassert_self();
+
+ if (self->fd < 0) {
+ lfatal("not connected");
+ }
+
+ if (self->blocking) {
+ return (core_producer_t)_produce_block;
+ }
+ return (core_producer_t)_produce;
+}
diff --git a/src/output/udpcli.h b/src/output/udpcli.h
new file mode 100644
index 0000000..4cb7af2
--- /dev/null
+++ b/src/output/udpcli.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "core/log.h"
+#include "core/receiver.h"
+#include "core/producer.h"
+#include "core/object/payload.h"
+#include "core/timespec.h"
+
+#ifndef __dnsjit_output_udpcli_h
+#define __dnsjit_output_udpcli_h
+
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include "output/udpcli.hh"
+
+#endif
diff --git a/src/output/udpcli.hh b/src/output/udpcli.hh
new file mode 100644
index 0000000..084a5b6
--- /dev/null
+++ b/src/output/udpcli.hh
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018-2021, OARC, Inc.
+ * All rights reserved.
+ *
+ * This file is part of dnsjit.
+ *
+ * dnsjit is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * dnsjit is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+//lua:require("dnsjit.core.compat_h")
+//lua:require("dnsjit.core.log")
+//lua:require("dnsjit.core.receiver_h")
+//lua:require("dnsjit.core.producer_h")
+//lua:require("dnsjit.core.object.payload_h")
+//lua:require("dnsjit.core.timespec_h")
+
+typedef struct output_udpcli {
+ core_log_t _log;
+ size_t pkts, errs;
+ int fd;
+
+ struct sockaddr_storage addr;
+ size_t addr_len;
+
+ uint8_t recvbuf[4 * 1024];
+ core_object_payload_t pkt;
+ size_t pkts_recv;
+
+ core_timespec_t timeout;
+ int8_t blocking;
+} output_udpcli_t;
+
+core_log_t* output_udpcli_log();
+
+void output_udpcli_init(output_udpcli_t* self);
+void output_udpcli_destroy(output_udpcli_t* self);
+int output_udpcli_connect(output_udpcli_t* self, const char* host, const char* port);
+int output_udpcli_nonblocking(output_udpcli_t* self);
+int output_udpcli_set_nonblocking(output_udpcli_t* self, int nonblocking);
+
+core_receiver_t output_udpcli_receiver(output_udpcli_t* self);
+core_producer_t output_udpcli_producer(output_udpcli_t* self);
diff --git a/src/output/udpcli.lua b/src/output/udpcli.lua
new file mode 100644
index 0000000..0584725
--- /dev/null
+++ b/src/output/udpcli.lua
@@ -0,0 +1,121 @@
+-- Copyright (c) 2018-2021, OARC, Inc.
+-- All rights reserved.
+--
+-- This file is part of dnsjit.
+--
+-- dnsjit is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- dnsjit is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+
+-- dnsjit.output.udpcli
+-- Simple and dumb UDP DNS client
+-- local output = require("dnsjit.output.udpcli").new("127.0.0.1", "53")
+--
+-- Simple and rather dumb DNS client that takes any payload you give it and
+-- sends the full payload over UDP.
+-- .SS Attributes
+-- .TP
+-- timeout
+-- A
+-- .I core.timespec
+-- that is used when producing objects.
+module(...,package.seeall)
+
+require("dnsjit.output.udpcli_h")
+local ffi = require("ffi")
+local C = ffi.C
+
+local t_name = "output_udpcli_t"
+local output_udpcli_t = ffi.typeof(t_name)
+local Udpcli = {}
+
+-- Create a new Udpcli output.
+function Udpcli.new()
+ local self = {
+ obj = output_udpcli_t(),
+ }
+ C.output_udpcli_init(self.obj)
+ ffi.gc(self.obj, C.output_udpcli_destroy)
+ return setmetatable(self, { __index = Udpcli })
+end
+
+-- Set the timeout when producing objects.
+function Udpcli:timeout(seconds, nanoseconds)
+ self.obj.timeout.sec = seconds
+ self.obj.timeout.nsec = nanoseconds
+end
+
+-- Connect to the
+-- .I host
+-- and
+-- .I port
+-- and return 0 if successful.
+function Udpcli:connect(host, port)
+ return C.output_udpcli_connect(self.obj, host, port)
+end
+
+-- Enable (true) or disable (false) nonblocking mode and
+-- return 0 if successful, if
+-- .I bool
+-- is not specified then return if nonblocking mode is on (true) or off (false).
+function Udpcli:nonblocking(bool)
+ if bool == nil then
+ if C.output_udpcli_nonblocking(self.obj) == 1 then
+ return true
+ end
+ return false
+ elseif bool == true then
+ return C.output_udpcli_set_nonblocking(self.obj, 1)
+ else
+ return C.output_udpcli_set_nonblocking(self.obj, 0)
+ end
+end
+
+-- Return the C functions and context for receiving objects, these objects
+-- will be sent.
+function Udpcli:receive()
+ return C.output_udpcli_receiver(self.obj), self.obj
+end
+
+-- Return the C functions and context for producing objects, these objects
+-- are received.
+-- If nonblocking mode is enabled the producer will return a payload object
+-- with length zero if there was nothing to receive.
+-- If nonblocking mode is disabled the producer will wait for data and if
+-- timed out (see
+-- .IR timeout )
+-- it will return a payload object with length zero.
+-- The producer returns nil on error.
+function Udpcli:produce()
+ return C.output_udpcli_producer(self.obj), self.obj
+end
+
+-- Return the number of "packets" sent, actually the number of completely sent
+-- payloads.
+function Udpcli:packets()
+ return tonumber(self.obj.pkts)
+end
+
+-- Return the number of "packets" received, actually the number of successful
+-- calls to
+-- .IR recvfrom (2)
+-- that returned data.
+function Udpcli:received()
+ return tonumber(self.obj.pkts_recv)
+end
+
+-- Return the number of errors when sending or receiving.
+function Udpcli:errors()
+ return tonumber(self.obj.errs)
+end
+
+return Udpcli