diff options
Diffstat (limited to 'src/output')
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 |