diff options
Diffstat (limited to '')
-rw-r--r-- | modules/dnstap/dnstap.c | 524 |
1 files changed, 524 insertions, 0 deletions
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) + |