From: Alan T. DeKok Date: Fri, 16 Jan 2026 12:24:59 +0000 (-0500) Subject: add in-memory KV module and tests X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a2096965f92c22f0ef873904f8cd3daf664240bc;p=thirdparty%2Ffreeradius-server.git add in-memory KV module and tests --- diff --git a/redhat/freeradius.spec b/redhat/freeradius.spec index 4019931e7c0..63411489d8f 100644 --- a/redhat/freeradius.spec +++ b/redhat/freeradius.spec @@ -1015,6 +1015,7 @@ fi %{_libdir}/freeradius/rlm_files.so %{_libdir}/freeradius/rlm_icmp.so %{_libdir}/freeradius/rlm_isc_dhcp.so +%{_libdir}/freeradius/rlm_kv.so %{_libdir}/freeradius/rlm_linelog.so %{_libdir}/freeradius/rlm_logtee.so %{_libdir}/freeradius/rlm_mschap.so diff --git a/src/modules/rlm_kv/README.md b/src/modules/rlm_kv/README.md new file mode 100644 index 00000000000..cb7a762fc00 --- /dev/null +++ b/src/modules/rlm_kv/README.md @@ -0,0 +1,8 @@ +# rlm_kv +## Metadata +
+
category
datastore
+
+ +## Summary +Memory only key-value store. diff --git a/src/modules/rlm_kv/all.mk b/src/modules/rlm_kv/all.mk new file mode 100644 index 00000000000..aa9630dba29 --- /dev/null +++ b/src/modules/rlm_kv/all.mk @@ -0,0 +1,4 @@ +TARGETNAME := rlm_kv +TARGET := $(TARGETNAME)$(L) +SOURCES := rlm_kv.c +LOG_ID_LIB = 61 diff --git a/src/modules/rlm_kv/rlm_kv.c b/src/modules/rlm_kv/rlm_kv.c new file mode 100644 index 00000000000..1ff8c9b62b3 --- /dev/null +++ b/src/modules/rlm_kv/rlm_kv.c @@ -0,0 +1,318 @@ +/* + * 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$ + * @file rlm_kv.c + * @brief Provide an ephemeral, in-memory kv store. + * + * This module will use infinite memory if asked, as it doesn't track + * or expire old entries. + * + * This module uses cross-thread mutex locks, so if used a lot, it + * will cause all threads to synchronise, and will kill performance. + * + * @copyright 2026 Network RADIUS SAS (legal@networkradus.com) + */ +RCSID("$Id$") + +#define LOG_PREFIX mctx->mi->name + +#include +#include +#include +#include + +#include + +typedef struct rlm_kv_data_s rlm_kv_data_t; + +FR_DLIST_TYPES(rlm_kv_list) +FR_DLIST_TYPEDEFS(rlm_kv_list, rlm_kv_list_t, rlm_kv_entry_t) + +/** KV structure + * + * The "key" field MUST be first, so that we can do lookups by giving + * the htrie code a "fr_value_box_t*", which is the key. + */ +struct rlm_kv_data_s { + fr_value_box_t key; //!< indexed key + fr_value_box_t value; //!< value to store + rlm_kv_entry_t entry; //!< for expiration +}; + +FR_DLIST_FUNCS(rlm_kv_list, rlm_kv_data_t, entry) + +/** Mutable data structure which is shared across all threads. + * + */ +typedef struct { + fr_htrie_t *tree; //!< for kv stores. + rlm_kv_list_t list; //!< for expiring old entries + pthread_mutex_t mutex; //!< for thread locking. +} rlm_kv_mutable_t; + +typedef struct { + uint32_t max_entries; + fr_htrie_type_t htype; + char const *key_type; //!< data type of the key + rlm_kv_mutable_t *mutable; +} rlm_kv_t; + +/* + * A mapping of configuration file names to internal variables. + */ +static const conf_parser_t module_config[] = { + { FR_CONF_OFFSET("key_type", rlm_kv_t, key_type), .dflt = "string" }, + + { FR_CONF_OFFSET("max_entries", rlm_kv_t, max_entries), .dflt = "8192" }, + + CONF_PARSER_TERMINATOR +}; + +static xlat_arg_parser_t const kv_write_xlat_args[] = { + { .required = true, .single = true, .type = FR_TYPE_VOID }, + { .required = true, .single = true, .type = FR_TYPE_VOID }, + XLAT_ARG_PARSER_TERMINATOR +}; + +static xlat_arg_parser_t const kv_read_xlat_args[] = { + { .required = true, .single = true, .type = FR_TYPE_VOID }, + XLAT_ARG_PARSER_TERMINATOR +}; + +/** Write an entry to the KV + * + * %kv.write(key, value) + */ +static xlat_action_t kv_write_xlat(UNUSED TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out, + xlat_ctx_t const *xctx, + UNUSED request_t *request, fr_value_box_list_t *args) +{ + rlm_kv_t const *in = talloc_get_type_abort(xctx->mctx->mi->data, rlm_kv_t); + rlm_kv_mutable_t *inst = talloc_get_type_abort(in->mutable, rlm_kv_mutable_t); + fr_value_box_t *key, *value; + rlm_kv_data_t *data, *old = NULL; + + XLAT_ARGS(args, &key, &value); + + MEM(data = talloc_zero(NULL, rlm_kv_data_t)); + if (fr_value_box_copy(data, &data->key, key) < 0) { + talloc_free(data); + return XLAT_ACTION_FAIL; + } + if (fr_value_box_copy(data, &data->value, value) < 0) { + talloc_free(data); + return XLAT_ACTION_FAIL; + } + + pthread_mutex_lock(&inst->mutex); + + if (fr_htrie_replace((void **) &old, inst->tree, data) < 0) { + pthread_mutex_unlock(&inst->mutex); + talloc_free(data); + REDEBUG("Failed inserting (key=%pV, value=%pV)", key, value); + return XLAT_ACTION_DONE; + } + + /* + * This is now the newest entry, as it has been recently written. + */ + (void) rlm_kv_list_insert_head(&inst->list, data); + + /* + * We've removed the old box from the tree. Unlink it. + * And since we removed an old box, we don't have to + * worry about the htrie being too full. + */ + if (old) { + (void) rlm_kv_list_remove(&inst->list, old); + talloc_free(old); + + /* + * We've inserted a brand new entry. If the list + * is full, delete an old entry. + */ + } else if (rlm_kv_list_num_elements(&inst->list) >= in->max_entries) { + old = rlm_kv_list_pop_tail(&inst->list); + fr_assert(old != NULL); + + talloc_free(old); + } + + pthread_mutex_unlock(&inst->mutex); + + return XLAT_ACTION_DONE; + +} + +/** Read an entry from the KV + * + * %kv.read(key) + */ +static xlat_action_t kv_read_xlat(TALLOC_CTX *ctx, fr_dcursor_t *out, + xlat_ctx_t const *xctx, + UNUSED request_t *request, fr_value_box_list_t *args) +{ + rlm_kv_t const *in = talloc_get_type_abort(xctx->mctx->mi->data, rlm_kv_t); + rlm_kv_mutable_t *inst = talloc_get_type_abort(in->mutable, rlm_kv_mutable_t); + fr_value_box_t *key, *dst; + rlm_kv_data_t *data; + + XLAT_ARGS(args, &key); + + pthread_mutex_lock(&inst->mutex); + data = fr_htrie_find(inst->tree, key); + if (!data) { + pthread_mutex_unlock(&inst->mutex); + RDEBUG("Failed to find entry for key %pV", key); + return XLAT_ACTION_DONE; + } + + MEM(dst = fr_value_box_acopy(ctx, &data->value)); + + /* + * This item was recently accessed. It's therefore now + * the newest entry. + */ + (void) rlm_kv_list_remove(&inst->list, data); + (void) rlm_kv_list_insert_head(&inst->list, data); + + pthread_mutex_unlock(&inst->mutex); + + fr_dcursor_append(out, dst); + + return XLAT_ACTION_DONE; + +} + +/** Delete an entry from the KV + * + * Returns the deleted entry, if one exists. Otherwise returns nothing. + * + * %kv.delete(key) + */ +static xlat_action_t kv_delete_xlat(TALLOC_CTX *ctx, fr_dcursor_t *out, + xlat_ctx_t const *xctx, + UNUSED request_t *request, fr_value_box_list_t *args) +{ + rlm_kv_t const *in = talloc_get_type_abort(xctx->mctx->mi->data, rlm_kv_t); + rlm_kv_mutable_t *inst = talloc_get_type_abort(in->mutable, rlm_kv_mutable_t); + fr_value_box_t *key, *dst; + rlm_kv_data_t *data; + + XLAT_ARGS(args, &key); + + /* + * @todo - if the key is a string, allow wildcards in the + * deletion path. In which case we need to be able to + * walk over the entire htrie. And the htrie API doesn't + * support that yet. + */ + + pthread_mutex_lock(&inst->mutex); + data = fr_htrie_remove(inst->tree, key); + if (!data) { + pthread_mutex_unlock(&inst->mutex); + return XLAT_ACTION_DONE; + } + (void) rlm_kv_list_remove(&inst->list, data); + + pthread_mutex_unlock(&inst->mutex); + + MEM(dst = fr_value_box_acopy(ctx, &data->value)); + fr_dcursor_append(out, dst); + + talloc_free(data); + + return XLAT_ACTION_DONE; + +} + +static int mod_mutable_free(rlm_kv_mutable_t *mutable) +{ + pthread_mutex_destroy(&mutable->mutex); + return 0; +} + + +static int mod_instantiate(module_inst_ctx_t const *mctx) +{ + rlm_kv_t *inst = talloc_get_type_abort(mctx->mi->data, rlm_kv_t); + fr_type_t type; + + /* + * Get the data type, and convert it to an htrie type. + */ + type = fr_type_from_str(inst->key_type); + if (type == FR_TYPE_NULL) { + cf_log_err(mctx->mi->conf, "Unknown data type '%s'", inst->key_type); + return -1; + } + + inst->htype = fr_htrie_hint(type); + if (inst->htype == FR_HTRIE_INVALID) { + cf_log_err(mctx->mi->conf, "Invalid data type '%s' for KV store", inst->key_type); + return -1; + } + + MEM(inst->mutable = talloc_zero(NULL, rlm_kv_mutable_t)); + + inst->mutable->tree = fr_htrie_alloc(inst->mutable, inst->htype, + (fr_hash_t) fr_value_box_hash, + (fr_cmp_t) fr_value_box_cmp, + (fr_trie_key_t) fr_value_box_to_key, NULL); + if (!inst->mutable->tree) return -1; + + pthread_mutex_init(&inst->mutable->mutex, NULL); + talloc_set_destructor(inst->mutable, mod_mutable_free); + + rlm_kv_list_init(&inst->mutable->list); + + FR_INTEGER_BOUND_CHECK("max_entries", inst->max_entries, >=, 1024); + FR_INTEGER_BOUND_CHECK("max_entries", inst->max_entries, <, (1 << 22)); /* 4M should be enough */ + + return 0; +} + +static int mod_bootstrap(module_inst_ctx_t const *mctx) +{ + xlat_t *xlat; + + xlat = module_rlm_xlat_register(mctx->mi->boot, mctx, "write", kv_write_xlat, FR_TYPE_NULL); + xlat_func_args_set(xlat, kv_write_xlat_args); /* path, value */ + + xlat = module_rlm_xlat_register(mctx->mi->boot, mctx, "read", kv_read_xlat, FR_TYPE_VOID); + xlat_func_args_set(xlat, kv_read_xlat_args); /* path */ + + xlat = module_rlm_xlat_register(mctx->mi->boot, mctx, "delete", kv_delete_xlat, FR_TYPE_VOID); + xlat_func_args_set(xlat, kv_read_xlat_args); /* path */ + + return 0; +} + +extern module_rlm_t rlm_kv; +module_rlm_t rlm_kv = { + .common = { + .magic = MODULE_MAGIC_INIT, + .name = "kv", + .inst_size = sizeof(rlm_kv_t), + .config = module_config, + .bootstrap = mod_bootstrap, + .instantiate = mod_instantiate, + }, +}; diff --git a/src/tests/keywords/kv b/src/tests/keywords/kv new file mode 100644 index 00000000000..fb230659b17 --- /dev/null +++ b/src/tests/keywords/kv @@ -0,0 +1,20 @@ +# +# Test the internal key-value store. +# +%kv.write("/foo/bar", User-Name) + +if (%kv.read("/foo/bar") != User-Name) { + test_fail +} + +%kv.delete("/foo/bar") + +if %kv.read("/foo/bar") { + test_fail +} + +if %kv.read("no such key") { + test_fail +} + +success diff --git a/src/tests/keywords/radius.conf b/src/tests/keywords/radius.conf index 8e56712baac..44444764d65 100644 --- a/src/tests/keywords/radius.conf +++ b/src/tests/keywords/radius.conf @@ -120,6 +120,11 @@ modules { test1 test2 } + + # Key-value store + kv { + + } } policy {