summaryrefslogtreecommitdiffstats
path: root/src/main/state.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/main/state.c724
1 files changed, 724 insertions, 0 deletions
diff --git a/src/main/state.c b/src/main/state.c
new file mode 100644
index 0000000..ab7a180
--- /dev/null
+++ b/src/main/state.c
@@ -0,0 +1,724 @@
+/*
+ * 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 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$
+ *
+ * @brief Multi-packet state handling
+ * @file main/state.c
+ *
+ * @ingroup AVP
+ *
+ * @copyright 2014 The FreeRADIUS server project
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/state.h>
+#include <freeradius-devel/md5.h>
+#include <freeradius-devel/rad_assert.h>
+#include <freeradius-devel/process.h>
+
+typedef struct state_entry_t {
+ uint8_t state[MD5_DIGEST_LENGTH];
+
+ time_t cleanup;
+ struct state_entry_t *prev;
+ struct state_entry_t *next;
+
+ int tries;
+ bool ours;
+
+ TALLOC_CTX *ctx;
+ VALUE_PAIR *vps;
+
+ char *server;
+ unsigned int request_number;
+ RADCLIENT *request_client;
+ main_config_t *request_root;
+
+ void *opaque;
+ void (*free_opaque)(void *opaque);
+} state_entry_t;
+
+struct fr_state_t {
+ rbtree_t *tree;
+
+ state_entry_t *head, *tail;
+
+#ifdef HAVE_PTHREAD_H
+ pthread_mutex_t mutex;
+#endif
+};
+
+static fr_state_t global_state;
+
+#define STATE_FREE(_x) if (_x != &global_state) talloc_free(_x)
+
+#ifdef HAVE_PTHREAD_H
+
+#define PTHREAD_MUTEX_LOCK pthread_mutex_lock
+#define PTHREAD_MUTEX_UNLOCK pthread_mutex_unlock
+
+#else
+/*
+ * This is easier than ifdef's throughout the code.
+ */
+#define PTHREAD_MUTEX_LOCK(_x)
+#define PTHREAD_MUTEX_UNLOCK(_x)
+
+#endif
+
+/*
+ * rbtree callback.
+ */
+static int state_entry_cmp(void const *one, void const *two)
+{
+ state_entry_t const *a = one;
+ state_entry_t const *b = two;
+
+ return memcmp(a->state, b->state, sizeof(a->state));
+}
+
+static bool state_entry_link(fr_state_t *state, state_entry_t *entry)
+{
+ if (!rbtree_insert(state->tree, entry)) {
+ return false;
+ }
+
+ /*
+ * Link it to the end of the list, which is implicitely
+ * ordered by cleanup time.
+ */
+ if (!state->head) {
+ entry->prev = entry->next = NULL;
+ state->head = state->tail = entry;
+ } else {
+ rad_assert(state->tail != NULL);
+
+ entry->prev = state->tail;
+ state->tail->next = entry;
+
+ entry->next = NULL;
+ state->tail = entry;
+ }
+
+ return true;
+}
+
+static void state_entry_unlink(fr_state_t *state, state_entry_t *entry)
+{
+ state_entry_t *prev, *next;
+
+ prev = entry->prev;
+ next = entry->next;
+
+ if (prev) {
+ rad_assert(state->head != entry);
+ prev->next = next;
+ } else if (state->head) {
+ rad_assert(state->head == entry);
+ state->head = next;
+ }
+
+ if (next) {
+ rad_assert(state->tail != entry);
+ next->prev = prev;
+ } else if (state->tail) {
+ rad_assert(state->tail == entry);
+ state->tail = prev;
+ }
+
+ rbtree_deletebydata(state->tree, entry);
+}
+
+/*
+ * When an entry is free'd, it's removed from the linked list of
+ * cleanup timers.
+ */
+static void state_entry_free(fr_state_t *state, state_entry_t *entry)
+{
+ /*
+ * If we're deleting the whole tree, don't bother doing
+ * all of the fixups.
+ */
+ if (!state || !state->tree) return;
+
+ state_entry_unlink(state, entry);
+
+ if (entry->opaque) {
+ entry->free_opaque(entry->opaque);
+ }
+
+#ifdef WITH_VERIFY_PTR
+ (void) talloc_get_type_abort(entry, state_entry_t);
+#endif
+ if (entry->ctx) talloc_free(entry->ctx);
+
+ talloc_free(entry);
+}
+
+fr_state_t *fr_state_init(TALLOC_CTX *ctx)
+{
+ fr_state_t *state;
+
+ if (!ctx) {
+ state = &global_state;
+ if (state->tree) return state;
+ } else {
+ state = talloc_zero(ctx, fr_state_t);
+ if (!state) return 0;
+ }
+
+#ifdef HAVE_PTHREAD_H
+ if (pthread_mutex_init(&state->mutex, NULL) != 0) {
+ STATE_FREE(state);
+ return NULL;
+ }
+#endif
+
+ state->tree = rbtree_create(NULL, state_entry_cmp, NULL, 0);
+ if (!state->tree) {
+ STATE_FREE(state);
+ return NULL;
+ }
+
+ return state;
+}
+
+void fr_state_delete(fr_state_t *state)
+{
+ rbtree_t *my_tree;
+
+ if (!state) return;
+
+ PTHREAD_MUTEX_LOCK(&state->mutex);
+
+ /*
+ * Tell the talloc callback to NOT delete the entry from
+ * the tree. We're deleting the entire tree.
+ */
+ my_tree = state->tree;
+ state->tree = NULL;
+
+ rbtree_free(my_tree);
+ PTHREAD_MUTEX_UNLOCK(&state->mutex);
+
+ STATE_FREE(state);
+}
+
+/*
+ * Create a fake request, based on what we know about the
+ * session that has expired, and inject it into the server to
+ * allow final logging or cleaning up.
+ */
+static REQUEST *fr_state_cleanup_request(state_entry_t *entry)
+{
+ REQUEST *request;
+ RADIUS_PACKET *packet = NULL;
+ RADIUS_PACKET *reply_packet = NULL;
+ VALUE_PAIR *vp;
+
+ /*
+ * Allocate a new fake request with enough to keep
+ * the rest of the server happy.
+ */
+ request = request_alloc(NULL);
+ if (unlikely(!request)) return NULL;
+
+ packet = rad_alloc(request, false);
+ if (unlikely(!packet)) {
+ error:
+ TALLOC_FREE(reply_packet);
+ TALLOC_FREE(packet);
+ TALLOC_FREE(request);
+ return NULL;
+ }
+
+ reply_packet = rad_alloc(request, false);
+ if (unlikely(!reply_packet)) goto error;
+
+ /*
+ * Move the server from the state entry over to the
+ * request. Clearing it in the state means this
+ * function will never be called again.
+ */
+ request->server = talloc_steal(request, entry->server);
+ entry->server = NULL;
+
+ /*
+ * Build the fake request with the limited
+ * information we have from the state.
+ */
+ request->packet = packet;
+ request->reply = reply_packet;
+ request->number = entry->request_number;
+ request->client = entry->request_client;
+ request->root = entry->request_root;
+ request->handle = rad_postauth;
+
+ /*
+ * Move session-state VPS over, after first freeing the
+ * separately-parented state_ctx that was allocated along with the
+ * fake request.
+ */
+ talloc_free(request->state_ctx);
+ request->state_ctx = entry->ctx;
+ request->state = entry->vps;
+
+ entry->ctx = NULL;
+ entry->vps = NULL;
+
+ /*
+ * Set correct Post-Auth-Type section
+ */
+ fr_pair_delete_by_num(&request->config, PW_POST_AUTH_TYPE, 0, TAG_ANY);
+ vp = pair_make_config("Post-Auth-Type", "Client-Lost", T_OP_SET);
+ if (unlikely(!vp)) goto error;
+
+ VERIFY_REQUEST(request);
+ return request;
+}
+
+/*
+ * Check state for old entries that need to be cleaned up. If
+ * they are old enough then move them from the global state
+ * list to a list of entries to clean up (outside the mutex).
+ * Called with the mutex held.
+ */
+static state_entry_t *fr_state_cleanup_find(fr_state_t *state)
+{
+ time_t now = time(NULL);
+ state_entry_t *entry, *next;
+ state_entry_t *head = NULL, **tail = &head;
+
+ for (entry = state->head; entry != NULL; entry = next) {
+ next = entry->next;
+
+ /*
+ * Unused. We can delete it, even if now isn't
+ * the time to clean it up.
+ */
+ if (!entry->ctx && !entry->opaque) {
+ state_entry_free(state, entry);
+ continue;
+ }
+
+ /*
+ * Not yet time to clean it up.
+ */
+ if (entry->cleanup > now) {
+ continue;
+ }
+
+ /*
+ * We're not running the "client lost" section.
+ * Just nuke the entry now.
+ */
+ if (!main_config.postauth_client_lost) {
+ state_entry_free(state, entry);
+ continue;
+ }
+
+ /*
+ * Old enough that the request has been removed.
+ * We can add it to the cleanup list.
+ */
+ state_entry_unlink(state, entry);
+ entry->prev = entry->next = NULL;
+ (*tail) = entry;
+ tail = &entry->next;
+ }
+
+ return head;
+}
+
+/*
+ * Inject all requests in cleanup list for cleanup post-auth
+ */
+static void fr_state_cleanup(state_entry_t *head)
+{
+ state_entry_t *entry, *next;
+
+ if (!head) return;
+
+ for (entry = head; entry != NULL; entry = next) {
+ REQUEST *request;
+
+ next = entry->next;
+
+ request = fr_state_cleanup_request(entry);
+ if (request) {
+ RDEBUG2("No response from client, cleaning up expired state");
+ RDEBUG2("Restoring &session-state");
+
+ /*
+ * @todo - print out message
+ * saying where the handler was
+ * in the process? i.e. "sent
+ * server cert", etc. This will
+ * require updating the EAP code
+ * to put a new attribute into
+ * the session state list.
+ */
+
+ rdebug_pair_list(L_DBG_LVL_2, request, request->state, "&session-state:");
+
+ request_inject(request);
+ }
+
+ if (entry->opaque) {
+ entry->free_opaque(entry->opaque);
+ }
+
+ if (entry->ctx) talloc_free(entry->ctx);
+
+ talloc_free(entry);
+ }
+}
+
+static void state_entry_calc(REQUEST *request, state_entry_t *entry, VALUE_PAIR *vp)
+{
+ /*
+ * Assume our own State first. This is where the state
+ * is the correct size, AND we're not proxying it to an
+ * external home server. If we are proxying it to an
+ * external home server, then that home server creates
+ * the State attribute, and we don't control it.
+ */
+ if (entry->ours ||
+ (vp->vp_length == sizeof(entry->state) &&
+ (!request->proxy || (request->proxy->dst_port == 0)))) {
+ memcpy(entry->state, vp->vp_octets, sizeof(entry->state));
+ entry->ours = true;
+
+ } else {
+ FR_MD5_CTX ctx;
+
+ /*
+ * We don't control the external State attribute.
+ * As a result, different home servers _may_
+ * create the same State attribute. In order to
+ * differentiate them, we "mix in" the User-Name,
+ * which should contain the realm. And we then
+ * hope that different home servers in the same
+ * realm don't create overlapping State
+ * attributes.
+ */
+ fr_md5_init(&ctx);
+ fr_md5_update(&ctx, vp->vp_octets, vp->vp_length);
+
+ vp = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY);
+ if (vp) fr_md5_update(&ctx, vp->vp_octets, vp->vp_length);
+
+ fr_md5_final(entry->state, &ctx);
+ fr_md5_destroy(&ctx);
+ }
+}
+
+
+/*
+ * Create a new entry. Called with the mutex held.
+ */
+static state_entry_t *fr_state_entry_create(fr_state_t *state, REQUEST *request, RADIUS_PACKET *packet, state_entry_t *old)
+{
+ size_t i;
+ uint32_t x;
+ time_t now = time(NULL);
+ VALUE_PAIR *vp;
+ state_entry_t *entry;
+
+ /*
+ * Limit the size of the cache based on how many requests
+ * we can handle at the same time.
+ */
+ if (rbtree_num_elements(state->tree) >= main_config.max_requests * 2) {
+ return NULL;
+ }
+
+ /*
+ * Allocate a new one.
+ */
+ entry = talloc_zero(state->tree, state_entry_t);
+ if (!entry) return NULL;
+
+ /*
+ * Limit the lifetime of this entry based on how long the
+ * server takes to process a request. Doing it this way
+ * isn't perfect, but it's reasonable, and it's one less
+ * thing for an administrator to configure.
+ */
+ entry->cleanup = now + main_config.max_request_time * 2;
+
+ /*
+ * Hacks for EAP, until we convert EAP to using the state API.
+ *
+ * The EAP module creates it's own State attribute, so we
+ * want to use that one in preference to one we create.
+ */
+ vp = fr_pair_find_by_num(packet->vps, PW_STATE, 0, TAG_ANY);
+
+ /*
+ * If possible, base the new one off of the old one.
+ */
+ if (old) {
+ entry->tries = old->tries + 1;
+ entry->ours = old->ours;
+
+ /*
+ * Track State
+ */
+ if (!vp && entry->ours) {
+ memcpy(entry->state, old->state, sizeof(entry->state));
+
+ entry->state[1] = entry->state[0] ^ entry->tries;
+ entry->state[8] = entry->state[2] ^ (((uint32_t) HEXIFY(RADIUSD_VERSION)) & 0xff);
+ entry->state[10] = entry->state[2] ^ ((((uint32_t) HEXIFY(RADIUSD_VERSION)) >> 8) & 0xff);
+ entry->state[12] = entry->state[2] ^ ((((uint32_t) HEXIFY(RADIUSD_VERSION)) >> 16) & 0xff);
+ }
+
+ /*
+ * The old one isn't used any more, so we can free it.
+ */
+ if (!old->opaque) state_entry_free(state, old);
+
+ } else if (!vp) {
+ /*
+ * 16 octets of randomness should be enough to
+ * have a globally unique state.
+ */
+ for (i = 0; i < sizeof(entry->state) / sizeof(x); i++) {
+ x = fr_rand();
+ memcpy(entry->state + (i * 4), &x, sizeof(x));
+ }
+
+ entry->ours = true; /* we created it */
+ }
+
+ /*
+ * If EAP created a State, use that. Otherwise, use the
+ * one we created above.
+ */
+ if (vp) {
+ state_entry_calc(request, entry, vp);
+
+ } else {
+ vp = fr_pair_afrom_num(packet, PW_STATE, 0);
+ fr_pair_value_memcpy(vp, entry->state, sizeof(entry->state));
+ fr_pair_add(&packet->vps, vp);
+ }
+
+ /* Make unique for different virtual servers handling same request
+ */
+ if (request->server) {
+ /*
+ * Make unique for different virtual servers handling same request
+ */
+ if (entry->ours) *((uint32_t *)(&entry->state[4])) ^= fr_hash_string(request->server);
+
+ /*
+ * Copy server to state in case it's needed for cleanup
+ */
+ entry->server = talloc_strdup(entry, request->server);
+ entry->request_number = request->number;
+ entry->request_client = request->client;
+ entry->request_root = request->root;
+ }
+
+ if (!state_entry_link(state, entry)) {
+ talloc_free(entry);
+ return NULL;
+ }
+
+ return entry;
+}
+
+
+/*
+ * Find the entry, based on the State attribute.
+ */
+static state_entry_t *fr_state_find(REQUEST *request, fr_state_t *state, const char *server, RADIUS_PACKET *packet)
+{
+ VALUE_PAIR *vp;
+ state_entry_t *entry, my_entry;
+
+ vp = fr_pair_find_by_num(packet->vps, PW_STATE, 0, TAG_ANY);
+ if (!vp) return NULL;
+
+ my_entry.ours = false;
+ state_entry_calc(request, &my_entry, vp);
+
+ /* Make unique for different virtual servers handling same request
+ */
+ if (server && my_entry.ours) *((uint32_t *)(&my_entry.state[4])) ^= fr_hash_string(server);
+
+ entry = rbtree_finddata(state->tree, &my_entry);
+
+#ifdef WITH_VERIFY_PTR
+ if (entry) (void) talloc_get_type_abort(entry, state_entry_t);
+#endif
+
+ return entry;
+}
+
+/*
+ * Called when sending Access-Accept or Access-Reject, so
+ * that all State is discarded.
+ */
+void fr_state_discard(REQUEST *request, RADIUS_PACKET *original)
+{
+ state_entry_t *entry;
+ fr_state_t *state = &global_state;
+
+ fr_pair_list_free(&request->state);
+ request->state = NULL;
+
+ PTHREAD_MUTEX_LOCK(&state->mutex);
+ entry = fr_state_find(request, state, request->server, original);
+ if (entry) state_entry_free(state, entry);
+ PTHREAD_MUTEX_UNLOCK(&state->mutex);
+}
+
+/*
+ * Get the VPS from the state.
+ */
+void fr_state_get_vps(REQUEST *request, RADIUS_PACKET *packet)
+{
+ state_entry_t *entry;
+ fr_state_t *state = &global_state;
+ TALLOC_CTX *old_ctx = NULL;
+
+ /*
+ * No State, don't do anything.
+ */
+ if (!fr_pair_find_by_num(request->packet->vps, PW_STATE, 0, TAG_ANY)) {
+ RDEBUG3("session-state: No State attribute");
+ return;
+ }
+
+ rad_assert(request->state == NULL);
+
+ PTHREAD_MUTEX_LOCK(&state->mutex);
+ entry = fr_state_find(request, state, request->server, packet);
+
+ /*
+ * This has to be done in a mutex lock, because talloc
+ * isn't thread-safe.
+ */
+ if (entry) {
+ RDEBUG2("Restoring &session-state");
+
+ if (request->state_ctx) old_ctx = request->state_ctx;
+
+ request->state_ctx = entry->ctx;
+ request->state = entry->vps;
+
+ entry->ctx = NULL;
+ entry->vps = NULL;
+
+ rdebug_pair_list(L_DBG_LVL_2, request, request->state, "&session-state:");
+
+ } else {
+ RDEBUG2("session-state: No cached attributes");
+ }
+
+ PTHREAD_MUTEX_UNLOCK(&state->mutex);
+
+ /*
+ * Free this outside of the mutex for less contention.
+ */
+ if (old_ctx) talloc_free(old_ctx);
+
+ VERIFY_REQUEST(request);
+ return;
+}
+
+
+/*
+ * Put request->state into the State attribute. Put the State
+ * attribute into the vps list. Delete the original entry, if it
+ * exists.
+ */
+bool fr_state_put_vps(REQUEST *request, RADIUS_PACKET *original, RADIUS_PACKET *packet)
+{
+ state_entry_t *entry, *old;
+ fr_state_t *state = &global_state;
+ state_entry_t *cleanup_list;
+
+ if (!request->state) {
+ size_t i;
+ uint32_t x;
+ VALUE_PAIR *vp;
+ uint8_t buffer[16];
+
+ RDEBUG3("session-state: Nothing to cache");
+
+ if (packet->code != PW_CODE_ACCESS_CHALLENGE) return true;
+
+ vp = fr_pair_find_by_num(packet->vps, PW_STATE, 0, TAG_ANY);
+ if (vp) return true;
+
+ /*
+ *
+ */
+ for (i = 0; i < sizeof(buffer) / sizeof(x); i++) {
+ x = fr_rand();
+ memcpy(buffer + (i * 4), &x, sizeof(x));
+ }
+
+ vp = fr_pair_afrom_num(packet, PW_STATE, 0);
+ fr_pair_value_memcpy(vp, buffer, sizeof(buffer));
+ fr_pair_add(&packet->vps, vp);
+
+ return true;
+ }
+
+ RDEBUG2("session-state: Saving cached attributes");
+ rdebug_pair_list(L_DBG_LVL_1, request, request->state, NULL);
+
+ PTHREAD_MUTEX_LOCK(&state->mutex);
+
+ cleanup_list = fr_state_cleanup_find(state);
+
+ if (original) {
+ old = fr_state_find(request, state, request->server, original);
+ } else {
+ old = NULL;
+ }
+
+ /*
+ * Create a new entry and add it to the list.
+ */
+ entry = fr_state_entry_create(state, request, packet, old);
+ if (!entry) {
+ PTHREAD_MUTEX_UNLOCK(&state->mutex);
+ fr_state_cleanup(cleanup_list);
+ return false;
+ }
+
+ rad_assert(entry->ctx == NULL);
+ entry->ctx = request->state_ctx;
+ entry->vps = request->state;
+
+ request->state_ctx = NULL;
+ request->state = NULL;
+
+ PTHREAD_MUTEX_UNLOCK(&state->mutex);
+ fr_state_cleanup(cleanup_list);
+
+ VERIFY_REQUEST(request);
+ return true;
+}