From: Alan T. DeKok Date: Mon, 25 Sep 2017 20:15:28 +0000 (-0400) Subject: start of detail file reader again X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a5c8deae04c985e2edd95403eaa97b69c5b22118;p=thirdparty%2Ffreeradius-server.git start of detail file reader again --- diff --git a/src/modules/proto_detail/all.mk b/src/modules/proto_detail/all.mk index 8beba1c590a..fc5bc537f63 100644 --- a/src/modules/proto_detail/all.mk +++ b/src/modules/proto_detail/all.mk @@ -1 +1 @@ -SUBMAKEFILES := proto_detail.mk proto_detail_process.mk +SUBMAKEFILES := proto_detail.mk proto_detail_file.mk proto_detail_process.mk diff --git a/src/modules/proto_detail/proto_detail_file.c b/src/modules/proto_detail/proto_detail_file.c index a0bbd67fc58..368aea1be0e 100644 --- a/src/modules/proto_detail/proto_detail_file.c +++ b/src/modules/proto_detail/proto_detail_file.c @@ -1,8 +1,4 @@ /* - * 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 @@ -16,1172 +12,405 @@ * 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 2007 The FreeRADIUS server project - * Copyright 2007 Alan DeKok */ -RCSID("$Id$") - +/** + * $Id$ + * @file proto_detail_file.c + * @brief Detail handler for files + * + * @copyright 2016 The FreeRADIUS server project. + * @copyright 2016 Alan DeKok (aland@deployingradius.com) + */ +#include #include -#include #include -#include +#include +#include +#include +#include +#include #include - -#include "detail.h" - -#ifdef HAVE_SYS_STAT_H -#include -#endif - -#ifdef HAVE_GLOB_H -#include -#endif - -#include +#include "proto_detail.h" #include -#define USEC (1000000) +typedef struct { + fr_time_t timestamp; //!< when we read the entry. + off_t done_offset; //!< where we're tracking the status +} fr_detail_entry_t; -static FR_NAME_NUMBER state_names[] = { - { "unopened", STATE_UNOPENED }, - { "unlocked", STATE_UNLOCKED }, - { "processing", STATE_PROCESSING }, +typedef struct { + proto_detail_t const *parent; //!< The module that spawned us! + char const *name; //!< debug name for printing - { "header", STATE_HEADER }, - { "vps", STATE_VPS }, - { "queued", STATE_QUEUED }, - { "running", STATE_RUNNING }, - { "no-reply", STATE_NO_REPLY }, - { "replied", STATE_REPLIED }, - - { NULL, 0 } -}; + int fd; //!< file descriptor + fr_event_list_t *el; //!< for various timers + fr_schedule_t *sc; //!< the scheduler, where we insert new readers -/* - * 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; + char const *filename; //!< file name, usually with wildcards + char const *filename_work; //!< work file name - rad_assert(request->listener == listener); - rad_assert(listener->send == detail_send); + bool vnode; //!< are we the vnode instance, or the filename_work instance? + bool eof; //!< are we at EOF on reading? - /* - * 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; + off_t header_offset; //!< offset of the current header we're reading + off_t read_offset; //!< where we're reading from in filename_work +} proto_detail_file_t; - 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); +static const CONF_PARSER file_listen_config[] = { + { FR_CONF_OFFSET("filename", FR_TYPE_STRING | FR_TYPE_REQUIRED, proto_detail_file_t, filename ) }, - /* - * 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); + CONF_PARSER_TERMINATOR +}; - /* - * 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); +static int mod_decode(UNUSED void const *instance, REQUEST *request, UNUSED uint8_t *const data, UNUSED size_t data_len) +{ - data->last_packet = now; - data->signal = 1; - data->entry_state = STATE_REPLIED; - data->counter++; - } +// fr_detail_entry_t const *track = request->async->packet_ctx; - if (write(data->child_pipe[1], &c, 1) < 0) { - RERROR("detail (%s): Failed writing ack to reader thread: %s", data->name, fr_syserror(errno)); - } + request->root = &main_config; + REQUEST_VERIFY(request); 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. + * @todo - put these into configuration! */ -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; -} - +static uint32_t priorities[FR_MAX_PACKET_CODE] = { + [FR_CODE_ACCESS_REQUEST] = PRIORITY_HIGH, + [FR_CODE_ACCOUNTING_REQUEST] = PRIORITY_LOW, + [FR_CODE_COA_REQUEST] = PRIORITY_NORMAL, + [FR_CODE_DISCONNECT_REQUEST] = PRIORITY_NORMAL, + [FR_CODE_STATUS_SERVER] = PRIORITY_NOW, +}; -/* - * 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) +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) { - 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; + proto_detail_file_t *inst = talloc_get_type_abort(instance, proto_detail_file_t); - rad_assert(packet != NULL); + ssize_t data_size; + size_t packet_len; + fr_detail_entry_t *track; + uint8_t *partial, *end, *next, *p; + size_t room; - 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)); - } - } + rad_assert(*leftover < buffer_len); /* - * Wait for the child thread to write an answer to the pipe + * There will be "leftover" bytes left over in the buffer + * from any previous read. At the start of the file, + * "leftover" will be zero. */ - 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 */ + partial= buffer + *leftover; + room = buffer_len - *leftover; /* - * 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... + * Try to read as much data as possible. */ - 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; - } + if (!inst->eof) { + data_size = read(inst->fd, partial, room); + if (data_size < 0) return -1; /* - * Only open for writing if we're - * marking requests as completed. + * Remember the read offset, or EOF. */ - 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); + if (data_size > 0) { + inst->read_offset = lseek(inst->fd, 0, SEEK_CUR); + } else { + inst->eof = true; } - - /* - * Look for the header - */ - data->file_state = STATE_PROCESSING; - data->entry_state = STATE_HEADER; - data->delay_time = USEC; - data->vps = NULL; - break; - + } else { /* - * Go to the next switch statement. + * We didn't read any more data. */ - case STATE_PROCESSING: - break; + data_size = 0; } - - 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. + * Remember where the end of all of the data is. */ - 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 */ + end = partial + data_size; /* - * If there's no reply, keep - * retransmitting the current packet - * forever. + * Look for "end of record" marker. We've already + * searched "leftover" bytes for \n\n, so we only have to + * search the remaining bytes. + * + * We MIGHT have the last character of the previously + * read data as \n, so we back up one character here. + * That lets us catch "\n\n" which crosses a read() + * boundary. */ - case STATE_NO_REPLY: - data->entry_state = STATE_QUEUED; - goto alloc_packet; + if (*leftover > 0) partial--; /* - * We have a reply. Clean up the old - * request, and go read another one. + * Note that all of the data MUST be printable, and raw + * LFs are forbidden in attribute contents. */ - 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)); - } + next = NULL; + for (p = partial; p < end; p++) { + if (p[0] != '\n') continue; + if ((p + 1) == end) break; /* no more data */ + if (p[1] == '\n') { + next = p + 2; + break; } - - 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. + * If there is a next record, remember how large this + * record is, and update "leftover" bytes. */ - 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; + if (next) { + packet_len = next - buffer; + *leftover = end - next; + } else if (!inst->eof) { /* - * 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. + * We're not at EOF, and there is no "next" + * entry. Remember all of the leftover data in + * the buffer, and ask the caller to call us when + * there's more data. */ - 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); + *leftover = end - buffer; + return 0; + } else { /* - * Read one VP. - * - * FIXME: do we want to check for non-protocol - * attributes like radsqlrelay does? + * Else we're at EOF, it's OK to miss an "end of + * record" marker. We just eat all of the + * remaining data. */ - 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); - } + packet_len = end - buffer; + *leftover = 0; } /* - * Some kind of error. - * - * FIXME: Leave the file in-place, and warn the - * administrator? + * Allocate the tracking entry. */ - if (ferror(data->fp)) goto cleanup; + track = talloc(instance, fr_detail_entry_t); + track->timestamp = fr_time(); - data->tries = 0; - data->packets++; + track->done_offset = 0; /* - * Process the packet. + * Search for the "Timestamp" attribute. We overload + * that to track which entries have been used. */ - 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; - } + end = buffer + packet_len; + for (p = buffer; p < end; p++) { + if (p[0] != '\n') continue; - 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; - } + if (((end - p) >= 5) && + (memcmp(p, "\tDone", 5) == 0)) { + /* + * @todo - try to grab another packet + * from the buffer. If there is a + * packet, memmove() the data to the + * start of the buffer, which is what + * fr_network_read() expects to see. + */ + rad_assert(0 == 1); + } - /* - * 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; + if (((end - p) > 10) && + (memcmp(p, "\tTimestamp", 10) == 0)) { + p += 2; + track->done_offset = inst->header_offset + (p - buffer); + } } /* - * Allocate the packet. If we fail, it's a serious - * problem. + * Too big? Ignore it. */ - packet = fr_radius_alloc(NULL, true); - if (!packet) { - ERROR("detail (%s): FATAL: Failed allocating memory for detail", data->name); - fr_exit(1); + if (packet_len > inst->parent->max_packet_size) { + DEBUG("Ignoring 'too large' entry at offset %llu of %s", + inst->header_offset, inst->filename_work); + DEBUG("Entry size %lu is greater than allowed maximum %u", + packet_len, inst->parent->max_packet_size); + return 0; } - 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. + * We've read one more packet. */ - 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; + inst->header_offset += packet_len; - gettimeofday(&packet->timestamp, NULL); + *packet_ctx = track; + *recv_time = &track->timestamp; + *priority = priorities[buffer[0]]; /* - * Remember where it came from, so that we don't - * proxy it to the place it came from... + * We're done reading the file, but not the buffer. Back + * up one byte so that the network code will try to read + * the byte again, which lets us then finish reading the + * buffer. + * + * We could make the network code smarter, to call our + * read() routine again if there are leftover bytes. But + * that logic doesn't integrate well into the event loop. + * So this hack is the next best thing. */ - 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; - } - } + if (inst->eof && (*leftover > 0)) { + off_t hack; - 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; - } + hack = inst->read_offset - 1; + (void) lseek(inst->fd, 0, SEEK_SET); } - /* - * 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); + return packet_len; +} - packet->dst_ipaddr.af = AF_INET; - packet->dst_ipaddr.addr.v4.s_addr = htonl((INADDR_LOOPBACK & ~0xffffff) | ((data->counter >> 24) & 0xff)); +static ssize_t mod_write(void *instance, void *packet_ctx, + UNUSED fr_time_t request_time, uint8_t *buffer, size_t buffer_len) +{ + proto_detail_file_t *inst = talloc_get_type_abort(instance, proto_detail_file_t); + fr_detail_entry_t *track = packet_ctx; - /* - * 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; - } + if (buffer_len < 1) return -1; - /* - * 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; - } + if (buffer[0] == 0) { + DEBUG3("Got Do-Not-Respond, not writing reply"); + talloc_free(track); + return buffer_len; } /* - * Set the transmission count. + * Seek to the entry, mark it as done, and then seek to + * the point in the file where we were reading from. */ - 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); + if (track->done_offset > 0) { + (void) lseek(inst->fd, track->done_offset, SEEK_SET); + (void) write(inst->fd, "Done", 4); + (void) lseek(inst->fd, inst->read_offset, SEEK_SET); } - vp->vp_uint32 = data->tries; - data->entry_state = STATE_RUNNING; - data->running = packet->timestamp.tv_sec; + /* + * @todo - add a used / free pool for these + */ + talloc_free(track); - return packet; + return buffer_len; } -/* - * Free detail-specific stuff. +/** Open a UDP listener for RADIUS + * + * @param[in] instance of the RADIUS UDP I/O path. + * @return + * - <0 on error + * - 0 on success */ -static int _detail_free(listen_detail_t *data) +static int mod_open(void *instance) { - if (!check_config) { - ssize_t ret; - void *arg = NULL; + proto_detail_file_t *inst = talloc_get_type_abort(instance, proto_detail_file_t); - /* - * Mark the child pipes as unusable - */ - close(data->child_pipe[0]); - close(data->child_pipe[1]); - data->child_pipe[0] = -1; + inst->fd = open(inst->filename_work, O_RDWR); + if (inst->fd < 0) return -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); - } + rad_assert(inst->name == NULL); + inst->name = talloc_asprintf(inst, "detail working file %s", inst->filename_work); - 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; - } + DEBUG("Listening om %s bound to virtual server %s", + inst->name, cf_section_name2(inst->parent->server_cs)); return 0; } - -static int detail_print(rad_listen_t const *this, char *buffer, size_t bufsize) +/** Get the file descriptor for this socket. + * + * @param[in] instance of the RADIUS UDP I/O path. + * @return the file descriptor + */ +static int mod_fd(void const *instance) { - if (!this->server) { - return snprintf(buffer, bufsize, "%s", - ((listen_detail_t *)(this->data))->filename); - } + proto_detail_file_t const *inst = talloc_get_type_abort_const(instance, proto_detail_file_t); - return snprintf(buffer, bufsize, "detail file %s as server %s", - ((listen_detail_t *)(this->data))->filename, - this->server); + return inst->fd; } -/* - * Delay while waiting for a file to be ready +/** Set the event list for a new socket + * + * @param[in] instance of the RADIUS UDP I/O path. + * @param[in] el the event list */ -static int detail_delay(listen_detail_t *data) +static void mod_event_list_set(void *instance, fr_event_list_t *el) { - int delay = (data->poll_interval - 1) * USEC; + proto_detail_file_t *inst; - /* - * Add +/- 0.25s of jitter - */ - delay += (USEC * 3) / 4; - delay += fr_rand() % (USEC / 2); + memcpy(&inst, &instance, sizeof(inst)); /* const issues */ - DEBUG2("detail (%s): Detail listener state %s waiting %d.%06d sec", - data->name, - fr_int2str(state_names, data->entry_state, "?"), - (delay / USEC), delay % USEC); + inst = talloc_get_type_abort(instance, proto_detail_file_t); - return delay; + inst->el = el; } -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) +static int mod_instantiate(UNUSED void *instance, UNUSED CONF_SECTION *cs) { - listen_detail_t *data = this->data; +// proto_detail_file_t *inst = talloc_get_type_abort(instance, proto_detail_file_t); - 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) +static int mod_bootstrap(void *instance, UNUSED CONF_SECTION *cs) { - 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); + proto_detail_file_t *inst = talloc_get_type_abort(instance, proto_detail_file_t); + dl_instance_t const *dl_inst; /* - * Only checking the config. Don't start threads or anything else. + * Find the dl_instance_t holding our instance data + * so we can find out what the parent of our instance + * was. */ - 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)); + dl_inst = dl_instance_find(instance); + rad_assert(dl_inst); - } 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; + inst->parent = talloc_get_type_abort(dl_inst->parent->data, proto_detail_t); return 0; } -/* - * Open detail files - */ -static int detail_socket_open(UNUSED CONF_SECTION *cs, rad_listen_t *this) +static int mod_detach(void *instance) { - listen_detail_t *data; - - data = this->data; - talloc_set_destructor(data, _detail_free); + proto_detail_file_t *inst = talloc_get_type_abort(instance, proto_detail_file_t); /* - * Create the communication pipes. + * @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. */ - 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]; + close(inst->fd); 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 + +/** Private interface for use by proto_detail_file + * + */ +extern fr_app_io_t proto_detail_file; +fr_app_io_t proto_detail_file = { + .magic = RLM_MODULE_INIT, + .name = "detail_file", + .config = file_listen_config, + .inst_size = sizeof(proto_detail_file_t), + .detach = mod_detach, + .bootstrap = mod_bootstrap, + .instantiate = mod_instantiate, + + .default_message_size = 65536, + + .open = mod_open, + .read = mod_read, + .decode = mod_decode, + .write = mod_write, + .fd = mod_fd, + .event_list_set = mod_event_list_set, }; diff --git a/src/modules/proto_detail/proto_detail_file.h b/src/modules/proto_detail/proto_detail_file.h deleted file mode 100644 index 2fa24fbb080..00000000000 --- a/src/modules/proto_detail/proto_detail_file.h +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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 */ diff --git a/src/modules/proto_detail/proto_detail_file.mk b/src/modules/proto_detail/proto_detail_file.mk index 9f0d5d356e5..61cca14b98e 100644 --- a/src/modules/proto_detail/proto_detail_file.mk +++ b/src/modules/proto_detail/proto_detail_file.mk @@ -1,9 +1,9 @@ -TARGETNAME := proto_detail +TARGETNAME := proto_detail_file ifneq "$(TARGETNAME)" "" TARGET := $(TARGETNAME).a endif -SOURCES := proto_detail.c +SOURCES := proto_detail_file.c TGT_PREREQS := libfreeradius-util.a libfreeradius-radius.a