freeipa/daemons/ipa-otpd/main.c
Nathaniel McCallum 203754691c Add the krb5/FreeIPA RADIUS companion daemon
This daemon listens for RADIUS packets on a well known
UNIX domain socket. When a packet is received, it queries
LDAP to see if the user is configured for RADIUS authentication.
If so, then the packet is forwarded to the 3rd party RADIUS server.
Otherwise, a bind is attempted against the LDAP server.

https://fedorahosted.org/freeipa/ticket/3366
http://freeipa.org/page/V3/OTP
2013-05-17 09:30:51 +02:00

341 lines
9.9 KiB
C

/*
* FreeIPA 2FA companion daemon
*
* Authors: Nathaniel McCallum <npmccallum@redhat.com>
*
* Copyright (C) 2013 Nathaniel McCallum, Red Hat
* see file 'COPYING' for use and warranty information
*
* 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 <http://www.gnu.org/licenses/>.
*/
/*
* This file initializes a systemd socket-activated daemon which receives
* RADIUS packets on STDIN and either proxies them to a third party RADIUS
* server or performs authentication directly by binding to the LDAP server.
* The choice between bind or proxy is made by evaluating LDAP configuration
* for the given user.
*/
#include "internal.h"
#include <signal.h>
#include <stdbool.h>
/* Our global state. */
struct otpd_context ctx;
/* Implementation function for logging a request's state. See internal.h. */
void otpd_log_req_(const char * const file, int line, krad_packet *req,
const char * const tmpl, ...)
{
const krb5_data *data;
va_list ap;
#ifdef DEBUG
if (file != NULL)
fprintf(stderr, "%8s:%03d: ", file, line);
#else
(void)file;
(void)line;
#endif
data = krad_packet_get_attr(req, krad_attr_name2num("User-Name"), 0);
if (data == NULL)
fprintf(stderr, "<unknown>: ");
else
fprintf(stderr, "%*s: ", data->length, data->data);
va_start(ap, tmpl);
vfprintf(stderr, tmpl, ap);
va_end(ap);
fprintf(stderr, "\n");
}
/* Implementation function for logging a generic error. See internal.h. */
void otpd_log_err_(const char * const file, int line, krb5_error_code code,
const char * const tmpl, ...)
{
const char *msg;
va_list ap;
if (file != NULL)
fprintf(stderr, "%10s:%03d: ", file, line);
if (code != 0) {
msg = krb5_get_error_message(ctx.kctx, code);
fprintf(stderr, "%s: ", msg);
krb5_free_error_message(ctx.kctx, msg);
}
va_start(ap, tmpl);
vfprintf(stderr, tmpl, ap);
va_end(ap);
fprintf(stderr, "\n");
}
static void on_ldap_free(verto_ctx *vctx, verto_ev *ev)
{
(void)vctx; /* Unused */
ldap_unbind_ext_s(verto_get_private(ev), NULL, NULL);
}
static void on_signal(verto_ctx *vctx, verto_ev *ev)
{
(void)ev; /* Unused */
fprintf(stderr, "Signaled, exiting...\n");
verto_break(vctx);
}
static char *find_base(LDAP *ldp)
{
LDAPMessage *results = NULL, *entry;
struct berval **vals = NULL;
struct timeval timeout;
int i, len;
char *base = NULL, *attrs[] = {
"namingContexts",
"defaultNamingContext",
NULL
};
timeout.tv_sec = -1;
i = ldap_search_ext_s(ldp, "", LDAP_SCOPE_BASE, NULL, attrs,
0, NULL, NULL, &timeout, 1, &results);
if (i != LDAP_SUCCESS) {
otpd_log_err(0, "Unable to search for query base: %s",
ldap_err2string(i));
goto egress;
}
entry = ldap_first_entry(ldp, results);
if (entry == NULL) {
otpd_log_err(0, "No entries found");
goto egress;
}
vals = ldap_get_values_len(ldp, entry, "defaultNamingContext");
if (vals == NULL) {
vals = ldap_get_values_len(ldp, entry, "namingContexts");
if (vals == NULL) {
otpd_log_err(0, "No namingContexts found");
goto egress;
}
}
len = ldap_count_values_len(vals);
if (len == 1)
base = strndup(vals[0]->bv_val, vals[0]->bv_len);
else
otpd_log_err(0, "Too many namingContexts found");
/* TODO: search multiple namingContexts to find the base? */
egress:
ldap_value_free_len(vals);
ldap_msgfree(results);
return base;
}
/* Set up an LDAP connection as a verto event. */
static krb5_error_code setup_ldap(const char *uri, krb5_boolean bind,
verto_callback *io, verto_ev **ev,
char **base)
{
struct timeval timeout;
int err, ver, fd;
char *basetmp;
LDAP *ldp;
err = ldap_initialize(&ldp, uri);
if (err != LDAP_SUCCESS)
return errno;
ver = LDAP_VERSION3;
ldap_set_option(ldp, LDAP_OPT_PROTOCOL_VERSION, &ver);
if (bind) {
err = ldap_sasl_bind_s(ldp, NULL, "EXTERNAL", NULL, NULL, NULL, NULL);
if (err != LDAP_SUCCESS)
return errno;
}
/* Always find the base since this forces open the socket. */
basetmp = find_base(ldp);
if (basetmp == NULL)
return ENOTCONN;
if (base != NULL)
*base = basetmp;
else
free(basetmp);
/* Set default timeout to just return immediately for async requests. */
memset(&timeout, 0, sizeof(timeout));
err = ldap_set_option(ldp, LDAP_OPT_TIMEOUT, &timeout);
if (err != LDAP_OPT_SUCCESS) {
ldap_unbind_ext_s(ldp, NULL, NULL);
return ENOMEM; /* What error code do I use? */
}
/* Get the file descriptor. */
if (ldap_get_option(ldp, LDAP_OPT_DESC, &fd) != LDAP_OPT_SUCCESS) {
ldap_unbind_ext_s(ldp, NULL, NULL);
return EINVAL;
}
*ev = verto_add_io(ctx.vctx, VERTO_EV_FLAG_PERSIST |
VERTO_EV_FLAG_IO_ERROR |
VERTO_EV_FLAG_IO_READ,
io, fd);
if (*ev == NULL) {
ldap_unbind_ext_s(ldp, NULL, NULL);
return ENOMEM; /* What error code do I use? */
}
verto_set_private(*ev, ldp, on_ldap_free);
return 0;
}
int main(int argc, char **argv)
{
char hostname[HOST_NAME_MAX + 1];
krb5_error_code retval;
krb5_data hndata;
verto_ev *sig;
if (argc != 2) {
fprintf(stderr, "Usage: %s <ldap_uri>\n", argv[0]);
return 1;
} else {
fprintf(stderr, "LDAP: %s\n", argv[1]);
}
memset(&ctx, 0, sizeof(ctx));
ctx.exitstatus = 1;
if (gethostname(hostname, sizeof(hostname)) < 0) {
otpd_log_err(errno, "Unable to get hostname");
goto error;
}
retval = krb5_init_context(&ctx.kctx);
if (retval != 0) {
otpd_log_err(retval, "Unable to initialize context");
goto error;
}
ctx.vctx = verto_new(NULL, VERTO_EV_TYPE_IO | VERTO_EV_TYPE_SIGNAL);
if (ctx.vctx == NULL) {
otpd_log_err(ENOMEM, "Unable to initialize event loop");
goto error;
}
/* Build attrset. */
retval = krad_attrset_new(ctx.kctx, &ctx.attrs);
if (retval != 0) {
otpd_log_err(retval, "Unable to initialize attrset");
goto error;
}
/* Set NAS-Identifier. */
hndata.data = hostname;
hndata.length = strlen(hndata.data);
retval = krad_attrset_add(ctx.attrs, krad_attr_name2num("NAS-Identifier"),
&hndata);
if (retval != 0) {
otpd_log_err(retval, "Unable to set NAS-Identifier");
goto error;
}
/* Set Service-Type. */
retval = krad_attrset_add_number(ctx.attrs,
krad_attr_name2num("Service-Type"),
KRAD_SERVICE_TYPE_AUTHENTICATE_ONLY);
if (retval != 0) {
otpd_log_err(retval, "Unable to set Service-Type");
goto error;
}
/* Radius Client */
retval = krad_client_new(ctx.kctx, ctx.vctx, &ctx.client);
if (retval != 0) {
otpd_log_err(retval, "Unable to initialize radius client");
goto error;
}
/* Signals */
sig = verto_add_signal(ctx.vctx, VERTO_EV_FLAG_NONE, on_signal, SIGTERM);
if (sig == NULL) {
otpd_log_err(ENOMEM, "Unable to initialize signal event");
goto error;
}
sig = verto_add_signal(ctx.vctx, VERTO_EV_FLAG_NONE, on_signal, SIGINT);
if (sig == NULL) {
otpd_log_err(ENOMEM, "Unable to initialize signal event");
goto error;
}
/* Standard IO */
ctx.stdio.reader = verto_add_io(ctx.vctx, VERTO_EV_FLAG_PERSIST |
VERTO_EV_FLAG_IO_ERROR |
VERTO_EV_FLAG_IO_READ,
otpd_on_stdin_readable, STDIN_FILENO);
if (ctx.stdio.reader == NULL) {
otpd_log_err(ENOMEM, "Unable to initialize reader event");
goto error;
}
ctx.stdio.writer = verto_add_io(ctx.vctx, VERTO_EV_FLAG_PERSIST |
VERTO_EV_FLAG_IO_ERROR |
VERTO_EV_FLAG_IO_READ,
otpd_on_stdout_writable, STDOUT_FILENO);
if (ctx.stdio.writer == NULL) {
otpd_log_err(ENOMEM, "Unable to initialize writer event");
goto error;
}
/* LDAP (Query) */
retval = setup_ldap(argv[1], TRUE, otpd_on_query_io,
&ctx.query.io, &ctx.query.base);
if (retval != 0) {
otpd_log_err(retval, "Unable to initialize LDAP (Query)");
goto error;
}
/* LDAP (Bind) */
retval = setup_ldap(argv[1], FALSE, otpd_on_bind_io,
&ctx.bind.io, NULL);
if (retval != 0) {
otpd_log_err(retval, "Unable to initialize LDAP (Bind)");
goto error;
}
ctx.exitstatus = 0;
verto_run(ctx.vctx);
error:
krad_client_free(ctx.client);
otpd_queue_free_items(&ctx.stdio.responses);
otpd_queue_free_items(&ctx.query.requests);
otpd_queue_free_items(&ctx.query.responses);
otpd_queue_free_items(&ctx.bind.requests);
otpd_queue_free_items(&ctx.bind.responses);
free(ctx.query.base);
verto_free(ctx.vctx);
krb5_free_context(ctx.kctx);
return ctx.exitstatus;
}