diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:49:46 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:49:46 +0000 |
commit | 50b37d4a27d3295a29afca2286f1a5a086142cec (patch) | |
tree | 9212f763934ee090ef72d823f559f52ce387f268 /src/main/radclient.c | |
parent | Initial commit. (diff) | |
download | freeradius-upstream/3.2.1+dfsg.tar.xz freeradius-upstream/3.2.1+dfsg.zip |
Adding upstream version 3.2.1+dfsg.upstream/3.2.1+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/main/radclient.c | 1704 |
1 files changed, 1704 insertions, 0 deletions
diff --git a/src/main/radclient.c b/src/main/radclient.c new file mode 100644 index 0000000..09d27c8 --- /dev/null +++ b/src/main/radclient.c @@ -0,0 +1,1704 @@ +/* + * radclient.c General radius packet debug tool. + * + * 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 2000,2006,2014 The FreeRADIUS server project + * Copyright 2000 Miquel van Smoorenburg <miquels@cistron.nl> + * Copyright 2000 Alan DeKok <aland@ox.org> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radclient.h> +#include <freeradius-devel/radpaths.h> +#include <freeradius-devel/udpfromto.h> +#include <freeradius-devel/conf.h> +#ifdef HAVE_OPENSSL_SSL_H +#include <openssl/ssl.h> +#include <freeradius-devel/openssl3.h> +#endif +#include <ctype.h> + +#ifdef HAVE_GETOPT_H +# include <getopt.h> +#endif + +#include <assert.h> + +USES_APPLE_DEPRECATED_API + +typedef struct REQUEST REQUEST; /* to shut up warnings about mschap.h */ + +#include "smbdes.h" +#include "mschap.h" + +static int retries = 3; +static float timeout = 5; +static char const *secret = NULL; +static bool do_output = true; + +static rc_stats_t stats; + +static uint16_t server_port = 0; +static int packet_code = PW_CODE_UNDEFINED; +static fr_ipaddr_t server_ipaddr; +static int resend_count = 1; +static bool done = true; +static bool print_filename = false; + +static fr_ipaddr_t client_ipaddr; +static uint16_t client_port = 0; + +static int sockfd; + +#ifdef WITH_TCP +static char const *proto = NULL; +#endif +static int ipproto = IPPROTO_UDP; + +static rbtree_t *filename_tree = NULL; +static fr_packet_list_t *pl = NULL; + +static int sleep_time = -1; + +static rc_request_t *request_head = NULL; +static rc_request_t *rc_request_tail = NULL; + +static char const *radclient_version = "radclient version " RADIUSD_VERSION_STRING +#ifdef RADIUSD_VERSION_COMMIT +" (git #" STRINGIFY(RADIUSD_VERSION_COMMIT) ")" +#endif +#ifndef ENABLE_REPRODUCIBLE_BUILDS +", built on " __DATE__ " at " __TIME__ +#endif +; + +static void NEVER_RETURNS usage(void) +{ + fprintf(stderr, "Usage: radclient [options] server[:port] <command> [<secret>]\n"); + + fprintf(stderr, " <command> One of auth, acct, status, coa, disconnect or auto.\n"); + fprintf(stderr, " -4 Use IPv4 address of server\n"); + fprintf(stderr, " -6 Use IPv6 address of server.\n"); + fprintf(stderr, " -c <count> Send each packet 'count' times.\n"); + fprintf(stderr, " -d <raddb> Set user dictionary directory (defaults to " RADDBDIR ").\n"); + fprintf(stderr, " -D <dictdir> Set main dictionary directory (defaults to " DICTDIR ").\n"); + fprintf(stderr, " -f <file>[:<file>] Read packets from file, not stdin.\n"); + fprintf(stderr, " If a second file is provided, it will be used to verify responses\n"); + fprintf(stderr, " -F Print the file name, packet number and reply code.\n"); + fprintf(stderr, " -h Print usage help information.\n"); + fprintf(stderr, " -n <num> Send N requests/s\n"); + fprintf(stderr, " -p <num> Send 'num' packets from a file in parallel.\n"); + fprintf(stderr, " -q Do not print anything out.\n"); + fprintf(stderr, " -r <retries> If timeout, retry sending the packet 'retries' times.\n"); + fprintf(stderr, " -s Print out summary information of auth results.\n"); + fprintf(stderr, " -S <file> read secret from file, not command line.\n"); + fprintf(stderr, " -t <timeout> Wait 'timeout' seconds before retrying (may be a floating point number).\n"); + fprintf(stderr, " -v Show program version information.\n"); + fprintf(stderr, " -x Debugging mode.\n"); + +#ifdef WITH_TCP + fprintf(stderr, " -P <proto> Use proto (tcp or udp) for transport.\n"); +#endif + + exit(1); +} + +static const FR_NAME_NUMBER request_types[] = { + { "auth", PW_CODE_ACCESS_REQUEST }, + { "challenge", PW_CODE_ACCESS_CHALLENGE }, + { "acct", PW_CODE_ACCOUNTING_REQUEST }, + { "status", PW_CODE_STATUS_SERVER }, + { "disconnect", PW_CODE_DISCONNECT_REQUEST }, + { "coa", PW_CODE_COA_REQUEST }, + { "auto", PW_CODE_UNDEFINED }, + + { NULL, 0} +}; + +/* + * Free a radclient struct, which may (or may not) + * already be in the list. + */ +static int _rc_request_free(rc_request_t *request) +{ + rc_request_t *prev, *next; + + prev = request->prev; + next = request->next; + + if (prev) { + assert(request_head != request); + prev->next = next; + } else if (request_head) { + assert(request_head == request); + request_head = next; + } + + if (next) { + assert(rc_request_tail != request); + next->prev = prev; + } else if (rc_request_tail) { + assert(rc_request_tail == request); + rc_request_tail = prev; + } + + return 0; +} + +#if defined(OPENSSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L +# include <openssl/provider.h> + +static OSSL_PROVIDER *openssl_default_provider = NULL; +static OSSL_PROVIDER *openssl_legacy_provider = NULL; + +static int openssl3_init(void) +{ + /* + * Load the default provider for most algorithms + */ + openssl_default_provider = OSSL_PROVIDER_load(NULL, "default"); + if (!openssl_default_provider) { + ERROR("(TLS) Failed loading default provider"); + return -1; + } + + /* + * Needed for MD4 + * + * https://www.openssl.org/docs/man3.0/man7/migration_guide.html#Legacy-Algorithms + */ + openssl_legacy_provider = OSSL_PROVIDER_load(NULL, "legacy"); + if (!openssl_legacy_provider) { + ERROR("(TLS) Failed loading legacy provider"); + return -1; + } + + return 0; +} + +static void openssl3_free(void) +{ + if (openssl_default_provider && !OSSL_PROVIDER_unload(openssl_default_provider)) { + ERROR("Failed unloading default provider"); + } + openssl_default_provider = NULL; + + if (openssl_legacy_provider && !OSSL_PROVIDER_unload(openssl_legacy_provider)) { + ERROR("Failed unloading legacy provider"); + } + openssl_legacy_provider = NULL; +} +#else +#define openssl3_init() +#define openssl3_free() +#endif + + + +static int mschapv1_encode(RADIUS_PACKET *packet, VALUE_PAIR **request, + char const *password) +{ + int rcode; + unsigned int i; + uint8_t *p; + VALUE_PAIR *challenge, *reply; + uint8_t nthash[16]; + + fr_pair_delete_by_num(&packet->vps, PW_MSCHAP_CHALLENGE, VENDORPEC_MICROSOFT, TAG_ANY); + fr_pair_delete_by_num(&packet->vps, PW_MSCHAP_RESPONSE, VENDORPEC_MICROSOFT, TAG_ANY); + + challenge = fr_pair_afrom_num(packet, PW_MSCHAP_CHALLENGE, VENDORPEC_MICROSOFT); + if (!challenge) { + return 0; + } + + fr_pair_add(request, challenge); + challenge->vp_length = 8; + challenge->vp_octets = p = talloc_array(challenge, uint8_t, challenge->vp_length); + for (i = 0; i < challenge->vp_length; i++) { + p[i] = fr_rand(); + } + + reply = fr_pair_afrom_num(packet, PW_MSCHAP_RESPONSE, VENDORPEC_MICROSOFT); + if (!reply) { + return 0; + } + + fr_pair_add(request, reply); + reply->vp_length = 50; + reply->vp_octets = p = talloc_array(reply, uint8_t, reply->vp_length); + memset(p, 0, reply->vp_length); + + p[1] = 0x01; /* NT hash */ + + rcode = mschap_ntpwdhash(nthash, password); + if (rcode < 0) return 0; + + smbdes_mschap(nthash, challenge->vp_octets, p + 26); + return 1; +} + + +static int getport(char const *name) +{ + struct servent *svp; + + svp = getservbyname(name, "udp"); + if (!svp) return 0; + + return ntohs(svp->s_port); +} + +/* + * Set a port from the request type if we don't already have one + */ +static void radclient_get_port(PW_CODE type, uint16_t *port) +{ + switch (type) { + default: + case PW_CODE_ACCESS_REQUEST: + case PW_CODE_ACCESS_CHALLENGE: + case PW_CODE_STATUS_SERVER: + if (*port == 0) *port = getport("radius"); + if (*port == 0) *port = PW_AUTH_UDP_PORT; + return; + + case PW_CODE_ACCOUNTING_REQUEST: + if (*port == 0) *port = getport("radacct"); + if (*port == 0) *port = PW_ACCT_UDP_PORT; + return; + + case PW_CODE_DISCONNECT_REQUEST: + if (*port == 0) *port = PW_POD_UDP_PORT; + return; + + case PW_CODE_COA_REQUEST: + if (*port == 0) *port = PW_COA_UDP_PORT; + return; + + case PW_CODE_UNDEFINED: + if (*port == 0) *port = 0; + return; + } +} + +/* + * Resolve a port to a request type + */ +static PW_CODE radclient_get_code(uint16_t port) +{ + /* + * getport returns 0 if the service doesn't exist + * so we need to return early, to avoid incorrect + * codes. + */ + if (port == 0) return PW_CODE_UNDEFINED; + + if ((port == getport("radius")) || (port == PW_AUTH_UDP_PORT) || (port == PW_AUTH_UDP_PORT_ALT)) { + return PW_CODE_ACCESS_REQUEST; + } + if ((port == getport("radacct")) || (port == PW_ACCT_UDP_PORT) || (port == PW_ACCT_UDP_PORT_ALT)) { + return PW_CODE_ACCOUNTING_REQUEST; + } + if (port == PW_COA_UDP_PORT) return PW_CODE_COA_REQUEST; + if (port == PW_POD_UDP_PORT) return PW_CODE_DISCONNECT_REQUEST; + + return PW_CODE_UNDEFINED; +} + + +static bool already_hex(VALUE_PAIR *vp) +{ + size_t i; + + if (!vp || (vp->da->type != PW_TYPE_OCTETS)) return true; + + /* + * If it's 17 octets, it *might* be already encoded. + * Or, it might just be a 17-character password (maybe UTF-8) + * Check it for non-printable characters. The odds of ALL + * of the characters being 32..255 is (1-7/8)^17, or (1/8)^17, + * or 1/(2^51), which is pretty much zero. + */ + for (i = 0; i < vp->vp_length; i++) { + if (vp->vp_octets[i] < 32) { + return true; + } + } + + return false; +} + + +/* + * Initialize a radclient data structure and add it to + * the global linked list. + */ +static int radclient_init(TALLOC_CTX *ctx, rc_file_pair_t *files) +{ + FILE *packets, *filters = NULL; + + vp_cursor_t cursor; + VALUE_PAIR *vp; + rc_request_t *request; + bool packets_done = false; + uint64_t num = 0; + + assert(files->packets != NULL); + + /* + * Determine where to read the VP's from. + */ + if (strcmp(files->packets, "-") != 0) { + packets = fopen(files->packets, "r"); + if (!packets) { + ERROR("Error opening %s: %s", files->packets, strerror(errno)); + return 0; + } + + /* + * Read in the pairs representing the expected response. + */ + if (files->filters) { + filters = fopen(files->filters, "r"); + if (!filters) { + ERROR("Error opening %s: %s", files->filters, strerror(errno)); + fclose(packets); + return 0; + } + } + } else { + packets = stdin; + } + + + /* + * Loop until the file is done. + */ + do { + /* + * Allocate it. + */ + request = talloc_zero(ctx, rc_request_t); + if (!request) { + ERROR("Out of memory"); + goto error; + } + + request->packet = rad_alloc(request, true); + if (!request->packet) { + ERROR("Out of memory"); + goto error; + } + + request->packet->src_ipaddr = client_ipaddr; + request->packet->src_port = client_port; + request->packet->dst_ipaddr = server_ipaddr; + request->packet->dst_port = server_port; +#ifdef WITH_TCP + request->packet->proto = ipproto; +#endif + + request->files = files; + request->packet->id = -1; /* allocate when sending */ + request->num = num++; + + /* + * Read the request VP's. + */ + if (fr_pair_list_afrom_file(request->packet, &request->packet->vps, packets, &packets_done) < 0) { + char const *input; + + if ((files->packets[0] == '-') && (files->packets[1] == '\0')) { + input = "stdin"; + } else { + input = files->packets; + } + + REDEBUG("Error parsing \"%s\"", input); + goto error; + } + + /* + * Skip empty entries + */ + if (!request->packet->vps) { + talloc_free(request); + continue; + } + + /* + * Read in filter VP's. + */ + if (filters) { + bool filters_done; + + if (fr_pair_list_afrom_file(request, &request->filter, filters, &filters_done) < 0) { + REDEBUG("Error parsing \"%s\"", files->filters); + goto error; + } + + if (filters_done && !packets_done) { + REDEBUG("Differing number of packets/filters in %s:%s " + "(too many requests))", files->packets, files->filters); + goto error; + } + + if (!filters_done && packets_done) { + REDEBUG("Differing number of packets/filters in %s:%s " + "(too many filters))", files->packets, files->filters); + goto error; + } + + /* + * xlat expansions aren't supported here + */ + for (vp = fr_cursor_init(&cursor, &request->filter); + vp; + vp = fr_cursor_next(&cursor)) { + if (vp->type == VT_XLAT) { + vp->type = VT_DATA; + vp->vp_strvalue = vp->value.xlat; + vp->vp_length = talloc_array_length(vp->vp_strvalue) - 1; + } + + if (vp->da->vendor == 0 ) switch (vp->da->attr) { + case PW_RESPONSE_PACKET_TYPE: + case PW_PACKET_TYPE: + fr_cursor_remove(&cursor); /* so we don't break the filter */ + request->filter_code = vp->vp_integer; + talloc_free(vp); + + default: + break; + } + } + + /* + * This allows efficient list comparisons later + */ + fr_pair_list_sort(&request->filter, fr_pair_cmp_by_da_tag); + } + + /* + * Process special attributes + */ + for (vp = fr_cursor_init(&cursor, &request->packet->vps); + vp; + vp = fr_cursor_next(&cursor)) { + /* + * Double quoted strings get marked up as xlat expansions, + * but we don't support that in request. + */ + if (vp->type == VT_XLAT) { + vp->type = VT_DATA; + vp->vp_strvalue = vp->value.xlat; + vp->vp_length = talloc_array_length(vp->vp_strvalue) - 1; + } + + if (!vp->da->vendor) switch (vp->da->attr) { + default: + break; + + /* + * Allow it to set the packet type in + * the attributes read from the file. + */ + case PW_PACKET_TYPE: + request->packet->code = vp->vp_integer; + break; + + case PW_RESPONSE_PACKET_TYPE: + request->filter_code = vp->vp_integer; + break; + + case PW_PACKET_DST_PORT: + request->packet->dst_port = (vp->vp_integer & 0xffff); + break; + + case PW_PACKET_DST_IP_ADDRESS: + request->packet->dst_ipaddr.af = AF_INET; + request->packet->dst_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; + request->packet->dst_ipaddr.prefix = 32; + break; + + case PW_PACKET_DST_IPV6_ADDRESS: + request->packet->dst_ipaddr.af = AF_INET6; + request->packet->dst_ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr; + request->packet->dst_ipaddr.prefix = 128; + break; + + case PW_PACKET_SRC_PORT: + if ((vp->vp_integer < 1024) || + (vp->vp_integer > 65535)) { + ERROR("Invalid value '%u' for Packet-Src-Port", vp->vp_integer); + goto error; + } + request->packet->src_port = (vp->vp_integer & 0xffff); + break; + + case PW_PACKET_SRC_IP_ADDRESS: + request->packet->src_ipaddr.af = AF_INET; + request->packet->src_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; + request->packet->src_ipaddr.prefix = 32; + break; + + case PW_PACKET_SRC_IPV6_ADDRESS: + request->packet->src_ipaddr.af = AF_INET6; + request->packet->src_ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr; + request->packet->src_ipaddr.prefix = 128; + break; + + case PW_DIGEST_REALM: + case PW_DIGEST_NONCE: + case PW_DIGEST_METHOD: + case PW_DIGEST_URI: + case PW_DIGEST_QOP: + case PW_DIGEST_ALGORITHM: + case PW_DIGEST_BODY_DIGEST: + case PW_DIGEST_CNONCE: + case PW_DIGEST_NONCE_COUNT: + case PW_DIGEST_USER_NAME: + /* overlapping! */ + { + DICT_ATTR const *da; + uint8_t *p, *q; + + p = talloc_array(vp, uint8_t, vp->vp_length + 2); + + memcpy(p + 2, vp->vp_octets, vp->vp_length); + p[0] = vp->da->attr - PW_DIGEST_REALM + 1; + vp->vp_length += 2; + p[1] = vp->vp_length; + + da = dict_attrbyvalue(PW_DIGEST_ATTRIBUTES, 0); + if (!da) { + ERROR("Out of memory"); + goto error; + } + vp->da = da; + + /* + * Re-do fr_pair_value_memsteal ourselves, + * because we play games with + * vp->da, and fr_pair_value_memsteal goes + * to GREAT lengths to sanitize + * and fix and change and + * double-check the various + * fields. + */ + memcpy(&q, &vp->vp_octets, sizeof(q)); + talloc_free(q); + + vp->vp_octets = talloc_steal(vp, p); + vp->type = VT_DATA; + + VERIFY_VP(vp); + } + break; + + /* + * Cache this for later. + */ + case PW_CLEARTEXT_PASSWORD: + request->password = vp; + break; + + /* + * Keep a copy of the the password attribute. + */ + case PW_CHAP_PASSWORD: + /* + * If it's already hex, do nothing. + */ + if ((vp->vp_length == 17) && + (already_hex(vp))) break; + + /* + * CHAP-Password is octets, so it may not be zero terminated. + */ + request->password = fr_pair_make(request->packet, &request->packet->vps, "Cleartext-Password", + "", T_OP_EQ); + fr_pair_value_bstrncpy(request->password, vp->vp_strvalue, vp->vp_length); + break; + + case PW_USER_PASSWORD: + case PW_MS_CHAP_PASSWORD: + request->password = fr_pair_make(request->packet, &request->packet->vps, "Cleartext-Password", + vp->vp_strvalue, T_OP_EQ); + break; + + case PW_RADCLIENT_TEST_NAME: + request->name = vp->vp_strvalue; + break; + } + } /* loop over the VP's we read in */ + + /* + * Use the default set on the command line + */ + if (request->packet->code == PW_CODE_UNDEFINED) request->packet->code = packet_code; + + /* + * Default to the filename + */ + if (!request->name) request->name = request->files->packets; + + /* + * Automatically set the response code from the request code + * (if one wasn't already set). + */ + if (request->filter_code == PW_CODE_UNDEFINED) { + switch (request->packet->code) { + case PW_CODE_ACCESS_REQUEST: + request->filter_code = PW_CODE_ACCESS_ACCEPT; + break; + + case PW_CODE_ACCOUNTING_REQUEST: + request->filter_code = PW_CODE_ACCOUNTING_RESPONSE; + break; + + case PW_CODE_COA_REQUEST: + request->filter_code = PW_CODE_COA_ACK; + break; + + case PW_CODE_DISCONNECT_REQUEST: + request->filter_code = PW_CODE_DISCONNECT_ACK; + break; + + case PW_CODE_STATUS_SERVER: + switch (radclient_get_code(request->packet->dst_port)) { + case PW_CODE_ACCESS_REQUEST: + request->filter_code = PW_CODE_ACCESS_ACCEPT; + break; + + case PW_CODE_ACCOUNTING_REQUEST: + request->filter_code = PW_CODE_ACCOUNTING_RESPONSE; + break; + + default: + request->filter_code = PW_CODE_UNDEFINED; + break; + } + break; + + case PW_CODE_UNDEFINED: + REDEBUG("Both Packet-Type and Response-Packet-Type undefined, specify at least one, " + "or a well known RADIUS port"); + goto error; + + default: + REDEBUG("Can't determine expected Response-Packet-Type for Packet-Type %i", + request->packet->code); + goto error; + } + /* + * Automatically set the request code from the response code + * (if one wasn't already set). + */ + } else if (request->packet->code == PW_CODE_UNDEFINED) { + switch (request->filter_code) { + case PW_CODE_ACCESS_ACCEPT: + case PW_CODE_ACCESS_REJECT: + request->packet->code = PW_CODE_ACCESS_REQUEST; + break; + + case PW_CODE_ACCOUNTING_RESPONSE: + request->packet->code = PW_CODE_ACCOUNTING_REQUEST; + break; + + case PW_CODE_DISCONNECT_ACK: + case PW_CODE_DISCONNECT_NAK: + request->packet->code = PW_CODE_DISCONNECT_REQUEST; + break; + + case PW_CODE_COA_ACK: + case PW_CODE_COA_NAK: + request->packet->code = PW_CODE_COA_REQUEST; + break; + + default: + REDEBUG("Can't determine expected Packet-Type for Response-Packet-Type %i", + request->filter_code); + goto error; + } + } + + /* + * Automatically set the dst port (if one wasn't already set). + */ + if (request->packet->dst_port == 0) { + radclient_get_port(request->packet->code, &request->packet->dst_port); + if (request->packet->dst_port == 0) { + REDEBUG("Can't determine destination port"); + goto error; + } + } + + /* + * Add it to the tail of the list. + */ + if (!request_head) { + assert(rc_request_tail == NULL); + request_head = request; + request->prev = NULL; + } else { + assert(rc_request_tail->next == NULL); + rc_request_tail->next = request; + request->prev = rc_request_tail; + } + rc_request_tail = request; + request->next = NULL; + + /* + * Set the destructor so it removes itself from the + * request list when freed. We don't set this until + * the packet is actually in the list, else we trigger + * the asserts in the free callback. + */ + talloc_set_destructor(request, _rc_request_free); + } while (!packets_done); /* loop until the file is done. */ + + if (packets != stdin) fclose(packets); + if (filters) fclose(filters); + + /* + * And we're done. + */ + return 1; + +error: + talloc_free(request); + + if (packets != stdin) fclose(packets); + if (filters) fclose(filters); + + return 0; +} + + +/* + * Sanity check each argument. + */ +static int radclient_sane(rc_request_t *request) +{ + if (request->packet->dst_port == 0) { + request->packet->dst_port = server_port; + } + if (request->packet->dst_ipaddr.af == AF_UNSPEC) { + if (server_ipaddr.af == AF_UNSPEC) { + ERROR("No server was given, and request %" PRIu64 " in file %s did not contain " + "Packet-Dst-IP-Address", request->num, request->files->packets); + return -1; + } + request->packet->dst_ipaddr = server_ipaddr; + } + if (request->packet->code == 0) { + if (packet_code == -1) { + ERROR("Request was \"auto\", and request %" PRIu64 " in file %s did not contain Packet-Type", + request->num, request->files->packets); + return -1; + } + request->packet->code = packet_code; + } + request->packet->sockfd = -1; + + return 0; +} + + +/* + * For request handling. + */ +static int filename_cmp(void const *one, void const *two) +{ + int cmp; + + rc_file_pair_t const *a = one; + rc_file_pair_t const *b = two; + + cmp = strcmp(a->packets, b->packets); + if (cmp != 0) return cmp; + + return strcmp(a->filters, b->filters); +} + +static int filename_walk(UNUSED void *context, void *data) +{ + rc_file_pair_t *files = data; + + /* + * Read request(s) from the file. + */ + if (!radclient_init(files, files)) return -1; /* stop walking */ + + return 0; +} + + +/* + * Deallocate packet ID, etc. + */ +static void deallocate_id(rc_request_t *request) +{ + if (!request || !request->packet || + (request->packet->id < 0)) { + return; + } + + /* + * One more unused RADIUS ID. + */ + fr_packet_list_id_free(pl, request->packet, true); + + /* + * If we've already sent a packet, free up the old one, + * and ensure that the next packet has a unique + * authentication vector. + */ + if (request->packet->data) TALLOC_FREE(request->packet->data); + if (request->reply) rad_free(&request->reply); +} + +/* + * Send one packet. + */ +static int send_one_packet(rc_request_t *request) +{ + assert(request->done == false); + + /* + * Remember when we have to wake up, to re-send the + * request, of we didn't receive a reply. + */ + if ((sleep_time == -1) || (sleep_time > (int) timeout)) sleep_time = (int) timeout; + + /* + * Haven't sent the packet yet. Initialize it. + */ + if (request->packet->id == -1) { + int i; + bool rcode; + + assert(request->reply == NULL); + + /* + * Didn't find a free packet ID, we're not done, + * we don't sleep, and we stop trying to process + * this packet. + */ + retry: + request->packet->src_ipaddr.af = server_ipaddr.af; + rcode = fr_packet_list_id_alloc(pl, ipproto, &request->packet, NULL); + if (!rcode) { + int mysockfd; + +#ifdef WITH_TCP + if (proto) { + mysockfd = fr_socket_client_tcp(NULL, + &request->packet->dst_ipaddr, + request->packet->dst_port, false); + if (mysockfd < 0) { + ERROR("Failed opening socket"); + exit(1); + } + } else +#endif + { + mysockfd = fr_socket(&client_ipaddr, 0); + if (mysockfd < 0) { + ERROR("Failed opening socket"); + exit(1); + } + +#ifdef WITH_UDPFROMTO + if (udpfromto_init(mysockfd) < 0) { + ERROR("Failed initializing socket"); + exit(1); + } +#endif + } + if (!fr_packet_list_socket_add(pl, mysockfd, ipproto, + &request->packet->dst_ipaddr, + request->packet->dst_port, NULL)) { + ERROR("Can't add new socket"); + exit(1); + } + goto retry; + } + + assert(request->packet->id != -1); + assert(request->packet->data == NULL); + + for (i = 0; i < 4; i++) { + ((uint32_t *) request->packet->vector)[i] = fr_rand(); + } + + /* + * Update the password, so it can be encrypted with the + * new authentication vector. + */ + if (request->password) { + VALUE_PAIR *vp; + + if ((vp = fr_pair_find_by_num(request->packet->vps, PW_USER_PASSWORD, 0, TAG_ANY)) != NULL) { + fr_pair_value_strcpy(vp, request->password->vp_strvalue); + + } else if ((vp = fr_pair_find_by_num(request->packet->vps, PW_CHAP_PASSWORD, 0, TAG_ANY)) != NULL) { + uint8_t buffer[17]; + + rad_chap_encode(request->packet, buffer, fr_rand() & 0xff, request->password); + fr_pair_value_memcpy(vp, buffer, 17); + + } else if (fr_pair_find_by_num(request->packet->vps, PW_MS_CHAP_PASSWORD, 0, TAG_ANY) != NULL) { + mschapv1_encode(request->packet, &request->packet->vps, request->password->vp_strvalue); + + } else { + DEBUG("WARNING: No password in the request"); + } + } + + request->timestamp = time(NULL); + request->tries = 1; + request->resend++; + + } else { /* request->packet->id >= 0 */ + time_t now = time(NULL); + + /* + * FIXME: Accounting packets are never retried! + * The Acct-Delay-Time attribute is updated to + * reflect the delay, and the packet is re-sent + * from scratch! + */ + + /* + * Not time for a retry, do so. + */ + if ((now - request->timestamp) < timeout) { + /* + * When we walk over the tree sending + * packets, we update the minimum time + * required to sleep. + */ + if ((sleep_time == -1) || + (sleep_time > (now - request->timestamp))) { + sleep_time = now - request->timestamp; + } + return 0; + } + + /* + * We're not trying later, maybe the packet is done. + */ + if (request->tries == retries) { + assert(request->packet->id >= 0); + + /* + * Delete the request from the tree of + * outstanding requests. + */ + fr_packet_list_yank(pl, request->packet); + + RDEBUG("No reply from server for ID %d socket %d", + request->packet->id, request->packet->sockfd); + deallocate_id(request); + + /* + * Normally we mark it "done" when we've received + * the reply, but this is a special case. + */ + if (request->resend == resend_count) { + request->done = true; + } + stats.lost++; + return -1; + } + + /* + * We are trying later. + */ + request->timestamp = now; + request->tries++; + } + + /* + * Send the packet. + */ + if (rad_send(request->packet, NULL, secret) < 0) { + REDEBUG("Failed to send packet for ID %d", request->packet->id); + deallocate_id(request); + request->done = true; + return -1; + } + + if (fr_log_fp) { + fr_packet_header_print(fr_log_fp, request->packet, false); + if (fr_debug_lvl > 0) vp_printlist(fr_log_fp, request->packet->vps); + } + + return 0; +} + +/* + * Receive one packet, maybe. + */ +static int recv_one_packet(int wait_time) +{ + fd_set set; + struct timeval tv; + rc_request_t *request; + RADIUS_PACKET *reply, **packet_p; + volatile int max_fd; + + /* And wait for reply, timing out as necessary */ + FD_ZERO(&set); + + max_fd = fr_packet_list_fd_set(pl, &set); + if (max_fd < 0) exit(1); /* no sockets to listen on! */ + + tv.tv_sec = (wait_time <= 0) ? 0 : wait_time; + tv.tv_usec = 0; + + /* + * No packet was received. + */ + if (select(max_fd, &set, NULL, NULL, &tv) <= 0) return 0; + + /* + * Look for the packet. + */ + reply = fr_packet_list_recv(pl, &set); + if (!reply) { + ERROR("Received bad packet"); +#ifdef WITH_TCP + /* + * If the packet is bad, we close the socket. + * I'm not sure how to do that now, so we just + * die... + */ + if (proto) exit(1); +#endif + return -1; /* bad packet */ + } + + packet_p = fr_packet_list_find_byreply(pl, reply); + if (!packet_p) { + ERROR("Received reply to request we did not send. (id=%d socket %d)", + reply->id, reply->sockfd); + rad_free(&reply); + return -1; /* got reply to packet we didn't send */ + } + request = fr_packet2myptr(rc_request_t, packet, packet_p); + + /* + * Fails the signature validation: not a real reply. + * FIXME: Silently drop it and listen for another packet. + */ + if (rad_verify(reply, request->packet, secret) < 0) { + REDEBUG("Reply verification failed"); + stats.lost++; + goto packet_done; /* shared secret is incorrect */ + } + + if (print_filename) { + RDEBUG("%s response code %d", request->files->packets, reply->code); + } + + deallocate_id(request); + request->reply = reply; + reply = NULL; + + /* + * If this fails, we're out of memory. + */ + if (rad_decode(request->reply, request->packet, secret) != 0) { + REDEBUG("Reply decode failed"); + stats.lost++; + goto packet_done; + } + + if (fr_log_fp) { + fr_packet_header_print(fr_log_fp, request->reply, true); + if (fr_debug_lvl > 0) vp_printlist(fr_log_fp, request->reply->vps); + } + + /* + * Increment counters... + */ + switch (request->reply->code) { + case PW_CODE_ACCESS_ACCEPT: + case PW_CODE_ACCOUNTING_RESPONSE: + case PW_CODE_COA_ACK: + case PW_CODE_DISCONNECT_ACK: + stats.accepted++; + break; + + case PW_CODE_ACCESS_CHALLENGE: + break; + + default: + stats.rejected++; + } + + /* + * If we had an expected response code, check to see if the + * packet matched that. + */ + if ((request->filter_code != PW_CODE_UNDEFINED) && (request->reply->code != request->filter_code)) { + fr_strerror_printf(NULL); + + if (is_radius_code(request->reply->code)) { + REDEBUG("%s: Expected %s got %s", request->name, fr_packet_codes[request->filter_code], + fr_packet_codes[request->reply->code]); + } else { + REDEBUG("%s: Expected %u got %i", request->name, request->filter_code, + request->reply->code); + } + stats.failed++; + /* + * Check if the contents of the packet matched the filter + */ + } else if (!request->filter) { + stats.passed++; + } else { + VALUE_PAIR const *failed[2]; + + fr_pair_list_sort(&request->reply->vps, fr_pair_cmp_by_da_tag); + if (fr_pair_validate(failed, request->filter, request->reply->vps)) { + RDEBUG("%s: Response passed filter", request->name); + stats.passed++; + } else { + fr_pair_validate_debug(request, failed); + REDEBUG("%s: Response for failed filter", request->name); + stats.failed++; + } + } + + if (request->resend == resend_count) { + request->done = true; + } + +packet_done: + rad_free(&request->reply); + rad_free(&reply); /* may be NULL */ + + return 0; +} + +int main(int argc, char **argv) +{ + int c; + char const *radius_dir = RADDBDIR; + char const *dict_dir = DICTDIR; + char filesecret[256]; + FILE *fp; + int do_summary = false; + int persec = 0; + int parallel = 1; + rc_request_t *this; + int force_af = AF_UNSPEC; + + /* + * It's easier having two sets of flags to set the + * verbosity of library calls and the verbosity of + * radclient. + */ + fr_debug_lvl = 0; + fr_log_fp = stdout; + +#ifndef NDEBUG + if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) { + fr_perror("radclient"); + exit(EXIT_FAILURE); + } +#endif + + talloc_set_log_stderr(); + + filename_tree = rbtree_create(NULL, filename_cmp, NULL, 0); + if (!filename_tree) { + oom: + ERROR("Out of memory"); + exit(1); + } + + while ((c = getopt(argc, argv, "46c:d:D:f:Fhn:p:qr:sS:t:vx" +#ifdef WITH_TCP + "P:" +#endif + )) != EOF) switch (c) { + case '4': + force_af = AF_INET; + break; + + case '6': + force_af = AF_INET6; + break; + + case 'c': + if (!isdigit((int) *optarg)) usage(); + + resend_count = atoi(optarg); + + if (resend_count < 1) usage(); + break; + + case 'D': + dict_dir = optarg; + break; + + case 'd': + radius_dir = optarg; + break; + + case 'f': + { + char const *p; + rc_file_pair_t *files; + + files = talloc(talloc_autofree_context(), rc_file_pair_t); + if (!files) goto oom; + + p = strchr(optarg, ':'); + if (p) { + files->packets = talloc_strndup(files, optarg, p - optarg); + if (!files->packets) goto oom; + files->filters = p + 1; + } else { + files->packets = optarg; + files->filters = NULL; + } + rbtree_insert(filename_tree, (void *) files); + } + break; + + case 'F': + print_filename = true; + break; + + case 'n': + persec = atoi(optarg); + if (persec <= 0) usage(); + break; + + /* + * Note that sending MANY requests in + * parallel can over-run the kernel + * queues, and Linux will happily discard + * packets. So even if the server responds, + * the client may not see the reply. + */ + case 'p': + parallel = atoi(optarg); + if (parallel <= 0) usage(); + break; + +#ifdef WITH_TCP + case 'P': + proto = optarg; + if (strcmp(proto, "tcp") != 0) { + if (strcmp(proto, "udp") == 0) { + proto = NULL; + } else { + usage(); + } + } else { + ipproto = IPPROTO_TCP; + } + break; + +#endif + + case 'q': + do_output = false; + fr_log_fp = NULL; /* no output from you, either! */ + break; + + case 'r': + if (!isdigit((int) *optarg)) usage(); + retries = atoi(optarg); + if ((retries == 0) || (retries > 1000)) usage(); + break; + + case 's': + do_summary = true; + break; + + case 'S': + { + char *p; + fp = fopen(optarg, "r"); + if (!fp) { + ERROR("Error opening %s: %s", optarg, fr_syserror(errno)); + exit(1); + } + if (fgets(filesecret, sizeof(filesecret), fp) == NULL) { + ERROR("Error reading %s: %s", optarg, fr_syserror(errno)); + exit(1); + } + fclose(fp); + + /* truncate newline */ + p = filesecret + strlen(filesecret) - 1; + while ((p >= filesecret) && + (*p < ' ')) { + *p = '\0'; + --p; + } + + if (strlen(filesecret) < 2) { + ERROR("Secret in %s is too short", optarg); + exit(1); + } + secret = filesecret; + } + break; + + case 't': + if (!isdigit((int) *optarg)) + usage(); + timeout = atof(optarg); + break; + + case 'v': + fr_debug_lvl = 1; + DEBUG("%s", radclient_version); + exit(0); + + case 'x': + fr_debug_lvl++; + break; + + case 'h': + default: + usage(); + } + argc -= (optind - 1); + argv += (optind - 1); + + if ((argc < 3) || ((secret == NULL) && (argc < 4))) { + ERROR("Insufficient arguments"); + usage(); + } + /* + * Mismatch between the binary and the libraries it depends on + */ + if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) { + fr_perror("radclient"); + return 1; + } + + if (dict_init(dict_dir, RADIUS_DICTIONARY) < 0) { + fr_perror("radclient"); + return 1; + } + + if (dict_read(radius_dir, RADIUS_DICTIONARY) == -1) { + fr_perror("radclient"); + return 1; + } + fr_strerror(); /* Clear the error buffer */ + + /* + * Get the request type + */ + if (!isdigit((int) argv[2][0])) { + packet_code = fr_str2int(request_types, argv[2], -2); + if (packet_code == -2) { + ERROR("Unrecognised request type \"%s\"", argv[2]); + usage(); + } + } else { + packet_code = atoi(argv[2]); + } + + /* + * Resolve hostname. + */ + if (strcmp(argv[1], "-") != 0) { + if (fr_pton_port(&server_ipaddr, &server_port, argv[1], -1, force_af, true) < 0) { + ERROR("%s", fr_strerror()); + exit(1); + } + + /* + * Work backwards from the port to determine the packet type + */ + if (packet_code == PW_CODE_UNDEFINED) packet_code = radclient_get_code(server_port); + } + radclient_get_port(packet_code, &server_port); + + /* + * Add the secret. + */ + if (argv[3]) secret = argv[3]; + + /* + * If no '-f' is specified, we're reading from stdin. + */ + if (rbtree_num_elements(filename_tree) == 0) { + rc_file_pair_t *files; + + files = talloc_zero(talloc_autofree_context(), rc_file_pair_t); + files->packets = "-"; + if (!radclient_init(files, files)) { + exit(1); + } + } + + /* + * Walk over the list of filenames, creating the requests. + */ + if (rbtree_walk(filename_tree, RBTREE_IN_ORDER, filename_walk, NULL) != 0) { + ERROR("Failed parsing input files"); + exit(1); + } + + /* + * No packets read. Die. + */ + if (!request_head) { + ERROR("Nothing to send"); + exit(1); + } + + openssl3_init(); + + /* + * Bind to the first specified IP address and port. + * This means we ignore later ones. + */ + if (request_head->packet->src_ipaddr.af == AF_UNSPEC) { + memset(&client_ipaddr, 0, sizeof(client_ipaddr)); + client_ipaddr.af = server_ipaddr.af; + } else { + client_ipaddr = request_head->packet->src_ipaddr; + } + + client_port = request_head->packet->src_port; + +#ifdef WITH_TCP + if (proto) { + sockfd = fr_socket_client_tcp(NULL, &server_ipaddr, server_port, false); + if (sockfd < 0) { + ERROR("Error opening socket"); + exit(1); + } + } else +#endif + { + sockfd = fr_socket(&client_ipaddr, client_port); + if (sockfd < 0) { + ERROR("Error opening socket"); + exit(1); + } + +#ifdef WITH_UDPFROMTO + if (udpfromto_init(sockfd) < 0) { + ERROR("Failed initializing socket"); + exit(1); + } +#endif + } + + pl = fr_packet_list_create(1); + if (!pl) { + ERROR("Out of memory"); + exit(1); + } + + if (!fr_packet_list_socket_add(pl, sockfd, ipproto, &server_ipaddr, + server_port, NULL)) { + ERROR("Out of memory"); + exit(1); + } + + /* + * Walk over the list of packets, sanity checking + * everything. + */ + for (this = request_head; this != NULL; this = this->next) { + this->packet->src_ipaddr = client_ipaddr; + this->packet->src_port = client_port; + if (radclient_sane(this) != 0) { + exit(1); + } + } + + /* + * Walk over the packets to send, until + * we're all done. + * + * FIXME: This currently busy-loops until it receives + * all of the packets. It should really have some sort of + * send packet, get time to wait, select for time, etc. + * loop. + */ + do { + int n = parallel; + rc_request_t *next; + char const *filename = NULL; + + done = true; + sleep_time = -1; + + /* + * Walk over the packets, sending them. + */ + + for (this = request_head; this != NULL; this = next) { + next = this->next; + + /* + * If there's a packet to receive, + * receive it, but don't wait for a + * packet. + */ + recv_one_packet(0); + + /* + * This packet is done. Delete it. + */ + if (this->done) { + talloc_free(this); + continue; + } + + /* + * Packets from multiple '-f' are sent + * in parallel. + * + * Packets from one file are sent in + * series, unless '-p' is specified, in + * which case N packets from each file + * are sent in parallel. + */ + if (this->files->packets != filename) { + filename = this->files->packets; + n = parallel; + } + + if (n > 0) { + n--; + + /* + * Send the current packet. + */ + if (send_one_packet(this) < 0) { + talloc_free(this); + break; + } + + /* + * Wait a little before sending + * the next packet, if told to. + */ + if (persec) { + struct timeval tv; + + /* + * Don't sleep elsewhere. + */ + sleep_time = 0; + + if (persec == 1) { + tv.tv_sec = 1; + tv.tv_usec = 0; + } else { + tv.tv_sec = 0; + tv.tv_usec = 1000000/persec; + } + + /* + * Sleep for milliseconds, + * portably. + * + * If we get an error or + * a signal, treat it like + * a normal timeout. + */ + select(0, NULL, NULL, NULL, &tv); + } + + /* + * If we haven't sent this packet + * often enough, we're not done, + * and we shouldn't sleep. + */ + if (this->resend < resend_count) { + done = false; + sleep_time = 0; + } + } else { /* haven't sent this packet, we're not done */ + assert(this->done == false); + assert(this->reply == NULL); + done = false; + } + } + + /* + * Still have outstanding requests. + */ + if (fr_packet_list_num_elements(pl) > 0) { + done = false; + } else { + sleep_time = 0; + } + + /* + * Nothing to do until we receive a request, so + * sleep until then. Once we receive one packet, + * we go back, and walk through the whole list again, + * sending more packets (if necessary), and updating + * the sleep time. + */ + if (!done && (sleep_time > 0)) { + recv_one_packet(sleep_time); + } + } while (!done); + + rbtree_free(filename_tree); + fr_packet_list_free(pl); + while (request_head) TALLOC_FREE(request_head); + dict_free(); + + if (do_summary) { + printf("Packet summary:\n" + "\tAccepted : %" PRIu64 "\n" + "\tRejected : %" PRIu64 "\n" + "\tLost : %" PRIu64 "\n" + "\tPassed filter : %" PRIu64 "\n" + "\tFailed filter : %" PRIu64 "\n", + stats.accepted, + stats.rejected, + stats.lost, + stats.passed, + stats.failed + ); + } + + if ((stats.lost > 0) || (stats.failed > 0)) { + exit(1); + } + + openssl3_free(); + + exit(0); +} |