summaryrefslogtreecommitdiffstats
path: root/modules/dnstap
diff options
context:
space:
mode:
Diffstat (limited to 'modules/dnstap')
-rw-r--r--modules/dnstap/.packaging/centos/7/builddeps3
-rw-r--r--modules/dnstap/.packaging/centos/7/rundeps2
-rw-r--r--modules/dnstap/.packaging/centos/8/builddeps3
-rw-r--r--modules/dnstap/.packaging/centos/8/rundeps2
-rw-r--r--modules/dnstap/.packaging/debian/10/builddeps3
-rw-r--r--modules/dnstap/.packaging/debian/10/rundeps2
-rw-r--r--modules/dnstap/.packaging/debian/9/builddeps3
-rw-r--r--modules/dnstap/.packaging/debian/9/rundeps2
-rw-r--r--modules/dnstap/.packaging/fedora/31/builddeps3
-rw-r--r--modules/dnstap/.packaging/fedora/31/rundeps2
-rw-r--r--modules/dnstap/.packaging/fedora/32/builddeps3
-rw-r--r--modules/dnstap/.packaging/fedora/32/rundeps2
-rw-r--r--modules/dnstap/.packaging/leap/15.2/builddeps3
-rw-r--r--modules/dnstap/.packaging/leap/15.2/rundeps2
-rw-r--r--modules/dnstap/.packaging/test.config4
-rw-r--r--modules/dnstap/.packaging/ubuntu/16.04/builddeps3
-rw-r--r--modules/dnstap/.packaging/ubuntu/16.04/rundeps2
-rw-r--r--modules/dnstap/.packaging/ubuntu/18.04/builddeps3
-rw-r--r--modules/dnstap/.packaging/ubuntu/18.04/rundeps2
-rw-r--r--modules/dnstap/.packaging/ubuntu/20.04/builddeps3
-rw-r--r--modules/dnstap/.packaging/ubuntu/20.04/rundeps2
-rw-r--r--modules/dnstap/README.rst42
-rw-r--r--modules/dnstap/dnstap.c524
-rw-r--r--modules/dnstap/dnstap.proto273
-rw-r--r--modules/dnstap/meson.build57
25 files changed, 950 insertions, 0 deletions
diff --git a/modules/dnstap/.packaging/centos/7/builddeps b/modules/dnstap/.packaging/centos/7/builddeps
new file mode 100644
index 0000000..d3ab354
--- /dev/null
+++ b/modules/dnstap/.packaging/centos/7/builddeps
@@ -0,0 +1,3 @@
+fstrm-devel
+protobuf-c-devel
+protobuf-c-compiler
diff --git a/modules/dnstap/.packaging/centos/7/rundeps b/modules/dnstap/.packaging/centos/7/rundeps
new file mode 100644
index 0000000..06c2792
--- /dev/null
+++ b/modules/dnstap/.packaging/centos/7/rundeps
@@ -0,0 +1,2 @@
+fstrm
+protobuf-c
diff --git a/modules/dnstap/.packaging/centos/8/builddeps b/modules/dnstap/.packaging/centos/8/builddeps
new file mode 100644
index 0000000..d3ab354
--- /dev/null
+++ b/modules/dnstap/.packaging/centos/8/builddeps
@@ -0,0 +1,3 @@
+fstrm-devel
+protobuf-c-devel
+protobuf-c-compiler
diff --git a/modules/dnstap/.packaging/centos/8/rundeps b/modules/dnstap/.packaging/centos/8/rundeps
new file mode 100644
index 0000000..06c2792
--- /dev/null
+++ b/modules/dnstap/.packaging/centos/8/rundeps
@@ -0,0 +1,2 @@
+fstrm
+protobuf-c
diff --git a/modules/dnstap/.packaging/debian/10/builddeps b/modules/dnstap/.packaging/debian/10/builddeps
new file mode 100644
index 0000000..417dc04
--- /dev/null
+++ b/modules/dnstap/.packaging/debian/10/builddeps
@@ -0,0 +1,3 @@
+libfstrm-dev
+libprotobuf-c-dev
+protobuf-c-compiler
diff --git a/modules/dnstap/.packaging/debian/10/rundeps b/modules/dnstap/.packaging/debian/10/rundeps
new file mode 100644
index 0000000..a726e12
--- /dev/null
+++ b/modules/dnstap/.packaging/debian/10/rundeps
@@ -0,0 +1,2 @@
+libfstrm0
+libprotobuf-c1
diff --git a/modules/dnstap/.packaging/debian/9/builddeps b/modules/dnstap/.packaging/debian/9/builddeps
new file mode 100644
index 0000000..417dc04
--- /dev/null
+++ b/modules/dnstap/.packaging/debian/9/builddeps
@@ -0,0 +1,3 @@
+libfstrm-dev
+libprotobuf-c-dev
+protobuf-c-compiler
diff --git a/modules/dnstap/.packaging/debian/9/rundeps b/modules/dnstap/.packaging/debian/9/rundeps
new file mode 100644
index 0000000..a726e12
--- /dev/null
+++ b/modules/dnstap/.packaging/debian/9/rundeps
@@ -0,0 +1,2 @@
+libfstrm0
+libprotobuf-c1
diff --git a/modules/dnstap/.packaging/fedora/31/builddeps b/modules/dnstap/.packaging/fedora/31/builddeps
new file mode 100644
index 0000000..d3ab354
--- /dev/null
+++ b/modules/dnstap/.packaging/fedora/31/builddeps
@@ -0,0 +1,3 @@
+fstrm-devel
+protobuf-c-devel
+protobuf-c-compiler
diff --git a/modules/dnstap/.packaging/fedora/31/rundeps b/modules/dnstap/.packaging/fedora/31/rundeps
new file mode 100644
index 0000000..06c2792
--- /dev/null
+++ b/modules/dnstap/.packaging/fedora/31/rundeps
@@ -0,0 +1,2 @@
+fstrm
+protobuf-c
diff --git a/modules/dnstap/.packaging/fedora/32/builddeps b/modules/dnstap/.packaging/fedora/32/builddeps
new file mode 100644
index 0000000..d3ab354
--- /dev/null
+++ b/modules/dnstap/.packaging/fedora/32/builddeps
@@ -0,0 +1,3 @@
+fstrm-devel
+protobuf-c-devel
+protobuf-c-compiler
diff --git a/modules/dnstap/.packaging/fedora/32/rundeps b/modules/dnstap/.packaging/fedora/32/rundeps
new file mode 100644
index 0000000..06c2792
--- /dev/null
+++ b/modules/dnstap/.packaging/fedora/32/rundeps
@@ -0,0 +1,2 @@
+fstrm
+protobuf-c
diff --git a/modules/dnstap/.packaging/leap/15.2/builddeps b/modules/dnstap/.packaging/leap/15.2/builddeps
new file mode 100644
index 0000000..30f8d9e
--- /dev/null
+++ b/modules/dnstap/.packaging/leap/15.2/builddeps
@@ -0,0 +1,3 @@
+fstrm-devel
+libprotobuf-c-devel
+protobuf-c
diff --git a/modules/dnstap/.packaging/leap/15.2/rundeps b/modules/dnstap/.packaging/leap/15.2/rundeps
new file mode 100644
index 0000000..06c2792
--- /dev/null
+++ b/modules/dnstap/.packaging/leap/15.2/rundeps
@@ -0,0 +1,2 @@
+fstrm
+protobuf-c
diff --git a/modules/dnstap/.packaging/test.config b/modules/dnstap/.packaging/test.config
new file mode 100644
index 0000000..5966860
--- /dev/null
+++ b/modules/dnstap/.packaging/test.config
@@ -0,0 +1,4 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+modules.load('dnstap')
+assert(dnstap)
+quit()
diff --git a/modules/dnstap/.packaging/ubuntu/16.04/builddeps b/modules/dnstap/.packaging/ubuntu/16.04/builddeps
new file mode 100644
index 0000000..417dc04
--- /dev/null
+++ b/modules/dnstap/.packaging/ubuntu/16.04/builddeps
@@ -0,0 +1,3 @@
+libfstrm-dev
+libprotobuf-c-dev
+protobuf-c-compiler
diff --git a/modules/dnstap/.packaging/ubuntu/16.04/rundeps b/modules/dnstap/.packaging/ubuntu/16.04/rundeps
new file mode 100644
index 0000000..a726e12
--- /dev/null
+++ b/modules/dnstap/.packaging/ubuntu/16.04/rundeps
@@ -0,0 +1,2 @@
+libfstrm0
+libprotobuf-c1
diff --git a/modules/dnstap/.packaging/ubuntu/18.04/builddeps b/modules/dnstap/.packaging/ubuntu/18.04/builddeps
new file mode 100644
index 0000000..417dc04
--- /dev/null
+++ b/modules/dnstap/.packaging/ubuntu/18.04/builddeps
@@ -0,0 +1,3 @@
+libfstrm-dev
+libprotobuf-c-dev
+protobuf-c-compiler
diff --git a/modules/dnstap/.packaging/ubuntu/18.04/rundeps b/modules/dnstap/.packaging/ubuntu/18.04/rundeps
new file mode 100644
index 0000000..a726e12
--- /dev/null
+++ b/modules/dnstap/.packaging/ubuntu/18.04/rundeps
@@ -0,0 +1,2 @@
+libfstrm0
+libprotobuf-c1
diff --git a/modules/dnstap/.packaging/ubuntu/20.04/builddeps b/modules/dnstap/.packaging/ubuntu/20.04/builddeps
new file mode 100644
index 0000000..417dc04
--- /dev/null
+++ b/modules/dnstap/.packaging/ubuntu/20.04/builddeps
@@ -0,0 +1,3 @@
+libfstrm-dev
+libprotobuf-c-dev
+protobuf-c-compiler
diff --git a/modules/dnstap/.packaging/ubuntu/20.04/rundeps b/modules/dnstap/.packaging/ubuntu/20.04/rundeps
new file mode 100644
index 0000000..a726e12
--- /dev/null
+++ b/modules/dnstap/.packaging/ubuntu/20.04/rundeps
@@ -0,0 +1,2 @@
+libfstrm0
+libprotobuf-c1
diff --git a/modules/dnstap/README.rst b/modules/dnstap/README.rst
new file mode 100644
index 0000000..456d218
--- /dev/null
+++ b/modules/dnstap/README.rst
@@ -0,0 +1,42 @@
+.. SPDX-License-Identifier: GPL-3.0-or-later
+
+.. _mod-dnstap:
+
+Dnstap (traffic collection)
+===========================
+
+The ``dnstap`` module supports logging DNS requests and responses to a unix
+socket in `dnstap format <https://dnstap.info>`_ using fstrm framing library.
+This logging is useful if you need effectively log all DNS traffic.
+
+The unix socket and the socket reader must be present before starting resolver instances.
+Also it needs appropriate filesystem permissions;
+the typical user and group of the daemon are called ``knot-resolver``.
+
+Tunables:
+
+* ``socket_path``: the unix socket file where dnstap messages will be sent
+* ``identity``: identity string as typically returned by an "NSID" (RFC 5001) query, empty by default
+* ``version``: version string of the resolver, defaulting to "Knot Resolver major.minor.patch"
+* ``client.log_queries``: if ``true`` queries from downstream in wire format will be logged
+* ``client.log_responses``: if ``true`` responses to downstream in wire format will be logged
+
+.. Very non-standard and it seems unlikely that others want to collect the RTT.
+.. * ``client.log_tcp_rtt``: if ``true`` and on Linux,
+ add "extra" field with "rtt=12345\n",
+ signifying kernel's current estimate of RTT micro-seconds for the non-UDP connection
+ (alongside every arrived DNS message).
+
+.. code-block:: lua
+
+ modules = {
+ dnstap = {
+ socket_path = "/tmp/dnstap.sock",
+ identity = nsid.name() or "",
+ version = "My Custom Knot Resolver " .. package_version(),
+ client = {
+ log_queries = true,
+ log_responses = true,
+ },
+ }
+ }
diff --git a/modules/dnstap/dnstap.c b/modules/dnstap/dnstap.c
new file mode 100644
index 0000000..7572667
--- /dev/null
+++ b/modules/dnstap/dnstap.c
@@ -0,0 +1,524 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * @file dnstap.c
+ * @brief dnstap based query logging support
+ *
+ */
+
+#include "lib/module.h"
+#include "modules/dnstap/dnstap.pb-c.h"
+
+#include "contrib/cleanup.h"
+#include "daemon/session.h"
+#include "daemon/worker.h"
+#include "lib/layer.h"
+#include "lib/resolve.h"
+
+#include <ccan/json/json.h>
+#include <fstrm.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <uv.h>
+
+#define DEBUG_MSG(fmt, ...) kr_log_debug(DNSTAP, fmt, ##__VA_ARGS__);
+#define ERROR_MSG(fmt, ...) kr_log_error(DNSTAP, fmt, ##__VA_ARGS__);
+#define CFG_SOCK_PATH "socket_path"
+#define CFG_IDENTITY_STRING "identity"
+#define CFG_VERSION_STRING "version"
+#define CFG_LOG_CLIENT_PKT "client"
+#define CFG_LOG_QR_PKT "log_queries"
+#define CFG_LOG_RESP_PKT "log_responses"
+#define CFG_LOG_TCP_RTT "log_tcp_rtt"
+#define DEFAULT_SOCK_PATH "/tmp/dnstap.sock"
+#define DNSTAP_CONTENT_TYPE "protobuf:dnstap.Dnstap"
+#define DNSTAP_INITIAL_BUF_SIZE 256
+
+#define auto_destroy_uopts __attribute__((cleanup(fstrm_unix_writer_options_destroy)))
+#define auto_destroy_wopts __attribute__((cleanup(fstrm_writer_options_destroy)))
+
+/*
+ * Internal processing phase
+ * Distinguishes whether query or response should be processed
+ */
+enum dnstap_log_phase {
+ CLIENT_QUERY_PHASE = 0,
+ CLIENT_RESPONSE_PHASE,
+};
+
+/* Internal data structure */
+struct dnstap_data {
+ char *identity;
+ size_t identity_len;
+ char *version;
+ size_t version_len;
+ bool log_qr_pkt;
+ bool log_resp_pkt;
+ bool log_tcp_rtt;
+ struct fstrm_iothr *iothread;
+ struct fstrm_iothr_queue *ioq;
+};
+
+/*
+ * dt_pack packs the dnstap message for transport
+ * https://gitlab.nic.cz/knot/knot-dns/blob/master/src/contrib/dnstap/dnstap.c#L24
+ * */
+uint8_t* dt_pack(const Dnstap__Dnstap *d, uint8_t **buf, size_t *sz)
+{
+ ProtobufCBufferSimple sbuf = { { NULL } };
+
+ sbuf.base.append = protobuf_c_buffer_simple_append;
+ sbuf.len = 0;
+ sbuf.alloced = DNSTAP_INITIAL_BUF_SIZE;
+ sbuf.data = malloc(sbuf.alloced);
+ if (sbuf.data == NULL) {
+ return NULL;
+ }
+ sbuf.must_free_data = true;
+
+ *sz = dnstap__dnstap__pack_to_buffer(d, (ProtobufCBuffer *) &sbuf);
+ *buf = sbuf.data;
+ return *buf;
+}
+
+/* set_address fills in address detail in dnstap_message
+ * https://gitlab.nic.cz/knot/knot-dns/blob/master/src/contrib/dnstap/message.c#L28
+ */
+static void set_address(const struct sockaddr *sockaddr,
+ ProtobufCBinaryData *addr,
+ protobuf_c_boolean *has_addr,
+ uint32_t *port,
+ protobuf_c_boolean *has_port) {
+ const char *saddr = kr_inaddr(sockaddr);
+ if (saddr == NULL) {
+ *has_addr = false;
+ *has_port = false;
+ return;
+ }
+
+ addr->data = (uint8_t *)(saddr);
+ addr->len = kr_inaddr_len(sockaddr);
+ *has_addr = true;
+ *port = kr_inaddr_port(sockaddr);
+ *has_port = true;
+}
+
+#ifndef HAS_TCP_INFO
+ /* TCP RTT: not portable; not sure where else it might work. */
+ #define HAS_TCP_INFO __linux__
+#endif
+#if HAS_TCP_INFO
+/** Fill a tcp_info or return kr_error(). */
+static int get_tcp_info(const struct kr_request *req, struct tcp_info *info)
+{
+ if(kr_fails_assert(req && info))
+ return kr_error(EINVAL);
+ if (!req->qsource.dst_addr || !req->qsource.flags.tcp) /* not TCP-based */
+ return -abs(ENOENT);
+ /* First obtain the file-descriptor. */
+ uv_handle_t *h = session_get_handle(worker_request_get_source_session(req));
+ uv_os_fd_t fd;
+ int ret = uv_fileno(h, &fd);
+ if (ret)
+ return kr_error(ret);
+
+ socklen_t tcp_info_length = sizeof(*info);
+ if (getsockopt(fd, SOL_TCP, TCP_INFO, info, &tcp_info_length))
+ return kr_error(errno);
+ return kr_ok();
+}
+#endif
+
+/* dnstap_log prepares dnstap message and sends it to fstrm
+ *
+ * Return codes are kr_error(E*) and unused for now.
+ */
+static int dnstap_log(kr_layer_t *ctx, enum dnstap_log_phase phase) {
+ const struct kr_request *req = ctx->req;
+ const struct kr_module *module = ctx->api->data;
+ const struct kr_rplan *rplan = &req->rplan;
+ const struct dnstap_data *dnstap_dt = module->data;
+
+ if (!req->qsource.addr) {
+ return kr_ok();
+ }
+
+ /* check if we have a valid iothread */
+ if (!dnstap_dt->iothread || !dnstap_dt->ioq) {
+ DEBUG_MSG("dnstap_dt->iothread or dnstap_dt->ioq is NULL\n");
+ return kr_error(EFAULT);
+ }
+
+ /* Create dnstap message */
+ Dnstap__Message m;
+ Dnstap__Dnstap dnstap = DNSTAP__DNSTAP__INIT;
+ dnstap.type = DNSTAP__DNSTAP__TYPE__MESSAGE;
+ dnstap.message = &m;
+
+ memset(&m, 0, sizeof(m));
+
+ m.base.descriptor = &dnstap__message__descriptor;
+
+ if (req->qsource.addr) {
+ set_address(req->qsource.addr,
+ &m.query_address,
+ &m.has_query_address,
+ &m.query_port,
+ &m.has_query_port);
+ }
+
+ if (req->qsource.dst_addr) {
+ if (req->qsource.flags.http) {
+ m.socket_protocol = DNSTAP__SOCKET_PROTOCOL__DOH;
+ } else if (req->qsource.flags.tls) {
+ m.socket_protocol = DNSTAP__SOCKET_PROTOCOL__DOT;
+ } else if (req->qsource.flags.tcp) {
+ m.socket_protocol = DNSTAP__SOCKET_PROTOCOL__TCP;
+ } else {
+ m.socket_protocol = DNSTAP__SOCKET_PROTOCOL__UDP;
+ }
+ m.has_socket_protocol = true;
+
+ set_address(req->qsource.dst_addr,
+ &m.response_address,
+ &m.has_response_address,
+ &m.response_port,
+ &m.has_response_port);
+ switch (req->qsource.dst_addr->sa_family) {
+ case AF_INET:
+ m.socket_family = DNSTAP__SOCKET_FAMILY__INET;
+ m.has_socket_family = true;
+ break;
+ case AF_INET6:
+ m.socket_family = DNSTAP__SOCKET_FAMILY__INET6;
+ m.has_socket_family = true;
+ break;
+ }
+ }
+
+ char dnstap_extra_buf[24];
+ if (phase == CLIENT_QUERY_PHASE) {
+ m.type = DNSTAP__MESSAGE__TYPE__CLIENT_QUERY;
+
+ if (dnstap_dt->log_qr_pkt) {
+ const knot_pkt_t *qpkt = req->qsource.packet;
+ m.has_query_message = qpkt != NULL;
+ if (qpkt != NULL) {
+ m.query_message.len = qpkt->size;
+ m.query_message.data = qpkt->wire;
+ }
+ }
+
+ /* set query time to the timestamp of the first kr_query */
+ if (rplan->initial) {
+ struct kr_query *first = rplan->initial;
+
+ m.query_time_sec = first->timestamp.tv_sec;
+ m.has_query_time_sec = true;
+ m.query_time_nsec = first->timestamp.tv_usec * 1000;
+ m.has_query_time_nsec = true;
+ }
+#if HAS_TCP_INFO
+ struct tcp_info ti = { 0 };
+ if (dnstap_dt->log_tcp_rtt && get_tcp_info(req, &ti) == kr_ok()) {
+ int len = snprintf(dnstap_extra_buf, sizeof(dnstap_extra_buf),
+ "rtt=%u\n", (unsigned)ti.tcpi_rtt);
+ if (len < sizeof(dnstap_extra_buf)) {
+ dnstap.extra.data = (uint8_t *)dnstap_extra_buf;
+ dnstap.extra.len = len;
+ dnstap.has_extra = true;
+ }
+ }
+#else
+ (void)dnstap_extra_buf;
+#endif
+ } else if (phase == CLIENT_RESPONSE_PHASE) {
+ m.type = DNSTAP__MESSAGE__TYPE__CLIENT_RESPONSE;
+
+ /* current time */
+ struct timeval now;
+ gettimeofday(&now, NULL);
+
+ if (dnstap_dt->log_resp_pkt) {
+ const knot_pkt_t *rpkt = req->answer;
+ m.has_response_message = rpkt != NULL;
+ if (rpkt != NULL) {
+ m.response_message.len = rpkt->size;
+ m.response_message.data = rpkt->wire;
+ }
+ }
+
+ /* Set response time to now */
+ m.response_time_sec = now.tv_sec;
+ m.has_response_time_sec = true;
+ m.response_time_nsec = now.tv_usec * 1000;
+ m.has_response_time_nsec = true;
+ }
+
+ if (dnstap_dt->identity) {
+ dnstap.identity.data = (uint8_t*)dnstap_dt->identity;
+ dnstap.identity.len = dnstap_dt->identity_len;
+ dnstap.has_identity = true;
+ }
+
+ if (dnstap_dt->version) {
+ dnstap.version.data = (uint8_t*)dnstap_dt->version;
+ dnstap.version.len = dnstap_dt->version_len;
+ dnstap.has_version = true;
+ }
+
+ /* Pack the message */
+ uint8_t *frame = NULL;
+ size_t size = 0;
+ dt_pack(&dnstap, &frame, &size);
+ if (!frame) {
+ return kr_error(ENOMEM);
+ }
+
+ /* Submit a request to send message to fstrm_iothr*/
+ fstrm_res res = fstrm_iothr_submit(dnstap_dt->iothread, dnstap_dt->ioq, frame, size,
+ fstrm_free_wrapper, NULL);
+ if (res != fstrm_res_success) {
+ DEBUG_MSG("Error submitting dnstap message to iothr\n");
+ free(frame);
+ return kr_error(EBUSY);
+ }
+
+ return kr_ok();
+}
+
+/* dnstap_log_query prepares dnstap CLIENT_QUERY message and sends it to fstrm */
+static int dnstap_log_query(kr_layer_t *ctx) {
+ dnstap_log(ctx, CLIENT_QUERY_PHASE);
+ return ctx->state;
+}
+
+/* dnstap_log_response prepares dnstap CLIENT_RESPONSE message and sends it to fstrm */
+static int dnstap_log_response(kr_layer_t *ctx) {
+ dnstap_log(ctx, CLIENT_RESPONSE_PHASE);
+ return ctx->state;
+}
+
+KR_EXPORT
+int dnstap_init(struct kr_module *module) {
+ static kr_layer_api_t layer = {
+ .begin = &dnstap_log_query,
+ .finish = &dnstap_log_response,
+ };
+ /* Store module reference */
+ layer.data = module;
+ module->layer = &layer;
+
+ /* allocated memory for internal data */
+ struct dnstap_data *data = calloc(1, sizeof(*data));
+ if (!data) {
+ return kr_error(ENOMEM);
+ }
+
+ /* save pointer to internal struct in module for future reference */
+ module->data = data;
+ return kr_ok();
+}
+
+/** Clear, i.e. get to state as after the first dnstap_init(). */
+static void dnstap_clear(struct kr_module *module) {
+ struct dnstap_data *data = module->data;
+ if (data) {
+ free(data->identity);
+ free(data->version);
+
+ fstrm_iothr_destroy(&data->iothread);
+ DEBUG_MSG("fstrm iothread destroyed\n");
+ }
+}
+
+KR_EXPORT
+int dnstap_deinit(struct kr_module *module) {
+ dnstap_clear(module);
+ free(module->data);
+ return kr_ok();
+}
+
+/* dnstap_unix_writer returns a unix fstream writer
+ * https://gitlab.nic.cz/knot/knot-dns/blob/master/src/knot/modules/dnstap.c#L159
+ */
+static struct fstrm_writer* dnstap_unix_writer(const char *path) {
+
+ auto_destroy_uopts struct fstrm_unix_writer_options *opt = fstrm_unix_writer_options_init();
+ if (!opt) {
+ return NULL;
+ }
+ fstrm_unix_writer_options_set_socket_path(opt, path);
+
+ auto_destroy_wopts struct fstrm_writer_options *wopt = fstrm_writer_options_init();
+ if (!wopt) {
+ fstrm_unix_writer_options_destroy(&opt);
+ return NULL;
+ }
+ fstrm_writer_options_add_content_type(wopt, DNSTAP_CONTENT_TYPE,
+ strlen(DNSTAP_CONTENT_TYPE));
+
+ struct fstrm_writer *writer = fstrm_unix_writer_init(opt, wopt);
+ fstrm_unix_writer_options_destroy(&opt);
+ fstrm_writer_options_destroy(&wopt);
+ if (!writer) {
+ return NULL;
+ }
+
+ fstrm_res res = fstrm_writer_open(writer);
+ if (res != fstrm_res_success) {
+ DEBUG_MSG("fstrm_writer_open returned %d\n", res);
+ fstrm_writer_destroy(&writer);
+ return NULL;
+ }
+
+ return writer;
+}
+
+/* find_string
+ * create a new string from json
+ * *var is set to pointer of new string
+ * node must of type JSON_STRING
+ * new string can be at most len bytes
+ */
+static int find_string(const JsonNode *node, char **val, size_t len) {
+ if (!node || !node->key)
+ return kr_error(EINVAL);
+ if (kr_fails_assert(node->tag == JSON_STRING))
+ return kr_error(EINVAL);
+ *val = strndup(node->string_, len);
+ if (kr_fails_assert(*val != NULL))
+ return kr_error(errno);
+ return kr_ok();
+}
+
+/* find_bool returns bool from json */
+static bool find_bool(const JsonNode *node) {
+ if (!node || !node->key)
+ return false;
+ if (kr_fails_assert(node->tag == JSON_BOOL))
+ return false;
+ return node->bool_;
+}
+
+/* parse config */
+KR_EXPORT
+int dnstap_config(struct kr_module *module, const char *conf) {
+ dnstap_clear(module);
+ if (!conf) return kr_ok(); /* loaded module without configuring */
+ struct dnstap_data *data = module->data;
+ auto_free char *sock_path = NULL;
+
+ /* Empty conf passed, set default */
+ if (strlen(conf) < 1) {
+ sock_path = strdup(DEFAULT_SOCK_PATH);
+ } else {
+
+ JsonNode *root_node = json_decode(conf);
+ if (!root_node) {
+ ERROR_MSG("error parsing json\n");
+ return kr_error(EINVAL);
+ }
+
+ JsonNode *node;
+ /* dnstapPath key */
+ node = json_find_member(root_node, CFG_SOCK_PATH);
+ if (!node || find_string(node, &sock_path, PATH_MAX) != kr_ok()) {
+ sock_path = strdup(DEFAULT_SOCK_PATH);
+ }
+
+ /* identity string key */
+ node = json_find_member(root_node, CFG_IDENTITY_STRING);
+ if (!node || find_string(node, &data->identity, KR_EDNS_PAYLOAD) != kr_ok()) {
+ data->identity = NULL;
+ data->identity_len = 0;
+ } else {
+ data->identity_len = strlen(data->identity);
+ }
+
+ /* version string key */
+ node = json_find_member(root_node, CFG_VERSION_STRING);
+ if (!node || find_string(node, &data->version, KR_EDNS_PAYLOAD) != kr_ok()) {
+ data->version = strdup("Knot Resolver " PACKAGE_VERSION);
+ if (data->version) {
+ data->version_len = strlen(data->version);
+ }
+ } else {
+ data->version_len = strlen(data->version);
+ }
+
+ node = json_find_member(root_node, CFG_LOG_CLIENT_PKT);
+ if (node) {
+ JsonNode *subnode;
+ /* logRespPkt key */
+ subnode = json_find_member(node, CFG_LOG_RESP_PKT);
+ if (subnode) {
+ data->log_resp_pkt = find_bool(subnode);
+ } else {
+ data->log_resp_pkt = false;
+ }
+
+ /* logQrPkt key */
+ subnode = json_find_member(node, CFG_LOG_QR_PKT);
+ if (subnode) {
+ data->log_qr_pkt = find_bool(subnode);
+ } else {
+ data->log_qr_pkt = false;
+ }
+
+ subnode = json_find_member(node, CFG_LOG_TCP_RTT);
+ if (subnode) {
+ data->log_tcp_rtt = find_bool(subnode);
+ } else {
+ data->log_tcp_rtt = false;
+ }
+ } else {
+ data->log_qr_pkt = false;
+ data->log_resp_pkt = false;
+ data->log_tcp_rtt = false;
+ }
+
+ /* clean up json, we don't need it no more */
+ json_delete(root_node);
+ }
+
+ DEBUG_MSG("opening sock file %s\n",sock_path);
+ struct fstrm_writer *writer = dnstap_unix_writer(sock_path);
+ if (!writer) {
+ ERROR_MSG("failed to open socket %s\n"
+ "Please ensure that it exists beforehand and has appropriate access permissions.\n",
+ sock_path);
+ return kr_error(EINVAL);
+ }
+
+ struct fstrm_iothr_options *opt = fstrm_iothr_options_init();
+ if (!opt) {
+ ERROR_MSG("can't init fstrm options\n");
+ fstrm_writer_destroy(&writer);
+ return kr_error(EINVAL);
+ }
+
+ /* Create the I/O thread. */
+ data->iothread = fstrm_iothr_init(opt, &writer);
+ fstrm_iothr_options_destroy(&opt);
+ if (!data->iothread) {
+ ERROR_MSG("can't init fstrm_iothr\n");
+ fstrm_writer_destroy(&writer);
+ return kr_error(ENOMEM);
+ }
+
+ /* Get fstrm thread handle
+ * We only have one input queue, hence idx=0
+ */
+ data->ioq = fstrm_iothr_get_input_queue_idx(data->iothread, 0);
+ if (!data->ioq) {
+ fstrm_iothr_destroy(&data->iothread);
+ ERROR_MSG("can't get fstrm queue\n");
+ return kr_error(EBUSY);
+ }
+
+ return kr_ok();
+}
+
+KR_MODULE_EXPORT(dnstap)
+
diff --git a/modules/dnstap/dnstap.proto b/modules/dnstap/dnstap.proto
new file mode 100644
index 0000000..f2b7273
--- /dev/null
+++ b/modules/dnstap/dnstap.proto
@@ -0,0 +1,273 @@
+// dnstap: flexible, structured event replication format for DNS software
+//
+// This file contains the protobuf schemas for the "dnstap" structured event
+// replication format for DNS software.
+
+// Written in 2013-2014 by Farsight Security, Inc.
+//
+// SPDX-License-Identifier: CC0-1.0
+
+syntax = "proto2";
+package dnstap;
+
+// "Dnstap": this is the top-level dnstap type, which is a "union" type that
+// contains other kinds of dnstap payloads, although currently only one type
+// of dnstap payload is defined.
+// See: https://developers.google.com/protocol-buffers/docs/techniques#union
+message Dnstap {
+ // DNS server identity.
+ // If enabled, this is the identity string of the DNS server which generated
+ // this message. Typically this would be the same string as returned by an
+ // "NSID" (RFC 5001) query.
+ optional bytes identity = 1;
+
+ // DNS server version.
+ // If enabled, this is the version string of the DNS server which generated
+ // this message. Typically this would be the same string as returned by a
+ // "version.bind" query.
+ optional bytes version = 2;
+
+ // Extra data for this payload.
+ // This field can be used for adding an arbitrary byte-string annotation to
+ // the payload. No encoding or interpretation is applied or enforced.
+ optional bytes extra = 3;
+
+ // Identifies which field below is filled in.
+ enum Type {
+ MESSAGE = 1;
+ }
+ required Type type = 15;
+
+ // One of the following will be filled in.
+ optional Message message = 14;
+}
+
+// SocketFamily: the network protocol family of a socket. This specifies how
+// to interpret "network address" fields.
+enum SocketFamily {
+ INET = 1; // IPv4 (RFC 791)
+ INET6 = 2; // IPv6 (RFC 2460)
+}
+
+// SocketProtocol: the protocol used to transport a DNS message.
+enum SocketProtocol {
+ UDP = 1; // DNS over UDP transport (RFC 1035 section 4.2.1)
+ TCP = 2; // DNS over TCP transport (RFC 1035 section 4.2.2)
+ DOT = 3; // DNS over TLS (RFC 7858)
+ DOH = 4; // DNS over HTTPS (RFC 8484)
+}
+
+// Message: a wire-format (RFC 1035 section 4) DNS message and associated
+// metadata. Applications generating "Message" payloads should follow
+// certain requirements based on the MessageType, see below.
+message Message {
+
+ // There are eight types of "Message" defined that correspond to the
+ // four arrows in the following diagram, slightly modified from RFC 1035
+ // section 2:
+
+ // +---------+ +----------+ +--------+
+ // | | query | | query | |
+ // | Stub |-SQ--------CQ->| Recursive|-RQ----AQ->| Auth. |
+ // | Resolver| | Server | | Name |
+ // | |<-SR--------CR-| |<-RR----AR-| Server |
+ // +---------+ response | | response | |
+ // +----------+ +--------+
+
+ // Each arrow has two Type values each, one for each "end" of each arrow,
+ // because these are considered to be distinct events. Each end of each
+ // arrow on the diagram above has been marked with a two-letter Type
+ // mnemonic. Clockwise from upper left, these mnemonic values are:
+ //
+ // SQ: STUB_QUERY
+ // CQ: CLIENT_QUERY
+ // RQ: RESOLVER_QUERY
+ // AQ: AUTH_QUERY
+ // AR: AUTH_RESPONSE
+ // RR: RESOLVER_RESPONSE
+ // CR: CLIENT_RESPONSE
+ // SR: STUB_RESPONSE
+
+ // Two additional types of "Message" have been defined for the
+ // "forwarding" case where an upstream DNS server is responsible for
+ // further recursion. These are not shown on the diagram above, but have
+ // the following mnemonic values:
+
+ // FQ: FORWARDER_QUERY
+ // FR: FORWARDER_RESPONSE
+
+ // The "Message" Type values are defined below.
+
+ enum Type {
+ // AUTH_QUERY is a DNS query message received from a resolver by an
+ // authoritative name server, from the perspective of the authoritative
+ // name server.
+ AUTH_QUERY = 1;
+
+ // AUTH_RESPONSE is a DNS response message sent from an authoritative
+ // name server to a resolver, from the perspective of the authoritative
+ // name server.
+ AUTH_RESPONSE = 2;
+
+ // RESOLVER_QUERY is a DNS query message sent from a resolver to an
+ // authoritative name server, from the perspective of the resolver.
+ // Resolvers typically clear the RD (recursion desired) bit when
+ // sending queries.
+ RESOLVER_QUERY = 3;
+
+ // RESOLVER_RESPONSE is a DNS response message received from an
+ // authoritative name server by a resolver, from the perspective of
+ // the resolver.
+ RESOLVER_RESPONSE = 4;
+
+ // CLIENT_QUERY is a DNS query message sent from a client to a DNS
+ // server which is expected to perform further recursion, from the
+ // perspective of the DNS server. The client may be a stub resolver or
+ // forwarder or some other type of software which typically sets the RD
+ // (recursion desired) bit when querying the DNS server. The DNS server
+ // may be a simple forwarding proxy or it may be a full recursive
+ // resolver.
+ CLIENT_QUERY = 5;
+
+ // CLIENT_RESPONSE is a DNS response message sent from a DNS server to
+ // a client, from the perspective of the DNS server. The DNS server
+ // typically sets the RA (recursion available) bit when responding.
+ CLIENT_RESPONSE = 6;
+
+ // FORWARDER_QUERY is a DNS query message sent from a downstream DNS
+ // server to an upstream DNS server which is expected to perform
+ // further recursion, from the perspective of the downstream DNS
+ // server.
+ FORWARDER_QUERY = 7;
+
+ // FORWARDER_RESPONSE is a DNS response message sent from an upstream
+ // DNS server performing recursion to a downstream DNS server, from the
+ // perspective of the downstream DNS server.
+ FORWARDER_RESPONSE = 8;
+
+ // STUB_QUERY is a DNS query message sent from a stub resolver to a DNS
+ // server, from the perspective of the stub resolver.
+ STUB_QUERY = 9;
+
+ // STUB_RESPONSE is a DNS response message sent from a DNS server to a
+ // stub resolver, from the perspective of the stub resolver.
+ STUB_RESPONSE = 10;
+
+ // TOOL_QUERY is a DNS query message sent from a DNS software tool to a
+ // DNS server, from the perspective of the tool.
+ TOOL_QUERY = 11;
+
+ // TOOL_RESPONSE is a DNS response message received by a DNS software
+ // tool from a DNS server, from the perspective of the tool.
+ TOOL_RESPONSE = 12;
+
+ // UPDATE_QUERY is a DNS update query message received from a resolver
+ // by an authoritative name server, from the perspective of the
+ // authoritative name server.
+ UPDATE_QUERY = 13;
+
+ // UPDATE_RESPONSE is a DNS update response message sent from an
+ // authoritative name server to a resolver, from the perspective of the
+ // authoritative name server.
+ UPDATE_RESPONSE = 14;
+ }
+
+ // One of the Type values described above.
+ required Type type = 1;
+
+ // One of the SocketFamily values described above.
+ optional SocketFamily socket_family = 2;
+
+ // One of the SocketProtocol values described above.
+ optional SocketProtocol socket_protocol = 3;
+
+ // The network address of the message initiator.
+ // For SocketFamily INET, this field is 4 octets (IPv4 address).
+ // For SocketFamily INET6, this field is 16 octets (IPv6 address).
+ optional bytes query_address = 4;
+
+ // The network address of the message responder.
+ // For SocketFamily INET, this field is 4 octets (IPv4 address).
+ // For SocketFamily INET6, this field is 16 octets (IPv6 address).
+ optional bytes response_address = 5;
+
+ // The transport port of the message initiator.
+ // This is a 16-bit UDP or TCP port number, depending on SocketProtocol.
+ optional uint32 query_port = 6;
+
+ // The transport port of the message responder.
+ // This is a 16-bit UDP or TCP port number, depending on SocketProtocol.
+ optional uint32 response_port = 7;
+
+ // The time at which the DNS query message was sent or received, depending
+ // on whether this is an AUTH_QUERY, RESOLVER_QUERY, or CLIENT_QUERY.
+ // This is the number of seconds since the UNIX epoch.
+ optional uint64 query_time_sec = 8;
+
+ // The time at which the DNS query message was sent or received.
+ // This is the seconds fraction, expressed as a count of nanoseconds.
+ optional fixed32 query_time_nsec = 9;
+
+ // The initiator's original wire-format DNS query message, verbatim.
+ optional bytes query_message = 10;
+
+ // The "zone" or "bailiwick" pertaining to the DNS query message.
+ // This is a wire-format DNS domain name.
+ optional bytes query_zone = 11;
+
+ // The time at which the DNS response message was sent or received,
+ // depending on whether this is an AUTH_RESPONSE, RESOLVER_RESPONSE, or
+ // CLIENT_RESPONSE.
+ // This is the number of seconds since the UNIX epoch.
+ optional uint64 response_time_sec = 12;
+
+ // The time at which the DNS response message was sent or received.
+ // This is the seconds fraction, expressed as a count of nanoseconds.
+ optional fixed32 response_time_nsec = 13;
+
+ // The responder's original wire-format DNS response message, verbatim.
+ optional bytes response_message = 14;
+}
+
+// All fields except for 'type' in the Message schema are optional.
+// It is recommended that at least the following fields be filled in for
+// particular types of Messages.
+
+// AUTH_QUERY:
+// socket_family, socket_protocol
+// query_address, query_port
+// query_message
+// query_time_sec, query_time_nsec
+
+// AUTH_RESPONSE:
+// socket_family, socket_protocol
+// query_address, query_port
+// query_time_sec, query_time_nsec
+// response_message
+// response_time_sec, response_time_nsec
+
+// RESOLVER_QUERY:
+// socket_family, socket_protocol
+// query_message
+// query_time_sec, query_time_nsec
+// query_zone
+// response_address, response_port
+
+// RESOLVER_RESPONSE:
+// socket_family, socket_protocol
+// query_time_sec, query_time_nsec
+// query_zone
+// response_address, response_port
+// response_message
+// response_time_sec, response_time_nsec
+
+// CLIENT_QUERY:
+// socket_family, socket_protocol
+// query_message
+// query_time_sec, query_time_nsec
+
+// CLIENT_RESPONSE:
+// socket_family, socket_protocol
+// query_time_sec, query_time_nsec
+// response_message
+// response_time_sec, response_time_nsec
diff --git a/modules/dnstap/meson.build b/modules/dnstap/meson.build
new file mode 100644
index 0000000..e8a94bf
--- /dev/null
+++ b/modules/dnstap/meson.build
@@ -0,0 +1,57 @@
+# C module: dnstap
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+dnstap_src = files([
+ 'dnstap.c',
+])
+
+## dnstap dependencies
+build_dnstap = false
+if get_option('dnstap') != 'disabled'
+ dnstap_required = get_option('dnstap') == 'enabled'
+ message('--- dnstap module dependencies ---')
+ libprotobuf_c = dependency('libprotobuf-c', version: '>=1', required: dnstap_required)
+ libfstrm = dependency('libfstrm', version: '>=0.2', required: dnstap_required)
+ protoc_c = find_program('protoc-c', required: dnstap_required)
+ message('----------------------------------')
+ if libprotobuf_c.found() and libfstrm.found() and protoc_c.found()
+ build_dnstap = true
+ endif
+endif
+
+
+if build_dnstap
+ c_src_lint += dnstap_src
+
+ # generate protobuf-c sources using protoc-c
+ dnstap_pb = custom_target(
+ 'dnstap_pb',
+ command: [
+ protoc_c,
+ '--c_out=' + meson.current_build_dir(),
+ '--proto_path', meson.current_source_dir(),
+ meson.current_source_dir() / 'dnstap.proto',
+ ],
+ input: [ 'dnstap.proto' ],
+ output: [
+ 'dnstap.pb-c.h',
+ 'dnstap.pb-c.c',
+ ],
+ )
+
+ # build dnstap module
+ dnstap_mod = shared_module(
+ 'dnstap',
+ dnstap_src,
+ dependencies: [
+ declare_dependency(sources: dnstap_pb),
+ libfstrm,
+ libprotobuf_c,
+ libknot,
+ ],
+ include_directories: mod_inc_dir,
+ name_prefix: '',
+ install: true,
+ install_dir: modules_dir,
+ )
+endif