diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:24:08 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:24:08 +0000 |
commit | f449f278dd3c70e479a035f50a9bb817a9b433ba (patch) | |
tree | 8ca2bfb785dda9bb4d573acdf9b42aea9cd51383 /src/knot/common | |
parent | Initial commit. (diff) | |
download | knot-f449f278dd3c70e479a035f50a9bb817a9b433ba.tar.xz knot-f449f278dd3c70e479a035f50a9bb817a9b433ba.zip |
Adding upstream version 3.2.6.upstream/3.2.6upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/knot/common')
-rw-r--r-- | src/knot/common/evsched.c | 268 | ||||
-rw-r--r-- | src/knot/common/evsched.h | 154 | ||||
-rw-r--r-- | src/knot/common/fdset.c | 336 | ||||
-rw-r--r-- | src/knot/common/fdset.h | 382 | ||||
-rw-r--r-- | src/knot/common/log.c | 491 | ||||
-rw-r--r-- | src/knot/common/log.h | 187 | ||||
-rw-r--r-- | src/knot/common/process.c | 194 | ||||
-rw-r--r-- | src/knot/common/process.h | 60 | ||||
-rw-r--r-- | src/knot/common/stats.c | 309 | ||||
-rw-r--r-- | src/knot/common/stats.h | 53 | ||||
-rw-r--r-- | src/knot/common/systemd.c | 168 | ||||
-rw-r--r-- | src/knot/common/systemd.h | 105 | ||||
-rw-r--r-- | src/knot/common/unreachable.c | 148 | ||||
-rw-r--r-- | src/knot/common/unreachable.h | 87 |
14 files changed, 2942 insertions, 0 deletions
diff --git a/src/knot/common/evsched.c b/src/knot/common/evsched.c new file mode 100644 index 0000000..0d65c6a --- /dev/null +++ b/src/knot/common/evsched.c @@ -0,0 +1,268 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <sys/time.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <assert.h> + +#include "libknot/libknot.h" +#include "knot/server/dthreads.h" +#include "knot/common/evsched.h" + +/*! \brief Some implementations of timercmp >= are broken, this is for compat.*/ +static inline int timercmp_ge(struct timeval *a, struct timeval *b) { + return !timercmp(a, b, <); +} + +static int compare_event_heap_nodes(void *e1, void *e2) +{ + if (timercmp(&((event_t *)e1)->tv, &((event_t *)e2)->tv, <)) return -1; + if (timercmp(&((event_t *)e1)->tv, &((event_t *)e2)->tv, >)) return 1; + return 0; +} + +/*! + * \brief Get time T (now) + dt milliseconds. + */ +static struct timeval timeval_in(uint32_t dt) +{ + struct timeval tv = { 0 }; + gettimeofday(&tv, NULL); + + /* Add number of seconds. */ + tv.tv_sec += dt / 1000; + + /* Add the number of microseconds. */ + tv.tv_usec += (dt % 1000) * 1000; + + /* Check for overflow. */ + while (tv.tv_usec > 999999) { + tv.tv_sec += 1; + tv.tv_usec -= 1 * 1000 * 1000; + } + + return tv; +} + +/*! \brief Event scheduler loop. */ +static int evsched_run(dthread_t *thread) +{ + evsched_t *sched = (evsched_t*)thread->data; + if (sched == NULL) { + return KNOT_EINVAL; + } + + /* Run event loop. */ + pthread_mutex_lock(&sched->heap_lock); + while (!dt_is_cancelled(thread)) { + if (!!EMPTY_HEAP(&sched->heap) || sched->paused) { + pthread_cond_wait(&sched->notify, &sched->heap_lock); + continue; + } + + /* Get current time. */ + struct timeval dt; + gettimeofday(&dt, 0); + + /* Get next event. */ + event_t *ev = *((event_t**)HHEAD(&sched->heap)); + assert(ev != NULL); + + if (timercmp_ge(&dt, &ev->tv)) { + heap_delmin(&sched->heap); + ev->cb(ev); + } else { + /* Wait for next event or interrupt. Unlock calendar. */ + struct timespec ts; + ts.tv_sec = ev->tv.tv_sec; + ts.tv_nsec = ev->tv.tv_usec * 1000L; + pthread_cond_timedwait(&sched->notify, &sched->heap_lock, &ts); + } + } + pthread_mutex_unlock(&sched->heap_lock); + + return KNOT_EOK; +} + +int evsched_init(evsched_t *sched, void *ctx) +{ + memset(sched, 0, sizeof(evsched_t)); + sched->ctx = ctx; + + /* Initialize event calendar. */ + pthread_mutex_init(&sched->heap_lock, 0); + pthread_cond_init(&sched->notify, 0); + heap_init(&sched->heap, compare_event_heap_nodes, 0); + + sched->thread = dt_create(1, evsched_run, NULL, sched); + + if (sched->thread == NULL) { + evsched_deinit(sched); + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +void evsched_deinit(evsched_t *sched) +{ + if (sched == NULL) { + return; + } + + /* Deinitialize event calendar. */ + pthread_mutex_destroy(&sched->heap_lock); + pthread_cond_destroy(&sched->notify); + + while (!EMPTY_HEAP(&sched->heap)) { + event_t *e = (event_t *)*HHEAD(&sched->heap); + heap_delmin(&sched->heap); + evsched_event_free(e); + } + + heap_deinit(&sched->heap); + + if (sched->thread != NULL) { + dt_delete(&sched->thread); + } + + /* Clear the structure. */ + memset(sched, 0, sizeof(evsched_t)); +} + +event_t *evsched_event_create(evsched_t *sched, event_cb_t cb, void *data) +{ + /* Create event. */ + if (sched == NULL) { + return NULL; + } + + /* Allocate. */ + event_t *e = malloc(sizeof(event_t)); + if (e == NULL) { + return NULL; + } + + /* Initialize. */ + memset(e, 0, sizeof(event_t)); + e->sched = sched; + e->cb = cb; + e->data = data; + e->hpos.pos = 0; + + return e; +} + +void evsched_event_free(event_t *ev) +{ + if (ev == NULL) { + return; + } + + free(ev); +} + +int evsched_schedule(event_t *ev, uint32_t dt) +{ + if (ev == NULL || ev->sched == NULL) { + return KNOT_EINVAL; + } + + struct timeval new_time = timeval_in(dt); + + evsched_t *sched = ev->sched; + + /* Lock calendar. */ + pthread_mutex_lock(&sched->heap_lock); + + ev->tv = new_time; + + /* Make sure it's not already enqueued. */ + int found = heap_find(&sched->heap, (heap_val_t *)ev); + if (found > 0) { + /* "Replacing" with itself -- just repositioning it. */ + heap_replace(&sched->heap, found, (heap_val_t *)ev); + } else { + heap_insert(&sched->heap, (heap_val_t *)ev); + } + + /* Unlock calendar. */ + pthread_cond_signal(&sched->notify); + pthread_mutex_unlock(&sched->heap_lock); + + return KNOT_EOK; +} + +int evsched_cancel(event_t *ev) +{ + if (ev == NULL || ev->sched == NULL) { + return KNOT_EINVAL; + } + + evsched_t *sched = ev->sched; + + /* Lock calendar. */ + pthread_mutex_lock(&sched->heap_lock); + + int found = heap_find(&sched->heap, (heap_val_t *)ev); + if (found > 0) { + heap_delete(&sched->heap, found); + pthread_cond_signal(&sched->notify); + } + + /* Unlock calendar. */ + pthread_mutex_unlock(&sched->heap_lock); + + /* Reset event timer. */ + memset(&ev->tv, 0, sizeof(struct timeval)); + + return KNOT_EOK; +} + +void evsched_start(evsched_t *sched) +{ + dt_start(sched->thread); +} + +void evsched_stop(evsched_t *sched) +{ + pthread_mutex_lock(&sched->heap_lock); + dt_stop(sched->thread); + pthread_cond_signal(&sched->notify); + pthread_mutex_unlock(&sched->heap_lock); +} + +void evsched_join(evsched_t *sched) +{ + dt_join(sched->thread); +} + +void evsched_pause(evsched_t *sched) +{ + pthread_mutex_lock(&sched->heap_lock); + sched->paused = true; + pthread_mutex_unlock(&sched->heap_lock); +} + +void evsched_resume(evsched_t *sched) +{ + pthread_mutex_lock(&sched->heap_lock); + sched->paused = false; + pthread_cond_signal(&sched->notify); + pthread_mutex_unlock(&sched->heap_lock); +} diff --git a/src/knot/common/evsched.h b/src/knot/common/evsched.h new file mode 100644 index 0000000..762c3f8 --- /dev/null +++ b/src/knot/common/evsched.h @@ -0,0 +1,154 @@ +/* Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +/*! + * \brief Event scheduler. + */ + +#pragma once + +#include <pthread.h> +#include <stdbool.h> +#include <stdint.h> +#include <sys/time.h> + +#include "knot/server/dthreads.h" +#include "contrib/ucw/heap.h" + +/* Forward decls. */ +struct evsched; +struct event; + +/*! + * \brief Event callback. + * + * Pointer to whole event structure is passed to the callback. + * Callback should return 0 on success and negative integer on error. + * + * Example callback: + * \code + * void print_callback(event_t *t) { + * printf("Callback: %s\n", t->data); + * } + * \endcode + */ +typedef void (*event_cb_t)(struct event *); + +/*! + * \brief Event structure. + */ +typedef struct event { + struct heap_val hpos; + struct timeval tv; /*!< Event scheduled time. */ + void *data; /*!< Usable data ptr. */ + event_cb_t cb; /*!< Event callback. */ + struct evsched *sched; /*!< Scheduler for this event. */ +} event_t; + +/*! + * \brief Event scheduler structure. + */ +typedef struct evsched { + volatile bool paused; /*!< Temporarily stop processing events. */ + pthread_mutex_t heap_lock; /*!< Event heap locking. */ + pthread_cond_t notify; /*!< Event heap notification. */ + struct heap heap; /*!< Event heap. */ + void *ctx; /*!< Scheduler context. */ + dt_unit_t *thread; +} evsched_t; + +/*! + * \brief Initialize event scheduler instance. + * + * \retval New instance on success. + * \retval NULL on error. + */ +int evsched_init(evsched_t *sched, void *ctx); + +/*! + * \brief Deinitialize and free event scheduler instance. + * + * \param sched Pointer to event scheduler instance. + */ +void evsched_deinit(evsched_t *sched); + +/*! + * \brief Create a callback event. + * + * \note Scheduler takes ownership of scheduled events. Created, but unscheduled + * events are in the ownership of the caller. + * + * \param sched Pointer to event scheduler instance. + * \param cb Callback handler. + * \param data Data for callback. + * + * \retval New instance on success. + * \retval NULL on error. + */ +event_t *evsched_event_create(evsched_t *sched, event_cb_t cb, void *data); + +/*! + * \brief Dispose event instance. + * + * \param ev Event instance. + */ +void evsched_event_free(event_t *ev); + +/*! + * \brief Schedule an event. + * + * \note This function checks if the event was already scheduled, if it was + * then it replaces this timer with the newer value. + * Running events are not canceled or waited for. + * + * \param ev Prepared event. + * \param dt Time difference in milliseconds from now (dt is relative). + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL + */ +int evsched_schedule(event_t *ev, uint32_t dt); + +/*! + * \brief Cancel a scheduled event. + * + * \warning May block until current running event is finished (as it cannot + * interrupt running event). + * + * \warning Never cancel event in it's callback. As it never finishes, + * it deadlocks. + * + * \param ev Scheduled event. + * + * \retval KNOT_EOK + * \retval KNOT_EINVAL + */ +int evsched_cancel(event_t *ev); + +/*! \brief Start event processing threads. */ +void evsched_start(evsched_t *sched); + +/*! \brief Stop event processing threads. */ +void evsched_stop(evsched_t *sched); + +/*! \brief Join event processing threads. */ +void evsched_join(evsched_t *sched); + +/*! \brief Temporarily stop processing events. */ +void evsched_pause(evsched_t *sched); + +/*! \brief Resume processing events. */ +void evsched_resume(evsched_t *sched); diff --git a/src/knot/common/fdset.c b/src/knot/common/fdset.c new file mode 100644 index 0000000..a4b37d9 --- /dev/null +++ b/src/knot/common/fdset.c @@ -0,0 +1,336 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "knot/common/fdset.h" +#include "contrib/time.h" +#include "contrib/macros.h" + +#define MEM_RESIZE(p, n) { \ + void *tmp = NULL; \ + if ((tmp = realloc((p), (n) * sizeof(*p))) == NULL) { \ + return KNOT_ENOMEM; \ + } \ + (p) = tmp; \ +} + +static int fdset_resize(fdset_t *set, const unsigned size) +{ + assert(set); + + MEM_RESIZE(set->ctx, size); + MEM_RESIZE(set->timeout, size); +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) + MEM_RESIZE(set->ev, size); +#else + MEM_RESIZE(set->pfd, size); +#endif + set->size = size; + return KNOT_EOK; +} + +int fdset_init(fdset_t *set, const unsigned size) +{ + if (set == NULL) { + return KNOT_EINVAL; + } + + memset(set, 0, sizeof(*set)); + +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) +#ifdef HAVE_EPOLL + set->pfd = epoll_create1(0); +#elif HAVE_KQUEUE + set->pfd = kqueue(); +#endif + if (set->pfd < 0) { + return knot_map_errno(); + } +#endif + int ret = fdset_resize(set, size); +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) + if (ret != KNOT_EOK) { + close(set->pfd); + } +#endif + return ret; +} + +void fdset_clear(fdset_t *set) +{ + if (set == NULL) { + return; + } + + free(set->ctx); + free(set->timeout); +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) + free(set->ev); + free(set->recv_ev); + close(set->pfd); +#else + free(set->pfd); +#endif + memset(set, 0, sizeof(*set)); +} + +int fdset_add(fdset_t *set, const int fd, const fdset_event_t events, void *ctx) +{ + if (set == NULL || fd < 0) { + return KNOT_EINVAL; + } + + if (set->n == set->size && + fdset_resize(set, set->size + FDSET_RESIZE_STEP) != KNOT_EOK) { + return KNOT_ENOMEM; + } + + const int idx = set->n++; + set->ctx[idx] = ctx; + set->timeout[idx] = 0; +#ifdef HAVE_EPOLL + set->ev[idx].data.fd = fd; + set->ev[idx].events = events; + struct epoll_event ev = { + .data.u64 = idx, + .events = events + }; + if (epoll_ctl(set->pfd, EPOLL_CTL_ADD, fd, &ev) != 0) { + return knot_map_errno(); + } +#elif HAVE_KQUEUE + EV_SET(&set->ev[idx], fd, events, EV_ADD, 0, 0, (void *)(intptr_t)idx); + if (kevent(set->pfd, &set->ev[idx], 1, NULL, 0, NULL) < 0) { + return knot_map_errno(); + } +#else + set->pfd[idx].fd = fd; + set->pfd[idx].events = events; + set->pfd[idx].revents = 0; +#endif + + return idx; +} + +int fdset_remove(fdset_t *set, const unsigned idx) +{ + if (set == NULL || idx >= set->n) { + return KNOT_EINVAL; + } + + const int fd = fdset_get_fd(set, idx); +#ifdef HAVE_EPOLL + /* This is necessary as DDNS duplicates file descriptors! */ + if (epoll_ctl(set->pfd, EPOLL_CTL_DEL, fd, NULL) != 0) { + close(fd); + return knot_map_errno(); + } +#elif HAVE_KQUEUE + /* Return delete flag back to original filter number. */ +#if defined(__NetBSD__) + if ((signed short)set->ev[idx].filter < 0) +#else + if (set->ev[idx].filter >= 0) +#endif + { + set->ev[idx].filter = ~set->ev[idx].filter; + } + set->ev[idx].flags = EV_DELETE; + if (kevent(set->pfd, &set->ev[idx], 1, NULL, 0, NULL) < 0) { + close(fd); + return knot_map_errno(); + } +#endif + close(fd); + + const unsigned last = --set->n; + /* Nothing else if it is the last one. Move last -> i if some remain. */ + if (idx < last) { + set->ctx[idx] = set->ctx[last]; + set->timeout[idx] = set->timeout[last]; +#if defined(HAVE_EPOLL) || defined (HAVE_KQUEUE) + set->ev[idx] = set->ev[last]; +#ifdef HAVE_EPOLL + struct epoll_event ev = { + .data.u64 = idx, + .events = set->ev[idx].events + }; + if (epoll_ctl(set->pfd, EPOLL_CTL_MOD, set->ev[last].data.fd, &ev) != 0) { + return knot_map_errno(); + } +#elif HAVE_KQUEUE + EV_SET(&set->ev[idx], set->ev[last].ident, set->ev[last].filter, + EV_ADD, 0, 0, (void *)(intptr_t)idx); + if (kevent(set->pfd, &set->ev[idx], 1, NULL, 0, NULL) < 0) { + return knot_map_errno(); + } +#endif +#else + set->pfd[idx] = set->pfd[last]; +#endif + } + + return KNOT_EOK; +} + +int fdset_poll(fdset_t *set, fdset_it_t *it, const unsigned offset, const int timeout_ms) +{ + if (it == NULL) { + return KNOT_EINVAL; + } + it->unprocessed = 0; + + if (set == NULL) { + return KNOT_EINVAL; + } + + it->set = set; + it->idx = offset; +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) + if (set->recv_size != set->size) { + MEM_RESIZE(set->recv_ev, set->size); + set->recv_size = set->size; + } + it->ptr = set->recv_ev; + it->dirty = 0; +#ifdef HAVE_EPOLL + if (set->n == 0) { + return 0; + } + if ((it->unprocessed = epoll_wait(set->pfd, set->recv_ev, set->recv_size, + timeout_ms)) == -1) { + return knot_map_errno(); + } +#ifndef NDEBUG + /* In specific circumstances with valgrind, it sometimes happens that + * `set->n < it->unprocessed`. */ + if (it->unprocessed > 0 && unlikely(it->unprocessed > set->n)) { + assert(it->unprocessed == 232); + it->unprocessed = 0; + } +#endif +#elif HAVE_KQUEUE + struct timespec timeout = { + .tv_sec = timeout_ms / 1000, + .tv_nsec = (timeout_ms % 1000) * 1000000 + }; + if ((it->unprocessed = kevent(set->pfd, NULL, 0, set->recv_ev, set->recv_size, + (timeout_ms >= 0) ? &timeout : NULL)) == -1) { + return knot_map_errno(); + } +#endif + /* + * NOTE: Can't skip offset without bunch of syscalls! + * Because of that it waits for `ctx->n` (every socket). Offset is set when TCP + * throttling is ON. Sometimes it can return with sockets where none of them is + * connected socket, but it should not be common. + */ + while (it->unprocessed > 0 && fdset_it_get_idx(it) < it->idx) { + it->ptr++; + it->unprocessed--; + } + return it->unprocessed; +#else + it->unprocessed = poll(&set->pfd[offset], set->n - offset, timeout_ms); +#ifndef NDEBUG + /* In specific circumstances with valgrind, it sometimes happens that + * `set->n < it->unprocessed`. */ + if (it->unprocessed > 0 && unlikely(it->unprocessed > set->n - offset)) { + assert(it->unprocessed == 7); + it->unprocessed = 0; + } +#endif + while (it->unprocessed > 0 && set->pfd[it->idx].revents == 0) { + it->idx++; + } + return it->unprocessed; +#endif +} + +void fdset_it_commit(fdset_it_t *it) +{ + if (it == NULL) { + return; + } +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) + /* NOTE: reverse iteration to avoid as much "remove last" operations + * as possible. I'm not sure about performance improvement. It + * will skip some syscalls at begin of iteration, but what + * performance increase do we get is a question. + */ + fdset_t *set = it->set; + for (int i = set->n - 1; it->dirty > 0 && i >= 0; --i) { +#ifdef HAVE_EPOLL + if (set->ev[i].events == FDSET_REMOVE_FLAG) +#else +#if defined(__NetBSD__) + if ((signed short)set->ev[i].filter < 0) +#else + if (set->ev[i].filter >= 0) +#endif +#endif + { + (void)fdset_remove(set, i); + it->dirty--; + } + } + assert(it->dirty == 0); +#endif +} + +int fdset_set_watchdog(fdset_t *set, const unsigned idx, const int interval) +{ + if (set == NULL || idx >= set->n) { + return KNOT_EINVAL; + } + + /* Lift watchdog if interval is negative. */ + if (interval < 0) { + set->timeout[idx] = 0; + return KNOT_EOK; + } + + /* Update clock. */ + const struct timespec now = time_now(); + set->timeout[idx] = now.tv_sec + interval; /* Only seconds precision. */ + + return KNOT_EOK; +} + +void fdset_sweep(fdset_t *set, const fdset_sweep_cb_t cb, void *data) +{ + if (set == NULL || cb == NULL) { + return; + } + + /* Get time threshold. */ + const struct timespec now = time_now(); + unsigned idx = 0; + while (idx < set->n) { + /* Check sweep state, remove if requested. */ + if (set->timeout[idx] > 0 && set->timeout[idx] <= now.tv_sec) { + const int fd = fdset_get_fd(set, idx); + if (cb(set, fd, data) == FDSET_SWEEP) { + (void)fdset_remove(set, idx); + continue; + } + } + ++idx; + } +} diff --git a/src/knot/common/fdset.h b/src/knot/common/fdset.h new file mode 100644 index 0000000..95a5c61 --- /dev/null +++ b/src/knot/common/fdset.h @@ -0,0 +1,382 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +/*! + * \brief I/O multiplexing with context and timeouts for each fd. + */ + +#pragma once + +#include <assert.h> +#include <stdbool.h> +#include <stddef.h> +#include <time.h> + +#ifdef HAVE_EPOLL +#include <sys/epoll.h> +#elif HAVE_KQUEUE +#include <sys/event.h> +#else +#include <poll.h> +#endif + +#include "libknot/errcode.h" + +#define FDSET_RESIZE_STEP 256 +#ifdef HAVE_EPOLL +#define FDSET_REMOVE_FLAG ~0U +#endif + +/*! \brief Set of file descriptors with associated context and timeouts. */ +typedef struct { + unsigned n; /*!< Active fds. */ + unsigned size; /*!< Array size (allocated). */ + void **ctx; /*!< Context for each fd. */ + time_t *timeout; /*!< Timeout for each fd (seconds precision). */ +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) +#ifdef HAVE_EPOLL + struct epoll_event *ev; /*!< Epoll event storage for each fd. */ + struct epoll_event *recv_ev; /*!< Array for polled events. */ +#elif HAVE_KQUEUE + struct kevent *ev; /*!< Kqueue event storage for each fd. */ + struct kevent *recv_ev; /*!< Array for polled events. */ +#endif + unsigned recv_size; /*!< Size of array for polled events. */ + int pfd; /*!< File descriptor of kernel polling structure (epoll or kqueue). */ +#else + struct pollfd *pfd; /*!< Poll state for each fd. */ +#endif +} fdset_t; + +/*! \brief State of iterator over received events */ +typedef struct { + fdset_t *set; /*!< Source fdset_t. */ + unsigned idx; /*!< Event index offset. */ + int unprocessed; /*!< Unprocessed events left. */ +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) +#ifdef HAVE_EPOLL + struct epoll_event *ptr; /*!< Pointer on processed event. */ +#elif HAVE_KQUEUE + struct kevent *ptr; /*!< Pointer on processed event. */ +#endif + unsigned dirty; /*!< Number of fd to be removed on commit. */ +#endif +} fdset_it_t; + +typedef enum { +#ifdef HAVE_EPOLL + FDSET_POLLIN = EPOLLIN, + FDSET_POLLOUT = EPOLLOUT, +#elif HAVE_KQUEUE + FDSET_POLLIN = EVFILT_READ, + FDSET_POLLOUT = EVFILT_WRITE, +#else + FDSET_POLLIN = POLLIN, + FDSET_POLLOUT = POLLOUT, +#endif +} fdset_event_t; + +/*! \brief Mark-and-sweep state. */ +typedef enum { + FDSET_KEEP, + FDSET_SWEEP +} fdset_sweep_state_t; + +/*! \brief Sweep callback (set, index, data) */ +typedef fdset_sweep_state_t (*fdset_sweep_cb_t)(fdset_t *, int, void *); + +/*! + * \brief Initialize fdset to given size. + * + * \param set Target set. + * \param size Initial set size. + * + * \return Error code, KNOT_EOK if success. + */ +int fdset_init(fdset_t *set, const unsigned size); + +/*! + * \brief Clear whole context of the fdset. + * + * \param set Target set. + */ +void fdset_clear(fdset_t *set); + +/*! + * \brief Add file descriptor to watched set. + * + * \param set Target set. + * \param fd Added file descriptor. + * \param events Mask of watched events. + * \param ctx Context (optional). + * + * \retval ret >= 0 is index of the added fd. + * \retval ret < 0 on error. + */ +int fdset_add(fdset_t *set, const int fd, const fdset_event_t events, void *ctx); + +/*! + * \brief Remove and close file descriptor from watched set. + * + * \param set Target set. + * \param idx Index of the removed fd. + * + * \return Error code, KNOT_EOK if success. + */ +int fdset_remove(fdset_t *set, const unsigned idx); + +/*! + * \brief Wait for receive events. + * + * Skip events based on offset and set iterator on first event. + * + * \param set Target set. + * \param it Event iterator storage. + * \param offset Index of first event. + * \param timeout_ms Timeout of operation in milliseconds (use -1 for unlimited). + * + * \retval ret >= 0 represents number of events received. + * \retval ret < 0 on error. + */ +int fdset_poll(fdset_t *set, fdset_it_t *it, const unsigned offset, const int timeout_ms); + +/*! + * \brief Set file descriptor watchdog interval. + * + * Set time (interval from now) after which the associated file descriptor + * should be sweeped (see fdset_sweep). Good example is setting a grace period + * of N seconds between socket activity. If socket is not active within + * <now, now + interval>, it is sweeped and closed. + * + * \param set Target set. + * \param idx Index of the file descriptor. + * \param interval Allowed interval without activity (seconds). + * -1 disables watchdog timer. + * + * \return Error code, KNOT_EOK if success. + */ +int fdset_set_watchdog(fdset_t *set, const unsigned idx, const int interval); + +/*! + * \brief Sweep file descriptors with exceeding inactivity period. + * + * \param set Target set. + * \param cb Callback for sweeped descriptors. + * \param data Pointer to extra data. + */ +void fdset_sweep(fdset_t *set, const fdset_sweep_cb_t cb, void *data); + +/*! + * \brief Returns file descriptor based on index. + * + * \param set Target set. + * \param idx Index of the file descriptor. + * + * \retval ret >= 0 for file descriptor. + * \retval ret < 0 on errors. + */ +inline static int fdset_get_fd(const fdset_t *set, const unsigned idx) +{ + assert(set && idx < set->n); + +#ifdef HAVE_EPOLL + return set->ev[idx].data.fd; +#elif HAVE_KQUEUE + return set->ev[idx].ident; +#else + return set->pfd[idx].fd; +#endif +} + +/*! + * \brief Returns number of file descriptors stored in set. + * + * \param set Target set. + * + * \retval Number of descriptors stored + */ +inline static unsigned fdset_get_length(const fdset_t *set) +{ + assert(set); + + return set->n; +} + +/*! + * \brief Get index of event in set referenced by iterator. + * + * \param it Target iterator. + * + * \retval Index of event. + */ +inline static unsigned fdset_it_get_idx(const fdset_it_t *it) +{ + assert(it); + +#ifdef HAVE_EPOLL + return it->ptr->data.u64; +#elif HAVE_KQUEUE + return (unsigned)(intptr_t)it->ptr->udata; +#else + return it->idx; +#endif +} + +/*! + * \brief Get file descriptor of event referenced by iterator. + * + * \param it Target iterator. + * + * \retval ret >= 0 for file descriptor. + * \retval ret < 0 on errors. + */ +inline static int fdset_it_get_fd(const fdset_it_t *it) +{ + assert(it); + +#ifdef HAVE_EPOLL + return it->set->ev[fdset_it_get_idx(it)].data.fd; +#elif HAVE_KQUEUE + return it->ptr->ident; +#else + return it->set->pfd[it->idx].fd; +#endif +} + +/*! + * \brief Move iterator on next received event. + * + * \param it Target iterator. + */ +inline static void fdset_it_next(fdset_it_t *it) +{ + assert(it); + +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) + do { + it->ptr++; + it->unprocessed--; + } while (it->unprocessed > 0 && fdset_it_get_idx(it) < it->idx); +#else + if (--it->unprocessed > 0) { + while (it->set->pfd[++it->idx].revents == 0); /* nop */ + } +#endif +} + +/*! + * \brief Remove file descriptor referenced by iterator from watched set. + * + * \param it Target iterator. + * + * \return Error code, KNOT_EOK if success. + */ +inline static void fdset_it_remove(fdset_it_t *it) +{ + assert(it); + +#ifdef HAVE_EPOLL + const int idx = fdset_it_get_idx(it); + it->set->ev[idx].events = FDSET_REMOVE_FLAG; + it->dirty++; +#elif HAVE_KQUEUE + const int idx = fdset_it_get_idx(it); + /* Bitwise negated filter marks event for delete. */ + /* Filters become: */ + /* [FreeBSD] */ + /* EVFILT_READ (-1) -> 0 */ + /* EVFILT_WRITE (-2) -> 1 */ + /* [NetBSD] */ + /* EVFILT_READ (0) -> -1 */ + /* EVFILT_WRITE (1) -> -2 */ + /* If not marked for delete then mark for delete. */ +#if defined(__NetBSD__) + if ((signed short)it->set->ev[idx].filter >= 0) +#else + if (it->set->ev[idx].filter < 0) +#endif + { + it->set->ev[idx].filter = ~it->set->ev[idx].filter; + } + it->dirty++; +#else + (void)fdset_remove(it->set, fdset_it_get_idx(it)); + /* Iterator should return on last valid already processed element. */ + /* On `next` call (in for-loop) will point on first unprocessed. */ + it->idx--; +#endif +} + +/*! + * \brief Commit changes made in fdset using iterator. + * + * \param it Target iterator. + */ +void fdset_it_commit(fdset_it_t *it); + +/*! + * \brief Decide if there is more received events. + * + * \param it Target iterator. + * + * \retval Logical flag representing 'done' state. + */ +inline static bool fdset_it_is_done(const fdset_it_t *it) +{ + assert(it); + + return it->unprocessed <= 0; +} + +/*! + * \brief Decide if event referenced by iterator is POLLIN event. + * + * \param it Target iterator. + * + * \retval Logical flag represents 'POLLIN' event received. + */ +inline static bool fdset_it_is_pollin(const fdset_it_t *it) +{ + assert(it); + +#ifdef HAVE_EPOLL + return it->ptr->events & EPOLLIN; +#elif HAVE_KQUEUE + return it->ptr->filter == EVFILT_READ; +#else + return it->set->pfd[it->idx].revents & POLLIN; +#endif +} + +/*! + * \brief Decide if event referenced by iterator is error event. + * + * \param it Target iterator. + * + * \retval Logical flag represents error event received. + */ +inline static bool fdset_it_is_error(const fdset_it_t *it) +{ + assert(it); + +#ifdef HAVE_EPOLL + return it->ptr->events & (EPOLLERR | EPOLLHUP); +#elif HAVE_KQUEUE + return it->ptr->flags & EV_ERROR; +#else + return it->set->pfd[it->idx].revents & (POLLERR | POLLHUP | POLLNVAL); +#endif +} diff --git a/src/knot/common/log.c b/src/knot/common/log.c new file mode 100644 index 0000000..8bbdc51 --- /dev/null +++ b/src/knot/common/log.c @@ -0,0 +1,491 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdarg.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/time.h> +#include <time.h> +#include <urcu.h> + +#ifdef ENABLE_SYSTEMD +#define SD_JOURNAL_SUPPRESS_LOCATION 1 +#include <systemd/sd-journal.h> +#include <systemd/sd-daemon.h> +#endif + +#include "knot/common/log.h" +#include "libknot/libknot.h" +#include "contrib/ucw/lists.h" + +/*! Single log message buffer length (one line). */ +#define LOG_BUFLEN 512 +#define NULL_ZONE_STR "?" + +#ifdef ENABLE_SYSTEMD +int use_journal = 0; +#endif + +/*! Log context. */ +typedef struct { + size_t target_count; /*!< Log target count. */ + int *target; /*!< Log targets. */ + size_t file_count; /*!< Open files count. */ + FILE **file; /*!< Open files. */ + log_flag_t flags; /*!< Formatting flags. */ +} log_t; + +/*! Log singleton. */ +log_t *s_log = NULL; + +static bool log_isopen(void) +{ + return s_log != NULL; +} + +static void sink_free(log_t *log) +{ + if (log == NULL) { + return; + } + + // Close open log files. + for (int i = 0; i < log->file_count; ++i) { + fclose(log->file[i]); + } + free(log->target); + free(log->file); + free(log); +} + +/*! + * \brief Create logging targets respecting their canonical order. + * + * Facilities ordering: Syslog, Stderr, Stdout, File0... + */ +static log_t *sink_setup(size_t file_count) +{ + log_t *log = malloc(sizeof(*log)); + if (log == NULL) { + return NULL; + } + memset(log, 0, sizeof(*log)); + + // Reserve space for targets. + log->target_count = LOG_TARGET_FILE + file_count; + log->target = malloc(LOG_SOURCE_ANY * sizeof(int) * log->target_count); + if (!log->target) { + free(log); + return NULL; + } + memset(log->target, 0, LOG_SOURCE_ANY * sizeof(int) * log->target_count); + + // Reserve space for log files. + if (file_count > 0) { + log->file = malloc(sizeof(FILE *) * file_count); + if (!log->file) { + free(log->target); + free(log); + return NULL; + } + memset(log->file, 0, sizeof(FILE *) * file_count); + } + + return log; +} + +static void sink_publish(log_t *log) +{ + log_t **current_log = &s_log; + log_t *old_log = rcu_xchg_pointer(current_log, log); + synchronize_rcu(); + sink_free(old_log); +} + +static int *src_levels(log_t *log, log_target_t target, log_source_t src) +{ + assert(src < LOG_SOURCE_ANY); + return &log->target[LOG_SOURCE_ANY * target + src]; +} + +static void sink_levels_set(log_t *log, log_target_t target, log_source_t src, int levels) +{ + // Assign levels to the specified source. + if (src != LOG_SOURCE_ANY) { + *src_levels(log, target, src) = levels; + } else { + // ANY ~ set levels to all sources. + for (int i = 0; i < LOG_SOURCE_ANY; ++i) { + *src_levels(log, target, i) = levels; + } + } +} + +static void sink_levels_add(log_t *log, log_target_t target, log_source_t src, int levels) +{ + // Add levels to the specified source. + if (src != LOG_SOURCE_ANY) { + *src_levels(log, target, src) |= levels; + } else { + // ANY ~ add levels to all sources. + for (int i = 0; i < LOG_SOURCE_ANY; ++i) { + *src_levels(log, target, i) |= levels; + } + } +} + +void log_init(void) +{ + // Setup initial state. + int emask = LOG_MASK(LOG_CRIT) | LOG_MASK(LOG_ERR) | LOG_MASK(LOG_WARNING); + int imask = LOG_MASK(LOG_NOTICE) | LOG_MASK(LOG_INFO); + + // Publish base log sink. + log_t *log = sink_setup(0); + if (log == NULL) { + fprintf(stderr, "Failed to setup logging\n"); + return; + } + +#ifdef ENABLE_SYSTEMD + // Should only use the journal if system was booted with systemd. + use_journal = sd_booted(); +#endif + + sink_levels_set(log, LOG_TARGET_SYSLOG, LOG_SOURCE_ANY, emask); + sink_levels_set(log, LOG_TARGET_STDERR, LOG_SOURCE_ANY, emask); + sink_levels_set(log, LOG_TARGET_STDOUT, LOG_SOURCE_ANY, imask); + sink_publish(log); + + setlogmask(LOG_UPTO(LOG_DEBUG)); + openlog(PACKAGE_NAME, LOG_PID, LOG_DAEMON); +} + +void log_close(void) +{ + sink_publish(NULL); + + fflush(stdout); + fflush(stderr); + + closelog(); +} + +void log_flag_set(log_flag_t flag) +{ + if (log_isopen()) { + s_log->flags |= flag; + } +} + +void log_levels_set(log_target_t target, log_source_t src, int levels) +{ + if (log_isopen()) { + sink_levels_set(s_log, target, src, levels); + } +} + +void log_levels_add(log_target_t target, log_source_t src, int levels) +{ + if (log_isopen()) { + sink_levels_add(s_log, target, src, levels); + } +} + +static void emit_log_msg(int level, log_source_t src, const char *zone, + size_t zone_len, const char *msg, const char *param) +{ + log_t *log = s_log; + + // Syslog target. + if (*src_levels(log, LOG_TARGET_SYSLOG, src) & LOG_MASK(level)) { +#ifdef ENABLE_SYSTEMD + if (use_journal) { + char *zone_fmt = zone ? "ZONE=%.*s." : NULL; + sd_journal_send("PRIORITY=%d", level, + "MESSAGE=%s", msg, + zone_fmt, zone_len, zone, + param, NULL); + } else +#endif + { + syslog(level, "%s", msg); + } + } + + // Prefix date and time. + char tstr[LOG_BUFLEN] = { 0 }; + if (!(s_log->flags & LOG_FLAG_NOTIMESTAMP)) { + struct tm lt; + struct timeval tv; + gettimeofday(&tv, NULL); + time_t sec = tv.tv_sec; + if (localtime_r(&sec, <) != NULL) { + strftime(tstr, sizeof(tstr), KNOT_LOG_TIME_FORMAT " ", <); + } + } + + // Other log targets. + for (int i = LOG_TARGET_STDERR; i < LOG_TARGET_FILE + log->file_count; ++i) { + if (*src_levels(log, i, src) & LOG_MASK(level)) { + FILE *stream; + switch (i) { + case LOG_TARGET_STDERR: stream = stderr; break; + case LOG_TARGET_STDOUT: stream = stdout; break; + default: stream = log->file[i - LOG_TARGET_FILE]; break; + } + + // Print the message. + fprintf(stream, "%s%s\n", tstr, msg); + if (stream == stdout) { + fflush(stream); + } + } + } +} + +static const char *level_prefix(int level) +{ + switch (level) { + case LOG_DEBUG: return "debug"; + case LOG_INFO: return "info"; + case LOG_NOTICE: return "notice"; + case LOG_WARNING: return "warning"; + case LOG_ERR: return "error"; + case LOG_CRIT: return "critical"; + default: return NULL; + }; +} + +static int log_msg_add(char **write, size_t *capacity, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + int written = vsnprintf(*write, *capacity, fmt, args); + va_end(args); + + if (written < 0 || written >= *capacity) { + return KNOT_ESPACE; + } + + *write += written; + *capacity -= written; + + return KNOT_EOK; +} + +static void log_msg_text(int level, log_source_t src, const char *zone, + const char *fmt, va_list args, const char *param) +{ + if (!log_isopen() || src == LOG_SOURCE_ANY) { + return; + } + + // Buffer for log message. + char buff[LOG_BUFLEN]; + char *write = buff; + size_t capacity = sizeof(buff); + + rcu_read_lock(); + + // Prefix error level. + if (level != LOG_INFO || !(s_log->flags & LOG_FLAG_NOINFO)) { + const char *prefix = level_prefix(level); + int ret = log_msg_add(&write, &capacity, "%s: ", prefix); + if (ret != KNOT_EOK) { + rcu_read_unlock(); + return; + } + } + + // Prefix zone name. + size_t zone_len = 0; + if (zone != NULL) { + zone_len = strlen(zone); + if (zone_len > 0 && zone[zone_len - 1] == '.') { + zone_len--; + } + + int ret = log_msg_add(&write, &capacity, "[%.*s.] ", (int)zone_len, zone); + if (ret != KNOT_EOK) { + rcu_read_unlock(); + return; + } + } + + // Compile log message. + int ret = vsnprintf(write, capacity, fmt, args); + if (ret >= 0) { + // Send to logging targets. + emit_log_msg(level, src, zone, zone_len, buff, param); + } + + rcu_read_unlock(); +} + +void log_fmt(int priority, log_source_t src, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + log_msg_text(priority, src, NULL, fmt, args, NULL); + va_end(args); +} + +void log_fmt_zone(int priority, log_source_t src, const knot_dname_t *zone, + const char *param, const char *fmt, ...) +{ + knot_dname_txt_storage_t buff; + char *zone_str = knot_dname_to_str(buff, zone, sizeof(buff)); + if (zone_str == NULL) { + zone_str = NULL_ZONE_STR; + } + + va_list args; + va_start(args, fmt); + log_msg_text(priority, src, zone_str, fmt, args, param); + va_end(args); +} + +void log_fmt_zone_str(int priority, log_source_t src, const char *zone, + const char *fmt, ...) +{ + if (zone == NULL) { + zone = NULL_ZONE_STR; + } + + va_list args; + va_start(args, fmt); + log_msg_text(priority, src, zone, fmt, args, NULL); + va_end(args); +} + +int log_update_privileges(int uid, int gid) +{ + if (!log_isopen()) { + return KNOT_EOK; + } + + for (int i = 0; i < s_log->file_count; ++i) { + if (fchown(fileno(s_log->file[i]), uid, gid) < 0) { + log_error("failed to change log file owner"); + } + } + + return KNOT_EOK; +} + +static log_target_t get_logtype(const char *logname) +{ + assert(logname); + + if (strcasecmp(logname, "syslog") == 0) { + return LOG_TARGET_SYSLOG; + } else if (strcasecmp(logname, "stderr") == 0) { + return LOG_TARGET_STDERR; + } else if (strcasecmp(logname, "stdout") == 0) { + return LOG_TARGET_STDOUT; + } else { + return LOG_TARGET_FILE; + } +} + +static int log_open_file(log_t *log, const char *filename) +{ + assert(LOG_TARGET_FILE + log->file_count < log->target_count); + + // Open the file. + log->file[log->file_count] = fopen(filename, "a"); + if (log->file[log->file_count] == NULL) { + return knot_map_errno(); + } + + // Disable buffering. + setvbuf(log->file[log->file_count], NULL, _IONBF, 0); + + return LOG_TARGET_FILE + log->file_count++; +} + +void log_reconfigure(conf_t *conf) +{ + // Use defaults if no 'log' section is configured. + if (conf_id_count(conf, C_LOG) == 0) { + log_close(); + log_init(); + return; + } + + // Find maximum log target id. + unsigned files = 0; + for (conf_iter_t iter = conf_iter(conf, C_LOG); iter.code == KNOT_EOK; + conf_iter_next(conf, &iter)) { + conf_val_t id = conf_iter_id(conf, &iter); + if (get_logtype(conf_str(&id)) == LOG_TARGET_FILE) { + ++files; + } + } + + // Initialize logsystem. + log_t *log = sink_setup(files); + if (log == NULL) { + fprintf(stderr, "Failed to setup logging\n"); + return; + } + + // Setup logs. + for (conf_iter_t iter = conf_iter(conf, C_LOG); iter.code == KNOT_EOK; + conf_iter_next(conf, &iter)) { + conf_val_t id = conf_iter_id(conf, &iter); + const char *logname = conf_str(&id); + + // Get target. + int target = get_logtype(logname); + if (target == LOG_TARGET_FILE) { + target = log_open_file(log, logname); + if (target < 0) { + log_error("failed to open log, file '%s' (%s)", + logname, knot_strerror(target)); + continue; + } + } + + conf_val_t levels_val; + unsigned levels; + + // Set SERVER logging. + levels_val = conf_id_get(conf, C_LOG, C_SERVER, &id); + levels = conf_opt(&levels_val); + sink_levels_add(log, target, LOG_SOURCE_SERVER, levels); + + // Set CONTROL logging. + levels_val = conf_id_get(conf, C_LOG, C_CTL, &id); + levels = conf_opt(&levels_val); + sink_levels_add(log, target, LOG_SOURCE_CONTROL, levels); + + // Set ZONE logging. + levels_val = conf_id_get(conf, C_LOG, C_ZONE, &id); + levels = conf_opt(&levels_val); + sink_levels_add(log, target, LOG_SOURCE_ZONE, levels); + + // Set ANY logging. + levels_val = conf_id_get(conf, C_LOG, C_ANY, &id); + levels = conf_opt(&levels_val); + sink_levels_add(log, target, LOG_SOURCE_ANY, levels); + } + + sink_publish(log); +} diff --git a/src/knot/common/log.h b/src/knot/common/log.h new file mode 100644 index 0000000..49a8375 --- /dev/null +++ b/src/knot/common/log.h @@ -0,0 +1,187 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +/*! + * \brief Logging facility. + * + * Supported log levels/priorities: + * LOG_CRIT, LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_INFO, and LOG_DEBUG. + * + * \see syslog.h + */ + +#pragma once + +#include <assert.h> +#include <syslog.h> +#include <stdint.h> +#include <stdbool.h> + +#include "libknot/dname.h" +#include "knot/conf/conf.h" + +/*! \brief Format for timestamps in log files. */ +#define KNOT_LOG_TIME_FORMAT "%Y-%m-%dT%H:%M:%S%z" + +/*! \brief Logging targets. */ +typedef enum { + LOG_TARGET_SYSLOG = 0, /*!< System log. */ + LOG_TARGET_STDERR = 1, /*!< Standard error stream. */ + LOG_TARGET_STDOUT = 2, /*!< Standard output stream. */ + LOG_TARGET_FILE = 3 /*!< Generic logging to a file (unbuffered). */ +} log_target_t; + +/*! \brief Logging sources. */ +typedef enum { + LOG_SOURCE_SERVER = 0, /*!< Server module. */ + LOG_SOURCE_CONTROL = 1, /*!< Server control module. */ + LOG_SOURCE_ZONE = 2, /*!< Zone manipulation module. */ + LOG_SOURCE_ANY = 3 /*!< Any module. */ +} log_source_t; + +/*! \brief Logging format flags. */ +typedef enum { + LOG_FLAG_NOTIMESTAMP = 1 << 0, /*!< Don't print timestamp prefix. */ + LOG_FLAG_NOINFO = 1 << 1 /*!< Don't print info level prefix. */ +} log_flag_t; + +/*! + * \brief Setup logging subsystem. + */ +void log_init(void); + +/*! + * \brief Close and deinitialize log. + */ +void log_close(void); + +/*! + * \brief Set logging format flag. + */ +void log_flag_set(log_flag_t flag); + +/*! + * \brief Set log levels for given target. + * + * \param target Logging target index (LOG_TARGET_SYSLOG...). + * \param src Logging source (LOG_SOURCE_SERVER...LOG_SOURCE_ANY). + * \param levels Bitmask of specified log levels. + */ +void log_levels_set(log_target_t target, log_source_t src, int levels); + +/*! + * \brief Add log levels to a given target. + * + * New levels are added on top of existing, the resulting levels set is + * "old_levels OR new_levels". + * + * \param target Logging target index (LOG_TARGET_SYSLOG...). + * \param src Logging source (LOG_SOURCE_SERVER...LOG_SOURCE_ANY). + * \param levels Bitmask of specified log levels. + */ +void log_levels_add(log_target_t target, log_source_t src, int levels); + +/*! + * \brief Log message into server category. + * + * Function follows printf() format. + * + * \note LOG_SOURCE_ANY is not a valid value for the src parameter. + * + * \param priority Message priority. + * \param src Message source (LOG_SOURCE_SERVER...LOG_SOURCE_ZONE). + * \param fmt Content of the logged message. + */ +void log_fmt(int priority, log_source_t src, const char *fmt, ...) +__attribute__((format(printf, 3, 4))); + +/*! + * \brief Log message into zone category. + * + * \see log_fmt + * + * \param priority Message priority. + * \param src Message source (LOG_SOURCE_SERVER...LOG_SOURCE_ZONE). + * \param zone Zone name in wire format. + * \param param Optional key-value parameter for structured logging. + * \param fmt Content of the logged message. + */ +void log_fmt_zone(int priority, log_source_t src, const knot_dname_t *zone, + const char *param, const char *fmt, ...) +__attribute__((format(printf, 5, 6))); + +/*! + * \brief Log message into zone category. + * + * \see log_fmt + * + * \param zone Zone name as an ASCII string. + * \param priority Message priority. + * \param src Message source (LOG_SOURCE_SERVER...LOG_SOURCE_ZONE). + * \param fmt Content of the logged message. + */ +void log_fmt_zone_str(int priority, log_source_t src, const char *zone, const char *fmt, ...) +__attribute__((format(printf, 4, 5))); + +/*! + * \brief Convenient logging macros. + */ +#define log_fatal(msg, ...) log_fmt(LOG_CRIT, LOG_SOURCE_SERVER, msg, ##__VA_ARGS__) +#define log_error(msg, ...) log_fmt(LOG_ERR, LOG_SOURCE_SERVER, msg, ##__VA_ARGS__) +#define log_warning(msg, ...) log_fmt(LOG_WARNING, LOG_SOURCE_SERVER, msg, ##__VA_ARGS__) +#define log_notice(msg, ...) log_fmt(LOG_NOTICE, LOG_SOURCE_SERVER, msg, ##__VA_ARGS__) +#define log_info(msg, ...) log_fmt(LOG_INFO, LOG_SOURCE_SERVER, msg, ##__VA_ARGS__) +#define log_debug(msg, ...) log_fmt(LOG_DEBUG, LOG_SOURCE_SERVER, msg, ##__VA_ARGS__) + +#define log_ctl_fatal(msg, ...) log_fmt(LOG_CRIT, LOG_SOURCE_CONTROL, msg, ##__VA_ARGS__) +#define log_ctl_error(msg, ...) log_fmt(LOG_ERR, LOG_SOURCE_CONTROL, msg, ##__VA_ARGS__) +#define log_ctl_warning(msg, ...) log_fmt(LOG_WARNING, LOG_SOURCE_CONTROL, msg, ##__VA_ARGS__) +#define log_ctl_notice(msg, ...) log_fmt(LOG_NOTICE, LOG_SOURCE_CONTROL, msg, ##__VA_ARGS__) +#define log_ctl_info(msg, ...) log_fmt(LOG_INFO, LOG_SOURCE_CONTROL, msg, ##__VA_ARGS__) +#define log_ctl_debug(msg, ...) log_fmt(LOG_DEBUG, LOG_SOURCE_CONTROL, msg, ##__VA_ARGS__) + +#define log_ctl_zone_str_error(zone, msg, ...) log_fmt_zone_str(LOG_ERR, LOG_SOURCE_CONTROL, zone, msg, ##__VA_ARGS__) +#define log_ctl_zone_str_info(zone, msg, ...) log_fmt_zone_str(LOG_INFO, LOG_SOURCE_CONTROL, zone, msg, ##__VA_ARGS__) +#define log_ctl_zone_str_debug(zone, msg, ...) log_fmt_zone_str(LOG_DEBUG, LOG_SOURCE_CONTROL, zone, msg, ##__VA_ARGS__) + +#define log_zone_fatal(zone, msg, ...) log_fmt_zone(LOG_CRIT, LOG_SOURCE_ZONE, zone, NULL, msg, ##__VA_ARGS__) +#define log_zone_error(zone, msg, ...) log_fmt_zone(LOG_ERR, LOG_SOURCE_ZONE, zone, NULL, msg, ##__VA_ARGS__) +#define log_zone_warning(zone, msg, ...) log_fmt_zone(LOG_WARNING, LOG_SOURCE_ZONE, zone, NULL, msg, ##__VA_ARGS__) +#define log_zone_notice(zone, msg, ...) log_fmt_zone(LOG_NOTICE, LOG_SOURCE_ZONE, zone, NULL, msg, ##__VA_ARGS__) +#define log_zone_info(zone, msg, ...) log_fmt_zone(LOG_INFO, LOG_SOURCE_ZONE, zone, NULL, msg, ##__VA_ARGS__) +#define log_zone_debug(zone, msg, ...) log_fmt_zone(LOG_DEBUG, LOG_SOURCE_ZONE, zone, NULL, msg, ##__VA_ARGS__) + +#define log_zone_str_fatal(zone, msg, ...) log_fmt_zone_str(LOG_CRIT, LOG_SOURCE_ZONE, zone, msg, ##__VA_ARGS__) +#define log_zone_str_error(zone, msg, ...) log_fmt_zone_str(LOG_ERR, LOG_SOURCE_ZONE, zone, msg, ##__VA_ARGS__) +#define log_zone_str_warning(zone, msg, ...) log_fmt_zone_str(LOG_WARNING, LOG_SOURCE_ZONE, zone, msg, ##__VA_ARGS__) +#define log_zone_str_notice(zone, msg, ...) log_fmt_zone_str(LOG_NOTICE, LOG_SOURCE_ZONE, zone, msg, ##__VA_ARGS__) +#define log_zone_str_info(zone, msg, ...) log_fmt_zone_str(LOG_INFO, LOG_SOURCE_ZONE, zone, msg, ##__VA_ARGS__) +#define log_zone_str_debug(zone, msg, ...) log_fmt_zone_str(LOG_DEBUG, LOG_SOURCE_ZONE, zone, msg, ##__VA_ARGS__) + +/*! + * \brief Update open files ownership. + * + * \param uid New owner id. + * \param gid New group id. + * + * \return Error code, KNOT_EOK if success. + */ +int log_update_privileges(int uid, int gid); + +/*! + * \brief Setup logging facilities from config. + */ +void log_reconfigure(conf_t *conf); diff --git a/src/knot/common/process.c b/src/knot/common/process.c new file mode 100644 index 0000000..bdec1d4 --- /dev/null +++ b/src/knot/common/process.c @@ -0,0 +1,194 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <errno.h> +#include <fcntl.h> +#include <grp.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "knot/common/log.h" +#include "knot/common/process.h" +#include "knot/conf/conf.h" +#include "libknot/errcode.h" + +static char* pid_filename(void) +{ + conf_val_t val = conf_get(conf(), C_SRV, C_RUNDIR); + char *rundir = conf_abs_path(&val, NULL); + val = conf_get(conf(), C_SRV, C_PIDFILE); + char *pidfile = conf_abs_path(&val, rundir); + free(rundir); + + return pidfile; +} + +static pid_t pid_read(const char *filename) +{ + if (filename == NULL) { + return 0; + } + + size_t len = 0; + char buf[64] = { 0 }; + + FILE *fp = fopen(filename, "r"); + if (fp == NULL) { + return 0; + } + + /* Read the content of the file. */ + len = fread(buf, 1, sizeof(buf) - 1, fp); + fclose(fp); + if (len < 1) { + return 0; + } + + /* Convert pid. */ + errno = 0; + char *end = 0; + unsigned long pid = strtoul(buf, &end, 10); + if (end == buf || *end != '\0'|| errno != 0) { + return 0; + } + + return (pid_t)pid; +} + +static int pid_write(const char *filename, pid_t pid) +{ + if (filename == NULL) { + return KNOT_EINVAL; + } + + /* Convert. */ + char buf[64]; + int len = 0; + len = snprintf(buf, sizeof(buf), "%lu", (unsigned long)pid); + if (len < 0 || len >= sizeof(buf)) { + return KNOT_ENOMEM; + } + + /* Create file. */ + int ret = KNOT_EOK; + int fd = open(filename, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP); + if (fd >= 0) { + if (write(fd, buf, len) != len) { + ret = knot_map_errno(); + } + close(fd); + } else { + ret = knot_map_errno(); + } + + return ret; +} + +unsigned long pid_check_and_create(void) +{ + struct stat st; + char *pidfile = pid_filename(); + pid_t pid = pid_read(pidfile); + + /* Check PID for existence and liveness. */ + if (pid > 0 && pid_running(pid)) { + log_fatal("server PID found, already running"); + free(pidfile); + return 0; + } else if (stat(pidfile, &st) == 0) { + assert(pidfile); + log_warning("removing stale PID file '%s'", pidfile); + pid_cleanup(); + } + + /* Get current PID. */ + pid = getpid(); + + /* Create a PID file. */ + int ret = pid_write(pidfile, pid); + if (ret != KNOT_EOK) { + log_fatal("failed to create a PID file '%s' (%s)", pidfile, + knot_strerror(ret)); + free(pidfile); + return 0; + } + free(pidfile); + + return (unsigned long)pid; +} + +void pid_cleanup(void) +{ + char *pidfile = pid_filename(); + if (pidfile != NULL) { + (void)unlink(pidfile); + free(pidfile); + } +} + +bool pid_running(pid_t pid) +{ + return kill(pid, 0) == 0; +} + +int proc_update_privileges(int uid, int gid) +{ +#ifdef HAVE_SETGROUPS + /* Drop supplementary groups. */ + if ((uid_t)uid != getuid() || (gid_t)gid != getgid()) { + if (setgroups(0, NULL) < 0) { + log_warning("failed to drop supplementary groups for " + "UID %d (%s)", getuid(), strerror(errno)); + } +# ifdef HAVE_INITGROUPS + struct passwd *pw; + if ((pw = getpwuid(uid)) == NULL) { + log_warning("failed to get passwd entry for UID %d (%s)", + uid, strerror(errno)); + } else { + if (initgroups(pw->pw_name, gid) < 0) { + log_warning("failed to set supplementary groups " + "for UID %d (%s)", uid, strerror(errno)); + } + } +# endif /* HAVE_INITGROUPS */ + } +#endif /* HAVE_SETGROUPS */ + + /* Watch uid/gid. */ + if ((gid_t)gid != getgid()) { + log_info("changing GID to %d", gid); + if (setregid(gid, gid) < 0) { + log_error("failed to change GID to %d", gid); + return KNOT_ERROR; + } + } + if ((uid_t)uid != getuid()) { + log_info("changing UID to %d", uid); + if (setreuid(uid, uid) < 0) { + log_error("failed to change UID to %d", uid); + return KNOT_ERROR; + } + } + + return KNOT_EOK; +} diff --git a/src/knot/common/process.h b/src/knot/common/process.h new file mode 100644 index 0000000..14ca34e --- /dev/null +++ b/src/knot/common/process.h @@ -0,0 +1,60 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +/*! + * \brief Functions for POSIX process handling. + */ + +#pragma once + +#include <stdbool.h> +#include <unistd.h> + +/*! + * \brief Check if PID file exists and create it if possible. + * + * \retval 0 if failed. + * \retval Current PID. + */ +unsigned long pid_check_and_create(void); + +/*! + * \brief Remove PID file. + * + * \warning PID file content won't be checked. + */ +void pid_cleanup(void); + +/*! + * \brief Return true if the PID is running. + * + * \param pid Process ID. + * + * \retval 1 if running. + * \retval 0 if not running (or error). + */ +bool pid_running(pid_t pid); + +/*! + * \brief Update process privileges to new UID/GID. + * + * \param uid New user ID. + * \param gid New group ID. + * + * \retval KNOT_EOK on success. + * \retval KNOT_ERROR if UID or GID change failed. + */ +int proc_update_privileges(int uid, int gid); diff --git a/src/knot/common/stats.c b/src/knot/common/stats.c new file mode 100644 index 0000000..2b8cb09 --- /dev/null +++ b/src/knot/common/stats.c @@ -0,0 +1,309 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <inttypes.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> +#include <urcu.h> + +#include "contrib/files.h" +#include "knot/common/stats.h" +#include "knot/common/log.h" +#include "knot/nameserver/query_module.h" + +struct { + bool active_dumper; + pthread_t dumper; + uint32_t timer; + server_t *server; +} stats = { 0 }; + +typedef struct { + FILE *fd; + const list_t *query_modules; + const knot_dname_t *zone; + bool zone_emitted; +} dump_ctx_t; + +#define DUMP_STR(fd, level, name, ...) do { \ + fprintf(fd, "%-.*s"name": %s\n", level, " ", ##__VA_ARGS__); \ + } while (0) +#define DUMP_CTR(fd, level, name, ...) do { \ + fprintf(fd, "%-.*s"name": %"PRIu64"\n", level, " ", ##__VA_ARGS__); \ + } while (0) + +uint64_t server_zone_count(server_t *server) +{ + return knot_zonedb_size(server->zone_db); +} + +const stats_item_t server_stats[] = { + { "zone-count", server_zone_count }, + { 0 } +}; + +uint64_t stats_get_counter(uint64_t **stats_vals, uint32_t offset, unsigned threads) +{ + uint64_t res = 0; + for (unsigned i = 0; i < threads; i++) { + res += ATOMIC_GET(stats_vals[i][offset]); + } + return res; +} + +static void dump_counters(FILE *fd, int level, mod_ctr_t *ctr, uint64_t **stats_vals, unsigned threads) +{ + for (uint32_t j = 0; j < ctr->count; j++) { + uint64_t counter = stats_get_counter(stats_vals, ctr->offset + j, threads); + + // Skip empty counters. + if (counter == 0) { + continue; + } + + if (ctr->idx_to_str != NULL) { + char *str = ctr->idx_to_str(j, ctr->count); + if (str != NULL) { + DUMP_CTR(fd, level, "%s", str, counter); + free(str); + } + } else { + DUMP_CTR(fd, level, "%u", j, counter); + } + } +} + +static void dump_modules(dump_ctx_t *ctx) +{ + int level = 0; + knotd_mod_t *mod; + WALK_LIST(mod, *ctx->query_modules) { + // Skip modules without statistics. + if (mod->stats_count == 0) { + continue; + } + + // Dump zone name. + if (ctx->zone != NULL) { + // Prevent from zone section override. + if (!ctx->zone_emitted) { + DUMP_STR(ctx->fd, 0, "zone", ""); + ctx->zone_emitted = true; + } + level = 1; + + knot_dname_txt_storage_t name; + if (knot_dname_to_str(name, ctx->zone, sizeof(name)) == NULL) { + return; + } + DUMP_STR(ctx->fd, level++, "\"%s\"", name, ""); + } else { + level = 0; + } + + unsigned threads = knotd_mod_threads(mod); + + // Dump module counters. + DUMP_STR(ctx->fd, level, "%s", mod->id->name + 1, ""); + for (int i = 0; i < mod->stats_count; i++) { + mod_ctr_t *ctr = mod->stats_info + i; + if (ctr->name == NULL) { + // Empty counter. + continue; + } + if (ctr->count == 1) { + // Simple counter. + uint64_t counter = stats_get_counter(mod->stats_vals, + ctr->offset, threads); + DUMP_CTR(ctx->fd, level + 1, "%s", ctr->name, counter); + } else { + // Array of counters. + DUMP_STR(ctx->fd, level + 1, "%s", ctr->name, ""); + dump_counters(ctx->fd, level + 2, ctr, mod->stats_vals, threads); + } + } + } +} + +static void zone_stats_dump(zone_t *zone, dump_ctx_t *ctx) +{ + if (EMPTY_LIST(zone->query_modules)) { + return; + } + + ctx->query_modules = &zone->query_modules; + ctx->zone = zone->name; + + dump_modules(ctx); +} + +static void dump_to_file(FILE *fd, server_t *server) +{ + char date[64] = ""; + + // Get formatted current time string. + struct tm tm; + time_t now = time(NULL); + localtime_r(&now, &tm); + strftime(date, sizeof(date), KNOT_LOG_TIME_FORMAT, &tm); + + // Get the server identity. + conf_val_t val = conf_get(conf(), C_SRV, C_IDENT); + const char *ident = conf_str(&val); + if (ident == NULL || ident[0] == '\0') { + ident = conf()->hostname; + } + + // Dump record header. + fprintf(fd, + "---\n" + "time: %s\n" + "identity: %s\n", + date, ident); + + // Dump server statistics. + DUMP_STR(fd, 0, "server", ""); + for (const stats_item_t *item = server_stats; item->name != NULL; item++) { + DUMP_CTR(fd, 1, "%s", item->name, item->val(server)); + } + + dump_ctx_t ctx = { + .fd = fd, + .query_modules = conf()->query_modules, + }; + + // Dump global statistics. + dump_modules(&ctx); + + // Dump zone statistics. + knot_zonedb_foreach(server->zone_db, zone_stats_dump, &ctx); +} + +static void dump_stats(server_t *server) +{ + conf_t *pconf = conf(); + conf_val_t val = conf_get(pconf, C_SRV, C_RUNDIR); + char *rundir = conf_abs_path(&val, NULL); + val = conf_get(pconf, C_STATS, C_FILE); + char *file_name = conf_abs_path(&val, rundir); + free(rundir); + + val = conf_get(pconf, C_STATS, C_APPEND); + bool append = conf_bool(&val); + + // Open or create output file. + FILE *fd = NULL; + char *tmp_name = NULL; + if (append) { + fd = fopen(file_name, "a"); + if (fd == NULL) { + log_error("stats, failed to append file '%s' (%s)", + file_name, knot_strerror(knot_map_errno())); + free(file_name); + return; + } + } else { + int ret = open_tmp_file(file_name, &tmp_name, &fd, + S_IRUSR | S_IWUSR | S_IRGRP); + if (ret != KNOT_EOK) { + log_error("stats, failed to open file '%s' (%s)", + file_name, knot_strerror(ret)); + free(file_name); + return; + } + } + assert(fd); + + // Dump stats into the file. + dump_to_file(fd, server); + + fflush(fd); + fclose(fd); + + // Switch the file contents. + if (!append) { + int ret = rename(tmp_name, file_name); + if (ret != 0) { + log_error("stats, failed to access file '%s' (%s)", + file_name, knot_strerror(knot_map_errno())); + unlink(tmp_name); + } + free(tmp_name); + } + + log_debug("stats, dumped into file '%s'", file_name); + free(file_name); +} + +static void *dumper(void *data) +{ + rcu_register_thread(); + while (true) { + assert(stats.timer > 0); + sleep(stats.timer); + + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + rcu_read_lock(); + dump_stats(stats.server); + rcu_read_unlock(); + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + } + rcu_unregister_thread(); + return NULL; +} + +void stats_reconfigure(conf_t *conf, server_t *server) +{ + if (conf == NULL || server == NULL) { + return; + } + + // Update server context. + stats.server = server; + + conf_val_t val = conf_get(conf, C_STATS, C_TIMER); + stats.timer = conf_int(&val); + if (stats.timer > 0) { + // Check if dumping is already running. + if (stats.active_dumper) { + return; + } + + int ret = pthread_create(&stats.dumper, NULL, dumper, NULL); + if (ret != 0) { + log_error("stats, failed to launch periodic dumping (%s)", + knot_strerror(knot_map_errno_code(ret))); + } else { + stats.active_dumper = true; + } + // Stop current dumping. + } else if (stats.active_dumper) { + pthread_cancel(stats.dumper); + pthread_join(stats.dumper, NULL); + stats.active_dumper = false; + } +} + +void stats_deinit(void) +{ + if (stats.active_dumper) { + pthread_cancel(stats.dumper); + pthread_join(stats.dumper, NULL); + } + + memset(&stats, 0, sizeof(stats)); +} diff --git a/src/knot/common/stats.h b/src/knot/common/stats.h new file mode 100644 index 0000000..bd6df6d --- /dev/null +++ b/src/knot/common/stats.h @@ -0,0 +1,53 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +/*! + * \brief Server statistics general API. + */ + +#pragma once + +#include "knot/server/server.h" + +typedef uint64_t (*stats_val_f)(server_t *server); + +/*! + * \brief Statistics metrics item. + */ +typedef struct { + const char *name; /*!< Metrics name. */ + stats_val_f val; /*!< Metrics value getter. */ +} stats_item_t; + +/*! + * \brief Basic server metrics. + */ +extern const stats_item_t server_stats[]; + +/*! + * \brief Read out value of single counter summed across threads. + */ +uint64_t stats_get_counter(uint64_t **stats_vals, uint32_t offset, unsigned threads); + +/*! + * \brief Reconfigures the statistics facility. + */ +void stats_reconfigure(conf_t *conf, server_t *server); + +/*! + * \brief Deinitializes the statistics facility. + */ +void stats_deinit(void); diff --git a/src/knot/common/systemd.c b/src/knot/common/systemd.c new file mode 100644 index 0000000..13c83e6 --- /dev/null +++ b/src/knot/common/systemd.c @@ -0,0 +1,168 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> + +#include "knot/common/systemd.h" +#include "contrib/strtonum.h" + +#ifdef ENABLE_SYSTEMD +#include <systemd/sd-daemon.h> + +#define ZONE_LOAD_TIMEOUT_DEFAULT 60 + +static int zone_load_timeout_s; + +static int systemd_zone_load_timeout(void) +{ + const char *timeout = getenv("KNOT_ZONE_LOAD_TIMEOUT_SEC"); + + int out; + if (timeout != NULL && timeout[0] != '\0' && + str_to_int(timeout, &out, 0, 24 * 3600) == KNOT_EOK) { + return out; + } else { + return ZONE_LOAD_TIMEOUT_DEFAULT; + } +} +#endif + +#ifdef ENABLE_DBUS +#include <systemd/sd-bus.h> + +static sd_bus *_dbus = NULL; +#endif + +void systemd_zone_load_timeout_notify(void) +{ +#ifdef ENABLE_SYSTEMD + if (zone_load_timeout_s == 0) { + zone_load_timeout_s = systemd_zone_load_timeout(); + } + sd_notifyf(0, "EXTEND_TIMEOUT_USEC=%d000000", zone_load_timeout_s); +#endif +} + +void systemd_tasks_status_notify(int tasks) +{ +#ifdef ENABLE_SYSTEMD + if (tasks > 0) { + sd_notifyf(0, "STATUS=Waiting for %d tasks to finish...", tasks); + } else { + sd_notify(0, "STATUS="); + } +#endif +} + +void systemd_ready_notify(void) +{ +#ifdef ENABLE_SYSTEMD + sd_notify(0, "READY=1\nSTATUS="); +#endif +} + +void systemd_reloading_notify(void) +{ +#ifdef ENABLE_SYSTEMD + sd_notify(0, "RELOADING=1\nSTATUS="); +#endif +} + +void systemd_stopping_notify(void) +{ +#ifdef ENABLE_SYSTEMD + sd_notify(0, "STOPPING=1\nSTATUS="); +#endif +} + +int systemd_dbus_open(void) +{ +#ifdef ENABLE_DBUS + if (_dbus != NULL) { + return KNOT_EOK; + } + + int ret = sd_bus_open_system(&_dbus); + if (ret < 0) { + return ret; + } + + /* Take a well-known service name so that clients can find us. */ + ret = sd_bus_request_name(_dbus, KNOT_DBUS_NAME, 0); + if (ret < 0) { + systemd_dbus_close(); + return ret; + } + + return KNOT_EOK; +#else + return KNOT_ENOTSUP; +#endif +} + +void systemd_dbus_close(void) +{ +#ifdef ENABLE_DBUS + _dbus = sd_bus_unref(_dbus); +#endif +} + +#define emit_event(event, ...) \ + sd_bus_emit_signal(_dbus, KNOT_DBUS_PATH, KNOT_DBUS_NAME".events", \ + event, __VA_ARGS__) + +void systemd_emit_running(bool up) +{ +#ifdef ENABLE_DBUS + emit_event(up ? KNOT_BUS_EVENT_STARTED : KNOT_BUS_EVENT_STOPPED, ""); +#endif +} + +void systemd_emit_zone_updated(const knot_dname_t *zone_name, uint32_t serial) +{ +#ifdef ENABLE_DBUS + knot_dname_txt_storage_t buff; + char *zone_str = knot_dname_to_str(buff, zone_name, sizeof(buff)); + if (zone_str != NULL) { + emit_event(KNOT_BUS_EVENT_ZONE_UPD, "su", zone_str, serial); + } +#endif +} + +void systemd_emit_zone_submission(const knot_dname_t *zone_name, uint16_t keytag, + const char *keyid) +{ +#ifdef ENABLE_DBUS + knot_dname_txt_storage_t buff; + char *zone_str = knot_dname_to_str(buff, zone_name, sizeof(buff)); + if (zone_str != NULL) { + emit_event(KNOT_BUS_EVENT_ZONE_KSK_SUBM, "sqs", zone_str, keytag, keyid); + } +#endif +} + +void systemd_emit_zone_invalid(const knot_dname_t *zone_name) +{ +#ifdef ENABLE_DBUS + knot_dname_txt_storage_t buff; + char *zone_str = knot_dname_to_str(buff, zone_name, sizeof(buff)); + if (zone_str != NULL) { + emit_event(KNOT_BUS_EVENT_ZONE_INVALID, "s", zone_str); + } +#endif +} diff --git a/src/knot/common/systemd.h b/src/knot/common/systemd.h new file mode 100644 index 0000000..1cefd9c --- /dev/null +++ b/src/knot/common/systemd.h @@ -0,0 +1,105 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +/*! + * \brief Systemd API wrappers. + */ + +#pragma once + +#include "libknot/libknot.h" + +#define KNOT_DBUS_NAME "cz.nic.knotd" +#define KNOT_DBUS_PATH "/cz/nic/knotd" + +#define KNOT_BUS_EVENT_STARTED "started" +#define KNOT_BUS_EVENT_STOPPED "stopped" +#define KNOT_BUS_EVENT_ZONE_UPD "zone_updated" +#define KNOT_BUS_EVENT_ZONE_KSK_SUBM "zone_ksk_submission" +#define KNOT_BUS_EVENT_ZONE_INVALID "zone_dnssec_invalid" + +/*! + * \brief Notify systemd about zone loading start. + */ +void systemd_zone_load_timeout_notify(void); + +/*! + * \brief Update systemd service status with information about number + * of scheduled tasks. + * + * \param tasks Number of tasks to be done. + */ +void systemd_tasks_status_notify(int tasks); + +/*! + * \brief Notify systemd about service is ready. + */ +void systemd_ready_notify(void); + +/*! + * \brief Notify systemd about service is reloading. + */ +void systemd_reloading_notify(void); + +/*! + * \brief Notify systemd about service is stopping. + */ +void systemd_stopping_notify(void); + +/*! + * \brief Creates unique D-Bus sender reference (common for whole process). + * + * \retval KNOT_EOK on successful create of reference. + * \retval Negative value on error. + */ +int systemd_dbus_open(void); + +/*! + * \brief Closes D-Bus. + */ +void systemd_dbus_close(void); + +/*! + * \brief Emit event signal for started daemon. + * + * \param up Indication if the server has been started. + */ +void systemd_emit_running(bool up); + +/*! + * \brief Emit event signal for updated zones. + * + * \param zone_name Zone name. + * \param serial Current zone SOA serial. + */ +void systemd_emit_zone_updated(const knot_dname_t *zone_name, uint32_t serial); + +/*! + * \brief Emit event signal for KSK submission. + * + * \param zone_name Zone name. + * \param keytag Keytag of the ready key. + * \param keyid KASP id of the ready key. + */ +void systemd_emit_zone_submission(const knot_dname_t *zone_name, uint16_t keytag, + const char *keyid); + +/*! + * \brief Emit event signal for failed DNSSEC validation. + * + * \param zone_name Zone name. + */ +void systemd_emit_zone_invalid(const knot_dname_t *zone_name); diff --git a/src/knot/common/unreachable.c b/src/knot/common/unreachable.c new file mode 100644 index 0000000..e137f3d --- /dev/null +++ b/src/knot/common/unreachable.c @@ -0,0 +1,148 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <stdlib.h> +#include <time.h> + +#include "unreachable.h" + +knot_unreachables_t *global_unreachables = NULL; + +static uint32_t get_timestamp(void) +{ + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + uint64_t res = (uint64_t)t.tv_sec * 1000; + res += (uint64_t)t.tv_nsec / 1000000; + return res & 0xffffffff; // overflow does not matter since we are working with differences +} + +knot_unreachables_t *knot_unreachables_init(uint32_t ttl_ms) +{ + knot_unreachables_t *res = calloc(1, sizeof(*res)); + if (res != NULL) { + pthread_mutex_init(&res->mutex, NULL); + res->ttl_ms = ttl_ms; + init_list(&res->urs); + } + return res; +} + +uint32_t knot_unreachables_ttl(knot_unreachables_t *urs, uint32_t new_ttl_ms) +{ + if (urs == NULL) { + return 0; + } + + pthread_mutex_lock(&urs->mutex); + + uint32_t prev = urs->ttl_ms; + urs->ttl_ms = new_ttl_ms; + + pthread_mutex_unlock(&urs->mutex); + + return prev; +} + +void knot_unreachables_deinit(knot_unreachables_t **urs) +{ + if (urs != NULL && *urs != NULL) { + knot_unreachable_t *ur, *nxt; + WALK_LIST_DELSAFE(ur, nxt, (*urs)->urs) { + rem_node((node_t *)ur); + free(ur); + } + pthread_mutex_destroy(&(*urs)->mutex); + free(*urs); + *urs = NULL; + } +} + +static bool clear_old(knot_unreachable_t *ur, uint32_t now, uint32_t ttl_ms) +{ + if (ur->time_ms != 0 && now - ur->time_ms > ttl_ms) { + rem_node((node_t *)ur); + free(ur); + return true; + } + return false; +} + +// also clears up (some) expired unreachables +static knot_unreachable_t *get_ur(knot_unreachables_t *urs, + const struct sockaddr_storage *addr, + const struct sockaddr_storage *via) +{ + assert(urs != NULL); + + uint32_t now = get_timestamp(); + knot_unreachable_t *ur, *nxt; + WALK_LIST_DELSAFE(ur, nxt, urs->urs) { + if (clear_old(ur, now, urs->ttl_ms)) { + continue; + } + + if (sockaddr_cmp(&ur->addr, addr, false) == 0 && + sockaddr_cmp(&ur->via, via, true) == 0) { + return ur; + } + } + + return NULL; +} + +bool knot_unreachable_is(knot_unreachables_t *urs, + const struct sockaddr_storage *addr, + const struct sockaddr_storage *via) +{ + if (urs == NULL) { + return false; + } + assert(addr); + assert(via); + + pthread_mutex_lock(&urs->mutex); + + bool res = (get_ur(urs, addr, via) != NULL); + + pthread_mutex_unlock(&urs->mutex); + + return res; +} + +void knot_unreachable_add(knot_unreachables_t *urs, + const struct sockaddr_storage *addr, + const struct sockaddr_storage *via) +{ + if (urs == NULL) { + return; + } + assert(addr); + assert(via); + + pthread_mutex_lock(&urs->mutex); + + knot_unreachable_t *ur = malloc(sizeof(*ur)); + if (ur != NULL) { + memcpy(&ur->addr, addr, sizeof(ur->addr)); + memcpy(&ur->via, via, sizeof(ur->via)); + ur->time_ms = get_timestamp(); + add_head(&urs->urs, (node_t *)ur); + } + + pthread_mutex_unlock(&urs->mutex); +} diff --git a/src/knot/common/unreachable.h b/src/knot/common/unreachable.h new file mode 100644 index 0000000..40094f9 --- /dev/null +++ b/src/knot/common/unreachable.h @@ -0,0 +1,87 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program 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. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pthread.h> +#include <stdbool.h> +#include <stdint.h> + +#include "contrib/sockaddr.h" +#include "contrib/ucw/lists.h" + +typedef struct { + node_t n; + struct sockaddr_storage addr; + struct sockaddr_storage via; + uint32_t time_ms; +} knot_unreachable_t; + +typedef struct { + pthread_mutex_t mutex; + uint32_t ttl_ms; + list_t urs; +} knot_unreachables_t; + +extern knot_unreachables_t *global_unreachables; + +/*! + * \brief Allocate Unreachables structure. + * + * \param ttl TTL for unreachable in milliseconds. + * + * \return Allocated structure, or NULL. + */ +knot_unreachables_t *knot_unreachables_init(uint32_t ttl_ms); + +/*! + * \brief Free Unreachables structure. + */ +void knot_unreachables_deinit(knot_unreachables_t **urs); + +/*! + * \brief Get and/or set the TTL. + * + * \param urs Unreachables structure. + * \param new_ttl_ms New TTL value in milliseconds. + * + * \return Previous value of TTL. + */ +uint32_t knot_unreachables_ttl(knot_unreachables_t *urs, uint32_t new_ttl_ms); + +/*! + * \brief Determine if given address is unreachable. + * + * \param urs Unreachables structure. + * \param addr Address and port in question. + * \param via Local outgoing address. + * + * \return True iff unreachable within TTL. + */ +bool knot_unreachable_is(knot_unreachables_t *urs, + const struct sockaddr_storage *addr, + const struct sockaddr_storage *via); + +/*! + * \brief Add an unreachable into Unreachables structure. + * + * \param urs Unreachables structure. + * \param addr Address and port being unreachable. + * \param via Local outgoing address. + */ +void knot_unreachable_add(knot_unreachables_t *urs, + const struct sockaddr_storage *addr, + const struct sockaddr_storage *via); |