525 lines
14 KiB
C
525 lines
14 KiB
C
/*
|
|
* 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;
|
|
default:;
|
|
}
|
|
}
|
|
|
|
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)
|
|
|