krb5 commit: Add libkrad

Greg Hudson ghudson at MIT.EDU
Thu Jul 11 14:24:38 EDT 2013


https://github.com/krb5/krb5/commit/8b8f031c6e64360a26c484b548d2158944e09087
commit 8b8f031c6e64360a26c484b548d2158944e09087
Author: Nathaniel McCallum <npmccallum at redhat.com>
Date:   Thu Apr 4 13:39:21 2013 -0400

    Add libkrad
    
    The new library libkrad provides code for the parsing of RADIUS packets
    as well as client implementation based around libverto.  This library
    should be considered unstable.
    
    ticket: 7678 (new)

 src/configure.in             |    2 +-
 src/include/Makefile.in      |    1 +
 src/include/krad.h           |  264 +++++++++++++++++++++
 src/lib/Makefile.in          |    2 +-
 src/lib/krad/Makefile.in     |   74 ++++++
 src/lib/krad/attr.c          |  317 +++++++++++++++++++++++++
 src/lib/krad/attrset.c       |  244 +++++++++++++++++++
 src/lib/krad/client.c        |  335 ++++++++++++++++++++++++++
 src/lib/krad/code.c          |  111 +++++++++
 src/lib/krad/deps            |  156 ++++++++++++
 src/lib/krad/internal.h      |  155 ++++++++++++
 src/lib/krad/libkrad.exports |   23 ++
 src/lib/krad/packet.c        |  470 +++++++++++++++++++++++++++++++++++++
 src/lib/krad/remote.c        |  532 ++++++++++++++++++++++++++++++++++++++++++
 src/lib/krad/t_attr.c        |   89 +++++++
 src/lib/krad/t_attrset.c     |   98 ++++++++
 src/lib/krad/t_client.c      |  126 ++++++++++
 src/lib/krad/t_code.c        |   54 +++++
 src/lib/krad/t_daemon.h      |   92 ++++++++
 src/lib/krad/t_daemon.py     |   76 ++++++
 src/lib/krad/t_packet.c      |  194 +++++++++++++++
 src/lib/krad/t_remote.c      |  170 ++++++++++++++
 src/lib/krad/t_test.c        |   50 ++++
 src/lib/krad/t_test.h        |   60 +++++
 24 files changed, 3693 insertions(+), 2 deletions(-)

diff --git a/src/configure.in b/src/configure.in
index 02955bb..2569092 100644
--- a/src/configure.in
+++ b/src/configure.in
@@ -1350,7 +1350,7 @@ dnl	lib/krb5/ccache/ccapi
 	lib/rpc lib/rpc/unit-test
 
 	lib/kadm5 lib/kadm5/clnt lib/kadm5/srv lib/kadm5/unit-test
-
+	lib/krad
 	lib/apputils
 
 dnl	ccapi ccapi/lib ccapi/lib/unix ccapi/server ccapi/server/unix ccapi/test
diff --git a/src/include/Makefile.in b/src/include/Makefile.in
index 03f511c..b8ea640 100644
--- a/src/include/Makefile.in
+++ b/src/include/Makefile.in
@@ -149,5 +149,6 @@ install-headers-unix install:: krb5/krb5.h profile.h
 	$(INSTALL_DATA) $(srcdir)/krb5/kadm5_hook_plugin.h $(DESTDIR)$(KRB5_INCDIR)$(S)krb5$(S)kadm5_hook_plugin.h
 	$(INSTALL_DATA) profile.h $(DESTDIR)$(KRB5_INCDIR)$(S)profile.h
 	$(INSTALL_DATA) $(srcdir)/gssapi.h $(DESTDIR)$(KRB5_INCDIR)$(S)gssapi.h
+	$(INSTALL_DATA) $(srcdir)/krad.h $(DESTDIR)$(KRB5_INCDIR)/krad.h
 
 depend:: krb5/krb5.h $(BUILT_HEADERS)
diff --git a/src/include/krad.h b/src/include/krad.h
new file mode 100644
index 0000000..913464c
--- /dev/null
+++ b/src/include/krad.h
@@ -0,0 +1,264 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * This API is not considered as stable as the main krb5 API.
+ *
+ * - We may make arbitrary incompatible changes between feature releases
+ *   (e.g. from 1.12 to 1.13).
+ * - We will make some effort to avoid making incompatible changes for
+ *   bugfix releases, but will make them if necessary.
+ */
+
+#ifndef KRAD_H_
+#define KRAD_H_
+
+#include <krb5.h>
+#include <verto.h>
+#include <stddef.h>
+#include <stdio.h>
+
+#define KRAD_PACKET_SIZE_MAX 4096
+
+#define KRAD_SERVICE_TYPE_LOGIN 1
+#define KRAD_SERVICE_TYPE_FRAMED 2
+#define KRAD_SERVICE_TYPE_CALLBACK_LOGIN 3
+#define KRAD_SERVICE_TYPE_CALLBACK_FRAMED 4
+#define KRAD_SERVICE_TYPE_OUTBOUND 5
+#define KRAD_SERVICE_TYPE_ADMINISTRATIVE 6
+#define KRAD_SERVICE_TYPE_NAS_PROMPT 7
+#define KRAD_SERVICE_TYPE_AUTHENTICATE_ONLY 8
+#define KRAD_SERVICE_TYPE_CALLBACK_NAS_PROMPT 9
+#define KRAD_SERVICE_TYPE_CALL_CHECK 10
+#define KRAD_SERVICE_TYPE_CALLBACK_ADMINISTRATIVE 11
+
+typedef struct krad_attrset_st krad_attrset;
+typedef struct krad_packet_st krad_packet;
+typedef struct krad_client_st krad_client;
+typedef unsigned char krad_code;
+typedef unsigned char krad_attr;
+
+/* Called when a response is received or the request times out. */
+typedef void
+(*krad_cb)(krb5_error_code retval, const krad_packet *request,
+           const krad_packet *response, void *data);
+
+/*
+ * Called to iterate over a set of requests.  Either the callback will be
+ * called until it returns NULL, or it will be called with cancel = TRUE to
+ * terminate in the middle of an iteration.
+ */
+typedef const krad_packet *
+(*krad_packet_iter_cb)(void *data, krb5_boolean cancel);
+
+/*
+ * Code
+ */
+
+/* Convert a code name to its number. Only works for codes defined
+ * by RFC 2875 or 2882. Returns 0 if the name was not found. */
+krad_code
+krad_code_name2num(const char *name);
+
+/* Convert a code number to its name. Only works for attributes defined
+ * by RFC 2865 or 2882. Returns NULL if the name was not found. */
+const char *
+krad_code_num2name(krad_code code);
+
+/*
+ * Attribute
+ */
+
+/* Convert an attribute name to its number. Only works for attributes defined
+ * by RFC 2865. Returns 0 if the name was not found. */
+krad_attr
+krad_attr_name2num(const char *name);
+
+/* Convert an attribute number to its name. Only works for attributes defined
+ * by RFC 2865. Returns NULL if the name was not found. */
+const char *
+krad_attr_num2name(krad_attr type);
+
+/*
+ * Attribute set
+ */
+
+/* Create a new attribute set. */
+krb5_error_code
+krad_attrset_new(krb5_context ctx, krad_attrset **set);
+
+/* Create a deep copy of an attribute set. */
+krb5_error_code
+krad_attrset_copy(const krad_attrset *set, krad_attrset **copy);
+
+/* Free an attribute set. */
+void
+krad_attrset_free(krad_attrset *set);
+
+/* Add an attribute to a set. */
+krb5_error_code
+krad_attrset_add(krad_attrset *set, krad_attr type, const krb5_data *data);
+
+/* Add a four-octet unsigned number attribute to the given set. */
+krb5_error_code
+krad_attrset_add_number(krad_attrset *set, krad_attr type, krb5_ui_4 num);
+
+/* Delete the specified attribute. */
+void
+krad_attrset_del(krad_attrset *set, krad_attr type, size_t indx);
+
+/* Get the specified attribute. */
+const krb5_data *
+krad_attrset_get(const krad_attrset *set, krad_attr type, size_t indx);
+
+/*
+ * Packet
+ */
+
+/* Determine the bytes needed from the socket to get the whole packet.  Don't
+ * cache the return value as it can change! Returns -1 on EBADMSG. */
+ssize_t
+krad_packet_bytes_needed(const krb5_data *buffer);
+
+/* Free a packet. */
+void
+krad_packet_free(krad_packet *pkt);
+
+/*
+ * Create a new request packet.
+ *
+ * This function takes the attributes specified in set and converts them into a
+ * radius packet. The packet will have a randomized id. If cb is not NULL, it
+ * will be called passing data as the argument to iterate over a set of
+ * outstanding requests. In this case, the id will be both random and unique
+ * across the set of requests.
+ */
+krb5_error_code
+krad_packet_new_request(krb5_context ctx, const char *secret, krad_code code,
+                        const krad_attrset *set, krad_packet_iter_cb cb,
+                        void *data, krad_packet **request);
+
+/*
+ * Create a new response packet.
+ *
+ * This function is similar to krad_packet_new_requst() except that it crafts a
+ * packet in response to a request packet. This new packet will borrow values
+ * from the request such as the id and the authenticator.
+ */
+krb5_error_code
+krad_packet_new_response(krb5_context ctx, const char *secret, krad_code code,
+                         const krad_attrset *set, const krad_packet *request,
+                         krad_packet **response);
+
+/*
+ * Decode a request radius packet from krb5_data.
+ *
+ * The resulting decoded packet will be a request packet stored in *reqpkt.
+ *
+ * If cb is NULL, *duppkt will always be NULL.
+ *
+ * If cb is not NULL, it will be called (with the data argument) to iterate
+ * over a set of requests currently being processed. In this case, if the
+ * packet is a duplicate of an already received request, the original request
+ * will be set in *duppkt.
+ */
+krb5_error_code
+krad_packet_decode_request(krb5_context ctx, const char *secret,
+                           const krb5_data *buffer, krad_packet_iter_cb cb,
+                           void *data, const krad_packet **duppkt,
+                           krad_packet **reqpkt);
+
+/*
+ * Decode a response radius packet from krb5_data.
+ *
+ * The resulting decoded packet will be a response packet stored in *rsppkt.
+ *
+ * If cb is NULL, *reqpkt will always be NULL.
+ *
+ * If cb is not NULL, it will be called (with the data argument) to iterate
+ * over a set of requests awaiting responses. In this case, if the response
+ * packet matches one of these requests, the original request will be set in
+ * *reqpkt.
+ */
+krb5_error_code
+krad_packet_decode_response(krb5_context ctx, const char *secret,
+                            const krb5_data *buffer, krad_packet_iter_cb cb,
+                            void *data, const krad_packet **reqpkt,
+                            krad_packet **rsppkt);
+
+/* Encode packet. */
+const krb5_data *
+krad_packet_encode(const krad_packet *pkt);
+
+/* Get the code for the given packet. */
+krad_code
+krad_packet_get_code(const krad_packet *pkt);
+
+/* Get the specified attribute. */
+const krb5_data *
+krad_packet_get_attr(const krad_packet *pkt, krad_attr type, size_t indx);
+
+/*
+ * Client
+ */
+
+/* Create a new client. */
+krb5_error_code
+krad_client_new(krb5_context kctx, verto_ctx *vctx, krad_client **client);
+
+/* Free the client. */
+void
+krad_client_free(krad_client *client);
+
+/*
+ * Send a request to a radius server.
+ *
+ * The remote host may be specified by one of the following formats:
+ *  - /path/to/unix.socket
+ *  - IPv4
+ *  - IPv4:port
+ *  - IPv4:service
+ *  - [IPv6]
+ *  - [IPv6]:port
+ *  - [IPv6]:service
+ *  - hostname
+ *  - hostname:port
+ *  - hostname:service
+ *
+ * The timeout parameter (milliseconds) is the total timeout across all remote
+ * hosts (when DNS returns multiple entries) and all retries.
+ *
+ * The cb function will be called with the data argument when either a response
+ * is received or the request times out on all possible remote hosts.
+ */
+krb5_error_code
+krad_client_send(krad_client *rc, krad_code code, const krad_attrset *attrs,
+                 const char *remote, const char *secret, int timeout,
+                 size_t retries, krad_cb cb, void *data);
+
+#endif /* KRAD_H_ */
diff --git a/src/lib/Makefile.in b/src/lib/Makefile.in
index 485db40..4dde514 100644
--- a/src/lib/Makefile.in
+++ b/src/lib/Makefile.in
@@ -1,5 +1,5 @@
 mydir=lib
-SUBDIRS=crypto krb5 gssapi rpc kdb kadm5 apputils
+SUBDIRS=crypto krb5 gssapi rpc kdb kadm5 apputils krad
 WINSUBDIRS=crypto krb5 gssapi
 BUILDTOP=$(REL)..
 
diff --git a/src/lib/krad/Makefile.in b/src/lib/krad/Makefile.in
new file mode 100644
index 0000000..75431a0
--- /dev/null
+++ b/src/lib/krad/Makefile.in
@@ -0,0 +1,74 @@
+mydir=lib$(S)krad
+BUILDTOP=$(REL)..$(S)..
+RELDIR=krad
+
+RUN_SETUP=@KRB5_RUN_ENV@
+PROG_LIBPATH=-L$(TOPLIBD)
+PROG_RPATH=$(KRB5_LIBDIR)
+
+SHLIB_EXPLIBS=$(KRB5_BASE_LIBS) $(VERTO_LIBS)
+SHLIB_EXPDEPLIBS=$(KRB5_BASE_DEPLIBS) $(VERTO_DEPLIB)
+SHLIB_DIRS=-L$(TOPLIBD)
+SHLIB_RDIRS=$(KRB5_LIBDIR)
+
+LIBBASE=krad
+LIBMAJOR=0
+LIBMINOR=0
+
+STLIBOBJS=attr.o attrset.o client.o code.o packet.o remote.o
+LIBOBJS=$(OUTPRE)attr.$(OBJEXT) \
+	$(OUTPRE)attrset.$(OBJEXT) \
+	$(OUTPRE)client.$(OBJEXT) \
+	$(OUTPRE)code.$(OBJEXT) \
+	$(OUTPRE)packet.$(OBJEXT) \
+	$(OUTPRE)remote.$(OBJEXT)
+SRCS=attr.c attrset.c client.c code.c packet.c remote.c \
+	t_attr.c t_attrset.c t_client.c t_code.c t_packet.c t_remote.c t_test.c
+
+STOBJLISTS=OBJS.ST
+
+all-unix:: all-liblinks
+install-unix:: install-libs
+
+clean-unix:: clean-liblinks clean-libs clean-libobjs
+
+check-unix:: t_attr t_attrset t_code t_packet t_remote t_client
+	$(RUN_SETUP) $(VALGRIND) ./t_attr
+	$(RUN_SETUP) $(VALGRIND) ./t_attrset
+	$(RUN_SETUP) $(VALGRIND) ./t_code
+	$(RUN_SETUP) $(VALGRIND) ./t_packet $(PYTHON) $(srcdir)/t_daemon.py
+	$(RUN_SETUP) $(VALGRIND) ./t_remote $(PYTHON) $(srcdir)/t_daemon.py
+	$(RUN_SETUP) $(VALGRIND) ./t_client $(PYTHON) $(srcdir)/t_daemon.py
+
+TESTDEPS=t_test.o $(KRB5_BASE_DEPLIBS)
+TESTLIBS=t_test.o $(KRB5_BASE_LIBS)
+
+T_ATTR_OBJS=attr.o t_attr.o
+t_attr: $(T_ATTR_OBJS) $(TESTDEPS)
+	$(CC_LINK) -o $@ $(T_ATTR_OBJS) $(TESTLIBS)
+
+T_ATTRSET_OBJS=attr.o attrset.o t_attrset.o
+t_attrset: $(T_ATTRSET_OBJS) $(TESTDEPS)
+	$(CC_LINK) -o $@ $(T_ATTRSET_OBJS) $(TESTLIBS)
+
+T_CODE_OBJS=code.o t_code.o
+t_code: $(T_CODE_OBJS) $(TESTDEPS)
+	$(CC_LINK) -o $@ $(T_CODE_OBJS) $(TESTLIBS)
+
+T_PACKET_OBJS=attr.o attrset.o code.o packet.o t_packet.o
+t_packet: $(T_PACKET_OBJS) $(TESTDEPS)
+	$(CC_LINK) -o $@ $(T_PACKET_OBJS) $(TESTLIBS)
+
+T_REMOTE_OBJS=attr.o attrset.o code.o packet.o remote.o t_remote.o
+t_remote: $(T_REMOTE_OBJS) $(TESTDEPS) $(VERTO_DEPLIB)
+	$(CC_LINK) -o $@ $(T_REMOTE_OBJS) $(TESTLIBS) $(VERTO_LIBS)
+
+T_CLIENT_OBJS=attr.o attrset.o code.o packet.o remote.o client.o t_client.o
+t_client: $(T_CLIENT_OBJS) $(TESTDEPS) $(VERTO_DEPLIB)
+	$(CC_LINK) -o $@ $(T_CLIENT_OBJS) $(TESTLIBS) $(VERTO_LIBS)
+
+clean-unix:: clean-libobjs
+	$(RM) *.o t_attr t_attrset t_code t_packet t_remote t_client
+
+ at lib_frag@
+ at libobj_frag@
diff --git a/src/lib/krad/attr.c b/src/lib/krad/attr.c
new file mode 100644
index 0000000..9c13d9d
--- /dev/null
+++ b/src/lib/krad/attr.c
@@ -0,0 +1,317 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/attr.c - RADIUS attribute functions for libkrad */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <k5-int.h>
+#include "internal.h"
+
+#include <string.h>
+
+/* RFC 2865 */
+#define BLOCKSIZE 16
+
+typedef krb5_error_code
+(*attribute_transform_fn)(krb5_context ctx, const char *secret,
+                          const unsigned char *auth, const krb5_data *in,
+                          unsigned char outbuf[MAX_ATTRSIZE], size_t *outlen);
+
+typedef struct {
+    const char *name;
+    unsigned char minval;
+    unsigned char maxval;
+    attribute_transform_fn encode;
+    attribute_transform_fn decode;
+} attribute_record;
+
+static krb5_error_code
+user_password_encode(krb5_context ctx, const char *secret,
+                     const unsigned char *auth, const krb5_data *in,
+                     unsigned char outbuf[MAX_ATTRSIZE], size_t *outlen);
+
+static krb5_error_code
+user_password_decode(krb5_context ctx, const char *secret,
+                     const unsigned char *auth, const krb5_data *in,
+                     unsigned char outbuf[MAX_ATTRSIZE], size_t *outlen);
+
+static const attribute_record attributes[UCHAR_MAX] = {
+    {"User-Name", 1, MAX_ATTRSIZE, NULL, NULL},
+    {"User-Password", 1, 128, user_password_encode, user_password_decode},
+    {"CHAP-Password", 17, 17, NULL, NULL},
+    {"NAS-IP-Address", 4, 4, NULL, NULL},
+    {"NAS-Port", 4, 4, NULL, NULL},
+    {"Service-Type", 4, 4, NULL, NULL},
+    {"Framed-Protocol", 4, 4, NULL, NULL},
+    {"Framed-IP-Address", 4, 4, NULL, NULL},
+    {"Framed-IP-Netmask", 4, 4, NULL, NULL},
+    {"Framed-Routing", 4, 4, NULL, NULL},
+    {"Filter-Id", 1, MAX_ATTRSIZE, NULL, NULL},
+    {"Framed-MTU", 4, 4, NULL, NULL},
+    {"Framed-Compression", 4, 4, NULL, NULL},
+    {"Login-IP-Host", 4, 4, NULL, NULL},
+    {"Login-Service", 4, 4, NULL, NULL},
+    {"Login-TCP-Port", 4, 4, NULL, NULL},
+    {NULL, 0, 0, NULL, NULL}, /* Unassigned */
+    {"Reply-Message", 1, MAX_ATTRSIZE, NULL, NULL},
+    {"Callback-Number", 1, MAX_ATTRSIZE, NULL, NULL},
+    {"Callback-Id", 1, MAX_ATTRSIZE, NULL, NULL},
+    {NULL, 0, 0, NULL, NULL}, /* Unassigned */
+    {"Framed-Route", 1, MAX_ATTRSIZE, NULL, NULL},
+    {"Framed-IPX-Network", 4, 4, NULL, NULL},
+    {"State", 1, MAX_ATTRSIZE, NULL, NULL},
+    {"Class", 1, MAX_ATTRSIZE, NULL, NULL},
+    {"Vendor-Specific", 5, MAX_ATTRSIZE, NULL, NULL},
+    {"Session-Timeout", 4, 4, NULL, NULL},
+    {"Idle-Timeout", 4, 4, NULL, NULL},
+    {"Termination-Action", 4, 4, NULL, NULL},
+    {"Called-Station-Id", 1, MAX_ATTRSIZE, NULL, NULL},
+    {"Calling-Station-Id", 1, MAX_ATTRSIZE, NULL, NULL},
+    {"NAS-Identifier", 1, MAX_ATTRSIZE, NULL, NULL},
+    {"Proxy-State", 1, MAX_ATTRSIZE, NULL, NULL},
+    {"Login-LAT-Service", 1, MAX_ATTRSIZE, NULL, NULL},
+    {"Login-LAT-Node", 1, MAX_ATTRSIZE, NULL, NULL},
+    {"Login-LAT-Group", 32, 32, NULL, NULL},
+    {"Framed-AppleTalk-Link", 4, 4, NULL, NULL},
+    {"Framed-AppleTalk-Network", 4, 4, NULL, NULL},
+    {"Framed-AppleTalk-Zone", 1, MAX_ATTRSIZE, NULL, NULL},
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {NULL, 0, 0, NULL, NULL}, /* Reserved for accounting */
+    {"CHAP-Challenge", 5, MAX_ATTRSIZE, NULL, NULL},
+    {"NAS-Port-Type", 4, 4, NULL, NULL},
+    {"Port-Limit", 4, 4, NULL, NULL},
+    {"Login-LAT-Port", 1, MAX_ATTRSIZE, NULL, NULL},
+};
+
+/* Encode User-Password attribute. */
+static krb5_error_code
+user_password_encode(krb5_context ctx, const char *secret,
+                     const unsigned char *auth, const krb5_data *in,
+                     unsigned char outbuf[MAX_ATTRSIZE], size_t *outlen)
+{
+    const unsigned char *indx;
+    krb5_error_code retval;
+    unsigned int seclen;
+    krb5_checksum sum;
+    size_t blck, len, i;
+    krb5_data tmp;
+
+    /* Copy the input buffer to the (zero-padded) output buffer. */
+    len = (in->length + BLOCKSIZE - 1) / BLOCKSIZE * BLOCKSIZE;
+    if (len > MAX_ATTRSIZE)
+        return ENOBUFS;
+    memset(outbuf, 0, len);
+    memcpy(outbuf, in->data, in->length);
+
+    /* Create our temporary space for processing each block. */
+    seclen = strlen(secret);
+    retval = alloc_data(&tmp, seclen + BLOCKSIZE);
+    if (retval != 0)
+        return retval;
+
+    memcpy(tmp.data, secret, seclen);
+    for (blck = 0, indx = auth; blck * BLOCKSIZE < len; blck++) {
+        memcpy(tmp.data + seclen, indx, BLOCKSIZE);
+
+        retval = krb5_c_make_checksum(ctx, CKSUMTYPE_RSA_MD5, NULL, 0, &tmp,
+                                      &sum);
+        if (retval != 0) {
+            zap(tmp.data, tmp.length);
+            zap(outbuf, len);
+            krb5_free_data_contents(ctx, &tmp);
+            return retval;
+        }
+
+        for (i = 0; i < BLOCKSIZE; i++)
+            outbuf[blck * BLOCKSIZE + i] ^= sum.contents[i];
+        krb5_free_checksum_contents(ctx, &sum);
+
+        indx = &outbuf[blck * BLOCKSIZE];
+    }
+
+    zap(tmp.data, tmp.length);
+    krb5_free_data_contents(ctx, &tmp);
+    *outlen = len;
+    return 0;
+}
+
+/* Decode User-Password attribute. */
+static krb5_error_code
+user_password_decode(krb5_context ctx, const char *secret,
+                     const unsigned char *auth, const krb5_data *in,
+                     unsigned char outbuf[MAX_ATTRSIZE], size_t *outlen)
+{
+    const unsigned char *indx;
+    krb5_error_code retval;
+    unsigned int seclen;
+    krb5_checksum sum;
+    ssize_t blck, i;
+    krb5_data tmp;
+
+    if (in->length % BLOCKSIZE != 0)
+        return EINVAL;
+    if (in->length > MAX_ATTRSIZE)
+        return ENOBUFS;
+
+    /* Create our temporary space for processing each block. */
+    seclen = strlen(secret);
+    retval = alloc_data(&tmp, seclen + BLOCKSIZE);
+    if (retval != 0)
+        return retval;
+
+    memcpy(tmp.data, secret, seclen);
+    for (blck = 0, indx = auth; blck * BLOCKSIZE < in->length; blck++) {
+        memcpy(tmp.data + seclen, indx, BLOCKSIZE);
+
+        retval = krb5_c_make_checksum(ctx, CKSUMTYPE_RSA_MD5, NULL, 0,
+                                      &tmp, &sum);
+        if (retval != 0) {
+            zap(tmp.data, tmp.length);
+            zap(outbuf, in->length);
+            krb5_free_data_contents(ctx, &tmp);
+            return retval;
+        }
+
+        for (i = 0; i < BLOCKSIZE; i++) {
+            outbuf[blck * BLOCKSIZE + i] = in->data[blck * BLOCKSIZE + i] ^
+                sum.contents[i];
+        }
+        krb5_free_checksum_contents(ctx, &sum);
+
+        indx = (const unsigned char *)&in->data[blck * BLOCKSIZE];
+    }
+
+    /* Strip off trailing NULL bytes. */
+    *outlen = in->length;
+    while (*outlen > 0 && outbuf[*outlen - 1] == '\0')
+        (*outlen)--;
+
+    krb5_free_data_contents(ctx, &tmp);
+    return 0;
+}
+
+krb5_error_code
+kr_attr_valid(krad_attr type, const krb5_data *data)
+{
+    const attribute_record *ar;
+
+    if (type == 0)
+        return EINVAL;
+
+    ar = &attributes[type - 1];
+    return (data->length >= ar->minval && data->length <= ar->maxval) ? 0 :
+        EMSGSIZE;
+}
+
+krb5_error_code
+kr_attr_encode(krb5_context ctx, const char *secret,
+               const unsigned char *auth, krad_attr type,
+               const krb5_data *in, unsigned char outbuf[MAX_ATTRSIZE],
+               size_t *outlen)
+{
+    krb5_error_code retval;
+
+    retval = kr_attr_valid(type, in);
+    if (retval != 0)
+        return retval;
+
+    if (attributes[type - 1].encode == NULL) {
+        if (in->length > MAX_ATTRSIZE)
+            return ENOBUFS;
+
+        *outlen = in->length;
+        memcpy(outbuf, in->data, in->length);
+        return 0;
+    }
+
+    return attributes[type - 1].encode(ctx, secret, auth, in, outbuf, outlen);
+}
+
+krb5_error_code
+kr_attr_decode(krb5_context ctx, const char *secret, const unsigned char *auth,
+               krad_attr type, const krb5_data *in,
+               unsigned char outbuf[MAX_ATTRSIZE], size_t *outlen)
+{
+    krb5_error_code retval;
+
+    retval = kr_attr_valid(type, in);
+    if (retval != 0)
+        return retval;
+
+    if (attributes[type - 1].encode == NULL) {
+        if (in->length > MAX_ATTRSIZE)
+            return ENOBUFS;
+
+        *outlen = in->length;
+        memcpy(outbuf, in->data, in->length);
+        return 0;
+    }
+
+    return attributes[type - 1].decode(ctx, secret, auth, in, outbuf, outlen);
+}
+
+krad_attr
+krad_attr_name2num(const char *name)
+{
+    unsigned char i;
+
+    for (i = 0; i < UCHAR_MAX; i++) {
+        if (attributes[i].name == NULL)
+            continue;
+
+        if (strcmp(attributes[i].name, name) == 0)
+            return i + 1;
+    }
+
+    return 0;
+}
+
+const char *
+krad_attr_num2name(krad_attr type)
+{
+    if (type == 0)
+        return NULL;
+
+    return attributes[type - 1].name;
+}
diff --git a/src/lib/krad/attrset.c b/src/lib/krad/attrset.c
new file mode 100644
index 0000000..fbd0621
--- /dev/null
+++ b/src/lib/krad/attrset.c
@@ -0,0 +1,244 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/attrset.c - RADIUS attribute set functions for libkrad */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <k5-int.h>
+#include <k5-queue.h>
+#include "internal.h"
+
+#include <string.h>
+
+TAILQ_HEAD(attr_head, attr_st);
+
+typedef struct attr_st attr;
+struct attr_st {
+    TAILQ_ENTRY(attr_st) list;
+    krad_attr type;
+    krb5_data attr;
+    char buffer[MAX_ATTRSIZE];
+};
+
+struct krad_attrset_st {
+    krb5_context ctx;
+    struct attr_head list;
+};
+
+krb5_error_code
+krad_attrset_new(krb5_context ctx, krad_attrset **set)
+{
+    krad_attrset *tmp;
+
+    tmp = calloc(1, sizeof(krad_attrset));
+    if (tmp == NULL)
+        return ENOMEM;
+    tmp->ctx = ctx;
+    TAILQ_INIT(&tmp->list);
+
+    *set = tmp;
+    return 0;
+}
+
+void
+krad_attrset_free(krad_attrset *set)
+{
+    attr *a;
+
+    if (set == NULL)
+        return;
+
+    while (!TAILQ_EMPTY(&set->list)) {
+        a = TAILQ_FIRST(&set->list);
+        TAILQ_REMOVE(&set->list, a, list);
+        zap(a->buffer, sizeof(a->buffer));
+        free(a);
+    }
+
+    free(set);
+}
+
+krb5_error_code
+krad_attrset_add(krad_attrset *set, krad_attr type, const krb5_data *data)
+{
+    krb5_error_code retval;
+    attr *tmp;
+
+    retval = kr_attr_valid(type, data);
+    if (retval != 0)
+        return retval;
+
+    tmp = calloc(1, sizeof(attr));
+    if (tmp == NULL)
+        return ENOMEM;
+
+    tmp->type = type;
+    tmp->attr = make_data(tmp->buffer, data->length);
+    memcpy(tmp->attr.data, data->data, data->length);
+
+    TAILQ_INSERT_TAIL(&set->list, tmp, list);
+    return 0;
+}
+
+krb5_error_code
+krad_attrset_add_number(krad_attrset *set, krad_attr type, krb5_ui_4 num)
+{
+    krb5_data data;
+
+    num = htonl(num);
+    data = make_data(&num, sizeof(num));
+    return krad_attrset_add(set, type, &data);
+}
+
+void
+krad_attrset_del(krad_attrset *set, krad_attr type, size_t indx)
+{
+    attr *a;
+
+    TAILQ_FOREACH(a, &set->list, list) {
+        if (a->type == type && indx-- == 0) {
+            TAILQ_REMOVE(&set->list, a, list);
+            zap(a->buffer, sizeof(a->buffer));
+            free(a);
+            return;
+        }
+    }
+}
+
+const krb5_data *
+krad_attrset_get(const krad_attrset *set, krad_attr type, size_t indx)
+{
+    attr *a;
+
+    TAILQ_FOREACH(a, &set->list, list) {
+        if (a->type == type && indx-- == 0)
+            return &a->attr;
+    }
+
+    return NULL;
+}
+
+krb5_error_code
+krad_attrset_copy(const krad_attrset *set, krad_attrset **copy)
+{
+    krb5_error_code retval;
+    krad_attrset *tmp;
+    attr *a;
+
+    retval = krad_attrset_new(set->ctx, &tmp);
+    if (retval != 0)
+        return retval;
+
+    TAILQ_FOREACH(a, &set->list, list) {
+        retval = krad_attrset_add(tmp, a->type, &a->attr);
+        if (retval != 0) {
+            krad_attrset_free(tmp);
+            return retval;
+        }
+    }
+
+    *copy = tmp;
+    return 0;
+}
+
+krb5_error_code
+kr_attrset_encode(const krad_attrset *set, const char *secret,
+                  const unsigned char *auth,
+                  unsigned char outbuf[MAX_ATTRSETSIZE], size_t *outlen)
+{
+    unsigned char buffer[MAX_ATTRSIZE];
+    krb5_error_code retval;
+    size_t i = 0, attrlen;
+    attr *a;
+
+    if (set == NULL) {
+        *outlen = 0;
+        return 0;
+    }
+
+    TAILQ_FOREACH(a, &set->list, list) {
+        retval = kr_attr_encode(set->ctx, secret, auth, a->type, &a->attr,
+                                buffer, &attrlen);
+        if (retval != 0)
+            return retval;
+
+        if (i + attrlen + 2 > MAX_ATTRSETSIZE)
+            return EMSGSIZE;
+
+        outbuf[i++] = a->type;
+        outbuf[i++] = attrlen + 2;
+        memcpy(&outbuf[i], buffer, attrlen);
+        i += attrlen;
+    }
+
+    *outlen = i;
+    return 0;
+}
+
+krb5_error_code
+kr_attrset_decode(krb5_context ctx, const krb5_data *in, const char *secret,
+                  const unsigned char *auth, krad_attrset **set_out)
+{
+    unsigned char buffer[MAX_ATTRSIZE];
+    krb5_data tmp;
+    krb5_error_code retval;
+    krad_attr type;
+    krad_attrset *set;
+    size_t i, len;
+
+    *set_out = NULL;
+
+    retval = krad_attrset_new(ctx, &set);
+    if (retval != 0)
+        return retval;
+
+    for (i = 0; i + 2 < in->length; ) {
+        type = in->data[i++];
+        tmp = make_data(&in->data[i + 1], in->data[i] - 2);
+        i += tmp.length + 1;
+
+        retval = (in->length < i) ? EBADMSG : 0;
+        if (retval != 0)
+            goto cleanup;
+
+        retval = kr_attr_decode(ctx, secret, auth, type, &tmp, buffer, &len);
+        if (retval != 0)
+            goto cleanup;
+
+        tmp = make_data(buffer, len);
+        retval = krad_attrset_add(set, type, &tmp);
+        if (retval != 0)
+            goto cleanup;
+    }
+
+    *set_out = set;
+    set = NULL;
+
+cleanup:
+    zap(buffer, sizeof(buffer));
+    krad_attrset_free(set);
+    return retval;
+}
diff --git a/src/lib/krad/client.c b/src/lib/krad/client.c
new file mode 100644
index 0000000..0c37680
--- /dev/null
+++ b/src/lib/krad/client.c
@@ -0,0 +1,335 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/client.c - Client request code for libkrad */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <k5-queue.h>
+#include "internal.h"
+
+#include <string.h>
+#include <sys/un.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <limits.h>
+
+LIST_HEAD(server_head, server_st);
+
+typedef struct remote_state_st remote_state;
+typedef struct request_st request;
+typedef struct server_st server;
+
+struct remote_state_st {
+    const krad_packet *packet;
+    krad_remote *remote;
+};
+
+struct request_st {
+    krad_client *rc;
+
+    krad_code code;
+    krad_attrset *attrs;
+    int timeout;
+    size_t retries;
+    krad_cb cb;
+    void *data;
+
+    remote_state *remotes;
+    ssize_t current;
+    ssize_t count;
+};
+
+struct server_st {
+    krad_remote *serv;
+    time_t last;
+    LIST_ENTRY(server_st) list;
+};
+
+struct krad_client_st {
+    krb5_context kctx;
+    verto_ctx *vctx;
+    struct server_head servers;
+};
+
+/* Return either a pre-existing server that matches the address info and the
+ * secret, or create a new one. */
+static krb5_error_code
+get_server(krad_client *rc, const struct addrinfo *ai, const char *secret,
+           krad_remote **out)
+{
+    krb5_error_code retval;
+    time_t currtime;
+    server *srv;
+
+    if (time(&currtime) == (time_t)-1)
+        return errno;
+
+    LIST_FOREACH(srv, &rc->servers, list) {
+        if (kr_remote_equals(srv->serv, ai, secret)) {
+            srv->last = currtime;
+            *out = srv->serv;
+            return 0;
+        }
+    }
+
+    srv = calloc(1, sizeof(server));
+    if (srv == NULL)
+        return ENOMEM;
+    srv->last = currtime;
+
+    retval = kr_remote_new(rc->kctx, rc->vctx, ai, secret, &srv->serv);
+    if (retval != 0) {
+        free(srv);
+        return retval;
+    }
+
+    LIST_INSERT_HEAD(&rc->servers, srv, list);
+    *out = srv->serv;
+    return 0;
+}
+
+/* Free a request. */
+static void
+request_free(request *req)
+{
+    krad_attrset_free(req->attrs);
+    free(req->remotes);
+    free(req);
+}
+
+/* Create a request. */
+static krb5_error_code
+request_new(krad_client *rc, krad_code code, const krad_attrset *attrs,
+            const struct addrinfo *ai, const char *secret, int timeout,
+            size_t retries, krad_cb cb, void *data, request **req)
+{
+    const struct addrinfo *tmp;
+    krb5_error_code retval;
+    request *rqst;
+    size_t i;
+
+    if (ai == NULL)
+        return EINVAL;
+
+    rqst = calloc(1, sizeof(request));
+    if (rqst == NULL)
+        return ENOMEM;
+
+    for (tmp = ai; tmp != NULL; tmp = tmp->ai_next)
+        rqst->count++;
+
+    rqst->rc = rc;
+    rqst->code = code;
+    rqst->cb = cb;
+    rqst->data = data;
+    rqst->timeout = timeout / rqst->count;
+    rqst->retries = retries;
+
+    retval = krad_attrset_copy(attrs, &rqst->attrs);
+    if (retval != 0) {
+        request_free(rqst);
+        return retval;
+    }
+
+    rqst->remotes = calloc(rqst->count + 1, sizeof(remote_state));
+    if (rqst->remotes == NULL) {
+        request_free(rqst);
+        return ENOMEM;
+    }
+
+    i = 0;
+    for (tmp = ai; tmp != NULL; tmp = tmp->ai_next) {
+        retval = get_server(rc, tmp, secret, &rqst->remotes[i++].remote);
+        if (retval != 0) {
+            request_free(rqst);
+            return retval;
+        }
+    }
+
+    *req = rqst;
+    return 0;
+}
+
+/* Close remotes that haven't been used in a while. */
+static void
+age(struct server_head *head, time_t currtime)
+{
+    server *srv, *tmp;
+
+    LIST_FOREACH_SAFE(srv, head, list, tmp) {
+        if (currtime == (time_t)-1 || currtime - srv->last > 60 * 60) {
+            LIST_REMOVE(srv, list);
+            kr_remote_free(srv->serv);
+            free(srv);
+        }
+    }
+}
+
+/* Handle a response from a server (or related errors). */
+static void
+on_response(krb5_error_code retval, const krad_packet *reqp,
+            const krad_packet *rspp, void *data)
+{
+    request *req = data;
+    time_t currtime;
+    size_t i;
+
+    /* Do nothing if we are already completed. */
+    if (req->count < 0)
+        return;
+
+    /* If we have timed out and have more remotes to try, do so. */
+    if (retval == ETIMEDOUT && req->remotes[++req->current].remote != NULL) {
+        retval = kr_remote_send(req->remotes[req->current].remote, req->code,
+                                req->attrs, on_response, req, req->timeout,
+                                req->retries,
+                                &req->remotes[req->current].packet);
+        if (retval == 0)
+            return;
+    }
+
+    /* Mark the request as complete. */
+    req->count = -1;
+
+    /* Inform the callback. */
+    req->cb(retval, reqp, rspp, req->data);
+
+    /* Cancel the outstanding packets. */
+    for (i = 0; req->remotes[i].remote != NULL; i++)
+        kr_remote_cancel(req->remotes[i].remote, req->remotes[i].packet);
+
+    /* Age out servers that haven't been used in a while. */
+    if (time(&currtime) != (time_t)-1)
+        age(&req->rc->servers, currtime);
+
+    request_free(req);
+}
+
+krb5_error_code
+krad_client_new(krb5_context kctx, verto_ctx *vctx, krad_client **out)
+{
+    krad_client *tmp;
+
+    tmp = calloc(1, sizeof(krad_client));
+    if (tmp == NULL)
+        return ENOMEM;
+
+    tmp->kctx = kctx;
+    tmp->vctx = vctx;
+
+    *out = tmp;
+    return 0;
+}
+
+void
+krad_client_free(krad_client *rc)
+{
+    if (rc == NULL)
+        return;
+
+    age(&rc->servers, -1);
+    free(rc);
+}
+
+static krb5_error_code
+resolve_remote(const char *remote, struct addrinfo **ai)
+{
+    const char *svc = "radius";
+    krb5_error_code retval;
+    struct addrinfo hints;
+    char *sep, *srv;
+
+    /* Isolate the port number if it exists. */
+    srv = strdup(remote);
+    if (srv == NULL)
+        return ENOMEM;
+
+    if (srv[0] == '[') {
+        /* IPv6 */
+        sep = strrchr(srv, ']');
+        if (sep != NULL && sep[1] == ':') {
+            sep[1] = '\0';
+            svc = &sep[2];
+        }
+    } else {
+        /* IPv4 or DNS */
+        sep = strrchr(srv, ':');
+        if (sep != NULL && sep[1] != '\0') {
+            sep[0] = '\0';
+            svc = &sep[1];
+        }
+    }
+
+    /* Perform the lookup. */
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_socktype = SOCK_DGRAM;
+    retval = gai_error_code(getaddrinfo(srv, svc, &hints, ai));
+    free(srv);
+    return retval;
+}
+
+krb5_error_code
+krad_client_send(krad_client *rc, krad_code code, const krad_attrset *attrs,
+                 const char *remote, const char *secret, int timeout,
+                 size_t retries, krad_cb cb, void *data)
+{
+    struct addrinfo usock, *ai = NULL;
+    krb5_error_code retval;
+    struct sockaddr_un ua;
+    request *req;
+
+    if (remote[0] == '/') {
+        ua.sun_family = AF_UNIX;
+        snprintf(ua.sun_path, sizeof(ua.sun_path), "%s", remote);
+        memset(&usock, 0, sizeof(usock));
+        usock.ai_family = AF_UNIX;
+        usock.ai_socktype = SOCK_STREAM;
+        usock.ai_addr = (struct sockaddr *)&ua;
+        usock.ai_addrlen = sizeof(ua);
+
+        retval = request_new(rc, code, attrs, &usock, secret, timeout, retries,
+                             cb, data, &req);
+    } else {
+        retval = resolve_remote(remote, &ai);
+        if (retval == 0) {
+            retval = request_new(rc, code, attrs, ai, secret, timeout, retries,
+                                 cb, data, &req);
+            freeaddrinfo(ai);
+        }
+    }
+    if (retval != 0)
+        return retval;
+
+    retval = kr_remote_send(req->remotes[req->current].remote, req->code,
+                            req->attrs, on_response, req, req->timeout,
+                            req->retries, &req->remotes[req->current].packet);
+    if (retval != 0) {
+        request_free(req);
+        return retval;
+    }
+
+    return 0;
+}
diff --git a/src/lib/krad/code.c b/src/lib/krad/code.c
new file mode 100644
index 0000000..16871bb
--- /dev/null
+++ b/src/lib/krad/code.c
@@ -0,0 +1,111 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/code.c - RADIUS code name table for libkrad */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "internal.h"
+
+#include <string.h>
+
+static const char *codes[UCHAR_MAX] = {
+    "Access-Request",
+    "Access-Accept",
+    "Access-Reject",
+    "Accounting-Request",
+    "Accounting-Response",
+    "Accounting-Status",
+    "Password-Request",
+    "Password-Ack",
+    "Password-Reject",
+    "Accounting-Message",
+    "Access-Challenge",
+    "Status-Server",
+    "Status-Client",
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    "Resource-Free-Request",
+    "Resource-Free-Response",
+    "Resource-Query-Request",
+    "Resource-Query-Response",
+    "Alternate-Resource-Reclaim-Request",
+    "NAS-Reboot-Request",
+    "NAS-Reboot-Response",
+    NULL,
+    "Next-Passcode",
+    "New-Pin",
+    "Terminate-Session",
+    "Password-Expired",
+    "Event-Request",
+    "Event-Response",
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    "Disconnect-Request",
+    "Disconnect-Ack",
+    "Disconnect-Nak",
+    "Change-Filters-Request",
+    "Change-Filters-Ack",
+    "Change-Filters-Nak",
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    "IP-Address-Allocate",
+    "IP-Address-Release",
+};
+
+krad_code
+krad_code_name2num(const char *name)
+{
+    unsigned char i;
+
+    for (i = 0; i < UCHAR_MAX; i++) {
+        if (codes[i] == NULL)
+            continue;
+
+        if (strcmp(codes[i], name) == 0)
+            return ++i;
+    }
+
+    return 0;
+}
+
+const char *
+krad_code_num2name(krad_code code)
+{
+    if (code == 0)
+        return NULL;
+
+    return codes[code - 1];
+}
diff --git a/src/lib/krad/deps b/src/lib/krad/deps
new file mode 100644
index 0000000..8171f94
--- /dev/null
+++ b/src/lib/krad/deps
@@ -0,0 +1,156 @@
+#
+# Generated makefile dependencies follow.
+#
+attr.so attr.po $(OUTPRE)attr.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
+  $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \
+  $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(VERTO_DEPS) \
+  $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \
+  $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \
+  $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-platform.h \
+  $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-thread.h \
+  $(top_srcdir)/include/k5-trace.h $(top_srcdir)/include/krad.h \
+  $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \
+  $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/port-sockets.h \
+  $(top_srcdir)/include/socket-utils.h attr.c internal.h
+attrset.so attrset.po $(OUTPRE)attrset.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
+  $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \
+  $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(VERTO_DEPS) \
+  $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \
+  $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \
+  $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-platform.h \
+  $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-queue.h \
+  $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \
+  $(top_srcdir)/include/krad.h $(top_srcdir)/include/krb5.h \
+  $(top_srcdir)/include/krb5/authdata_plugin.h $(top_srcdir)/include/krb5/plugin.h \
+  $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \
+  attrset.c internal.h
+client.so client.po $(OUTPRE)client.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
+  $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \
+  $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(VERTO_DEPS) \
+  $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \
+  $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \
+  $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-platform.h \
+  $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-queue.h \
+  $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \
+  $(top_srcdir)/include/krad.h $(top_srcdir)/include/krb5.h \
+  $(top_srcdir)/include/krb5/authdata_plugin.h $(top_srcdir)/include/krb5/plugin.h \
+  $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \
+  client.c internal.h
+code.so code.po $(OUTPRE)code.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
+  $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \
+  $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(VERTO_DEPS) \
+  $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \
+  $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \
+  $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-platform.h \
+  $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-thread.h \
+  $(top_srcdir)/include/k5-trace.h $(top_srcdir)/include/krad.h \
+  $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \
+  $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/port-sockets.h \
+  $(top_srcdir)/include/socket-utils.h code.c internal.h
+packet.so packet.po $(OUTPRE)packet.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
+  $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \
+  $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(VERTO_DEPS) \
+  $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \
+  $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \
+  $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-platform.h \
+  $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-thread.h \
+  $(top_srcdir)/include/k5-trace.h $(top_srcdir)/include/krad.h \
+  $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \
+  $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/port-sockets.h \
+  $(top_srcdir)/include/socket-utils.h internal.h packet.c
+remote.so remote.po $(OUTPRE)remote.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
+  $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \
+  $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(VERTO_DEPS) \
+  $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \
+  $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \
+  $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-platform.h \
+  $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-queue.h \
+  $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \
+  $(top_srcdir)/include/krad.h $(top_srcdir)/include/krb5.h \
+  $(top_srcdir)/include/krb5/authdata_plugin.h $(top_srcdir)/include/krb5/plugin.h \
+  $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \
+  internal.h remote.c
+t_attr.so t_attr.po $(OUTPRE)t_attr.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
+  $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \
+  $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(VERTO_DEPS) \
+  $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \
+  $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \
+  $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-platform.h \
+  $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-thread.h \
+  $(top_srcdir)/include/k5-trace.h $(top_srcdir)/include/krad.h \
+  $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \
+  $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/port-sockets.h \
+  $(top_srcdir)/include/socket-utils.h internal.h t_attr.c \
+  t_test.h
+t_attrset.so t_attrset.po $(OUTPRE)t_attrset.$(OBJEXT): \
+  $(BUILDTOP)/include/autoconf.h $(BUILDTOP)/include/krb5/krb5.h \
+  $(BUILDTOP)/include/osconf.h $(BUILDTOP)/include/profile.h \
+  $(COM_ERR_DEPS) $(VERTO_DEPS) $(top_srcdir)/include/k5-buf.h \
+  $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h \
+  $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h \
+  $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h \
+  $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \
+  $(top_srcdir)/include/krad.h $(top_srcdir)/include/krb5.h \
+  $(top_srcdir)/include/krb5/authdata_plugin.h $(top_srcdir)/include/krb5/plugin.h \
+  $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \
+  internal.h t_attrset.c t_test.h
+t_client.so t_client.po $(OUTPRE)t_client.$(OBJEXT): \
+  $(BUILDTOP)/include/autoconf.h $(BUILDTOP)/include/krb5/krb5.h \
+  $(BUILDTOP)/include/osconf.h $(BUILDTOP)/include/profile.h \
+  $(COM_ERR_DEPS) $(VERTO_DEPS) $(top_srcdir)/include/k5-buf.h \
+  $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h \
+  $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h \
+  $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h \
+  $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \
+  $(top_srcdir)/include/krad.h $(top_srcdir)/include/krb5.h \
+  $(top_srcdir)/include/krb5/authdata_plugin.h $(top_srcdir)/include/krb5/plugin.h \
+  $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \
+  internal.h t_client.c t_daemon.h t_test.h
+t_code.so t_code.po $(OUTPRE)t_code.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
+  $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \
+  $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(VERTO_DEPS) \
+  $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \
+  $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \
+  $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-platform.h \
+  $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-thread.h \
+  $(top_srcdir)/include/k5-trace.h $(top_srcdir)/include/krad.h \
+  $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \
+  $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/port-sockets.h \
+  $(top_srcdir)/include/socket-utils.h internal.h t_code.c \
+  t_test.h
+t_packet.so t_packet.po $(OUTPRE)t_packet.$(OBJEXT): \
+  $(BUILDTOP)/include/autoconf.h $(BUILDTOP)/include/krb5/krb5.h \
+  $(BUILDTOP)/include/osconf.h $(BUILDTOP)/include/profile.h \
+  $(COM_ERR_DEPS) $(VERTO_DEPS) $(top_srcdir)/include/k5-buf.h \
+  $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h \
+  $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h \
+  $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h \
+  $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \
+  $(top_srcdir)/include/krad.h $(top_srcdir)/include/krb5.h \
+  $(top_srcdir)/include/krb5/authdata_plugin.h $(top_srcdir)/include/krb5/plugin.h \
+  $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \
+  internal.h t_daemon.h t_packet.c t_test.h
+t_remote.so t_remote.po $(OUTPRE)t_remote.$(OBJEXT): \
+  $(BUILDTOP)/include/autoconf.h $(BUILDTOP)/include/krb5/krb5.h \
+  $(BUILDTOP)/include/osconf.h $(BUILDTOP)/include/profile.h \
+  $(COM_ERR_DEPS) $(VERTO_DEPS) $(top_srcdir)/include/k5-buf.h \
+  $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h \
+  $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h \
+  $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h \
+  $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \
+  $(top_srcdir)/include/krad.h $(top_srcdir)/include/krb5.h \
+  $(top_srcdir)/include/krb5/authdata_plugin.h $(top_srcdir)/include/krb5/plugin.h \
+  $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \
+  internal.h t_daemon.h t_remote.c t_test.h
+t_test.so t_test.po $(OUTPRE)t_test.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
+  $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \
+  $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(VERTO_DEPS) \
+  $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \
+  $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \
+  $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-platform.h \
+  $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-thread.h \
+  $(top_srcdir)/include/k5-trace.h $(top_srcdir)/include/krad.h \
+  $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \
+  $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/port-sockets.h \
+  $(top_srcdir)/include/socket-utils.h internal.h t_test.c \
+  t_test.h
diff --git a/src/lib/krad/internal.h b/src/lib/krad/internal.h
new file mode 100644
index 0000000..996a893
--- /dev/null
+++ b/src/lib/krad/internal.h
@@ -0,0 +1,155 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/internal.h - Internal declarations for libkrad */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef INTERNAL_H_
+#define INTERNAL_H_
+
+#include <k5-int.h>
+#include "krad.h"
+
+#include <errno.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+
+#ifndef UCHAR_MAX
+#define UCHAR_MAX 255
+#endif
+
+/* RFC 2865 */
+#define MAX_ATTRSIZE (UCHAR_MAX - 2)
+#define MAX_ATTRSETSIZE (KRAD_PACKET_SIZE_MAX - 20)
+
+typedef struct krad_remote_st krad_remote;
+
+/* Validate constraints of an attribute. */
+krb5_error_code
+kr_attr_valid(krad_attr type, const krb5_data *data);
+
+/* Encode an attribute. */
+krb5_error_code
+kr_attr_encode(krb5_context ctx, const char *secret, const unsigned char *auth,
+               krad_attr type, const krb5_data *in,
+               unsigned char outbuf[MAX_ATTRSIZE], size_t *outlen);
+
+/* Decode an attribute. */
+krb5_error_code
+kr_attr_decode(krb5_context ctx, const char *secret, const unsigned char *auth,
+               krad_attr type, const krb5_data *in,
+               unsigned char outbuf[MAX_ATTRSIZE], size_t *outlen);
+
+/* Encode the attributes into the buffer. */
+krb5_error_code
+kr_attrset_encode(const krad_attrset *set, const char *secret,
+                  const unsigned char *auth,
+                  unsigned char outbuf[MAX_ATTRSETSIZE], size_t *outlen);
+
+/* Decode attributes from a buffer. */
+krb5_error_code
+kr_attrset_decode(krb5_context ctx, const krb5_data *in, const char *secret,
+                  const unsigned char *auth, krad_attrset **set);
+
+/* Create a new remote object which manages a socket and the state of
+ * outstanding requests. */
+krb5_error_code
+kr_remote_new(krb5_context kctx, verto_ctx *vctx, const struct addrinfo *info,
+              const char *secret, krad_remote **rr);
+
+/* Free a remote object. */
+void
+kr_remote_free(krad_remote *rr);
+
+/*
+ * Send the packet to the remote. The cb will be called when a response is
+ * received, the request times out, the request is canceled or an error occurs.
+ *
+ * The timeout parameter is the total timeout across all retries in
+ * milliseconds.
+ *
+ * If the cb is called with a retval of ETIMEDOUT it indicates that the alloted
+ * time has elapsed. However, in the case of a timeout, we continue to listen
+ * for the packet until krad_remote_cancel() is called or a response is
+ * received. This means that cb will always be called twice in the event of a
+ * timeout. This permits you to pursue other remotes while still listening for
+ * a response from the first one.
+ */
+krb5_error_code
+kr_remote_send(krad_remote *rr, krad_code code, krad_attrset *attrs,
+               krad_cb cb, void *data, int timeout, size_t retries,
+               const krad_packet **pkt);
+
+/* Remove packet from the queue of requests awaiting responses. */
+void
+kr_remote_cancel(krad_remote *rr, const krad_packet *pkt);
+
+/* Determine if this remote object refers to the remote resource identified
+ * by the addrinfo struct and the secret. */
+krb5_boolean
+kr_remote_equals(const krad_remote *rr, const struct addrinfo *info,
+                 const char *secret);
+
+/* Adapted from lib/krb5/os/sendto_kdc.c. */
+static inline krb5_error_code
+gai_error_code(int err)
+{
+    switch (err) {
+    case 0:
+        return 0;
+    case EAI_BADFLAGS:
+    case EAI_FAMILY:
+    case EAI_SOCKTYPE:
+    case EAI_SERVICE:
+#ifdef EAI_ADDRFAMILY
+    case EAI_ADDRFAMILY:
+#endif
+        return EINVAL;
+    case EAI_AGAIN:
+        return EAGAIN;
+    case EAI_MEMORY:
+        return ENOMEM;
+#if defined(EAI_NODATA) && EAI_NODATA != EAI_NONAME
+    case EAI_NODATA:
+#endif
+    case EAI_NONAME:
+        return EADDRNOTAVAIL;
+#ifdef EAI_OVERFLOW
+    case EAI_OVERFLOW:
+        return EOVERFLOW;
+#endif
+#ifdef EAI_SYSTEM
+    case EAI_SYSTEM:
+        return errno;
+#endif
+    default:
+        return EINVAL;
+    }
+}
+
+#endif /* INTERNAL_H_ */
diff --git a/src/lib/krad/libkrad.exports b/src/lib/krad/libkrad.exports
new file mode 100644
index 0000000..fe3f159
--- /dev/null
+++ b/src/lib/krad/libkrad.exports
@@ -0,0 +1,23 @@
+krad_code_name2num
+krad_code_num2name
+krad_attr_name2num
+krad_attr_num2name
+krad_attrset_new
+krad_attrset_copy
+krad_attrset_free
+krad_attrset_add
+krad_attrset_add_number
+krad_attrset_del
+krad_attrset_get
+krad_packet_bytes_needed
+krad_packet_free
+krad_packet_new_request
+krad_packet_new_response
+krad_packet_decode_request
+krad_packet_decode_response
+krad_packet_encode
+krad_packet_get_code
+krad_packet_get_attr
+krad_client_new
+krad_client_free
+krad_client_send
diff --git a/src/lib/krad/packet.c b/src/lib/krad/packet.c
new file mode 100644
index 0000000..c597174
--- /dev/null
+++ b/src/lib/krad/packet.c
@@ -0,0 +1,470 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/packet.c - Packet functions for libkrad */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "internal.h"
+
+#include <string.h>
+
+#include <arpa/inet.h>
+
+typedef unsigned char uchar;
+
+/* RFC 2865 */
+#define OFFSET_CODE 0
+#define OFFSET_ID 1
+#define OFFSET_LENGTH 2
+#define OFFSET_AUTH 4
+#define OFFSET_ATTR 20
+#define AUTH_FIELD_SIZE (OFFSET_ATTR - OFFSET_AUTH)
+
+#define offset(d, o) (&(d)->data[o])
+#define pkt_code_get(p) (*(krad_code *)offset(&(p)->pkt, OFFSET_CODE))
+#define pkt_code_set(p, v) (*(krad_code *)offset(&(p)->pkt, OFFSET_CODE)) = v
+#define pkt_id_get(p) (*(uchar *)offset(&(p)->pkt, OFFSET_ID))
+#define pkt_id_set(p, v) (*(uchar *)offset(&(p)->pkt, OFFSET_ID)) = v
+#define pkt_len_get(p)  load_16_be(offset(&(p)->pkt, OFFSET_LENGTH))
+#define pkt_len_set(p, v)  store_16_be(v, offset(&(p)->pkt, OFFSET_LENGTH))
+#define pkt_auth(p) ((uchar *)offset(&(p)->pkt, OFFSET_AUTH))
+#define pkt_attr(p) ((unsigned char *)offset(&(p)->pkt, OFFSET_ATTR))
+
+struct krad_packet_st {
+    char buffer[KRAD_PACKET_SIZE_MAX];
+    krad_attrset *attrset;
+    krb5_data pkt;
+};
+
+typedef struct {
+    uchar x[(UCHAR_MAX + 1) / 8];
+} idmap;
+
+/* Ensure the map is empty. */
+static inline void
+idmap_init(idmap *map)
+{
+    memset(map, 0, sizeof(*map));
+}
+
+/* Set an id as already allocated. */
+static inline void
+idmap_set(idmap *map, uchar id)
+{
+    map->x[id / 8] |= 1 << (id % 8);
+}
+
+/* Determine whether or not an id is used. */
+static inline krb5_boolean
+idmap_isset(const idmap *map, uchar id)
+{
+    return (map->x[id / 8] & (1 << (id % 8))) != 0;
+}
+
+/* Find an unused id starting the search at the value specified in id.
+ * NOTE: For optimal security, the initial value of id should be random. */
+static inline krb5_error_code
+idmap_find(const idmap *map, uchar *id)
+{
+    krb5_int16 i;
+
+    for (i = *id; i >= 0 && i <= UCHAR_MAX; (*id % 2 == 0) ? i++ : i--) {
+        if (!idmap_isset(map, i))
+            goto success;
+    }
+
+    for (i = *id; i >= 0 && i <= UCHAR_MAX; (*id % 2 == 1) ? i++ : i--) {
+        if (!idmap_isset(map, i))
+            goto success;
+    }
+
+    return ERANGE;
+
+success:
+    *id = i;
+    return 0;
+}
+
+/* Generate size bytes of random data into the buffer. */
+static inline krb5_error_code
+randomize(krb5_context ctx, void *buffer, unsigned int size)
+{
+    krb5_data rdata = make_data(buffer, size);
+    return krb5_c_random_make_octets(ctx, &rdata);
+}
+
+/* Generate a radius packet id. */
+static krb5_error_code
+id_generate(krb5_context ctx, krad_packet_iter_cb cb, void *data, uchar *id)
+{
+    krb5_error_code retval;
+    const krad_packet *tmp;
+    idmap used;
+    uchar i;
+
+    retval = randomize(ctx, &i, sizeof(i));
+    if (retval != 0) {
+        if (cb != NULL)
+            (*cb)(data, TRUE);
+        return retval;
+    }
+
+    if (cb != NULL) {
+        idmap_init(&used);
+        for (tmp = (*cb)(data, FALSE); tmp != NULL; tmp = (*cb)(data, FALSE))
+            idmap_set(&used, tmp->pkt.data[1]);
+
+        retval = idmap_find(&used, &i);
+        if (retval != 0)
+            return retval;
+    }
+
+    *id = i;
+    return 0;
+}
+
+/* Generate a random authenticator field. */
+static krb5_error_code
+auth_generate_random(krb5_context ctx, uchar *rauth)
+{
+    krb5_ui_4 trunctime;
+    time_t currtime;
+
+    /* Get the least-significant four bytes of the current time. */
+    currtime = time(NULL);
+    if (currtime == (time_t)-1)
+        return errno;
+    trunctime = (krb5_ui_4)currtime;
+    memcpy(rauth, &trunctime, sizeof(trunctime));
+
+    /* Randomize the rest of the buffer. */
+    return randomize(ctx, rauth + sizeof(trunctime),
+                     AUTH_FIELD_SIZE - sizeof(trunctime));
+}
+
+/* Generate a response authenticator field. */
+static krb5_error_code
+auth_generate_response(krb5_context ctx, const char *secret,
+                       const krad_packet *response, const uchar *auth,
+                       uchar *rauth)
+{
+    krb5_error_code retval;
+    krb5_checksum hash;
+    krb5_data data;
+
+    /* Allocate the temporary buffer. */
+    retval = alloc_data(&data, response->pkt.length + strlen(secret));
+    if (retval != 0)
+        return retval;
+
+    /* Encoded RADIUS packet with the request's
+     * authenticator and the secret at the end. */
+    memcpy(data.data, response->pkt.data, response->pkt.length);
+    memcpy(data.data + OFFSET_AUTH, auth, AUTH_FIELD_SIZE);
+    memcpy(data.data + response->pkt.length, secret, strlen(secret));
+
+    /* Hash it. */
+    retval = krb5_c_make_checksum(ctx, CKSUMTYPE_RSA_MD5, NULL, 0, &data,
+                                  &hash);
+    free(data.data);
+    if (retval != 0)
+        return retval;
+
+    memcpy(rauth, hash.contents, AUTH_FIELD_SIZE);
+    krb5_free_checksum_contents(ctx, &hash);
+    return 0;
+}
+
+/* Create a new packet. */
+static krad_packet *
+packet_new()
+{
+    krad_packet *pkt;
+
+    pkt = calloc(1, sizeof(krad_packet));
+    if (pkt == NULL)
+        return NULL;
+    pkt->pkt = make_data(pkt->buffer, sizeof(pkt->buffer));
+
+    return pkt;
+}
+
+/* Set the attrset object by decoding the packet. */
+static krb5_error_code
+packet_set_attrset(krb5_context ctx, const char *secret, krad_packet *pkt)
+{
+    krb5_data tmp;
+
+    tmp = make_data(pkt_attr(pkt), pkt->pkt.length - OFFSET_ATTR);
+    return kr_attrset_decode(ctx, &tmp, secret, pkt_auth(pkt), &pkt->attrset);
+}
+
+ssize_t
+krad_packet_bytes_needed(const krb5_data *buffer)
+{
+    size_t len;
+
+    if (buffer->length < OFFSET_AUTH)
+        return OFFSET_AUTH - buffer->length;
+
+    len = load_16_be(offset(buffer, OFFSET_LENGTH));
+    if (len > KRAD_PACKET_SIZE_MAX)
+        return -1;
+
+    return (buffer->length > len) ? 0 : len - buffer->length;
+}
+
+void
+krad_packet_free(krad_packet *pkt)
+{
+    if (pkt)
+        krad_attrset_free(pkt->attrset);
+    free(pkt);
+}
+
+/* Create a new request packet. */
+krb5_error_code
+krad_packet_new_request(krb5_context ctx, const char *secret, krad_code code,
+                        const krad_attrset *set, krad_packet_iter_cb cb,
+                        void *data, krad_packet **request)
+{
+    krb5_error_code retval;
+    krad_packet *pkt;
+    uchar id;
+    size_t attrset_len;
+
+    pkt = packet_new();
+    if (pkt == NULL) {
+        if (cb != NULL)
+            (*cb)(data, TRUE);
+        return ENOMEM;
+    }
+
+    /* Generate the ID. */
+    retval = id_generate(ctx, cb, data, &id);
+    if (retval != 0)
+        goto error;
+    pkt_id_set(pkt, id);
+
+    /* Generate the authenticator. */
+    retval = auth_generate_random(ctx, pkt_auth(pkt));
+    if (retval != 0)
+        goto error;
+
+    /* Encode the attributes. */
+    retval = kr_attrset_encode(set, secret, pkt_auth(pkt), pkt_attr(pkt),
+                               &attrset_len);
+    if (retval != 0)
+        goto error;
+
+    /* Set the code, ID and length. */
+    pkt->pkt.length = attrset_len + OFFSET_ATTR;
+    pkt_code_set(pkt, code);
+    pkt_len_set(pkt, pkt->pkt.length);
+
+    /* Copy the attrset for future use. */
+    retval = packet_set_attrset(ctx, secret, pkt);
+    if (retval != 0)
+        goto error;
+
+    *request = pkt;
+    return 0;
+
+error:
+    free(pkt);
+    return retval;
+}
+
+/* Create a new request packet. */
+krb5_error_code
+krad_packet_new_response(krb5_context ctx, const char *secret, krad_code code,
+                         const krad_attrset *set, const krad_packet *request,
+                         krad_packet **response)
+{
+    krb5_error_code retval;
+    krad_packet *pkt;
+    size_t attrset_len;
+
+    pkt = packet_new();
+    if (pkt == NULL)
+        return ENOMEM;
+
+    /* Encode the attributes. */
+    retval = kr_attrset_encode(set, secret, pkt_auth(request), pkt_attr(pkt),
+                               &attrset_len);
+    if (retval != 0)
+        goto error;
+
+    /* Set the code, ID and length. */
+    pkt->pkt.length = attrset_len + OFFSET_ATTR;
+    pkt_code_set(pkt, code);
+    pkt_id_set(pkt, pkt_id_get(request));
+    pkt_len_set(pkt, pkt->pkt.length);
+
+    /* Generate the authenticator. */
+    retval = auth_generate_response(ctx, secret, pkt, pkt_auth(request),
+                                    pkt_auth(pkt));
+    if (retval != 0)
+        goto error;
+
+    /* Copy the attrset for future use. */
+    retval = packet_set_attrset(ctx, secret, pkt);
+    if (retval != 0)
+        goto error;
+
+    *response = pkt;
+    return 0;
+
+error:
+    free(pkt);
+    return retval;
+}
+
+/* Decode a packet. */
+static krb5_error_code
+decode_packet(krb5_context ctx, const char *secret, const krb5_data *buffer,
+              krad_packet **pkt)
+{
+    krb5_error_code retval;
+    krad_packet *tmp;
+    krb5_ui_2 len;
+
+    tmp = packet_new();
+    if (tmp == NULL) {
+        retval = ENOMEM;
+        goto error;
+    }
+
+    /* Ensure a proper message length. */
+    retval = (buffer->length < OFFSET_ATTR) ? EMSGSIZE : 0;
+    if (retval != 0)
+        goto error;
+    len = load_16_be(offset(buffer, OFFSET_LENGTH));
+    retval = (len < OFFSET_ATTR) ? EBADMSG : 0;
+    if (retval != 0)
+        goto error;
+    retval = (len > buffer->length || len > tmp->pkt.length) ? EBADMSG : 0;
+    if (retval != 0)
+        goto error;
+
+    /* Copy over the buffer. */
+    tmp->pkt.length = len;
+    memcpy(tmp->pkt.data, buffer->data, len);
+
+    /* Parse the packet to ensure it is well-formed. */
+    retval = packet_set_attrset(ctx, secret, tmp);
+    if (retval != 0)
+        goto error;
+
+    *pkt = tmp;
+    return 0;
+
+error:
+    krad_packet_free(tmp);
+    return retval;
+}
+
+krb5_error_code
+krad_packet_decode_request(krb5_context ctx, const char *secret,
+                           const krb5_data *buffer, krad_packet_iter_cb cb,
+                           void *data, const krad_packet **duppkt,
+                           krad_packet **reqpkt)
+{
+    const krad_packet *tmp = NULL;
+    krb5_error_code retval;
+
+    retval = decode_packet(ctx, secret, buffer, reqpkt);
+    if (cb != NULL && retval == 0) {
+        for (tmp = (*cb)(data, FALSE); tmp != NULL; tmp = (*cb)(data, FALSE)) {
+            if (pkt_id_get(*reqpkt) == pkt_id_get(tmp))
+                break;
+        }
+    }
+
+    if (cb != NULL && (retval != 0 || tmp != NULL))
+        (*cb)(data, TRUE);
+
+    *duppkt = tmp;
+    return retval;
+}
+
+krb5_error_code
+krad_packet_decode_response(krb5_context ctx, const char *secret,
+                            const krb5_data *buffer, krad_packet_iter_cb cb,
+                            void *data, const krad_packet **reqpkt,
+                            krad_packet **rsppkt)
+{
+    uchar auth[AUTH_FIELD_SIZE];
+    const krad_packet *tmp = NULL;
+    krb5_error_code retval;
+
+    retval = decode_packet(ctx, secret, buffer, rsppkt);
+    if (cb != NULL && retval == 0) {
+        for (tmp = (*cb)(data, FALSE); tmp != NULL; tmp = (*cb)(data, FALSE)) {
+            if (pkt_id_get(*rsppkt) != pkt_id_get(tmp))
+                continue;
+
+            /* Response */
+            retval = auth_generate_response(ctx, secret, *rsppkt,
+                                            pkt_auth(tmp), auth);
+            if (retval != 0) {
+                krad_packet_free(*rsppkt);
+                break;
+            }
+
+            /* If the authenticator matches, then the response is valid. */
+            if (memcmp(pkt_auth(*rsppkt), auth, sizeof(auth)) == 0)
+                break;
+        }
+    }
+
+    if (cb != NULL && (retval != 0 || tmp != NULL))
+        (*cb)(data, TRUE);
+
+    *reqpkt = tmp;
+    return retval;
+}
+
+const krb5_data *
+krad_packet_encode(const krad_packet *pkt)
+{
+    return &pkt->pkt;
+}
+
+krad_code
+krad_packet_get_code(const krad_packet *pkt)
+{
+    if (pkt == NULL)
+        return 0;
+
+    return pkt_code_get(pkt);
+}
+
+const krb5_data *
+krad_packet_get_attr(const krad_packet *pkt, krad_attr type, size_t indx)
+{
+    return krad_attrset_get(pkt->attrset, type, indx);
+}
diff --git a/src/lib/krad/remote.c b/src/lib/krad/remote.c
new file mode 100644
index 0000000..bb7c061
--- /dev/null
+++ b/src/lib/krad/remote.c
@@ -0,0 +1,532 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/remote.c - Protocol code for libkrad */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <k5-int.h>
+#include <k5-queue.h>
+#include "internal.h"
+
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/un.h>
+
+#define FLAGS_NONE  VERTO_EV_FLAG_NONE
+#define FLAGS_READ  VERTO_EV_FLAG_IO_READ
+#define FLAGS_WRITE VERTO_EV_FLAG_IO_WRITE
+#define FLAGS_BASE  VERTO_EV_FLAG_PERSIST | VERTO_EV_FLAG_IO_ERROR
+
+TAILQ_HEAD(request_head, request_st);
+
+typedef struct request_st request;
+struct request_st {
+    TAILQ_ENTRY(request_st) list;
+    krad_remote *rr;
+    krad_packet *request;
+    krad_cb cb;
+    void *data;
+    verto_ev *timer;
+    int timeout;
+    size_t retries;
+    size_t sent;
+};
+
+struct krad_remote_st {
+    krb5_context kctx;
+    verto_ctx *vctx;
+    int fd;
+    verto_ev *io;
+    char *secret;
+    struct addrinfo *info;
+    struct request_head list;
+    char buffer_[KRAD_PACKET_SIZE_MAX];
+    krb5_data buffer;
+};
+
+static void
+on_io(verto_ctx *ctx, verto_ev *ev);
+
+static void
+on_timeout(verto_ctx *ctx, verto_ev *ev);
+
+/* Iterate over the set of outstanding packets. */
+static const krad_packet *
+iterator(request **out)
+{
+    request *tmp = *out;
+
+    if (tmp == NULL)
+        return NULL;
+
+    *out = TAILQ_NEXT(tmp, list);
+    return tmp->request;
+}
+
+/* Create a new request. */
+static krb5_error_code
+request_new(krad_remote *rr, krad_packet *rqst, int timeout, size_t retries,
+            krad_cb cb, void *data, request **out)
+{
+    request *tmp;
+
+    tmp = calloc(1, sizeof(request));
+    if (tmp == NULL)
+        return ENOMEM;
+
+    tmp->rr = rr;
+    tmp->request = rqst;
+    tmp->cb = cb;
+    tmp->data = data;
+    tmp->timeout = timeout;
+    tmp->retries = retries;
+
+    *out = tmp;
+    return 0;
+}
+
+/* Finish a request, calling the callback and freeing it. */
+static inline void
+request_finish(request *req, krb5_error_code retval,
+               const krad_packet *response)
+{
+    if (retval != ETIMEDOUT)
+        TAILQ_REMOVE(&req->rr->list, req, list);
+
+    req->cb(retval, req->request, response, req->data);
+
+    if (retval != ETIMEDOUT) {
+        krad_packet_free(req->request);
+        verto_del(req->timer);
+        free(req);
+    }
+}
+
+/* Start the timeout timer for the request. */
+static krb5_error_code
+request_start_timer(request *r, verto_ctx *vctx)
+{
+    verto_del(r->timer);
+
+    r->timer = verto_add_timeout(vctx, VERTO_EV_FLAG_NONE, on_timeout,
+                                 r->timeout);
+    if (r->timer != NULL)
+        verto_set_private(r->timer, r, NULL);
+
+    return (r->timer == NULL) ? ENOMEM : 0;
+}
+
+/* Disconnect from the remote host. */
+static void
+remote_disconnect(krad_remote *rr)
+{
+    close(rr->fd);
+    verto_del(rr->io);
+    rr->fd = -1;
+    rr->io = NULL;
+}
+
+/* Add the specified flags to the remote. This automatically manages the
+ * lifecyle of the underlying event. Also connects if disconnected. */
+static krb5_error_code
+remote_add_flags(krad_remote *remote, verto_ev_flag flags)
+{
+    verto_ev_flag curflags = VERTO_EV_FLAG_NONE;
+    int i;
+
+    flags &= (FLAGS_READ | FLAGS_WRITE);
+    if (remote == NULL || flags == FLAGS_NONE)
+        return EINVAL;
+
+    /* If there is no connection, connect. */
+    if (remote->fd < 0) {
+        verto_del(remote->io);
+        remote->io = NULL;
+
+        remote->fd = socket(remote->info->ai_family, remote->info->ai_socktype,
+                            remote->info->ai_protocol);
+        if (remote->fd < 0)
+            return errno;
+
+        i = connect(remote->fd, remote->info->ai_addr,
+                    remote->info->ai_addrlen);
+        if (i < 0) {
+            i = errno;
+            remote_disconnect(remote);
+            return i;
+        }
+    }
+
+    if (remote->io == NULL) {
+        remote->io = verto_add_io(remote->vctx, FLAGS_BASE | flags,
+                                  on_io, remote->fd);
+        if (remote->io == NULL)
+            return ENOMEM;
+        verto_set_private(remote->io, remote, NULL);
+    }
+
+    curflags = verto_get_flags(remote->io);
+    if ((curflags & flags) != flags)
+        verto_set_flags(remote->io, FLAGS_BASE | curflags | flags);
+
+    return 0;
+}
+
+/* Remove the specified flags to the remote. This automatically manages the
+ * lifecyle of the underlying event. */
+static void
+remote_del_flags(krad_remote *remote, verto_ev_flag flags)
+{
+    if (remote == NULL || remote->io == NULL)
+        return;
+
+    flags = verto_get_flags(remote->io) & (FLAGS_READ | FLAGS_WRITE) & ~flags;
+    if (flags == FLAGS_NONE) {
+        verto_del(remote->io);
+        remote->io = NULL;
+        return;
+    }
+
+    verto_set_flags(remote->io, FLAGS_BASE | flags);
+}
+
+/* Close the connection and start the timers of all outstanding requests. */
+static void
+remote_shutdown(krad_remote *rr)
+{
+    krb5_error_code retval;
+    request *r;
+
+    remote_disconnect(rr);
+
+    /* Start timers for all unsent packets. */
+    TAILQ_FOREACH(r, &rr->list, list) {
+        if (r->timer == NULL) {
+            retval = request_start_timer(r, rr->vctx);
+            if (retval != 0)
+                request_finish(r, retval, NULL);
+        }
+    }
+}
+
+/* Handle when packets receive no response within their alloted time. */
+static void
+on_timeout(verto_ctx *ctx, verto_ev *ev)
+{
+    request *req = verto_get_private(ev);
+    krb5_error_code retval = ETIMEDOUT;
+
+    req->timer = NULL;          /* Void the timer event. */
+
+    /* If we have more retries to perform, resend the packet. */
+    if (req->retries-- > 1) {
+        req->sent = 0;
+        retval = remote_add_flags(req->rr, FLAGS_WRITE);
+        if (retval == 0)
+            return;
+    }
+
+    request_finish(req, retval, NULL);
+}
+
+/* Write data to the socket. */
+static void
+on_io_write(krad_remote *rr)
+{
+    const krb5_data *tmp;
+    ssize_t written;
+    request *r;
+
+    TAILQ_FOREACH(r, &rr->list, list) {
+        tmp = krad_packet_encode(r->request);
+
+        /* If the packet has already been sent, do nothing. */
+        if (r->sent == tmp->length)
+            continue;
+
+        /* Send the packet. */
+        written = sendto(verto_get_fd(rr->io), tmp->data + r->sent,
+                         tmp->length - r->sent, 0, NULL, 0);
+        if (written < 0) {
+            /* Should we try again? */
+            if (errno == EWOULDBLOCK || errno == EAGAIN || errno == ENOBUFS ||
+                errno == EINTR)
+                return;
+
+            /* This error can't be worked around. */
+            remote_shutdown(rr);
+            return;
+        }
+
+        /* If the packet was completely sent, set a timeout. */
+        r->sent += written;
+        if (r->sent == tmp->length) {
+            if (request_start_timer(r, rr->vctx) != 0) {
+                request_finish(r, ENOMEM, NULL);
+                return;
+            }
+
+            if (remote_add_flags(rr, FLAGS_READ) != 0) {
+                remote_shutdown(rr);
+                return;
+            }
+        }
+
+        return;
+    }
+
+    remote_del_flags(rr, FLAGS_WRITE);
+    return;
+}
+
+/* Read data from the socket. */
+static void
+on_io_read(krad_remote *rr)
+{
+    const krad_packet *req = NULL;
+    krad_packet *rsp = NULL;
+    krb5_error_code retval;
+    ssize_t pktlen;
+    request *tmp, *r;
+    int i;
+
+    pktlen = sizeof(rr->buffer_);
+    if (rr->info->ai_socktype == SOCK_STREAM) {
+        pktlen = krad_packet_bytes_needed(&rr->buffer);
+        if (pktlen < 0) {
+            /* If we received a malformed packet on a stream socket,
+             * assume the socket to be unrecoverable. */
+            remote_shutdown(rr);
+            return;
+        }
+    }
+
+    /* Read the packet. */
+    i = recv(verto_get_fd(rr->io), rr->buffer.data + rr->buffer.length,
+             pktlen - rr->buffer.length, 0);
+    if (i < 0) {
+        /* Should we try again? */
+        if (errno == EWOULDBLOCK || errno == EAGAIN || errno == EINTR)
+            return;
+
+        /* The socket is unrecoverable. */
+        remote_shutdown(rr);
+        return;
+    } else if (i == 0) {
+        remote_del_flags(rr, FLAGS_READ);
+    }
+
+    /* If we have a partial read or just the header, try again. */
+    rr->buffer.length += i;
+    pktlen = krad_packet_bytes_needed(&rr->buffer);
+    if (rr->info->ai_socktype == SOCK_STREAM && pktlen > 0)
+        return;
+
+    /* Decode the packet. */
+    tmp = TAILQ_FIRST(&rr->list);
+    retval = krad_packet_decode_response(rr->kctx, rr->secret, &rr->buffer,
+                                         (krad_packet_iter_cb)iterator, &tmp,
+                                         &req, &rsp);
+    rr->buffer.length = 0;
+    if (retval != 0)
+        return;
+
+    /* Match the response with an outstanding request. */
+    if (req != NULL) {
+        TAILQ_FOREACH(r, &rr->list, list) {
+            if (r->request == req &&
+                r->sent == krad_packet_encode(req)->length) {
+                request_finish(r, 0, rsp);
+                break;
+            }
+        }
+    }
+
+    krad_packet_free(rsp);
+}
+
+/* Handle when IO is ready on the socket. */
+static void
+on_io(verto_ctx *ctx, verto_ev *ev)
+{
+    krad_remote *rr;
+
+    rr = verto_get_private(ev);
+
+    if (verto_get_fd_state(ev) & VERTO_EV_FLAG_IO_WRITE)
+        on_io_write(rr);
+    else
+        on_io_read(rr);
+}
+
+krb5_error_code
+kr_remote_new(krb5_context kctx, verto_ctx *vctx, const struct addrinfo *info,
+              const char *secret, krad_remote **rr)
+{
+    krb5_error_code retval = ENOMEM;
+    krad_remote *tmp = NULL;
+
+    tmp = calloc(1, sizeof(krad_remote));
+    if (tmp == NULL)
+        goto error;
+    tmp->kctx = kctx;
+    tmp->vctx = vctx;
+    tmp->buffer = make_data(tmp->buffer_, 0);
+    TAILQ_INIT(&tmp->list);
+    tmp->fd = -1;
+
+    tmp->secret = strdup(secret);
+    if (tmp->secret == NULL)
+        goto error;
+
+    tmp->info = k5memdup(info, sizeof(*info), &retval);
+    if (tmp->info == NULL)
+        goto error;
+
+    tmp->info->ai_addr = k5memdup(info->ai_addr, info->ai_addrlen, &retval);
+    if (tmp->info == NULL)
+        goto error;
+    tmp->info->ai_next = NULL;
+    tmp->info->ai_canonname = NULL;
+
+    *rr = tmp;
+    return 0;
+
+error:
+    kr_remote_free(tmp);
+    return retval;
+}
+
+void
+kr_remote_free(krad_remote *rr)
+{
+    if (rr == NULL)
+        return;
+
+    while (!TAILQ_EMPTY(&rr->list))
+        request_finish(TAILQ_FIRST(&rr->list), ECANCELED, NULL);
+
+    free(rr->secret);
+    if (rr->info != NULL)
+        free(rr->info->ai_addr);
+    free(rr->info);
+    remote_disconnect(rr);
+    free(rr);
+}
+
+krb5_error_code
+kr_remote_send(krad_remote *rr, krad_code code, krad_attrset *attrs,
+               krad_cb cb, void *data, int timeout, size_t retries,
+               const krad_packet **pkt)
+{
+    krad_packet *tmp = NULL;
+    krb5_error_code retval;
+    request *r;
+
+    r = TAILQ_FIRST(&rr->list);
+    retval = krad_packet_new_request(rr->kctx, rr->secret, code, attrs,
+                                     (krad_packet_iter_cb)iterator, &r, &tmp);
+    if (retval != 0)
+        goto error;
+
+    TAILQ_FOREACH(r, &rr->list, list) {
+        if (r->request == tmp) {
+            retval = EALREADY;
+            goto error;
+        }
+    }
+
+    timeout = timeout / (retries + 1);
+    retval = request_new(rr, tmp, timeout, retries, cb, data, &r);
+    if (retval != 0)
+        goto error;
+
+    retval = remote_add_flags(rr, FLAGS_WRITE);
+    if (retval != 0)
+        goto error;
+
+    TAILQ_INSERT_TAIL(&rr->list, r, list);
+    if (pkt != NULL)
+        *pkt = tmp;
+    return 0;
+
+error:
+    krad_packet_free(tmp);
+    return retval;
+}
+
+void
+kr_remote_cancel(krad_remote *rr, const krad_packet *pkt)
+{
+    request *r;
+
+    TAILQ_FOREACH(r, &rr->list, list) {
+        if (r->request == pkt) {
+            request_finish(r, ECANCELED, NULL);
+            return;
+        }
+    }
+}
+
+krb5_boolean
+kr_remote_equals(const krad_remote *rr, const struct addrinfo *info,
+                 const char *secret)
+{
+    struct sockaddr_un *a, *b;
+
+    if (strcmp(rr->secret, secret) != 0)
+        return FALSE;
+
+    if (info->ai_addrlen != rr->info->ai_addrlen)
+        return FALSE;
+
+    if (info->ai_family != rr->info->ai_family)
+        return FALSE;
+
+    if (info->ai_socktype != rr->info->ai_socktype)
+        return FALSE;
+
+    if (info->ai_protocol != rr->info->ai_protocol)
+        return FALSE;
+
+    if (info->ai_flags != rr->info->ai_flags)
+        return FALSE;
+
+    if (memcmp(rr->info->ai_addr, info->ai_addr, info->ai_addrlen) != 0) {
+        /* AF_UNIX fails the memcmp() test due to uninitialized bytes after the
+         * socket name. */
+        if (info->ai_family != AF_UNIX)
+            return FALSE;
+
+        a = (struct sockaddr_un *)info->ai_addr;
+        b = (struct sockaddr_un *)rr->info->ai_addr;
+        if (strncmp(a->sun_path, b->sun_path, sizeof(a->sun_path)) != 0)
+            return FALSE;
+    }
+
+    return TRUE;
+}
diff --git a/src/lib/krad/t_attr.c b/src/lib/krad/t_attr.c
new file mode 100644
index 0000000..e80d77b
--- /dev/null
+++ b/src/lib/krad/t_attr.c
@@ -0,0 +1,89 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/t_attr.c - Attribute test program */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "t_test.h"
+
+const static char encoded[] = {
+    0xba, 0xfc, 0xed, 0x50, 0xe1, 0xeb, 0xa6, 0xc3,
+    0xc1, 0x75, 0x20, 0xe9, 0x10, 0xce, 0xc2, 0xcb
+};
+
+const static unsigned char auth[] = {
+    0xac, 0x9d, 0xc1, 0x62, 0x08, 0xc4, 0xc7, 0x8b,
+    0xa1, 0x2f, 0x25, 0x0a, 0xc4, 0x1d, 0x36, 0x41
+};
+
+int
+main()
+{
+    unsigned char outbuf[MAX_ATTRSETSIZE];
+    const char *decoded = "accept";
+    const char *secret = "foo";
+    krb5_error_code retval;
+    krb5_context ctx;
+    const char *tmp;
+    krb5_data in;
+    size_t len;
+
+    noerror(krb5_init_context(&ctx));
+
+    /* Make sure User-Name is 1. */
+    insist(krad_attr_name2num("User-Name") == 1);
+
+    /* Make sure 2 is User-Password. */
+    tmp = krad_attr_num2name(2);
+    insist(tmp != NULL);
+    insist(strcmp(tmp, "User-Password") == 0);
+
+    /* Test decoding. */
+    in = make_data((void *)encoded, sizeof(encoded));
+    noerror(kr_attr_decode(ctx, secret, auth,
+                           krad_attr_name2num("User-Password"),
+                           &in, outbuf, &len));
+    insist(len == strlen(decoded));
+    insist(memcmp(outbuf, decoded, len) == 0);
+
+    /* Test encoding. */
+    in = string2data((char *)decoded);
+    retval = kr_attr_encode(ctx, secret, auth,
+                            krad_attr_name2num("User-Password"),
+                            &in, outbuf, &len);
+    insist(retval == 0);
+    insist(len == sizeof(encoded));
+    insist(memcmp(outbuf, encoded, len) == 0);
+
+    /* Test constraint. */
+    in.length = 100;
+    insist(kr_attr_valid(krad_attr_name2num("User-Password"), &in) == 0);
+    in.length = 200;
+    insist(kr_attr_valid(krad_attr_name2num("User-Password"), &in) != 0);
+
+    krb5_free_context(ctx);
+    return 0;
+}
diff --git a/src/lib/krad/t_attrset.c b/src/lib/krad/t_attrset.c
new file mode 100644
index 0000000..afae5e4
--- /dev/null
+++ b/src/lib/krad/t_attrset.c
@@ -0,0 +1,98 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/t_attrset.c - Attribute set test program */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "t_test.h"
+
+const static unsigned char auth[] = {
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+const static char encpass[] = {
+    0x58, 0x8d, 0xff, 0xda, 0x37, 0xf9, 0xe4, 0xca,
+    0x19, 0xae, 0x49, 0xb7, 0x16, 0x6d, 0x58, 0x27
+};
+
+int
+main()
+{
+    unsigned char buffer[KRAD_PACKET_SIZE_MAX], encoded[MAX_ATTRSETSIZE];
+    const char *username = "testUser", *password = "accept";
+    const krb5_data *tmpp;
+    krad_attrset *set;
+    krb5_context ctx;
+    size_t len = 0, encode_len;
+    krb5_data tmp;
+
+    noerror(krb5_init_context(&ctx));
+    noerror(krad_attrset_new(ctx, &set));
+
+    /* Add username. */
+    tmp = string2data((char *)username);
+    noerror(krad_attrset_add(set, krad_attr_name2num("User-Name"), &tmp));
+
+    /* Add password. */
+    tmp = string2data((char *)password);
+    noerror(krad_attrset_add(set, krad_attr_name2num("User-Password"), &tmp));
+
+    /* Encode attrset. */
+    noerror(kr_attrset_encode(set, "foo", auth, buffer, &encode_len));
+    krad_attrset_free(set);
+
+    /* Manually encode User-Name. */
+    encoded[len + 0] = krad_attr_name2num("User-Name");
+    encoded[len + 1] = strlen(username) + 2;
+    memcpy(encoded + len + 2, username, strlen(username));
+    len += encoded[len + 1];
+
+    /* Manually encode User-Password. */
+    encoded[len + 0] = krad_attr_name2num("User-Password");
+    encoded[len + 1] = sizeof(encpass) + 2;
+    memcpy(encoded + len + 2, encpass, sizeof(encpass));
+    len += encoded[len + 1];
+
+    /* Compare output. */
+    insist(len == encode_len);
+    insist(memcmp(encoded, buffer, len) == 0);
+
+    /* Decode output. */
+    tmp = make_data(buffer, len);
+    noerror(kr_attrset_decode(ctx, &tmp, "foo", auth, &set));
+
+    /* Test getting an attribute. */
+    tmp = string2data((char *)username);
+    tmpp = krad_attrset_get(set, krad_attr_name2num("User-Name"), 0);
+    insist(tmpp != NULL);
+    insist(tmpp->length == tmp.length);
+    insist(strncmp(tmpp->data, tmp.data, tmp.length) == 0);
+
+    krad_attrset_free(set);
+    krb5_free_context(ctx);
+    return 0;
+}
diff --git a/src/lib/krad/t_client.c b/src/lib/krad/t_client.c
new file mode 100644
index 0000000..3d0fda9
--- /dev/null
+++ b/src/lib/krad/t_client.c
@@ -0,0 +1,126 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/t_client.c - Client request test program */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "t_daemon.h"
+
+#define EVENT_COUNT 4
+
+static struct
+{
+    int count;
+    struct event events[EVENT_COUNT];
+} record;
+
+static verto_ctx *vctx;
+
+static void
+callback(krb5_error_code retval, const krad_packet *request,
+         const krad_packet *response, void *data)
+{
+    struct event *evt;
+
+    evt = &record.events[record.count++];
+    evt->error = retval != 0;
+    if (evt->error)
+        evt->result.retval = retval;
+    else
+        evt->result.code = krad_packet_get_code(response);
+    verto_break(vctx);
+}
+
+int
+main(int argc, const char **argv)
+{
+    krad_attrset *attrs;
+    krad_client *rc;
+    krb5_context kctx;
+    krb5_data tmp;
+
+    if (!daemon_start(argc, argv)) {
+        fprintf(stderr, "Unable to start pyrad daemon, skipping test...\n");
+        return 0;
+    }
+
+    noerror(krb5_init_context(&kctx));
+    vctx = verto_new(NULL, VERTO_EV_TYPE_IO | VERTO_EV_TYPE_TIMEOUT);
+    insist(vctx != NULL);
+    noerror(krad_client_new(kctx, vctx, &rc));
+
+    tmp = string2data("testUser");
+    noerror(krad_attrset_new(kctx, &attrs));
+    noerror(krad_attrset_add(attrs, krad_attr_name2num("User-Name"), &tmp));
+
+    /* Test accept. */
+    tmp = string2data("accept");
+    noerror(krad_attrset_add(attrs, krad_attr_name2num("User-Password"),
+                             &tmp));
+    noerror(krad_client_send(rc, krad_code_name2num("Access-Request"), attrs,
+                             "localhost", "foo", 1000, 3, callback, NULL));
+    verto_run(vctx);
+
+    /* Test reject. */
+    tmp = string2data("reject");
+    krad_attrset_del(attrs, krad_attr_name2num("User-Password"), 0);
+    noerror(krad_attrset_add(attrs, krad_attr_name2num("User-Password"),
+                             &tmp));
+    noerror(krad_client_send(rc, krad_code_name2num("Access-Request"), attrs,
+                             "localhost", "foo", 1000, 3, callback, NULL));
+    verto_run(vctx);
+
+    /* Test timeout. */
+    daemon_stop();
+    noerror(krad_client_send(rc, krad_code_name2num("Access-Request"), attrs,
+                             "localhost", "foo", 1000, 3, callback, NULL));
+    verto_run(vctx);
+
+    /* Test outstanding packet freeing. */
+    noerror(krad_client_send(rc, krad_code_name2num("Access-Request"), attrs,
+                             "localhost", "foo", 1000, 3, callback, NULL));
+    krad_client_free(rc);
+    rc = NULL;
+
+    /* Verify the results. */
+    insist(record.count == EVENT_COUNT);
+    insist(record.events[0].error == FALSE);
+    insist(record.events[0].result.code ==
+           krad_code_name2num("Access-Accept"));
+    insist(record.events[1].error == FALSE);
+    insist(record.events[1].result.code ==
+           krad_code_name2num("Access-Reject"));
+    insist(record.events[2].error == TRUE);
+    insist(record.events[2].result.retval == ETIMEDOUT);
+    insist(record.events[3].error == TRUE);
+    insist(record.events[3].result.retval == ECANCELED);
+
+    krad_attrset_free(attrs);
+    krad_client_free(rc);
+    verto_free(vctx);
+    krb5_free_context(kctx);
+    return 0;
+}
diff --git a/src/lib/krad/t_code.c b/src/lib/krad/t_code.c
new file mode 100644
index 0000000..b245a7e
--- /dev/null
+++ b/src/lib/krad/t_code.c
@@ -0,0 +1,54 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/t_code.c - RADIUS code table test program */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "t_test.h"
+
+int
+main()
+{
+    const char *tmp;
+
+    insist(krad_code_name2num("Access-Request") == 1);
+    insist(krad_code_name2num("Access-Accept") == 2);
+    insist(krad_code_name2num("Access-Reject") == 3);
+
+    tmp = krad_code_num2name(1);
+    insist(tmp != NULL);
+    insist(strcmp(tmp, "Access-Request") == 0);
+
+    tmp = krad_code_num2name(2);
+    insist(tmp != NULL);
+    insist(strcmp(tmp, "Access-Accept") == 0);
+
+    tmp = krad_code_num2name(3);
+    insist(tmp != NULL);
+    insist(strcmp(tmp, "Access-Reject") == 0);
+
+    return 0;
+}
diff --git a/src/lib/krad/t_daemon.h b/src/lib/krad/t_daemon.h
new file mode 100644
index 0000000..7c345a6
--- /dev/null
+++ b/src/lib/krad/t_daemon.h
@@ -0,0 +1,92 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/t_daemon.h - Daemonization helper for RADIUS test programs */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef T_DAEMON_H_
+#define T_DAEMON_H_
+
+#include "t_test.h"
+#include <libgen.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+static pid_t daemon_pid;
+
+static void
+daemon_stop(void)
+{
+    if (daemon_pid == 0)
+        return;
+    kill(daemon_pid, SIGTERM);
+    waitpid(daemon_pid, NULL, 0);
+    daemon_pid = 0;
+}
+
+static krb5_boolean
+daemon_start(int argc, const char **argv)
+{
+    sigset_t set;
+    int sig;
+
+    if (argc != 3 || argv == NULL)
+        return FALSE;
+
+    if (daemon_pid != 0)
+        return TRUE;
+
+    if (sigemptyset(&set) != 0)
+        return FALSE;
+
+    if (sigaddset(&set, SIGUSR1) != 0)
+        return FALSE;
+
+    if (sigaddset(&set, SIGCHLD) != 0)
+        return FALSE;
+
+    if (sigprocmask(SIG_BLOCK, &set, NULL) != 0)
+        return FALSE;
+
+    daemon_pid = fork();
+    if (daemon_pid == 0) {
+        close(STDOUT_FILENO);
+        open("/dev/null", O_WRONLY);
+        exit(execlp(argv[1], argv[1], argv[2], NULL));
+    }
+
+    if (sigwait(&set, &sig) != 0 || sig == SIGCHLD) {
+        daemon_stop();
+        daemon_pid = 0;
+        return FALSE;
+    }
+
+    atexit(daemon_stop);
+    return TRUE;
+}
+
+#endif /* T_DAEMON_H_ */
diff --git a/src/lib/krad/t_daemon.py b/src/lib/krad/t_daemon.py
new file mode 100644
index 0000000..71e70dd
--- /dev/null
+++ b/src/lib/krad/t_daemon.py
@@ -0,0 +1,76 @@
+#!/usr/bin/python
+#
+# Copyright 2013 Red Hat, Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#
+#    2. Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in
+#       the documentation and/or other materials provided with the
+#       distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import StringIO
+import os
+import sys
+import signal
+
+try:
+    from pyrad import dictionary, packet, server
+except ImportError:
+    sys.stdout.write("pyrad not found!\n")
+    sys.exit(0)
+
+# We could use a dictionary file, but since we need
+# such few attributes, we'll just include them here
+DICTIONARY = """
+ATTRIBUTE\tUser-Name\t1\tstring
+ATTRIBUTE\tUser-Password\t2\toctets
+ATTRIBUTE\tNAS-Identifier\t32\tstring
+"""
+
+class TestServer(server.Server):
+    def _HandleAuthPacket(self, pkt):
+        server.Server._HandleAuthPacket(self, pkt)
+
+        passwd = []
+
+        print "Request: "
+        for key in pkt.keys():
+            if key == "User-Password":
+                passwd = map(pkt.PwDecrypt, pkt[key])
+                print "\t%s\t%s" % (key, passwd)
+            else:
+                print "\t%s\t%s" % (key, pkt[key])
+
+        reply = self.CreateReplyPacket(pkt)
+        if passwd == ['accept']:
+            reply.code = packet.AccessAccept
+            print "Response: %s" % "Access-Accept"
+        else:
+            reply.code = packet.AccessReject
+            print "Response: %s" % "Access-Reject"
+        print
+        self.SendReplyPacket(pkt.fd, reply)
+
+srv = TestServer(addresses=["localhost"],
+                 hosts={"127.0.0.1":
+                        server.RemoteHost("127.0.0.1", "foo", "localhost")},
+                 dict=dictionary.Dictionary(StringIO.StringIO(DICTIONARY)))
+os.kill(os.getppid(), signal.SIGUSR1)
+srv.Run()
diff --git a/src/lib/krad/t_packet.c b/src/lib/krad/t_packet.c
new file mode 100644
index 0000000..0a92e9c
--- /dev/null
+++ b/src/lib/krad/t_packet.c
@@ -0,0 +1,194 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/t_packet.c - RADIUS packet test program */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "t_daemon.h"
+
+#define ACCEPT_PACKET 0
+#define REJECT_PACKET 1
+
+static krad_packet *packets[3];
+
+static const krad_packet *
+iterator(void *data, krb5_boolean cancel)
+{
+    krad_packet *tmp;
+    int *i = data;
+
+    if (cancel || packets[*i] == NULL)
+        return NULL;
+
+    tmp = packets[*i];
+    *i += 1;
+    return tmp;
+}
+
+static krb5_error_code
+make_packet(krb5_context ctx, const krb5_data *username,
+            const krb5_data *password, krad_packet **pkt)
+{
+    krad_attrset *set = NULL;
+    krad_packet *tmp = NULL;
+    krb5_error_code retval;
+    const krb5_data *data;
+    int i = 0;
+
+    retval = krad_attrset_new(ctx, &set);
+    if (retval != 0)
+        goto out;
+
+    retval = krad_attrset_add(set, krad_attr_name2num("User-Name"), username);
+    if (retval != 0)
+        goto out;
+
+    retval = krad_attrset_add(set, krad_attr_name2num("User-Password"),
+                              password);
+    if (retval != 0)
+        goto out;
+
+    retval = krad_packet_new_request(ctx, "foo",
+                                     krad_code_name2num("Access-Request"),
+                                     set, iterator, &i, &tmp);
+    if (retval != 0)
+        goto out;
+
+    data = krad_packet_get_attr(tmp, krad_attr_name2num("User-Name"), 0);
+    if (data == NULL) {
+        retval = ENOENT;
+        goto out;
+    }
+
+    if (data->length != username->length ||
+        memcmp(data->data, username->data, data->length) != 0) {
+        retval = EINVAL;
+        goto out;
+    }
+
+    *pkt = tmp;
+    tmp = NULL;
+
+out:
+    krad_attrset_free(set);
+    krad_packet_free(tmp);
+    return retval;
+}
+
+static krb5_error_code
+do_auth(krb5_context ctx, struct addrinfo *ai, const char *secret,
+        const krad_packet *rqst, krb5_boolean *auth)
+{
+    const krad_packet *req = NULL;
+    char tmp[KRAD_PACKET_SIZE_MAX];
+    const krb5_data *request;
+    krad_packet *rsp = NULL;
+    krb5_error_code retval;
+    krb5_data response;
+    int sock = -1, i;
+
+    response = make_data(tmp, sizeof(tmp));
+
+    sock = socket(ai->ai_family, ai->ai_socktype, 0);
+    if (sock < 0) {
+        retval = errno;
+        goto out;
+    }
+
+    request = krad_packet_encode(rqst);
+    if (sendto(sock, request->data, request->length, 0, ai->ai_addr,
+               ai->ai_addrlen) < 0) {
+        retval = errno;
+        goto out;
+    }
+
+    i = recv(sock, response.data, sizeof(tmp), 0);
+    if (i < 0) {
+        retval = errno;
+        goto out;
+    }
+    response.length = i;
+
+    i = 0;
+    retval = krad_packet_decode_response(ctx, secret, &response, iterator, &i,
+                                         &req, &rsp);
+    if (retval != 0)
+        goto out;
+
+    if (req != rqst) {
+        retval = EBADMSG;
+        goto out;
+    }
+
+    *auth = krad_packet_get_code(rsp) == krad_code_name2num("Access-Accept");
+
+out:
+    krad_packet_free(rsp);
+    if (sock >= 0)
+        close(sock);
+    return retval;
+}
+
+int
+main(int argc, const char **argv)
+{
+    struct addrinfo *ai = NULL, hints;
+    krb5_data username, password;
+    krb5_boolean auth = FALSE;
+    krb5_context ctx;
+
+    username = string2data("testUser");
+
+    if (!daemon_start(argc, argv)) {
+        fprintf(stderr, "Unable to start pyrad daemon, skipping test...\n");
+        return 0;
+    }
+
+    noerror(krb5_init_context(&ctx));
+
+    password = string2data("accept");
+    noerror(make_packet(ctx, &username, &password, &packets[ACCEPT_PACKET]));
+
+    password = string2data("reject");
+    noerror(make_packet(ctx, &username, &password, &packets[REJECT_PACKET]));
+
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_family = AF_INET;
+    hints.ai_socktype = SOCK_DGRAM;
+    noerror(gai_error_code(getaddrinfo("127.0.0.1", "radius", &hints, &ai)));
+
+    noerror(do_auth(ctx, ai, "foo", packets[ACCEPT_PACKET], &auth));
+    insist(auth == TRUE);
+
+    noerror(do_auth(ctx, ai, "foo", packets[REJECT_PACKET], &auth));
+    insist(auth == FALSE);
+
+    krad_packet_free(packets[ACCEPT_PACKET]);
+    krad_packet_free(packets[REJECT_PACKET]);
+    krb5_free_context(ctx);
+    freeaddrinfo(ai);
+    return 0;
+}
diff --git a/src/lib/krad/t_remote.c b/src/lib/krad/t_remote.c
new file mode 100644
index 0000000..a521ecb
--- /dev/null
+++ b/src/lib/krad/t_remote.c
@@ -0,0 +1,170 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/t_remote.c - Protocol test program */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "t_daemon.h"
+
+#define EVENT_COUNT 6
+
+static struct
+{
+    int count;
+    struct event events[EVENT_COUNT];
+} record;
+
+static krad_attrset *set;
+static krad_remote *rr;
+static verto_ctx *vctx;
+
+static void
+callback(krb5_error_code retval, const krad_packet *request,
+         const krad_packet *response, void *data)
+{
+    struct event *evt;
+
+    evt = &record.events[record.count++];
+    evt->error = retval != 0;
+    if (evt->error)
+        evt->result.retval = retval;
+    else
+        evt->result.code = krad_packet_get_code(response);
+    verto_break(vctx);
+}
+
+static void
+remote_new(krb5_context kctx, krad_remote **remote)
+{
+    struct addrinfo *ai = NULL, hints;
+
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_family = AF_INET;
+    hints.ai_socktype = SOCK_DGRAM;
+    noerror(gai_error_code(getaddrinfo("127.0.0.1", "radius", &hints, &ai)));
+
+    noerror(kr_remote_new(kctx, vctx, ai, "foo", remote));
+    insist(kr_remote_equals(*remote, ai, "foo"));
+    freeaddrinfo(ai);
+}
+
+static krb5_error_code
+do_auth(const char *password, const krad_packet **pkt)
+{
+    const krad_packet *tmppkt;
+    krb5_error_code retval;
+    krb5_data tmp = string2data((char *)password);
+
+    retval = krad_attrset_add(set, krad_attr_name2num("User-Password"), &tmp);
+    if (retval != 0)
+        return retval;
+
+    retval = kr_remote_send(rr, krad_code_name2num("Access-Request"), set,
+                            callback, NULL, 1000, 3, &tmppkt);
+    krad_attrset_del(set, krad_attr_name2num("User-Password"), 0);
+    if (retval != 0)
+        return retval;
+
+    if (pkt != NULL)
+        *pkt = tmppkt;
+    return 0;
+}
+
+static void
+test_timeout(verto_ctx *ctx, verto_ev *ev)
+{
+    static const krad_packet *pkt;
+
+    noerror(do_auth("accept", &pkt));
+    kr_remote_cancel(rr, pkt);
+}
+
+int
+main(int argc, const char **argv)
+{
+    krb5_context kctx = NULL;
+    krb5_data tmp;
+
+    if (!daemon_start(argc, argv)) {
+        fprintf(stderr, "Unable to start pyrad daemon, skipping test...\n");
+        return 0;
+    }
+
+    /* Initialize. */
+    noerror(krb5_init_context(&kctx));
+    vctx = verto_new(NULL, VERTO_EV_TYPE_IO | VERTO_EV_TYPE_TIMEOUT);
+    insist(vctx != NULL);
+    remote_new(kctx, &rr);
+
+    /* Create attribute set. */
+    noerror(krad_attrset_new(kctx, &set));
+    tmp = string2data("testUser");
+    noerror(krad_attrset_add(set, krad_attr_name2num("User-Name"), &tmp));
+
+    /* Send accept packet. */
+    noerror(do_auth("accept", NULL));
+    verto_run(vctx);
+
+    /* Send reject packet. */
+    noerror(do_auth("reject", NULL));
+    verto_run(vctx);
+
+    /* Send canceled packet. */
+    insist(verto_add_timeout(vctx, VERTO_EV_FLAG_NONE, test_timeout, 0) !=
+           NULL);
+    verto_run(vctx);
+
+    /* Test timeout. */
+    daemon_stop();
+    noerror(do_auth("accept", NULL));
+    verto_run(vctx);
+
+    /* Test outstanding packet freeing. */
+    noerror(do_auth("accept", NULL));
+    kr_remote_free(rr);
+    krad_attrset_free(set);
+
+    /* Verify the results. */
+    insist(record.count == EVENT_COUNT);
+    insist(record.events[0].error == FALSE);
+    insist(record.events[0].result.code ==
+           krad_code_name2num("Access-Accept"));
+    insist(record.events[1].error == FALSE);
+    insist(record.events[1].result.code ==
+           krad_code_name2num("Access-Reject"));
+    insist(record.events[2].error == TRUE);
+    insist(record.events[2].result.retval == ECANCELED);
+    insist(record.events[3].error == TRUE);
+    insist(record.events[3].result.retval == ETIMEDOUT);
+    insist(record.events[4].error == TRUE);
+    insist(record.events[4].result.retval == ECANCELED);
+    insist(record.events[5].error == TRUE);
+    insist(record.events[5].result.retval == ECANCELED);
+
+    verto_free(vctx);
+    krb5_free_context(kctx);
+    return 0;
+}
diff --git a/src/lib/krad/t_test.c b/src/lib/krad/t_test.c
new file mode 100644
index 0000000..152bc77
--- /dev/null
+++ b/src/lib/krad/t_test.c
@@ -0,0 +1,50 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/t_test.c - Utility functions for libkrad tests */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "t_test.h"
+
+void
+noerror_impl(const char *file, int line, const char *cmd, int retval)
+{
+    if (retval == 0)
+        return;
+
+    fprintf(stderr, "%s:%d: %s:\n\t%s\n", file, line, strerror(retval), cmd);
+    exit(1);
+}
+
+void
+insist_impl(const char *file, int line, const char *cmd, krb5_boolean result)
+{
+    if (result)
+        return;
+
+    fprintf(stderr, "%s:%d: insist failed:\n\t%s\n", file, line, cmd);
+    exit(1);
+}
diff --git a/src/lib/krad/t_test.h b/src/lib/krad/t_test.h
new file mode 100644
index 0000000..f44742f
--- /dev/null
+++ b/src/lib/krad/t_test.h
@@ -0,0 +1,60 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krad/t_test.h - Shared declarations for libkrad test programs */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef T_TEST_H_
+#define T_TEST_H_
+
+#include "internal.h"
+
+#include <sys/wait.h>
+#include <signal.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define insist(x) insist_impl(__FILE__, __LINE__, #x, x)
+#define noerror(x) noerror_impl(__FILE__, __LINE__, #x, x)
+
+struct event {
+    krb5_boolean error;
+    union
+    {
+        krb5_error_code retval;
+        krad_code code;
+    } result;
+};
+
+void
+noerror_impl(const char *file, int line, const char *cmd, int retval);
+
+void
+insist_impl(const char *file, int line, const char *cmd, krb5_boolean result);
+
+#endif /* T_TEST_H_ */


More information about the cvs-krb5 mailing list