we (work) use radiusctl as part of a check script with relayd so
we can try and keep a radius service available with some magical
routing and redirect configs.

radiusctl is currently pretty simple and sends a radius request,
and then waits for a reply. however, it does not implement retries
and timeouts, so if either the reply or request are lost, radiusctl
ends up waiting in recv forever for a packet that will never turn
up.

combined with this, we seem to tickle something in relayd where it
loses track of its children. this results in us "leaking" radiusctl
processes. historically we've coped with this by running pkill -x
radiusctl out of cron, and while it made us a sad on the inside,
we've been busy recently and had to ignore this for now.

unfortunatly one of the radius servers failed this morning. this
meant that instead of a few radius packets being lost over a long
period time causing a slow leak of radiusctl processes, we never
got a reply to any radius packet and started accumulating a ton of 
radius processes. in fact, we hit maxproc, which made recovering very
annoying.

we run a bunch of different checks out of relayd, but the radiusctl
one is the only one that doesnt implement timeouts and retries. so
im fixing it first, and then we'll try and figure out what's wrong
with relayd.

the specific changes in this diff is the introduction of a transmission
retry counter, a time interval between the tries, and a maximum
wait time that the process has before it gives up waiting for a
reply. each of these is configurable, but i think the defaults are
reasonable for a test.

it introduces libevent, because that makes it easy to manage the
timeouts.

ok?

Index: Makefile
===================================================================
RCS file: /cvs/src/usr.sbin/radiusctl/Makefile,v
retrieving revision 1.2
diff -u -p -r1.2 Makefile
--- Makefile    3 Aug 2015 04:10:21 -0000       1.2
+++ Makefile    21 Feb 2020 05:15:14 -0000
@@ -3,7 +3,7 @@ PROG=           radiusctl
 SRCS=          radiusctl.c parser.c chap_ms.c
 MAN=           radiusctl.8
 CFLAGS+=       -Wall -Wextra -Wno-unused-parameter
-LDADD+=                -lradius -lcrypto
-DPADD+=                ${LIBRADIUS} ${LIBCRYPTO}
+LDADD+=                -lradius -lcrypto -levent
+DPADD+=                ${LIBRADIUS} ${LIBCRYPTO} ${LIBEVENT}
 
 .include <bsd.prog.mk>
Index: parser.c
===================================================================
RCS file: /cvs/src/usr.sbin/radiusctl/parser.c,v
retrieving revision 1.1
diff -u -p -r1.1 parser.c
--- parser.c    21 Jul 2015 04:06:04 -0000      1.1
+++ parser.c    21 Feb 2020 05:15:14 -0000
@@ -18,6 +18,8 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <sys/time.h>
+
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -35,6 +37,9 @@ enum token_type {
        PORT,
        METHOD,
        NAS_PORT,
+       TRIES,
+       INTERVAL,
+       MAXWAIT,
        ENDTOKEN
 };
 
@@ -45,7 +50,11 @@ struct token {
        const struct token      *next;
 };
 
-static struct parse_result res;
+static struct parse_result res = {
+       .tries          = TEST_TRIES_DEFAULT,
+       .interval       = { TEST_INTERVAL_DEFAULT, 0 },
+       .maxwait        = { TEST_MAXWAIT_DEFAULT, 0 },
+};
 
 static const struct token t_test[];
 static const struct token t_secret[];
@@ -55,6 +64,9 @@ static const struct token t_password[];
 static const struct token t_port[];
 static const struct token t_method[];
 static const struct token t_nas_port[];
+static const struct token t_tries[];
+static const struct token t_interval[];
+static const struct token t_maxwait[];
 
 static const struct token t_main[] = {
        { KEYWORD,      "test",         TEST,           t_test },
@@ -82,6 +94,9 @@ static const struct token t_test_opts[] 
        { KEYWORD,      "port",         NONE,           t_port },
        { KEYWORD,      "method",       NONE,           t_method },
        { KEYWORD,      "nas-port",     NONE,           t_nas_port },
+       { KEYWORD,      "interval",     NONE,           t_interval },
+       { KEYWORD,      "tries",        NONE,           t_tries },
+       { KEYWORD,      "maxwait",      NONE,           t_maxwait },
        { ENDTOKEN,     "",             NONE,           NULL }
 };
 
@@ -105,6 +120,21 @@ static const struct token t_nas_port[] =
        { ENDTOKEN,     "",             NONE,           NULL }
 };
 
+static const struct token t_tries[] = {
+       { TRIES,        "",             NONE,           t_test_opts },
+       { ENDTOKEN,     "",             NONE,           NULL }
+};
+
+static const struct token t_interval[] = {
+       { INTERVAL,     "",             NONE,           t_test_opts },
+       { ENDTOKEN,     "",             NONE,           NULL }
+};
+
+static const struct token t_maxwait[] = {
+       { MAXWAIT,      "",             NONE,           t_test_opts },
+       { ENDTOKEN,     "",             NONE,           NULL }
+};
+
 
 static const struct token      *match_token(char *, const struct token []);
 static void                     show_valid_args(const struct token []);
@@ -115,8 +145,6 @@ parse(int argc, char *argv[])
        const struct token      *table = t_main;
        const struct token      *match;
 
-       bzero(&res, sizeof(res));
-
        while (argc >= 0) {
                if ((match = match_token(argv[0], table)) == NULL) {
                        fprintf(stderr, "valid commands/args:\n");
@@ -138,6 +166,12 @@ parse(int argc, char *argv[])
                return (NULL);
        }
 
+       if (res.tries * res.interval.tv_sec > res.maxwait.tv_sec) {
+               fprintf(stderr, "tries %u by interval %lld > maxwait %lld",
+                   res.tries, res.interval.tv_sec, res.maxwait.tv_sec);
+               return (NULL);
+       }
+
        return (&res);
 }
 
@@ -240,6 +274,50 @@ match_token(char *word, const struct tok
                        res.nas_port = num;
                        t = &table[i];
                        break;
+
+               case TRIES:
+                       if (word == NULL)
+                               break;
+                       num = strtonum(word,
+                           TEST_TRIES_MIN, TEST_TRIES_MAX, &errstr);
+                       if (errstr != NULL) {
+                               printf("invalid argument: %s is %s"
+                                   " for \"tries\"\n", word, errstr);
+                               return (NULL);
+                       }
+                       match++;
+                       res.tries = num;
+                       t = &table[i];
+                       break;
+               case INTERVAL:
+                       if (word == NULL)
+                               break;
+                       num = strtonum(word,
+                           TEST_INTERVAL_MIN, TEST_INTERVAL_MAX, &errstr);
+                       if (errstr != NULL) {
+                               printf("invalid argument: %s is %s"
+                                   " for \"interval\"\n", word, errstr);
+                               return (NULL);
+                       }
+                       match++;
+                       res.interval.tv_sec = num;
+                       t = &table[i];
+                       break;
+               case MAXWAIT:
+                       if (word == NULL)
+                               break;
+                       num = strtonum(word,
+                           TEST_MAXWAIT_MIN, TEST_MAXWAIT_MAX, &errstr);
+                       if (errstr != NULL) {
+                               printf("invalid argument: %s is %s"
+                                   " for \"maxwait\"\n", word, errstr);
+                               return (NULL);
+                       }
+                       match++;
+                       res.maxwait.tv_sec = num;
+                       t = &table[i];
+                       break;
+
                case ENDTOKEN:
                        break;
                }
@@ -292,6 +370,18 @@ show_valid_args(const struct token table
                        break;
                case NAS_PORT:
                        fprintf(stderr, "  <nas-port (0-65535)>\n");
+                       break;
+               case TRIES:
+                       fprintf(stderr, "  <tries (%u-%u)>\n",
+                           TEST_TRIES_MIN, TEST_TRIES_MAX);
+                       break;
+               case INTERVAL:
+                       fprintf(stderr, "  <interval (%u-%u)>\n",
+                           TEST_INTERVAL_MIN, TEST_INTERVAL_MAX);
+                       break;
+               case MAXWAIT:
+                       fprintf(stderr, "  <maxwait (%u-%u)>\n",
+                           TEST_MAXWAIT_MIN, TEST_MAXWAIT_MAX);
                        break;
                case ENDTOKEN:
                        break;
Index: parser.h
===================================================================
RCS file: /cvs/src/usr.sbin/radiusctl/parser.h,v
retrieving revision 1.1
diff -u -p -r1.1 parser.h
--- parser.h    21 Jul 2015 04:06:04 -0000      1.1
+++ parser.h    21 Feb 2020 05:15:14 -0000
@@ -31,6 +31,18 @@ enum auth_method {
        MSCHAPV2
 };
 
+#define TEST_TRIES_MIN         1
+#define TEST_TRIES_MAX         32
+#define TEST_TRIES_DEFAULT     3
+
+#define TEST_INTERVAL_MIN      1
+#define TEST_INTERVAL_MAX      10
+#define TEST_INTERVAL_DEFAULT  2
+
+#define TEST_MAXWAIT_MIN       3
+#define TEST_MAXWAIT_MAX       60
+#define TEST_MAXWAIT_DEFAULT   8
+
 struct parse_result {
        enum actions             action;
        const char              *hostname;
@@ -40,6 +52,13 @@ struct parse_result {
        u_short                  port;
        int                      nas_port;
        enum auth_method         auth_method;
+
+       /* number of packets to try sending */
+       unsigned int             tries;
+       /* how long between packet sends */
+       struct timeval           interval;
+       /* overall process wait time for a reply */
+       struct timeval           maxwait;
 };
 
 struct parse_result    *parse(int, char *[]);
Index: radiusctl.8
===================================================================
RCS file: /cvs/src/usr.sbin/radiusctl/radiusctl.8,v
retrieving revision 1.3
diff -u -p -r1.3 radiusctl.8
--- radiusctl.8 25 Aug 2015 01:21:57 -0000      1.3
+++ radiusctl.8 21 Feb 2020 05:15:14 -0000
@@ -77,6 +77,19 @@ when sending a packet to
 .Ar hostname .
 If the port is omitted,
 the default port number 1812 is used.
+.It Cm tries Ar number
+Specifies the number of packets to try sending.
+By default
+.Nm
+will send 3 packets before giving up.
+.It Cm interval Ar seconds
+Specifies how many seconds to wait before resending a packet.
+The default interval between packet retries is 2 seconds.
+.It Cm maxwait Ar seconds
+Specifies the maximum amount of time to wait for a valid reply packet.
+By default
+.Nm
+will wait 8 seconds for a valid reply.
 .El
 .El
 .Sh SEE ALSO
Index: radiusctl.c
===================================================================
RCS file: /cvs/src/usr.sbin/radiusctl/radiusctl.c,v
retrieving revision 1.7
diff -u -p -r1.7 radiusctl.c
--- radiusctl.c 1 Apr 2019 09:51:56 -0000       1.7
+++ radiusctl.c 21 Feb 2020 05:15:14 -0000
@@ -19,6 +19,7 @@
 #include <netinet/in.h>
 
 #include <arpa/inet.h>
+#include <errno.h>
 #include <err.h>
 #include <md5.h>
 #include <netdb.h>
@@ -30,11 +31,13 @@
 
 #include <radius.h>
 
+#include <event.h>
+
 #include "parser.h"
 #include "chap_ms.h"
 
 
-static void             radius_test (struct parse_result *);
+static int              radius_test (struct parse_result *);
 static void             radius_dump (FILE *, RADIUS_PACKET *, bool,
                            const char *);
 static const char      *radius_code_str (int code);
@@ -53,6 +56,7 @@ main(int argc, char *argv[])
 {
        int                      ch;
        struct parse_result     *result;
+       int                      ecode = EXIT_SUCCESS;
 
        while ((ch = getopt(argc, argv, "")) != -1)
                switch (ch) {
@@ -72,21 +76,38 @@ main(int argc, char *argv[])
        case TEST:
                if (pledge("stdio dns inet", NULL) == -1)
                        err(EXIT_FAILURE, "pledge");
-               radius_test(result);
+               ecode = radius_test(result);
                break;
        }
 
-       return (EXIT_SUCCESS);
+       return (ecode);
 }
 
-static void
+struct radius_test {
+       const struct parse_result       *res;
+       int                              ecode;
+
+       RADIUS_PACKET                   *reqpkt;
+       int                              sock;
+       unsigned int                     tries;
+       struct event                     ev_send;
+       struct event                     ev_recv;
+       struct event                     ev_timedout;
+};
+
+static void    radius_test_send(int, short, void *);
+static void    radius_test_recv(int, short, void *);
+static void    radius_test_timedout(int, short, void *);
+
+static int
 radius_test(struct parse_result *res)
 {
+       struct radius_test       test = { .res = res };
+       RADIUS_PACKET           *reqpkt;
        struct addrinfo          hints, *ai;
        int                      sock, retval;
        struct sockaddr_storage  sockaddr;
        socklen_t                sockaddrlen;
-       RADIUS_PACKET           *reqpkt, *respkt;
        struct sockaddr_in      *sin4;
        struct sockaddr_in6     *sin6;
        uint32_t                 u32val;
@@ -110,7 +131,8 @@ radius_test(struct parse_result *res)
                ((struct sockaddr_in *)ai->ai_addr)->sin_port =
                    htons(res->port);
 
-       sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+       sock = socket(ai->ai_family, ai->ai_socktype | SOCK_NONBLOCK,
+           ai->ai_protocol);
        if (sock == -1)
                err(1, "socket");
 
@@ -211,24 +233,31 @@ radius_test(struct parse_result *res)
 
        radius_put_message_authenticator(reqpkt, res->secret);
 
+       event_init();
+
+       test.ecode = EXIT_FAILURE;
+       test.res = res;
+       test.sock = sock;
+       test.reqpkt = reqpkt;
+       
+       event_set(&test.ev_recv, sock, EV_READ|EV_PERSIST,
+           radius_test_recv, &test);
+
+       evtimer_set(&test.ev_send, radius_test_send, &test);
+       evtimer_set(&test.ev_timedout, radius_test_timedout, &test);
+
+       event_add(&test.ev_recv, NULL);
+       evtimer_add(&test.ev_timedout, &res->maxwait);
+
        /* Send! */
        fprintf(stderr, "Sending:\n");
        radius_dump(stdout, reqpkt, false, res->secret);
-       if (send(sock, radius_get_data(reqpkt), radius_get_length(reqpkt), 0)
-           == -1)
-               warn("send");
-       if ((respkt = radius_recv(sock, 0)) == NULL)
-               warn("recv");
-       else {
-               radius_set_request_packet(respkt, reqpkt);
-               fprintf(stderr, "\nReceived:\n");
-               radius_dump(stdout, respkt, true, res->secret);
-       }
+       radius_test_send(0, EV_TIMEOUT, &test);
+
+       event_dispatch();
 
        /* Release the resources */
        radius_delete_packet(reqpkt);
-       if (respkt)
-               radius_delete_packet(respkt);
        close(sock);
        freeaddrinfo(ai);
 
@@ -236,7 +265,79 @@ radius_test(struct parse_result *res)
        if (res->password)
                explicit_bzero((char *)res->password, strlen(res->password));
 
-       return;
+       return (test.ecode);
+}
+
+static void
+radius_test_send(int thing, short revents, void *arg)
+{
+       struct radius_test      *test = arg;
+       RADIUS_PACKET           *reqpkt = test->reqpkt;
+       ssize_t                  rv;
+
+retry:
+       rv = send(test->sock,
+           radius_get_data(reqpkt), radius_get_length(reqpkt), 0);
+       if (rv == -1) {
+               switch (errno) {
+               case EINTR:
+               case EAGAIN:
+                       goto retry;
+               default:
+                       break;
+               }
+
+               warn("send");
+       }
+
+       if (++test->tries >= test->res->tries)
+               return;
+
+       evtimer_add(&test->ev_send, &test->res->interval);
+}
+
+static void
+radius_test_recv(int sock, short revents, void *arg)
+{
+       struct radius_test      *test = arg;
+       RADIUS_PACKET           *respkt;
+       RADIUS_PACKET           *reqpkt = test->reqpkt;
+
+retry:
+       respkt = radius_recv(sock, 0);
+       if (respkt == NULL) {
+               switch (errno) {
+               case EINTR:
+               case EAGAIN:
+                       goto retry;
+               default:
+                       break;
+               }
+
+               warn("recv");
+               return;
+       }
+
+       radius_set_request_packet(respkt, reqpkt);
+       if (radius_get_id(respkt) == radius_get_id(reqpkt)) {
+               fprintf(stderr, "\nReceived:\n");
+               radius_dump(stdout, respkt, true, test->res->secret);
+
+               event_del(&test->ev_recv);
+               evtimer_del(&test->ev_send);
+               evtimer_del(&test->ev_timedout);
+               test->ecode = EXIT_SUCCESS;
+       }
+
+       radius_delete_packet(respkt);
+}
+
+static void
+radius_test_timedout(int thing, short revents, void *arg)
+{
+       struct radius_test      *test = arg;
+
+       event_del(&test->ev_recv);
 }
 
 static void

Reply via email to