+/* RADIUS authentication load test */
+
+#define _SVID_SOURCE
+#define _POSIX_SOURCE
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/wait.h>
+#include <sys/mman.h>
+#include <netinet/in.h>
+#include <netdb.h>
+#include <sys/time.h>
+#include <time.h>
+#include <fcntl.h>
+#include <sys/select.h>
+#include <signal.h>
+#include "../md5.h"
+
+extern char *optarg;
+extern int optind;
+
+struct user {
+ char *user;
+ char *pass;
+ int flags;
+#define F_FAKE 1
+#define F_BAD 2
+#define F_USED 4
+ char *request;
+ int request_len;
+ struct user *next;
+};
+
+typedef uint32_t u32;
+
+struct user_list {
+ struct user *entry;
+ int attempts;
+ int response;
+ u32 begin;
+ u32 retry;
+ u32 end;
+};
+
+struct stats {
+ int total;
+ int out;
+ int in;
+ int err;
+ int ready;
+};
+
+enum {
+ AccessRequest = 1,
+ AccessAccept,
+ AccessReject,
+ AccessFail = 99
+};
+
+#define USAGE "Usage: %s [-i input] [-n instances] [-f fake] [-b bad] " \
+ "[-l limit] server port secret\n"
+
+#define MAX_ATTEMPTS 5
+
+void *xmalloc(size_t size)
+{
+ void *p = malloc(size);
+ if (!p)
+ {
+ fprintf(stderr, "out of memory allocating %d bytes\n", size);
+ exit(1);
+ }
+
+ return p;
+}
+
+char *xstrdup(char *s)
+{
+ int l = strlen(s);
+ char *p = xmalloc(l + 1);
+ return strcpy(p, s);
+}
+
+void *xmmap(size_t size)
+{
+ void *p = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, 0, 0);
+
+ if (p == MAP_FAILED)
+ {
+ fprintf(stderr, "out of memory allocating %d shared bytes\n", size);
+ exit(1);
+ }
+
+ return p;
+}
+
+void logmsg(char *fmt, ...)
+{
+ static int new = 1;
+
+ if (new)
+ {
+ static char time_s[] = "YYYY-MM-DD HH:MM:SS ";
+ time_t now = time(NULL);
+
+ strftime(time_s, sizeof(time_s), "%Y-%m-%d %T ", localtime(&now));
+ fputs(time_s, stdout);
+ }
+
+ va_list ap;
+ va_start(ap, fmt);
+ vprintf(fmt, ap);
+ va_end(ap);
+
+ fflush(stdout);
+
+ new = strchr(fmt, '\n') != NULL;
+}
+
+void catch(int sig __attribute__ ((unused)) ) {}
+
+void child(struct user_list *users, int count, int rshift,
+ struct stats *stats, in_addr_t addr, int port, int limit)
+ __attribute__ ((noreturn));
+
+time_t basetime;
+
+int main(int argc, char *argv[])
+{
+ char *input = 0;
+ int instances = 1;
+ int fake = 0;
+ int bad = 0;
+ int limit = 0;
+ int o;
+
+ while ((o = getopt(argc, argv, "i:n:f:b:l:")) != -1)
+ {
+ switch (o)
+ {
+ case 'i': /* input file */
+ input = optarg;
+ break;
+
+ case 'n': /* parallel instances */
+ instances = atoi(optarg);
+ if (instances < 1 || instances > 32)
+ {
+ fprintf(stderr, "invalid instances value: `%s' (1-32)\n", optarg);
+ return 2;
+ }
+ break;
+
+ case 'f': /* percentage of additional fake users to add */
+ fake = atoi(optarg);
+ if (fake < 1 || fake > 100)
+ {
+ fprintf(stderr, "invalid fake value: `%s' (1-100)\n", optarg);
+ return 2;
+ }
+ break;
+
+ case 'b': /* percentage of users to use incorrect passwords for */
+ bad = atoi(optarg);
+ if (bad < 1 || bad > 100)
+ {
+ fprintf(stderr, "invalid bad value: `%s' (1-100)\n", optarg);
+ return 2;
+ }
+ break;
+
+ case 'l': /* limit number of messages per 1/10 sec */
+ limit = atoi(optarg);
+ if (limit < 1)
+ {
+ fprintf(stderr, "invalid limit value: `%s'\n", optarg);
+ return 2;
+ }
+ break;
+
+ default:
+ fprintf(stderr, USAGE, argv[0]);
+ return 2;
+ }
+ }
+
+ if (argc - optind != 3)
+ {
+ fprintf(stderr, USAGE, argv[0]);
+ return 2;
+ }
+
+ char *server = argv[optind++];
+ char *port_s = argv[optind++];
+ char *secret = argv[optind];
+
+ int port = atoi(port_s);
+ if (port < 1)
+ {
+ fprintf(stderr, "invalid port: `%s'\n", port_s);
+ return 2;
+ }
+
+ in_addr_t server_addr;
+ {
+ struct hostent *h;
+ if (!(h = gethostbyname(server)) || h->h_addrtype != AF_INET)
+ {
+ fprintf(stderr, "invalid server `%s' (%s)\n", server,
+ h ? "no address" : hstrerror(h_errno));
+
+ return 1;
+ }
+
+ memcpy(&server_addr, h->h_addr, sizeof(server_addr));
+ }
+
+ time(&basetime); /* start clock */
+
+ FILE *in = stdin;
+ if (input && !(in = fopen(input, "r")))
+ {
+ fprintf(stderr, "can't open input file `%s' (%s)\n", input,
+ strerror(errno));
+
+ return 1;
+ }
+
+ logmsg("Loading users from %s: ", input ? input : "stdin");
+
+ struct user *users = 0;
+ struct user *u = 0;
+
+ int count = 0;
+ char buf[1024];
+
+ while (fgets(buf, sizeof(buf), in))
+ {
+ count++;
+
+ /* format: username \t password \n */
+ char *p = strchr(buf, '\t');
+ if (!p)
+ {
+ fprintf(stderr, "invalid input line %d (no TAB)\n", count);
+ return 1;
+ }
+
+ *p++ = 0;
+ if (!u)
+ {
+ users = xmalloc(sizeof(struct user));
+ u = users;
+ }
+ else
+ {
+ u->next = xmalloc(sizeof(struct user));
+ u = u->next;
+ }
+
+ u->user = xstrdup(buf);
+ while (*p == '\t')
+ p++;
+
+ char *q = strchr(p, '\n');
+ if (q)
+ *q = 0;
+
+ if (!*p)
+ {
+ fprintf(stderr, "invalid input line %d (no password)\n", count);
+ return 1;
+ }
+
+ u->pass = xstrdup(p);
+ u->flags = 0;
+ u->next = 0;
+ }
+
+ if (input)
+ fclose(in);
+
+ logmsg("%d\n", count);
+
+ if (!count)
+ return 1;
+
+ char *fake_pw = "__fake__";
+ if (fake)
+ {
+ /* add f fake users to make a total of which fake% are bogus */
+ int f = ((count * fake) / (100.0 - fake) + 0.5);
+ char fake_user[] = "__fake_99999999";
+
+ logmsg("Generating %d%% extra fake users: ", fake);
+ for (int i = 0; i < f; i++, count++)
+ {
+ snprintf(fake_user, sizeof(fake_user), "__fake_%d", i);
+ u->next = xmalloc(sizeof(struct user));
+ u = u->next;
+ u->user = xstrdup(fake_user);
+ u->pass = fake_pw;
+ u->flags = F_FAKE;
+ u->next = 0;
+ }
+
+ logmsg("%d\n", f);
+ }
+
+ if (bad)
+ {
+ int b = (count * bad) / 100.0 + 0.5;
+
+ logmsg("Setting %d%% bad passwords: ", bad);
+
+ u = users;
+ for (int i = 0; i < b; i++, u = u->next)
+ {
+ if (u->pass != fake_pw)
+ free(u->pass);
+
+ u->pass = "__bad__";
+ u->flags |= F_BAD;
+ }
+
+ logmsg("%d\n", b);
+ }
+
+ struct user **unsorted = xmalloc(sizeof(struct user) * count);
+
+ u = users;
+ for (int i = 0; i < count; i++, u = u->next)
+ unsorted[i] = u;
+
+ struct user_list *random = xmmap(sizeof(struct user_list) * count);
+ memset(random, 0, sizeof(struct user_list) * count);
+
+ logmsg("Randomising users: ");
+
+ srand(time(NULL) ^ getpid());
+
+ for (int i = 0; i < count; )
+ {
+ int j = 1.0 * count * rand() / RAND_MAX;
+ if (unsorted[j]->flags & F_USED)
+ continue;
+
+ random[i++].entry = unsorted[j];
+ unsorted[j]->flags |= F_USED;
+ }
+
+ logmsg("done\n");
+ logmsg("Building RADIUS queries: ");
+
+ {
+ char pass[128];
+
+ for (u = users; u; u = u->next)
+ {
+ int pw_len = strlen(u->pass);
+ int len = 4 /* code, identifier, length */
+ + 16 /* authenticator */
+ + 2 + strlen(u->user) /* user */
+ + 2 + ((pw_len / 16) + ((pw_len % 16) ? 1 : 0)) * 16;
+ /* encoded password */
+
+ char *p = xmalloc(len);
+ u->request = p;
+ u->request_len = len;
+
+ *p++ = AccessRequest;
+ *p++ = 0; /* identifier set in child */
+ *(uint16_t *) p = htons(len);
+ p += 2;
+
+ /* authenticator */
+ for (int j = 0; j < 16; j++)
+ *p++ = rand();
+
+ *p = 1; /* user name */
+ p[1] = strlen(u->user) + 2;
+ strcpy(p + 2, u->user);
+ p += p[1];
+
+ strcpy(pass, u->pass);
+ while (pw_len % 16)
+ pass[pw_len++] = 0; /* pad */
+
+ for (int j = 0; j < pw_len; j += 16)
+ {
+ MD5_CTX ctx;
+ MD5Init(&ctx);
+ MD5Update(&ctx, secret, strlen(secret));
+ if (j)
+ MD5Update(&ctx, pass + j - 16, 16);
+ else
+ /* authenticator */
+ MD5Update(&ctx, u->request + 4, 16);
+
+ char digest[16];
+ MD5Final(digest, &ctx);
+
+ for (int k = 0; k < 16; k++)
+ pass[j + k] ^= digest[k];
+ }
+
+ *p = 2; /* password */
+ p[1] = pw_len + 2;
+ memcpy(p + 2, pass, pw_len);
+ p += p[1];
+ }
+ }
+
+ logmsg("done\n");
+
+ signal(SIGUSR1, catch);
+
+ struct stats *stats = xmmap(sizeof(struct stats) * instances);
+ memset(stats, 0, sizeof(struct stats) * instances);
+
+ logmsg("Spawning %d processes: ", instances);
+
+ int per_child = count / instances;
+ int rshift = 0;
+ for (u32 tmp = per_child; tmp & 0xff00; tmp >>= 1)
+ rshift++;
+
+ for (int i = 0, offset = 0; i < instances; i++)
+ {
+ int slack = i ? 0 : count % instances;
+
+ stats[i].total = per_child + slack;
+ if (!fork())
+ child(random + offset, per_child + slack, rshift, stats + i,
+ server_addr, port, limit / instances);
+
+ offset += per_child + slack;
+ }
+
+ logmsg("done\n");
+
+ /* wait for children to setup */
+ int ready = 0;
+ do {
+ ready = 0;
+ for (int i = 0; i < instances; i++)
+ ready += stats[i].ready;
+
+ sleep(1);
+ } while (ready < instances);
+
+ /* go! */
+ kill(0, SIGUSR1);
+
+ logmsg("Processing...\n");
+ logmsg(" total: ");
+
+ for (int i = 0; i < instances; i++)
+ logmsg("[%5d %5s %5s]", stats[i].total, "", "");
+
+ logmsg("\n");
+ logmsg(" out/in/err: ");
+
+ int done = 0;
+ do {
+ for (int i = 0; i < instances; i++)
+ logmsg("[%5d %5d %5d]", stats[i].out, stats[i].in,
+ stats[i].err);
+
+ logmsg("\n");
+
+ if (waitpid(-1, NULL, WNOHANG) > 0)
+ done++;
+
+ if (done < instances)
+ {
+ sleep(1);
+ logmsg(" ");
+ }
+ } while (done < instances);
+
+ int a_hist[MAX_ATTEMPTS + 1];
+ memset(&a_hist, 0, sizeof(a_hist));
+
+ u32 min = 0;
+ u32 max = 0;
+ u32 r_hist[64];
+ memset(&r_hist, 0, sizeof(r_hist));
+ int hsz = sizeof(r_hist) / sizeof(*r_hist);
+
+ for (int i = 0; i < count; i++)
+ {
+ if ((random[i].response != AccessAccept &&
+ random[i].response != AccessReject) ||
+ (random[i].attempts < 1 ||
+ random[i].attempts > MAX_ATTEMPTS))
+ {
+ a_hist[MAX_ATTEMPTS]++;
+ continue;
+ }
+
+ a_hist[random[i].attempts - 1]++;
+
+ u32 interval = random[i].end - random[i].begin;
+
+ if (!i || interval < min)
+ min = interval;
+
+ if (interval > max)
+ max = interval;
+
+ /* histogram in 1/10s intervals */
+ int t = interval / 10 + 0.5;
+ if (t > hsz - 1)
+ t = hsz - 1;
+
+ r_hist[t]++;
+ }
+
+ logmsg("Send attempts:\n");
+ for (int i = 0; i < MAX_ATTEMPTS; i++)
+ logmsg(" %6d: %d\n", i + 1, a_hist[i]);
+
+ logmsg(" failed: %d\n", a_hist[MAX_ATTEMPTS]);
+
+ logmsg("Response time in seconds (min %.2f, max %.2f)\n",
+ min / 100.0, max / 100.0);
+
+ for (int i = 0; i < hsz; i++)
+ {
+ if (i < hsz - 1)
+ logmsg(" %3.1f:", i / 10.0);
+ else
+ logmsg(" more:");
+
+ logmsg(" %6d\n", r_hist[i]);
+ }
+
+ return 0;
+}
+
+/* time in sec/100 since program commenced */
+u32 now(void)
+{
+ struct timeval t;
+ gettimeofday(&t, 0);
+ return (t.tv_sec - basetime) * 100 + t.tv_usec / 10000 + 1;
+}
+
+void child(struct user_list *users, int count, int rshift,
+ struct stats *stats, in_addr_t addr, int port, int limit)
+{
+ int sockets = 1 << rshift;
+ unsigned rmask = sockets - 1;
+
+ int *sock = xmalloc(sizeof(int) * sockets);
+
+ fd_set r_in;
+ int nfd = 0;
+
+ FD_ZERO(&r_in);
+
+ for (int s = 0; s < sockets; s++)
+ {
+ if ((sock[s] = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0)
+ {
+ fprintf(stderr, "can't create a UDP socket (%s)\n",
+ strerror(errno));
+
+ exit(1);
+ }
+
+ int flags = fcntl(sock[s], F_GETFL, 0);
+ fcntl(sock[s], F_SETFL, flags | O_NONBLOCK);
+
+ struct sockaddr_in svr;
+ memset(&svr, 0, sizeof(svr));
+ svr.sin_family = AF_INET;
+ svr.sin_port = htons(port);
+ svr.sin_addr.s_addr = addr;
+
+ connect(sock[s], (struct sockaddr *) &svr, sizeof(svr));
+
+ FD_SET(sock[s], &r_in);
+ if (sock[s] + 1 > nfd)
+ nfd = sock[s] + 1;
+ }
+
+ for (int i = 0; i < count; i++)
+ /* set identifier */
+ *((unsigned char *) users[i].entry->request + 1) = i >> rshift;
+
+ stats->ready = 1;
+ pause();
+
+ u32 out_timer = now();
+ int out_count = 0;
+
+ while ((stats->in + stats->err) < count)
+ {
+ u32 time_now = now();
+
+ while (out_timer + 10 < time_now)
+ {
+ out_timer += 10;
+ if (out_count > 0)
+ out_count -= limit;
+ }
+
+ for (int pass = 1; pass <= 2; pass++)
+ {
+ for (int i = 0; i < count && out_count < limit; i++)
+ {
+ if (users[i].response)
+ continue;
+
+ if (users[i].attempts)
+ {
+ if (users[i].retry > time_now)
+ continue;
+ }
+ else if (pass == 1)
+ {
+ /* retries only on the first pass */
+ continue;
+ }
+
+ struct user *e = users[i].entry;
+ if (write(sock[i & rmask], e->request, e->request_len)
+ != e->request_len)
+ break;
+
+ time_now = now();
+ out_count++;
+
+ if (!users[i].attempts)
+ {
+ users[i].begin = time_now;
+ stats->out++;
+ }
+
+ if (++users[i].attempts > MAX_ATTEMPTS)
+ {
+ users[i].response = AccessFail;
+ stats->err++;
+ continue;
+ }
+
+ users[i].retry = time_now + 200 + 100 * (1 << users[i].attempts);
+ }
+ }
+
+ struct timeval tv = { 0, 100000 };
+
+ fd_set r;
+ memcpy(&r, &r_in, sizeof(r));
+
+ if (select(nfd, &r, NULL, NULL, &tv) < 1)
+ continue;
+
+ char buf[4096];
+
+ for (int s = 0; s < sockets; s++)
+ {
+ if (!FD_ISSET(sock[s], &r))
+ continue;
+
+ int sz;
+
+ while ((sz = read(sock[s], buf, sizeof(buf))) > 0)
+ {
+ if (sz < 2)
+ {
+ fprintf(stderr, "short packet returned\n");
+ continue;
+ }
+
+ if (buf[0] != AccessAccept && buf[0] != AccessReject)
+ {
+ fprintf(stderr, "unrecognised response type %d\n",
+ (int) buf[0]);
+
+ continue;
+ }
+
+ int i = s | (((unsigned char) buf[1]) << rshift);
+ if (i < 0 || i > count)
+ {
+ fprintf(stderr, "bogus identifier returned %d\n", i);
+ continue;
+ }
+
+ if (!users[i].attempts)
+ {
+ fprintf(stderr, "unexpected identifier returned %d\n", i);
+ continue;
+ }
+
+ if (users[i].response)
+ continue;
+
+ int expect = (users[i].entry->flags & (F_FAKE|F_BAD))
+ ? AccessReject : AccessAccept;
+
+ if (buf[0] != expect)
+ fprintf(stderr, "unexpected response %d for user %s "
+ "(expected %d)\n", (int) buf[0], users[i].entry->user,
+ expect);
+
+ users[i].response = buf[0];
+ users[i].end = now();
+ stats->in++;
+ }
+ }
+ }
+
+ exit(0);
+}