summaryrefslogtreecommitdiffstats
path: root/src/modules/rlm_unbound/rlm_unbound.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules/rlm_unbound/rlm_unbound.c')
-rw-r--r--src/modules/rlm_unbound/rlm_unbound.c758
1 files changed, 758 insertions, 0 deletions
diff --git a/src/modules/rlm_unbound/rlm_unbound.c b/src/modules/rlm_unbound/rlm_unbound.c
new file mode 100644
index 0000000..dddc3bf
--- /dev/null
+++ b/src/modules/rlm_unbound/rlm_unbound.c
@@ -0,0 +1,758 @@
+/*
+ * This program is 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 2 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ * @file rlm_unbound.c
+ * @brief DNS services via libunbound.
+ *
+ * @copyright 2013 The FreeRADIUS server project
+ * @copyright 2013 Brian S. Julin <bjulin@clarku.edu>
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/log.h>
+#include <fcntl.h>
+
+#ifdef HAVE_WDOCUMENTATION
+DIAG_OFF(documentation)
+#endif
+#include <unbound.h>
+#ifdef HAVE_WDOCUMENTATION
+DIAG_ON(documentation)
+#endif
+
+typedef struct rlm_unbound_t {
+ struct ub_ctx *ub; /* This must come first. Do not move */
+ fr_event_list_t *el; /* This must come second. Do not move. */
+
+ char const *name;
+ char const *xlat_a_name;
+ char const *xlat_aaaa_name;
+ char const *xlat_ptr_name;
+
+ uint32_t timeout;
+
+ char const *filename;
+ char const *resolvconf;
+ char const *hosts;
+
+ int log_fd;
+ FILE *log_stream;
+
+ int log_pipe[2];
+ FILE *log_pipe_stream[2];
+ bool log_pipe_in_use;
+} rlm_unbound_t;
+
+/*
+ * A mapping of configuration file names to internal variables.
+ */
+static const CONF_PARSER module_config[] = {
+ { "filename", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT | PW_TYPE_REQUIRED, rlm_unbound_t, filename), "${modconfdir}/unbound/default.conf" },
+ { "resolvconf", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_unbound_t, resolvconf), NULL },
+ { "hosts", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_unbound_t, hosts), NULL },
+ { "timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_unbound_t, timeout), "3000" },
+ CONF_PARSER_TERMINATOR
+};
+
+/*
+ * Callback sent to libunbound for xlat functions. Simply links the
+ * new ub_result via a pointer that has been allocated from the heap.
+ * This pointer has been pre-initialized to a magic value.
+ */
+static void link_ubres(void* my_arg, int err, struct ub_result* result)
+{
+ struct ub_result **ubres = (struct ub_result **)my_arg;
+
+ /*
+ * Note that while result will be NULL on error, we are explicit
+ * here because that is actually a behavior that is suboptimal
+ * and only documented in the examples. It could change.
+ */
+ if (err) {
+ ERROR("rlm_unbound: %s", ub_strerror(err));
+ *ubres = NULL;
+ } else {
+ *ubres = result;
+ }
+}
+
+/*
+ * Convert labels as found in a DNS result to a NULL terminated string.
+ *
+ * Result is written to memory pointed to by "out" but no result will
+ * be written unless it and its terminating NULL character fit in "left"
+ * bytes. Returns the number of bytes written excluding the terminating
+ * NULL, or -1 if nothing was written because it would not fit or due
+ * to a violation in the labels format.
+ */
+static int rrlabels_tostr(char *out, char *rr, size_t left)
+{
+ int offset = 0;
+
+ /*
+ * TODO: verify that unbound results (will) always use this label
+ * format, and review the specs on this label format for nuances.
+ */
+
+ if (!left) {
+ return -1;
+ }
+ if (left > 253) {
+ left = 253; /* DNS length limit */
+ }
+ /* As a whole this should be "NULL terminated" by the 0-length label */
+ if (strnlen(rr, left) > left - 1) {
+ return -1;
+ }
+
+ /* It will fit, but does it it look well formed? */
+ while (1) {
+ size_t count;
+
+ count = *((unsigned char *)(rr + offset));
+ if (!count) break;
+
+ offset++;
+ if (count > 63 || strlen(rr + offset) < count) {
+ return -1;
+ }
+ offset += count;
+ }
+
+ /* Data is valid and fits. Copy it. */
+ offset = 0;
+ while (1) {
+ int count;
+
+ count = *((unsigned char *)(rr));
+ if (!count) break;
+
+ if (offset) {
+ *(out + offset) = '.';
+ offset++;
+ }
+
+ rr++;
+ memcpy(out + offset, rr, count);
+ rr += count;
+ offset += count;
+ }
+
+ *(out + offset) = '\0';
+ return offset;
+}
+
+static int ub_common_wait(rlm_unbound_t *inst, REQUEST *request, char const *tag, struct ub_result **ub, int async_id)
+{
+ useconds_t iv, waited;
+
+ iv = inst->timeout > 64 ? 64000 : inst->timeout * 1000;
+ ub_process(inst->ub);
+
+ for (waited = 0; (void*)*ub == (void *)inst; waited += iv, iv *= 2) {
+
+ if (waited + iv > (useconds_t)inst->timeout * 1000) {
+ usleep(inst->timeout * 1000 - waited);
+ ub_process(inst->ub);
+ break;
+ }
+
+ usleep(iv);
+
+ /* Check if already handled by event loop */
+ if ((void *)*ub != (void *)inst) {
+ break;
+ }
+
+ /* In case we are running single threaded */
+ ub_process(inst->ub);
+ }
+
+ if ((void *)*ub == (void *)inst) {
+ int res;
+
+ RDEBUG("rlm_unbound (%s): DNS took too long", tag);
+
+ res = ub_cancel(inst->ub, async_id);
+ if (res) {
+ REDEBUG("rlm_unbound (%s): ub_cancel: %s",
+ tag, ub_strerror(res));
+ }
+ return -1;
+ }
+
+ return 0;
+}
+
+static int ub_common_fail(REQUEST *request, char const *tag, struct ub_result *ub)
+{
+ if (ub->bogus) {
+ RWDEBUG("rlm_unbound (%s): Bogus DNS response", tag);
+ return -1;
+ }
+
+ if (ub->nxdomain) {
+ RDEBUG("rlm_unbound (%s): NXDOMAIN", tag);
+ return -1;
+ }
+
+ if (!ub->havedata) {
+ RDEBUG("rlm_unbound (%s): empty result", tag);
+ return -1;
+ }
+
+ return 0;
+}
+
+static ssize_t xlat_a(void *instance, REQUEST *request, char const *fmt, char *out, size_t freespace)
+{
+ rlm_unbound_t *inst = instance;
+ struct ub_result **ubres;
+ int async_id;
+
+ /* This has to be on the heap, because threads. */
+ ubres = talloc(inst, struct ub_result *);
+
+ /* Used and thus impossible value from heap to designate incomplete */
+ *ubres = (void *)instance;
+
+ ub_resolve_async(inst->ub, fmt, 1, 1, ubres, link_ubres, &async_id);
+
+ if (ub_common_wait(inst, request, inst->xlat_a_name, ubres, async_id)) {
+ goto error0;
+ }
+
+ if (*ubres) {
+ if (ub_common_fail(request, inst->xlat_a_name, *ubres)) {
+ goto error1;
+ }
+
+ if (!inet_ntop(AF_INET, (*ubres)->data[0], out, freespace)) {
+ goto error1;
+ };
+
+ ub_resolve_free(*ubres);
+ talloc_free(ubres);
+ return strlen(out);
+ }
+
+ RWDEBUG("rlm_unbound (%s): no result", inst->xlat_a_name);
+
+ error1:
+ ub_resolve_free(*ubres); /* Handles NULL gracefully */
+
+ error0:
+ talloc_free(ubres);
+ return -1;
+}
+
+static ssize_t xlat_aaaa(void *instance, REQUEST *request, char const *fmt, char *out, size_t freespace)
+{
+ rlm_unbound_t *inst = instance;
+ struct ub_result **ubres;
+ int async_id;
+
+ /* This has to be on the heap, because threads. */
+ ubres = talloc(inst, struct ub_result *);
+
+ /* Used and thus impossible value from heap to designate incomplete */
+ *ubres = (void *)instance;
+
+ ub_resolve_async(inst->ub, fmt, 28, 1, ubres, link_ubres, &async_id);
+
+ if (ub_common_wait(inst, request, inst->xlat_aaaa_name, ubres, async_id)) {
+ goto error0;
+ }
+
+ if (*ubres) {
+ if (ub_common_fail(request, inst->xlat_aaaa_name, *ubres)) {
+ goto error1;
+ }
+ if (!inet_ntop(AF_INET6, (*ubres)->data[0], out, freespace)) {
+ goto error1;
+ };
+ ub_resolve_free(*ubres);
+ talloc_free(ubres);
+ return strlen(out);
+ }
+
+ RWDEBUG("rlm_unbound (%s): no result", inst->xlat_aaaa_name);
+
+error1:
+ ub_resolve_free(*ubres); /* Handles NULL gracefully */
+
+error0:
+ talloc_free(ubres);
+ return -1;
+}
+
+static ssize_t xlat_ptr(void *instance, REQUEST *request, char const *fmt, char *out, size_t freespace)
+{
+ rlm_unbound_t *inst = instance;
+ struct ub_result **ubres;
+ int async_id;
+
+ /* This has to be on the heap, because threads. */
+ ubres = talloc(inst, struct ub_result *);
+
+ /* Used and thus impossible value from heap to designate incomplete */
+ *ubres = (void *)instance;
+
+ ub_resolve_async(inst->ub, fmt, 12, 1, ubres, link_ubres, &async_id);
+
+ if (ub_common_wait(inst, request, inst->xlat_ptr_name,
+ ubres, async_id)) {
+ goto error0;
+ }
+
+ if (*ubres) {
+ if (ub_common_fail(request, inst->xlat_ptr_name, *ubres)) {
+ goto error1;
+ }
+ if (rrlabels_tostr(out, (*ubres)->data[0], freespace) < 0) {
+ goto error1;
+ }
+ ub_resolve_free(*ubres);
+ talloc_free(ubres);
+ return strlen(out);
+ }
+
+ RWDEBUG("rlm_unbound (%s): no result", inst->xlat_ptr_name);
+
+error1:
+ ub_resolve_free(*ubres); /* Handles NULL gracefully */
+
+error0:
+ talloc_free(ubres);
+ return -1;
+}
+
+/*
+ * Even when run in asyncronous mode, callbacks sent to libunbound still
+ * must be run in an application-side thread (via ub_process.) This is
+ * probably to keep the API usage consistent across threaded and forked
+ * embedded client modes. This callback function lets an event loop call
+ * ub_process when the instance's file descriptor becomes ready.
+ */
+static void ub_fd_handler(UNUSED fr_event_list_t *el, UNUSED int sock, void *ctx)
+{
+ rlm_unbound_t *inst = ctx;
+ int err;
+
+ err = ub_process(inst->ub);
+ if (err) {
+ ERROR("rlm_unbound (%s) async ub_process: %s",
+ inst->name, ub_strerror(err));
+ }
+}
+
+#ifndef HAVE_PTHREAD_H
+
+/* If we have to use a pipe to redirect logging, this does the work. */
+static void log_spew(UNUSED fr_event_list_t *el, UNUSED int sock, void *ctx)
+{
+ rlm_unbound_t *inst = ctx;
+ char line[1024];
+
+ /*
+ * This works for pipes from processes, but not from threads
+ * right now. The latter is hinky and will require some fancy
+ * blocking/nonblocking trickery which is not figured out yet,
+ * since selecting on a pipe from a thread in the same process
+ * seems to behave differently. It will likely preclude the use
+ * of fgets and streams. Left for now since some unbound logging
+ * infrastructure is still global across multiple contexts. Maybe
+ * we can get unbound folks to provide a ub_ctx_debugout_async that
+ * takes a function hook instead to just bypass the piping when
+ * used in threaded mode.
+ */
+ while (fgets(line, 1024, inst->log_pipe_stream[0])) {
+ DEBUG("rlm_unbound (%s): %s", inst->name, line);
+ }
+}
+
+#endif
+
+static int mod_bootstrap(CONF_SECTION *conf, void *instance)
+{
+ rlm_unbound_t *inst = instance;
+
+ inst->name = cf_section_name2(conf);
+ if (!inst->name) {
+ inst->name = cf_section_name1(conf);
+ }
+
+ if (inst->timeout > 10000) {
+ cf_log_err_cs(conf, "timeout must be 0 to 10000");
+ return -1;
+ }
+
+ MEM(inst->xlat_a_name = talloc_typed_asprintf(inst, "%s-a", inst->name));
+ MEM(inst->xlat_aaaa_name = talloc_typed_asprintf(inst, "%s-aaaa", inst->name));
+ MEM(inst->xlat_ptr_name = talloc_typed_asprintf(inst, "%s-ptr", inst->name));
+
+ if (xlat_register(inst->xlat_a_name, xlat_a, NULL, inst) ||
+ xlat_register(inst->xlat_aaaa_name, xlat_aaaa, NULL, inst) ||
+ xlat_register(inst->xlat_ptr_name, xlat_ptr, NULL, inst)) {
+ cf_log_err_cs(conf, "Failed registering xlats");
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mod_instantiate(CONF_SECTION *conf, void *instance)
+{
+ rlm_unbound_t *inst = instance;
+ int res;
+ char *optval;
+
+ log_dst_t log_dst;
+ int log_level;
+ int log_fd = -1;
+
+ inst->el = radius_event_list_corral(EVENT_CORRAL_AUX);
+ inst->log_pipe_stream[0] = NULL;
+ inst->log_pipe_stream[1] = NULL;
+ inst->log_fd = -1;
+ inst->log_pipe_in_use = false;
+
+ inst->ub = ub_ctx_create();
+ if (!inst->ub) {
+ cf_log_err_cs(conf, "ub_ctx_create failed");
+ return -1;
+ }
+
+#ifdef HAVE_PTHREAD_H
+ /*
+ * Note unbound threads WILL happen with -s option, if it matters.
+ * We cannot tell from here whether that option is in effect.
+ */
+ res = ub_ctx_async(inst->ub, 1);
+#else
+ /*
+ * Uses forked subprocesses instead.
+ */
+ res = ub_ctx_async(inst->ub, 0);
+#endif
+
+ if (res) goto error;
+
+ /* Glean some default settings to match the main server. */
+ /* TODO: debug_level can be changed at runtime. */
+ /* TODO: log until fork when stdout or stderr and !rad_debug_lvl. */
+ log_level = 0;
+
+ if (rad_debug_lvl > 0) {
+ log_level = rad_debug_lvl;
+
+ } else if (main_config.debug_level > 0) {
+ log_level = main_config.debug_level;
+ }
+
+ switch (log_level) {
+ /* TODO: This will need some tweaking */
+ case 0:
+ case 1:
+ break;
+
+ case 2:
+ log_level = 1;
+ break;
+
+ case 3:
+ case 4:
+ log_level = 2; /* mid-to-heavy levels of output */
+ break;
+
+ case 5:
+ case 6:
+ case 7:
+ case 8:
+ log_level = 3; /* Pretty crazy amounts of output */
+ break;
+
+ default:
+ log_level = 4; /* Insane amounts of output including crypts */
+ break;
+ }
+
+ res = ub_ctx_debuglevel(inst->ub, log_level);
+ if (res) goto error;
+
+ switch (default_log.dst) {
+ case L_DST_STDOUT:
+ if (!rad_debug_lvl) {
+ log_dst = L_DST_NULL;
+ break;
+ }
+ log_dst = L_DST_STDOUT;
+ log_fd = dup(STDOUT_FILENO);
+ break;
+
+ case L_DST_STDERR:
+ if (!rad_debug_lvl) {
+ log_dst = L_DST_NULL;
+ break;
+ }
+ log_dst = L_DST_STDOUT;
+ log_fd = dup(STDERR_FILENO);
+ break;
+
+ case L_DST_FILES:
+ if (main_config.log_file) {
+ res = ub_ctx_set_option(inst->ub, "logfile:", main_config.log_file);
+ if (res) {
+ goto error;
+ }
+ log_dst = L_DST_FILES;
+ break;
+ }
+ /* FALL-THROUGH */
+
+ case L_DST_NULL:
+ log_dst = L_DST_NULL;
+ break;
+
+ default:
+ log_dst = L_DST_SYSLOG;
+ break;
+ }
+
+ /* Now load the config file, which can override gleaned settings. */
+ {
+ res = ub_ctx_config(inst->ub, inst->filename);
+ if (res) goto error;
+ }
+
+ /*
+ * Check if the config file tried to use syslog. Unbound
+ * does not share syslog gracefully.
+ */
+ res = ub_ctx_get_option(inst->ub, "use-syslog", &optval);
+ if (res || !optval) goto error;
+
+ if (!strcmp(optval, "yes")) {
+ free(optval);
+
+ WARN("rlm_unbound (%s): Overriding syslog settings.", inst->name);
+ res = ub_ctx_set_option(inst->ub, "use-syslog:", "no");
+ if (res) goto error;
+
+ if (log_dst == L_DST_FILES) {
+ /* Reinstate the log file name JIC */
+ res = ub_ctx_set_option(inst->ub, "logfile:", main_config.log_file);
+ if (res) goto error;
+ }
+
+ } else {
+ if (optval) free(optval);
+
+ res = ub_ctx_get_option(inst->ub, "logfile", &optval);
+ if (res) goto error;
+
+ if (optval && strlen(optval)) {
+ log_dst = L_DST_FILES;
+
+ } else if (!rad_debug_lvl) {
+ log_dst = L_DST_NULL;
+ }
+
+ if (optval) free(optval);
+ }
+
+ switch (log_dst) {
+ case L_DST_STDOUT:
+ /*
+ * We have an fd to log to. And we've already attempted to
+ * dup it so libunbound doesn't close it on us.
+ */
+ if (log_fd == -1) {
+ cf_log_err_cs(conf, "Could not dup fd");
+ goto error_nores;
+ }
+
+ inst->log_stream = fdopen(log_fd, "w");
+ if (!inst->log_stream) {
+ cf_log_err_cs(conf, "error setting up log stream");
+ goto error_nores;
+ }
+
+ res = ub_ctx_debugout(inst->ub, inst->log_stream);
+ if (res) goto error;
+ break;
+
+ case L_DST_FILES:
+ /* We gave libunbound a filename. It is on its own now. */
+ break;
+
+ case L_DST_NULL:
+ /* We tell libunbound not to log at all. */
+ res = ub_ctx_debugout(inst->ub, NULL);
+ if (res) goto error;
+ break;
+
+ case L_DST_SYSLOG:
+#ifdef HAVE_PTHREAD_H
+ /*
+ * Currently this wreaks havoc when running threaded, so just
+ * turn logging off until that gets figured out.
+ */
+ res = ub_ctx_debugout(inst->ub, NULL);
+ if (res) goto error;
+ break;
+#else
+ /*
+ * We need to create a pipe, because libunbound does not
+ * share syslog nicely. Or the core added some new logsink.
+ */
+ if (pipe(inst->log_pipe)) {
+ error_pipe:
+ cf_log_err_cs(conf, "Error setting up log pipes");
+ goto error_nores;
+ }
+
+ if ((fcntl(inst->log_pipe[0], F_SETFL, O_NONBLOCK) < 0) ||
+ (fcntl(inst->log_pipe[0], F_SETFD, FD_CLOEXEC) < 0)) {
+ goto error_pipe;
+ }
+
+ /* Opaque to us when this can be closed, so we do not. */
+ if (fcntl(inst->log_pipe[1], F_SETFL, O_NONBLOCK) < 0) {
+ goto error_pipe;
+ }
+
+ inst->log_pipe_stream[0] = fdopen(inst->log_pipe[0], "r");
+ inst->log_pipe_stream[1] = fdopen(inst->log_pipe[1], "w");
+
+ if (!inst->log_pipe_stream[0] || !inst->log_pipe_stream[1]) {
+ if (!inst->log_pipe_stream[1]) {
+ close(inst->log_pipe[1]);
+ }
+
+ if (!inst->log_pipe_stream[0]) {
+ close(inst->log_pipe[0]);
+ }
+ cf_log_err_cs(conf, "Error setting up log stream");
+ goto error_nores;
+ }
+
+ res = ub_ctx_debugout(inst->ub, inst->log_pipe_stream[1]);
+ if (res) goto error;
+
+ if (!fr_event_fd_insert(inst->el, 0, inst->log_pipe[0], log_spew, inst)) {
+ cf_log_err_cs(conf, "could not insert log fd");
+ goto error_nores;
+ }
+
+ inst->log_pipe_in_use = true;
+#endif
+ default:
+ break;
+ }
+
+ /*
+ * Load resolv.conf if specified
+ */
+ if (inst->resolvconf) ub_ctx_resolvconf(inst->ub, inst->resolvconf);
+
+ /*
+ * Load hosts file if specified
+ */
+ if (inst->hosts) ub_ctx_hosts(inst->ub, inst->hosts);
+
+ /*
+ * Now we need to finalize the context.
+ *
+ * There's no clean API to just finalize the context made public
+ * in libunbound. But we can trick it by trying to delete data
+ * which as it happens fails quickly and quietly even though the
+ * data did not exist.
+ */
+ ub_ctx_data_remove(inst->ub, "notar33lsite.foo123.nottld A 127.0.0.1");
+
+ inst->log_fd = ub_fd(inst->ub);
+ if (inst->log_fd >= 0) {
+ if (!fr_event_fd_insert(inst->el, 0, inst->log_fd, ub_fd_handler, inst)) {
+ cf_log_err_cs(conf, "could not insert async fd");
+ inst->log_fd = -1;
+ goto error_nores;
+ }
+
+ }
+
+ return 0;
+
+ error:
+ cf_log_err_cs(conf, "%s", ub_strerror(res));
+
+ error_nores:
+ if (log_fd > -1) close(log_fd);
+
+ return -1;
+}
+
+static int mod_detach(UNUSED void *instance)
+{
+ rlm_unbound_t *inst = instance;
+
+ if (inst->log_fd >= 0) {
+ fr_event_fd_delete(inst->el, 0, inst->log_fd);
+ if (inst->ub) {
+ ub_process(inst->ub);
+ /* This can hang/leave zombies currently
+ * see upstream bug #519
+ * ...so expect valgrind to complain with -m
+ */
+#if 0
+ ub_ctx_delete(inst->ub);
+#endif
+ }
+ }
+
+ if (inst->log_pipe_stream[1]) {
+ fclose(inst->log_pipe_stream[1]);
+ }
+
+ if (inst->log_pipe_stream[0]) {
+ if (inst->log_pipe_in_use) {
+ fr_event_fd_delete(inst->el, 0, inst->log_pipe[0]);
+ }
+ fclose(inst->log_pipe_stream[0]);
+ }
+
+ if (inst->log_stream) {
+ fclose(inst->log_stream);
+ }
+
+ return 0;
+}
+
+extern module_t rlm_unbound;
+module_t rlm_unbound = {
+ .magic = RLM_MODULE_INIT,
+ .name = "unbound",
+ .type = RLM_TYPE_THREAD_SAFE,
+ .inst_size = sizeof(rlm_unbound_t),
+ .config = module_config,
+ .bootstrap = mod_bootstrap,
+ .instantiate = mod_instantiate,
+ .detach = mod_detach
+};