* @param[in] name of attribute including #request_ref_t and #pair_list_t qualifiers.
* If only #request_ref_t #pair_list_t qualifiers are found,
* a #TMPL_TYPE_LIST #vp_tmpl_t will be produced.
+ * @param[in] name_len Length of name, or -1 to do strlen()
* @param[in] rules Rules which control parsing:
* - dict_def The default dictionary to use if attributes
* are unqualified.
* - > 0 on success (number of bytes parsed).
*/
ssize_t tmpl_afrom_attr_substr(TALLOC_CTX *ctx, attr_ref_error_t *err,
- vp_tmpl_t **out, char const *name, vp_tmpl_rules_t const *rules)
+ vp_tmpl_t **out, char const *name, ssize_t name_len, vp_tmpl_rules_t const *rules)
{
char const *p, *q;
long num;
if (err) *err = ATTR_REF_ERROR_NONE;
+ if (name_len < 0) name_len = strlen(name);
+
p = name;
if (!*p) {
vpt->tmpl_num = NUM_ANY;
vpt->type = TMPL_TYPE_ATTR;
+ /*
+ * No more input after parsing the list ref, we're done.
+ */
+ if (p == (name + name_len)) {
+ vpt->type = TMPL_TYPE_LIST;
+ goto finish;
+ }
+
/*
* This may be just a bare list, but it can still
* have instance selectors and tag selectors.
* Copy the name to a field for later resolution
*/
vpt->type = TMPL_TYPE_ATTR_UNDEFINED;
- for (q = p; fr_dict_attr_allowed_chars[(uint8_t) *q]; q++);
+ for (q = p; (q < (name + name_len)) && fr_dict_attr_allowed_chars[(uint8_t) *q]; q++);
if (q == p) {
fr_strerror_printf("Invalid attribute name");
if (err) *err = ATTR_REF_ERROR_INVALID_ATTRIBUTE_NAME;
ssize_t tmpl_afrom_attr_str(TALLOC_CTX *ctx, attr_ref_error_t *err,
vp_tmpl_t **out, char const *name, vp_tmpl_rules_t const *rules)
{
- ssize_t slen;
+ ssize_t slen, name_len;
if (!rules) rules = &default_rules; /* Use the defaults */
- slen = tmpl_afrom_attr_substr(ctx, err, out, name, rules);
+ name_len = strlen(name);
+ slen = tmpl_afrom_attr_substr(ctx, err, out, name, name_len, rules);
if (slen <= 0) return slen;
if (!fr_cond_assert(*out)) return -1;
- if (slen != (ssize_t)strlen(name)) {
+ if (slen != name_len) {
/* This looks wrong, but it produces meaningful errors for unknown attrs with tags */
fr_strerror_printf("Unexpected text after %s", fr_table_str_by_value(tmpl_type_table, (*out)->type, "<INVALID>"));
return -slen;
* We can't define a template with garbage after
* the attribute name.
*/
- slen = tmpl_afrom_attr_substr(ctx, NULL, &vpt, in, &mrules);
+ slen = tmpl_afrom_attr_substr(ctx, NULL, &vpt, in, inlen, &mrules);
if (mrules.allow_undefined && (slen <= 0)) return slen;
if (slen > 0) {
if ((size_t) slen < inlen) {
int tmpl_afrom_value_box(TALLOC_CTX *ctx, vp_tmpl_t **out, fr_value_box_t *data, bool steal);
ssize_t tmpl_afrom_attr_substr(TALLOC_CTX *ctx, attr_ref_error_t *err,
- vp_tmpl_t **out, char const *name,
+ vp_tmpl_t **out, char const *name, ssize_t name_len,
vp_tmpl_rules_t const *rules);
ssize_t tmpl_afrom_attr_str(TALLOC_CTX *ctx, attr_ref_error_t *err,
*/
fr_skip_spaces(p);
- slen = tmpl_afrom_attr_substr(ctx, NULL, &vpt, p, &(vp_tmpl_rules_t){ .dict_def = request->dict });
+ slen = tmpl_afrom_attr_substr(ctx, NULL, &vpt, p, -1, &(vp_tmpl_rules_t){ .dict_def = request->dict });
if (slen <= 0) {
RPEDEBUG("Invalid input");
return -1;
return 0;
}
- slen = tmpl_afrom_attr_substr(request, NULL, &vpt, p, &(vp_tmpl_rules_t){ .dict_def = request->dict });
+ slen = tmpl_afrom_attr_substr(request, NULL, &vpt, p, -1, &(vp_tmpl_rules_t){ .dict_def = request->dict });
if (slen <= 0) {
RPEDEBUG("Failed parsing input string");
return slen;
our_rules.allow_undefined = true; /* So we can check for virtual attributes later */
our_rules.prefix = VP_ATTR_REF_PREFIX_NO; /* Must be NO to stop %{&User-Name} */
- slen = tmpl_afrom_attr_substr(NULL, &err, &vpt, p, &our_rules);
+ slen = tmpl_afrom_attr_substr(NULL, &err, &vpt, p, -1, &our_rules);
if (slen <= 0) {
/*
* If the parse error occurred before the ':'
*/
fr_skip_spaces(p);
- slen = tmpl_afrom_attr_substr(our_ctx, NULL, &vpt, p,
+ slen = tmpl_afrom_attr_substr(our_ctx, NULL, &vpt, p, -1,
&(vp_tmpl_rules_t){
.dict_def = request->dict,
.prefix = VP_ATTR_REF_PREFIX_AUTO
*/
fr_skip_spaces(p);
- slen = tmpl_afrom_attr_substr(our_ctx, NULL, &vpt, p,
+ slen = tmpl_afrom_attr_substr(our_ctx, NULL, &vpt, p, -1,
&(vp_tmpl_rules_t){
.dict_def = request->dict,
.prefix = VP_ATTR_REF_PREFIX_AUTO
*/
fr_skip_spaces(p);
- slen = tmpl_afrom_attr_substr(our_ctx, NULL, &vpt, p,
+ slen = tmpl_afrom_attr_substr(our_ctx, NULL, &vpt, p, -1,
&(vp_tmpl_rules_t){
.dict_def = request->dict,
.prefix = VP_ATTR_REF_PREFIX_AUTO
*/
fr_skip_spaces(p);
- slen = tmpl_afrom_attr_substr(our_ctx, NULL, &id_vpt, p,
+ slen = tmpl_afrom_attr_substr(our_ctx, NULL, &id_vpt, p, -1,
&(vp_tmpl_rules_t){
.dict_def = request->dict,
.prefix = VP_ATTR_REF_PREFIX_AUTO
}
p++;
- slen = tmpl_afrom_attr_substr(our_ctx, NULL, &key_vpt, p,
+ slen = tmpl_afrom_attr_substr(our_ctx, NULL, &key_vpt, p, -1,
&(vp_tmpl_rules_t){
.dict_def = request->dict,
.prefix = VP_ATTR_REF_PREFIX_AUTO
*/
fr_skip_spaces(p);
- slen = tmpl_afrom_attr_substr(our_ctx, NULL, &id_vpt, p,
+ slen = tmpl_afrom_attr_substr(our_ctx, NULL, &id_vpt, p, -1,
&(vp_tmpl_rules_t){
.dict_def = request->dict,
.prefix = VP_ATTR_REF_PREFIX_AUTO
}
p++;
- slen = tmpl_afrom_attr_substr(our_ctx, NULL, &key_vpt, p,
+ slen = tmpl_afrom_attr_substr(our_ctx, NULL, &key_vpt, p, -1,
&(vp_tmpl_rules_t){
.dict_def = request->dict,
.prefix = VP_ATTR_REF_PREFIX_AUTO
--- /dev/null
+# Modules to fix for namespace
+
+Some modules need an explicit `dict` pointer.
+
+Other modules use xlat's, etc. which need to have a dict / namespace set.
+
+We MAY need to add a `namespace = ...` config to EVERY module. <sigh>
+It would be much preferable to just get the namespace from where the
+module is being referenced.
+
+The daemon parses the config, and then bootstraps the modules *before*
+the virtual servers. So we can't say "hey, this module is used from
+virtual server X, let's go figure out it's namespace!"
+
+* attr_filter
+* files
+* detail
+* cache
+ * get dict into TYPE_TMPL config file parsing
+* exec
+* passwd
+* radutmp
+* linelog
--- /dev/null
+# TO DO
+
+Add `leftover` argument to `mod_write()`. So the caller knows if the
+data has been partially written. That way all of the pending / retry
+code is handled in `network.c`.
+
+This change will substantially simplify the writers.
+
+* EWOULDBLOCK, network side retries whole packet
+* `leftover != 0`, network side localizes message, and retries at a later time.
--- /dev/null
+# TODO
+
+* add config file items for
+ recv_type = { accounting, preacct, etc.}
+ send_type
+
+* code
+ for packet processing request->packet->code
+
+# DONE
+
+* priority is now set manually
--- /dev/null
+/*
+ * proto_detail.c Process the detail file
+ *
+ * Version: $Id$
+ *
+ * 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
+ *
+ * 2007 The FreeRADIUS server project
+ * 2007 Alan DeKok <aland@deployingradius.com>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/protocol.h>
+#include <freeradius-devel/process.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include "detail.h"
+
+#ifdef HAVE_SYS_STAT_H
+#include <sys/stat.h>
+#endif
+
+#ifdef HAVE_GLOB_H
+#include <glob.h>
+#endif
+
+#include <pthread.h>
+
+#include <fcntl.h>
+
+#define USEC (1000000)
+
+static FR_NAME_NUMBER state_names[] = {
+ { "unopened", STATE_UNOPENED },
+ { "unlocked", STATE_UNLOCKED },
+ { "processing", STATE_PROCESSING },
+
+ { "header", STATE_HEADER },
+ { "vps", STATE_VPS },
+ { "queued", STATE_QUEUED },
+ { "running", STATE_RUNNING },
+ { "no-reply", STATE_NO_REPLY },
+ { "replied", STATE_REPLIED },
+
+ { NULL, 0 }
+};
+
+
+/*
+ * If we're limiting outstanding packets, then mark the response
+ * as being sent.
+ */
+static int detail_send(rad_listen_t *listener, REQUEST *request)
+{
+ char c = 0;
+ listen_detail_t *data = listener->data;
+
+ rad_assert(request->listener == listener);
+ rad_assert(listener->send == detail_send);
+
+ /*
+ * This request timed out. Remember that, and tell the
+ * caller it's OK to read more "detail" file stuff.
+ */
+ if (request->reply->code == 0) {
+ data->delay_time = data->retry_interval * USEC;
+ data->signal = 1;
+ data->entry_state = STATE_NO_REPLY;
+
+ RDEBUG("detail (%s): No response to request. Will retry in %d seconds",
+ data->name, data->retry_interval);
+ } else {
+ int rtt;
+ struct timeval now;
+ /*
+ * We call gettimeofday a lot. But it should be OK,
+ * because there's nothing else to do.
+ */
+ gettimeofday(&now, NULL);
+
+ /*
+ * If we haven't sent a packet in the last second, reset
+ * the RTT.
+ */
+ now.tv_sec -= 1;
+ if (fr_timeval_cmp(&data->last_packet, &now) < 0) {
+ data->has_rtt = false;
+ }
+ now.tv_sec += 1;
+
+ /*
+ * Only one detail packet may be outstanding at a time,
+ * so it's safe to update some entries in the detail
+ * structure.
+ *
+ * We keep smoothed round trip time (SRTT), but not round
+ * trip timeout (RTO). We use SRTT to calculate a rough
+ * load factor.
+ */
+ rtt = now.tv_sec - request->packet->timestamp.tv_sec;
+ rtt *= USEC;
+ rtt += now.tv_usec;
+ rtt -= request->packet->timestamp.tv_usec;
+
+ /*
+ * If we're proxying, the RTT is our processing time,
+ * plus the network delay there and back, plus the time
+ * on the other end to process the packet. Ideally, we
+ * should remove the network delays from the RTT, but we
+ * don't know what they are.
+ *
+ * So, to be safe, we over-estimate the total cost of
+ * processing the packet.
+ */
+ if (!data->has_rtt) {
+ data->has_rtt = true;
+ data->srtt = rtt;
+ data->rttvar = rtt / 2;
+
+ } else {
+ data->rttvar -= data->rttvar >> 2;
+ data->rttvar += (data->srtt - rtt);
+ data->srtt -= data->srtt >> 3;
+ data->srtt += rtt >> 3;
+ }
+
+ /*
+ * Calculate the time we wait before sending the next
+ * packet.
+ *
+ * rtt / (rtt + delay) = load_factor / 100
+ */
+ data->delay_time = (data->srtt * (100 - data->load_factor)) / (data->load_factor);
+
+ /*
+ * Cap delay at no less than 4 packets/s. If the
+ * end system can't handle this, then it's very
+ * broken.
+ */
+ if (data->delay_time > (USEC / 4)) data->delay_time= USEC / 4;
+
+ RDEBUG3("detail (%s): Received response for request %" PRIu64 ". "
+ "Will read the next packet in %d seconds",
+ data->name, request->number, data->delay_time / USEC);
+
+ data->last_packet = now;
+ data->signal = 1;
+ data->entry_state = STATE_REPLIED;
+ data->counter++;
+ }
+
+ if (write(data->child_pipe[1], &c, 1) < 0) {
+ RERROR("detail (%s): Failed writing ack to reader thread: %s", data->name, fr_syserror(errno));
+ }
+
+ return 0;
+}
+
+
+/*
+ * Open the detail file, if we can.
+ *
+ * FIXME: create it, if it's not already there, so that the main
+ * server select() will wake us up if there's anything to read.
+ */
+static int detail_open(rad_listen_t *this)
+{
+ struct stat st;
+ listen_detail_t *data = this->data;
+
+ rad_assert(data->file_state == STATE_UNOPENED);
+ data->delay_time = USEC;
+
+ /*
+ * Open detail.work first, so we don't lose
+ * accounting packets. It's probably better to
+ * duplicate them than to lose them.
+ *
+ * Note that we're not writing to the file, but
+ * we've got to open it for writing in order to
+ * establish the lock, to prevent rlm_detail from
+ * writing to it.
+ *
+ * This also means that if we're doing globbing,
+ * this file will be read && processed before the
+ * file globbing is done.
+ */
+ data->fp = NULL;
+ data->work_fd = open(data->filename_work, O_RDWR);
+
+ /*
+ * Couldn't open it for a reason OTHER than "it doesn't
+ * exist". Complain and tell the admin.
+ */
+ if ((data->work_fd < 0) && (errno != ENOENT)) {
+ ERROR("Failed opening detail file %s: %s",
+ data->filename_work, fr_syserror(errno));
+ return 0;
+ }
+
+ /*
+ * The file doesn't exist. Poll for it again.
+ */
+ if (data->work_fd < 0) {
+#ifndef HAVE_GLOB_H
+ return 0;
+#else
+ unsigned int i;
+ int found;
+ time_t chtime;
+ char const *filename;
+ glob_t files;
+
+ DEBUG2("detail (%s): Polling for detail file", data->name);
+
+ memset(&files, 0, sizeof(files));
+ if (glob(data->filename, 0, NULL, &files) != 0) {
+ noop:
+ globfree(&files);
+ return 0;
+ }
+
+ /*
+ * Loop over the glob'd files, looking for the
+ * oldest one.
+ */
+ chtime = 0;
+ found = -1;
+ for (i = 0; i < files.gl_pathc; i++) {
+ if (stat(files.gl_pathv[i], &st) < 0) continue;
+
+ if ((i == 0) || (st.st_ctime < chtime)) {
+ chtime = st.st_ctime;
+ found = i;
+ }
+ }
+
+ if (found < 0) goto noop;
+
+ /*
+ * Rename detail to detail.work
+ */
+ filename = files.gl_pathv[found];
+
+ DEBUG("detail (%s): Renaming %s -> %s", data->name, filename, data->filename_work);
+ if (rename(filename, data->filename_work) < 0) {
+ ERROR("detail (%s): Failed renaming %s to %s: %s",
+ data->name, filename, data->filename_work, fr_syserror(errno));
+ goto noop;
+ }
+
+ globfree(&files); /* Shouldn't be using anything in files now */
+
+ /*
+ * And try to open the filename.
+ */
+ data->work_fd = open(data->filename_work, O_RDWR);
+ if (data->work_fd < 0) {
+ ERROR("detail (%s): Failed opening %s: %s",
+ data->name, data->filename_work, fr_syserror(errno));
+ return 0;
+ }
+#endif
+ } /* else detail.work existed, and we opened it */
+
+ rad_assert(data->vps == NULL);
+ rad_assert(data->fp == NULL);
+
+ data->file_state = STATE_UNLOCKED;
+
+ data->client_ip.af = AF_UNSPEC;
+ data->timestamp = 0;
+ data->offset = data->last_offset = data->timestamp_offset = 0;
+ data->packets = 0;
+ data->tries = 0;
+ data->done_entry = false;
+
+ return 1;
+}
+
+
+/*
+ * FIXME: add a configuration "exit when done" so that the detail
+ * file reader can be used as a one-off tool to update stuff.
+ *
+ * The time sequence for reading from the detail file is:
+ *
+ * t_0 signalled that the server is idle, and we
+ * can read from the detail file.
+ *
+ * t_rtt the packet has been processed successfully,
+ * wait for t_delay to enforce load factor.
+ *
+ * t_rtt + t_delay wait for signal that the server is idle.
+ *
+ */
+static int detail_recv(rad_listen_t *listener)
+{
+ char c = 0;
+ ssize_t rcode;
+ RADIUS_PACKET *packet;
+ listen_detail_t *data = listener->data;
+ RAD_REQUEST_FUNP fun = NULL;
+
+ /*
+ * Block until there's a packet ready.
+ */
+ rcode = read(data->master_pipe[0], &packet, sizeof(packet));
+ if (rcode <= 0) return rcode;
+
+ rad_assert(packet != NULL);
+
+ switch (packet->code) {
+ case FR_CODE_ACCOUNTING_REQUEST:
+ fun = rad_accounting;
+ break;
+
+ case FR_CODE_COA_REQUEST:
+ case FR_CODE_DISCONNECT_REQUEST:
+ fun = rad_coa_recv;
+ break;
+
+ default:
+ data->entry_state = STATE_REPLIED;
+ goto signal_thread;
+ }
+
+ if (!request_receive(NULL, listener, packet, &data->detail_client, fun)) {
+ data->entry_state = STATE_NO_REPLY; /* try again later */
+
+ signal_thread:
+ fr_radius_free(&packet);
+ if (write(data->child_pipe[1], &c, 1) < 0) {
+ ERROR("detail (%s): Failed writing ack to reader thread: %s", data->name,
+ fr_syserror(errno));
+ }
+ }
+
+ /*
+ * Wait for the child thread to write an answer to the pipe
+ */
+ return 0;
+}
+
+static RADIUS_PACKET *detail_poll(rad_listen_t *listener)
+{
+ char key[256], op[8], value[1024];
+ vp_cursor_t cursor;
+ VALUE_PAIR *vp;
+ RADIUS_PACKET *packet;
+ char buffer[2048];
+ listen_detail_t *data = listener->data;
+
+ switch (data->file_state) {
+ case STATE_UNOPENED:
+open_file:
+ rad_assert(data->work_fd < 0);
+
+ if (!detail_open(listener)) return NULL;
+
+ rad_assert(data->file_state == STATE_UNLOCKED);
+ rad_assert(data->work_fd >= 0);
+
+ /* FALL-THROUGH */
+
+ /*
+ * Try to lock fd. If we can't, return.
+ * If we can, continue. This means that
+ * the server doesn't block while waiting
+ * for the lock to open...
+ */
+ case STATE_UNLOCKED:
+ /*
+ * Note that we do NOT block waiting for
+ * the lock. We've re-named the file
+ * above, so we've already guaranteed
+ * that any *new* detail writer will not
+ * be opening this file. The only
+ * purpose of the lock is to catch a race
+ * condition where the execution
+ * "ping-pongs" between radiusd &
+ * radrelay.
+ */
+ if (rad_lockfd_nonblock(data->work_fd, 0) < 0) {
+ /*
+ * Close the FD. The main loop
+ * will wake up in a second and
+ * try again.
+ */
+ close(data->work_fd);
+ data->fp = NULL;
+ data->work_fd = -1;
+ data->file_state = STATE_UNOPENED;
+ return NULL;
+ }
+
+ /*
+ * Only open for writing if we're
+ * marking requests as completed.
+ */
+ data->fp = fdopen(data->work_fd, data->track ? "r+" : "r");
+ if (!data->fp) {
+ ERROR("detail (%s): FATAL: Failed to re-open detail file: %s",
+ data->name, fr_syserror(errno));
+ fr_exit(1);
+ }
+
+ /*
+ * Look for the header
+ */
+ data->file_state = STATE_PROCESSING;
+ data->entry_state = STATE_HEADER;
+ data->delay_time = USEC;
+ data->vps = NULL;
+ break;
+
+ /*
+ * Go to the next switch statement.
+ */
+ case STATE_PROCESSING:
+ break;
+ }
+
+
+ switch (data->entry_state) {
+ case STATE_HEADER:
+ do_header:
+ data->done_entry = false;
+ data->timestamp_offset = 0;
+
+ data->tries = 0;
+ if (!data->fp) {
+ data->file_state = STATE_UNOPENED;
+ goto open_file;
+ }
+
+ {
+ struct stat buf;
+
+ if (fstat(data->work_fd, &buf) < 0) {
+ ERROR("detail (%s): Failed to stat detail file: %s",
+ data->name, fr_syserror(errno));
+
+ goto cleanup;
+ }
+ if (((off_t) ftell(data->fp)) == buf.st_size) { //-V595
+ goto cleanup;
+ }
+ }
+
+ /*
+ * End of file. Delete it, and re-set
+ * everything.
+ */
+ if (feof(data->fp)) {
+ cleanup:
+ DEBUG("detail (%s): Unlinking %s", data->name, data->filename_work);
+ unlink(data->filename_work);
+ if (data->fp) fclose(data->fp);
+ data->fp = NULL;
+ data->work_fd = -1;
+ data->file_state = STATE_UNOPENED;
+ rad_assert(data->vps == NULL);
+
+ if (data->one_shot) {
+ INFO("detail (%s): Finished reading \"one shot\" detail file - Exiting", data->name);
+ radius_signal_self(RADIUS_SIGNAL_SELF_EXIT);
+ }
+
+ return NULL;
+ }
+
+ /*
+ * Else go read something.
+ */
+ break;
+
+ /*
+ * Read more value-pair's, unless we're
+ * at EOF. In that case, queue whatever
+ * we have.
+ */
+ case STATE_VPS:
+ if (data->fp && !feof(data->fp)) break;
+ data->entry_state = STATE_QUEUED;
+
+ /* FALL-THROUGH */
+
+ case STATE_QUEUED:
+ goto alloc_packet;
+
+ /*
+ * Periodically check what's going on.
+ * If the request is taking too long,
+ * retry it.
+ */
+ case STATE_RUNNING:
+ if (time(NULL) < (data->running + (int)data->retry_interval)) {
+ return NULL;
+ }
+
+ DEBUG("detail (%s): No response to detail request. Retrying", data->name);
+ /* FALL-THROUGH */
+
+ /*
+ * If there's no reply, keep
+ * retransmitting the current packet
+ * forever.
+ */
+ case STATE_NO_REPLY:
+ data->entry_state = STATE_QUEUED;
+ goto alloc_packet;
+
+ /*
+ * We have a reply. Clean up the old
+ * request, and go read another one.
+ */
+ case STATE_REPLIED:
+ if (data->track) {
+ rad_assert(data->fp != NULL);
+
+ if (fseek(data->fp, data->timestamp_offset, SEEK_SET) < 0) {
+ WARN("detail (%s): Failed seeking to timestamp offset: %s",
+ data->name, fr_syserror(errno));
+ } else if (fwrite("\tDone", 1, 5, data->fp) < 5) {
+ WARN("detail (%s): Failed marking request as done: %s",
+ data->name, fr_syserror(errno));
+ } else if (fflush(data->fp) != 0) {
+ WARN("detail (%s): Failed flushing marked detail file to disk: %s",
+ data->name, fr_syserror(errno));
+ }
+
+ if (fseek(data->fp, data->offset, SEEK_SET) < 0) {
+ WARN("detail (%s): Failed seeking to next detail request: %s",
+ data->name, fr_syserror(errno));
+ }
+ }
+
+ fr_pair_list_free(&data->vps);
+ data->entry_state = STATE_HEADER;
+ goto do_header;
+ }
+
+ fr_pair_cursor_init(&cursor, &data->vps);
+
+ /*
+ * Read a header, OR a value-pair.
+ */
+ while (fgets(buffer, sizeof(buffer), data->fp)) {
+ data->last_offset = data->offset;
+ data->offset = ftell(data->fp); /* for statistics */
+
+ /*
+ * Badly formatted file: delete it.
+ *
+ * FIXME: Maybe flag an error?
+ */
+ if (!strchr(buffer, '\n')) {
+ fr_pair_list_free(&data->vps);
+ goto cleanup;
+ }
+
+ /*
+ * We're reading VP's, and got a blank line.
+ * Queue the packet.
+ */
+ if ((data->entry_state == STATE_VPS) &&
+ (buffer[0] == '\n')) {
+ data->entry_state = STATE_QUEUED;
+ break;
+ }
+
+ /*
+ * Look for date/time header, and read VP's if
+ * found. If not, keep reading lines until we
+ * find one.
+ */
+ if (data->entry_state == STATE_HEADER) {
+ int y;
+
+ if (sscanf(buffer, "%*s %*s %*d %*d:%*d:%*d %d", &y)) {
+ data->entry_state = STATE_VPS;
+ }
+ continue;
+ }
+
+ /*
+ * We have a full "attribute = value" line.
+ * If it doesn't look reasonable, skip it.
+ *
+ * FIXME: print an error for badly formatted attributes?
+ */
+ if (sscanf(buffer, "%255s %7s %1023s", key, op, value) != 3) {
+ WARN("detail (%s): Skipping badly formatted line %s", data->name, buffer);
+ continue;
+ }
+
+ /*
+ * Should be =, :=, +=, ...
+ */
+ if (!strchr(op, '=')) {
+ WARN("detail (%s): Skipping line without operator - %s", data->name, buffer);
+ continue;
+ }
+
+ /*
+ * Skip non-protocol attributes.
+ */
+ if (!strcasecmp(key, "Request-Authenticator")) continue;
+
+ /*
+ * Set the original client IP address, based on
+ * what's in the detail file.
+ *
+ * Hmm... we don't set the server IP address.
+ * or port. Oh well.
+ */
+ if (!strcasecmp(key, "Client-IP-Address")) {
+ data->client_ip.af = AF_INET;
+ if (fr_inet_hton(&data->client_ip, AF_INET, value, false) < 0) {
+ ERROR("detail (%s): Failed parsing Client-IP-Address", data->name);
+
+ fr_pair_list_free(&data->vps);
+ goto cleanup;
+ }
+ continue;
+ }
+
+ /*
+ * The original time at which we received the
+ * packet. We need this to properly calculate
+ * Acct-Delay-Time.
+ */
+ if (!strcasecmp(key, "Timestamp")) {
+ data->timestamp = atoi(value);
+ data->timestamp_offset = data->last_offset;
+
+ vp = fr_pair_afrom_num(data, 0, FR_PACKET_ORIGINAL_TIMESTAMP);
+ if (vp) {
+ vp->vp_date = (uint32_t) data->timestamp;
+ vp->type = VT_DATA;
+ fr_pair_cursor_append(&cursor, vp);
+ }
+ continue;
+ }
+
+ if (!strcasecmp(key, "Donestamp")) {
+ data->timestamp = atoi(value);
+ data->done_entry = true;
+ continue;
+ }
+
+ DEBUG3("detail (%s): Trying to read VP from line - %s", data->name, buffer);
+
+ /*
+ * Read one VP.
+ *
+ * FIXME: do we want to check for non-protocol
+ * attributes like radsqlrelay does?
+ */
+ vp = NULL;
+ if ((fr_pair_list_afrom_str(data, buffer, &vp) > 0) &&
+ (vp != NULL)) {
+ fr_pair_cursor_merge(&cursor, vp);
+ } else {
+ WARN("detail (%s): Failed reading VP from line - %s", data->name, buffer);
+ }
+ }
+
+ /*
+ * Some kind of error.
+ *
+ * FIXME: Leave the file in-place, and warn the
+ * administrator?
+ */
+ if (ferror(data->fp)) goto cleanup;
+
+ data->tries = 0;
+ data->packets++;
+
+ /*
+ * Process the packet.
+ */
+ alloc_packet:
+ if (data->done_entry) {
+ DEBUG2("detail (%s): Skipping record for timestamp %lu", data->name, data->timestamp);
+ fr_pair_list_free(&data->vps);
+ data->entry_state = STATE_HEADER;
+ goto do_header;
+ }
+
+ data->tries++;
+
+ /*
+ * The writer doesn't check that the record was
+ * completely written. If the disk is full, this can
+ * result in a truncated record. When that happens,
+ * treat it as EOF.
+ */
+ if (data->entry_state != STATE_QUEUED) {
+ ERROR("detail (%s): Truncated record: treating it as EOF for detail file %s",
+ data->name, data->filename_work);
+ fr_pair_list_free(&data->vps);
+ goto cleanup;
+ }
+
+ /*
+ * We're done reading the file, but we didn't read
+ * anything. Clean up, and don't return anything.
+ */
+ if (!data->vps) {
+ WARN("detail (%s): Read empty packet from file %s",
+ data->name, data->filename_work);
+ data->entry_state = STATE_HEADER;
+ if (!data->fp || feof(data->fp)) goto cleanup;
+ return NULL;
+ }
+
+ /*
+ * Allocate the packet. If we fail, it's a serious
+ * problem.
+ */
+ packet = fr_radius_alloc(NULL, true);
+ if (!packet) {
+ ERROR("detail (%s): FATAL: Failed allocating memory for detail", data->name);
+ fr_exit(1);
+ }
+
+ memset(packet, 0, sizeof(*packet));
+ packet->sockfd = -1;
+ packet->src_ipaddr.af = AF_INET;
+ packet->src_ipaddr.addr.v4.s_addr = htonl(INADDR_NONE);
+
+ /*
+ * If everything's OK, this is a waste of memory.
+ * Otherwise, it lets us re-send the original packet
+ * contents, unmolested.
+ */
+ packet->vps = fr_pair_list_copy(packet, data->vps);
+
+ packet->code = FR_CODE_ACCOUNTING_REQUEST;
+ vp = fr_pair_find_by_num(packet->vps, 0, FR_PACKET_TYPE, TAG_ANY);
+ if (vp) packet->code = vp->vp_uint32;
+
+ gettimeofday(&packet->timestamp, NULL);
+
+ /*
+ * Remember where it came from, so that we don't
+ * proxy it to the place it came from...
+ */
+ if (data->client_ip.af != AF_UNSPEC) {
+ packet->src_ipaddr = data->client_ip;
+ }
+
+ vp = fr_pair_find_by_num(packet->vps, 0, FR_PACKET_SRC_IP_ADDRESS, TAG_ANY);
+ if (vp) {
+ packet->src_ipaddr.af = AF_INET;
+ packet->src_ipaddr.addr.v4.s_addr = vp->vp_ipv4addr;
+ packet->src_ipaddr.prefix = 32;
+ } else {
+ vp = fr_pair_find_by_num(packet->vps, 0, FR_PACKET_SRC_IPV6_ADDRESS, TAG_ANY);
+ if (vp) {
+ packet->src_ipaddr.af = AF_INET6;
+ memcpy(&packet->src_ipaddr.addr.v6,
+ &vp->vp_ipv6addr, sizeof(vp->vp_ipv6addr));
+ packet->src_ipaddr.prefix = 128;
+ }
+ }
+
+ vp = fr_pair_find_by_num(packet->vps, 0, FR_PACKET_DST_IP_ADDRESS, TAG_ANY);
+ if (vp) {
+ packet->dst_ipaddr.af = AF_INET;
+ packet->dst_ipaddr.addr.v4.s_addr = vp->vp_ipv4addr;
+ packet->dst_ipaddr.prefix = 32;
+ } else {
+ vp = fr_pair_find_by_num(packet->vps, 0, FR_PACKET_DST_IPV6_ADDRESS, TAG_ANY);
+ if (vp) {
+ packet->dst_ipaddr.af = AF_INET6;
+ memcpy(&packet->dst_ipaddr.addr.v6,
+ &vp->vp_ipv6addr, sizeof(vp->vp_ipv6addr));
+ packet->dst_ipaddr.prefix = 128;
+ }
+ }
+
+ /*
+ * Generate packet ID, ports, IP via a counter.
+ */
+ packet->id = data->counter & 0xff;
+ packet->src_port = 1024 + ((data->counter >> 8) & 0xff);
+ packet->dst_port = 1024 + ((data->counter >> 16) & 0xff);
+
+ packet->dst_ipaddr.af = AF_INET;
+ packet->dst_ipaddr.addr.v4.s_addr = htonl((INADDR_LOOPBACK & ~0xffffff) | ((data->counter >> 24) & 0xff));
+
+ /*
+ * Create / update accounting attributes.
+ */
+ if (packet->code == FR_CODE_ACCOUNTING_REQUEST) {
+ /*
+ * Prefer the Event-Timestamp in the packet, if it
+ * exists. That is when the event occurred, whereas the
+ * "Timestamp" field is when we wrote the packet to the
+ * detail file, which could have been much later.
+ */
+ vp = fr_pair_find_by_num(packet->vps, 0, FR_EVENT_TIMESTAMP, TAG_ANY);
+ if (vp) {
+ data->timestamp = vp->vp_uint32;
+ }
+
+ /*
+ * Look for Acct-Delay-Time, and update
+ * based on Acct-Delay-Time += (time(NULL) - timestamp)
+ */
+ vp = fr_pair_find_by_num(packet->vps, 0, FR_ACCT_DELAY_TIME, TAG_ANY);
+ if (!vp) {
+ vp = fr_pair_afrom_num(packet, 0, FR_ACCT_DELAY_TIME);
+ rad_assert(vp != NULL);
+ fr_pair_add(&packet->vps, vp);
+ }
+ if (data->timestamp != 0) {
+ vp->vp_uint32 += time(NULL) - data->timestamp;
+ }
+ }
+
+ /*
+ * Set the transmission count.
+ */
+ vp = fr_pair_find_by_num(packet->vps, 0, FR_PACKET_TRANSMIT_COUNTER, TAG_ANY);
+ if (!vp) {
+ vp = fr_pair_afrom_num(packet, 0, FR_PACKET_TRANSMIT_COUNTER);
+ rad_assert(vp != NULL);
+ fr_pair_add(&packet->vps, vp);
+ }
+ vp->vp_uint32 = data->tries;
+
+ data->entry_state = STATE_RUNNING;
+ data->running = packet->timestamp.tv_sec;
+
+ return packet;
+}
+
+/*
+ * Free detail-specific stuff.
+ */
+static int _detail_free(listen_detail_t *data)
+{
+ if (!check_config) {
+ ssize_t ret;
+ void *arg = NULL;
+
+ /*
+ * Mark the child pipes as unusable
+ */
+ close(data->child_pipe[0]);
+ close(data->child_pipe[1]);
+ data->child_pipe[0] = -1;
+
+ /*
+ * Tell it to stop (interrupting its sleep)
+ */
+ pthread_kill(data->pthread_id, SIGTERM);
+
+ /*
+ * Wait for it to acknowledge that it's stopped.
+ */
+ ret = read(data->master_pipe[0], &arg, sizeof(arg));
+ if (ret < 0) {
+ ERROR("detail (%s): Reader thread exited without informing the master: %s",
+ data->name, fr_syserror(errno));
+ } else if (ret != sizeof(arg)) {
+ ERROR("detail (%s): Invalid thread pointer received from reader thread during exit",
+ data->name);
+ ERROR("detail (%s): Expected %zu bytes, got %zi bytes", data->name, sizeof(arg), ret);
+ }
+
+ close(data->master_pipe[0]);
+ close(data->master_pipe[1]);
+
+ if (arg) pthread_join(data->pthread_id, &arg);
+ }
+
+ if (data->fp != NULL) {
+ fclose(data->fp);
+ data->fp = NULL;
+ }
+
+ return 0;
+}
+
+
+static int detail_print(rad_listen_t const *this, char *buffer, size_t bufsize)
+{
+ if (!this->server) {
+ return snprintf(buffer, bufsize, "%s",
+ ((listen_detail_t *)(this->data))->filename);
+ }
+
+ return snprintf(buffer, bufsize, "detail file %s as server %s",
+ ((listen_detail_t *)(this->data))->filename,
+ this->server);
+}
+
+
+/*
+ * Delay while waiting for a file to be ready
+ */
+static int detail_delay(listen_detail_t *data)
+{
+ int delay = (data->poll_interval - 1) * USEC;
+
+ /*
+ * Add +/- 0.25s of jitter
+ */
+ delay += (USEC * 3) / 4;
+ delay += fr_rand() % (USEC / 2);
+
+ DEBUG2("detail (%s): Detail listener state %s waiting %d.%06d sec",
+ data->name,
+ fr_int2str(state_names, data->entry_state, "?"),
+ (delay / USEC), delay % USEC);
+
+ return delay;
+}
+
+static int detail_encode(rad_listen_t *this, REQUEST *request)
+{
+ listen_detail_t *data = this->data;
+
+ RDEBUG2("detail (%s): Finished %s packet", data->name,
+ fr_packet_codes[request->packet->code]);
+
+ return 0;
+}
+
+static int detail_decode(rad_listen_t *this, REQUEST *request)
+{
+ listen_detail_t *data = this->data;
+
+ if (DEBUG_ENABLED2) {
+ RDEBUG2("detail (%s): Read %s packet from %s", data->name,
+ fr_packet_codes[request->packet->code], data->filename_work);
+ rdebug_pair_list(L_DBG_LVL_1, request, request->packet->vps, NULL);
+ }
+
+ return 0;
+}
+
+
+static void *detail_handler_thread(void *arg)
+{
+ char c;
+ rad_listen_t *this = arg;
+ listen_detail_t *data = this->data;
+
+ while (true) {
+ RADIUS_PACKET *packet;
+
+ while ((packet = detail_poll(this)) == NULL) {
+ usleep(detail_delay(data));
+
+ /*
+ * If we're supposed to exit then tell
+ * the master thread we've exited.
+ */
+ if (data->child_pipe[0] < 0) {
+ packet = NULL;
+ if (write(data->master_pipe[1], &packet, sizeof(packet)) < 0) {
+ ERROR("detail (%s): Failed writing exit status to master: %s",
+ data->name, fr_syserror(errno));
+ }
+ return NULL;
+ }
+ }
+
+ /*
+ * Keep retrying forever.
+ *
+ * FIXME: cap the retries.
+ */
+ do {
+ if (write(data->master_pipe[1], &packet, sizeof(packet)) < 0) {
+ ERROR("detail (%s): Failed passing detail packet pointer to master: %s",
+ data->name, fr_syserror(errno));
+ }
+
+ if (read(data->child_pipe[0], &c, 1) < 0) {
+ ERROR("detail (%s): Failed getting detail packet ack from master: %s",
+ data->name, fr_syserror(errno));
+ break;
+ }
+
+ if (data->delay_time > 0) usleep(data->delay_time);
+
+ packet = detail_poll(this);
+ if (!packet) break;
+ } while (data->entry_state != STATE_REPLIED);
+ }
+
+ return NULL;
+}
+
+
+static const CONF_PARSER detail_config[] = {
+ { FR_CONF_OFFSET("detail", FR_TYPE_FILE_OUTPUT | FR_TYPE_DEPRECATED, listen_detail_t, filename) },
+ { FR_CONF_OFFSET("filename", FR_TYPE_FILE_OUTPUT | FR_TYPE_REQUIRED, listen_detail_t, filename) },
+ { FR_CONF_OFFSET("load_factor", FR_TYPE_UINT32, listen_detail_t, load_factor), .dflt = STRINGIFY(10) },
+ { FR_CONF_OFFSET("poll_interval", FR_TYPE_UINT32, listen_detail_t, poll_interval), .dflt = STRINGIFY(1) },
+ { FR_CONF_OFFSET("retry_interval", FR_TYPE_UINT32, listen_detail_t, retry_interval), .dflt = STRINGIFY(30) },
+ { FR_CONF_OFFSET("one_shot", FR_TYPE_BOOL, listen_detail_t, one_shot), .dflt = "no" },
+ { FR_CONF_OFFSET("track", FR_TYPE_BOOL, listen_detail_t, track), .dflt = "no" },
+ CONF_PARSER_TERMINATOR
+};
+
+/*
+ * Parse a detail section.
+ */
+static int detail_parse(CONF_SECTION *cs, rad_listen_t *this)
+{
+ int rcode;
+ listen_detail_t *data;
+ RADCLIENT *client;
+ char buffer[2048];
+
+ data = this->data;
+
+ if (cf_section_rules_push(cs, detail_config) < 0) return -1;
+
+ rcode = cf_section_parse(data, data, cs);
+ if (rcode < 0) {
+ cf_log_err(cs, "Failed parsing listen section");
+ return -1;
+ }
+
+ data->name = cf_section_name2(cs);
+ if (!data->name) data->name = data->filename;
+
+ /*
+ * We don't do duplicate detection for "detail" sockets.
+ */
+ this->nodup = true;
+
+ if (!data->filename) {
+ cf_log_err(cs, "No detail file specified in listen section");
+ return -1;
+ }
+
+ FR_INTEGER_BOUND_CHECK("load_factor", data->load_factor, >=, 1);
+ FR_INTEGER_BOUND_CHECK("load_factor", data->load_factor, <=, 100);
+
+ FR_INTEGER_BOUND_CHECK("poll_interval", data->poll_interval, >=, 1);
+ FR_INTEGER_BOUND_CHECK("poll_interval", data->poll_interval, <=, 60);
+
+ FR_INTEGER_BOUND_CHECK("retry_interval", data->retry_interval, >=, 4);
+ FR_INTEGER_BOUND_CHECK("retry_interval", data->retry_interval, <=, 3600);
+
+ /*
+ * Only checking the config. Don't start threads or anything else.
+ */
+ if (check_config) return 0;
+
+ /*
+ * If the filename is a glob, use "detail.work" as the
+ * work file name.
+ */
+ if ((strchr(data->filename, '*') != NULL) ||
+ (strchr(data->filename, '[') != NULL)) {
+ char *p;
+
+#ifndef HAVE_GLOB_H
+ WARN("detail (%s): File \"%s\" appears to use file globbing, but it is not supported on this system",
+ data->name, data->filename);
+#endif
+ strlcpy(buffer, data->filename, sizeof(buffer));
+ p = strrchr(buffer, FR_DIR_SEP);
+ if (p) {
+ p[1] = '\0';
+ } else {
+ buffer[0] = '\0';
+ }
+
+ /*
+ * Globbing cannot be done across directories.
+ */
+ if ((strchr(buffer, '*') != NULL) ||
+ (strchr(buffer, '[') != NULL)) {
+ cf_log_err(cs, "Wildcard directories are not supported");
+ return -1;
+ }
+
+ strlcat(buffer, "detail.work",
+ sizeof(buffer) - strlen(buffer));
+
+ } else {
+ snprintf(buffer, sizeof(buffer), "%s.work", data->filename);
+ }
+
+ data->filename_work = talloc_strdup(data, buffer);
+
+ data->work_fd = -1;
+ data->vps = NULL;
+ data->fp = NULL;
+ data->file_state = STATE_UNOPENED;
+ data->entry_state = STATE_HEADER;
+ data->delay_time = data->poll_interval * USEC;
+ data->signal = 1;
+
+ /*
+ * Initialize the fake client.
+ */
+ client = &data->detail_client;
+ memset(client, 0, sizeof(*client));
+ client->ipaddr.af = AF_INET;
+ client->ipaddr.addr.v4.s_addr = INADDR_NONE;
+ client->ipaddr.prefix = 0;
+ client->longname = client->shortname = data->filename;
+ client->secret = client->shortname;
+ client->nas_type = talloc_strdup(data, "none"); /* Part of 'data' not dynamically allocated */
+
+ this->server_cs = cf_item_to_section(cf_parent(this->cs));
+ client->server_cs = this->server_cs;
+
+ return 0;
+}
+
+/*
+ * Open detail files
+ */
+static int detail_socket_open(UNUSED CONF_SECTION *cs, rad_listen_t *this)
+{
+ listen_detail_t *data;
+
+ data = this->data;
+ talloc_set_destructor(data, _detail_free);
+
+ /*
+ * Create the communication pipes.
+ */
+ if (pipe(data->master_pipe) < 0) {
+ ERROR("detail (%s): Error opening internal pipe: %s", data->name, fr_syserror(errno));
+ fr_exit(1);
+ }
+
+ if (pipe(data->child_pipe) < 0) {
+ ERROR("detail (%s): Error opening internal pipe: %s", data->name, fr_syserror(errno));
+ fr_exit(1);
+ }
+
+ if (pthread_create(&data->pthread_id, NULL, detail_handler_thread, this) != 0) {
+ ERROR("detail (%s): Error creating detail reader thread: %s", data->name, fr_syserror(errno));
+ fr_exit(1);
+ }
+
+ this->fd = data->master_pipe[0];
+
+ return 0;
+}
+
+extern rad_protocol_t proto_detail;
+rad_protocol_t proto_detail = {
+ .magic = RLM_MODULE_INIT,
+ .name = "detail",
+ .inst_size = sizeof(listen_detail_t),
+ .tls = false,
+ .parse = detail_parse,
+ .open = detail_socket_open,
+ .recv = detail_recv,
+ .send = detail_send,
+ .print = detail_print,
+ .debug = common_packet_debug,
+ .encode = detail_encode,
+ .decode = detail_decode
+};
--- /dev/null
+/*
+ * 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
+ */
+#ifndef _FR_DETAIL_H
+#define _FR_DETAIL_H
+/**
+ * $Id$
+ *
+ * @file proto_detail_file.h
+ * @brief API to deserialise packets in detail file format and inject them into the server.
+ *
+ * @copyright 2015 The FreeRADIUS server project
+ */
+RCSIDH(detail_h, "$Id$")
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum detail_file_state_t {
+ STATE_UNOPENED = 0,
+ STATE_UNLOCKED,
+ STATE_PROCESSING,
+} detail_file_state_t;
+
+typedef enum detail_entry_state_t {
+ STATE_HEADER = 0,
+ STATE_VPS,
+ STATE_QUEUED,
+ STATE_RUNNING,
+ STATE_NO_REPLY,
+ STATE_REPLIED
+} detail_entry_state_t;
+
+typedef struct listen_detail_t {
+ fr_event_timer_t const *ev; /* has to be first entry (ugh) */
+ char const *name; //!< Identifier used in log messages
+ int delay_time;
+ char const *filename;
+ char const *filename_work;
+ VALUE_PAIR *vps;
+ int work_fd;
+
+ int master_pipe[2];
+ int child_pipe[2];
+ pthread_t pthread_id;
+
+ FILE *fp;
+ off_t offset;
+ detail_file_state_t file_state;
+ detail_entry_state_t entry_state;
+ time_t timestamp;
+ time_t running;
+ fr_ipaddr_t client_ip;
+
+ off_t last_offset;
+ off_t timestamp_offset;
+ bool done_entry; //!< Are we done reading this entry?
+ bool track; //!< Do we track progress through the file?
+
+ uint32_t load_factor; /* 1..100 */
+ uint32_t poll_interval;
+ uint32_t retry_interval;
+
+ int signal;
+ int packets;
+ int tries;
+ bool one_shot;
+ int outstanding;
+ int has_rtt;
+ int srtt;
+ int rttvar;
+ uint32_t counter;
+ struct timeval last_packet;
+ RADCLIENT detail_client;
+} listen_detail_t;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _FR_DETAIL_H */
--- /dev/null
+/*
+ * 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 dynamic_clients.c
+ * @brief Track dynamic clients
+ *
+ * @copyright 2018 The FreeRADIUS server project.
+ * @copyright 2018 Alan DeKok (aland@deployingradius.com)
+ */
+#include <netdb.h>
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/protocol.h>
+#include <freeradius-devel/udp.h>
+#include <freeradius-devel/radius/radius.h>
+#include <freeradius-devel/io/io.h>
+#include <freeradius-devel/io/application.h>
+#include <freeradius-devel/io/track.h>
+#include <freeradius-devel/io/listen.h>
+#include <freeradius-devel/io/schedule.h>
+#include <freeradius-devel/rad_assert.h>
+#include "proto_radius.h"
+
+typedef struct dynamic_client_t {
+ dl_instance_t *submodule; //!< proto_radius_dynamic_client
+ fr_ipaddr_t *network; //!< dynamic networks to allow
+
+ RADCLIENT_LIST *clients; //!< local clients
+ RADCLIENT_LIST *expired; //!< expired local clients
+
+ fr_dlist_t packets; //!< list of accepted packets
+ fr_dlist_t pending; //!< pending clients
+
+ uint32_t max_clients; //!< maximum number of dynamic clients
+ uint32_t num_clients; //!< total number of active clients
+ uint32_t max_pending_clients; //!< maximum number of pending clients
+ uint32_t num_pending_clients; //!< number of pending clients
+ uint32_t max_pending_packets; //!< maximum accepted pending packets
+ uint32_t num_pending_packets; //!< how many packets are received, but not accepted
+
+ uint32_t lifetime; //!< of the dynamic client, in seconds.
+} dynamic_client_t;
+
+typedef struct dynamic_packet_t {
+ uint8_t *packet;
+ fr_tracking_entry_t *track;
+ fr_dlist_t entry;
+} dynamic_packet_t;
+
+static const CONF_PARSER dynamic_client_config[] = {
+ { FR_CONF_OFFSET("network", FR_TYPE_COMBO_IP_PREFIX | FR_TYPE_MULTI, dynamic_client_t, network) },
+
+ { FR_CONF_OFFSET("max_clients", FR_TYPE_UINT32, dynamic_client_t, max_clients), .dflt = "65536" },
+ { FR_CONF_OFFSET("max_pending_clients", FR_TYPE_UINT32, dynamic_client_t, max_pending_clients), .dflt = "256" },
+ { FR_CONF_OFFSET("max_pending_packets", FR_TYPE_UINT32, dynamic_client_t, max_pending_packets), .dflt = "65536" },
+
+ { FR_CONF_OFFSET("lifetime", FR_TYPE_UINT32, dynamic_client_t, lifetime), .dflt = "600" },
+
+ CONF_PARSER_TERMINATOR
+};
--- /dev/null
+#!/usr/bin/env perl
+
+@array = ( "max_connections", "max_clients", "max_pending_packets", "num_connections", "num_clients",
+ "num_pending_packets", "cleanup_delay", "idle_timeout", "nak_lifetime", "check_interval",
+ "dynamic_clients", "listen", "sc", "ipproto", "transport", "el", "nr", "trie",
+ "networks", "pending_clients" );
+
+while (<>) {
+ for my $i (0 .. $#array) {
+ s/inst->$array[$i]/inst->io.$array[$i]/g;
+ s/proto_radius_t, $array[$i]/proto_radius_t, io.$array[$i]/g;
+ }
+
+ print;
+}
--- /dev/null
+# TO DO
+
+* add proto_radius_connection_io as mostly a clone of master_io
+ * which lets us get rid of the horrible `get_inst` hack.
+ * We can then either split all functions into 2 which is code duplication...
+ * or, have each function call underlying one with both inst && connection...
+
+* REQUIRED doesn't work for "network"?
+ * src/main/cf_parse.c[884]: Configuration item "network" must have a value
+ * that's a TERRIBLE message. it should be a fake filename? <internal>...
+
+* add "new connection" method to sites-available/default ?
+ * to separate it from new client?
+
+## fr_io_instance_t
+
+* app_io_private
+ * connection set - should just move to app_io function callback
+ * network_get is run in proto_radius
+ * should be called from io bootstrap function
+
+* add mod_bootstrap to io.c
+ * and then grabs network information using public API
+* add mod_instantiate to io.c
+ * io.c calls app_io->instantiate
+ * and creates the client trie
+* add mod_open to io.c
+ * io.c makes the listener
+* move non-proto_radius headers to src/lib/io.h
+* move io.c to src/lib/io/io.c
+* and have it work!
+
+Things which need to be abstracted
+
+* priorities
+* process by code
+ * probably just process set?
+ * or abstract a way for the app method to know which packets it should accept.
+* cleanup delay
+ * for access-request. Not everything needs this
+* connection set
+
+## proto_radius uses
+
+* code_allowed
+* process_by_code
+ * can probably be a callback
+
+* app_io for decode / encode
+ * app_io_instance
+
+* max_packet_size
+* num_messages
+
+## TO DO
+
+* move bootstrap / instantiate of app_io to io.c
+ * and add a bootstrap / instantiate method there
+ * have io.c initialize all of the stuff it uses
+* have fr_io_instance_t public
+ * so that proto_radius can initialize some things...
+ * have TALLOC_CTX at the start, so that it can clean things up
+ * for parsing simplicity, the struct should just be inline in proto_radius_t
--- /dev/null
+ Let me recap the history of this document. There's a lot to take
+in, so I'll present concerns in point form. First, the document.
+Second, engagement with the WG. Third, reviews. Fourth, process.
+And finally steps to take to address the ongoing issues.
+
+ First, the document.
+
+* my "Security Considerations" text was first plagarised in draft-06,
+
+* when I pointed this out (May 9, 2017), there was no reply to my
+ message by the authors, and no change was made to the document.
+
+* the ADs did respond, and indicated that they had talked to the
+ authors about the issue, and that it was a simple misunderstanding
+ and would be fixed.
+
+* A year later, I raised the issue again (March 2018). There as no
+ reply to my concerns by the authors, and no change was made to the
+ document.
+
+* in all, 4 separate revisions of the document plagarized my text, for
+ over a year, sometimes with minor edits, despite repeated requests
+ to address the issue.
+
+ Those issues alone are surprising.
+
+* The text which was good enough to plagarize was then claimed to have
+ deficiencies
+
+* no one in the WG had noted any technical issues with the text
+
+* the only issues were with attribution, not with the text in the
+ Security Considerations section
+
+* there is now a -10, which has essentially the same points as the
+ previous text, just reworded
+
+ I should point out that the RFC process is supposed to be about
+content, not authorship. There are many RFCs issued with text written
+by multiple people. Where the authors cannot all be acknowledged on
+the first page, the primary editor can be listed with the (Ed.)
+suffix, to indicate editorship. Other authors can be named as authors
+on the first page, or in the Acknowledgements section.
+
+ Second, concerns with engagement with the WG. It continues.
+
+* multiple people in the WG have requested the authors engage with the
+ Working Group. Most notably, many messages in May 2017.
+
+* multiple people in the WG have requested the authors explain what's
+ changed in each new revision, or perhaps to acknowlege comments and
+ reviews (May 2017 again, among other times).
+
+* This engagement has been minimal, despite multiple revisions of
+ the document being published after these WG requests.
+
+* new revisions have most often been "thrown over the wall" with
+ minimal (or no) explanation as to what changed, and why.
+
+* this new draft is no different, i.e. it "revised the security
+ section". Why? How? What changed? What were any alleged
+ "deficiencies"?
+
+* the author have stated again that they "will endeavor to be much
+ more reactive to comments".
+
+* this statement or similar ones have been made repeatedly, with
+ little change in observable behavior.
+
+* The authors request (again) that the WG review this new document
+ wholesale.
+
+ Speaking of reviews, let's continue with third, responses to
+reviews.
+
+* I have given multiple line-by-line reviews of the entire document,
+ for multiple revisions of the document.
+
+* as noted above, these reviews have generally been ignored.
+
+* as noted above, new versions of the document have appeared which may
+ or may not have addressed these reviews.
+
+* Given the lack of feedback on the reviews before a new draft is
+ issued, it is unclear whether the review comments have been
+ addressed.
+
+* due to these issues, I have stopped reviewing the document, as it
+ is not productive.
+
+ There are other issues with the larger community. I'll continue.
+
+* there was broad support for publication of early revisions of the
+ document, despite it clearly not describing the protocol in a way
+ that permits inter-operable implementations
+
+* the people who supported adopting the document as a WG document
+ have generally not reviewed or commented on it
+
+* existing implementors of TACACS+ have not reviewed or commented on
+ the document (Alex Clouter's review was done as part of a brand-new
+ implementation)
+
+* we have therefore no idea whether or not this document describes
+ anything that anyone has implemented
+
+ In order for the document to be published, we should have (at the
+minimum) statements from multiple implementors that the draft matches
+their implementations.
+
+ Fourth, there are process issues, too. I'll continue.
+
+* the IETF has traditionally started a new WG to standardize new
+ management protocols (i.e. a protocol new to the IETF)
+
+* examples include the protocols described in RFC 6632. These
+ protocols have had their own working groups, going back to at least
+ 1996, and continuing to the present
+
+* working on a new (and eventually standards track) document in the
+ OPS area is therefore a new step for the IETF
+
+* I have raised all of the concerns mentioned herein with the chairs
+ and ADs in private email, public email on the list, and in person at
+ multiple meetings
+
+* There does not appear to be any action taken as a result.
+
+* Messages to the list raising these issues have largely been
+ unaddressed
+
+ It is general IETF practice for document authors to interact with
+the WG. And, to respond to WG reviews and comments. See the many
+comments in the OPSAWG archives in May 2017 for broad WG support of
+this position.
+
+ As the authors have largely ignored the WG comments, the chairs and
+ADs should have taken steps to address this issue. As of this
+writing, it appears to be the same "status quo" of the past few years.
+
+ I don't think these comments are unreasonable. If we truly don't
+care about the WG opinion, then the chairs and AD can:
+
+* make a public statement saying that WG feedback can be ignored
+
+* state that that the document can be published in whatever form the
+ authors choose
+
+* publish the document even if it does not describe the protocol in
+ sufficient detail to create inter-operable implementations.
+
+ I don't think that's the right approach, of course. But it's
+largely what's been happening due to silence and/or inaction of the
+parties involved.
+
+ I see no point in further reviewing the document. I again oppose
+publication of this document until such time as all of the WG issues
+have been addressed.
+
+ If we do actually care about what's in the document, then I suggest
+finally the following steps be taken to address the ongoing concerns:
+
+* the document should not be published until such time as multiple
+ implementors have agreed that the specification matches their
+ implementation.
+
+* the chairs and ADs should insist that the authors address the
+ outstanding WG concerns about this document
+
+* the chairs and ADs should insist that authors follow established
+ practice of *interacting* with the WG, including responding to
+ future reviews and comments.
+
+* the chairs and ADs should insist that new revisions of the document
+ are accompanied by an explanation of what changed, and why
+
+* that if the authors do not follow these steps (as they have not so
+ far), that the chairs and ADs should replace them with other WG
+ member(s) who will follow the IETF process.
+
+ Alan DeKok.
--- /dev/null
+/*
+ * 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 proto_vmps_udp.c
+ * @brief VMPS handler for UDP.
+ *
+ * @copyright 2016 The Freeradius server project.
+ * @copyright 2016 Alan DeKok (aland@deployingradius.com)
+ */
+#include <netdb.h>
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/protocol.h>
+#include <freeradius-devel/udp.h>
+#include <freeradius-devel/io/io.h>
+#include <freeradius-devel/io/application.h>
+#include <freeradius-devel/io/listen.h>
+#include <freeradius-devel/rad_assert.h>
+#include "proto_vmps.h"
+
+typedef struct {
+ proto_vmps_t const *parent; //!< The module that spawned us!
+
+ int sockfd;
+
+ fr_ipaddr_t ipaddr; //!< Ipaddr to listen on.
+
+ bool ipaddr_is_set; //!< ipaddr config item is set.
+ bool ipv4addr_is_set; //!< ipv4addr config item is set.
+ bool ipv6addr_is_set; //!< ipv6addr config item is set.
+
+ char const *interface; //!< Interface to bind to.
+ char const *port_name; //!< Name of the port for getservent().
+
+ uint16_t port; //!< Port to listen on.
+ uint32_t recv_buff; //!< How big the kernel's receive buffer should be.
+ bool recv_buff_is_set; //!< Whether we were provided with a receive
+ //!< buffer value.
+} proto_vmps_udp_t;
+
+static const CONF_PARSER udp_listen_config[] = {
+ { FR_CONF_IS_SET_OFFSET("ipaddr", FR_TYPE_COMBO_IP_ADDR, proto_vmps_udp_t, ipaddr) },
+ { FR_CONF_IS_SET_OFFSET("ipv4addr", FR_TYPE_IPV4_ADDR, proto_vmps_udp_t, ipaddr) },
+ { FR_CONF_IS_SET_OFFSET("ipv6addr", FR_TYPE_IPV6_ADDR, proto_vmps_udp_t, ipaddr) },
+
+ { FR_CONF_OFFSET("interface", FR_TYPE_STRING, proto_vmps_udp_t, interface) },
+ { FR_CONF_OFFSET("port_name", FR_TYPE_STRING, proto_vmps_udp_t, port_name) },
+
+ { FR_CONF_OFFSET("port", FR_TYPE_UINT16, proto_vmps_udp_t, port) },
+ { FR_CONF_IS_SET_OFFSET("recv_buff", FR_TYPE_UINT32, proto_vmps_udp_t, recv_buff) },
+
+ CONF_PARSER_TERMINATOR
+};
+
+
+/** Return the src address associated with the packet_ctx
+ *
+ */
+static int mod_src_address(fr_socket_addr_t *src, UNUSED void const *instance, void const *packet_ctx)
+{
+ fr_ip_srcdst_t const *ip = packet_ctx;
+
+ memset(src, 0, sizeof(*src));
+
+ src->proto = IPPROTO_UDP;
+ memcpy(&src->ipaddr, &ip->src_ipaddr, sizeof(src->ipaddr));
+
+ return 0;
+}
+
+/** Return the dst address associated with the packet_ctx
+ *
+ */
+static int mod_dst_address(fr_socket_addr_t *dst, UNUSED void const *instance, void const *packet_ctx)
+{
+ fr_ip_srcdst_t const *ip = packet_ctx;
+
+ memset(dst, 0, sizeof(*dst));
+
+ dst->proto = IPPROTO_UDP;
+ memcpy(&dst->ipaddr, &ip->dst_ipaddr, sizeof(dst->ipaddr));
+
+ return 0;
+}
+
+
+/** Decode the packet.
+ *
+ */
+static int mod_decode(UNUSED void const *instance, UNUSED REQUEST *request, UNUSED uint8_t *const data, UNUSED size_t data_len)
+{
+#if 0
+// proto_vmps_udp_t const *inst = talloc_get_type_abort_const(instance, proto_vmps_udp_t);
+ fr_ip_srcdst_t *ip;
+ uint8_t *packet;
+ size_t packet_len;
+
+ ip = talloc_memdup(request, request->async->packet_ctx, sizeof(*ip));
+ if (!ip) return -1;
+
+ request->async->packet_ctx = ip;
+
+ packet = data + sizeof(*ip);
+ packet_len = data_len - sizeof(*ip);
+
+ // decode the packet into attributes.
+#endif
+
+ return 0;
+}
+
+static ssize_t mod_encode(UNUSED void const *instance, UNUSED REQUEST *request, UNUSED uint8_t *buffer, UNUSED size_t buffer_len)
+{
+#if 0
+// proto_vmps_udp_t const *inst = talloc_get_type_abort_const(instance, proto_vmps_udp_t);
+ fr_ip_srcdst_t *ip;
+ uint8_t *packet;
+ size_t packet_len;
+
+ ip = request->async->packet_ctx;
+ packet = buffer + sizeof(*ip);
+ packet_len = buffer_len - sizeof(*ip);
+
+ memcpy(buffer, ip, sizeof(*ip));
+
+ // encode packet in buffer
+#endif
+
+ return 0;
+}
+
+static ssize_t mod_read(void *instance, void **packet_ctx, fr_time_t **recv_time, uint8_t *buffer, size_t buffer_len, size_t *leftover, uint32_t *priority, bool *is_dup)
+{
+ proto_vmps_udp_t const *inst = talloc_get_type_abort(instance, proto_vmps_udp_t);
+ fr_ip_srcdst_t *ip;
+ uint8_t *packet;
+ size_t packet_len;
+ ssize_t data_size;
+ struct timeval timestamp;
+
+ ip = (fr_ip_srcdst_t *) buffer; /* @todo - should be aligned */
+ packet = buffer + sizeof(*ip);
+ packet_len = buffer_len - sizeof(*ip);
+ *leftover = 0;
+ *is_dup = false;
+
+ data_size = udp_recv(inst->sockfd, packet, packet_len, 0,
+ &ip->src_ipaddr, &ip->src_port,
+ &ip->dst_ipaddr, &ip->dst_port,
+ &ip->if_index, ×tamp);
+ if (data_size <= 0) return data_size;
+
+ packet_len = data_size;
+
+ /*
+ * If it's not a VMPS packet, ignore it.
+ */
+ if (!fr_vqp_ok(packet, &packet_len)) return 0;
+
+ *packet_ctx = ip;
+ *recv_time = NULL;
+ *priority = PRIORITY_NORMAL;
+
+ return packet_len + sizeof(*ip);
+}
+
+static ssize_t mod_write(void *instance, void *packet_ctx,
+ UNUSED fr_time_t request_time, uint8_t *buffer, size_t buffer_len)
+{
+ proto_vmps_udp_t *inst = talloc_get_type_abort(instance, proto_vmps_udp_t);
+ fr_ip_srcdst_t *ip = packet_ctx;
+ uint8_t *packet;
+ size_t packet_len;
+ ssize_t data_size;
+
+ rad_assert(packet_ctx == buffer);
+
+ /*
+ * Don't reply.
+ */
+ if (buffer_len == 1) return buffer_len;
+
+ packet = buffer + sizeof(*ip);
+ packet_len = buffer_len - sizeof(*ip);
+
+
+ /*
+ * Only write replies if they're VMPS packets.
+ * sometimes we want to NOT send a reply...
+ */
+ data_size = udp_send(inst->sockfd, packet, packet_len, 0,
+ &ip->dst_ipaddr, ip->dst_port,
+ ip->if_index,
+ &ip->src_ipaddr, ip->src_port);
+ if (data_size < 0) return data_size;
+
+ /*
+ * Tell the caller we've written it all.
+ */
+ return buffer_len;
+}
+
+/** Open a UDP listener for VMPS
+ *
+ * @param[in] instance of the VMPS UDP I/O path.
+ * @return
+ * - <0 on error
+ * - 0 on success
+ */
+static int mod_open(void *instance)
+{
+ proto_vmps_udp_t *inst = talloc_get_type_abort(instance, proto_vmps_udp_t);
+
+ int sockfd = 0;
+ uint16_t port = inst->port;
+
+ sockfd = fr_socket_server_udp(&inst->ipaddr, &port, inst->port_name, true);
+ if (sockfd < 0) {
+ PERROR("Failed creating UDP socket");
+ error:
+ return -1;
+ }
+
+ if (fr_socket_bind(sockfd, &inst->ipaddr, &port, inst->interface) < 0) {
+ PERROR("Failed binding socket");
+ goto error;
+ }
+
+ inst->sockfd = sockfd;
+
+ return 0;
+}
+
+/** Get the file descriptor for this socket.
+ *
+ * @param[in] instance of the VMPS UDP I/O path.
+ * @return the file descriptor
+ */
+static int mod_fd(void const *instance)
+{
+ proto_vmps_udp_t const *inst = talloc_get_type_abort_const(instance, proto_vmps_udp_t);
+
+ return inst->sockfd;
+}
+
+
+static int mod_instantiate(void *instance, CONF_SECTION *cs)
+{
+ proto_vmps_udp_t *inst = talloc_get_type_abort(instance, proto_vmps_udp_t);
+
+ /*
+ * Default to all IPv6 interfaces (it's the future)
+ */
+ if (!inst->ipaddr_is_set && !inst->ipv4addr_is_set && !inst->ipv6addr_is_set) {
+ inst->ipaddr.af = AF_INET6;
+ inst->ipaddr.prefix = 128;
+ inst->ipaddr.addr.v6 = in6addr_any; /* in6addr_any binds to all addresses */
+ }
+
+ if (inst->recv_buff_is_set) {
+ FR_INTEGER_BOUND_CHECK("recv_buff", inst->recv_buff, >=, 32);
+ FR_INTEGER_BOUND_CHECK("recv_buff", inst->recv_buff, <=, INT_MAX);
+ }
+
+ if (!inst->port) {
+ struct servent *s;
+
+ if (!inst->port_name) {
+ cf_log_err(cs, "No 'port' specified in 'udp' section");
+ return -1;
+ }
+
+ s = getservbyname(inst->port_name, "udp");
+ if (!s) {
+ cf_log_err(cs, "Unknown value for 'port_name = %s", inst->port_name);
+ return -1;
+ }
+
+ inst->port = ntohl(s->s_port);
+ }
+
+ return 0;
+}
+
+static int mod_bootstrap(void *instance, UNUSED CONF_SECTION *cs)
+{
+ proto_vmps_udp_t *inst = talloc_get_type_abort(instance, proto_vmps_udp_t);
+ dl_instance_t const *dl_inst;
+
+ /*
+ * Find the dl_instance_t holding our instance data
+ * so we can find out what the parent of our instance
+ * was.
+ */
+ dl_inst = dl_instance_find(instance);
+ rad_assert(dl_inst);
+
+ inst->parent = talloc_get_type_abort(dl_inst->parent->data, proto_vmps_t);
+
+ return 0;
+}
+
+static int mod_detach(void *instance)
+{
+ proto_vmps_udp_t *inst = talloc_get_type_abort(instance, proto_vmps_udp_t);
+
+ /*
+ * @todo - have our OWN event loop for timers, and a
+ * "copy timer from -> to, which means we only have to
+ * delete our child event loop from the parent on close.
+ */
+
+ close(inst->sockfd);
+ return 0;
+}
+
+
+/** Private interface for use by proto_vmps
+ *
+ */
+extern proto_vmps_app_io_t proto_vmps_app_io_private;
+proto_vmps_app_io_t proto_vmps_app_io_private = {
+ .src = mod_src_address,
+ .dst = mod_dst_address
+};
+
+extern fr_app_io_t proto_vmps_udp;
+fr_app_io_t proto_vmps_udp = {
+ .magic = RLM_MODULE_INIT,
+ .name = "vmps_udp",
+ .config = udp_listen_config,
+ .inst_size = sizeof(proto_vmps_udp_t),
+ .detach = mod_detach,
+ .bootstrap = mod_bootstrap,
+ .instantiate = mod_instantiate,
+
+ .default_message_size = 4096,
+ .open = mod_open,
+ .read = mod_read,
+ .decode = mod_decode,
+ .encode = mod_encode,
+ .write = mod_write,
+ .fd = mod_fd,
+};
request, inst->config.key, NULL, NULL);
if (key_len < 0) return -1;
- slen = tmpl_afrom_attr_substr(ctx, NULL, &target, fmt,
+ slen = tmpl_afrom_attr_substr(ctx, NULL, &target, fmt, -1,
&(vp_tmpl_rules_t){
.dict_def = request->dict,
.prefix = VP_ATTR_REF_PREFIX_AUTO
--- /dev/null
+/*
+ * rlm_eap_tls.c contains the interfaces that are called from eap
+ *
+ * Version: $Id$
+ *
+ * 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
+ *
+ * @copyright 2001 hereUare Communications, Inc. <raghud@hereuare.com>
+ * @copyright 2003 Alan DeKok <aland@freeradius.org>
+ * @copyright 2006 The FreeRADIUS server project
+ *
+ */
+RCSID("$Id$")
+USES_APPLE_DEPRECATED_API /* OpenSSL API has been deprecated by Apple */
+
+#define LOG_PREFIX "rlm_eap_tls - "
+
+#ifdef HAVE_OPENSSL_RAND_H
+# include <openssl/rand.h>
+#endif
+#ifdef HAVE_OPENSSL_EVP_H
+# include <openssl/evp.h>
+#endif
+#ifdef HAVE_SYS_STAT_H
+# include <sys/stat.h>
+#endif
+
+#include <freeradius-devel/unlang/base.h>
+
+#include "rlm_eap_tls.h"
+
+static CONF_PARSER submodule_config[] = {
+ { FR_CONF_OFFSET("tls", FR_TYPE_STRING, rlm_eap_tls_t, tls_conf_name) },
+
+ { FR_CONF_OFFSET("require_client_cert", FR_TYPE_BOOL, rlm_eap_tls_t, req_client_cert), .dflt = "yes" },
+ { FR_CONF_OFFSET("include_length", FR_TYPE_BOOL, rlm_eap_tls_t, include_length), .dflt = "yes" },
+ { FR_CONF_OFFSET("virtual_server", FR_TYPE_STRING, rlm_eap_tls_t, virtual_server) },
+ CONF_PARSER_TERMINATOR
+};
+
+static fr_dict_t *dict_freeradius;
+
+extern fr_dict_autoload_t rlm_eap_tls_dict[];
+fr_dict_autoload_t rlm_eap_tls_dict[] = {
+ { .out = &dict_freeradius, .proto = "freeradius" },
+ { NULL }
+};
+
+static fr_dict_attr_t const *attr_eap_tls_require_client_cert;
+static fr_dict_attr_t const *attr_virtual_server;
+
+extern fr_dict_attr_autoload_t rlm_eap_tls_dict_attr[];
+fr_dict_attr_autoload_t rlm_eap_tls_dict_attr[] = {
+ { .out = &attr_eap_tls_require_client_cert, .name = "EAP-TLS-Require-Client-Cert", .type = FR_TYPE_UINT32, .dict = &dict_freeradius },
+ { .out = &attr_virtual_server, .name = "Virtual-Server", .type = FR_TYPE_STRING, .dict = &dict_freeradius },
+ { NULL }
+};
+
+/*
+ * Do authentication, by letting EAP-TLS do most of the work.
+ */
+static rlm_rcode_t CC_HINT(nonnull) mod_process(void *instance, void *thread, REQUEST *request);
+
+static rlm_rcode_t eap_tls_success_with_prf(eap_session_t *eap_session)
+{
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+ eap_tls_session_t *eap_tls_session = talloc_get_type_abort(eap_session->opaque, eap_tls_session_t);
+ tls_session_t *tls_session = eap_tls_session->tls_session;
+
+ /*
+ * Set the PRF label based on the TLS version negotiated
+ * in the handshake.
+ */
+ switch (SSL_SESSION_get_protocol_version(SSL_get_session(tls_session->ssl))) {
+ case SSL2_VERSION: /* Should never happen */
+ case SSL3_VERSION: /* Should never happen */
+ rad_assert(0);
+ return RLM_MODULE_INVALID;
+
+ case TLS1_VERSION:
+ case TLS1_1_VERSION:
+ case TLS1_2_VERSION:
+#endif
+ {
+ static char const keying_prf_label[] = "client EAP encryption";
+
+ if (eap_tls_success(eap_session,
+ keying_prf_label, sizeof(keying_prf_label) - 1,
+ NULL, 0) < 0) return RLM_MODULE_FAIL;
+ }
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+ break;
+
+ case TLS1_3_VERSION:
+ default:
+ {
+ static char const keying_prf_label[] = "EXPORTER_EAP_TLS_Key_Material";
+ static char const sessid_prf_label[] = "EXPORTER_EAP_TLS_Method-Id";
+
+ if (eap_tls_success(eap_session,
+ keying_prf_label, sizeof(keying_prf_label) - 1,
+ sessid_prf_label, sizeof(sessid_prf_label) - 1) < 0) return RLM_MODULE_FAIL;
+ }
+ break;
+ }
+#endif
+ return RLM_MODULE_OK;
+}
+
+static unlang_action_t eap_tls_virtual_server_result(REQUEST *request, rlm_rcode_t *presult,
+ UNUSED int *priority, void *uctx)
+{
+ eap_session_t *eap_session = talloc_get_type_abort(uctx, eap_session_t);
+
+ switch (*presult) {
+ case RLM_MODULE_OK:
+ case RLM_MODULE_UPDATED:
+ *presult = eap_tls_success_with_prf(eap_session);
+ break;
+
+ default:
+ REDEBUG2("Certificate rejected by the virtual server");
+ eap_tls_fail(eap_session);
+ *presult = RLM_MODULE_REJECT;
+ break;
+ }
+
+ return UNLANG_ACTION_CALCULATE_RESULT;
+}
+
+static rlm_rcode_t eap_tls_virtual_server(rlm_eap_tls_t *inst, eap_session_t *eap_session)
+{
+ REQUEST *request = eap_session->request;
+ CONF_SECTION *server_cs;
+ CONF_SECTION *section;
+ VALUE_PAIR *vp;
+
+ /* set the virtual server to use */
+ vp = fr_pair_find_by_da(request->control, attr_virtual_server, TAG_ANY);
+ if (vp) {
+ server_cs = virtual_server_find(vp->vp_strvalue);
+ if (!server_cs) {
+ REDEBUG2("Virtual server \"%s\" not found", vp->vp_strvalue);
+ error:
+ eap_tls_fail(eap_session);
+ return RLM_MODULE_INVALID;
+ }
+ } else {
+ server_cs = virtual_server_find(inst->virtual_server);
+ rad_assert(server_cs);
+ }
+
+ section = cf_section_find(server_cs, "recv", "Access-Request");
+ if (!section) {
+ REDEBUG2("Failed finding 'recv Access-Request { ... }' section of virtual server %s",
+ cf_section_name2(server_cs));
+ goto error;
+ }
+
+ if (!unlang_section(section)) {
+ REDEBUG("Failed to find pre-compiled unlang for section %s %s { ... }",
+ cf_section_name1(server_cs), cf_section_name2(server_cs));
+ goto error;
+ }
+
+ RDEBUG2("Validating certificate");
+
+ /*
+ * Catch the interpreter on the way back up the stack
+ */
+ unlang_interpret_push_function(request, NULL, eap_tls_virtual_server_result, eap_session);
+
+ /*
+ * Push unlang instructions for the virtual server section
+ */
+ unlang_interpret_push_section(request, section, RLM_MODULE_NOOP, UNLANG_SUB_FRAME);
+
+ return RLM_MODULE_YIELD;
+}
+
+static rlm_rcode_t mod_process(void *instance, UNUSED void *thread, REQUEST *request)
+{
+ eap_tls_status_t status;
+
+ rlm_eap_tls_t *inst = talloc_get_type_abort(instance, rlm_eap_tls_t);
+ eap_session_t *eap_session = eap_session_get(request);
+ eap_tls_session_t *eap_tls_session = talloc_get_type_abort(eap_session->opaque, eap_tls_session_t);
+ tls_session_t *tls_session = eap_tls_session->tls_session;
+
+ status = eap_tls_process(eap_session);
+ if ((status == EAP_TLS_INVALID) || (status == EAP_TLS_FAIL)) {
+ REDEBUG("[eap-tls process] = %s", fr_int2str(eap_tls_status_table, status, "<INVALID>"));
+ } else {
+ RDEBUG2("[eap-tls process] = %s", fr_int2str(eap_tls_status_table, status, "<INVALID>"));
+ }
+
+ switch (status) {
+ /*
+ * EAP-TLS handshake was successful, return an
+ * EAP-TLS-Success packet here.
+ *
+ * If a virtual server was configured, check that
+ * it accepts the certificates, too.
+ */
+ case EAP_TLS_ESTABLISHED:
+ if (inst->virtual_server) return eap_tls_virtual_server(inst, eap_session);
+ return eap_tls_success_with_prf(eap_session);
+
+
+ /*
+ * The TLS code is still working on the TLS
+ * exchange, and it's a valid TLS request.
+ * do nothing.
+ */
+ case EAP_TLS_HANDLED:
+ return RLM_MODULE_HANDLED;
+
+ /*
+ * Handshake is done, proceed with decoding tunneled
+ * data.
+ */
+ case EAP_TLS_RECORD_RECV_COMPLETE:
+ REDEBUG("Received unexpected tunneled data after successful handshake");
+ eap_tls_fail(eap_session);
+
+ return RLM_MODULE_INVALID;
+
+ /*
+ * Anything else: fail.
+ *
+ * Also, remove the session from the cache so that
+ * the client can't re-use it.
+ */
+ default:
+ tls_cache_deny(tls_session);
+
+ return RLM_MODULE_REJECT;
+ }
+}
+
+/*
+ * Send an initial eap-tls request to the peer, using the libeap functions.
+ */
+static rlm_rcode_t mod_session_init(void *instance, UNUSED void *thread, REQUEST *request)
+{
+ rlm_eap_tls_t *inst = talloc_get_type_abort(instance, rlm_eap_tls_t);
+ eap_session_t *eap_session = eap_session_get(request);
+ eap_tls_session_t *eap_tls_session;
+
+ VALUE_PAIR *vp;
+ bool client_cert;
+
+ eap_session->tls = true;
+
+ /*
+ * EAP-TLS-Require-Client-Cert attribute will override
+ * the require_client_cert configuration option.
+ */
+ vp = fr_pair_find_by_da(eap_session->request->control, attr_eap_tls_require_client_cert, TAG_ANY);
+ if (vp) {
+ client_cert = vp->vp_uint32 ? true : false;
+ } else {
+ client_cert = inst->req_client_cert;
+ }
+
+ /*
+ * EAP-TLS always requires a client certificate.
+ */
+ eap_session->opaque = eap_tls_session = eap_tls_session_init(eap_session, inst->tls_conf, client_cert);
+ if (!eap_tls_session) return RLM_MODULE_FAIL;
+
+ eap_tls_session->include_length = inst->include_length;
+
+ /*
+ * TLS session initialization is over. Now handle TLS
+ * related handshaking or application data.
+ */
+ if (eap_tls_start(eap_session) < 0) {
+ talloc_free(eap_tls_session);
+ return RLM_MODULE_FAIL;
+ }
+
+ eap_session->process = mod_process;
+
+ return RLM_MODULE_HANDLED;
+}
+
+/*
+ * Attach the EAP-TLS module.
+ */
+static int mod_instantiate(void *instance, CONF_SECTION *cs)
+{
+ rlm_eap_tls_t *inst = talloc_get_type_abort(instance, rlm_eap_tls_t);
+
+ inst->tls_conf = eap_tls_conf_parse(cs, "tls");
+ if (!inst->tls_conf) {
+ ERROR("Failed initializing SSL context");
+ return -1;
+ }
+
+ if (inst->virtual_server && !virtual_server_find(inst->virtual_server)) {
+ cf_log_err_by_name(cs, "virtual_server", "Unknown virtual server '%s'", inst->virtual_server);
+ return -1;
+ }
+
+ return 0;
+}
+
+#define ACTION_SECTION(_out, _field, _verb, _name) \
+do { \
+ CONF_SECTION *_tmp; \
+ _tmp = cf_section_find(server_cs, _verb, _name); \
+ if (_tmp) { \
+ if (unlang_compile(_tmp, MOD_AUTHORIZE, NULL) < 0) return -1; \
+ found = true; \
+ } \
+ if (_out) _out->_field = _tmp; \
+} while (0)
+
+/** Compile virtual server sections
+ *
+ */
+static int mod_section_compile(eap_tls_actions_t *actions, CONF_SECTION *server_cs)
+{
+ bool found = false;
+
+ if (!fr_cond_assert(server_cs)) return -1;
+
+ ACTION_SECTION(actions, recv_access_request, "recv", "Access-Request");
+
+ /*
+ * Warn if we couldn't find any actions.
+ */
+ if (!found) {
+ cf_log_warn(server_cs, "No \"eap-tls\" actions found in virtual server \"%s\"",
+ cf_section_name2(server_cs));
+ }
+
+ return 0;
+}
+
+/** Compile any virtual servers with the "eap-tls" namespace
+ *
+ */
+static int mod_namespace_load(CONF_SECTION *server_cs)
+{
+ return mod_section_compile(NULL, server_cs);
+}
+
+static int mod_load(void)
+{
+// if (virtual_server_namespace_register("eap-tls", mod_namespace_load) < 0) return -1;
+
+ return 0;
+}
+
+/*
+ * The module name should be the only globally exported symbol.
+ * That is, everything else should be 'static'.
+ */
+extern rlm_eap_submodule_t rlm_eap_tls;
+rlm_eap_submodule_t rlm_eap_tls = {
+ .name = "eap_tls",
+ .magic = RLM_MODULE_INIT,
+
+ .provides = { FR_EAP_METHOD_TLS },
+ .inst_size = sizeof(rlm_eap_tls_t),
+ .config = submodule_config,
+ .instantiate = mod_instantiate, /* Create new submodule instance */
+
+ .onload = mod_load,
+ .session_init = mod_session_init, /* Initialise a new EAP session */
+ .entry_point = mod_process /* Process next round of EAP method */
+};
VALUE_PAIR *vp;
fr_cursor_t cursor;
- slen = tmpl_afrom_attr_substr(request, NULL, &vpt, p, &(vp_tmpl_rules_t){ .dict_def = request->dict });
+ slen = tmpl_afrom_attr_substr(request, NULL, &vpt, p, -1, &(vp_tmpl_rules_t){ .dict_def = request->dict });
if (slen <= 0) {
RPEDEBUG("Failed parsing attribute name '%s'", p);
return false;
--- /dev/null
+TARGETNAME := libfreeradius-json
+
+ifneq "$(TARGETNAME)" ""
+TARGET := $(TARGETNAME).a
+endif
+
+SOURCES := json_missing.c json.c jpath.c
+SRC_CFLAGS :=
+TGT_LDLIBS := -ljson-c
--- /dev/null
+/*
+ * rlm_radius state machine
+ */
+static void mod_radius_fd_idle(rlm_radius_connection_t *c);
+
+static void mod_radius_fd_active(rlm_radius_connection_t *c);
+
+static void mod_radius_conn_error(UNUSED fr_event_list_t *el, int sock, UNUSED int flags, int fd_errno, void *uctx);
+
+static int CC_HINT(nonnull) mod_add(rlm_radius_t *inst, rlm_radius_connection_t *c, REQUEST *request);
+
+
+#ifndef USEC
+#define USEC (1000000)
+#endif
+
+// @todo - pass in REQUEST, or maybe request_io_ctx, so we can store these numbers in the rlm_link_t?
+bool rlm_radius_update_delay(struct timeval *start, uint32_t *rt, uint32_t *count, int code, void *client_io_ctx, struct timeval *now)
+{
+ uint32_t delay, frac;
+ rlm_radius_retry_t const *retry;
+ rlm_radius_thread_t *t;
+
+ (void) talloc_get_type_abort(client_io_ctx, rlm_radius_client_io_ctx_t);
+
+ t = talloc_parent(client_io_ctx);
+
+ (void) talloc_get_type_abort(t, rlm_radius_thread_t);
+
+ rad_assert(code > 0);
+ rad_assert(code < FR_MAX_PACKET_CODE);
+ rad_assert(t->inst->packets[code].irt != 0);
+
+ retry = &t->inst->packets[code];
+
+ /*
+ * First packet: use IRT.
+ */
+ if (!*rt) {
+ gettimeofday(start, NULL);
+ *rt = retry->irt * USEC;
+ *count = 1;
+ return true;
+ }
+
+ /*
+ * Later packets, do more stuff.
+ */
+ (*count)++;
+
+ /*
+ * We retried too many times. Fail.
+ */
+ if (retry->mrc && (*count > retry->mrc)) {
+ return false;
+ }
+
+ /*
+ * Cap delay at MRD
+ */
+ if (now && retry->mrd) {
+ struct timeval end;
+
+ end = *start;
+ end.tv_sec += retry->mrd;
+
+ if (timercmp(now, &end, >=)) {
+ return false;
+ }
+ }
+
+ /*
+ * RFC 5080 Section 2.2.1
+ *
+ * RT = 2*RTprev + RAND*RTprev
+ * = 1.9 * RTprev + rand(0,.2) * RTprev
+ * = 1.9 * RTprev + rand(0,1) * (RTprev / 5)
+ */
+ delay = fr_rand();
+ delay ^= (delay >> 16);
+ delay &= 0xffff;
+ frac = *rt / 5;
+ delay = ((frac >> 16) * delay) + (((frac & 0xffff) * delay) >> 16);
+
+ delay += (2 * *rt) - (*rt / 10);
+
+ /*
+ * Cap delay at MRT
+ */
+ if (retry->mrt && (delay > (retry->mrt * USEC))) {
+ int mrt_usec = retry->mrt * USEC;
+
+ /*
+ * delay = MRT + RAND * MRT
+ * = 0.9 MRT + rand(0,.2) * MRT
+ */
+ delay = fr_rand();
+ delay ^= (delay >> 15);
+ delay &= 0x1ffff;
+ delay = ((mrt_usec >> 16) * delay) + (((mrt_usec & 0xffff) * delay) >> 16);
+ delay += mrt_usec - (mrt_usec / 10);
+ }
+
+ *rt = delay;
+
+ return true;
+}
+
+
+
+
+/*
+ * So transports can calculate retransmission timers.
+ */
+bool rlm_radius_update_delay(struct timeval *start, uint32_t *rt, uint32_t *count, int code, void *client_io_ctx, struct timeval *now);
+
+typedef struct rlm_radius_client_io_ctx_t rlm_radius_client_io_ctx_t;
+
+
+
+static int mod_write(REQUEST *request, void *request_ctx, void *io_ctx)
+{
+ rlm_radius_client_io_ctx_t *io = talloc_get_type_abort(io_ctx, rlm_radius_client_io_ctx_t);
+ request_ctx_t *track = (request_ctx_t *) request_ctx; /* not talloc'd */
+ ssize_t packet_len, data_size;
+ rlm_radius_request_t *rr;
+
+ rad_assert(request->packet->code > 0);
+ rad_assert(request->packet->code < FR_MAX_PACKET_CODE);
+
+ /*
+ * Create the tracking table if it doesn't already exist.
+ *
+ * @todo - move this into the "init" routine? where was can examine
+ * the rlm_radius_t, and create the relevant data structures.
+ */
+ if (!io->id[request->packet->code]) {
+ io->id[request->packet->code] = rr_track_create(io_ctx);
+ if (!io->id[request->packet->code]) {
+ RDEBUG("Failed creating tracking table for code %d", request->packet->code);
+ return -1;
+ }
+ }
+
+ /*
+ * Allocate an ID
+ */
+ rr = rr_track_alloc(io->id[request->packet->code], request, request->packet->code, io, request_ctx);
+ if (!rr) {
+ RDEBUG("Failed allocating packet ID for code %d", request->packet->code);
+ return -1;
+ }
+
+ packet_len = fr_radius_encode(io->buffer, io->buflen, NULL, io->inst->secret, strlen(io->inst->secret),
+ rr->code, rr->id, request->packet->vps);
+ if (packet_len < 0) {
+ RDEBUG("Failed encoding packet: %s", fr_strerror());
+
+ // @todo - distinguish write errors from encode errors?
+ return -1;
+ }
+
+ data_size = udp_send(io->fd, io->buffer, packet_len, 0,
+ &io->dst_ipaddr, io->dst_port,
+ 0, /* if_index */
+ &io->src_ipaddr, io->src_port);
+
+ // @todo - put the packet into an RB tree, too, so we can find replies...
+ memcpy(&track->header, io->buffer, 20);
+
+ if (data_size < packet_len) {
+ rad_assert(0 == 1);
+ }
+
+ return 1;
+}
+
+/** Get a printable name for the socket
+ *
+ */
+static char const *mod_get_name(TALLOC_CTX *ctx, void *io_ctx)
+{
+ rlm_radius_client_io_ctx_t *io = talloc_get_type_abort(io_ctx, rlm_radius_client_io_ctx_t);
+ char src_buf[FR_IPADDR_STRLEN], dst_buf[FR_IPADDR_STRLEN];
+
+ fr_inet_ntop(dst_buf, sizeof(dst_buf), &io->dst_ipaddr);
+
+ // @todo - make sure to get the local port number we're bound to
+
+ if (fr_ipaddr_is_inaddr_any(&io->inst->src_ipaddr)) {
+ return talloc_asprintf(ctx, "home server %s port %u", dst_buf, io->dst_port);
+ }
+
+ fr_inet_ntop(src_buf, sizeof(src_buf), &io->inst->src_ipaddr);
+ return talloc_asprintf(ctx, "from %s to home server %s port %u", src_buf, dst_buf, io->dst_port);
+}
+
+
+/** Shutdown/close a file descriptor
+ *
+ */
+static void mod_close(int fd, void *io_ctx)
+{
+ rlm_radius_client_io_ctx_t *io = talloc_get_type_abort(io_ctx, rlm_radius_client_io_ctx_t);
+
+ if (shutdown(fd, SHUT_RDWR) < 0) DEBUG3("Shutdown on socket (%i) failed: %s", fd, fr_syserror(errno));
+ if (close(fd) < 0) DEBUG3("Closing socket (%i) failed: %s", fd, fr_syserror(errno));
+
+ io->fd = -1;
+}
+
+/** Do more setup once the connection has been opened
+ *
+ */
+static fr_connection_state_t mod_open(UNUSED fr_event_list_t *el, UNUSED int fd, UNUSED void *io_ctx)
+{
+// rlm_radius_client_io_ctx_t_t *io = talloc_get_type_abort(io_ctx, rlm_radius_client_io_ctx_t);
+
+ // @todo - create the initial Status-Server for negotiation and send that
+
+ return FR_CONNECTION_STATE_CONNECTED;
+}
+
+
+/** Initialize the connection.
+ *
+ */
+static fr_connection_state_t mod_init(int *fd_out, void *io_ctx, void const *uctx)
+{
+ int fd;
+ rlm_radius_client_io_ctx_t *io = talloc_get_type_abort(io_ctx, rlm_radius_client_io_ctx_t);
+ rlm_radius_udp_t const *inst = talloc_get_type_abort(uctx, rlm_radius_udp_t);
+
+ io->inst = inst;
+
+ io->max_packet_size = inst->max_packet_size;
+ io->buflen = io->max_packet_size;
+ io->buffer = talloc_array(io, uint8_t, io->buflen);
+
+ if (!io->buffer) {
+ return FR_CONNECTION_STATE_FAILED;
+ }
+
+ io->dst_ipaddr = inst->dst_ipaddr;
+ io->dst_port = inst->dst_port;
+ io->src_ipaddr = inst->src_ipaddr;
+ io->src_port = 0;
+
+ /*
+ * Open the outgoing socket.
+ *
+ * @todo - pass src_ipaddr, and remove later call to fr_socket_bind()
+ * which does return the src_port, but doesn't set the "don't fragment" bit.
+ */
+ fd = fr_socket_client_udp(&io->src_ipaddr, &io->dst_ipaddr, io->dst_port, true);
+ if (fd < 0) {
+ DEBUG("Failed opening RADIUS client UDP socket: %s", fr_strerror());
+ return FR_CONNECTION_STATE_FAILED;
+ }
+
+#if 0
+ if (fr_socket_bind(fd, &io->src_ipaddr, &io->src_port, inst->interface) < 0) {
+ DEBUG("Failed binding RADIUS client UDP socket: %s FD %d %pV port %u interface %s", fr_strerror(), fd, fr_box_ipaddr(io->src_ipaddr),
+ io->src_port, inst->interface);
+ return FR_CONNECTION_STATE_FAILED;
+ }
+#endif
+
+ // @todo - set recv_buff and send_buff socket options
+
+ io->fd = fd;
+
+ // @todo - initialize the tracking memory, etc.
+
+ *fd_out = fd;
+
+ return FR_CONNECTION_STATE_CONNECTING;
+}
--- /dev/null
+# converting to connection tracker...
+
+Notes:
+
+* probably want a "synchronous" flag in connection struct, it just makes things easier
+* and also whether or not we do status checks...
+
+## status_check_timeout()
+
+pretty much all RADIUS stuff
+
+## conn_zombie_timeout()
+
+about half RADIUS stuff
+
+* needs to know if status checks are used
+ * likely can go to a bool in connection
+
+* needs to have callback io_
+
+## state_transition()
+
+* add io_transition_state_out(ctx, u, c)
+ * returns true/false for continue
+ * on false the caller just returns
+ * due to status checks not transitioning to a different state
+ * and on true, sets c->slots_free
+
+* add io_transition_state_in(ctx, u, c)
+ * returns true/false for succeeded
+ * on false, it transitions u back to QUEUED
+ * and on true, sets c->slots_free
+
+## conn_read()
+
+* can move all of the RADIUS stuff into a callback
+ * it's all pretty well encapsulated
+
+## retransmit_packet()
+
+Essentially all RADIUS stuff
+
+* called from response_timeout (conn handler)
+* and mod_signal
+
+* should be io_retransmit_packet()
+
+## response_timeout()
+
+Top half is all RADIUS
+
+bottom half is connection handling
+* which calls retransmit_packet()
+
+## conn_write()
+
+Writes a packet to a connection
+
+Essentially 100% RADIUS
+
+## conn_writable()
+
+* checks for pending status packets, and writes those if necessary
+
+* calls conn_write() to do protocol-specific dirty work
+
+otherwise 100% non-RADIUS
+
+## udp_request_free()
+
+checks synchronous. Otherwise 100% non-RADIUS
+
+## status_udp_request_free()
+
+essentially 100% RADIUS
+
+## _conn_failed()
+
+* needs RADIUS to delete status check retransmission timers and clean up the packets
+
+* ideally io_transition_failed()
+
+## _conn_open()
+
+* callback to print the name of the connection?
+
+* maybe io_transition_open() to build the status checks
+ * can be moved to separate function without too much trouble
+
+## conn_alloc()
+
+* calls rr_track_create, maybe io_alloc() ?
+
+## mod_push()
+
+* sets initial retry timer && u->code?
+
+everything else is non-RADIUS
+
+## mod_signal()
+
+check synchronous, but otherwise non-RADIUS
--- /dev/null
+TARGETNAME := libfreeradius-redis
+
+ifneq "$(TARGETNAME)" ""
+TARGET := $(TARGETNAME).a
+endif
+
+SOURCES := redis.c crc16.c cluster.c
+
+SRC_CFLAGS :=
+TGT_LDLIBS := -lhiredis