From af754e596a8dbb05ed8580c342e7fe02e08b28e0 Mon Sep 17 00:00:00 2001
From: Daniel Baumann <daniel.baumann@progress-linux.org>
Date: Sat, 13 Apr 2024 16:11:00 +0200
Subject: Adding upstream version 3.2.3+dfsg.

Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
---
 src/main/.gitignore              |   13 +
 src/main/acct.c                  |  186 ++
 src/main/all.mk                  |    3 +
 src/main/auth.c                  |  894 ++++++
 src/main/cb.c                    |  247 ++
 src/main/channel.c               |  231 ++
 src/main/checkrad.in             | 1515 +++++++++
 src/main/checkrad.mk             |    5 +
 src/main/client.c                | 1581 ++++++++++
 src/main/collectd.c              |  382 +++
 src/main/command.c               | 3632 +++++++++++++++++++++
 src/main/conffile.c              | 3821 ++++++++++++++++++++++
 src/main/connection.c            | 1520 +++++++++
 src/main/crypt.c                 |   97 +
 src/main/detail.c                | 1266 ++++++++
 src/main/evaluate.c              | 1144 +++++++
 src/main/exec.c                  |  633 ++++
 src/main/exfile.c                |  547 ++++
 src/main/files.c                 |  361 +++
 src/main/libfreeradius-server.mk |   22 +
 src/main/listen.c                | 4486 ++++++++++++++++++++++++++
 src/main/log.c                   |  923 ++++++
 src/main/mainconfig.c            | 1420 +++++++++
 src/main/map.c                   | 1717 ++++++++++
 src/main/modcall.c               | 4041 ++++++++++++++++++++++++
 src/main/modules.c               | 2302 ++++++++++++++
 src/main/pair.c                  |  911 ++++++
 src/main/parser.c                | 1809 +++++++++++
 src/main/process.c               | 6457 ++++++++++++++++++++++++++++++++++++++
 src/main/radattr.c               | 1123 +++++++
 src/main/radattr.mk              |   10 +
 src/main/radclient.c             | 1712 ++++++++++
 src/main/radclient.mk            |    8 +
 src/main/radiusd.c               |  794 +++++
 src/main/radiusd.mk              |   21 +
 src/main/radlast.in              |    7 +
 src/main/radlast.mk              |    5 +
 src/main/radmin.c                |  773 +++++
 src/main/radmin.mk               |    7 +
 src/main/radsniff.c              | 2683 ++++++++++++++++
 src/main/radsniff.mk.in          |   13 +
 src/main/radtest.in              |  135 +
 src/main/radtest.mk              |    5 +
 src/main/radwho.c                |  565 ++++
 src/main/radwho.mk               |    5 +
 src/main/radzap                  |   54 +
 src/main/radzap.mk               |    5 +
 src/main/realms.c                | 3247 +++++++++++++++++++
 src/main/regex.c                 |  279 ++
 src/main/session.c               |  262 ++
 src/main/soh.c                   |  675 ++++
 src/main/state.c                 |  713 +++++
 src/main/stats.c                 | 1028 ++++++
 src/main/threads.c               | 1697 ++++++++++
 src/main/tls.c                   | 5420 ++++++++++++++++++++++++++++++++
 src/main/tls_listen.c            | 1568 +++++++++
 src/main/tmpl.c                  | 2399 ++++++++++++++
 src/main/unittest.c              |  982 ++++++
 src/main/unittest.mk             |   25 +
 src/main/util.c                  | 1732 ++++++++++
 src/main/version.c               |  625 ++++
 src/main/xlat.c                  | 2696 ++++++++++++++++
 62 files changed, 73439 insertions(+)
 create mode 100644 src/main/.gitignore
 create mode 100644 src/main/acct.c
 create mode 100644 src/main/all.mk
 create mode 100644 src/main/auth.c
 create mode 100644 src/main/cb.c
 create mode 100644 src/main/channel.c
 create mode 100644 src/main/checkrad.in
 create mode 100644 src/main/checkrad.mk
 create mode 100644 src/main/client.c
 create mode 100644 src/main/collectd.c
 create mode 100644 src/main/command.c
 create mode 100644 src/main/conffile.c
 create mode 100644 src/main/connection.c
 create mode 100644 src/main/crypt.c
 create mode 100644 src/main/detail.c
 create mode 100644 src/main/evaluate.c
 create mode 100644 src/main/exec.c
 create mode 100644 src/main/exfile.c
 create mode 100644 src/main/files.c
 create mode 100644 src/main/libfreeradius-server.mk
 create mode 100644 src/main/listen.c
 create mode 100644 src/main/log.c
 create mode 100644 src/main/mainconfig.c
 create mode 100644 src/main/map.c
 create mode 100644 src/main/modcall.c
 create mode 100644 src/main/modules.c
 create mode 100644 src/main/pair.c
 create mode 100644 src/main/parser.c
 create mode 100644 src/main/process.c
 create mode 100644 src/main/radattr.c
 create mode 100644 src/main/radattr.mk
 create mode 100644 src/main/radclient.c
 create mode 100644 src/main/radclient.mk
 create mode 100644 src/main/radiusd.c
 create mode 100644 src/main/radiusd.mk
 create mode 100755 src/main/radlast.in
 create mode 100644 src/main/radlast.mk
 create mode 100644 src/main/radmin.c
 create mode 100644 src/main/radmin.mk
 create mode 100644 src/main/radsniff.c
 create mode 100644 src/main/radsniff.mk.in
 create mode 100644 src/main/radtest.in
 create mode 100644 src/main/radtest.mk
 create mode 100644 src/main/radwho.c
 create mode 100644 src/main/radwho.mk
 create mode 100755 src/main/radzap
 create mode 100644 src/main/radzap.mk
 create mode 100644 src/main/realms.c
 create mode 100644 src/main/regex.c
 create mode 100644 src/main/session.c
 create mode 100644 src/main/soh.c
 create mode 100644 src/main/state.c
 create mode 100644 src/main/stats.c
 create mode 100644 src/main/threads.c
 create mode 100644 src/main/tls.c
 create mode 100644 src/main/tls_listen.c
 create mode 100644 src/main/tmpl.c
 create mode 100644 src/main/unittest.c
 create mode 100644 src/main/unittest.mk
 create mode 100644 src/main/util.c
 create mode 100644 src/main/version.c
 create mode 100644 src/main/xlat.c

(limited to 'src/main')

diff --git a/src/main/.gitignore b/src/main/.gitignore
new file mode 100644
index 0000000..cc538fe
--- /dev/null
+++ b/src/main/.gitignore
@@ -0,0 +1,13 @@
+Makefile
+radsniff.mk
+checkrad
+radclient
+radiusd
+radlast
+radtest
+radsniff
+radwho
+radmin
+radconf2xml
+dhclient
+*_ext
diff --git a/src/main/acct.c b/src/main/acct.c
new file mode 100644
index 0000000..90a0dd8
--- /dev/null
+++ b/src/main/acct.c
@@ -0,0 +1,186 @@
+/*
+ * acct.c	Accounting routines.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2000,2006  The FreeRADIUS server project
+ * Copyright 2000  Miquel van Smoorenburg <miquels@cistron.nl>
+ * Copyright 2000  Alan DeKok <aland@ox.org>
+ * Copyright 2000  Alan Curry <pacman@world.std.com>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+
+#ifdef WITH_ACCOUNTING
+/*
+ *	rad_accounting: call modules.
+ *
+ *	The return value of this function isn't actually used right now, so
+ *	it's not entirely clear if it is returning the right things. --Pac.
+ */
+int rad_accounting(REQUEST *request)
+{
+	int result = RLM_MODULE_OK;
+
+
+#ifdef WITH_PROXY
+#define WAS_PROXIED (request->proxy)
+#else
+#define WAS_PROXIED (0)
+#endif
+
+	/*
+	 *	Run the modules only once, before proxying.
+	 */
+	if (!WAS_PROXIED) {
+		VALUE_PAIR	*vp;
+		int		acct_type = 0;
+
+		result = module_preacct(request);
+		switch (result) {
+		/*
+		 *	The module has a number of OK return codes.
+		 */
+		case RLM_MODULE_NOOP:
+		case RLM_MODULE_OK:
+		case RLM_MODULE_UPDATED:
+			break;
+		/*
+		 *	The module handled the request, stop here.
+		 */
+		case RLM_MODULE_HANDLED:
+			return result;
+		/*
+		 *	The module failed, or said the request is
+		 *	invalid, therefore we stop here.
+		 */
+		case RLM_MODULE_FAIL:
+		case RLM_MODULE_INVALID:
+		case RLM_MODULE_NOTFOUND:
+		case RLM_MODULE_REJECT:
+		case RLM_MODULE_USERLOCK:
+		default:
+			return result;
+		}
+
+		/*
+		 *	Do the data storage before proxying. This is to ensure
+		 *	that we log the packet, even if the proxy never does.
+		 */
+		vp = fr_pair_find_by_num(request->config, PW_ACCT_TYPE, 0, TAG_ANY);
+		if (vp) {
+			acct_type = vp->vp_integer;
+			DEBUG2("  Found Acct-Type %s",
+			       dict_valnamebyattr(PW_ACCT_TYPE, 0, acct_type));
+		}
+		result = process_accounting(acct_type, request);
+		switch (result) {
+		/*
+		 *	In case the accounting module returns FAIL,
+		 *	it's still useful to send the data to the
+		 *	proxy.
+		 */
+		case RLM_MODULE_FAIL:
+		case RLM_MODULE_NOOP:
+		case RLM_MODULE_OK:
+		case RLM_MODULE_UPDATED:
+			break;
+		/*
+		 *	The module handled the request, don't reply.
+		 */
+		case RLM_MODULE_HANDLED:
+			return result;
+		/*
+		 *	Neither proxy, nor reply to invalid requests.
+		 */
+		case RLM_MODULE_INVALID:
+		case RLM_MODULE_NOTFOUND:
+		case RLM_MODULE_REJECT:
+		case RLM_MODULE_USERLOCK:
+		default:
+			return result;
+		}
+
+		/*
+		 *	Maybe one of the preacct modules has decided
+		 *	that a proxy should be used.
+		 */
+		if ((vp = fr_pair_find_by_num(request->config, PW_PROXY_TO_REALM, 0, TAG_ANY))) {
+			REALM *realm;
+
+			/*
+			 *	Check whether Proxy-To-Realm is
+			 *	a LOCAL realm.
+			 */
+			realm = realm_find2(vp->vp_strvalue);
+			if (realm && !realm->acct_pool) {
+				DEBUG("rad_accounting: Cancelling proxy to realm %s, as it is a LOCAL realm.", realm->name);
+				fr_pair_delete_by_num(&request->config, PW_PROXY_TO_REALM, 0, TAG_ANY);
+			} else {
+				/*
+				 *	Don't reply to the NAS now because
+				 *	we have to send the proxied packet
+				 *	before that.
+				 */
+				return result;
+			}
+		}
+	}
+
+#ifdef WITH_PROXY
+	/*
+	 *	We didn't see a reply to the proxied request.  Fail.
+	 */
+	if (request->proxy && !request->proxy_reply) return RLM_MODULE_FAIL;
+#endif
+
+	/*
+	 *	We get here IF we're not proxying, OR if we've
+	 *	received the accounting reply from the end server,
+	 *	THEN we can reply to the NAS.
+	 *      If the accounting module returns NOOP, the data
+	 *      storage did not succeed, so radiusd should not send
+	 *      Accounting-Response.
+	 */
+	switch (result) {
+	/*
+	 *	Send back an ACK to the NAS.
+	 */
+	case RLM_MODULE_OK:
+	case RLM_MODULE_UPDATED:
+		request->reply->code = PW_CODE_ACCOUNTING_RESPONSE;
+		break;
+
+	/*
+	 *	Failed to log or to proxy the accounting data,
+	 *	therefore don't reply to the NAS.
+	 */
+	case RLM_MODULE_FAIL:
+	case RLM_MODULE_INVALID:
+	case RLM_MODULE_NOOP:
+	case RLM_MODULE_NOTFOUND:
+	case RLM_MODULE_REJECT:
+	case RLM_MODULE_USERLOCK:
+	default:
+		break;
+	}
+	return result;
+}
+#endif
diff --git a/src/main/all.mk b/src/main/all.mk
new file mode 100644
index 0000000..2517cd2
--- /dev/null
+++ b/src/main/all.mk
@@ -0,0 +1,3 @@
+SUBMAKEFILES := radclient.mk radiusd.mk radsniff.mk radmin.mk radattr.mk \
+	radwho.mk radlast.mk radtest.mk radzap.mk checkrad.mk \
+	libfreeradius-server.mk unittest.mk
diff --git a/src/main/auth.c b/src/main/auth.c
new file mode 100644
index 0000000..84889b8
--- /dev/null
+++ b/src/main/auth.c
@@ -0,0 +1,894 @@
+/*
+ * auth.c	User authentication.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2000,2006  The FreeRADIUS server project
+ * Copyright 2000  Miquel van Smoorenburg <miquels@cistron.nl>
+ * Copyright 2000  Jeff Carneal <jeff@apex.net>
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/state.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <ctype.h>
+
+/*
+ *	Return a short string showing the terminal server, port
+ *	and calling station ID.
+ */
+char *auth_name(char *buf, size_t buflen, REQUEST *request, bool do_cli)
+{
+	VALUE_PAIR	*cli;
+	VALUE_PAIR	*pair;
+	uint32_t	port = 0;	/* RFC 2865 NAS-Port is 4 bytes */
+	char const	*tls = "";
+
+	if ((cli = fr_pair_find_by_num(request->packet->vps, PW_CALLING_STATION_ID, 0, TAG_ANY)) == NULL) {
+		do_cli = false;
+	}
+
+	if ((pair = fr_pair_find_by_num(request->packet->vps, PW_NAS_PORT, 0, TAG_ANY)) != NULL) {
+		port = pair->vp_integer;
+	}
+
+	if (request->packet->dst_port == 0) {
+		if (fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_PROXIED_TO, 0, TAG_ANY)) {
+			tls = " via TLS tunnel";
+		} else {
+			tls = " via proxy to virtual server";
+		}
+	}
+
+	snprintf(buf, buflen, "from client %.128s port %u%s%.128s%s",
+			request->client->shortname, port,
+		 (do_cli ? " cli " : ""), (do_cli ? cli->vp_strvalue : ""),
+		 tls);
+
+	return buf;
+}
+
+
+
+/*
+ * Make sure user/pass are clean
+ * and then log them
+ */
+static int rad_authlog(char const *msg, REQUEST *request, int goodpass)
+{
+	int logit;
+	char const *extra_msg = NULL;
+	char clean_password[1024];
+	char clean_username[1024];
+	char buf[1024];
+	char extra[1024];
+	char *p;
+	VALUE_PAIR *username = NULL;
+
+	if ((request->reply->code == PW_CODE_ACCESS_ACCEPT) && !request->root->log_accept) {
+		return 0;
+	}
+
+	if ((request->reply->code == PW_CODE_ACCESS_REJECT) && !request->root->log_reject) {
+		return 0;
+	}
+
+	/*
+	 * Get the correct username based on the configured value
+	 */
+	if (!log_stripped_names) {
+		username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY);
+	} else {
+		username = request->username;
+	}
+
+	/*
+	 *	Clean up the username
+	 */
+	if (username == NULL) {
+		strcpy(clean_username, "<no User-Name attribute>");
+	} else {
+		fr_prints(clean_username, sizeof(clean_username), username->vp_strvalue, username->vp_length, '\0');
+	}
+
+	/*
+	 *	Clean up the password
+	 */
+	if (request->root->log_auth_badpass || request->root->log_auth_goodpass) {
+		if (!request->password) {
+			VALUE_PAIR *auth_type;
+
+			auth_type = fr_pair_find_by_num(request->config, PW_AUTH_TYPE, 0, TAG_ANY);
+			if (auth_type) {
+				snprintf(clean_password, sizeof(clean_password),
+					 "<via Auth-Type = %s>",
+					 dict_valnamebyattr(PW_AUTH_TYPE, 0,
+							    auth_type->vp_integer));
+			} else {
+				strcpy(clean_password, "<no User-Password attribute>");
+			}
+		} else if (fr_pair_find_by_num(request->packet->vps, PW_CHAP_PASSWORD, 0, TAG_ANY)) {
+			strcpy(clean_password, "<CHAP-Password>");
+		} else {
+			fr_prints(clean_password, sizeof(clean_password),
+				  request->password->vp_strvalue, request->password->vp_length, '\0');
+		}
+	}
+
+	if (goodpass) {
+		logit = request->root->log_auth_goodpass;
+		extra_msg = request->root->auth_goodpass_msg;
+	} else {
+		logit = request->root->log_auth_badpass;
+		extra_msg = request->root->auth_badpass_msg;
+	}
+
+	if (extra_msg) {
+		extra[0] = ' ';
+		p = extra + 1;
+		if (radius_xlat(p, sizeof(extra) - 1, request, extra_msg, NULL, NULL) < 0) {
+			return -1;
+		}
+	} else {
+		*extra = '\0';
+	}
+
+	RAUTH("%s: [%s%s%s] (%s)%s",
+		       msg,
+		       clean_username,
+		       logit ? "/" : "",
+		       logit ? clean_password : "",
+		       auth_name(buf, sizeof(buf), request, 1),
+		       extra);
+
+	return 0;
+}
+
+/*
+ *	Check password.
+ *
+ *	Returns:	0  OK
+ *			-1 Password fail
+ *			-2 Rejected (Auth-Type = Reject, send Port-Message back)
+ *			1  End check & return, don't reply
+ *
+ *	NOTE: NOT the same as the RLM_ values !
+ */
+static int CC_HINT(nonnull) rad_check_password(REQUEST *request)
+{
+	vp_cursor_t cursor;
+	VALUE_PAIR *auth_type_pair;
+	int auth_type = -1;
+	int result;
+	int auth_type_count = 0;
+
+	/*
+	 *	Look for matching check items. We skip the whole lot
+	 *	if the authentication type is PW_AUTH_TYPE_ACCEPT or
+	 *	PW_AUTH_TYPE_REJECT.
+	 */
+	fr_cursor_init(&cursor, &request->config);
+	while ((auth_type_pair = fr_cursor_next_by_num(&cursor, PW_AUTH_TYPE, 0, TAG_ANY))) {
+		auth_type = auth_type_pair->vp_integer;
+		auth_type_count++;
+
+		RDEBUG2("Found Auth-Type = %s", dict_valnamebyattr(PW_AUTH_TYPE, 0, auth_type));
+		if (auth_type == PW_AUTH_TYPE_REJECT) {
+			RDEBUG2("Auth-Type = Reject, rejecting user");
+
+			return -2;
+		}
+	}
+
+	/*
+	 *	Warn if more than one Auth-Type was found, because only the last
+	 *	one found will actually be used.
+	 */
+	if ((auth_type_count > 1) && (rad_debug_lvl) && request->username) {
+		RERROR("Warning:  Found %d auth-types on request for user '%s'",
+			auth_type_count, request->username->vp_strvalue);
+	}
+
+	/*
+	 *	This means we have a proxy reply or an accept and it wasn't
+	 *	rejected in the above loop. So that means it is accepted and we
+	 *	do no further authentication.
+	 */
+	if ((auth_type == PW_AUTH_TYPE_ACCEPT)
+#ifdef WITH_PROXY
+	    || (request->proxy)
+#endif
+	    ) {
+		RDEBUG2("Auth-Type = Accept, accepting the user");
+		return 0;
+	}
+
+	/*
+	 *	Check that Auth-Type has been set, and reject if not.
+	 *
+	 *	Do quick checks to see if Cleartext-Password or Crypt-Password have
+	 *	been set, and complain if so.
+	 */
+	if (auth_type < 0) {
+		if (fr_pair_find_by_num(request->config, PW_CRYPT_PASSWORD, 0, TAG_ANY) != NULL) {
+			RWDEBUG2("No module configured to handle comparisons with &control:Crypt-Password");
+			RWDEBUG2("Add pap to the authorize { ... } and authenticate { ... } sections of this "
+				 "virtual server to handle this \"known good\" password type");
+		}
+		else if (fr_pair_find_by_num(request->config, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY) != NULL) {
+			RWDEBUG2("No module configured to handle comparisons with &control:Cleartext-Password");
+			RWDEBUG2("Add pap or chap to the authorize { ... } and authenticate { ... } sections "
+				 "of this virtual server to handle this \"known good\" password type");
+		}
+
+		/*
+		 *	The admin hasn't told us how to
+		 *	authenticate the user, so we reject them!
+		 *
+		 *	This is fail-safe.
+		 */
+
+		REDEBUG2("No Auth-Type found: rejecting the user via Post-Auth-Type = Reject");
+		return -2;
+	}
+
+	/*
+	 *	See if there is a module that handles
+	 *	this Auth-Type, and turn the RLM_ return
+	 *	status into the values as defined at
+	 *	the top of this function.
+	 */
+	result = process_authenticate(auth_type, request);
+	switch (result) {
+	/*
+	 *	An authentication module FAIL
+	 *	return code, or any return code that
+	 *	is not expected from authentication,
+	 *	is the same as an explicit REJECT!
+	 */
+	case RLM_MODULE_FAIL:
+	case RLM_MODULE_INVALID:
+	case RLM_MODULE_NOOP:
+	case RLM_MODULE_NOTFOUND:
+	case RLM_MODULE_REJECT:
+	case RLM_MODULE_UPDATED:
+	case RLM_MODULE_USERLOCK:
+	default:
+		result = -1;
+		break;
+
+	case RLM_MODULE_OK:
+		result = 0;
+		break;
+
+	case RLM_MODULE_HANDLED:
+		result = 1;
+		break;
+	}
+
+	return result;
+}
+
+/*
+ *	Post-authentication step processes the response before it is
+ *	sent to the NAS. It can receive both Access-Accept and Access-Reject
+ *	replies.
+ */
+int rad_postauth(REQUEST *request)
+{
+	int	result;
+	int	postauth_type = 0;
+	VALUE_PAIR *vp;
+
+	if (request->reply->code == PW_CODE_ACCESS_CHALLENGE) {
+		fr_pair_delete_by_num(&request->config, PW_POST_AUTH_TYPE, 0, TAG_ANY);
+		vp = pair_make_config("Post-Auth-Type", "Challenge", T_OP_SET);
+		if (!vp) return RLM_MODULE_OK;
+
+	} else if (request->reply->code == PW_CODE_ACCESS_REJECT) {
+		fr_pair_delete_by_num(&request->config, PW_POST_AUTH_TYPE, 0, TAG_ANY);
+		vp = pair_make_config("Post-Auth-Type", "Reject", T_OP_SET);
+		if (!vp) return RLM_MODULE_OK;
+
+	} else {
+		vp = fr_pair_find_by_num(request->config, PW_POST_AUTH_TYPE, 0, TAG_ANY);
+	}
+
+	/*
+	 *	If a method was chosen, use that.
+	 */
+	if (vp) {
+		postauth_type = vp->vp_integer;
+		RDEBUG2("Using Post-Auth-Type %s",
+			dict_valnamebyattr(PW_POST_AUTH_TYPE, 0, postauth_type));
+
+		if (postauth_type == PW_POST_AUTH_TYPE_CHALLENGE) request->reply->code = PW_CODE_ACCESS_CHALLENGE;
+
+		if (postauth_type == PW_POST_AUTH_TYPE_REJECT) request->reply->code = PW_CODE_ACCESS_REJECT;
+	}
+
+	result = process_post_auth(postauth_type, request);
+	switch (result) {
+	/*
+	 *	The module failed, or said to reject the user: Do so.
+	 */
+	case RLM_MODULE_FAIL:
+	case RLM_MODULE_INVALID:
+	case RLM_MODULE_REJECT:
+	case RLM_MODULE_USERLOCK:
+	default:
+		/*
+		 *	We WERE going to have a nice reply, but
+		 *	something went wrong.  So we've got to run
+		 *	Post-Auth-Type Reject.
+		 */
+		if (request->reply->code != PW_CODE_ACCESS_REJECT) {
+			RDEBUG("Using Post-Auth-Type Reject");
+
+			request->reply->code = PW_CODE_ACCESS_REJECT;
+			process_post_auth(PW_POST_AUTH_TYPE_REJECT, request);
+		}
+
+		/*
+		 *	Only discard session state when we're sending
+		 *	packets to the network.  The State attribute
+		 *	is use both for the outer session and copied
+		 *	to the inner-tunnel session for (e.g.) PEAP.
+		 *	So we don't want to delete the information in
+		 *	the inner tunnel, and then have it no longer
+		 *	accessible from the outer session.
+		 */
+		if (!request->parent) fr_state_discard(request, request->packet);
+		result = RLM_MODULE_REJECT;
+		break;
+	/*
+	 *	The module handled the request, cancel the reply.
+	 */
+	case RLM_MODULE_HANDLED:
+		/* FIXME */
+		break;
+	/*
+	 *	The module had a number of OK return codes.
+	 */
+	case RLM_MODULE_NOOP:
+	case RLM_MODULE_NOTFOUND:
+	case RLM_MODULE_OK:
+	case RLM_MODULE_UPDATED:
+		result = RLM_MODULE_OK;
+
+		if (request->reply->code == PW_CODE_ACCESS_CHALLENGE) {
+			fr_state_put_vps(request, request->packet, request->reply);
+
+		} else {
+			fr_state_discard(request, request->packet);
+		}
+		break;
+	}
+
+	/*
+	 *	Rejects during authorize, etc. are handled by the
+	 *	earlier code, which logs a reason for the rejection.
+	 *	If the packet is rejected in post-auth, we need to log
+	 *	that as a separate reason.
+	 */
+	if (result == RLM_MODULE_REJECT) {
+		if (request->reply->code != RLM_MODULE_REJECT) {
+			rad_authlog("Rejected in post-auth", request, 0);
+		}
+		request->reply->code = PW_CODE_ACCESS_REJECT;
+	}
+
+	if (request->reply->code == PW_CODE_ACCESS_REJECT) {
+		if ((vp = fr_pair_find_by_num(request->packet->vps, PW_MODULE_FAILURE_MESSAGE, 0, TAG_ANY)) != NULL) {
+			char msg[MAX_STRING_LEN+19];
+
+			snprintf(msg, sizeof(msg), "Login incorrect (%s)",
+				 vp->vp_strvalue);
+			rad_authlog(msg, request, 0);
+		} else {
+			rad_authlog("Login incorrect", request, 0);
+		}
+	}
+
+	/*
+	 *	If we're still accepting the user, say so.
+	 */
+	if (request->reply->code == PW_CODE_ACCESS_ACCEPT) {
+		if ((vp = fr_pair_find_by_num(request->packet->vps, PW_MODULE_SUCCESS_MESSAGE, 0, TAG_ANY)) != NULL) {
+			char msg[MAX_STRING_LEN+12];
+
+			snprintf(msg, sizeof(msg), "Login OK (%s)",
+				 vp->vp_strvalue);
+			rad_authlog(msg, request, 1);
+		} else {
+			rad_authlog("Login OK", request, 1);
+		}
+	}
+
+	return result;
+}
+
+/*
+ *	Process and reply to an authentication request
+ *
+ *	The return value of this function isn't actually used right now, so
+ *	it's not entirely clear if it is returning the right things. --Pac.
+ */
+int rad_authenticate(REQUEST *request)
+{
+#ifdef WITH_SESSION_MGMT
+	VALUE_PAIR	*check_item;
+#endif
+	VALUE_PAIR	*module_msg;
+	VALUE_PAIR	*tmp = NULL;
+	int		result;
+	char		autz_retry = 0;
+	int		autz_type = 0;
+
+#ifdef WITH_PROXY
+	/*
+	 *	If this request got proxied to another server, we need
+	 *	to check whether it authenticated the request or not.
+	 *
+	 *	request->proxy gets set only AFTER authorization, so
+	 *	it's safe to check it here.  If it exists, it means
+	 *	we're doing a second pass through rad_authenticate().
+	 */
+	if (request->proxy) {
+		int code = 0;
+
+		if (request->proxy_reply) code = request->proxy_reply->code;
+
+		switch (code) {
+		/*
+		 *	Reply of ACCEPT means accept, thus set Auth-Type
+		 *	accordingly.
+		 */
+		case PW_CODE_ACCESS_ACCEPT:
+			tmp = radius_pair_create(request,
+						&request->config,
+						PW_AUTH_TYPE, 0);
+			if (tmp) tmp->vp_integer = PW_AUTH_TYPE_ACCEPT;
+			goto authenticate;
+
+		/*
+		 *	Challenges are punted back to the NAS without any
+		 *	further processing.
+		 */
+		case PW_CODE_ACCESS_CHALLENGE:
+			request->reply->code = PW_CODE_ACCESS_CHALLENGE;
+			fr_state_put_vps(request, request->packet, request->reply);
+			return RLM_MODULE_OK;
+
+		/*
+		 *	ALL other replies mean reject. (this is fail-safe)
+		 *
+		 *	Do NOT do any authorization or authentication. They
+		 *	are being rejected, so we minimize the amount of work
+		 *	done by the server, by rejecting them here.
+		 */
+		case PW_CODE_ACCESS_REJECT:
+			request->reply->code = PW_CODE_ACCESS_REJECT;
+			rad_authlog("Login incorrect (Home Server says so)",
+				    request, 0);
+			return RLM_MODULE_REJECT;
+
+		default:
+			rad_authlog("Login incorrect (Home Server failed to respond)",
+				    request, 0);
+			return RLM_MODULE_REJECT;
+		}
+	}
+#endif
+	/*
+	 *	Look for, and cache, passwords.
+	 */
+	if (!request->password) {
+		request->password = fr_pair_find_by_num(request->packet->vps, PW_USER_PASSWORD, 0, TAG_ANY);
+	}
+	if (!request->password) {
+		request->password = fr_pair_find_by_num(request->packet->vps, PW_CHAP_PASSWORD, 0, TAG_ANY);
+	}
+
+	/*
+	 *	Grab the VPS associated with the State attribute.
+	 */
+	fr_state_get_vps(request, request->packet);
+
+	/*
+	 *	Get the user's authorization information from the database
+	 */
+autz_redo:
+	result = process_authorize(autz_type, request);
+	switch (result) {
+	case RLM_MODULE_NOOP:
+	case RLM_MODULE_NOTFOUND:
+	case RLM_MODULE_OK:
+	case RLM_MODULE_UPDATED:
+		break;
+	case RLM_MODULE_HANDLED:
+		return result;
+	case RLM_MODULE_FAIL:
+	case RLM_MODULE_INVALID:
+	case RLM_MODULE_REJECT:
+	case RLM_MODULE_USERLOCK:
+	default:
+		request->reply->code = PW_CODE_ACCESS_REJECT;
+		if ((module_msg = fr_pair_find_by_num(request->packet->vps, PW_MODULE_FAILURE_MESSAGE, 0, TAG_ANY)) != NULL) {
+			char msg[MAX_STRING_LEN + 16];
+			snprintf(msg, sizeof(msg), "Invalid user (%s)",
+				 module_msg->vp_strvalue);
+			rad_authlog(msg,request,0);
+		} else {
+			rad_authlog("Invalid user", request, 0);
+		}
+		return result;
+	}
+	if (!autz_retry) {
+		tmp = fr_pair_find_by_num(request->config, PW_AUTZ_TYPE, 0, TAG_ANY);
+		if (tmp) {
+			autz_type = tmp->vp_integer;
+			RDEBUG2("Using Autz-Type %s",
+				dict_valnamebyattr(PW_AUTZ_TYPE, 0, autz_type));
+			autz_retry = 1;
+			goto autz_redo;
+		}
+	}
+
+	/*
+	 *	If we haven't already proxied the packet, then check
+	 *	to see if we should.  Maybe one of the authorize
+	 *	modules has decided that a proxy should be used. If
+	 *	so, get out of here and send the packet.
+	 */
+#ifdef WITH_PROXY
+	if (request->proxy == NULL)
+#endif
+	{
+		if ((tmp = fr_pair_find_by_num(request->config, PW_PROXY_TO_REALM, 0, TAG_ANY)) != NULL) {
+			REALM *realm;
+
+			realm = realm_find2(tmp->vp_strvalue);
+
+			/*
+			 *	Don't authenticate, as the request is going to
+			 *	be proxied.
+			 */
+			if (realm && realm->auth_pool) {
+				return RLM_MODULE_OK;
+			}
+
+			/*
+			 *	Catch users who set Proxy-To-Realm to a LOCAL
+			 *	realm (sigh).  But don't complain if it is
+			 *	*the* LOCAL realm.
+			 */
+			if (realm && (strcmp(realm->name, "LOCAL") != 0)) {
+				RWDEBUG2("You set Proxy-To-Realm = %s, but it is a LOCAL realm!  Cancelling proxy request.", realm->name);
+			}
+
+			if (!realm) {
+				RWDEBUG2("You set Proxy-To-Realm = %s, but the realm does not exist!  Cancelling invalid proxy request.", tmp->vp_strvalue);
+			}
+		} else if (((tmp = fr_pair_find_by_num(request->config, PW_HOME_SERVER_POOL, 0, TAG_ANY)) != NULL) ||
+			  ((tmp = fr_pair_find_by_num(request->config, PW_PACKET_DST_IP_ADDRESS, 0, TAG_ANY)) != NULL) ||
+			  ((tmp = fr_pair_find_by_num(request->config, PW_PACKET_DST_IPV6_ADDRESS, 0, TAG_ANY)) != NULL) ||
+			  ((tmp = fr_pair_find_by_num(request->config, PW_HOME_SERVER_NAME, 0, TAG_ANY)) != NULL)) {
+			RDEBUG("Proxying due to %s", tmp->da->name);
+			return RLM_MODULE_OK;
+		}
+	}
+
+#ifdef WITH_PROXY
+authenticate:
+#endif
+
+	/*
+	 *	Validate the user
+	 */
+	do {
+		result = rad_check_password(request);
+		if (result > 0) {
+			return RLM_MODULE_HANDLED;
+		}
+
+	} while(0);
+
+	/*
+	 *	Failed to validate the user.
+	 *
+	 *	We PRESUME that the code which failed will clean up
+	 *	request->reply->vps, to be ONLY the reply items it
+	 *	wants to send back.
+	 */
+	if (result < 0) {
+		RDEBUG2("Failed to authenticate the user");
+		request->reply->code = PW_CODE_ACCESS_REJECT;
+
+		if (request->password) {
+			VERIFY_VP(request->password);
+			/* double check: maybe the secret is wrong? */
+			if ((rad_debug_lvl > 1) && (request->password->da->attr == PW_USER_PASSWORD)) {
+				uint8_t const *p;
+
+				p = (uint8_t const *) request->password->vp_strvalue;
+				while (*p) {
+					int size;
+
+					size = fr_utf8_char(p, -1);
+					if (!size) {
+						RWDEBUG("Unprintable characters in the password.  Double-check the "
+							"shared secret on the server and the NAS!");
+						break;
+					}
+					p += size;
+				}
+			}
+		}
+	}
+
+#ifdef WITH_SESSION_MGMT
+	if (result >= 0 &&
+	    (check_item = fr_pair_find_by_num(request->config, PW_SIMULTANEOUS_USE, 0, TAG_ANY)) != NULL) {
+		int r, session_type = 0;
+		char		logstr[1024];
+		char		umsg[MAX_STRING_LEN + 1];
+
+		tmp = fr_pair_find_by_num(request->config, PW_SESSION_TYPE, 0, TAG_ANY);
+		if (tmp) {
+			session_type = tmp->vp_integer;
+			RDEBUG2("Using Session-Type %s",
+				dict_valnamebyattr(PW_SESSION_TYPE, 0, session_type));
+		}
+
+		/*
+		 *	User authenticated O.K. Now we have to check
+		 *	for the Simultaneous-Use parameter.
+		 */
+		if (request->username &&
+		    (r = process_checksimul(session_type, request, check_item->vp_integer)) != 0) {
+			char mpp_ok = 0;
+
+			if (r == 2){
+				/* Multilink attempt. Check if port-limit > simultaneous-use */
+				VALUE_PAIR *port_limit;
+
+				if ((port_limit = fr_pair_find_by_num(request->reply->vps, PW_PORT_LIMIT, 0, TAG_ANY)) != NULL &&
+					port_limit->vp_integer > check_item->vp_integer){
+					RDEBUG2("MPP is OK");
+					mpp_ok = 1;
+				}
+			}
+			if (!mpp_ok){
+				if (check_item->vp_integer > 1) {
+					snprintf(umsg, sizeof(umsg), "%s (%u)", main_config.denied_msg,
+						 check_item->vp_integer);
+				} else {
+					strlcpy(umsg, main_config.denied_msg, sizeof(umsg));
+				}
+
+				request->reply->code = PW_CODE_ACCESS_REJECT;
+
+				/*
+				 *	They're trying to log in too many times.
+				 *	Remove ALL reply attributes.
+				 */
+				fr_pair_list_free(&request->reply->vps);
+				pair_make_reply("Reply-Message", umsg, T_OP_SET);
+
+				snprintf(logstr, sizeof(logstr), "Multiple logins (max %d) %s",
+					check_item->vp_integer,
+					r == 2 ? "[MPP attempt]" : "");
+				rad_authlog(logstr, request, 1);
+
+				result = -1;
+			}
+		}
+	}
+#endif
+
+	/*
+	 *	Result should be >= 0 here - if not, it means the user
+	 *	is rejected, so we just process post-auth and return.
+	 */
+	if (result < 0) {
+		return RLM_MODULE_REJECT;
+	}
+
+	/*
+	 *	Set the reply to Access-Accept, if it hasn't already
+	 *	been set to something.  (i.e. Access-Challenge)
+	 */
+	if (request->reply->code == 0) {
+		request->reply->code = PW_CODE_ACCESS_ACCEPT;
+	}
+
+	return result;
+}
+
+/*
+ *	Run a virtual server auth and postauth
+ *
+ */
+int rad_virtual_server(REQUEST *request)
+{
+	VALUE_PAIR *vp;
+	int result;
+
+	RDEBUG("Virtual server %s received request", request->server);
+	rdebug_pair_list(L_DBG_LVL_1, request, request->packet->vps, NULL);
+
+	if (!request->username) {
+		request->username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY);
+	}
+
+	/*
+	 *	Complain about possible issues related to tunnels.
+	 */
+	if (request->parent && request->parent->username && request->username) {
+		/*
+		 *	Look at the full User-Name with realm.
+		 */
+		if (request->parent->username->da->attr == PW_STRIPPED_USER_NAME) {
+			vp = fr_pair_find_by_num(request->parent->packet->vps, PW_USER_NAME, 0, TAG_ANY);
+			rad_assert(vp != NULL);
+		} else {
+			vp = request->parent->username;
+		}
+
+		/*
+		 *	If the names aren't identical, we do some detailed checks.
+		 */
+		if (strcmp(vp->vp_strvalue, request->username->vp_strvalue) != 0) {
+			char const *outer, *inner;
+
+			outer = strchr(vp->vp_strvalue, '@');
+
+			/*
+			 *	If there's no realm, or there's a user identifier before
+			 *	the realm name, check the user identifier.
+			 *
+			 *	It SHOULD be "anonymous", or "anonymous@realm"
+			 */
+			if (outer) {
+				if ((outer != vp->vp_strvalue) &&
+				    ((vp->vp_length < 10) || (memcmp(vp->vp_strvalue, "anonymous@", 10) != 0))) {
+					RWDEBUG("Outer User-Name is not anonymized.  User privacy is compromised.");
+				} /* else it is anonymized */
+
+				/*
+				 *	Check when there's no realm, and without the trailing '@'
+				 */
+			} else if ((vp->vp_length < 9) || (memcmp(vp->vp_strvalue, "anonymous", 9) != 0)) {
+					RWDEBUG("Outer User-Name is not anonymized.  User privacy is compromised.");
+
+			} /* else the user identifier is anonymized */
+
+			/*
+			 *	Look for an inner realm, which may or may not exist.
+			 */
+			inner = strchr(request->username->vp_strvalue, '@');
+			if (outer && inner) {
+				outer++;
+				inner++;
+
+				/*
+				 *	The realms are different, do
+				 *	more detailed checks.
+				 */
+				if (strcmp(outer, inner) != 0) {
+					size_t outer_len, inner_len;
+
+					outer_len = vp->vp_length;
+					outer_len -= (outer - vp->vp_strvalue);
+
+					inner_len = request->username->vp_length;
+					inner_len -= (inner - request->username->vp_strvalue);
+
+					/*
+					 *	Inner: secure.example.org
+					 *	Outer: example.org
+					 */
+					if (inner_len > outer_len) {
+						char const *suffix;
+
+						suffix = inner + (inner_len - outer_len) - 1;
+
+						if ((*suffix != '.') ||
+						    (strcmp(suffix + 1, outer) != 0)) {
+							RWDEBUG("Possible spoofing: Inner realm '%s' is not a subdomain of the outer realm '%s'", inner, outer);
+						}
+
+					} else {
+						RWDEBUG("Possible spoofing: Inner realm and outer realms are different");
+					}
+				}
+			}
+
+		} else {
+			RWDEBUG("Outer and inner identities are the same.  User privacy is compromised.");
+		}
+	}
+
+	RDEBUG("server %s {", request->server);
+	RINDENT();
+
+	/*
+	 *	We currently only handle AUTH packets here.
+	 *	This could be expanded to handle other packets as well if required.
+	 */
+	rad_assert(request->packet->code == PW_CODE_ACCESS_REQUEST);
+
+	result = rad_authenticate(request);
+
+	/*
+	 *	Allow bare "accept" and "reject" policies in the inner
+	 *	tunnel.
+	 */
+	if (!request->reply->code &&
+	    (vp = fr_pair_find_by_num(request->config, PW_AUTH_TYPE, 0, TAG_ANY)) != NULL) {
+		switch (vp->vp_integer) {
+		case PW_AUTH_TYPE_ACCEPT:
+			request->reply->code = PW_CODE_ACCESS_ACCEPT;
+			break;
+
+		case PW_AUTH_TYPE_REJECT:
+				request->reply->code = PW_CODE_ACCESS_REJECT;
+				break;
+
+		default:
+			break;
+		}
+	}
+
+	if (request->reply->code == PW_CODE_ACCESS_REJECT) {
+		fr_pair_delete_by_num(&request->config, PW_POST_AUTH_TYPE, 0, TAG_ANY);
+		vp = pair_make_config("Post-Auth-Type", "Reject", T_OP_SET);
+		if (vp) rad_postauth(request);
+	}
+
+	if (request->reply->code == PW_CODE_ACCESS_ACCEPT) {
+		/*
+		 *	Check that there is a name which can be used
+		 *	to identify the user.  The configuration
+		 *	depends on User-Name or Stripped-User-Name
+		 *	existing, and being (mostly) unique to that
+		 *	user.
+		 */
+		if (!request->parent && request->username &&
+		    (request->username->da->attr == PW_USER_NAME) &&
+		    (request->username->vp_strvalue[0] == '@') &&
+		    !fr_pair_find_by_num(request->packet->vps, PW_STRIPPED_USER_NAME, 0, TAG_ANY)) {
+			RWDEBUG("User-Name is anonymized, and no Stripped-User-Name exists.");
+			RWDEBUG("It may be difficult or impossible to identify the user");
+			RWDEBUG("Please update Stripped-User-Name with information which identifies the user");
+		}
+
+		rad_postauth(request);
+	}
+
+	REXDENT();
+	RDEBUG("} # server %s", request->server);
+
+	RDEBUG("Virtual server sending reply");
+	rdebug_pair_list(L_DBG_LVL_1, request, request->reply->vps, NULL);
+
+	return result;
+}
diff --git a/src/main/cb.c b/src/main/cb.c
new file mode 100644
index 0000000..db764aa
--- /dev/null
+++ b/src/main/cb.c
@@ -0,0 +1,247 @@
+/*
+ * cb.c
+ *
+ * Version:     $Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2001  hereUare Communications, Inc. <raghud@hereuare.com>
+ * Copyright 2006  The FreeRADIUS server project
+ */
+
+RCSID("$Id$")
+USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
+
+#include <freeradius-devel/radiusd.h>
+
+#ifdef WITH_TLS
+void cbtls_info(SSL const *s, int where, int ret)
+{
+	char const *role, *state;
+	REQUEST *request = SSL_get_ex_data(s, FR_TLS_EX_INDEX_REQUEST);
+
+	if ((where & ~SSL_ST_MASK) & SSL_ST_CONNECT) {
+		role = "Client ";
+	} else if (((where & ~SSL_ST_MASK)) & SSL_ST_ACCEPT) {
+		role = "Server ";
+	} else {
+		role = "";
+	}
+
+	state = SSL_state_string_long(s);
+	state = state ? state : "<none>";
+
+	if ((where & SSL_CB_LOOP) || (where & SSL_CB_HANDSHAKE_START) || (where & SSL_CB_HANDSHAKE_DONE)) {
+		if (RDEBUG_ENABLED3) {
+			char const *abbrv = SSL_state_string(s);
+			size_t len;
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+			STACK_OF(SSL_CIPHER) *client_ciphers;
+			STACK_OF(SSL_CIPHER) *server_ciphers;
+#endif
+
+			/*
+			 *	Trim crappy OpenSSL state strings...
+			 */
+			len = strlen(abbrv);
+			if ((len > 1) && (abbrv[len - 1] == ' ')) len--;
+
+			RDEBUG3("(TLS) Handshake state [%.*s] - %s%s (%d)",
+				(int)len, abbrv, role, state, SSL_get_state(s));
+
+			/*
+			 *	After a ClientHello, list all the proposed ciphers from the client
+			 */
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+			if (SSL_get_state(s) == TLS_ST_SR_CLNT_HELLO) {
+				int i;
+				int num_ciphers;
+				const SSL_CIPHER *this_cipher;
+
+				server_ciphers = SSL_get_ciphers(s);
+				if (server_ciphers) {
+					RDEBUG3("Server preferred ciphers (by priority)");
+					num_ciphers = sk_SSL_CIPHER_num(server_ciphers);
+					for (i = 0; i < num_ciphers; i++) {
+						this_cipher = sk_SSL_CIPHER_value(server_ciphers, i);
+						RDEBUG3("(TLS)    [%i] %s", i, SSL_CIPHER_get_name(this_cipher));
+					}
+				}
+	
+				client_ciphers = SSL_get_client_ciphers(s);
+				if (client_ciphers) {
+					RDEBUG3("Client preferred ciphers (by priority)");
+					num_ciphers = sk_SSL_CIPHER_num(client_ciphers);
+					for (i = 0; i < num_ciphers; i++) {
+						this_cipher = sk_SSL_CIPHER_value(client_ciphers, i);
+						RDEBUG3("(TLS)    [%i] %s", i, SSL_CIPHER_get_name(this_cipher));
+					}
+				}
+			}
+#endif
+		} else {
+			RDEBUG2("(TLS) Handshake state - %s%s", role, state);
+		}
+		return;
+	}
+
+	if (where & SSL_CB_ALERT) {
+		if ((ret & 0xff) == SSL_AD_CLOSE_NOTIFY) return;
+
+		RERROR("(TLS) Alert %s:%s:%s", (where & SSL_CB_READ) ? "read": "write",
+		       SSL_alert_type_string_long(ret), SSL_alert_desc_string_long(ret));
+		return;
+	}
+
+	if (where & SSL_CB_EXIT) {
+		if (ret == 0) {
+			RERROR("(TLS) %s: Failed in %s", role, state);
+			return;
+		}
+
+		if (ret < 0) {
+			if (SSL_want_read(s)) {
+				RDEBUG2("(TLS) %s: Need to read more data: %s", role, state);
+				return;
+			}
+			RERROR("(TLS) %s: Error in %s", role, state);
+		}
+	}
+}
+
+/*
+ *	Fill in our 'info' with TLS data.
+ */
+void cbtls_msg(int write_p, int msg_version, int content_type,
+	       void const *inbuf, size_t len,
+	       SSL *ssl UNUSED, void *arg)
+{
+	uint8_t const *buf = inbuf;
+	tls_session_t *state = (tls_session_t *)arg;
+
+	/*
+	 *	OpenSSL 1.0.2 calls this function with 'pseudo'
+	 *	content types.  Which breaks our tracking of
+	 *	the SSL Session state.
+	 */
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
+	if ((msg_version == 0) && (content_type > UINT8_MAX)) {
+#else
+	/*
+         *      "...we do not see the need to resolve application breakage
+         *      just because the documentation now is incorrect."
+         *
+         *      https://github.com/openssl/openssl/issues/17262
+	 */
+	if ((content_type > UINT8_MAX) && (content_type != SSL3_RT_INNER_CONTENT_TYPE)) {
+#endif
+		DEBUG4("(TLS) Ignoring cbtls_msg call with pseudo content type %i, version %08x",
+		       content_type, msg_version);
+		return;
+	}
+
+	if ((write_p != 0) && (write_p != 1)) {
+		DEBUG4("(TLS) Ignoring cbtls_msg call with invalid write_p %d", write_p);
+		return;
+	}
+
+	/*
+	 *	Work around bug #298, where we may be called with a NULL
+	 *	argument.  We should really log a serious error
+	 */
+	if (!state) return;
+
+	if (rad_debug_lvl > 3) {
+		size_t i, j, data_len = len;
+		char buffer[3*16 + 1];
+		uint8_t const *in = inbuf;
+
+		DEBUG("(TLS) Received %zu bytes of TLS data", len);
+		if (data_len > 256) data_len = 256;
+
+		for (i = 0; i < data_len; i += 16) {
+			for (j = 0; j < 16; j++) {
+				if ((i + j) >= data_len) break;
+
+				sprintf(buffer + 3 * j, "%02x ", in[i + j]);
+			}
+
+			DEBUG("(TLS)        %s", buffer);
+		}
+	}
+
+	/*
+	 *	0 - received (from peer)
+	 *	1 - sending (to peer)
+	 */
+	state->info.origin = write_p;
+	state->info.content_type = content_type;
+	state->info.record_len = len;
+	state->info.initialized = true;
+
+	if (content_type == SSL3_RT_ALERT) {
+		state->info.alert_level = buf[0];
+		state->info.alert_description = buf[1];
+		state->info.handshake_type = 0x00;
+
+	} else if (content_type == SSL3_RT_HANDSHAKE) {
+		state->info.handshake_type = buf[0];
+		state->info.alert_level = 0x00;
+		state->info.alert_description = 0x00;
+
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+	} else if (content_type == SSL3_RT_INNER_CONTENT_TYPE && buf[0] == SSL3_RT_APPLICATION_DATA) {
+		/* let tls_ack_handler set application_data */
+		state->info.content_type = SSL3_RT_HANDSHAKE;
+#endif
+
+#ifdef SSL3_RT_HEARTBEAT
+	} else if (content_type == TLS1_RT_HEARTBEAT) {
+		uint8_t *p = buf;
+
+		if ((len >= 3) && (p[0] == 1)) {
+			size_t payload_len;
+
+			payload_len = (p[1] << 8) | p[2];
+
+			if ((payload_len + 3) > len) {
+				state->invalid_hb_used = true;
+				ERROR("OpenSSL Heartbeat attack detected.  Closing connection");
+				return;
+			}
+		}
+#endif
+	}
+
+	tls_session_information(state);
+}
+
+int cbtls_password(char *buf,
+		   int num,
+		   int rwflag UNUSED,
+		   void *userdata)
+{
+	size_t len;
+
+	len = strlcpy(buf, (char *)userdata, num);
+	if (len >= (size_t) num) {
+		ERROR("Password too long.  Maximum length is %i bytes", num - 1);
+		return 0;
+	}
+
+	return len;
+}
+
+#endif
diff --git a/src/main/channel.c b/src/main/channel.c
new file mode 100644
index 0000000..757ccd2
--- /dev/null
+++ b/src/main/channel.c
@@ -0,0 +1,231 @@
+/*
+ * radmin.c	RADIUS Administration tool.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2015   The FreeRADIUS server project
+ * Copyright 2015   Alan DeKok <aland@deployingradius.com>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/channel.h>
+
+typedef struct rchannel_t {
+	uint32_t	channel;
+	uint32_t	length;
+} rchannel_t;
+
+
+static ssize_t lo_read(int fd, void *inbuf, size_t buflen)
+{
+	size_t total;
+	ssize_t r;
+	uint8_t *p = inbuf;
+
+	for (total = 0; total < buflen; total += r) {
+		r = read(fd, p + total, buflen - total);
+
+		if (r == 0) return 0;
+
+		if (r < 0) {
+			if (errno == EINTR) continue;
+
+			return -1;
+
+		}
+	}
+
+	return total;
+}
+
+
+/*
+ *	A non-blocking copy of fr_channel_read().
+ */
+ssize_t fr_channel_drain(int fd, fr_channel_type_t *pchannel, void *inbuf, size_t buflen, uint8_t **outbuf, size_t have_read)
+{
+	ssize_t r;
+	size_t data_len;
+	uint8_t *buffer = inbuf;
+	rchannel_t hdr;
+
+	/*
+	 *	If we can't even read a header, die.
+	 */
+	if (buflen <= sizeof(hdr)) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	/*
+	 *	Ensure that we read the header first.
+	 */
+	if (have_read < sizeof(hdr)) {
+		*pchannel = FR_CHANNEL_WANT_MORE;
+
+		r = lo_read(fd, buffer + have_read, sizeof(hdr) - have_read);
+		if (r <= 0) return r;
+
+		have_read += r;
+
+		if (have_read < sizeof(hdr)) return have_read;
+	}
+
+	/*
+	 *	We've read the header.  Figure out how much more data
+	 *	we need to read.
+	 */
+	memcpy(&hdr, buffer, sizeof(hdr));
+	data_len = ntohl(hdr.length);
+
+	/*
+	 *	The data will overflow the buffer.  Die.
+	 */
+	if ((sizeof(hdr) + data_len) > buflen) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	/*
+	 *	This is how much we really want.
+	 */
+	buflen = sizeof(hdr) + data_len;
+
+	r = lo_read(fd, buffer + have_read, buflen - have_read);
+	if (r <= 0) return r;
+
+	have_read += r;
+
+	if (have_read == buflen) {
+		*pchannel = ntohl(hdr.channel);
+		*outbuf = buffer + sizeof(hdr);
+		return data_len;
+	}
+
+	*pchannel = FR_CHANNEL_WANT_MORE;
+	return have_read;
+}
+
+ssize_t fr_channel_read(int fd, fr_channel_type_t *pchannel, void *inbuf, size_t buflen)
+{
+	ssize_t r;
+	size_t data_len;
+	uint8_t *buffer = inbuf;
+	rchannel_t hdr;
+
+	/*
+	 *	Read the header
+	 */
+	r = lo_read(fd, &hdr, sizeof(hdr));
+	if (r <= 0) return r;
+
+	/*
+	 *	Read the data into the buffer.
+	 */
+	*pchannel = ntohl(hdr.channel);
+	data_len = ntohl(hdr.length);
+
+#if 0
+	fprintf(stderr, "CHANNEL R %zu length %zu\n", *pchannel, data_len);
+#endif
+
+	/*
+	 *	Shrink the output buffer to the size of the data we
+	 *	have.
+	 */
+	if (buflen > data_len) buflen = data_len;
+
+	r = lo_read(fd, buffer, buflen);
+	if (r <= 0) return r;
+
+	/*
+	 *	Read and discard any extra data sent to us.  Sorry,
+	 *	caller, you should have used a larger buffer!
+	 */
+	while (data_len > buflen) {
+		size_t discard;
+		uint8_t junk[64];
+
+		discard = data_len - buflen;
+		if (discard > sizeof(junk)) discard = sizeof(junk);
+
+		r = lo_read(fd, junk, discard);
+		if (r <= 0) break;
+
+		data_len -= r;
+	}
+
+	return buflen;
+}
+
+static ssize_t lo_write(int fd, void const *inbuf, size_t buflen)
+{
+	size_t total;
+	ssize_t r;
+	uint8_t const *buffer = inbuf;
+
+	total = buflen;
+
+	while (total > 0) {
+		r = write(fd, buffer, total);
+		if (r == 0) {
+			errno = EAGAIN;
+			return -1;
+		}
+
+		if (r < 0) {
+			if (errno == EINTR) continue;
+
+			return -1;
+		}
+
+		buffer += r;
+		total -= r;
+	}
+
+	return buflen;
+}
+
+ssize_t fr_channel_write(int fd, fr_channel_type_t channel, void const *inbuf, size_t buflen)
+{
+	ssize_t r;
+	rchannel_t hdr;
+	uint8_t const *buffer = inbuf;
+
+	hdr.channel = htonl(channel);
+	hdr.length = htonl(buflen);
+
+#if 0
+	fprintf(stderr, "CHANNEL W %zu length %zu\n", channel, buflen);
+#endif
+
+	/*
+	 *	write the header
+	 */
+	r = lo_write(fd, &hdr, sizeof(hdr));
+	if (r <= 0) return r;
+
+	/*
+	 *	write the data directly from the buffer
+	 */
+	r = lo_write(fd, buffer, buflen);
+	if (r <= 0) return r;
+
+	return buflen;
+}
diff --git a/src/main/checkrad.in b/src/main/checkrad.in
new file mode 100644
index 0000000..c0cf440
--- /dev/null
+++ b/src/main/checkrad.in
@@ -0,0 +1,1515 @@
+#!@PERL@
+#
+# checkrad	See if a user is (still) logged in on a certain port.
+#
+#		This is used by the FreeRADIUS server to check
+#		if its idea of a user logged in on a certain port/nas
+#		is correct if a double login is detected.
+#
+# Called as:	nas_type nas_ip nas_port login session_id
+#
+# Returns:	0 = no duplicate, 1 = duplicate, >1 = error.
+#
+# Version:	$Id$
+#
+#		livingston_snmp  1.2	Author: miquels@cistron.nl
+#		cvx_snmp	 1.0	Author: miquels@cistron.nl
+#		portslave_finger 1.0	Author: miquels@cistron.nl
+#		max40xx_finger	 1.0	Author: costa@mdi.ca
+#		ascend_snmp      1.1    Author: blaz@amis.net
+#		computone_finger 1.2	Author: pacman@world.std.com
+#		sub tc_tccheck	 1.1	Author: alexisv@compass.com.ph
+#		cyclades_telnet	 1.2	Author: accdias@sst.com.br
+#		patton_snmp	 1.0	Author: accdias@sst.com.br
+#		digitro_rusers	 1.1	Author: accdias@sst.com.br
+#		cyclades_snmp	 1.0	Author: accdias@sst.com.br
+#		usrhiper_snmp    1.0    Author: igor@ipass.net
+#		juniper_e_snmp   1.1    Author: guilhermefranco@gmail.com
+#		multitech_snmp   1.0    Author: ehonzay@willmar.com
+#		netserver_telnet 1.0	Author: mts@interplanet.es
+#		versanet_snmp    1.0    Author: support@versanetcomm.com
+#		bay_finger	 1.0	Author: chris@shenton.org
+#		cisco_l2tp	 1.14	Author: paul@distributel.net
+#		mikrotik_telnet  1.1    Author: Evren Yurtesen <yurtesen@ispro.net.tr>
+#		mikrotik_snmp    1.0    Author: Evren Yurtesen <yurtesen@ispro.net.tr>
+#		redback_telnet          Author: Eduardo Roldan
+#
+#	Config: $debug is the file you want to put debug messages in
+#		$snmpget is the location of your ``snmpget'' program
+#		$snmpwalk is the location of your ``snmpwalk'' program
+#	        $snmp_timeout is the timeout for snmp queries
+#	        $snmp_retries is the number of retries for timed out snmp queries
+#		$snmp_version is the version of to use for snmp queries [1,2c,3]
+#		$rusers is the location of your ``rusers'' program
+#		$naspass is the location of your NAS admin password file
+#
+
+$prefix		= "@prefix@";
+$localstatedir	= "@localstatedir@";
+$logdir		= "@logdir@";
+$sysconfdir	= "@sysconfdir@";
+$raddbdir	= "@raddbdir@";
+
+$debug		= "";
+#$debug		= "$logdir/checkrad.log";
+
+$snmpget	= "@SNMPGET@";
+$snmpwalk	= "@SNMPWALK@";
+$snmp_timeout	= 5;
+$snmp_retries	= 1;
+$snmp_version	= "2c";
+$rusers		= "@RUSERS@";
+$naspass	= "$raddbdir/naspasswd";
+
+# Community string. Change this if yours isn't "public".
+$cmmty_string	= "public";
+# path to finger command
+$finger = "/usr/bin/finger";
+
+# Extremely slow way of converting port descriptions to actual indexes
+$portisdescr = 0;
+
+# Realm used by Cisco sub
+$realm = '';
+
+#
+#	USR-Hiper: $hiper_density is the reported port density (default 256
+#	but 24 makes more sense)
+#
+$hiper_density = 256;
+
+#
+#	Try to load Net::Telnet, SNMP_Session etc.
+#	Do not complain if we cannot find it.
+#	Prefer a locally installed copy.
+#
+BEGIN {
+	unshift @INC, "/usr/local/lib/site_perl";
+
+	eval "use Net::Telnet 3.00;";
+	$::HAVE_NET_TELNET = ($@ eq "");
+
+	eval "use SNMP_Session;";
+	if ($@ eq "") {
+		eval "use BER;";
+		$::HAVE_SNMP_SESSION = ($@ eq "");
+		eval "use Socket;";
+	}
+};
+
+#
+#	Get password from /etc/raddb/naspasswd file.
+#	Returns (login, password).
+#
+sub naspasswd {
+	my ($terminalserver, $emptyok) = @_;
+	my ($login, $password);
+	my ($ts, $log, $pass);
+
+	unless (open(NFD, $naspass)) {
+		if (!$emptyok) {
+			print LOG "checkrad: naspasswd file not found; " .
+			"possible match for $ARGV[3]\n" if ($debug);
+			print STDERR "checkrad: naspasswd file not found; " .
+			"possible match for $ARGV[3]\n";
+		}
+		return ();
+	}
+	while (<NFD>) {
+		chop;
+		next if (m/^(#|$|[\t ]+$)/);
+		($ts, $log, $pass) = split(/\s+/, $_, 3);
+		if ($ts eq $terminalserver) {
+			$login = $log;
+			$password = $pass;
+			last;
+		}
+	}
+	close NFD;
+	if ($password eq "" && !$emptyok) {
+		print LOG "checkrad: password for $ARGV[1] is null; " .
+			"possible match for $ARGV[3] on " .
+			"port $ARGV[2]\n" if ($debug);
+		print STDERR "checkrad: password for $ARGV[1] is null; " .
+			"possible match for $ARGV[3] on port $ARGV[2]\n";
+	}
+	($login, $password);
+}
+
+#
+#	See if Net::Telnet is there.
+#
+sub check_net_telnet {
+	if (!$::HAVE_NET_TELNET) {
+		print LOG
+		"  checkrad: Net::Telnet 3.00+ CPAN module not installed\n"
+		if ($debug);
+		print STDERR
+		"checkrad: Net::Telnet 3.00+ CPAN module not installed\n";
+		return 0;
+	}
+	1;
+}
+
+#
+#       Do snmpwalk by calling snmpwalk.
+#
+sub snmpwalk_prog {
+	my ($host, $community, $oid) = @_;
+	local $_;
+
+	print LOG "snpwalk: $snmpwalk -r $snmp_retries -t $snmp_timeout -v$snmp_version -c '$community' $host $oid\n";
+	$_ = `$snmpwalk -r $snmp_retries -t $snmp_timeout -v$snmp_version -c '$community' $host $oid`;
+
+	return $_;
+}
+
+#
+#       Do snmpwalk.
+#
+sub snmpwalk {
+	my $ret;
+
+	if (-x $snmpwalk) {
+		$ret = snmpwalk_prog(@_);
+	} else {
+		$e = "$snmpwalk not found!";
+		print LOG "$e\n" if ($debug);
+		print STDERR "checkrad: $e\n";
+		$ret = "";
+	}
+	$ret;
+}
+
+
+#
+#	Do snmpget by calling snmpget.
+#
+sub snmpget_prog {
+	my ($host, $community, $oid) = @_;
+	my ($ret);
+	local $_;
+
+	print LOG "snmpget: $snmpget -r $snmp_retries -t $snmp_timeout -v$snmp_version -c '$community' $host $oid\n";
+	$_ = `$snmpget -r $snmp_retries -t $snmp_timeout -v$snmp_version -c '$community' $host $oid`;
+	if (/^.*(\s|\")([0-9A-Za-z]{8})(\s|\"|$).*$/) {
+		# Session ID format.
+		$ret = $2;
+	} elsif (/^.*=.*"(.*)"/) {
+		# oid = "...." junk format.
+		$ret = $1;
+	} elsif (/^.*=\s*(?:.*:\s*)?(\S+)/) {
+		# oid = string format
+		$ret = $1;
+	}
+
+	# Strip trailing junk if any.
+	$ret =~ s/\s*Hex:.*$//;
+	$ret;
+}
+
+#
+#	Do snmpget by using SNMP_Session.
+#	Coded by Jerry Workman <jerry@newwave.net>
+#
+sub snmpget_session {
+	my ($host, $community, $OID) = @_;
+	my ($ret);
+	local $_;
+	my (@enoid, $var,$response, $bindings, $binding, $value);
+	my ($inoid, $outoid, $upoid, $oid, @retvals);
+
+	$OID =~ s/^.iso.org.dod.internet.private.enterprises/.1.3.6.1.4.1/;
+
+	push @enoid,  encode_oid((split /\./, $OID));
+	srand();
+
+	my $session = SNMP_Session->open($host, $community, 161);
+	if (!$session->get_request_response(@enoid)) {
+		$e = "No SNMP answer from $ARGV[0].";
+		print LOG "$e\n" if ($debug);
+		print STDERR "checkrad: $e\n";
+		return "";
+	}
+	$response = $session->pdu_buffer;
+	($bindings) = $session->decode_get_response ($response);
+	$session->close ();
+	while ($bindings) {
+		($binding,$bindings) = decode_sequence ($bindings);
+		($oid,$value) = decode_by_template ($binding, "%O%@");
+		my $tempo = pretty_print($value);
+		$tempo=~s/\t/ /g;
+		$tempo=~s/\n/ /g;
+		$tempo=~s/^\s+//;
+		$tempo=~s/\s+$//;
+
+		push @retvals, $tempo;
+	}
+	$retvals[0];
+}
+
+#
+#	Do snmpget
+#
+sub snmpget {
+	my $ret;
+
+	if ($::HAVE_SNMP_SESSION) {
+		$ret = snmpget_session(@_);
+	} elsif (-x $snmpget) {
+		$ret = snmpget_prog(@_);
+	} else {
+		$e = "Neither SNMP_Session module or $snmpget found!";
+		print LOG "$e\n" if ($debug);
+		print STDERR "checkrad: $e\n";
+		$ret = "";
+	}
+	$ret;
+}
+
+#
+#   Get ifindex from description
+#
+sub ifindex {
+	my $port = shift;
+	
+	# If its not an integer, portisdescr lies!
+	return $port unless $portisdescr || $port !~ /^[0-9]*$/;
+	
+	$_ = snmpwalk($ARGV[1], "$cmmty_string", ".1.3.6.1.2.1.2.2.1.2");
+	
+	foreach (split /\n/){
+		if(/\.([0-9]+)\s*=.*$port"?$/){
+			print LOG "  port descr $port is at SNMP ifIndex $1\n" if ($debug);
+			return $1;
+		}
+	}
+	
+
+	return $port;
+}
+
+#
+#	Strip domains, prefixes and suffixes from username
+#
+#	Known prefixes: (P)PP, (S)LIP e (C)SLIP
+#	Known suffixes: .ppp, .slip e .cslip
+#
+#	Author: Antonio Dias of SST Internet <accdias@sst.com.br>
+#
+sub strip_username {
+	my ($user) = @_;
+	#
+	#	Trim white spaces.
+	#
+	$user =~ s/^\s*(.*?)\s*$/$1/;
+	#
+	#	Strip out domains, prefix and suffixes
+	#
+	$user =~ s/\@(.)*$//;
+	$user =~ s/^[PSC]//;
+	$user =~ s/\.(ppp|slip|cslip)$//;
+	$user;
+}
+
+#
+#	Check whether a session is current on any device which implements the standard IEEE 802.1X MIB
+#
+#	Note: Vendors use different formats for the session ID, and it often doesn't map
+#	between Acct-Session-ID so can't be used to identify and 802.1X session (we ignore it).
+#
+#	If a session matching the username is found on the port specified, and the 
+#	session is still active then thats good enough...
+#
+#	Author: Arran Cudbard-Bell <arran.cudbard-bell@freeradius.org>
+#
+$ieeedot1m   = '.iso.0.8802.1.1';
+sub dot1x_snmp {
+	$ifIndex = ifindex($ARGV[2]);
+	
+	# User matches and not terminated yet?
+	if(
+		snmpget($ARGV[1], "$cmmty_string", "$ieeedot1m.1.1.2.4.1.9.$ifIndex") eq $ARGV[3] &&
+		snmpget($ARGV[1], "$cmmty_string", "$ieeedot1m.1.1.2.4.1.8.$ifIndex") eq '999'
+	){
+		print LOG "  found user $ARGV[3] at port $ARGV[2] ($ifIndex)" if $debug;
+		return 1;
+	}
+	
+	0;
+}
+
+#
+#	See if the user is logged in using the Livingston MIB.
+#	We don't check the username but the session ID.
+#
+$lvm	 = '.iso.org.dod.internet.private.enterprises.307';
+sub livingston_snmp {
+
+	#
+	#	We don't know at which ifIndex S0 is, and
+	#	there might be a hole at S23, or at S30+S31.
+	#	So we figure out dynamically which offset to use.
+	#
+	#	If the port < S23, probe ifIndex 5.
+	#	If the port < S30, probe IfIndex 23.
+	#	Otherwise probe ifIndex 32.
+	#
+	my $ifIndex;
+	my $test_index;
+	if ($ARGV[2] < 23) {
+		$test_index = 5;
+	} elsif ($ARGV[2] < 30) {
+		$test_index = 23;
+	} else {
+		$test_index = 32;
+	}
+	$_ = snmpget($ARGV[1], "$cmmty_string", "$lvm.3.2.1.1.1.2.$test_index");
+	/S([0-9]+)/;
+	$xport = $1 + 0;
+	$ifIndex = $ARGV[2] + ($test_index - $xport);
+
+	print LOG "  port S$ARGV[2] at SNMP ifIndex $ifIndex\n"
+		if ($debug);
+
+	#
+	#	Now get the session id from the terminal server.
+	#
+	$sessid = snmpget($ARGV[1], "$cmmty_string", "$lvm.3.2.1.1.1.5.$ifIndex");
+
+	print LOG "  session id at port S$ARGV[2]: $sessid\n" if ($debug);
+
+	($sessid eq $ARGV[4]) ? 1 : 0;
+}
+
+#
+#	See if the user is logged in using the Aptis MIB.
+#	We don't check the username but the session ID.
+#
+# sessionStatusActiveName
+$apm1	 = '.iso.org.dod.internet.private.enterprises.2637.2.2.102.1.12';
+# sessionStatusActiveStopTime
+$apm2	 = '.iso.org.dod.internet.private.enterprises.2637.2.2.102.1.20';
+sub cvx_snmp {
+
+	# Remove unique identifier, then take remainder of the
+	# session-id as a hex number, convert that to decimal.
+	my $sessid = $ARGV[4];
+	$sessid =~ s/^.*://;
+	$sessid =~ s/^0*//;
+	$sessid = "0" if ($sessid eq '');
+
+	#
+	#	Now get the login from the terminal server.
+	#	Blech - the SNMP table is called 'sessionStatusActiveTable,
+	#	but it sometimes lists inactive sessions too.
+	#	However an active session doesn't have a Stop time,
+	#	so we can differentiate that way.
+	#
+	my $login = snmpget($ARGV[1], "$cmmty_string", "$apm1." . hex($sessid));
+	my $stopt = snmpget($ARGV[1], "$cmmty_string", "$apm2." . hex($sessid));
+	$login = "--" if ($stopt > 0);
+
+	print LOG "  login with session-id $ARGV[4]: $login\n" if ($debug);
+
+	(strip_username($login) eq strip_username($ARGV[3])) ? 1 : 0;
+}
+
+#
+#	See if the user is logged in using the Cisco MIB
+#
+$csm	 = '.iso.org.dod.internet.private.enterprises.9';
+sub cisco_snmp {
+
+	# Look up community string in naspasswd file.
+	my ($login, $pass) = naspasswd($ARGV[1], 1);
+	if ($login eq '') {
+		$pass = $cmmty_string;
+	} elsif ($login ne 'SNMP') {
+		if ($debug) {
+			print LOG
+			"   Error: Need SNMP community string for $ARGV[1]\n";
+		}
+		return 2;
+	}
+
+	my $port = $ARGV[2];
+	my $sess_id = hex($ARGV[4]);
+
+	if ($port < 20000) {
+		#
+		#	The AS5350 doesn't support polling the session ID,
+		#	so we do it based on nas-port-id. This only works
+		#	for analog sessions where port < 20000.
+		#	Yes, this means that simultaneous-use on the as5350
+		#	doesn't work for ISDN users.
+		#
+		$login = snmpget($ARGV[1], $pass, "$csm.2.9.2.1.18.$port");
+		print LOG "  user at port S$port: $login\n" if ($debug);
+	} else {
+		$login = snmpget($ARGV[1], $pass,
+				"$csm.9.150.1.1.3.1.2.$sess_id");
+		print LOG "  user with session id $ARGV[4] ($sess_id): " .
+			"$login\n" if ($debug);
+	}
+
+	#	($login eq $ARGV[3]) ? 1 : 0;
+	if($login eq $ARGV[3]) {
+		return 1;
+	}else{
+		$out=snmpwalk($ARGV[1],$pass,".iso.org.dod.internet.private.enterprises.9.10.19.1.3.1.1.3");
+		if($out=~/\"$ARGV[3]\"/){
+			return 1;
+		}else{
+			return 0;
+		}
+	}
+}
+
+#
+#       Check the subscriber name on a Juniper JunosE E-Series BRAS (ERX, E120, E320). Requires "radius acct-session-id-format decimal" configuration in the BRAS.
+#
+#       Author: Guilherme Franco <guilhermefranco@gmail.com>
+#
+sub juniper_e_snmp {
+		#receives acct_session
+		my $temp = $ARGV[4];
+		#removes the leading 0s
+		my $clean_temp = int $temp;
+
+		$out=snmpget($ARGV[1], $cmmty_string, ".1.3.6.1.4.1.4874.2.2.20.1.8.4.1.2.$clean_temp");
+		if($out=~/\"$ARGV[3]\"/){
+			return 1;
+		}else{
+			return 0;
+		}
+}
+
+#
+#       Check a MultiTech CommPlete Server ( CC9600 & CC2400 )
+#
+#       Author: Eric Honzay of Bennett Office Products <ehonzay@willmar.com>
+#
+$msm    = '.iso.org.dod.internet.private.enterprises.995';
+sub multitech_snmp {
+	my $temp = $ARGV[2] + 1;
+
+	$login = snmpget($ARGV[1], "$cmmty_string", "$msm.2.31.1.1.1.$temp");
+	print LOG " user at port S$ARGV[2]: $login\n" if ($debug);
+
+	($login eq $ARGV[3]) ? 1 : 0;
+}
+
+#
+#       Check a Computone Powerrack via finger
+#
+#       Old Author: Shiloh Costa of MDI Internet Inc. <costa@mdi.ca>
+#       New Author: Alan Curry <pacman@world.std.com>
+#
+# The finger response format is version-dependent. To do this *right*, you
+# need to know exactly where the port number and username are. I know that
+# for 1.7.2, and 3.0.4 but for others I just guess.
+# Oh yeah and on top of it all, the thing truncates usernames. --Pac.
+#
+# 1.7.2 and 3.0.4 both look like this:
+#
+# 0    0 000 00:56 luser         pppfsm  Incoming PPP, ppp00, 10.0.0.1
+#
+# and the truncated ones look like this:
+#
+# 25   0 000 00:15 longnameluse..pppfsm  Incoming PPP, ppp25, 10.0.0.26
+#
+# Yes, the fields run together. Long Usernames Considered Harmful.
+#
+sub computone_finger {
+	my $trunc, $ver;
+
+	open(FD, "$finger \@$ARGV[1]|") or return 2;
+	<FD>; # the [hostname] line is definitely uninteresting
+	$trunc = substr($ARGV[3], 0, 12);
+	$ver = "";
+	while(<FD>) {
+		if(/cnx kernel release ([^ ,]+)[, ]/) {
+			$ver = $1;
+			next;
+		}
+		# Check for known versions
+		if ($ver eq '1.7.2' || $ver eq '3.0.4') {
+			if (/^\Q$ARGV[2]\E\s+\S+\s+\S+\s+\S+\s+\Q$trunc\E(\s+|\.\.)/) {
+				close FD;
+				return 1;
+			}
+			next;
+		}
+		# All others.
+		if (/^\s*\Q$ARGV[2]\E\s+.*\s+\Q$trunc\E\s+/) {
+			close FD;
+			return 1;
+		}
+	}
+
+	close FD;
+	return 0;
+}
+
+#
+#	Check an Ascend Max4000 or similar model via finger
+#
+#	Note: Not all software revisions support finger
+#	      You may also need to enable the finger option.
+#
+#	Author: Shiloh Costa of MDI Internet Inc. <costa@mdi.ca>
+#
+sub max40xx_finger {
+	open(FD, "$finger $ARGV[3]\@$ARGV[1]|");
+	while(<FD>) {
+	   $line = $_;
+	   if( $line =~ /Session/ ){
+		next;
+	   }
+
+	   if( $line =~ /$ARGV[4]/ ){
+	      return 1; # user is online
+	   }
+	}
+	close FD;
+	return 0; # user is offline
+}
+
+
+#
+#	Check an Ascend Max4000 or similar model via SNMP
+#
+#	Author: Blaz Zupan of Medinet <blaz@amis.net>
+#
+$asm   = '.iso.org.dod.internet.private.enterprises.529';
+sub ascend_snmp {
+	my $sess_id;
+	my $l1, $l2;
+
+	$l1 = '';
+	$l2 = '';
+
+	#
+	#	If it looks like hex, only try it as hex,
+	#	otherwise try it as both decimal and hex.
+	#
+	$sess_id = $ARGV[4];
+	if ($sess_id !~ /^0/ && $sess_id !~ /[a-f]/i) {
+		$l1 = snmpget($ARGV[1], "$cmmty_string", "$asm.12.3.1.4.$sess_id");
+	}
+	if (!$l1){
+		$sess_id = hex $ARGV[4];
+		$l2 = snmpget($ARGV[1], "$cmmty_string", "$asm.12.3.1.4.$sess_id");
+	}
+
+	print LOG "  user at port S$ARGV[2]: $l1 (dec)\n" if ($debug && $l1);
+	print LOG "  user at port S$ARGV[2]: $l2 (hex)\n" if ($debug && $l2);
+
+	(($l1 && $l1 eq $ARGV[3]) || ($l2 && $l2 eq $ARGV[3])) ? 1 : 0;
+}
+
+
+#
+#	See if the user is logged in using the portslave finger.
+#
+sub portslave_finger {
+	my ($Port_seen);
+
+	$Port_seen = 0;
+
+	open(FD, "$finger \@$ARGV[1]|");
+	while(<FD>) {
+		#
+		#	Check for ^Port. If we don't see it we
+		#	wont get confused by non-portslave-finger
+		#	output too.
+		#
+		if (/^Port/) {
+			$Port_seen++;
+			next;
+		}
+		next if (!$Port_seen);
+		next if (/^---/);
+
+		($port, $user) = /^.(...) (...............)/;
+
+		$port =~ s/ .*//;
+		$user =~ s/ .*//;
+		$ulen = length($user);
+		#
+		#	HACK: strip [PSC] from the front of the username,
+		#	and things like .ppp from the end.
+		#
+		$user =~ s/^[PSC]//;
+		$user =~ s/\.(ppp|slip|cslip)$//;
+
+		#
+		#	HACK: because ut_user usually has max. 8 characters
+		#	we only compare up the the length of $user if the
+		#	unstripped name had 8 chars.
+		#
+		$argv_user = $ARGV[3];
+		if ($ulen == 8) {
+			$ulen = length($user);
+			$argv_user = substr($ARGV[3], 0, $ulen);
+		}
+
+		if ($port == $ARGV[2]) {
+			if ($user eq $argv_user) {
+				print LOG "  $user matches $argv_user " .
+					"on port $port" if ($debug);
+				close FD;
+				return 1;
+			} else {
+				print LOG "  $user doesn't match $argv_user " .
+					"on port $port" if ($debug);
+				close FD;
+				return 0;
+			}
+		}
+	}
+	close FD;
+	0;
+}
+
+#
+#	See if the user is already logged-in at the 3Com/USR Total Control.
+#	(this routine by Alexis C. Villalon <alexisv@compass.com.ph>).
+#	You must have the Net::Telnet module from CPAN for this to work.
+#	You must also have your /etc/raddb/naspasswd made up.
+#
+sub tc_tccheck {
+	#
+	#	Localize all variables first.
+	#
+	my ($Port_seen, $ts, $terminalserver, $log, $login, $pass, $password);
+	my ($telnet, $curprompt, $curline, $ok, $totlines, $ccntr);
+	my (@curlines, @cltok, $user, $port, $ulen);
+
+	return 2 unless (check_net_telnet());
+
+	$terminalserver = $ARGV[1];
+	$Port_seen = 0;
+	#
+	#	Get login name and password for a certain NAS from $naspass.
+	#
+	($login, $password) = naspasswd($terminalserver, 1);
+	return 2 if ($password eq "");
+
+	#
+	#	Communicate with NAS using Net::Telnet, then issue
+	#	the command "show sessions" to see who are logged in.
+	#	Thanks to Chris Jackson <chrisj@tidewater.net> for the
+	#	for the "-- Press Return for More --" workaround.
+	#
+	$telnet = new Net::Telnet (Timeout => 5,
+				   Prompt => '/\>/');
+	$telnet->open($terminalserver);
+	$telnet->login($login, $password);
+	$telnet->print("show sessions");
+	while ($curprompt ne "\>") {
+		($curline, $curprompt) = $telnet->waitfor
+			(String => "-- Press Return for More --",
+			 String => "\>",
+			 Timeout => 5);
+		$ok = $telnet->print("");
+		push @curlines, split(/^/m, $curline);
+	}
+	$telnet->close;
+	#
+	#	Telnet closed.  We got the info.  Let's examine it.
+	#
+	$totlines = @curlines;
+	$ccntr = 0;
+	while($ccntr < $totlines) {
+		#
+		#	Check for ^Port.
+		#
+		if ($curlines[$ccntr] =~ /^Port/) {
+			$Port_seen++;
+			$ccntr++;
+			next;
+		}
+		#
+		#	Ignore all unnecessary lines.
+		#
+		if (!$Port_seen || $curlines[$ccntr] =~ /^---/ ||
+			$curlines[$ccntr] =~ /^ .*$/) {
+			$ccntr++;
+			next;
+		}
+		#
+		#	Parse the current line for the port# and username.
+		#
+		@cltok = split(/\s+/, $curlines[$ccntr]);
+		$ccntr++;
+		$port = $cltok[0];
+		$user = $cltok[1];
+		$ulen = length($user);
+		#
+		#	HACK: strip [PSC] from the front of the username,
+		#	and things like .ppp from the end.  Strip S from
+		#	the front of the port number.
+		#
+		$user =~ s/^[PSC]//;
+		$user =~ s/\.(ppp|slip|cslip)$//;
+		$port =~ s/^S//;
+		#
+		#	HACK: because "show sessions" shows max. 15 characters
+		#	we only compare up to the length of $user if the
+		#	unstripped name had 15 chars.
+		#
+		$argv_user = $ARGV[3];
+		if ($ulen == 15) {
+			$ulen = length($user);
+			$argv_user = substr($ARGV[3], 0, $ulen);
+		}
+		if ($port == $ARGV[2]) {
+			if ($user eq $argv_user) {
+				print LOG "  $user matches $argv_user " .
+					"on port $port" if ($debug);
+				return 1;
+			} else {
+				print LOG "  $user doesn't match $argv_user " .
+					"on port $port" if ($debug);
+				return 0;
+			}
+		}
+	}
+	0;
+}
+
+#
+#	Check a Cyclades PathRAS via telnet
+#
+#	Version: 1.2
+#
+#	Author: Antonio Dias of SST Internet <accdias@sst.com.br>
+#
+sub cyclades_telnet {
+	#
+	#	Localize all variables first.
+	#
+	my ($pr, $pr_login, $pr_passwd, $pr_prompt, $endlist, @list, $port, $user);
+	#
+	#	This variable must match PathRAS' command prompt
+	#	string as entered in menu option 6.2.
+	#	The value below matches the default command prompt.
+	#
+	$pr_prompt = '/Select option ==\>$/i';
+
+	#
+	#	This variable match the end of userslist.
+	#
+	$endlist = '/Type \<enter\>/i';
+
+	#
+	#	Do we have Net::Telnet installed?
+	#
+	return 2 unless (check_net_telnet());
+
+	#
+	#	Get login name and password for NAS
+	#	from $naspass file.
+	#
+	($pr_login, $pr_passwd) = naspasswd($ARGV[1], 1);
+
+	#
+	#	Communicate with PathRAS using Net::Telnet, then access
+	#	menu option 6.8 to see who are logged in.
+	#	Based on PathRAS firmware version 1.2.3
+	#
+	$pr = new Net::Telnet (
+		Timeout		=> 5,
+		Host		=> $ARGV[1],
+		ErrMode		=> 'return'
+	) || return 2;
+
+	#
+	#	Force PathRAS shows its banner.
+	#
+	$pr->break();
+
+	#
+	#	Log on PathRAS
+	#
+	if ($pr->waitfor(Match => '/login : $/i') == 1) {
+		$pr->print($pr_login);
+	} else {
+		print LOG " Error: sending login name to PathRAS\n" if ($debug);
+		$pr->close;
+		return 2;
+	}
+
+	if ($pr->waitfor(Match => '/password : $/i') == 1) {
+		$pr->print($pr_passwd);
+	} else {
+		print LOG " Error: sending password to PathRAS.\n" if ($debug);
+		$pr->close;
+		return 2;
+	}
+
+	$pr->print();
+
+	#
+	#	Access menu option 6 "PathRAS Management"
+	#
+	if ($pr->waitfor(Match => $pr_prompt) == 1) {
+		$pr->print('6');
+	} else {
+		print LOG "  Error: accessing menu option '6'.\n" if ($debug);
+		$pr->close;
+		return 2;
+	}
+	#
+	#	Access menu option 8 "Show Active Ports"
+	#
+	if ($pr->waitfor(Match => $pr_prompt) == 1) {
+		@list = $pr->cmd(String => '8', Prompt => $endlist);
+	} else {
+		print LOG "  Error: accessing menu option '8'.\n" if ($debug);
+		$pr->close;
+		return 2;
+	}
+	#
+	#	Since we got the info we want, let's close
+	#	the telnet session
+	#
+	$pr->close;
+
+	#
+	#	Lets examine the userlist stored in @list
+	#
+	foreach(@list) {
+		#
+		#	We are interested in active sessions only
+		#
+		if (/Active/i) {
+			($port, $user) = split;
+			#
+			#	Strip out any prefix, suffix and
+			#	realm from $user check to see if
+			#	$ARGV[3] matches.
+			#
+			if(strip_username($ARGV[3]) eq strip_username($user)) {
+				print LOG "  User '$ARGV[3]' found on '$ARGV[1]:$port'.\n" if ($debug);
+				return 1;
+			}
+		}
+	}
+	print LOG "  User '$ARGV[3]' not found on '$ARGV[1]'.\n" if ($debug);
+	0;
+}
+
+#
+#	Check a Patton 2800 via SNMP
+#
+#	Version: 1.0
+#
+#	Author: Antonio Dias of SST Internet <accdias@sst.com.br>
+#
+sub patton_snmp {
+   my($oid);
+
+   #$oid = '.1.3.6.1.4.1.1768.5.100.1.40.' . hex $ARGV[4];
+   # Reported by "Andria Legon" <andria@patton.com>
+   # The OID below should be the correct one instead of the one above.
+   $oid = '.1.3.6.1.4.1.1768.5.100.1.56.' . hex $ARGV[4];
+   #
+   # Check if the session still active
+   #
+   if (snmpget($ARGV[1], "monitor", "$oid") == 0) {
+      print LOG "  Session $ARGV[4] still active on NAS " .
+	"$ARGV[1], port $ARGV[2], for user $ARGV[3].\n" if ($debug);
+      return 1;
+   }
+   0;
+}
+
+#
+#      Check a Digitro BXS via rusers
+#
+#      Version: 1.1
+#
+#      Author: Antonio Dias of SST Internet <accdias@sst.com.br>
+#
+sub digitro_rusers {
+   my ($ret);
+   local $_;
+
+   if (-e $rusers && -x $rusers) {
+      #
+      # Get a list of users logged in via rusers
+      #
+      $_ = `$rusers $ARGV[1]`;
+      $ret = ((/$ARGV[3]/) ? 1 : 0);
+   } else {
+      print LOG "   Error: can't execute $rusers\n" if $debug;
+      $ret = 2;
+   }
+   $ret;
+}
+
+#
+#	Check Cyclades PR3000 and PR4000 via SNMP
+#
+#	Version: 1.0
+#
+#	Author: Antonio Dias of SST Internet <accdias@sst.com.br>
+#
+sub cyclades_snmp {
+   my ($oid, $ret);
+   local $_;
+
+   $oid = ".1.3.6.1.4.1.2925.3.3.6.1.1.2";
+
+   $_ = snmpwalk($ARGV[1],"$cmmty_string",$oid);
+   $ret = ((/$ARGV[3]/) ? 1 : 0);
+   $ret;
+}
+
+#
+#	3Com/USR HiPer Arc Total Control.
+#	This works with HiPer Arc 4.0.30
+#	(this routine by Igor Brezac <igor@ipass.net>)
+#
+
+#       This routine modified by Dan Halverson <danh@tbc.net>
+#       to support additional versions of Hiper Arc
+#
+
+$usrm	 = '.iso.org.dod.internet.private.enterprises.429';
+sub usrhiper_snmp {
+	my ($login,$password,$oidext);
+
+	# Look up community string in naspasswd file.
+	($login, $password) = naspasswd($ARGV[1], 1);
+	if ($login && $login ne 'SNMP') {
+		if($debug) {
+			print LOG
+			"   Error: Need SNMP community string for $ARGV[1]\n";
+		}
+		return 2;
+	} else {
+# If password is defined in naspasswd file, use it as community, otherwise use $cmmty_string
+		if ($password eq '') {
+		    $password = "$cmmty_string";
+		}
+	}
+	my ($ver) = get_hiper_ver(usrm=>$usrm, target=>$ARGV[1], community=>$password);
+	$oidext = get_oidext(ver=>$ver, tty=>$ARGV[2]);
+	my ($login);
+
+	$login = snmpget($ARGV[1], $password, "$usrm.4.10.1.1.18.$oidext");
+	if ($login =~ /\"/) {
+		$login =~ /^.*\"([^"]+)\"/;
+		$login = $1;
+	}
+
+	print LOG "  user at port S$ARGV[2]: $login\n" if ($debug);
+
+	($login eq $ARGV[3]) ? 1 : 0;
+}
+
+#
+#     get_hiper_ver and get_oidext by Dan Halverson <danh@tbc.net>
+#
+sub get_hiper_ver {
+    my (%args) = @_;
+    my ($ver
+	);
+    $ver = snmpget ($args{'target'}, $args{'community'}, $args{'usrm'}.".4.1.14.0");
+    return($ver);
+}
+
+#
+#   Add additional OID checks below before the else.
+#   Else is for 4.0.30
+#
+sub get_oidext {
+    my (%args) = @_;
+    my ($oid
+	);
+    if ($args{'ver'} =~ /V5.1.99/) {
+	$oid = $args{'tty'}+1257-1;
+    }
+    else {
+	$oid = 1257 + 256*int(($args{'tty'}-1) / $hiper_density) +
+			     (($args{'tty'}-1) % $hiper_density);
+    }
+    return($oid);
+}
+
+#
+#	Check USR Netserver with Telnet - based on tc_tccheck.
+#	By "Marti" <mts@interplanet.es>
+#
+sub usrnet_telnet {
+	#
+	#	Localize all variables first.
+	#
+	my ($ts, $terminalserver, $login, $password);
+	my ($telnet, $curprompt, $curline, $ok);
+	my (@curlines, $user, $port);
+
+	return 2 unless (check_net_telnet());
+
+	$terminalserver = $ARGV[1];
+	$Port_seen = 0;
+	#
+	#	Get login name and password for a certain NAS from $naspass.
+	#
+	($login, $password) = naspasswd($terminalserver, 1);
+	return 2 if ($password eq "");
+
+	#
+	#	Communicate with Netserver using Net::Telnet, then access
+	#	list connectionsto see who are logged in.
+	#
+	$telnet = new Net::Telnet (Timeout => 5,
+				   Prompt => '/\>/');
+	$telnet->open($terminalserver);
+
+	#
+	#	Log on Netserver
+	#
+	$telnet->login($login, $password);
+
+	#
+	#     Launch list connections command
+
+	$telnet->print("list connections");
+
+	while ($curprompt ne "\>") {
+		($curline, $curprompt) = $telnet->waitfor
+			( String => "\>",
+			 Timeout => 5);
+		$ok = $telnet->print("");
+		push @curlines, split(/^/m, $curline);
+	}
+
+	$telnet->close;
+	#
+	#	Telnet closed.  We got the info.  Let's examine it.
+	#
+	foreach(@curlines) {
+		if ( /mod\:/ ) {
+			($port, $user, $dummy) = split;
+			#
+			# Strip out any prefixes and suffixes
+			# from the username
+			#
+			# uncomment this if you use the standard
+			# prefixes
+			#$user =~ s/^[PSC]//;
+			#$user =~ s/\.(ppp|slip|cslip)$//;
+			#
+			# Check to see if $user is already connected
+			#
+			if ($user eq $ARGV[3]) {
+				print LOG "  $user matches $ARGV[3] " .
+					"on port $port" if ($debug);
+				return 1;
+			};
+		};
+	};
+	print LOG
+	"  $ARGV[3] not found on Netserver logged users list " if ($debug);
+	0;
+}
+
+#
+#	Versanet's Perl Script Support:
+#
+#	___ versanet_snmp 1.0 by support@versanetcomm.com ___ July 1999
+#	Versanet Enterprise MIB Base: 1.3.6.1.4.1.2180
+#
+#	VN2001/2002 use slot/port number to locate modems. To use snmp get we
+#	have to translate the original port number into a slot/port pair.
+#
+$vsm     = '.iso.org.dod.internet.private.enterprises.2180';
+sub versanet_snmp {
+
+	print LOG "argv[2] = $ARGV[2] " if ($debug);
+	$port = $ARGV[2]%8;
+	$port = 8 if ($port eq 0);
+	print LOG "port = $port " if ($debug);
+	$slot = (($ARGV[2]-$port)/8)+1;
+	print LOG "slot = $slot" if ($debug);
+	$loginname = snmpget($ARGV[1], "$cmmty_string", "$vsm.27.1.1.3.$slot.$port");
+#
+#	Note: the "$cmmty_string" string above could be replaced by the public
+#	      community string defined in Versanet VN2001/VN2002.
+#
+	  print LOG "  user at slot $slot port $port: $loginname\n" if ($debug);	  ($loginname eq $ARGV[3]) ? 1 : 0;
+}
+
+
+# 1999/08/24 Chris Shenton <chris@shenton.org>
+# Check Bay8000 NAS (aka: Annex) using finger.
+# Returns from "finger @bay" like:
+#   Port  What User         Location         When          Idle  Address
+#   asy2  PPP  bill         ---              9:33am         :08  192.0.2.194
+#   asy4  PPP  hillary      ---              9:36am         :04  192.0.2.195
+#   [...]
+# But also returns partial-match users if you say like "finger g@bay":
+#   Port  What User         Location         When          Idle  Address
+#   asy2  PPP  gore         ---              9:33am         :09  192.0.2.194
+#   asy22 PPP  gwbush       ---              Mon  9:19am    :07  192.0.2.80
+# So check exact match of username!
+
+sub bay_finger {		# ARGV: 1=nas_ip, 2=nas_port, 3=login, 4=sessid
+    open(FINGER, "$finger $ARGV[3]\@$ARGV[1]|") || return 2; # error
+    while(<FINGER>) {
+	my ($Asy, $PPP, $User) = split;
+	if( $User =~ /^$ARGV[3]$/ ){
+	    close FINGER;
+	    print LOG "checkrad:bay_finger: ONLINE $ARGV[3]\@$ARGV[1]"
+		if ($debug);
+	    return 1; # online
+	}
+    }
+    close FINGER;
+    print LOG "checkrad:bay_finger: offline $ARGV[3]\@$ARGV[1]" if ($debug);
+    return 0; # offline
+}
+
+#
+#	Cisco L2TP support
+#	This is for PPP sessions coming from an L2TP tunnel from a Dial
+#	or DSL wholesale provider
+#	Paul Khavkine <paul@distributel.net>
+#	July 19 2001
+#
+# find_l2tp_login() walks a part of cisco vpdn tree to find out what session
+# and tunnel ID's are for a given Virtual-Access interface to construct
+# the following OID: .1.3.6.1.4.1.9.10.24.1.3.2.1.2.2.$tunID.$sessID
+# Then gets the username from that OID.
+# Make sure you set the $realm variable at the begining of the file if
+# needed. The new type for naslist is cisco_l2tp
+
+sub find_l2tp_login
+{
+  my($host, $community, $port_num) = @_;
+  my $l2tp_oid = '.1.3.6.1.4.1.9.10.24.1.3.2.1.2.2';
+  my $port_oid = '.iso.org.dod.internet.private.enterprises.9.10.51.1.2.1.1.2.2';
+  my $port = 'Vi' . $port_num;
+
+  my $sess = new SNMP::Session(DestHost => $host, Community =>  $community);
+  my $snmp_var = new  SNMP::Varbind(["$port_oid"]);
+  my $val = $sess->getnext($snmp_var);
+
+  do
+  {
+    $sess->getnext($snmp_var);
+  } until ($snmp_var->[$SNMP::Varbind::val_f] =~ /$port/) ||
+	(!($snmp_var->[$SNMP::Varbind::ref_f] =~ /^$port_oid\.(\d+)\.(\d+)$/)) ||
+	($sess->{ErrorNum});
+
+  my $val1 = $snmp_var->[$SNMP::Varbind::ref_f];
+
+  if ($val1 =~ /^$port_oid/) {
+    $result = substr($val1, length($port_oid));
+    $result =~ /^\.(\d+)\.(\d+)$/;
+    $tunID = $1;
+    $sessID = $2;
+  }
+
+  my $snmp_var1 = new SNMP::Varbind(["$l2tp_oid\.$tunID\.$sessID"]);
+  $val = $sess->get($snmp_var1);
+  my $login = $snmp_var1->[$SNMP::Varbind::val_f];
+
+  return $login;
+}
+
+sub cisco_l2tp_snmp
+{
+  my $login = find_l2tp_login("$ARGV[1]", $cmmty_string, "$ARGV[2]");
+  print LOG "  user at port S$ARGV[2]: $login\n" if ($debug);
+  ($login eq "$ARGV[3]\@$realm") ? 1 : 0;
+}
+
+sub mikrotik_snmp {
+
+  # Set SNMP version
+  # MikroTik only supports version 1
+  $snmp_version = "1";
+
+  # Look up community string in naspasswd file.
+  ($login, $password) = naspasswd($ARGV[1], 1);
+  if ($login && $login ne 'SNMP') {
+    if($debug) {
+      print LOG "Error: Need SNMP community string for $ARGV[1]\n";
+    }
+    return 2;
+  } else {
+  # If password is defined in naspasswd file, use it as community,
+  # otherwise use $cmmty_string
+    if ($password eq '') {
+      $password = "$cmmty_string";
+    }
+  }
+
+  # We want  mtxrInterfaceStatsName from MIKROTIK-MIB
+  $oid = "1.3.6.1.4.1.14988.1.1.14.1.1.2";
+
+  # Mikrotik doesnt give port IDs correctly to RADIUS :(
+  # practically this would limit us to a simple only-one user limit for
+  # this script to work properly.
+  @output = snmpwalk_prog($ARGV[1], $password, "$oid");
+
+  foreach $line ( @output ) {
+    #remove newline
+    chomp $line;
+    #remove trailing whitespace
+    ($line = $line) =~ s/\s+$//;
+    if( $line =~ /<.*-$ARGV[3]>/ ) {
+      $username_seen++;
+    }
+  }
+
+  #lets return something
+  if ($username_seen > 0) {
+    return 1;
+  } else {
+    return 0;
+  }
+}
+
+sub mikrotik_telnet {
+  # Localize all variables first.
+  my ($t, $login, $password);
+  my (@fields, @output, $output, $username_seen, $user);
+
+  return 2 unless (check_net_telnet());
+
+  $terminalserver = $ARGV[1];
+  $user = $ARGV[3];
+
+  # Get login name and password for a certain NAS from $naspass.
+  ($login, $password) = naspasswd($terminalserver, 1);
+  return 2 if ($password eq "");
+
+  # MikroTik routeros doesnt tell us to which port the user is connected
+  # practically this would limit us to a simple only-one user limit for
+  # this script to work properly.
+  $t = new Net::Telnet (Timeout => 5,
+			Prompt => '//\[.*@.*\] > /');
+
+  # Dont just exit when there is error
+  $t->errmode('return');
+
+  # Telnet to terminal server
+  $t->open($terminalserver) or return 2;
+
+  #Send login and password etc.
+  $t->login(Name => $login,
+	    Password => $password,
+  # We must detect if we are logged in from the login banner.
+  # Because if routeros is with a free license the command
+  # prompt dont come. Instead it waits us to press "Enter".
+	    Prompt => '/MikroTik/');
+
+  # Just be sure that routeros isn't waiting for us to press "Enter"
+  $t->print("");
+
+  # Wait for the real prompt
+  $t->waitfor('/\[.*@.*\] > /');
+
+  # It is not possible to get the line numbers etc.
+  # Thus we cant support if simultaneous-use is over 1
+  # At least I was using pppoe so it wasnt possible.
+  $t->print('ppp active print column name detail');
+
+  # Somehow routeros echo'es our commands 2 times. We dont want to mix
+  # this with the real command prompt.
+  $t->waitfor('/\[.*@.*\] > ppp active print column name detail/');
+
+  # Now lets get the list of online ppp users.
+  ( $output ) = $t->waitfor('/\[.*@.*\] > /');
+
+  # For debugging we can print the list to stdout
+#  print $output;
+
+  #Lets logout to make everybody happy.
+  #If we close the connection without logging out then routeros
+  #starts to complain after a while. Saying;
+  #telnetd: All network ports in use.
+  $t->print("quit");
+  $t->close;
+
+  #check for # of $user in output
+  #the output includes only one = between name and username so we can
+  #safely use it as a seperator.
+
+#disabled until mikrotik starts to send newline after each line...
+#  @output = $output;
+#  foreach $line ( @output ) {
+#    #remove newline
+#    chomp $line;
+#    #remove trailing whitespace
+#    ($line = $line) =~ s/\s+$//;
+#    if( $line =~ /name=/ ) {
+#      print($line);
+#      @fields = split( /=/, $line );
+#      if( $fields[1] == "\"$user\"") {
+#        $username_seen++;
+#      }
+#    }
+#  }
+
+  if( $output =~ /name="$user"/ ) {
+    $username_seen++;
+  }
+
+  #lets return something
+  if ($username_seen > 0) {
+    return 1;
+  } else {
+    return 0;
+  }
+}
+
+sub redback_telnet {
+    #Localize all variables first.
+    my ($terminalserver, $login, $password);
+    my ($user, $context, $operprompt, $adminprompt, $t);
+    return 2 unless (check_net_telnet());
+    $terminalserver = $ARGV[1];
+    ($user, $context) = split /@/, $ARGV[3];
+    if (not $user) {
+	print LOG " Error: No user defined\n" if ($debug);
+	return 2;
+    }
+    if (not $context) {
+	print LOG " Error: No context defined\n" if ($debug);
+	return 2;
+    }
+
+    # Get loggin information
+    ($root, $password) = naspasswd($terminalserver, 1);
+    return 2 if ($password eq "");
+
+    $operprompt = '/\[.*\].*>$/';
+    $adminprompt = '/\[.*\].*#$/';
+
+    # Logging to the RedBack NAS
+    $t = new Net::Telnet (Timeout => 5, Prompt => $operprompt);
+    $t->input_log("./debug");
+    $t->open($terminalserver);
+    $t->login($root, $password);
+
+    #Enable us
+    $t->print('ena');
+    $t->waitfor('/Password/');
+    $t->print($password);
+    $t->waitfor($adminprompt);
+    $t->prompt($adminprompt);
+
+    #Switch context
+    $t->cmd(String => "context $context");
+
+    #Ask the question
+    @lines = $t->cmd(String => "show subscribers active $user\@$context");
+    if ($lines[0] =~ /subscriber $user\@$context/ ) {
+	return 1;
+    }
+    return 0;
+}
+
+###############################################################################
+
+# Poor man's getopt (for -d)
+if ($ARGV[0] eq '-d') {
+	shift @ARGV;
+	$debug = "stdout";
+}
+
+if ($debug) {
+	if ($debug eq 'stdout') {
+		open(LOG, ">&STDOUT");
+	} elsif ($debug eq 'stderr') {
+		open(LOG, ">&STDERR");
+	} else {
+		open(LOG, ">>$debug");
+		$now = localtime;
+		print LOG "$now checkrad @ARGV\n";
+	}
+}
+
+if ($#ARGV != 4) {
+	print LOG "Usage: checkrad nas_type nas_ip " .
+			"nas_port login session_id\n" if ($debug);
+	print STDERR "Usage: checkrad nas_type nas_ip " .
+			"nas_port login session_id\n"
+			unless ($debug =~ m/^(stdout|stderr)$/);
+	close LOG if ($debug);
+	exit(2);
+}
+
+if ($ARGV[0] eq 'livingston') {
+	$ret = &livingston_snmp;
+} elsif ($ARGV[0] eq 'cisco') {
+	$ret = &cisco_snmp;
+} elsif ($ARGV[0] eq 'cvx') {
+	$ret = &cvx_snmp;
+} elsif ($ARGV[0] eq 'juniper') {
+	$ret = &juniper_e_snmp;
+} elsif ($ARGV[0] eq 'multitech') {
+	$ret = &multitech_snmp;
+} elsif ($ARGV[0] eq 'computone') {
+	$ret = &computone_finger;
+} elsif ($ARGV[0] eq 'max40xx') {
+	$ret = &max40xx_finger;
+} elsif ($ARGV[0] eq 'ascend' || $ARGV[0] eq 'max40xx_snmp') {
+	$ret = &ascend_snmp;
+} elsif ($ARGV[0] eq 'portslave') {
+	$ret = &portslave_finger;
+} elsif ($ARGV[0] eq 'tc') {
+	$ret = &tc_tccheck;
+} elsif ($ARGV[0] eq 'pathras') {
+	$ret = &cyclades_telnet;
+} elsif ($ARGV[0] eq 'pr3000') {
+	$ret = &cyclades_snmp;
+} elsif ($ARGV[0] eq 'pr4000') {
+	$ret = &cyclades_snmp;
+} elsif ($ARGV[0] eq 'patton') {
+	$ret = &patton_snmp;
+} elsif ($ARGV[0] eq 'digitro') {
+	$ret = &digitro_rusers;
+} elsif ($ARGV[0] eq 'usrhiper') {
+	$ret = &usrhiper_snmp;
+} elsif ($ARGV[0] eq 'netserver') {
+	$ret = &usrnet_telnet;
+} elsif ($ARGV[0] eq 'versanet') {
+	$ret = &versanet_snmp;
+} elsif ($ARGV[0] eq 'bay') {
+	$ret = &bay_finger;
+} elsif ($ARGV[0] eq 'cisco_l2tp'){
+	$ret = &cisco_l2tp_snmp;
+} elsif ($ARGV[0] eq 'mikrotik'){
+	$ret = &mikrotik_telnet;
+} elsif ($ARGV[0] eq 'mikrotik_snmp'){
+	$ret = &mikrotik_snmp;
+} elsif ($ARGV[0] eq 'redback'){
+	$ret = &redback_telnet;
+} elsif ($ARGV[0] eq 'dot1x'){
+	$ret = &dot1x_snmp;
+} elsif ($ARGV[0] eq 'other') {
+	$ret = 1;
+} else {
+	print LOG "  checkrad: unknown NAS type $ARGV[0]\n" if ($debug);
+	print STDERR "checkrad: unknown NAS type $ARGV[0]\n";
+	$ret = 2;
+}
+
+if ($debug) {
+	$mn = "login ok";
+	$mn = "double detected" if ($ret == 1);
+	$mn = "error detected" if ($ret == 2);
+	print LOG "  Returning $ret ($mn)\n";
+	close LOG;
+}
+
+exit($ret);
diff --git a/src/main/checkrad.mk b/src/main/checkrad.mk
new file mode 100644
index 0000000..e991bf7
--- /dev/null
+++ b/src/main/checkrad.mk
@@ -0,0 +1,5 @@
+install: $(R)$(sbindir)/checkrad
+
+$(R)$(sbindir)/checkrad: src/main/checkrad | $(R)$(sbindir)
+	@echo INSTALL $(notdir $<)
+	@$(INSTALL) -m 755 $< $(R)$(sbindir)
diff --git a/src/main/client.c b/src/main/client.c
new file mode 100644
index 0000000..12f7824
--- /dev/null
+++ b/src/main/client.c
@@ -0,0 +1,1581 @@
+/*
+ *   This program is is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or (at
+ *   your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ * @file main/client.c
+ * @brief Manage clients allowed to communicate with the server.
+ *
+ * @copyright 2015 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
+ * @copyright 2000,2006 The FreeRADIUS server project
+ * @copyright 2000 Alan DeKok <aland@ox.org>
+ * @copyright 2000 Miquel van Smoorenburg <miquels@cistron.nl>
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <fcntl.h>
+
+#ifdef WITH_DYNAMIC_CLIENTS
+#ifdef HAVE_DIRENT_H
+#include <dirent.h>
+#endif
+#endif
+
+struct radclient_list {
+	char const	*name;	/* name of this list */
+	char const	*server; /* virtual server associated with this client list */
+
+	/*
+	 *	FIXME: One set of trees for IPv4, and another for IPv6?
+	 */
+	rbtree_t	*trees[129]; /* for 0..128, inclusive. */
+	uint32_t       	min_prefix;
+
+	bool		parsed;
+};
+
+
+#ifdef WITH_STATS
+static rbtree_t		*tree_num = NULL;     /* client numbers 0..N */
+static int		tree_num_max = 0;
+#endif
+static RADCLIENT_LIST	*root_clients = NULL;
+
+/*
+ *	Callback for freeing a client.
+ */
+void client_free(RADCLIENT *client)
+{
+	if (!client) return;
+
+	talloc_free(client);
+}
+
+/*
+ *	Callback for comparing two clients.
+ */
+static int client_ipaddr_cmp(void const *one, void const *two)
+{
+	RADCLIENT const *a = one;
+	RADCLIENT const *b = two;
+#ifndef WITH_TCP
+
+	return fr_ipaddr_cmp(&a->ipaddr, &b->ipaddr);
+#else
+	int rcode;
+
+	rcode = fr_ipaddr_cmp(&a->ipaddr, &b->ipaddr);
+	if (rcode != 0) return rcode;
+
+	/*
+	 *	Wildcard match
+	 */
+	if ((a->proto == IPPROTO_IP) ||
+	    (b->proto == IPPROTO_IP)) return 0;
+
+	return (a->proto - b->proto);
+#endif
+}
+
+#ifdef WITH_STATS
+static int client_num_cmp(void const *one, void const *two)
+{
+	RADCLIENT const *a = one;
+	RADCLIENT const *b = two;
+
+	return (a->number - b->number);
+}
+#endif
+
+/*
+ *	Free a RADCLIENT list.
+ */
+void client_list_free(RADCLIENT_LIST *clients)
+{
+	int i;
+
+	if (!clients) clients = root_clients;
+	if (!clients) return;	/* Clients may not have been initialised yet */
+
+	for (i = 0; i <= 128; i++) {
+		if (clients->trees[i]) rbtree_free(clients->trees[i]);
+		clients->trees[i] = NULL;
+	}
+
+	if (clients == root_clients) {
+#ifdef WITH_STATS
+		if (tree_num) rbtree_free(tree_num);
+		tree_num = NULL;
+		tree_num_max = 0;
+#endif
+		root_clients = NULL;
+	}
+
+#ifdef WITH_DYNAMIC_CLIENTS
+	/*
+	 *	FIXME: No fr_fifo_delete()
+	 */
+#endif
+
+	talloc_free(clients);
+}
+
+/*
+ *	Return a new, initialized, set of clients.
+ */
+RADCLIENT_LIST *client_list_init(CONF_SECTION *cs)
+{
+	RADCLIENT_LIST *clients = talloc_zero(cs, RADCLIENT_LIST);
+
+	if (!clients) return NULL;
+
+	clients->min_prefix = 128;
+
+	/*
+	 *	Associate the "clients" list with the virtual server.
+	 */
+	if (cs && (cf_data_add(cs, "clients", clients, NULL) < 0)) {
+		ERROR("Failed to associate client list with section %s\n", cf_section_name1(cs));
+		client_list_free(clients);
+		return false;
+	}
+
+	return clients;
+}
+
+/** Add a client to a RADCLIENT_LIST
+ *
+ * @param clients list to add client to, may be NULL if global client list is being used.
+ * @param client to add.
+ * @return true on success, false on failure.
+ */
+bool client_add(RADCLIENT_LIST *clients, RADCLIENT *client)
+{
+	RADCLIENT *old;
+	char buffer[INET6_ADDRSTRLEN + 3];
+
+	if (!client) return false;
+
+	/*
+	 *	Initialize the global list, if not done already.
+	 */
+	if (!root_clients) {
+		root_clients = cf_data_find(main_config.config, "clients");
+		if (!root_clients) root_clients = client_list_init(main_config.config);
+		if (!root_clients) {
+			ERROR("Cannot add client - failed creating client list");
+			return false;
+		}
+	}
+
+	/*
+	 *	Hack to fixup wildcard clients
+	 *
+	 *	If the IP is all zeros, with a 32 or 128 bit netmask
+	 *	assume the user meant to configure 0.0.0.0/0 instead
+	 *	of 0.0.0.0/32 - which would require the src IP of
+	 *	the client to be all zeros.
+	 */
+	if (fr_inaddr_any(&client->ipaddr) == 1) switch (client->ipaddr.af) {
+	case AF_INET:
+		if (client->ipaddr.prefix == 32) client->ipaddr.prefix = 0;
+		break;
+
+	case AF_INET6:
+		if (client->ipaddr.prefix == 128) client->ipaddr.prefix = 0;
+		break;
+
+	default:
+		rad_assert(0);
+	}
+
+	fr_ntop(buffer, sizeof(buffer), &client->ipaddr);
+	DEBUG3("Adding client %s (%s) to prefix tree %i", buffer, client->longname, client->ipaddr.prefix);
+
+	/*
+	 *	If the client also defines a server, do that now.
+	 */
+	if (client->defines_coa_server) if (!realm_home_server_add(client->coa_home_server)) return false;
+
+	/*
+	 *	If there's no client list, BUT there's a virtual
+	 *	server, try to add the client to the appropriate
+	 *	"clients" section for that virtual server.
+	 */
+	if (!clients && client->server) {
+		CONF_SECTION *cs;
+		CONF_SECTION *subcs;
+		CONF_PAIR *cp;
+		char const *section_name;
+
+		cs = cf_section_sub_find_name2(main_config.config, "server", client->server);
+		if (!cs) {
+			ERROR("Cannot add client - virtual server %s does not exist", client->server);
+			return false;
+		}
+
+		/*
+		 *	If this server has no "listen" section, add the clients
+		 *	to the global client list.
+		 */
+		subcs = cf_section_sub_find(cs, "listen");
+		if (!subcs) {
+			DEBUG("No 'listen' section in virtual server %s.  Adding client to global client list",
+			      client->server);
+			goto check_list;
+		}
+
+		cp = cf_pair_find(subcs, "clients");
+		if (!cp) {
+			DEBUG("No 'clients' configuration item in first listener of virtual server %s.  Adding client to global client list",
+			      client->server);
+			goto check_list;
+		}
+
+		/*
+		 *	Duplicate the lookup logic in common_socket_parse()
+		 *
+		 *	Explicit list given: use it.
+		 */
+		section_name = cf_pair_value(cp);
+		if (!section_name) goto check_list;
+
+		subcs = cf_section_sub_find_name2(main_config.config, "clients", section_name);
+		if (!subcs) {
+			subcs = cf_section_find(section_name);
+		}
+		if (!subcs) {
+			cf_log_err_cs(cs,
+				   "Failed to find clients %s {...}",
+				   section_name);
+			return false;
+		}
+
+		DEBUG("Adding client to client list %s", section_name);
+
+		/*
+		 *	If the client list already exists, use that.
+		 *	Otherwise, create a new client list.
+		 *
+		 *	@todo - add the client to _all_ listeners?
+		 */
+		clients = cf_data_find(subcs, "clients");
+		if (clients) goto check_list;
+
+		clients = client_list_init(subcs);
+		if (!clients) {
+			ERROR("Cannot add client - failed creating client list %s for server %s", section_name,
+			      client->server);
+			return false;
+		}
+	}
+
+check_list:
+	if (!clients) clients = root_clients;
+	client->list = clients;
+
+	/*
+	 *	Create a tree for it.
+	 */
+	if (!clients->trees[client->ipaddr.prefix]) {
+		clients->trees[client->ipaddr.prefix] = rbtree_create(clients, client_ipaddr_cmp, NULL, 0);
+		if (!clients->trees[client->ipaddr.prefix]) {
+			return false;
+		}
+	}
+
+#define namecmp(a) ((!old->a && !client->a) || (old->a && client->a && (strcmp(old->a, client->a) == 0)))
+
+	/*
+	 *	Cannot insert the same client twice.
+	 */
+	old = rbtree_finddata(clients->trees[client->ipaddr.prefix], client);
+	if (old) {
+		/*
+		 *	If it's a complete duplicate, then free the new
+		 *	one, and return "OK".
+		 */
+		if ((fr_ipaddr_cmp(&old->ipaddr, &client->ipaddr) == 0) &&
+		    (old->ipaddr.prefix == client->ipaddr.prefix) &&
+		    namecmp(longname) && namecmp(secret) &&
+		    namecmp(shortname) && namecmp(nas_type) &&
+		    namecmp(login) && namecmp(password) && namecmp(server) &&
+#ifdef WITH_DYNAMIC_CLIENTS
+		    (old->lifetime == client->lifetime) &&
+		    namecmp(client_server) &&
+#endif
+#ifdef WITH_COA
+		    namecmp(coa_name) &&
+		    (old->coa_home_server == client->coa_home_server) &&
+		    (old->coa_home_pool == client->coa_home_pool) &&
+#endif
+		    (old->message_authenticator == client->message_authenticator)) {
+			WARN("Ignoring duplicate client %s", client->longname);
+			client_free(client);
+			return true;
+		}
+
+		ERROR("Failed to add duplicate client %s", client->shortname);
+		return false;
+	}
+#undef namecmp
+
+	/*
+	 *	Other error adding client: likely is fatal.
+	 */
+	if (!rbtree_insert(clients->trees[client->ipaddr.prefix], client)) {
+		return false;
+	}
+
+#ifdef WITH_STATS
+	if (!tree_num) {
+		tree_num = rbtree_create(clients, client_num_cmp, NULL, 0);
+	}
+
+#ifdef WITH_DYNAMIC_CLIENTS
+	/*
+	 *	More catching of clients added by rlm_sql.
+	 *
+	 *	The sql modules sets the dynamic flag BEFORE calling
+	 *	us.  The client_afrom_request() function sets it AFTER
+	 *	calling us.
+	 */
+	if (client->dynamic && (client->lifetime == 0)) {
+		RADCLIENT *network;
+
+		/*
+		 *	If there IS an enclosing network,
+		 *	inherit the lifetime from it.
+		 */
+		network = client_find(clients, &client->ipaddr, client->proto);
+		if (network) {
+			client->lifetime = network->lifetime;
+		}
+	}
+#endif
+
+	client->number = tree_num_max;
+	tree_num_max++;
+	if (tree_num) rbtree_insert(tree_num, client);
+#endif
+
+	if (client->ipaddr.prefix < clients->min_prefix) {
+		clients->min_prefix = client->ipaddr.prefix;
+	}
+
+	(void) talloc_steal(clients, client); /* reparent it */
+
+	return true;
+}
+
+
+#ifdef WITH_DYNAMIC_CLIENTS
+void client_delete(RADCLIENT_LIST *clients, RADCLIENT *client)
+{
+	if (!client) return;
+
+	if (!clients) clients = root_clients;
+
+	if (!client->dynamic) return;
+
+	rad_assert(client->ipaddr.prefix <= 128);
+
+#ifdef WITH_STATS
+	rbtree_deletebydata(tree_num, client);
+#endif
+	rbtree_deletebydata(clients->trees[client->ipaddr.prefix], client);
+}
+#endif
+
+#ifdef WITH_STATS
+/*
+ *	Find a client in the RADCLIENTS list by number.
+ *	This is a support function for the statistics code.
+ */
+RADCLIENT *client_findbynumber(RADCLIENT_LIST const *clients, int number)
+{
+	if (!clients) clients = root_clients;
+
+	if (!clients) return NULL;
+
+	if (number >= tree_num_max) return NULL;
+
+	if (tree_num) {
+		RADCLIENT myclient;
+
+		myclient.number = number;
+
+		return rbtree_finddata(tree_num, &myclient);
+	}
+
+	return NULL;
+}
+#else
+RADCLIENT *client_findbynumber(UNUSED const RADCLIENT_LIST *clients, UNUSED int number)
+{
+	return NULL;
+}
+#endif
+
+
+/*
+ *	Find a client in the RADCLIENTS list.
+ */
+RADCLIENT *client_find(RADCLIENT_LIST const *clients, fr_ipaddr_t const *ipaddr, int proto)
+{
+  int32_t i, max_prefix;
+	RADCLIENT myclient;
+
+	if (!clients) clients = root_clients;
+
+	if (!clients || !ipaddr) return NULL;
+
+	switch (ipaddr->af) {
+	case AF_INET:
+		max_prefix = 32;
+		break;
+
+	case AF_INET6:
+		max_prefix = 128;
+		break;
+
+	default :
+		return NULL;
+	}
+
+	for (i = max_prefix; i >= (int32_t) clients->min_prefix; i--) {
+		void *data;
+
+		myclient.ipaddr = *ipaddr;
+		myclient.proto = proto;
+		fr_ipaddr_mask(&myclient.ipaddr, i);
+
+		if (!clients->trees[i]) continue;
+
+		data = rbtree_finddata(clients->trees[i], &myclient);
+		if (data) return data;
+	}
+
+	return NULL;
+}
+
+/*
+ *	Old wrapper for client_find
+ */
+RADCLIENT *client_find_old(fr_ipaddr_t const *ipaddr)
+{
+	return client_find(root_clients, ipaddr, IPPROTO_UDP);
+}
+
+static fr_ipaddr_t cl_ipaddr;
+static uint32_t cl_netmask;
+static char const *cl_srcipaddr = NULL;
+static char const *hs_proto = NULL;
+
+#ifdef WITH_TCP
+static CONF_PARSER limit_config[] = {
+	{ "max_connections", FR_CONF_OFFSET(PW_TYPE_INTEGER, RADCLIENT, limit.max_connections),   "16" },
+
+	{ "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, RADCLIENT, limit.lifetime),   "0" },
+
+	{ "idle_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, RADCLIENT, limit.idle_timeout), "30" },
+
+	CONF_PARSER_TERMINATOR
+};
+#endif
+
+static const CONF_PARSER client_config[] = {
+	{ "ipaddr", FR_CONF_POINTER(PW_TYPE_COMBO_IP_PREFIX, &cl_ipaddr), NULL },
+	{ "ipv4addr", FR_CONF_POINTER(PW_TYPE_IPV4_PREFIX, &cl_ipaddr), NULL },
+	{ "ipv6addr", FR_CONF_POINTER(PW_TYPE_IPV6_PREFIX, &cl_ipaddr), NULL },
+
+	{ "netmask", FR_CONF_POINTER(PW_TYPE_INTEGER, &cl_netmask), NULL },
+
+	{ "src_ipaddr", FR_CONF_POINTER(PW_TYPE_STRING, &cl_srcipaddr), NULL },
+
+	{ "require_message_authenticator",  FR_CONF_OFFSET(PW_TYPE_BOOLEAN, RADCLIENT, message_authenticator), "no" },
+
+	{ "secret", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, RADCLIENT, secret), NULL },
+	{ "shortname", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, shortname), NULL },
+
+	{ "nas_type", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, nas_type), NULL },
+
+	{ "login", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, login), NULL },
+	{ "password", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, password), NULL },
+	{ "virtual_server", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, server), NULL },
+	{ "response_window", FR_CONF_OFFSET(PW_TYPE_TIMEVAL, RADCLIENT, response_window), NULL },
+
+#ifdef WITH_TCP
+	{ "proto", FR_CONF_POINTER(PW_TYPE_STRING, &hs_proto), NULL },
+	{ "limit", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) limit_config },
+#endif
+
+#ifdef WITH_DYNAMIC_CLIENTS
+	{ "dynamic_clients", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, client_server), NULL },
+	{ "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, RADCLIENT, lifetime), NULL },
+	{ "rate_limit", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, RADCLIENT, rate_limit), NULL },
+#endif
+
+#ifdef WITH_RADIUSV11
+	{ "radiusv1_1", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, radiusv11_name), NULL },
+#endif
+
+	CONF_PARSER_TERMINATOR
+};
+
+/** Create the linked list of clients from the new configuration type
+ *
+ */
+#ifdef WITH_TLS
+RADCLIENT_LIST *client_list_parse_section(CONF_SECTION *section, bool tls_required)
+#else
+RADCLIENT_LIST *client_list_parse_section(CONF_SECTION *section, UNUSED bool tls_required)
+#endif
+{
+	bool		global = false, in_server = false;
+	CONF_SECTION	*cs;
+	RADCLIENT	*c = NULL;
+	RADCLIENT_LIST	*clients = NULL;
+
+	/*
+	 *	Be forgiving.  If there's already a clients, return
+	 *	it.  Otherwise create a new one.
+	 */
+	clients = cf_data_find(section, "clients");
+	if (clients) {
+		/*
+		 *	Modules are initialized before the listeners.
+		 *	Which means that we MIGHT have read clients
+		 *	from SQL before parsing this "clients"
+		 *	section.  So there may already be a clients
+		 *	list.
+		 *
+		 *	But the list isn't _our_ list that we parsed,
+		 *	so we still need to parse the clients here.
+		 */
+		if (clients->parsed) return clients;		
+	} else {
+		clients = client_list_init(section);
+		if (!clients) return NULL;
+	}
+
+	if (cf_top_section(section) == section) {
+		global = true;
+		clients->name = "global";
+		clients->server = NULL;
+	}
+
+	if (strcmp("server", cf_section_name1(section)) == 0) {
+		clients->name = NULL;
+		clients->server = cf_section_name2(section);
+		in_server = true;
+	}
+
+	for (cs = cf_subsection_find_next(section, NULL, "client");
+	     cs;
+	     cs = cf_subsection_find_next(section, cs, "client")) {
+		c = client_afrom_cs(cs, cs, in_server, false);
+		if (!c) {
+		error:
+			client_free(c);
+			client_list_free(clients);
+			return NULL;
+		}
+
+#ifdef WITH_TLS
+		/*
+		 *	TLS clients CANNOT use non-TLS listeners.
+		 *	non-TLS clients CANNOT use TLS listeners.
+		 */
+		if (tls_required != c->tls_required) {
+			cf_log_err_cs(cs, "Client does not have the same TLS configuration as the listener");
+			goto error;
+		}
+#endif
+
+		/*
+		 *	FIXME: Add the client as data via cf_data_add,
+		 *	for migration issues.
+		 */
+
+#ifdef WITH_DYNAMIC_CLIENTS
+#ifdef HAVE_DIRENT_H
+		if (c->client_server) {
+			char const	*value;
+			CONF_PAIR	*cp;
+			DIR		*dir;
+			struct dirent	*dp;
+			struct stat	stat_buf;
+			char		buf2[2048];
+
+			/*
+			 *	Find the directory where individual
+			 *	client definitions are stored.
+			 */
+			cp = cf_pair_find(cs, "directory");
+			if (!cp) goto add_client;
+
+			value = cf_pair_value(cp);
+			if (!value) {
+				cf_log_err_cs(cs, "The \"directory\" entry must not be empty");
+				goto error;
+			}
+
+			DEBUG("including dynamic clients in %s", value);
+
+			dir = opendir(value);
+			if (!dir) {
+				cf_log_err_cs(cs, "Error reading directory %s: %s", value, fr_syserror(errno));
+				goto error;
+			}
+
+			/*
+			 *	Read the directory, ignoring "." files.
+			 */
+			while ((dp = readdir(dir)) != NULL) {
+				char const *p;
+				RADCLIENT *dc;
+
+				if (dp->d_name[0] == '.') continue;
+
+				/*
+				 *	Check for valid characters
+				 */
+				for (p = dp->d_name; *p != '\0'; p++) {
+					if (isalpha((uint8_t)*p) ||
+					    isdigit((uint8_t)*p) ||
+					    (*p == ':') ||
+					    (*p == '.')) continue;
+					break;
+				}
+				if (*p != '\0') continue;
+
+				snprintf(buf2, sizeof(buf2), "%s/%s", value, dp->d_name);
+
+				if ((stat(buf2, &stat_buf) != 0) || S_ISDIR(stat_buf.st_mode)) continue;
+
+				dc = client_read(buf2, in_server, true);
+				if (!dc) {
+					cf_log_err_cs(cs, "Failed reading client file \"%s\"", buf2);
+					closedir(dir);
+					goto error;
+				}
+
+				/*
+				 *	Validate, and add to the list.
+				 */
+				if (!client_add_dynamic(clients, c, dc)) {
+					closedir(dir);
+					goto error;
+				}
+			} /* loop over the directory */
+			closedir(dir);
+		}
+#endif /* HAVE_DIRENT_H */
+
+	add_client:
+#endif /* WITH_DYNAMIC_CLIENTS */
+		if (!client_add(clients, c)) {
+			cf_log_err_cs(cs, "Failed to add client %s", cf_section_name2(cs));
+			goto error;
+		}
+
+	}
+
+	/*
+	 *	Replace the global list of clients with the new one.
+	 *	The old one is still referenced from the original
+	 *	configuration, and will be freed when that is freed.
+	 */
+	if (global) root_clients = clients;
+
+	clients->parsed = true;
+	return clients;
+}
+
+#ifdef WITH_DYNAMIC_CLIENTS
+/*
+ *	We overload this structure a lot.
+ */
+static const CONF_PARSER dynamic_config[] = {
+	{ "FreeRADIUS-Client-IP-Address", FR_CONF_OFFSET(PW_TYPE_IPV4_ADDR, RADCLIENT, ipaddr), NULL },
+	{ "FreeRADIUS-Client-IPv6-Address", FR_CONF_OFFSET(PW_TYPE_IPV6_ADDR, RADCLIENT, ipaddr), NULL },
+	{ "FreeRADIUS-Client-IP-Prefix", FR_CONF_OFFSET(PW_TYPE_IPV4_PREFIX, RADCLIENT, ipaddr), NULL },
+	{ "FreeRADIUS-Client-IPv6-Prefix", FR_CONF_OFFSET(PW_TYPE_IPV6_PREFIX, RADCLIENT, ipaddr), NULL },
+	{ "FreeRADIUS-Client-Src-IP-Address", FR_CONF_OFFSET(PW_TYPE_IPV4_ADDR, RADCLIENT, src_ipaddr), NULL },
+	{ "FreeRADIUS-Client-Src-IPv6-Address", FR_CONF_OFFSET(PW_TYPE_IPV6_ADDR, RADCLIENT, src_ipaddr), NULL },
+
+	{ "FreeRADIUS-Client-Require-MA", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, RADCLIENT, message_authenticator), NULL },
+
+	{ "FreeRADIUS-Client-Secret",  FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, secret), "" },
+	{ "FreeRADIUS-Client-Shortname",  FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, shortname), "" },
+	{ "FreeRADIUS-Client-NAS-Type",  FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, nas_type), NULL },
+	{ "FreeRADIUS-Client-Virtual-Server",  FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, server), NULL },
+
+	CONF_PARSER_TERMINATOR
+};
+
+/** Add a dynamic client
+ *
+ */
+bool client_add_dynamic(RADCLIENT_LIST *clients, RADCLIENT *master, RADCLIENT *c)
+{
+	char buffer[128];
+
+	/*
+	 *	No virtual server defined.  Inherit the parent's
+	 *	definition.
+	 */
+	if (master->server && !c->server) {
+		c->server = talloc_typed_strdup(c, master->server);
+	}
+
+	/*
+	 *	If the client network isn't global (not tied to a
+	 *	virtual server), then ensure that this clients server
+	 *	is the same as the enclosing networks virtual server.
+	 */
+	if (master->server && (strcmp(master->server, c->server) != 0)) {
+		ERROR("Cannot add client %s/%i: Virtual server %s is not the same as the virtual server for the network",
+		      ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)), c->ipaddr.prefix, c->server);
+
+		goto error;
+	}
+
+	if (!client_add(clients, c)) {
+		ERROR("Cannot add client %s/%i: Internal error",
+		      ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)), c->ipaddr.prefix);
+
+		goto error;
+	}
+
+	/*
+	 *	Initialize the remaining fields.
+	 */
+	c->dynamic = true;
+	c->lifetime = master->lifetime;
+	c->created = time(NULL);
+	c->longname = talloc_typed_strdup(c, c->shortname);
+
+	if (rad_debug_lvl <= 2) {
+		INFO("Adding client %s/%i",
+		     ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)), c->ipaddr.prefix);
+	} else {
+		INFO("Adding client %s/%i with shared secret \"%s\"",
+		     ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)), c->ipaddr.prefix, c->secret);
+	}
+	return true;
+
+error:
+	client_free(c);
+	return false;
+}
+
+/** Create a client CONF_SECTION using a mapping section to map values from a result set to client attributes
+ *
+ * If we hit a CONF_SECTION we recurse and process its CONF_PAIRS too.
+ *
+ * @note Caller should free CONF_SECTION passed in as out, on error.
+ *	 Contents of that section will be in an undefined state.
+ *
+ * @param[in,out] out Section to perform mapping on. Either the root of the client config, or a parent section
+ *	(when this function is called recursively).
+ *	Should be alloced with cf_section_alloc, or if there's a separate template section, the
+ *	result of calling cf_section_dup on that section.
+ * @param[in] map section.
+ * @param[in] func to call to retrieve CONF_PAIR values. Must return a talloced buffer containing the value.
+ * @param[in] data to pass to func, usually a result pointer.
+ * @return 0 on success else -1 on error.
+ */
+int client_map_section(CONF_SECTION *out, CONF_SECTION const *map, client_value_cb_t func, void *data)
+{
+	CONF_ITEM const *ci;
+
+	for (ci = cf_item_find_next(map, NULL);
+	     ci != NULL;
+	     ci = cf_item_find_next(map, ci)) {
+	     	CONF_PAIR const *cp;
+	     	CONF_PAIR *old;
+	     	char *value;
+		char const *attr;
+
+		/*
+		 *	Recursively process map subsection
+		 */
+		if (cf_item_is_section(ci)) {
+			CONF_SECTION *cs, *cc;
+
+			cs = cf_item_to_section(ci);
+			/*
+			 *	Use pre-existing section or alloc a new one
+			 */
+			cc = cf_section_sub_find_name2(out, cf_section_name1(cs), cf_section_name2(cs));
+			if (!cc) {
+				cc = cf_section_alloc(out, cf_section_name1(cs), cf_section_name2(cs));
+				cf_section_add(out, cc);
+				if (!cc) return -1;
+			}
+
+			if (client_map_section(cc, cs, func, data) < 0) return -1;
+			continue;
+		}
+
+		cp = cf_item_to_pair(ci);
+		attr = cf_pair_attr(cp);
+
+		/*
+		 *	The callback can return 0 (success) and not provide a value
+		 *	in which case we skip the mapping pair.
+		 *
+		 *	Or return -1 in which case we error out.
+		 */
+		if (func(&value, cp, data) < 0) {
+			cf_log_err_cs(out, "Failed performing mapping \"%s\" = \"%s\"", attr, cf_pair_value(cp));
+			return -1;
+		}
+		if (!value) continue;
+
+		/*
+		 *	Replace an existing CONF_PAIR
+		 */
+		old = cf_pair_find(out, attr);
+		if (old) {
+			cf_pair_replace(out, old, value);
+			talloc_free(value);
+			continue;
+		}
+
+		/*
+		 *	...or add a new CONF_PAIR
+		 */
+		cp = cf_pair_alloc(out, attr, value, T_OP_SET, T_BARE_WORD, T_SINGLE_QUOTED_STRING);
+		if (!cp) {
+			cf_log_err_cs(out, "Failed allocing pair \"%s\" = \"%s\"", attr, value);
+			talloc_free(value);
+			return -1;
+		}
+		talloc_free(value);
+		cf_item_add(out, cf_pair_to_item(cp));
+	}
+
+	return 0;
+}
+
+/** Allocate a new client from a config section
+ *
+ * @param ctx to allocate new clients in.
+ * @param cs to process as a client.
+ * @param in_server Whether the client should belong to a specific virtual server.
+ * @param with_coa If true and coa_home_server or coa_home_pool aren't specified automatically,
+ *	create a coa home_server section and add it to the client CONF_SECTION.
+ * @return new RADCLIENT struct.
+ */
+RADCLIENT *client_afrom_cs(TALLOC_CTX *ctx, CONF_SECTION *cs, bool in_server, bool with_coa)
+{
+	RADCLIENT	*c;
+	char const	*name2;
+
+	name2 = cf_section_name2(cs);
+	if (!name2) {
+		cf_log_err_cs(cs, "Missing client name");
+		return NULL;
+	}
+
+	/*
+	 *	The size is fine.. Let's create the buffer
+	 */
+	c = talloc_zero(ctx, RADCLIENT);
+	c->cs = cs;
+
+	memset(&cl_ipaddr, 0, sizeof(cl_ipaddr));
+	cl_netmask = 255;
+
+	if (cf_section_parse(cs, c, client_config) < 0) {
+		cf_log_err_cs(cs, "Error parsing client section");
+	error:
+		client_free(c);
+#ifdef WITH_TCP
+		hs_proto = NULL;
+		cl_srcipaddr = NULL;
+#endif
+
+		return NULL;
+	}
+
+	/*
+	 *	Global clients can set servers to use, per-server clients cannot.
+	 */
+	if (in_server && c->server) {
+		cf_log_err_cs(cs, "Clients inside of an server section cannot point to a server");
+		goto error;
+	}
+
+	/*
+	 *	Allow the old method to specify "netmask".  Just using "1.2.3.4" means it's a /32.
+	 */
+	if (cl_netmask != 255) {
+		if ((cl_ipaddr.prefix != cl_netmask) &&
+		    (((cl_ipaddr.af == AF_INET) && cl_ipaddr.prefix != 32) ||
+		     ((cl_ipaddr.af == AF_INET6) && cl_ipaddr.prefix != 128))) {
+			cf_log_err_cs(cs, "Clients cannot use 'ipaddr/mask' and 'netmask' at the same time.");
+			goto error;
+		}
+
+		cl_ipaddr.prefix = cl_netmask;
+	}
+
+	/*
+	 *	Newer style client definitions with either ipaddr or ipaddr6
+	 *	config items.
+	 */
+	if (cf_pair_find(cs, "ipaddr") || cf_pair_find(cs, "ipv4addr") || cf_pair_find(cs, "ipv6addr")) {
+		char buffer[128];
+
+		/*
+		 *	Sets ipv4/ipv6 address and prefix.
+		 */
+		c->ipaddr = cl_ipaddr;
+
+		/*
+		 *	Set the long name to be the result of a reverse lookup on the IP address.
+		 */
+		ip_ntoh(&c->ipaddr, buffer, sizeof(buffer));
+		c->longname = talloc_typed_strdup(c, buffer);
+
+		/*
+		 *	Set the short name to the name2.
+		 */
+		if (!c->shortname) c->shortname = talloc_typed_strdup(c, name2);
+	/*
+	 *	No "ipaddr" or "ipv6addr", use old-style "client <ipaddr> {" syntax.
+	 */
+	} else {
+		WARN("No 'ipaddr' or 'ipv4addr' or 'ipv6addr' field found in client %s. "
+		     "Please fix your configuration", name2);
+		WARN("Support for old-style clients will be removed in a future release");
+
+#ifdef WITH_TCP
+		if (cf_pair_find(cs, "proto") != NULL) {
+			cf_log_err_cs(cs, "Cannot use 'proto' inside of old-style client definition");
+			goto error;
+		}
+#endif
+		if (fr_pton(&c->ipaddr, name2, -1, AF_UNSPEC, true) < 0) {
+			cf_log_err_cs(cs, "Failed parsing client name \"%s\" as ip address or hostname: %s", name2,
+				      fr_strerror());
+			goto error;
+		}
+
+		c->longname = talloc_typed_strdup(c, name2);
+		if (!c->shortname) c->shortname = talloc_typed_strdup(c, c->longname);
+	}
+
+	c->proto = IPPROTO_UDP;
+	if (hs_proto) {
+		if (strcmp(hs_proto, "udp") == 0) {
+			hs_proto = NULL;
+
+#ifdef WITH_TCP
+		} else if (strcmp(hs_proto, "tcp") == 0) {
+			hs_proto = NULL;
+			c->proto = IPPROTO_TCP;
+#  ifdef WITH_TLS
+		} else if (strcmp(hs_proto, "tls") == 0) {
+			hs_proto = NULL;
+			c->proto = IPPROTO_TCP;
+			c->tls_required = true;
+
+		} else if (strcmp(hs_proto, "radsec") == 0) {
+			hs_proto = NULL;
+			c->proto = IPPROTO_TCP;
+			c->tls_required = true;
+#  endif
+		} else if (strcmp(hs_proto, "*") == 0) {
+			hs_proto = NULL;
+			c->proto = IPPROTO_IP; /* fake for dual */
+#endif
+		} else {
+			cf_log_err_cs(cs, "Unknown proto \"%s\".", hs_proto);
+			goto error;
+		}
+	}
+
+	/*
+	 *	If a src_ipaddr is specified, when we send the return packet
+	 *	we will use this address instead of the src from the
+	 *	request.
+	 */
+	if (cl_srcipaddr) {
+#ifdef WITH_UDPFROMTO
+		switch (c->ipaddr.af) {
+		case AF_INET:
+			if (fr_pton4(&c->src_ipaddr, cl_srcipaddr, -1, true, false) < 0) {
+				cf_log_err_cs(cs, "Failed parsing src_ipaddr: %s", fr_strerror());
+				goto error;
+			}
+			break;
+
+		case AF_INET6:
+			if (fr_pton6(&c->src_ipaddr, cl_srcipaddr, -1, true, false) < 0) {
+				cf_log_err_cs(cs, "Failed parsing src_ipaddr: %s", fr_strerror());
+				goto error;
+			}
+			break;
+		default:
+			rad_assert(0);
+		}
+#else
+		WARN("Server not built with udpfromto, ignoring client src_ipaddr");
+#endif
+		cl_srcipaddr = NULL;
+	}
+
+#ifdef WITH_RADIUSV11
+	if (c->tls_required && c->radiusv11_name) {
+		int rcode;
+
+		rcode = fr_str2int(radiusv11_types, c->radiusv11_name, -1);
+		if (rcode < 0) {
+			cf_log_err_cs(cs, "Invalid value for 'radiusv11'");
+			goto error;
+		}
+
+		c->radiusv11 = rcode;
+	}
+#endif
+
+	/*
+	 *	A response_window of zero is OK, and means that it's
+	 *	ignored by the rest of the server timers.
+	 */
+	if (timerisset(&c->response_window)) {
+		FR_TIMEVAL_BOUND_CHECK("response_window", &c->response_window, >=, 0, 1000);
+		FR_TIMEVAL_BOUND_CHECK("response_window", &c->response_window, <=, 60, 0);
+		FR_TIMEVAL_BOUND_CHECK("response_window", &c->response_window, <=, main_config.max_request_time, 0);
+	}
+
+#ifdef WITH_DYNAMIC_CLIENTS
+	if (c->client_server) {
+		c->secret = talloc_typed_strdup(c, "testing123");
+
+		if (((c->ipaddr.af == AF_INET) && (c->ipaddr.prefix == 32)) ||
+		    ((c->ipaddr.af == AF_INET6) && (c->ipaddr.prefix == 128))) {
+			cf_log_err_cs(cs, "Dynamic clients MUST be a network, not a single IP address");
+			goto error;
+		}
+
+		return c;
+	}
+#endif
+
+	if (!c->secret || (c->secret[0] == '\0')) {
+#ifdef WITH_DHCP
+		char const *value = NULL;
+		CONF_PAIR *cp = cf_pair_find(cs, "dhcp");
+
+		if (cp) value = cf_pair_value(cp);
+
+		/*
+		 *	Secrets aren't needed for DHCP.
+		 */
+		if (value && (strcmp(value, "yes") == 0)) return c;
+#endif
+
+#ifdef WITH_TLS
+		/*
+		 *	If the client is TLS only, the secret can be
+		 *	omitted.  When omitted, it's hard-coded to
+		 *	"radsec".  See RFC 6614.
+		 */
+		if (c->tls_required) {
+			c->secret = talloc_typed_strdup(cs, "radsec");
+		} else
+#endif
+
+		{
+			cf_log_err_cs(cs, "secret must be at least 1 character long");
+			goto error;
+		}
+	}
+
+#ifdef WITH_COA
+	{
+		CONF_PAIR *cp;
+
+		/*
+		 *	Point the client to the home server pool, OR to the
+		 *	home server.  This gets around the problem of figuring
+		 *	out which port to use.
+		 */
+		cp = cf_pair_find(cs, "coa_server");
+		if (cp) {
+			c->coa_name = cf_pair_value(cp);
+			c->coa_home_pool = home_pool_byname(c->coa_name, HOME_TYPE_COA);
+			if (!c->coa_home_pool) {
+				c->coa_home_server = home_server_byname(c->coa_name, HOME_TYPE_COA);
+			}
+			if (!c->coa_home_pool && !c->coa_home_server) {
+				cf_log_err_cs(cs, "No such home_server or home_server_pool \"%s\"", c->coa_name);
+				goto error;
+			}
+		/*
+		 *	If we're implicitly adding a CoA home server for
+		 *	every client, or there's a server subsection,
+		 *	create a home server CONF_SECTION and then parse
+		 *	it into a home_server_t.
+		 */
+		} else if (with_coa || cf_section_sub_find(cs, "coa_server")) {
+			CONF_SECTION *server;
+			home_server_t *home;
+
+			if (((c->ipaddr.af == AF_INET) && (c->ipaddr.prefix != 32)) ||
+			    ((c->ipaddr.af == AF_INET6) && (c->ipaddr.prefix != 128))) {
+			 	WARN("Subnets not supported for home servers.  "
+			 	     "Not adding client %s as home_server", name2);
+				goto done_coa;
+			}
+
+			server = home_server_cs_afrom_client(cs);
+			if (!server) goto error;
+
+			/*
+			 *	Must be allocated in the context of the client,
+			 *	as allocating using the context of the
+			 *	realm_config_t without a mutex, by one of the
+			 *	workers, would be bad.
+			 */
+			home = home_server_afrom_cs(NULL, NULL, server);
+			if (!home) {
+				talloc_free(server);
+				goto error;
+			}
+
+			rad_assert(home->type == HOME_TYPE_COA);
+
+			c->coa_home_server = home;
+			c->defines_coa_server = true;
+		}
+	}
+done_coa:
+#endif
+
+#ifdef WITH_TCP
+	if ((c->proto == IPPROTO_TCP) || (c->proto == IPPROTO_IP)) {
+		if ((c->limit.idle_timeout > 0) && (c->limit.idle_timeout < 5))
+			c->limit.idle_timeout = 5;
+		if ((c->limit.lifetime > 0) && (c->limit.lifetime < 5))
+			c->limit.lifetime = 5;
+		if ((c->limit.lifetime > 0) && (c->limit.idle_timeout > c->limit.lifetime))
+			c->limit.idle_timeout = 0;
+	}
+#endif
+
+	return c;
+}
+
+/** Add a client from a result set (SQL)
+ *
+ * @todo This function should die. SQL should use client_afrom_cs.
+ *
+ * @param ctx Talloc context.
+ * @param identifier Client IP Address / IPv4 subnet / IPv6 subnet / FQDN.
+ * @param secret Client secret.
+ * @param shortname Client friendly name.
+ * @param type NAS-Type.
+ * @param server Virtual-Server to associate clients with.
+ * @param require_ma If true all packets from client must include a message-authenticator.
+ * @return The new client, or NULL on error.
+ */
+RADCLIENT *client_afrom_query(TALLOC_CTX *ctx, char const *identifier, char const *secret,
+			      char const *shortname, char const *type, char const *server, bool require_ma)
+{
+	RADCLIENT *c;
+	char buffer[128];
+
+	c = talloc_zero(ctx, RADCLIENT);
+
+	if (fr_pton(&c->ipaddr, identifier, -1, AF_UNSPEC, true) < 0) {
+		ERROR("%s", fr_strerror());
+		talloc_free(c);
+
+		return NULL;
+	}
+
+#ifdef WITH_DYNAMIC_CLIENTS
+	c->dynamic = true;
+#endif
+	ip_ntoh(&c->ipaddr, buffer, sizeof(buffer));
+	c->longname = talloc_typed_strdup(c, buffer);
+
+	/*
+	 *	Other values (secret, shortname, nas_type, virtual_server)
+	 */
+	c->secret = talloc_typed_strdup(c, secret);
+	if (shortname) c->shortname = talloc_typed_strdup(c, shortname);
+	if (type) c->nas_type = talloc_typed_strdup(c, type);
+	if (server) c->server = talloc_typed_strdup(c, server);
+	c->message_authenticator = require_ma;
+
+	return c;
+}
+
+/** Create a new client, consuming all attributes in the control list of the request
+ *
+ * @param clients list to add new client to.
+ * @param request Fake request.
+ * @return a new client on success, else NULL on error.
+ */
+RADCLIENT *client_afrom_request(RADCLIENT_LIST *clients, REQUEST *request)
+{
+	static int	cnt;
+	int		i, *pi;
+	char		**p;
+	RADCLIENT	*c;
+	char		buffer[128];
+
+	vp_cursor_t	cursor;
+	VALUE_PAIR	*vp = NULL;
+
+	if (!clients || !request) return NULL;
+
+	/*
+	 *	Hack for the "dynamic_clients" module.
+	 */
+	if (request->client->dynamic) {
+		c = request->client;
+		goto validate;
+	}
+
+	snprintf(buffer, sizeof(buffer), "dynamic%i", cnt++);
+
+	c = talloc_zero(clients, RADCLIENT);
+	c->cs = cf_section_alloc(NULL, "client", buffer);
+	talloc_steal(c, c->cs);
+	c->ipaddr.af = AF_UNSPEC;
+	c->src_ipaddr.af = AF_UNSPEC;
+
+	fr_cursor_init(&cursor, &request->config);
+
+	RDEBUG2("Converting control list to client fields");
+	RINDENT();
+	for (i = 0; dynamic_config[i].name != NULL; i++) {
+		DICT_ATTR const *da;
+		char *strvalue = NULL;
+		CONF_PAIR *cp = NULL;
+
+		da = dict_attrbyname(dynamic_config[i].name);
+		if (!da) {
+			RERROR("Cannot add client %s: attribute \"%s\" is not in the dictionary",
+			       ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)),
+			       dynamic_config[i].name);
+		error:
+			REXDENT();
+			talloc_free(vp);
+			client_free(c);
+			return NULL;
+		}
+
+		fr_cursor_first(&cursor);
+		if (!fr_cursor_next_by_da(&cursor, da, TAG_ANY)) {
+			/*
+			 *	Not required.  Skip it.
+			 */
+			if (!dynamic_config[i].dflt) continue;
+
+			RERROR("Cannot add client %s: Required attribute \"%s\" is missing",
+			       ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)),
+			       dynamic_config[i].name);
+			goto error;
+		}
+		vp = fr_cursor_remove(&cursor);
+
+		/*
+		 *	Freed at the same time as the vp.
+		 */
+		strvalue = vp_aprints_value(vp, vp, '\'');
+
+		switch (dynamic_config[i].type) {
+		case PW_TYPE_IPV4_ADDR:
+			if (da->attr == PW_FREERADIUS_CLIENT_IP_ADDRESS) {
+				c->ipaddr.af = AF_INET;
+				c->ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+				c->ipaddr.prefix = 32;
+				cp = cf_pair_alloc(c->cs, "ipv4addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+			} else if (da->attr == PW_FREERADIUS_CLIENT_SRC_IP_ADDRESS) {
+#ifdef WITH_UDPFROMTO
+				RDEBUG2("src_ipaddr = %s", strvalue);
+				c->src_ipaddr.af = AF_INET;
+				c->src_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+				c->src_ipaddr.prefix = 32;
+				cp = cf_pair_alloc(c->cs, "src_ipaddr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+#else
+				RWARN("Server not built with udpfromto, ignoring FreeRADIUS-Client-Src-IP-Address");
+#endif
+			}
+
+			break;
+
+		case PW_TYPE_IPV6_ADDR:
+			if (da->attr == PW_FREERADIUS_CLIENT_IPV6_ADDRESS) {
+				c->ipaddr.af = AF_INET6;
+				c->ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr;
+				c->ipaddr.prefix = 128;
+				cp = cf_pair_alloc(c->cs, "ipv6addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+			} else if (da->attr == PW_FREERADIUS_CLIENT_SRC_IPV6_ADDRESS) {
+#ifdef WITH_UDPFROMTO
+				c->src_ipaddr.af = AF_INET6;
+				c->src_ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr;
+				c->src_ipaddr.prefix = 128;
+				cp = cf_pair_alloc(c->cs, "src_addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+#else
+				RWARN("Server not built with udpfromto, ignoring FreeRADIUS-Client-Src-IPv6-Address");
+#endif
+			}
+
+			break;
+
+		case PW_TYPE_IPV4_PREFIX:
+			if (da->attr == PW_FREERADIUS_CLIENT_IP_PREFIX) {
+				c->ipaddr.af = AF_INET;
+				memcpy(&c->ipaddr.ipaddr.ip4addr, &vp->vp_ipv4prefix[2],
+				       sizeof(c->ipaddr.ipaddr.ip4addr.s_addr));
+				fr_ipaddr_mask(&c->ipaddr, (vp->vp_ipv4prefix[1] & 0x3f));
+				cp = cf_pair_alloc(c->cs, "ipv4addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+			}
+
+			break;
+
+		case PW_TYPE_IPV6_PREFIX:
+			if (da->attr == PW_FREERADIUS_CLIENT_IPV6_PREFIX) {
+				c->ipaddr.af = AF_INET6;
+				memcpy(&c->ipaddr.ipaddr.ip6addr, &vp->vp_ipv6prefix[2],
+				       sizeof(c->ipaddr.ipaddr.ip6addr));
+				fr_ipaddr_mask(&c->ipaddr, vp->vp_ipv6prefix[1]);
+				cp = cf_pair_alloc(c->cs, "ipv6addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+			}
+
+			break;
+
+		case PW_TYPE_STRING:
+		{
+			CONF_PARSER const *parse;
+
+			/*
+			 *	Cache pointer to CONF_PAIR buffer in RADCLIENT struct
+			 */
+			p = (char **) ((char *) c + dynamic_config[i].offset);
+			if (*p) TALLOC_FREE(*p);
+			if (!vp->vp_strvalue[0]) break;
+
+			/*
+			 *	We could reuse the CONF_PAIR buff, this just keeps things
+			 *	consistent between client_afrom_cs, and client_afrom_query.
+			 */
+			*p = talloc_strdup(c, vp->vp_strvalue);
+
+			/*
+			 *	This is fairly nasty... In order to figure out the CONF_PAIR
+			 *	name associated with a field, find offsets that match between
+			 *	the dynamic_config CONF_PARSER table, and the client_config
+			 *	CONF_PARSER table.
+			 *
+			 *	This is so that things that expect to find CONF_PAIRs in the
+			 *	client CONF_SECTION for fields like 'nas_type' can.
+			 */
+			for (parse = client_config; parse->name; parse++) {
+				if (parse->offset == dynamic_config[i].offset) break;
+			}
+
+			if (!parse) break;
+
+			cp = cf_pair_alloc(c->cs, parse->name, strvalue, T_OP_SET, T_BARE_WORD, T_SINGLE_QUOTED_STRING);
+		}
+			break;
+
+		case PW_TYPE_BOOLEAN:
+		{
+			CONF_PARSER const *parse;
+
+			pi = (int *) ((bool *) ((char *) c + dynamic_config[i].offset));
+			*pi = vp->vp_integer;
+
+			/*
+			 *	Same nastiness as above.
+			 */
+			for (parse = client_config; parse->name; parse++) {
+				if (parse->offset == dynamic_config[i].offset) break;
+			}
+			if (!parse) break;
+
+			cp = cf_pair_alloc(c->cs, parse->name, strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+		}
+			break;
+
+		default:
+			goto error;
+		}
+
+		if (!cp) {
+			RERROR("Error creating equivalent conf pair for %s", vp->da->name);
+			goto error;
+		}
+
+		if (cf_pair_attr_type(cp) == T_SINGLE_QUOTED_STRING) {
+			RDEBUG2("%s = '%s'", cf_pair_attr(cp), cf_pair_value(cp));
+		} else {
+			RDEBUG2("%s = %s", cf_pair_attr(cp), cf_pair_value(cp));
+		}
+		cf_pair_add(c->cs, cp);
+
+		talloc_free(vp);
+	}
+
+	fr_cursor_first(&cursor);
+	vp = fr_cursor_remove(&cursor);
+	if (vp) {
+		CONF_PAIR *cp;
+
+		do {
+			char *value;
+
+			value = vp_aprints_value(vp, vp, '\'');
+			if (!value) {
+				ERROR("Failed stringifying value of &control:%s", vp->da->name);
+				goto error;
+			}
+
+			if (vp->da->type == PW_TYPE_STRING) {
+				RDEBUG2("%s = '%s'", vp->da->name, value);
+				cp = cf_pair_alloc(c->cs, vp->da->name, value, T_OP_SET,
+						   T_BARE_WORD, T_SINGLE_QUOTED_STRING);
+			} else {
+				RDEBUG2("%s = %s", vp->da->name, value);
+				cp = cf_pair_alloc(c->cs, vp->da->name, value, T_OP_SET,
+						   T_BARE_WORD, T_BARE_WORD);
+			}
+			cf_pair_add(c->cs, cp);
+
+			talloc_free(vp);
+		} while ((vp = fr_cursor_remove(&cursor)));
+	}
+	REXDENT();
+
+validate:
+	if (c->ipaddr.af == AF_UNSPEC) {
+		RERROR("Cannot add client %s: No IP address was specified.",
+		       ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)));
+
+		goto error;
+	}
+
+	{
+		fr_ipaddr_t addr;
+
+		/*
+		 *	Need to apply the same mask as we set for the client
+		 *	else clients created with FreeRADIUS-Client-IPv6-Prefix
+		 *	or FreeRADIUS-Client-IPv4-Prefix will fail this check.
+		 */
+		addr = request->packet->src_ipaddr;
+		fr_ipaddr_mask(&addr, c->ipaddr.prefix);
+		if (fr_ipaddr_cmp(&addr, &c->ipaddr) != 0) {
+			char buf2[128];
+
+			RERROR("Cannot add client %s: Not in specified subnet %s/%i",
+			       ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)),
+			       ip_ntoh(&c->ipaddr, buf2, sizeof(buf2)), c->ipaddr.prefix);
+			goto error;
+		}
+	}
+
+	if (!c->secret || !*c->secret) {
+		RERROR("Cannot add client %s: No secret was specified",
+		       ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)));
+		goto error;
+	}
+
+	if (!client_add_dynamic(clients, request->client, c)) {
+		return NULL;
+	}
+
+	if ((c->src_ipaddr.af != AF_UNSPEC) && (c->src_ipaddr.af != c->ipaddr.af)) {
+		RERROR("Cannot add client %s: Client IP and src address are different IP version",
+		       ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)));
+
+		goto error;
+	}
+
+	return c;
+}
+
+/*
+ *	Read a client definition from the given filename.
+ */
+RADCLIENT *client_read(char const *filename, int in_server, int flag)
+{
+	char const *p;
+	RADCLIENT *c;
+	CONF_SECTION *cs;
+	char buffer[256];
+
+	if (!filename) return NULL;
+
+	cs = cf_section_alloc(NULL, "main", NULL);
+	if (!cs) return NULL;
+
+	if (cf_file_read(cs, filename) < 0) {
+		talloc_free(cs);
+		return NULL;
+	}
+
+	cs = cf_section_sub_find(cs, "client");
+	if (!cs) {
+		ERROR("No \"client\" section found in client file");
+		return NULL;
+	}
+
+	c = client_afrom_cs(cs, cs, in_server, false);
+	if (!c) return NULL;
+
+	p = strrchr(filename, FR_DIR_SEP);
+	if (p) {
+		p++;
+	} else {
+		p = filename;
+	}
+
+	if (!flag) return c;
+
+	/*
+	 *	Additional validations
+	 */
+	ip_ntoh(&c->ipaddr, buffer, sizeof(buffer));
+	if (strcmp(p, buffer) != 0) {
+		ERROR("Invalid client definition in %s: IP address %s does not match name %s", filename, buffer, p);
+		client_free(c);
+		return NULL;
+	}
+
+	return c;
+}
+#endif
+
diff --git a/src/main/collectd.c b/src/main/collectd.c
new file mode 100644
index 0000000..b720471
--- /dev/null
+++ b/src/main/collectd.c
@@ -0,0 +1,382 @@
+/*
+ *   This program is is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or (at
+ *   your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ * @file collectd.c
+ * @brief Helper functions to enabled radsniff to talk to collectd
+ *
+ * @copyright 2013 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
+ */
+#include <assert.h>
+#include <ctype.h>
+
+#ifdef HAVE_COLLECTDC_H
+#include <collectd/client.h>
+#include <freeradius-devel/radsniff.h>
+
+/** Copy a 64bit unsigned integer into a double
+ *
+ */
+/*
+static void _copy_uint64_to_double(UNUSED rs_t *conf, rs_stats_value_tmpl_t *tmpl)
+{
+	assert(tmpl->src);
+	assert(tmpl->dst);
+
+	*((double *) tmpl->dst) = *((uint64_t *) tmpl->src);
+}
+*/
+
+/*
+static void _copy_uint64_to_uint64(UNUSED rs_t *conf, rs_stats_value_tmpl_t *tmpl)
+{
+	assert(tmpl->src);
+	assert(tmpl->dst);
+
+	*((uint64_t *) tmpl->dst) = *((uint64_t *) tmpl->src);
+}
+*/
+
+static void _copy_double_to_double(UNUSED rs_t *conf, rs_stats_value_tmpl_t *tmpl)
+{
+	assert(tmpl->src);
+	assert(tmpl->dst);
+
+	*((double *) tmpl->dst) = *((double*) tmpl->src);
+}
+
+
+/** Allocates a stats template which describes a single guage/counter
+ *
+ * This is just intended to simplify allocating a fairly complex memory structure
+ * src and dst pointers must be set
+ *
+ * @param ctx Context to allocate collectd struct in.
+ * @param conf Radsniff configuration.
+ * @param plugin_instance usually the type of packet (in our case).
+ * @param type string, the name of a collection of stats e.g. exchange
+ * @param type_instance the name of the counter/guage within the collection e.g. latency.
+ * @param stats structure to derive statistics from.
+ * @param values Value templates used to populate lcc_value_list.
+ * @return a new rs_stats_tmpl_t on success or NULL on failure.
+ */
+static rs_stats_tmpl_t *rs_stats_collectd_init(TALLOC_CTX *ctx, rs_t *conf,
+					       char const *plugin_instance,
+					       char const *type, char const *type_instance,
+					       void *stats,
+					       rs_stats_value_tmpl_t const *values)
+{
+	static char hostname[255];
+	static char fqdn[LCC_NAME_LEN];
+
+	size_t len;
+	int i;
+	char *p;
+
+	rs_stats_tmpl_t *tmpl;
+	lcc_value_list_t *value;
+
+	assert(conf);
+	assert(type);
+	assert(type_instance);
+
+	for (len = 0; values[len].src; len++) {} ;
+	assert(len > 0);
+
+	/*
+	 *	Initialise hostname once so we don't call gethostname every time
+	 */
+	if (*fqdn == '\0') {
+		int ret;
+		struct addrinfo hints, *info = NULL;
+
+		if (gethostname(hostname, sizeof(hostname)) < 0) {
+			ERROR("Error getting hostname: %s", fr_syserror(errno));
+
+			return NULL;
+		}
+
+		memset(&hints, 0, sizeof hints);
+		hints.ai_family = AF_UNSPEC; /*either IPV4 or IPV6*/
+		hints.ai_socktype = SOCK_STREAM;
+		hints.ai_flags = AI_CANONNAME;
+
+		if ((ret = getaddrinfo(hostname, "radius", &hints, &info)) != 0) {
+			ERROR("Error getting hostname: %s", gai_strerror(ret));
+			return NULL;
+		}
+
+		strlcpy(fqdn, info->ai_canonname, sizeof(fqdn));
+
+		freeaddrinfo(info);
+	}
+
+	tmpl = talloc_zero(ctx, rs_stats_tmpl_t);
+	if (!tmpl) {
+		return NULL;
+	}
+
+	tmpl->value_tmpl = talloc_zero_array(tmpl, rs_stats_value_tmpl_t, len);
+	if (!tmpl->value_tmpl) {
+		goto error;
+	}
+
+	tmpl->stats = stats;
+
+	value = talloc_zero(tmpl, lcc_value_list_t);
+	if (!value) {
+		goto error;
+	}
+	tmpl->value = value;
+
+	value->interval = conf->stats.interval;
+	value->values_len = len;
+
+	value->values_types = talloc_zero_array(value, int, len);
+	if (!value->values_types) {
+		goto error;
+	}
+
+	value->values = talloc_zero_array(value, value_t, len);
+	if (!value->values) {
+		goto error;
+	}
+
+	for (i = 0; i < (int) len; i++) {
+		assert(values[i].src);
+		assert(values[i].cb);
+
+		tmpl->value_tmpl[i] = values[i];
+		switch (tmpl->value_tmpl[i].type) {
+		case LCC_TYPE_COUNTER:
+			tmpl->value_tmpl[i].dst = &value->values[i].counter;
+			break;
+
+		case LCC_TYPE_GAUGE:
+			tmpl->value_tmpl[i].dst = &value->values[i].gauge;
+			break;
+
+		case LCC_TYPE_DERIVE:
+			tmpl->value_tmpl[i].dst = &value->values[i].derive;
+			break;
+
+		case LCC_TYPE_ABSOLUTE:
+			tmpl->value_tmpl[i].dst = &value->values[i].absolute;
+			break;
+
+		default:
+			assert(0);
+		}
+		value->values_types[i] = tmpl->value_tmpl[i].type;
+	}
+
+	/*
+	 *	These should be OK as is
+	 */
+	strlcpy(value->identifier.host, fqdn, sizeof(value->identifier.host));
+
+	/*
+	 *	Plugin is ASCII only and no '/'
+	 */
+	fr_prints(value->identifier.plugin, sizeof(value->identifier.plugin),
+		  conf->stats.prefix, strlen(conf->stats.prefix), '\0');
+	for (p = value->identifier.plugin; *p; ++p) {
+		if ((*p == '-') || (*p == '/'))*p = '_';
+	}
+
+	/*
+	 *	Plugin instance is ASCII only (assuming printable only) and no '/'
+	 */
+	fr_prints(value->identifier.plugin_instance, sizeof(value->identifier.plugin_instance),
+		  plugin_instance, strlen(plugin_instance), '\0');
+	for (p = value->identifier.plugin_instance; *p; ++p) {
+		if ((*p == '-') || (*p == '/')) *p = '_';
+	}
+
+	/*
+	 *	Type is ASCII only (assuming printable only) and no '/' or '-'
+	 */
+	fr_prints(value->identifier.type, sizeof(value->identifier.type),
+		  type, strlen(type), '\0');
+	for (p = value->identifier.type; *p; ++p) {
+		if ((*p == '-') || (*p == '/')) *p = '_';
+	}
+
+	fr_prints(value->identifier.type_instance, sizeof(value->identifier.type_instance),
+		  type_instance, strlen(type_instance), '\0');
+	for (p = value->identifier.type_instance; *p; ++p) {
+		if ((*p == '-') || (*p == '/')) *p = '_';
+	}
+
+
+	return tmpl;
+
+error:
+	talloc_free(tmpl);
+	return NULL;
+}
+
+
+/** Setup stats templates for latency
+ *
+ */
+rs_stats_tmpl_t *rs_stats_collectd_init_latency(TALLOC_CTX *ctx, rs_stats_tmpl_t **out, rs_t *conf,
+						char const *type, rs_latency_t *stats, PW_CODE code)
+{
+	rs_stats_tmpl_t **tmpl, *last;
+	char *p;
+	char buffer[LCC_NAME_LEN];
+	tmpl = out;
+
+	rs_stats_value_tmpl_t rtx[(RS_RETRANSMIT_MAX + 1) + 1 + 1];	// RTX bins + 0 bin + lost + NULL
+	int i;
+
+	/* not static so were thread safe */
+	rs_stats_value_tmpl_t const _packet_count[] = {
+		{ &stats->interval.received, LCC_TYPE_GAUGE,  _copy_double_to_double, NULL },
+		{ &stats->interval.linked, LCC_TYPE_GAUGE,  _copy_double_to_double, NULL },
+		{ &stats->interval.unlinked, LCC_TYPE_GAUGE,  _copy_double_to_double, NULL },
+		{ &stats->interval.reused, LCC_TYPE_GAUGE,  _copy_double_to_double, NULL },
+		{ NULL, 0, NULL, NULL }
+	};
+
+	rs_stats_value_tmpl_t const _latency[] = {
+		{ &stats->latency_smoothed, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
+		{ &stats->interval.latency_average, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
+		{ &stats->interval.latency_high, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
+		{ &stats->interval.latency_low, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
+		{ NULL, 0, NULL, NULL }
+	};
+
+#define INIT_STATS(_ti, _v) do {\
+		strlcpy(buffer, fr_packet_codes[code], sizeof(buffer)); \
+		for (p = buffer; *p; ++p) *p = tolower((uint8_t) *p);\
+		last = *tmpl = rs_stats_collectd_init(ctx, conf, type, _ti, buffer, stats, _v);\
+		if (!*tmpl) {\
+			TALLOC_FREE(*out);\
+			return NULL;\
+		}\
+		tmpl = &(*tmpl)->next;\
+		ctx = *tmpl;\
+		} while (0)
+
+
+	INIT_STATS("radius_count", _packet_count);
+	INIT_STATS("radius_latency", _latency);
+
+	for (i = 0; i < (RS_RETRANSMIT_MAX + 1); i++) {
+		rtx[i].src = &stats->interval.rt[i];
+		rtx[i].type = LCC_TYPE_GAUGE;
+		rtx[i].cb = _copy_double_to_double;
+		rtx[i].dst = NULL;
+	}
+
+	rtx[i].src = &stats->interval.lost;
+	rtx[i].type = LCC_TYPE_GAUGE;
+	rtx[i].cb = _copy_double_to_double;
+	rtx[i].dst = NULL;
+
+	memset(&rtx[++i], 0, sizeof(rs_stats_value_tmpl_t));
+
+	INIT_STATS("radius_rtx", rtx);
+
+	return last;
+}
+
+/** Refresh and send the stats to the collectd server
+ *
+ */
+void rs_stats_collectd_do_stats(rs_t *conf, rs_stats_tmpl_t *tmpls, struct timeval *now)
+{
+	rs_stats_tmpl_t *tmpl = tmpls;
+	char identifier[6 * LCC_NAME_LEN];
+	int i;
+
+	while (tmpl) {
+		/*
+		 *	Refresh the value of whatever were sending
+		 */
+		for (i = 0; i < (int) tmpl->value->values_len; i++) {
+			tmpl->value_tmpl[i].cb(conf, &tmpl->value_tmpl[i]);
+		}
+
+		tmpl->value->time = now->tv_sec;
+
+		lcc_identifier_to_string(conf->stats.handle, identifier, sizeof(identifier), &tmpl->value->identifier);
+
+		if (lcc_putval(conf->stats.handle, tmpl->value) < 0) {
+			char const *error;
+
+			error = lcc_strerror(conf->stats.handle);
+			ERROR("Failed PUTVAL \"%s\" interval=%i %" PRIu64 " : %s",
+			      identifier,
+			      (int) tmpl->value->interval,
+			      (uint64_t) tmpl->value->time,
+			      error ? error : "unknown error");
+		}
+
+		tmpl = tmpl->next;
+	}
+}
+
+/** Connect to a collectd server for stats output
+ *
+ * @param[in,out] conf radsniff configuration, we write the generated handle here.
+ * @return 0 on success -1 on failure.
+ */
+int rs_stats_collectd_open(rs_t *conf)
+{
+	assert(conf->stats.collectd);
+
+	/*
+	 *	Tear down stale connections gracefully.
+	 */
+	rs_stats_collectd_close(conf);
+
+	/*
+	 *	There's no way to get the error from the connection handle
+	 *	because it's freed on failure, before lcc returns.
+	 */
+	if (lcc_connect(conf->stats.collectd, &conf->stats.handle) < 0) {
+		ERROR("Failed opening connection to collectd: %s", fr_syserror(errno));
+		return -1;
+	}
+	DEBUG2("Connected to \"%s\"", conf->stats.collectd);
+
+	assert(conf->stats.handle);
+	return 0;
+}
+
+/** Close connection
+ *
+ * @param[in,out] conf radsniff configuration.
+ * @return 0 on success -1 on failure.
+ */
+int rs_stats_collectd_close(rs_t *conf)
+{
+	assert(conf->stats.collectd);
+
+	int ret = 0;
+
+	if (conf->stats.handle) {
+		ret = lcc_disconnect(conf->stats.handle);
+		conf->stats.handle = NULL;
+	}
+
+	return ret;
+}
+#endif
diff --git a/src/main/command.c b/src/main/command.c
new file mode 100644
index 0000000..988f43b
--- /dev/null
+++ b/src/main/command.c
@@ -0,0 +1,3632 @@
+/*
+ * command.c	Command socket processing.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2008 The FreeRADIUS server project
+ * Copyright 2008 Alan DeKok <aland@deployingradius.com>
+ */
+
+#ifdef WITH_COMMAND_SOCKET
+
+#include <freeradius-devel/parser.h>
+#include <freeradius-devel/modcall.h>
+#include <freeradius-devel/md5.h>
+#include <freeradius-devel/channel.h>
+#include <freeradius-devel/connection.h>
+#include <freeradius-devel/socket.h>
+
+#include <libgen.h>
+#ifdef HAVE_INTTYPES_H
+#include <inttypes.h>
+#endif
+
+#ifdef HAVE_SYS_STAT_H
+#include <sys/stat.h>
+#endif
+
+#include <pwd.h>
+#include <grp.h>
+#include <ctype.h>
+
+typedef struct fr_command_table_t fr_command_table_t;
+
+typedef int (*fr_command_func_t)(rad_listen_t *, int, char *argv[]);
+
+#define FR_READ  (1)
+#define FR_WRITE (2)
+
+#define CMD_FAIL FR_CHANNEL_FAIL
+#define CMD_OK   FR_CHANNEL_SUCCESS
+
+struct fr_command_table_t {
+	char const *command;
+	int mode;		/* read/write */
+	char const *help;
+	fr_command_func_t func;
+	fr_command_table_t *table;
+};
+
+#define COMMAND_BUFFER_SIZE (1024)
+
+typedef struct fr_cs_buffer_t {
+	int		auth;
+	int		mode;
+	ssize_t		offset;
+	ssize_t		next;
+	char		buffer[COMMAND_BUFFER_SIZE];
+} fr_cs_buffer_t;
+
+#define COMMAND_SOCKET_MAGIC (0xffdeadee)
+typedef struct fr_command_socket_t {
+	uint32_t	magic;
+	char const	*path;
+	char		*copy;		/* <sigh> */
+	uid_t		uid;
+	gid_t		gid;
+	char const	*uid_name;
+	char const	*gid_name;
+	char const	*mode_name;
+	bool		peercred;
+	char user[256];
+
+	/*
+	 *	The next few entries handle fake packets injected by
+	 *	the control socket.
+	 */
+	fr_ipaddr_t	src_ipaddr; /* src_port is always 0 */
+	fr_ipaddr_t	dst_ipaddr;
+	uint16_t	dst_port;
+	rad_listen_t	*inject_listener;
+	RADCLIENT	*inject_client;
+
+	fr_cs_buffer_t  co;
+} fr_command_socket_t;
+
+static const CONF_PARSER command_config[] = {
+	{ "socket", FR_CONF_OFFSET(PW_TYPE_STRING, fr_command_socket_t, path), "${run_dir}/radiusd.sock" },
+	{ "uid", FR_CONF_OFFSET(PW_TYPE_STRING, fr_command_socket_t, uid_name), NULL },
+	{ "gid", FR_CONF_OFFSET(PW_TYPE_STRING, fr_command_socket_t, gid_name), NULL },
+	{ "mode", FR_CONF_OFFSET(PW_TYPE_STRING, fr_command_socket_t, mode_name), NULL },
+	{ "peercred", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_command_socket_t, peercred), "yes" },
+	CONF_PARSER_TERMINATOR
+};
+
+static FR_NAME_NUMBER mode_names[] = {
+	{ "ro", FR_READ },
+	{ "read-only", FR_READ },
+	{ "read-write", FR_READ | FR_WRITE },
+	{ "rw", FR_READ | FR_WRITE },
+	{ NULL, 0 }
+};
+
+#if !defined(HAVE_GETPEEREID) && defined(SO_PEERCRED)
+static int getpeereid(int s, uid_t *euid, gid_t *egid)
+{
+	struct ucred cr;
+	socklen_t cl = sizeof(cr);
+
+	if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cl) < 0) {
+		return -1;
+	}
+
+	*euid = cr.uid;
+	*egid = cr.gid;
+	return 0;
+}
+
+/* we now have getpeereid() in this file */
+#define HAVE_GETPEEREID (1)
+
+#endif /* HAVE_GETPEEREID */
+
+/** Initialise a socket for use with peercred authentication
+ *
+ * This function initialises a socket and path in a way suitable for use with
+ * peercred.
+ *
+ * @param path to socket.
+ * @param uid that should own the socket (linux only).
+ * @param gid that should own the socket (linux only).
+ * @return 0 on success -1 on failure.
+ */
+#ifdef __linux__
+static int fr_server_domain_socket_peercred(char const *path, uid_t uid, gid_t gid)
+#else
+static int fr_server_domain_socket_peercred(char const *path, uid_t UNUSED uid, UNUSED gid_t gid)
+#endif
+{
+	int sockfd;
+	size_t len;
+	socklen_t socklen;
+	struct sockaddr_un salocal;
+	struct stat buf;
+
+	if (!path) {
+		fr_strerror_printf("No path provided, was NULL");
+		return -1;
+	}
+
+	len = strlen(path);
+	if (len >= sizeof(salocal.sun_path)) {
+		fr_strerror_printf("Path too long in socket filename");
+		return -1;
+	}
+
+	if ((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
+		fr_strerror_printf("Failed creating socket: %s", fr_syserror(errno));
+		return -1;
+	}
+
+	memset(&salocal, 0, sizeof(salocal));
+	salocal.sun_family = AF_UNIX;
+	memcpy(salocal.sun_path, path, len + 1); /* SUN_LEN does strlen */
+
+	socklen = SUN_LEN(&salocal);
+
+	/*
+	 *	Check the path.
+	 */
+	if (stat(path, &buf) < 0) {
+		if (errno != ENOENT) {
+			fr_strerror_printf("Failed to stat %s: %s", path, fr_syserror(errno));
+			close(sockfd);
+			return -1;
+		}
+
+		/*
+		 *	FIXME: Check the enclosing directory?
+		 */
+	} else {		/* it exists */
+		int client_fd;
+
+		if (!S_ISREG(buf.st_mode)
+#ifdef S_ISSOCK
+		    && !S_ISSOCK(buf.st_mode)
+#endif
+			) {
+			fr_strerror_printf("Cannot turn %s into socket", path);
+			close(sockfd);
+			return -1;
+		}
+
+		/*
+		 *	Refuse to open sockets not owned by us.
+		 */
+		if (buf.st_uid != geteuid()) {
+			fr_strerror_printf("We do not own %s", path);
+			close(sockfd);
+			return -1;
+		}
+
+		/*
+		 *	Check if a server is already listening on the
+		 *	socket?
+		 */
+		client_fd = fr_socket_client_unix(path, false);
+		if (client_fd >= 0) {
+			fr_strerror_printf("Control socket '%s' is already in use", path);
+			close(client_fd);
+			close(sockfd);
+			return -1;
+		}
+
+		if (unlink(path) < 0) {
+		       fr_strerror_printf("Failed to delete %s: %s", path, fr_syserror(errno));
+		       close(sockfd);
+		       return -1;
+		}
+	}
+
+	if (bind(sockfd, (struct sockaddr *)&salocal, socklen) < 0) {
+		fr_strerror_printf("Failed binding to %s: %s", path, fr_syserror(errno));
+		close(sockfd);
+		return -1;
+	}
+
+	/*
+	 *	FIXME: There's a race condition here.  But Linux
+	 *	doesn't seem to permit fchmod on domain sockets.
+	 */
+	if (chmod(path, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP) < 0) {
+		fr_strerror_printf("Failed setting permissions on %s: %s", path, fr_syserror(errno));
+		close(sockfd);
+		return -1;
+	}
+
+	if (listen(sockfd, 8) < 0) {
+		fr_strerror_printf("Failed listening to %s: %s", path, fr_syserror(errno));
+		close(sockfd);
+		return -1;
+	}
+
+#ifdef O_NONBLOCK
+	{
+		int flags;
+
+		if ((flags = fcntl(sockfd, F_GETFL, NULL)) < 0)  {
+			fr_strerror_printf("Failure getting socket flags: %s", fr_syserror(errno));
+			close(sockfd);
+			return -1;
+		}
+
+		flags |= O_NONBLOCK;
+		if( fcntl(sockfd, F_SETFL, flags) < 0) {
+			fr_strerror_printf("Failure setting socket flags: %s", fr_syserror(errno));
+			close(sockfd);
+			return -1;
+		}
+	}
+#endif
+
+	/*
+	 *	Changing socket permissions only works on linux.
+	 *	BSDs ignore socket permissions.
+	 */
+#ifdef __linux__
+	/*
+	 *	Don't chown it from (possibly) non-root to root.
+	 *	Do chown it from (possibly) root to non-root.
+	 */
+	if ((uid != (uid_t) -1) || (gid != (gid_t) -1)) {
+		/*
+		 *	Don't do chown if it's already owned by us.
+		 */
+		if (fstat(sockfd, &buf) < 0) {
+			fr_strerror_printf("Failed reading %s: %s", path, fr_syserror(errno));
+			close(sockfd);
+			return -1;
+		}
+
+		if ((buf.st_uid != uid) || (buf.st_gid != gid)) {
+			rad_suid_up();
+			if (fchown(sockfd, uid, gid) < 0) {
+				fr_strerror_printf("Failed setting ownership of %s to (%d, %d): %s",
+				      path, uid, gid, fr_syserror(errno));
+				rad_suid_down();
+				close(sockfd);
+				return -1;
+			}
+			rad_suid_down();
+		}
+	}
+#endif
+
+	return sockfd;
+}
+
+#if !defined(HAVE_OPENAT) || !defined(HAVE_MKDIRAT) || !defined(HAVE_UNLINKAT)
+static int fr_server_domain_socket_perm(UNUSED char const *path, UNUSED uid_t uid, UNUSED gid_t gid)
+{
+	fr_strerror_printf("Unable to initialise control socket.  Set peercred = yes or update to "
+			   "POSIX-2008 compliant libc");
+	return -1;
+}
+#else
+/** Alternative function for creating Unix domain sockets and enforcing permissions
+ *
+ * Unlike fr_server_unix_socket which is intended to be used with peercred auth
+ * this function relies on the file system to enforce access.
+ *
+ * The way it does this depends on the operating system. On Linux systems permissions
+ * can be set on the socket directly and the system will enforce them.
+ *
+ * On most other systems fchown and fchmod fail when called with socket descriptors,
+ * and although permissions can be changed in other ways, they're not enforced.
+ *
+ * For these systems we use the permissions on the parent directory to enforce
+ * permissions on the socket. It's not safe to modify these permissions ourselves
+ * due to TOCTOU attacks, so if they don't match what we require, we error out and
+ * get the user to change them (which arguably isn't any safer, but releases us of
+ * the responsibility).
+ *
+ * @note must be called without effective root permissions (fr_suid_down).
+ *
+ * @param path where domain socket should be created.
+ * @return a file descriptor for the bound socket on success, -1 on failure.
+ */
+static int fr_server_domain_socket_perm(char const *path, uid_t uid, gid_t gid)
+{
+	int			dir_fd = -1, sock_fd = -1, parent_fd = -1;
+	char const		*name;
+	char			*buff = NULL, *dir = NULL, *p;
+
+	uid_t			euid;
+	gid_t			egid;
+
+	mode_t			perm = 0;
+	struct stat		st;
+
+	size_t			len;
+
+	socklen_t		socklen;
+	struct sockaddr_un	salocal;
+
+	rad_assert(path);
+
+	euid = geteuid();
+	egid = getegid();
+
+	/*
+	 *	Determine the correct permissions for the socket, or its
+	 *	containing directory.
+	 */
+	perm |= S_IREAD | S_IWRITE | S_IEXEC;
+	if (gid != (gid_t) -1) perm |= S_IRGRP | S_IWGRP | S_IXGRP;
+
+	buff = talloc_strdup(NULL, path);
+	if (!buff) return -1;
+
+	/*
+	 *	Some implementations modify it in place others use internal
+	 *	storage *sigh*. dirname also formats the path else we wouldn't
+	 *	be using it.
+	 */
+	dir = dirname(buff);
+	if (dir != buff) {
+		dir = talloc_strdup(NULL, dir);
+		if (!dir) return -1;
+		talloc_free(buff);
+	}
+
+	p = strrchr(dir, FR_DIR_SEP);
+	if (!p) {
+		fr_strerror_printf("Failed determining parent directory");
+	error:
+		talloc_free(dir);
+		if (sock_fd >= 0) close(sock_fd);
+		if (dir_fd >= 0) close(dir_fd);
+		if (parent_fd >= 0) close(parent_fd);
+		return -1;
+	}
+
+	*p = '\0';
+
+	/*
+	 *	Ensure the parent of the control socket directory exists,
+	 *	and the euid we're running under has access to it.
+	 */
+	parent_fd = open(dir, O_DIRECTORY);
+	if (parent_fd < 0) {
+		struct passwd *user;
+		struct group *group;
+
+		if (rad_getpwuid(NULL, &user, euid) < 0) goto error;
+		if (rad_getgrgid(NULL, &group, egid) < 0) {
+			talloc_free(user);
+			goto error;
+		}
+		fr_strerror_printf("Can't open directory \"%s\": %s.  Must be created manually, or modified, "
+				   "with permissions that allow writing by user %s or group %s", dir,
+				   user->pw_name, group->gr_name, fr_syserror(errno));
+		talloc_free(user);
+		talloc_free(group);
+		goto error;
+	}
+
+	*p = FR_DIR_SEP;
+
+	dir_fd = openat(parent_fd, p + 1, O_NOFOLLOW | O_DIRECTORY);
+	if (dir_fd < 0) {
+		int ret = 0;
+
+		if (errno != ENOENT) {
+			fr_strerror_printf("Failed opening control socket directory: %s", fr_syserror(errno));
+			goto error;
+		}
+
+		/*
+		 *	This fails if the radius user can't write
+		 *	to the parent directory.
+		 */
+	 	if (mkdirat(parent_fd, p + 1, 0700) < 0) {
+			fr_strerror_printf("Failed creating control socket directory: %s", fr_syserror(errno));
+			goto error;
+	 	}
+
+		dir_fd = openat(parent_fd, p + 1, O_NOFOLLOW | O_DIRECTORY);
+		if (dir_fd < 0) {
+			fr_strerror_printf("Failed opening the control socket directory we created: %s",
+					   fr_syserror(errno));
+			goto error;
+		}
+		if (fchmod(dir_fd, perm) < 0) {
+			fr_strerror_printf("Failed setting permissions on control socket directory: %s",
+					   fr_syserror(errno));
+			goto error;
+		}
+
+		rad_suid_up();
+		if ((uid != (uid_t)-1) || (gid != (gid_t)-1)) ret = fchown(dir_fd, uid, gid);
+		rad_suid_down();
+		if (ret < 0) {
+			fr_strerror_printf("Failed changing ownership of control socket directory: %s",
+					   fr_syserror(errno));
+			goto error;
+		}
+	/*
+	 *	Control socket dir already exists, but we still need to
+	 *	check the permissions are what we expect.
+	 */
+	} else {
+		int ret;
+		int client_fd;
+
+		ret = fstat(dir_fd, &st);
+		if (ret < 0) {
+			fr_strerror_printf("Failed checking permissions of control socket directory: %s",
+					   fr_syserror(errno));
+			goto error;
+		}
+
+		if ((uid != (uid_t)-1) && (st.st_uid != uid)) {
+			struct passwd *need_user, *have_user;
+
+			if (rad_getpwuid(NULL, &need_user, uid) < 0) goto error;
+			if (rad_getpwuid(NULL, &have_user, st.st_uid) < 0) {
+				talloc_free(need_user);
+				goto error;
+			}
+			fr_strerror_printf("Control socket directory must be owned by user %s, "
+					   "currently owned by %s", need_user->pw_name, have_user->pw_name);
+			talloc_free(need_user);
+			talloc_free(have_user);
+			goto error;
+		}
+
+		if ((gid != (gid_t)-1) && (st.st_gid != gid)) {
+			struct group *need_group, *have_group;
+
+			if (rad_getgrgid(NULL, &need_group, gid) < 0) goto error;
+			if (rad_getgrgid(NULL, &have_group, st.st_gid) < 0) {
+				talloc_free(need_group);
+				goto error;
+			}
+			fr_strerror_printf("Control socket directory \"%s\" must be owned by group %s, "
+					   "currently owned by %s", dir, need_group->gr_name, have_group->gr_name);
+			talloc_free(need_group);
+			talloc_free(have_group);
+			goto error;
+		}
+
+		if ((perm & 0x0c) != (st.st_mode & 0x0c)) {
+			char str_need[10], oct_need[5];
+			char str_have[10], oct_have[5];
+
+			rad_mode_to_str(str_need, perm);
+			rad_mode_to_oct(oct_need, perm);
+			rad_mode_to_str(str_have, st.st_mode);
+			rad_mode_to_oct(oct_have, st.st_mode);
+			fr_strerror_printf("Control socket directory must have permissions %s (%s), current "
+					   "permissions are %s (%s)", str_need, oct_need, str_have, oct_have);
+			goto error;
+		}
+
+		/*
+		 *	Check if a server is already listening on the
+		 *	socket?
+		 */
+		client_fd = fr_socket_client_unix(path, false);
+		if (client_fd >= 0) {
+			fr_strerror_printf("Control socket '%s' is already in use", path);
+			close(client_fd);
+			goto error;
+		}
+	}
+
+	name = strrchr(path, FR_DIR_SEP);
+	if (!name) {
+		fr_strerror_printf("Can't determine socket name");
+		goto error;
+	}
+	name++;
+
+	/*
+	 *	We've checked the containing directory has the permissions
+	 *	we expect, and as we have the FD, and aren't following
+	 *	symlinks no one can trick us into changing or creating a
+	 *	file elsewhere.
+	 *
+	 *	It's possible an attacker may still be able to create hard
+	 *	links, for the socket file. But they would need write
+	 *	access to the directory we just created or verified, so
+	 *	this attack vector is unlikely.
+	 */
+	if ((uid != (uid_t)-1) && (rad_seuid(uid) < 0)) goto error;
+	if ((gid != (gid_t)-1) && (rad_segid(gid) < 0)) {
+		rad_seuid(euid);
+		goto error;
+	}
+
+	/*
+	 *	The original code, did openat, used fstat to figure out
+	 *	what type the file was and then used unlinkat to unlink
+	 *	it. Except on OSX (at least) openat refuses to open
+	 *	socket files. So we now rely on the fact that unlinkat
+	 *	has sane and consistent behaviour, and will not unlink
+	 *	directories. unlinkat should also fail if the socket user
+	 *	hasn't got permission to modify the socket.
+	 */
+	if ((unlinkat(dir_fd, name, 0) < 0) && (errno != ENOENT)) {
+		fr_strerror_printf("Failed removing stale socket: %s", fr_syserror(errno));
+	sock_error:
+		if (uid != (uid_t)-1) rad_seuid(euid);
+		if (gid != (gid_t)-1) rad_segid(egid);
+		close(sock_fd);
+
+		goto error;
+	}
+
+	/*
+	 *	At this point we should have established a secure directory
+	 *	to house our socket, and cleared out any stale sockets.
+	 */
+	sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
+	if (sock_fd < 0) {
+		fr_strerror_printf("Failed creating socket: %s", fr_syserror(errno));
+		goto sock_error;
+	}
+
+#ifdef HAVE_BINDAT
+	len = strlen(name);
+#else
+	len = strlen(path);
+#endif
+	if (len >= sizeof(salocal.sun_path)) {
+		fr_strerror_printf("Path too long in socket filename");
+		goto error;
+	}
+
+	memset(&salocal, 0, sizeof(salocal));
+	salocal.sun_family = AF_UNIX;
+
+#ifdef HAVE_BINDAT
+	memcpy(salocal.sun_path, name, len + 1); /* SUN_LEN does strlen */
+#else
+	memcpy(salocal.sun_path, path, len + 1); /* SUN_LEN does strlen */
+#endif
+	socklen = SUN_LEN(&salocal);
+
+	/*
+	 *	Direct socket permissions are only useful on Linux which
+	 *	actually enforces them. BSDs don't. They also need to be
+	 *	set before binding the socket to a file.
+	 */
+#ifdef __linux__
+	if (fchmod(sock_fd, perm) < 0) {
+		char str_need[10], oct_need[5];
+
+		rad_mode_to_str(str_need, perm);
+		rad_mode_to_oct(oct_need, perm);
+		fr_strerror_printf("Failed changing socket permissions to %s (%s)", str_need, oct_need);
+
+		goto sock_error;
+	}
+
+	if (fchown(sock_fd, uid, gid) < 0) {
+		struct passwd *user;
+		struct group *group;
+
+		if (rad_getpwuid(NULL, &user, uid) < 0) goto sock_error;
+		if (rad_getgrgid(NULL, &group, gid) < 0) {
+			talloc_free(user);
+			goto sock_error;
+		}
+
+		fr_strerror_printf("Failed changing ownership of socket to %s:%s", user->pw_name, group->gr_name);
+		talloc_free(user);
+		talloc_free(group);
+		goto sock_error;
+	}
+#endif
+	/*
+	 *	The correct function to use here is bindat(), but only
+	 *	quite recent versions of FreeBSD actually have it, and
+	 *	it's definitely not POSIX.
+	 */
+#ifdef HAVE_BINDAT
+	if (bindat(dir_fd, sock_fd, (struct sockaddr *)&salocal, socklen) < 0) {
+#else
+	if (bind(sock_fd, (struct sockaddr *)&salocal, socklen) < 0) {
+#endif
+		fr_strerror_printf("Failed binding socket: %s", fr_syserror(errno));
+		goto sock_error;
+	}
+
+	if (listen(sock_fd, 8) < 0) {
+		fr_strerror_printf("Failed listening on socket: %s", fr_syserror(errno));
+		goto sock_error;
+	}
+
+#ifdef O_NONBLOCK
+	{
+		int flags;
+
+		flags = fcntl(sock_fd, F_GETFL, NULL);
+		if (flags < 0)  {
+			fr_strerror_printf("Failed getting socket flags: %s", fr_syserror(errno));
+			goto sock_error;
+		}
+
+		flags |= O_NONBLOCK;
+		if (fcntl(sock_fd, F_SETFL, flags) < 0) {
+			fr_strerror_printf("Failed setting nonblocking socket flag: %s", fr_syserror(errno));
+			goto sock_error;
+		}
+	}
+#endif
+
+	if (uid != (uid_t)-1) rad_seuid(euid);
+	if (gid != (gid_t)-1) rad_segid(egid);
+
+	if (dir_fd >= 0) close(dir_fd);
+	if (parent_fd >= 0) close(parent_fd);
+
+	return sock_fd;
+}
+#endif
+
+static void command_close_socket(rad_listen_t *this)
+{
+	this->status = RAD_LISTEN_STATUS_EOL;
+
+	/*
+	 *	This removes the socket from the event fd, so no one
+	 *	will be calling us any more.
+	 */
+	radius_update_listener(this);
+}
+
+static ssize_t CC_HINT(format (printf, 2, 3)) cprintf(rad_listen_t *listener, char const *fmt, ...)
+{
+	ssize_t r, len;
+	va_list ap;
+	char buffer[256];
+
+	va_start(ap, fmt);
+	len = vsnprintf(buffer, sizeof(buffer), fmt, ap);
+	va_end(ap);
+
+	if (listener->status == RAD_LISTEN_STATUS_EOL) return 0;
+
+	r = fr_channel_write(listener->fd, FR_CHANNEL_STDOUT, buffer, len);
+	if (r <= 0) command_close_socket(listener);
+
+	/*
+	 *	FIXME: Keep writing until done?
+	 */
+	return r;
+}
+
+static ssize_t CC_HINT(format (printf, 2, 3)) cprintf_error(rad_listen_t *listener, char const *fmt, ...)
+{
+	ssize_t r, len;
+	va_list ap;
+	char buffer[256];
+
+	va_start(ap, fmt);
+	len = vsnprintf(buffer, sizeof(buffer), fmt, ap);
+	va_end(ap);
+
+	if (listener->status == RAD_LISTEN_STATUS_EOL) return 0;
+
+	r = fr_channel_write(listener->fd, FR_CHANNEL_STDERR, buffer, len);
+	if (r <= 0) command_close_socket(listener);
+
+	/*
+	 *	FIXME: Keep writing until done?
+	 */
+	return r;
+}
+
+static int command_hup(rad_listen_t *listener, int argc, char *argv[])
+{
+	CONF_SECTION *cs;
+	module_instance_t *mi;
+	char buffer[256];
+
+	if (argc == 0) {
+		radius_signal_self(RADIUS_SIGNAL_SELF_HUP);
+		return CMD_OK;
+	}
+
+	/*
+	 *	Hack a "main" HUP thingy
+	 */
+	if (strcmp(argv[0], "main.log") == 0) {
+		hup_logfile();
+		return CMD_OK;
+	}
+
+	cs = cf_section_find("modules");
+	if (!cs) return CMD_FAIL;
+
+	mi = module_find(cs, argv[0]);
+	if (!mi) {
+		cprintf_error(listener, "No such module \"%s\"\n", argv[0]);
+		return CMD_FAIL;
+	}
+
+	if ((mi->entry->module->type & RLM_TYPE_HUP_SAFE) == 0) {
+		cprintf_error(listener, "Module %s cannot be hup'd\n",
+			argv[0]);
+		return CMD_FAIL;
+	}
+
+	if (!module_hup_module(mi->cs, mi, time(NULL))) {
+		cprintf_error(listener, "Failed to reload module\n");
+		return CMD_FAIL;
+	}
+
+	snprintf(buffer, sizeof(buffer), "modules.%s.hup",
+		 cf_section_name1(mi->cs));
+	exec_trigger(NULL, mi->cs, buffer, true);
+
+	return CMD_OK;
+}
+
+static int command_terminate(UNUSED rad_listen_t *listener,
+			     UNUSED int argc, UNUSED char *argv[])
+{
+	radius_signal_self(RADIUS_SIGNAL_SELF_TERM);
+
+	return CMD_OK;
+}
+
+static int command_uptime(rad_listen_t *listener,
+			  UNUSED int argc, UNUSED char *argv[])
+{
+	char buffer[128];
+
+	CTIME_R(&fr_start_time, buffer, sizeof(buffer));
+	cprintf(listener, "Up since %s", buffer); /* no \r\n */
+
+	return CMD_OK;
+}
+
+static int command_show_config(rad_listen_t *listener, int argc, char *argv[])
+{
+	CONF_ITEM *ci;
+	CONF_PAIR *cp;
+	char const *value;
+
+	if (argc != 1) {
+		cprintf_error(listener, "No path was given\n");
+		return CMD_FAIL;
+	}
+
+	ci = cf_reference_item(main_config.config, main_config.config, argv[0]);
+	if (!ci) return CMD_FAIL;
+
+	if (!cf_item_is_pair(ci)) return CMD_FAIL;
+
+	cp = cf_item_to_pair(ci);
+	value = cf_pair_value(cp);
+	if (!value) return CMD_FAIL;
+
+	cprintf(listener, "%s\n", value);
+
+	return CMD_OK;
+}
+
+static char const tabs[] = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";
+
+/*
+ *	FIXME: Recurse && indent?
+ */
+static void cprint_conf_parser(rad_listen_t *listener, int indent, CONF_SECTION *cs,
+			       void const *base)
+
+{
+	int i;
+	char const *name1 = cf_section_name1(cs);
+	char const *name2 = cf_section_name2(cs);
+	CONF_PARSER const *variables = cf_section_parse_table(cs);
+
+	if (name2) {
+		cprintf(listener, "%.*s%s %s {\n", indent, tabs, name1, name2);
+	} else {
+		cprintf(listener, "%.*s%s {\n", indent, tabs, name1);
+	}
+
+	indent++;
+
+	/*
+	 *	Print
+	 */
+	if (variables) for (i = 0; variables[i].name != NULL; i++) {
+		void const *data;
+		char buffer[256];
+
+		/*
+		 *	No base struct offset, data must be the pointer.
+		 *	If data doesn't exist, ignore the entry, there
+		 *	must be something wrong.
+		 */
+		if (!base) {
+			if (!variables[i].data) {
+				continue;
+			}
+
+			data = variables[i].data;
+
+		} else if (variables[i].data) {
+			data = variables[i].data;
+
+		} else {
+			data = (((char const *)base) + variables[i].offset);
+		}
+
+		/*
+		 *	Ignore the various flags
+		 */
+		switch (variables[i].type & 0xff) {
+		default:
+			cprintf(listener, "%.*s%s = ?\n", indent, tabs,
+				variables[i].name);
+			break;
+
+		case PW_TYPE_INTEGER:
+			cprintf(listener, "%.*s%s = %u\n", indent, tabs,
+				variables[i].name, *(int const *) data);
+			break;
+
+		case PW_TYPE_IPV4_ADDR:
+			inet_ntop(AF_INET, data, buffer, sizeof(buffer));
+			break;
+
+		case PW_TYPE_IPV6_ADDR:
+			inet_ntop(AF_INET6, data, buffer, sizeof(buffer));
+			break;
+
+		case PW_TYPE_BOOLEAN:
+			cprintf(listener, "%.*s%s = %s\n", indent, tabs,
+				variables[i].name,
+				((*(bool const *) data) == false) ? "no" : "yes");
+			break;
+
+		case PW_TYPE_STRING:
+		case PW_TYPE_FILE_INPUT:
+		case PW_TYPE_FILE_OUTPUT:
+			/*
+			 *	FIXME: Escape things in the string!
+			 */
+			if (*(char const * const *) data) {
+				cprintf(listener, "%.*s%s = \"%s\"\n", indent, tabs,
+					variables[i].name, *(char const * const *) data);
+			} else {
+				cprintf(listener, "%.*s%s = \n", indent, tabs,
+					variables[i].name);
+			}
+
+			break;
+		}
+	}
+
+	indent--;
+
+	cprintf(listener, "%.*s}\n", indent, tabs);
+}
+
+static void cprint_conf_section(rad_listen_t *listener, int indent, CONF_SECTION *cs)
+{
+	char const *name1 = cf_section_name1(cs);
+	char const *name2 = cf_section_name2(cs);
+	CONF_ITEM *ci;
+
+	if (name2) {
+		cprintf(listener, "%.*s%s %s {\n", indent, tabs, name1, name2);
+	} else {
+		cprintf(listener, "%.*s%s {\n", indent, tabs, name1);
+	}
+
+	indent++;
+
+
+	for (ci = cf_item_find_next(cs, NULL);
+	     ci != NULL;
+	     ci = cf_item_find_next(cs, ci)) {
+		CONF_PAIR const *cp;
+		char const *value;
+
+		if (cf_item_is_section(ci)) {
+			cprint_conf_section(listener, indent, cf_item_to_section(ci));
+			continue;
+		}
+
+		if (!cf_item_is_pair(ci)) continue;
+
+		cp = cf_item_to_pair(ci);
+		value = cf_pair_value(cp);
+
+		if (value) {
+			/*
+			 *	@todo - quote the value if necessary.
+			 */
+			cprintf(listener, "%.*s%s = %s\n",
+				indent, tabs,
+				cf_pair_attr(cp), value);
+		} else {
+			cprintf(listener, "%.*s%s\n",
+				indent, tabs,
+				cf_pair_attr(cp));
+		}
+	}
+
+	indent--;
+
+	cprintf(listener, "%.*s}\n", indent, tabs);
+}
+
+static int command_show_module_config(rad_listen_t *listener, int argc, char *argv[])
+{
+	CONF_SECTION *cs;
+	module_instance_t *mi;
+
+	if (argc != 1) {
+		cprintf_error(listener, "No module name was given\n");
+		return CMD_FAIL;
+	}
+
+	cs = cf_section_find("modules");
+	if (!cs) return CMD_FAIL;
+
+	mi = module_find(cs, argv[0]);
+	if (!mi) {
+		cprintf_error(listener, "No such module \"%s\"\n", argv[0]);
+		return CMD_FAIL;
+	}
+
+	cprint_conf_parser(listener, 0, mi->cs, mi->insthandle);
+
+	return CMD_OK;
+}
+
+static char const *method_names[MOD_COUNT] = {
+	"authenticate",
+	"authorize",
+	"preacct",
+	"accounting",
+	"session",
+	"pre-proxy",
+	"post-proxy",
+	"post-auth"
+#ifdef WITH_COA
+	,
+	"recv-coa",
+	"send-coa"
+#endif
+};
+
+
+static int command_show_module_methods(rad_listen_t *listener, int argc, char *argv[])
+{
+	int i;
+	CONF_SECTION *cs;
+	module_instance_t const *mi;
+	module_t const *mod;
+
+	if (argc != 1) {
+		cprintf_error(listener, "No module name was given\n");
+		return CMD_FAIL;
+	}
+
+	cs = cf_section_find("modules");
+	if (!cs) return CMD_FAIL;
+
+	mi = module_find(cs, argv[0]);
+	if (!mi) {
+		cprintf_error(listener, "No such module \"%s\"\n", argv[0]);
+		return CMD_FAIL;
+	}
+
+	mod = mi->entry->module;
+
+	for (i = 0; i < MOD_COUNT; i++) {
+		if (mod->methods[i]) cprintf(listener, "%s\n", method_names[i]);
+	}
+
+	return CMD_OK;
+}
+
+
+static int command_show_module_flags(rad_listen_t *listener, int argc, char *argv[])
+{
+	CONF_SECTION *cs;
+	module_instance_t const *mi;
+	module_t const *mod;
+
+	if (argc != 1) {
+		cprintf_error(listener, "No module name was given\n");
+		return CMD_FAIL;
+	}
+
+	cs = cf_section_find("modules");
+	if (!cs) return CMD_FAIL;
+
+	mi = module_find(cs, argv[0]);
+	if (!mi) {
+		cprintf_error(listener, "No such module \"%s\"\n", argv[0]);
+		return CMD_FAIL;
+	}
+
+	mod = mi->entry->module;
+
+	if ((mod->type & RLM_TYPE_THREAD_UNSAFE) != 0)
+		cprintf(listener, "thread-unsafe\n");
+
+	if ((mod->type & RLM_TYPE_HUP_SAFE) != 0)
+		cprintf(listener, "reload-on-hup\n");
+
+	return CMD_OK;
+}
+
+static int command_show_module_status(rad_listen_t *listener, int argc, char *argv[])
+{
+	CONF_SECTION *cs;
+	const module_instance_t *mi;
+
+	if (argc != 1) {
+		cprintf_error(listener, "No module name was given\n");
+		return CMD_FAIL;
+	}
+
+	cs = cf_section_find("modules");
+	if (!cs) return CMD_FAIL;
+
+	mi = module_find(cs, argv[0]);
+	if (!mi) {
+		cprintf_error(listener, "No such module \"%s\"\n", argv[0]);
+		return CMD_FAIL;
+	}
+
+	if (!mi->force) {
+		cprintf(listener, "alive\n");
+	} else {
+		cprintf(listener, "%s\n", fr_int2str(mod_rcode_table, mi->code, "<invalid>"));
+	}
+
+
+	return CMD_OK;
+}
+
+
+/*
+ *	Show all loaded modules
+ */
+static int command_show_modules(rad_listen_t *listener, UNUSED int argc, UNUSED char *argv[])
+{
+	CONF_SECTION *cs, *subcs;
+
+	cs = cf_section_find("modules");
+	if (!cs) return CMD_FAIL;
+
+	subcs = NULL;
+	while ((subcs = cf_subsection_find_next(cs, subcs, NULL)) != NULL) {
+		char const *name1 = cf_section_name1(subcs);
+		char const *name2 = cf_section_name2(subcs);
+
+		module_instance_t *mi;
+
+		if (name2) {
+			mi = module_find(cs, name2);
+			if (!mi) continue;
+
+			cprintf(listener, "%s (%s)\n", name2, name1);
+		} else {
+			mi = module_find(cs, name1);
+			if (!mi) continue;
+
+			cprintf(listener, "%s\n", name1);
+		}
+	}
+
+	return CMD_OK;
+}
+
+#ifdef WITH_PROXY
+static int command_show_home_servers(rad_listen_t *listener, int argc, char *argv[])
+{
+	int i;
+	home_server_t *home;
+	char const *type, *state, *proto;
+
+	char buffer[256];
+
+	for (i = 0; i < home_server_max_number; i++) {
+
+		if ((home = home_server_bynumber(i)) == NULL)
+			continue;
+
+		/*
+		 *	Internal "virtual" home server.
+		 */
+		if (home->ipaddr.af == AF_UNSPEC) continue;
+
+		if (home->type == HOME_TYPE_AUTH) {
+			type = "auth";
+
+		} else if (home->type == HOME_TYPE_ACCT) {
+			type = "acct";
+
+		} else if (home->type == HOME_TYPE_AUTH_ACCT) {
+			type = "auth+acct";
+
+#ifdef WITH_COA
+		} else if (home->type == HOME_TYPE_COA) {
+			type = "coa";
+#endif
+
+		} else continue;
+
+		if (home->proto == IPPROTO_UDP) {
+			proto = "udp";
+		}
+#ifdef WITH_TCP
+		else if (home->proto == IPPROTO_TCP) {
+			proto = "tcp";
+		}
+#endif
+		else proto = "??";
+
+		if (home->state == HOME_STATE_ALIVE) {
+			state = "alive";
+
+		} else if (home->state == HOME_STATE_ZOMBIE) {
+			state = "zombie";
+
+		} else if (home->state == HOME_STATE_IS_DEAD) {
+			state = "dead";
+
+		} else if (home->state == HOME_STATE_ADMIN_DOWN) {
+			state = "down";
+
+		} else if (home->state == HOME_STATE_UNKNOWN) {
+			time_t now = time(NULL);
+
+			/*
+			 *	We've recently received a packet, so
+			 *	the home server seems to be alive.
+			 *
+			 *	The *reported* state changes because
+			 *	the internal state machine NEEDS THE
+			 *	RIGHT STATE.  However, reporting that
+			 *	to the admin will confuse them.
+			 *	So... we lie.  No, that dress doesn't
+			 *	make you look fat...
+			 */
+			if ((home->last_packet_recv + (int)home->ping_interval) >= now) {
+				state = "alive";
+			} else {
+				state = "unknown";
+			}
+
+		} else continue;
+
+		if (argc > 0 && !strcmp(argv[0], "all")) {
+			char const *dynamic = home->dynamic ? "yes" : "no";
+
+			cprintf(listener, "%s\t%d\t%s\t%s\t%s\t%d\t(name=%s, dynamic=%s)\n",
+				ip_ntoh(&home->ipaddr, buffer, sizeof(buffer)),
+				home->port, proto, type, state,
+				home->currently_outstanding, home->name, dynamic);
+		} else {
+			cprintf(listener, "%s\t%d\t%s\t%s\t%s\t%d\n",
+				ip_ntoh(&home->ipaddr, buffer, sizeof(buffer)),
+				home->port, proto, type, state,
+				home->currently_outstanding);
+		}
+	}
+
+	return CMD_OK;
+}
+#endif
+
+static RADCLIENT *get_client(rad_listen_t *listener, int argc, char *argv[]);
+
+static int command_show_client_config(rad_listen_t *listener, int argc, char *argv[])
+{
+	RADCLIENT *client;
+
+	client = get_client(listener, argc, argv);
+	if (!client) {
+		return 0;
+	}
+
+	if (!client->cs) return 1;
+
+	cprint_conf_section(listener, 0, client->cs);
+	return 1;
+}
+
+/*
+ *	@todo - copied from clients.c.  Better to re-use, but whatever.
+ */
+struct radclient_list {
+	char const	*name;	/* name of this list */
+	char const	*server; /* virtual server associated with this client list */
+
+	/*
+	 *	FIXME: One set of trees for IPv4, and another for IPv6?
+	 */
+	rbtree_t	*trees[129]; /* for 0..128, inclusive. */
+	uint32_t       	min_prefix;
+};
+
+
+static int command_show_clients(rad_listen_t *listener, int argc, char *argv[])
+{
+	int i;
+	RADCLIENT *client;
+	char buffer[256];
+
+	if (argc == 0) {
+		for (i = 0; (client = client_findbynumber(NULL, i)) != NULL; i++) {
+			ip_ntoh(&client->ipaddr, buffer, sizeof(buffer));
+
+			if (((client->ipaddr.af == AF_INET) &&
+			     (client->ipaddr.prefix != 32)) ||
+			    ((client->ipaddr.af == AF_INET6) &&
+			     (client->ipaddr.prefix != 128))) {
+				cprintf(listener, "%s/%d\n", buffer, client->ipaddr.prefix);
+			} else {
+				cprintf(listener, "%s\n", buffer);
+			}
+		}
+
+		return CMD_OK;
+	}
+
+	if (argc != 1) {
+		cprintf_error(listener, "Unknown command %s %s ...\n", argv[0], argv[1]);
+		return -1;
+	}
+
+	if (strcmp(argv[0], "verbose") != 0) {
+		cprintf_error(listener, "Unknown command %s\n", argv[0]);
+		return -1;
+	}
+
+	for (i = 0; (client = client_findbynumber(NULL, i)) != NULL; i++) {
+		if (client->cs) {
+			cprintf(listener, "client %s {\n", cf_section_name2(client->cs));
+		} else {
+			cprintf(listener, "client {\n");
+		}
+
+		fr_ntop(buffer, sizeof(buffer), &client->ipaddr);
+		cprintf(listener, "\tipaddr = %s\n", buffer);
+
+		if (client->src_ipaddr.af != AF_UNSPEC) {
+			fr_ntop(buffer, sizeof(buffer), &client->src_ipaddr);
+			cprintf(listener, "\tsrc_ipaddr = %s\n", buffer);
+		}
+
+		if (client->proto == IPPROTO_UDP) {
+			cprintf(listener, "\tproto = udp\n");
+		} else if (client->proto == IPPROTO_TCP) {
+			cprintf(listener, "\tproto = tcp\n");
+		} else {
+			cprintf(listener, "\tproto = *\n");
+		}
+
+		cprintf(listener, "\tsecret = %s\n", client->secret);
+		cprintf(listener, "\tlongname = %s\n", client->longname);
+		cprintf(listener, "\tshortname = %s\n", client->shortname);
+		if (client->nas_type) cprintf(listener, "\tnas_type = %s\n", client->nas_type);
+		cprintf(listener, "\tnumber = %d\n", client->number);
+
+		if (client->server) {
+			cprintf(listener, "\tvirtual_server = %s\n", client->server);
+		}
+
+#ifdef WITH_DYNAMIC_CLIENTS
+		if (client->dynamic) {
+			cprintf(listener, "\tdynamic = yes\n");
+			cprintf(listener, "\tlifetime = %u\n", client->lifetime);
+		}
+#endif
+
+#ifdef WITH_TLS
+		if (client->tls_required) {
+			cprintf(listener, "\ttls = yes\n");
+		}
+#endif
+
+		if (client->list && client->list->server) {
+			cprintf(listener, "\tparent_virtual_server = %s\n", client->list->server);
+		} else {
+			cprintf(listener, "\tglobal = yes\n");
+		}
+
+		cprintf(listener, "}\n");
+	}
+
+	return CMD_OK;
+}
+
+
+static int command_show_version(rad_listen_t *listener, UNUSED int argc, UNUSED char *argv[])
+{
+	cprintf(listener, "%s\n", radiusd_version);
+	return CMD_OK;
+}
+
+static int command_debug_level(rad_listen_t *listener, int argc, char *argv[])
+{
+	int number;
+
+	if (argc == 0) {
+		cprintf_error(listener, "Must specify <number>\n");
+		return -1;
+	}
+
+	number = atoi(argv[0]);
+	if ((number < 0) || (number > 4)) {
+		cprintf_error(listener, "<number> must be between 0 and 4\n");
+		return -1;
+	}
+
+	fr_debug_lvl = rad_debug_lvl = number;
+
+	return CMD_OK;
+}
+
+static char debug_log_file_buffer[1024];
+
+static int command_debug_file(rad_listen_t *listener, int argc, char *argv[])
+{
+	if (rad_debug_lvl && default_log.dst == L_DST_STDOUT) {
+		cprintf_error(listener, "Cannot redirect debug logs to a file when already in debugging mode.\n");
+		return -1;
+	}
+
+	if ((argc > 0) && (strchr(argv[0], FR_DIR_SEP) == argv[0])) {
+		cprintf_error(listener, "Cannot direct debug logs to absolute path.\n");
+	}
+
+	default_log.debug_file = NULL;
+
+	if (argc == 0) return CMD_OK;
+
+	/*
+	 *	This looks weird, but it's here to avoid locking
+	 *	a mutex for every log message.
+	 */
+	memset(debug_log_file_buffer, 0, sizeof(debug_log_file_buffer));
+
+	/*
+	 *	Debug files always go to the logging directory.
+	 */
+	snprintf(debug_log_file_buffer, sizeof(debug_log_file_buffer),
+		 "%s/%s", radlog_dir, argv[0]);
+
+	default_log.debug_file = &debug_log_file_buffer[0];
+
+	return CMD_OK;
+}
+
+extern fr_cond_t *debug_condition;
+static int command_debug_condition(rad_listen_t *listener, int argc, char *argv[])
+{
+	int i;
+	char const *error;
+	ssize_t slen = 0;
+	fr_cond_t *new_condition = NULL;
+	char *p, buffer[1024];
+
+	/*
+	 *	Disable it.
+	 */
+	if (argc == 0) {
+		TALLOC_FREE(debug_condition);
+		debug_condition = NULL;
+		return CMD_OK;
+	}
+
+	if (!((argc == 1) &&
+	      ((argv[0][0] == '"') || (argv[0][0] == '\'')))) {
+		p = buffer;
+		*p = '\0';
+		for (i = 0; i < argc; i++) {
+			size_t len;
+
+			len = strlcpy(p, argv[i], buffer + sizeof(buffer) - p);
+			p += len;
+			*(p++) = ' ';
+			*p = '\0';
+		}
+
+	} else {
+		/*
+		 *	Backwards compatibility.  De-escape the string.
+		 */
+		char quote;
+		char *q;
+
+		p = argv[0];
+		q = buffer;
+
+		quote = *(p++);
+
+		while (true) {
+			if (!*p) {
+				error = "Unexpected end of string";
+				slen = -strlen(argv[0]);
+				p = argv[0];
+
+				goto parse_error;
+			}
+
+			if (*p == quote) {
+				if (p[1]) {
+					error = "Unexpected text after end of string";
+					slen = -(p - argv[0]);
+					p = argv[0];
+
+					goto parse_error;
+				}
+				*q = '\0';
+				break;
+			}
+
+			if (*p == '\\') {
+				*(q++) = p[1];
+				p += 2;
+				continue;
+			}
+
+			*(q++) = *(p++);
+		}
+	}
+
+	p = buffer;
+
+	slen = fr_condition_tokenize(NULL, NULL, p, &new_condition, &error, FR_COND_ONE_PASS);
+	if (slen <= 0) {
+		char *spaces, *text;
+
+	parse_error:
+		fr_canonicalize_error(NULL, &spaces, &text, slen, p);
+
+		ERROR("Parse error in condition");
+		ERROR("%s", p);
+		ERROR("%s^ %s", spaces, error);
+
+		cprintf_error(listener, "Parse error in condition \"%s\": %s\n", p, error);
+
+		talloc_free(spaces);
+		talloc_free(text);
+		return CMD_FAIL;
+	}
+
+	(void) modcall_pass2_condition(new_condition);
+
+	/*
+	 *	Delete old condition.
+	 *
+	 *	This is thread-safe because the condition is evaluated
+	 *	in the main server thread, along with this code.
+	 */
+	TALLOC_FREE(debug_condition);
+	debug_condition = new_condition;
+
+	return CMD_OK;
+}
+
+static int command_show_debug_condition(rad_listen_t *listener,
+					UNUSED int argc, UNUSED char *argv[])
+{
+	char buffer[1024];
+
+	if (!debug_condition) {
+		cprintf(listener, "\n");
+		return CMD_OK;
+	}
+
+	fr_cond_sprint(buffer, sizeof(buffer), debug_condition);
+
+	cprintf(listener, "%s\n", buffer);
+	return CMD_OK;
+}
+
+
+static int command_show_debug_file(rad_listen_t *listener,
+					UNUSED int argc, UNUSED char *argv[])
+{
+	if (!default_log.debug_file) return CMD_FAIL;
+
+	cprintf(listener, "%s\n", default_log.debug_file);
+	return CMD_OK;
+}
+
+
+static int command_show_debug_level(rad_listen_t *listener,
+					UNUSED int argc, UNUSED char *argv[])
+{
+	cprintf(listener, "%d\n", rad_debug_lvl);
+	return CMD_OK;
+}
+
+
+static RADCLIENT *get_client(rad_listen_t *listener, int argc, char *argv[])
+{
+	RADCLIENT *client;
+	fr_ipaddr_t ipaddr;
+	int myarg;
+	int proto = IPPROTO_UDP;
+	RADCLIENT_LIST *list = NULL;
+
+	if (argc < 1) {
+		cprintf_error(listener, "Must specify <ipaddr>\n");
+		return NULL;
+	}
+
+	/*
+	 *	First arg is IP address.
+	 */
+	if (ip_hton(&ipaddr, AF_UNSPEC, argv[0], false) < 0) {
+		cprintf_error(listener, "Failed parsing IP address; %s\n",
+			fr_strerror());
+		return NULL;
+	}
+	myarg = 1;
+
+	while (myarg < argc) {
+		if (strcmp(argv[myarg], "udp") == 0) {
+			proto = IPPROTO_UDP;
+			myarg++;
+			continue;
+		}
+
+#ifdef WITH_TCP
+		if (strcmp(argv[myarg], "tcp") == 0) {
+			proto = IPPROTO_TCP;
+			myarg++;
+			continue;
+		}
+#endif
+
+		if (strcmp(argv[myarg], "listen") == 0) {
+			uint16_t server_port;
+			fr_ipaddr_t server_ipaddr;
+
+			if ((argc - myarg) < 2) {
+				cprintf_error(listener, "Must specify listen <ipaddr> <port>\n");
+				return NULL;
+			}
+
+			if (ip_hton(&server_ipaddr, ipaddr.af, argv[myarg + 1], false) < 0) {
+				cprintf_error(listener, "Failed parsing IP address; %s\n",
+					      fr_strerror());
+				return NULL;
+			}
+
+			server_port = atoi(argv[myarg + 2]);
+
+			list = listener_find_client_list(&server_ipaddr, server_port, proto);
+			if (!list) {
+				cprintf_error(listener, "No such listener %s %s\n", argv[myarg + 1], argv[myarg + 2]);
+				return NULL;
+			}
+			myarg += 3;
+			continue;
+		}
+
+		cprintf_error(listener, "Unknown argument %s.\n", argv[myarg]);
+		return NULL;
+	}
+
+	client = client_find(list, &ipaddr, proto);
+	if (!client) {
+		cprintf_error(listener, "No such client\n");
+		return NULL;
+	}
+
+	return client;
+}
+
+#ifdef WITH_PROXY
+static home_server_t *get_home_server(rad_listen_t *listener, int argc,
+				      char *argv[], int *last)
+{
+	int myarg = 2;
+	home_server_t *home;
+	uint16_t port;
+	int proto = IPPROTO_UDP;
+	fr_ipaddr_t ipaddr, src_ipaddr;
+
+	if (argc < 2) {
+		cprintf_error(listener, "Must specify <ipaddr> <port> [udp|tcp] OR <name> <type>\n");
+		return NULL;
+	}
+
+	if (isdigit((uint8_t) *argv[1])) {
+		if (ip_hton(&ipaddr, AF_UNSPEC, argv[0], false) < 0) {
+			cprintf_error(listener, "Failed parsing IP address; %s\n",
+				      fr_strerror());
+			return NULL;
+		}
+
+		memset(&src_ipaddr, 0, sizeof(src_ipaddr));
+		src_ipaddr.af = ipaddr.af;
+
+		port = atoi(argv[1]);
+
+		while (myarg < argc) {
+			if (strcmp(argv[myarg], "udp") == 0) {
+				proto = IPPROTO_UDP;
+				myarg++;
+				continue;
+			}
+
+#ifdef WITH_TCP
+			if (strcmp(argv[myarg], "tcp") == 0) {
+				proto = IPPROTO_TCP;
+				myarg++;
+				continue;
+			}
+#endif
+
+			/*
+			 *	Allow the caller to specify src, too.
+			 */
+			if (strcmp(argv[myarg], "src") == 0) {
+				if ((myarg + 2) < argc) {
+					cprintf_error(listener, "You must specify an address after 'src' \n");
+					return NULL;
+				}
+
+				if (ip_hton(&src_ipaddr, ipaddr.af, argv[myarg + 1], false) < 0) {
+					cprintf_error(listener, "Failed parsing IP address; %s\n",
+						      fr_strerror());
+					return NULL;
+				}
+
+				myarg += 2;
+			continue;
+			}
+
+			/*
+			 *	Unknown argument.  Leave it for the caller.
+			 */
+			break;
+		}
+
+		home = home_server_find_bysrc(&ipaddr, port, proto, &src_ipaddr);
+	} else {
+		int type;
+
+		static const FR_NAME_NUMBER home_server_types[] = {
+			{ "auth",		HOME_TYPE_AUTH },
+			{ "acct",		HOME_TYPE_ACCT },
+			{ "auth+acct",		HOME_TYPE_AUTH_ACCT },
+			{ "coa",		HOME_TYPE_COA },
+#ifdef WITH_COA_TUNNEL
+			{ "auth+coa",		HOME_TYPE_AUTH_COA },
+			{ "auth+acct+coa",	HOME_TYPE_AUTH_ACCT_COA },
+#endif
+			{ NULL, 0 }
+		};
+
+		type = fr_str2int(home_server_types, argv[1], HOME_TYPE_INVALID);
+		if (type == HOME_TYPE_INVALID) {
+			cprintf_error(listener, "Invalid home server type '%s'\n", argv[1]);
+			return NULL;
+		}
+
+		home = home_server_byname(argv[0], type);
+	}
+
+	if (!home) {
+		cprintf_error(listener, "No such home server - %s %s\n", argv[0], argv[1]);
+		return NULL;
+	}
+
+	if (last) *last = myarg;
+
+	return home;
+}
+
+static int command_set_home_server_state(rad_listen_t *listener, int argc, char *argv[])
+{
+	int last;
+	home_server_t *home;
+
+	if (argc < 3) {
+		cprintf_error(listener, "Must specify <ipaddr> <port> [udp|tcp] <state>\n");
+		return CMD_FAIL;
+	}
+
+	home = get_home_server(listener, argc, argv, &last);
+	if (!home) {
+		return CMD_FAIL;
+	}
+
+	if (strcmp(argv[last], "alive") == 0) {
+		revive_home_server(home);
+
+	} else if (strcmp(argv[last], "dead") == 0) {
+		struct timeval now;
+
+		gettimeofday(&now, NULL); /* we do this WAY too often */
+		mark_home_server_dead(home, &now, false);
+
+	} else if (strcmp(argv[last], "down") == 0) {
+		struct timeval now;
+
+		gettimeofday(&now, NULL); /* we do this WAY too often */
+		mark_home_server_dead(home, &now, true);
+
+	} else {
+		cprintf_error(listener, "Unknown state \"%s\"\n", argv[last]);
+		return CMD_FAIL;
+	}
+
+	return CMD_OK;
+}
+
+static int command_show_home_server_state(rad_listen_t *listener, int argc, char *argv[])
+{
+	home_server_t *home;
+
+	home = get_home_server(listener, argc, argv, NULL);
+	if (!home) return CMD_FAIL;
+
+	switch (home->state) {
+	case HOME_STATE_ALIVE:
+		cprintf(listener, "alive\n");
+		break;
+
+	case HOME_STATE_IS_DEAD:
+		cprintf(listener, "dead\n");
+		break;
+
+	case HOME_STATE_ZOMBIE:
+		cprintf(listener, "zombie\n");
+		break;
+
+	case HOME_STATE_ADMIN_DOWN:
+		cprintf(listener, "down\n");
+		break;
+
+	case HOME_STATE_UNKNOWN:
+		cprintf(listener, "unknown\n");
+		break;
+
+	default:
+		cprintf(listener, "invalid\n");
+		break;
+	}
+
+	return CMD_OK;
+}
+#endif
+
+/*
+ *	For encode/decode stuff
+ */
+static int null_socket_dencode(UNUSED rad_listen_t *listener, UNUSED REQUEST *request)
+{
+	return 0;
+}
+
+static int null_socket_send(UNUSED rad_listen_t *listener, REQUEST *request)
+{
+	vp_cursor_t cursor;
+	char *output_file;
+	FILE *fp;
+
+	output_file = request_data_reference(request, (void *)null_socket_send, 0);
+	if (!output_file) {
+		ERROR("No output file for injected packet %d", request->number);
+		return 0;
+	}
+
+	fp = fopen(output_file, "w");
+	if (!fp) {
+		ERROR("Failed to send injected file to %s: %s", output_file, fr_syserror(errno));
+		return 0;
+	}
+
+	if (request->reply->code != 0) {
+		char const *what = "reply";
+		VALUE_PAIR *vp;
+		char buffer[1024];
+
+		if (request->reply->code < FR_MAX_PACKET_CODE) {
+			what = fr_packet_codes[request->reply->code];
+		}
+
+		fprintf(fp, "%s\n", what);
+
+		if (rad_debug_lvl) {
+			RDEBUG("Injected %s packet to host %s port 0 code=%d, id=%d", what,
+			       inet_ntop(request->reply->src_ipaddr.af,
+					 &request->reply->src_ipaddr.ipaddr,
+					 buffer, sizeof(buffer)),
+					 request->reply->code, request->reply->id);
+		}
+
+		RINDENT();
+		for (vp = fr_cursor_init(&cursor, &request->reply->vps);
+		     vp;
+		     vp = fr_cursor_next(&cursor)) {
+			vp_prints(buffer, sizeof(buffer), vp);
+			fprintf(fp, "%s\n", buffer);
+			RDEBUG("%s", buffer);
+		}
+		REXDENT();
+	}
+	fclose(fp);
+
+	return 0;
+}
+
+static rad_listen_t *get_socket(rad_listen_t *listener, int argc,
+			       char *argv[], int *last)
+{
+	rad_listen_t *sock;
+	uint16_t port;
+	int proto = IPPROTO_UDP;
+	fr_ipaddr_t ipaddr;
+
+	if (argc < 2) {
+		cprintf_error(listener, "Must specify <ipaddr> <port> [udp|tcp]\n");
+		return NULL;
+	}
+
+	if (ip_hton(&ipaddr, AF_UNSPEC, argv[0], false) < 0) {
+		cprintf_error(listener, "Failed parsing IP address; %s\n",
+			fr_strerror());
+		return NULL;
+	}
+
+	port = atoi(argv[1]);
+
+	if (last) *last = 2;
+	if (argc > 2) {
+		if (strcmp(argv[2], "udp") == 0) {
+			proto = IPPROTO_UDP;
+			if (last) *last = 3;
+		}
+#ifdef WITH_TCP
+		if (strcmp(argv[2], "tcp") == 0) {
+			proto = IPPROTO_TCP;
+			if (last) *last = 3;
+		}
+#endif
+	}
+
+	sock = listener_find_byipaddr(&ipaddr, port, proto);
+	if (!sock) {
+		cprintf_error(listener, "No such listen section\n");
+		return NULL;
+	}
+
+	return sock;
+}
+
+
+static int command_inject_to(rad_listen_t *listener, int argc, char *argv[])
+{
+	fr_command_socket_t *sock;
+	listen_socket_t *data;
+	rad_listen_t *found;
+
+	if (listener->recv == command_tcp_recv) {
+		cprintf_error(listener, "Cannot inject from command socket over TCP");
+		return CMD_FAIL;
+	}
+
+	found = get_socket(listener, argc, argv, NULL);
+	if (!found) {
+		return 0;
+	}
+
+	sock = listener->data;
+	data = found->data;
+	sock->inject_listener = found;
+	sock->dst_ipaddr = data->my_ipaddr;
+	sock->dst_port = data->my_port;
+
+	return CMD_OK;
+}
+
+static int command_inject_from(rad_listen_t *listener, int argc, char *argv[])
+{
+	RADCLIENT *client;
+	fr_command_socket_t *sock;
+
+	if (argc < 1) {
+		cprintf_error(listener, "No <ipaddr> was given\n");
+		return 0;
+	}
+
+	if (listener->recv == command_tcp_recv) {
+		cprintf_error(listener, "Cannot inject from command socket over TCP");
+		return CMD_FAIL;
+	}
+
+	sock = listener->data;
+	if (!sock->inject_listener) {
+		cprintf_error(listener, "You must specify \"inject to\" before using \"inject from\"\n");
+		return 0;
+	}
+
+	sock->src_ipaddr.af = AF_UNSPEC;
+	if (ip_hton(&sock->src_ipaddr, AF_UNSPEC, argv[0], false) < 0) {
+		cprintf_error(listener, "Failed parsing IP address; %s\n",
+			fr_strerror());
+		return 0;
+	}
+
+	client = client_listener_find(sock->inject_listener, &sock->src_ipaddr,
+				      0);
+	if (!client) {
+		cprintf_error(listener, "No such client %s\n", argv[0]);
+		return 0;
+	}
+	sock->inject_client = client;
+
+	return CMD_OK;
+}
+
+static int command_inject_file(rad_listen_t *listener, int argc, char *argv[])
+{
+	static int inject_id = 0;
+	int ret;
+	bool filedone;
+	fr_command_socket_t *sock;
+	rad_listen_t *fake;
+	RADIUS_PACKET *packet;
+	vp_cursor_t cursor;
+	VALUE_PAIR *vp;
+	FILE *fp;
+	RAD_REQUEST_FUNP fun = NULL;
+	char buffer[2048];
+
+	if (argc < 2) {
+		cprintf_error(listener, "You must specify <input-file> <output-file>\n");
+		return 0;
+	}
+
+	if (listener->recv == command_tcp_recv) {
+		cprintf_error(listener, "Cannot inject from command socket over TCP");
+		return CMD_FAIL;
+	}
+
+	sock = listener->data;
+	if (!sock->inject_listener) {
+		cprintf_error(listener, "You must specify \"inject to\" before using \"inject file\"\n");
+		return 0;
+	}
+
+	if (!sock->inject_client) {
+		cprintf_error(listener, "You must specify \"inject from\" before using \"inject file\"\n");
+		return 0;
+	}
+
+	/*
+	 *	Output files always go to the logging directory.
+	 */
+	snprintf(buffer, sizeof(buffer), "%s/%s", radlog_dir, argv[1]);
+
+	fp = fopen(argv[0], "r");
+	if (!fp ) {
+		cprintf_error(listener, "Failed opening %s: %s\n",
+			argv[0], fr_syserror(errno));
+		return 0;
+	}
+
+	ret = fr_pair_list_afrom_file(NULL, &vp, fp, &filedone);
+	fclose(fp);
+	if (ret < 0) {
+		cprintf_error(listener, "Failed reading attributes from %s: %s\n",
+			argv[0], fr_strerror());
+		return 0;
+	}
+
+	fake = talloc(NULL, rad_listen_t);
+	memcpy(fake, sock->inject_listener, sizeof(*fake));
+
+	/*
+	 *	Re-write the IO for the listener.
+	 */
+	fake->encode = null_socket_dencode;
+	fake->decode = null_socket_dencode;
+	fake->send = null_socket_send;
+
+	packet = rad_alloc(NULL, false);
+	packet->src_ipaddr = sock->src_ipaddr;
+	packet->src_port = 0;
+
+	packet->dst_ipaddr = sock->dst_ipaddr;
+	packet->dst_port = sock->dst_port;
+	packet->vps = vp;
+	packet->id = inject_id++;
+
+	if (fake->type == RAD_LISTEN_AUTH) {
+		packet->code = PW_CODE_ACCESS_REQUEST;
+		fun = rad_authenticate;
+
+	} else {
+#ifdef WITH_ACCOUNTING
+		packet->code = PW_CODE_ACCOUNTING_REQUEST;
+		fun = rad_accounting;
+#else
+		cprintf_error(listener, "This server was built without accounting support.\n");
+		rad_free(&packet);
+		free(fake);
+		return 0;
+#endif
+	}
+
+	if (rad_debug_lvl) {
+		DEBUG("Injecting %s packet from host %s port 0 code=%d, id=%d",
+				fr_packet_codes[packet->code],
+				inet_ntop(packet->src_ipaddr.af,
+					  &packet->src_ipaddr.ipaddr,
+					  buffer, sizeof(buffer)),
+				packet->code, packet->id);
+
+		for (vp = fr_cursor_init(&cursor, &packet->vps);
+		     vp;
+		     vp = fr_cursor_next(&cursor)) {
+			vp_prints(buffer, sizeof(buffer), vp);
+			DEBUG("\t%s", buffer);
+		}
+
+		WARN("INJECTION IS LEAKING MEMORY!");
+	}
+
+	if (!request_receive(NULL, fake, packet, sock->inject_client, fun)) {
+		cprintf_error(listener, "Failed to inject request.  See log file for details\n");
+		rad_free(&packet);
+		free(fake);
+		return 0;
+	}
+
+#if 0
+	/*
+	 *	Remember what the output file is, and remember to
+	 *	delete the fake listener when done.
+	 */
+	request_data_add(request, null_socket_send, 0, talloc_typed_strdup(NULL, buffer), true);
+	request_data_add(request, null_socket_send, 1, fake, true);
+
+#endif
+
+	return CMD_OK;
+}
+
+
+static fr_command_table_t command_table_inject[] = {
+	{ "to", FR_WRITE,
+	  "inject to <ipaddr> <port> - Inject packets to the destination IP and port.",
+	  command_inject_to, NULL },
+
+	{ "from", FR_WRITE,
+	  "inject from <ipaddr> - Inject packets as if they came from <ipaddr>",
+	  command_inject_from, NULL },
+
+	{ "file", FR_WRITE,
+	  "inject file <input-file> <output-file> - Inject packet from <input-file>, with results sent to <output-file>",
+	  command_inject_file, NULL },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+static fr_command_table_t command_table_debug[] = {
+	{ "condition", FR_WRITE,
+	  "debug condition [condition] - Enable debugging for requests matching [condition]",
+	  command_debug_condition, NULL },
+
+	{ "level", FR_WRITE,
+	  "debug level <number> - Set debug level to <number>.  Higher is more debugging.",
+	  command_debug_level, NULL },
+
+	{ "file", FR_WRITE,
+	  "debug file [filename] - Send all debugging output to [filename]",
+	  command_debug_file, NULL },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+static fr_command_table_t command_table_show_debug[] = {
+	{ "condition", FR_READ,
+	  "show debug condition - Shows current debugging condition.",
+	  command_show_debug_condition, NULL },
+
+	{ "level", FR_READ,
+	  "show debug level - Shows current debugging level.",
+	  command_show_debug_level, NULL },
+
+	{ "file", FR_READ,
+	  "show debug file - Shows current debugging file.",
+	  command_show_debug_file, NULL },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+static fr_command_table_t command_table_show_module[] = {
+	{ "config", FR_READ,
+	  "show module config <module> - show configuration for given module",
+	  command_show_module_config, NULL },
+	{ "flags", FR_READ,
+	  "show module flags <module> - show other module properties",
+	  command_show_module_flags, NULL },
+	{ "list", FR_READ,
+	  "show module list - shows list of loaded modules",
+	  command_show_modules, NULL },
+	{ "methods", FR_READ,
+	  "show module methods <module> - show sections where <module> may be used",
+	  command_show_module_methods, NULL },
+	{ "status", FR_READ,
+	  "show module status <module> - show the module status",
+	  command_show_module_status, NULL },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+static fr_command_table_t command_table_show_client[] = {
+	{ "config", FR_READ,
+	  "show client config <ipaddr> "
+#ifdef WITH_TCP
+	  "[udp|tcp] "
+#endif
+	  "- show configuration for given client",
+	  command_show_client_config, NULL },
+	{ "list", FR_READ,
+	  "show client list [verbose] - shows list of global clients",
+	  command_show_clients, NULL },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+#ifdef WITH_PROXY
+static fr_command_table_t command_table_show_home[] = {
+	{ "list", FR_READ,
+	  "show home_server list [all] - shows list of home servers",
+	  command_show_home_servers, NULL },
+	{ "state", FR_READ,
+	  "show home_server state <ipaddr> <port> [udp|tcp] [src <ipaddr>] - shows state of given home server",
+	  command_show_home_server_state, NULL },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+#endif
+
+
+static fr_command_table_t command_table_show[] = {
+	{ "client", FR_READ,
+	  "show client <command> - do sub-command of client",
+	  NULL, command_table_show_client },
+	{ "config", FR_READ,
+	  "show config <path> - shows the value of configuration option <path>",
+	  command_show_config, NULL },
+	{ "debug", FR_READ,
+	  "show debug <command> - show debug properties",
+	  NULL, command_table_show_debug },
+#ifdef WITH_PROXY
+	{ "home_server", FR_READ,
+	  "show home_server <command> - do sub-command of home_server",
+	  NULL, command_table_show_home },
+#endif
+	{ "module", FR_READ,
+	  "show module <command> - do sub-command of module",
+	  NULL, command_table_show_module },
+	{ "uptime", FR_READ,
+	  "show uptime - shows time at which server started",
+	  command_uptime, NULL },
+	{ "version", FR_READ,
+	  "show version - Prints version of the running server",
+	  command_show_version, NULL },
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+
+static int command_set_module_config(rad_listen_t *listener, int argc, char *argv[])
+{
+	int i, rcode;
+	CONF_PAIR *cp;
+	CONF_SECTION *cs;
+	module_instance_t *mi;
+	CONF_PARSER const *variables;
+	void *data;
+
+	if (argc < 3) {
+		cprintf_error(listener, "No module name or variable was given\n");
+		return 0;
+	}
+
+	cs = cf_section_find("modules");
+	if (!cs) return 0;
+
+	mi = module_find(cs, argv[0]);
+	if (!mi) {
+		cprintf_error(listener, "No such module \"%s\"\n", argv[0]);
+		return 0;
+	}
+
+	if ((mi->entry->module->type & RLM_TYPE_HUP_SAFE) == 0) {
+		cprintf_error(listener, "Cannot change configuration of module as it is cannot be HUP'd.\n");
+		return 0;
+	}
+
+	variables = cf_section_parse_table(mi->cs);
+	if (!variables) {
+		cprintf_error(listener, "Cannot find configuration for module\n");
+		return 0;
+	}
+
+	rcode = -1;
+	for (i = 0; variables[i].name != NULL; i++) {
+		/*
+		 *	FIXME: Recurse into sub-types somehow...
+		 */
+		if (variables[i].type == PW_TYPE_SUBSECTION) continue;
+
+		if (strcmp(variables[i].name, argv[1]) == 0) {
+			rcode = i;
+			break;
+		}
+	}
+
+	if (rcode < 0) {
+		cprintf_error(listener, "No such variable \"%s\"\n", argv[1]);
+		return 0;
+	}
+
+	i = rcode;		/* just to be safe */
+
+	/*
+	 *	It's not part of the dynamic configuration.  The module
+	 *	needs to re-parse && validate things.
+	 */
+	if (variables[i].data) {
+		cprintf_error(listener, "Variable cannot be dynamically updated\n");
+		return 0;
+	}
+
+	data = ((char *) mi->insthandle) + variables[i].offset;
+
+	cp = cf_pair_find(mi->cs, argv[1]);
+	if (!cp) return 0;
+
+	/*
+	 *	Replace the OLD value in the configuration file with
+	 *	the NEW value.
+	 *
+	 *	FIXME: Parse argv[2] depending on it's data type!
+	 *	If it's a string, look for leading single/double quotes,
+	 *	end then call tokenize functions???
+	 */
+	cf_pair_replace(mi->cs, cp, argv[2]);
+
+	rcode = cf_item_parse(mi->cs, argv[1], variables[i].type, data, argv[2]);
+	if (rcode < 0) {
+		cprintf_error(listener, "Failed to parse value\n");
+		return 0;
+	}
+
+	return CMD_OK;
+}
+
+static int command_set_module_status(rad_listen_t *listener, int argc, char *argv[])
+{
+	CONF_SECTION *cs;
+	module_instance_t *mi;
+
+	if (argc < 2) {
+		cprintf_error(listener, "No module name or status was given\n");
+		return 0;
+	}
+
+	cs = cf_section_find("modules");
+	if (!cs) return 0;
+
+	mi = module_find(cs, argv[0]);
+	if (!mi) {
+		cprintf_error(listener, "No such module \"%s\"\n", argv[0]);
+		return 0;
+	}
+
+
+	if (strcmp(argv[1], "alive") == 0) {
+		mi->force = false;
+
+	} else if (strcmp(argv[1], "dead") == 0) {
+		mi->code = RLM_MODULE_FAIL;
+		mi->force = true;
+
+	} else {
+		int rcode;
+
+		rcode = fr_str2int(mod_rcode_table, argv[1], -1);
+		if (rcode < 0) {
+			cprintf_error(listener, "Unknown status \"%s\"\n", argv[1]);
+			return 0;
+		}
+
+		mi->code = rcode;
+		mi->force = true;
+	}
+
+	return CMD_OK;
+}
+
+#ifdef WITH_STATS
+static char const *elapsed_names[8] = {
+	"1us", "10us", "100us", "1ms", "10ms", "100ms", "1s", "10s"
+};
+
+static int command_print_stats(rad_listen_t *listener, fr_stats_t *stats,
+			       int auth, int server)
+{
+	int i;
+
+	cprintf(listener, "requests\t%" PRIu64 "\n", stats->total_requests);
+	cprintf(listener, "responses\t%" PRIu64 "\n", stats->total_responses);
+
+	if (auth) {
+		cprintf(listener, "accepts\t\t%" PRIu64 "\n",
+			stats->total_access_accepts);
+		cprintf(listener, "rejects\t\t%" PRIu64 "\n",
+			stats->total_access_rejects);
+		cprintf(listener, "challenges\t%" PRIu64 "\n",
+			stats->total_access_challenges);
+	}
+
+	cprintf(listener, "dup\t\t%" PRIu64 "\n", stats->total_dup_requests);
+	cprintf(listener, "invalid\t\t%" PRIu64 "\n", stats->total_invalid_requests);
+	cprintf(listener, "malformed\t%" PRIu64 "\n", stats->total_malformed_requests);
+	cprintf(listener, "bad_authenticator\t%" PRIu64 "\n", stats->total_bad_authenticators);
+	cprintf(listener, "dropped\t\t%" PRIu64 "\n", stats->total_packets_dropped);
+	cprintf(listener, "unknown_types\t%" PRIu64 "\n", stats->total_unknown_types);
+
+	if (server) {
+		cprintf(listener, "timeouts\t%" PRIu64 "\n", stats->total_timeouts);
+	} else {
+		cprintf(listener, "conflicts\t%" PRIu64 "\n", stats->total_conflicts);
+		cprintf(listener, "unresponsive_child\t%" PRIu64 "\n", stats->unresponsive_child);
+	}
+
+	cprintf(listener, "last_packet\t%" PRId64 "\n", (int64_t) stats->last_packet);
+	for (i = 0; i < 8; i++) {
+		cprintf(listener, "elapsed.%s\t%" PRIu64 "\n",
+			elapsed_names[i], stats->elapsed[i]);
+	}
+
+	return CMD_OK;
+}
+
+
+#ifdef HAVE_PTHREAD_H
+static int command_stats_queue(rad_listen_t *listener, UNUSED int argc, UNUSED char *argv[])
+{
+	int array[RAD_LISTEN_MAX], pps[2];
+
+	thread_pool_queue_stats(array, pps);
+
+	cprintf(listener, "queue_len_internal\t%d\n", array[0]);
+	cprintf(listener, "queue_len_proxy\t\t%d\n", array[1]);
+	cprintf(listener, "queue_len_auth\t\t%d\n", array[2]);
+	cprintf(listener, "queue_len_acct\t\t%d\n", array[3]);
+	cprintf(listener, "queue_len_detail\t%d\n", array[4]);
+
+	cprintf(listener, "queue_pps_in\t\t%d\n", pps[0]);
+	cprintf(listener, "queue_pps_out\t\t%d\n", pps[1]);
+
+	return CMD_OK;
+}
+
+static int command_stats_threads(rad_listen_t *listener, UNUSED int argc, UNUSED char *argv[])
+{
+	int stats[3];
+
+	thread_pool_thread_stats(stats);
+
+	cprintf(listener, "threads_active\t%d\n", stats[0]);
+	cprintf(listener, "threads_total\t\t%d\n", stats[1]);
+	cprintf(listener, "threads_max\t\t%d\n", stats[2]);
+
+	return CMD_OK;
+}
+#endif
+
+#ifndef NDEBUG
+static int command_stats_memory(rad_listen_t *listener, int argc, char *argv[])
+{
+
+	if (!main_config.debug_memory || !main_config.memory_report) {
+		cprintf(listener, "No memory debugging was enabled.\n");
+		return CMD_OK;
+	}
+
+	if (argc == 0) goto fail;
+
+	if (strcmp(argv[0], "total") == 0) {
+		cprintf(listener, "%zd\n", talloc_total_size(NULL));
+		return CMD_OK;
+	}
+
+	if (strcmp(argv[0], "blocks") == 0) {
+		cprintf(listener, "%zd\n", talloc_total_blocks(NULL));
+		return CMD_OK;
+	}
+
+	if (strcmp(argv[0], "full") == 0) {
+		cprintf(listener, "see stdout of the server for the full report.\n");
+		fr_log_talloc_report(NULL);
+		return CMD_OK;
+	}
+
+fail:
+	cprintf_error(listener, "Must use 'stats memory [blocks|full|total]'\n");
+	return CMD_FAIL;
+}
+#endif
+
+#ifdef WITH_DETAIL
+static FR_NAME_NUMBER state_names[] = {
+	{ "unopened", STATE_UNOPENED },
+	{ "unlocked", STATE_UNLOCKED },
+	{ "header", STATE_HEADER },
+	{ "reading", STATE_READING },
+	{ "queued", STATE_QUEUED },
+	{ "running", STATE_RUNNING },
+	{ "no-reply", STATE_NO_REPLY },
+	{ "replied", STATE_REPLIED },
+
+	{ NULL, 0 }
+};
+
+static int command_stats_detail(rad_listen_t *listener, int argc, char *argv[])
+{
+	rad_listen_t *this;
+	listen_detail_t *data, *needle;
+	struct stat buf;
+
+	if (argc == 0) {
+		cprintf_error(listener, "Must specify <filename>\n");
+		return 0;
+	}
+
+	data = NULL;
+	for (this = main_config.listen; this != NULL; this = this->next) {
+		if (this->type != RAD_LISTEN_DETAIL) continue;
+
+		needle = this->data;
+		if (!strcmp(argv[0], needle->filename)) {
+			data = needle;
+			break;
+		}
+	}
+
+	if (!data) {
+		cprintf_error(listener, "No detail file listener\n");
+		return 0;
+	}
+
+	cprintf(listener, "state\t%s\n",
+		fr_int2str(state_names, data->state, "?"));
+
+	if ((data->state == STATE_UNOPENED) ||
+	    (data->state == STATE_UNLOCKED)) {
+		return CMD_OK;
+	}
+
+	/*
+	 *	Race conditions: file might not exist.
+	 */
+	if (stat(data->filename_work, &buf) < 0) {
+		cprintf(listener, "packets\t0\n");
+		cprintf(listener, "tries\t0\n");
+		cprintf(listener, "offset\t0\n");
+		cprintf(listener, "size\t0\n");
+		return CMD_OK;
+	}
+
+	cprintf(listener, "packets\t%d\n", data->packets);
+	cprintf(listener, "tries\t%d\n", data->tries);
+	cprintf(listener, "offset\t%u\n", (unsigned int) data->offset);
+	cprintf(listener, "size\t%u\n", (unsigned int) buf.st_size);
+
+	return CMD_OK;
+}
+#endif
+
+#ifdef WITH_PROXY
+static int command_stats_home_server(rad_listen_t *listener, int argc, char *argv[])
+{
+	home_server_t *home;
+
+	if (argc == 0) {
+		cprintf_error(listener, "Must specify [auth|acct|coa|disconnect] OR <ipaddr> <port>\n");
+		return 0;
+	}
+
+	if (argc == 1) {
+		if (strcmp(argv[0], "auth") == 0) {
+			return command_print_stats(listener,
+						   &proxy_auth_stats, 1, 1);
+		}
+
+#ifdef WITH_ACCOUNTING
+		if (strcmp(argv[0], "acct") == 0) {
+			return command_print_stats(listener,
+						   &proxy_acct_stats, 0, 1);
+		}
+#endif
+
+#ifdef WITH_ACCOUNTING
+		if (strcmp(argv[0], "coa") == 0) {
+			return command_print_stats(listener,
+						   &proxy_coa_stats, 0, 1);
+		}
+#endif
+
+#ifdef WITH_ACCOUNTING
+		if (strcmp(argv[0], "disconnect") == 0) {
+			return command_print_stats(listener,
+						   &proxy_dsc_stats, 0, 1);
+		}
+#endif
+
+		cprintf_error(listener, "Should specify [auth|acct|coa|disconnect]\n");
+		return 0;
+	}
+
+	home = get_home_server(listener, argc, argv, NULL);
+	if (!home) return 0;
+
+	command_print_stats(listener, &home->stats,
+			    (home->type == HOME_TYPE_AUTH), 1);
+	cprintf(listener, "outstanding\t%d\n", home->currently_outstanding);
+	return CMD_OK;
+}
+#endif
+
+static int command_stats_client(rad_listen_t *listener, int argc, char *argv[])
+{
+	bool auth = true;
+	fr_stats_t *stats;
+	RADCLIENT *client, fake;
+
+	if (argc < 1) {
+		cprintf_error(listener, "Must specify [auth/acct]\n");
+		return 0;
+	}
+
+	if (argc == 1) {
+		/*
+		 *	Global statistics.
+		 */
+		fake.auth = radius_auth_stats;
+#ifdef WITH_ACCOUNTING
+		fake.acct = radius_acct_stats;
+#endif
+#ifdef WITH_COA
+		fake.coa = radius_coa_stats;
+		fake.dsc = radius_dsc_stats;
+#endif
+		client = &fake;
+
+	} else {
+		/*
+		 *	Per-client statistics.
+		 */
+		client = get_client(listener, argc - 1, argv + 1);
+		if (!client) return 0;
+	}
+
+	if (strcmp(argv[0], "auth") == 0) {
+		auth = true;
+		stats = &client->auth;
+
+	} else if (strcmp(argv[0], "acct") == 0) {
+#ifdef WITH_ACCOUNTING
+		auth = false;
+		stats = &client->acct;
+#else
+		cprintf_error(listener, "This server was built without accounting support.\n");
+		return 0;
+#endif
+
+	} else if (strcmp(argv[0], "coa") == 0) {
+#ifdef WITH_COA
+		auth = false;
+		stats = &client->coa;
+#else
+		cprintf_error(listener, "This server was built without CoA support.\n");
+		return 0;
+#endif
+
+	} else if (strcmp(argv[0], "disconnect") == 0) {
+#ifdef WITH_COA
+		auth = false;
+		stats = &client->dsc;
+#else
+		cprintf_error(listener, "This server was built without CoA support.\n");
+		return 0;
+#endif
+
+	} else {
+		cprintf_error(listener, "Unknown statistics type\n");
+		return 0;
+	}
+
+	/*
+	 *	Global results for all client.
+	 */
+	if (argc == 1) {
+#ifdef WITH_ACCOUNTING
+		if (!auth) {
+			return command_print_stats(listener,
+						   &radius_acct_stats, auth, 0);
+		}
+#endif
+		return command_print_stats(listener, &radius_auth_stats, auth, 0);
+	}
+
+	return command_print_stats(listener, stats, auth, 0);
+}
+
+
+static int command_stats_socket(rad_listen_t *listener, int argc, char *argv[])
+{
+	bool auth = true;
+	rad_listen_t *sock;
+
+	sock = get_socket(listener, argc, argv, NULL);
+	if (!sock) return 0;
+
+	if (sock->type != RAD_LISTEN_AUTH) auth = false;
+
+	return command_print_stats(listener, &sock->stats, auth, 0);
+}
+
+static int command_stats_pool(rad_listen_t *listener, UNUSED int argc, UNUSED char *argv[])
+{
+	CONF_SECTION *cs;
+	module_instance_t *mi;
+	fr_connection_pool_stats_t const *stats;
+
+	if (argc < 1) {
+		cprintf_error(listener, "Must specify <name>\n");
+		return CMD_FAIL;
+	}
+
+	cs = cf_section_find("modules");
+	if (!cs) return CMD_FAIL;
+
+	mi = module_find(cs, argv[0]);
+	if (!mi) {
+		cprintf_error(listener, "No such module \"%s\"\n", argv[0]);
+		return CMD_FAIL;
+	}
+
+	stats = fr_connection_pool_stats(mi->cs);
+	if (!stats) {
+		cprintf_error(listener, "Module %s has no pool statistics", argv[0]);
+		return CMD_FAIL;
+	}
+
+	cprintf(listener, "last_checked\t\t%zu\n", stats->last_checked);
+	cprintf(listener, "last_opened\t\t%zu\n", stats->last_opened);
+	cprintf(listener, "last_closed\t\t%zu\n", stats->last_closed);
+	cprintf(listener, "last_failed\t\t%zu\n", stats->last_failed);
+	cprintf(listener, "last_throttled\t\t%zu\n", stats->last_throttled);
+	cprintf(listener, "total_opened\t\t%" PRIu64 "\n", stats->opened);
+	cprintf(listener, "total_closed\t\t%" PRIu64 "\n", stats->closed);
+	cprintf(listener, "total_failed\t\t%" PRIu64 "\n", stats->failed);
+	cprintf(listener, "num_open\t\t%u\n", stats->num);
+	cprintf(listener, "num_in_use\t\t%u\n", stats->active);
+
+	return CMD_OK;
+}
+#endif	/* WITH_STATS */
+
+
+#ifdef WITH_DYNAMIC_CLIENTS
+static int command_add_client_file(rad_listen_t *listener, int argc, char *argv[])
+{
+	RADCLIENT *c;
+
+	if (argc < 1) {
+		cprintf_error(listener, "<file> is required\n");
+		return 0;
+	}
+
+	/*
+	 *	Read the file and generate the client.
+	 */
+	c = client_read(argv[0], false, false);
+	if (!c) {
+		cprintf_error(listener, "Unknown error reading client file.\n");
+		return 0;
+	}
+
+	if (!client_add(NULL, c)) {
+		cprintf_error(listener, "Unknown error inserting new client.\n");
+		client_free(c);
+		return 0;
+	}
+
+	return CMD_OK;
+}
+
+
+static int command_del_client(rad_listen_t *listener, int argc, char *argv[])
+{
+	RADCLIENT *client;
+
+	client = get_client(listener, argc, argv);
+	if (!client) return 0;
+
+	if (!client->dynamic) {
+		cprintf_error(listener, "Client %s was not dynamically defined.\n", argv[0]);
+		return 0;
+	}
+
+	/*
+	 *	DON'T delete it.  Instead, mark it as "dead now".  The
+	 *	next time we receive a packet for the client, it will
+	 *	be deleted.
+	 *
+	 *	If we don't receive a packet from it, the client
+	 *	structure will stick around for a while.  Oh well...
+	 */
+	client->lifetime = 1;
+
+	return CMD_OK;
+}
+
+
+static int command_del_home_server(rad_listen_t *listener, int argc, char *argv[])
+{
+	if (argc < 2) {
+		cprintf_error(listener, "<name> and <type> are required\n");
+		return 0;
+	}
+
+	if (home_server_delete(argv[0], argv[1]) < 0) {
+		cprintf_error(listener, "Failed deleted home_server %s - %s\n", argv[1], fr_strerror());
+		return 0;
+	}
+
+	return CMD_OK;
+}
+
+static int command_add_home_server_file(rad_listen_t *listener, int argc, char *argv[])
+{
+	if (argc < 1) {
+		cprintf_error(listener, "<file> is required\n");
+		return 0;
+	}
+
+	if (home_server_afrom_file(argv[0]) < 0) {
+		cprintf_error(listener, "Unable to add home server - %s\n", fr_strerror());
+		return 0;
+	}
+
+	return CMD_OK;
+}
+
+static fr_command_table_t command_table_del_client[] = {
+	{ "ipaddr", FR_WRITE,
+	  "del client ipaddr <ipaddr> [udp|tcp] [listen <ipaddr> <port>] - Delete a dynamically created client",
+	  command_del_client, NULL },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+static fr_command_table_t command_table_del_home_server[] = {
+	{ "file", FR_WRITE,
+	  "del home_server file <name> [auth|acct|coa] - Delete a dynamically created home_server",
+	  command_del_home_server, NULL },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+static fr_command_table_t command_table_del[] = {
+	{ "client", FR_WRITE,
+	  "del client <command> - Delete client configuration commands",
+	  NULL, command_table_del_client },
+
+	{ "home_server", FR_WRITE,
+	  "del home_server <command> - Delete home_server configuration commands",
+	  NULL, command_table_del_home_server },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+static fr_command_table_t command_table_add_client[] = {
+	{ "file", FR_WRITE,
+	  "add client file <filename> - Add new client definition from <filename>",
+	  command_add_client_file, NULL },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+static fr_command_table_t command_table_add_home_server[] = {
+	{ "file", FR_WRITE,
+	  "add home_server file <filename> - Add new home serverdefinition from <filename>",
+	  command_add_home_server_file, NULL },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+static fr_command_table_t command_table_add[] = {
+	{ "client", FR_WRITE,
+	  "add client <command> - Add client configuration commands",
+	  NULL, command_table_add_client },
+
+	{ "home_server", FR_WRITE,
+	  "add home_server <command> - Add home server configuration commands",
+	  NULL, command_table_add_home_server },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+#endif
+
+#ifdef WITH_PROXY
+static fr_command_table_t command_table_set_home[] = {
+	{ "state", FR_WRITE,
+	  "set home_server state <ipaddr> <port> [udp|tcp] [src <ipaddr>] [alive|dead|down] - set state for given home server",
+	  command_set_home_server_state, NULL },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+#endif
+
+static fr_command_table_t command_table_set_module[] = {
+	{ "config", FR_WRITE,
+	  "set module config <module> variable value - set configuration for <module>",
+	  command_set_module_config, NULL },
+
+	{ "status", FR_WRITE,
+	  "set module status <module> [alive|...] - set the module status to be alive (operating normally), or force a particular code (ok,fail, etc.)",
+	  command_set_module_status, NULL },
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+
+static fr_command_table_t command_table_set[] = {
+	{ "module", FR_WRITE,
+	  "set module <command> - set module commands",
+	  NULL, command_table_set_module },
+#ifdef WITH_PROXY
+	{ "home_server", FR_WRITE,
+	  "set home_server <command> - set home server commands",
+	  NULL, command_table_set_home },
+#endif
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+
+#ifdef WITH_STATS
+static fr_command_table_t command_table_stats[] = {
+	{ "client", FR_READ,
+	  "stats client [auth/acct/coa] <ipaddr> [udp|tcp] [listen <ipaddr> <port>] "
+	  "- show statistics for given client, or for all clients (auth or acct)",
+	  command_stats_client, NULL },
+
+#ifdef WITH_DETAIL
+	{ "detail", FR_READ,
+	  "stats detail <filename> - show statistics for the given detail file",
+	  command_stats_detail, NULL },
+#endif
+
+#ifdef WITH_PROXY
+	{ "home_server", FR_READ,
+	  "stats home_server [<ipaddr>|auth|acct|coa|disconnect] <port> [udp|tcp] [src <ipaddr>] - show statistics for given home server (ipaddr and port), or for all home servers (auth or acct)",
+	  command_stats_home_server, NULL },
+#endif
+
+	{ "pool", FR_READ,
+	  "stats pool <name> "
+	  "- show pool statistics for given module",
+	  command_stats_pool, NULL },
+
+#ifdef HAVE_PTHREAD_H
+	{ "queue", FR_READ,
+	  "stats queue - show statistics for packet queues",
+	  command_stats_queue, NULL },
+	{ "threads", FR_READ,
+	  "stats threads - show statistics for the worker threads",
+	  command_stats_threads, NULL },
+#endif
+
+	{ "socket", FR_READ,
+	  "stats socket <ipaddr> <port> [udp|tcp] "
+	  "- show statistics for given socket",
+	  command_stats_socket, NULL },
+
+#ifndef NDEBUG
+	{ "memory", FR_READ,
+	  "stats memory [blocks|full|total] - show statistics on used memory",
+	  command_stats_memory, NULL },
+#endif
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+#endif
+
+static fr_command_table_t command_table[] = {
+#ifdef WITH_DYNAMIC_CLIENTS
+	{ "add", FR_WRITE, NULL, NULL, command_table_add },
+#endif
+	{ "debug", FR_WRITE,
+	  "debug <command> - debugging commands",
+	  NULL, command_table_debug },
+#ifdef WITH_DYNAMIC_CLIENTS
+	{ "del", FR_WRITE, NULL, NULL, command_table_del },
+#endif
+	{ "hup", FR_WRITE,
+	  "hup [module] - sends a HUP signal to the server, or optionally to one module",
+	  command_hup, NULL },
+	{ "inject", FR_WRITE,
+	  "inject <command> - commands to inject packets into a running server",
+	  NULL, command_table_inject },
+	{ "reconnect", FR_READ,
+	  "reconnect - reconnect to a running server",
+	  NULL, NULL },		/* just here for "help" */
+	{ "terminate", FR_WRITE,
+	  "terminate - terminates the server, and cause it to exit",
+	  command_terminate, NULL },
+	{ "set", FR_WRITE, NULL, NULL, command_table_set },
+	{ "show",  FR_READ, NULL, NULL, command_table_show },
+#ifdef WITH_STATS
+	{ "stats",  FR_READ, NULL, NULL, command_table_stats },
+#endif
+
+	{ NULL, 0, NULL, NULL, NULL }
+};
+
+
+static void command_socket_free(rad_listen_t *this)
+{
+	fr_command_socket_t *cmd = this->data;
+
+	/*
+	 *	If it's a TCP socket, don't do anything.
+	 */
+	if (cmd->magic != COMMAND_SOCKET_MAGIC) {
+		return;
+	}
+
+	if (!cmd->copy) return;
+	unlink(cmd->copy);
+}
+
+
+/*
+ *	Parse the unix domain sockets.
+ *
+ *	FIXME: TCP + SSL, after RadSec is in.
+ */
+static int command_socket_parse_unix(CONF_SECTION *cs, rad_listen_t *this)
+{
+	fr_command_socket_t *sock;
+
+	if (check_config) return 0;
+
+	sock = this->data;
+
+	if (cf_section_parse(cs, sock, command_config) < 0) return -1;
+
+	/*
+	 *	Can't get uid or gid of connecting user, so can't do
+	 *	peercred authentication.
+	 */
+#ifndef HAVE_GETPEEREID
+	if (sock->peercred && (sock->uid_name || sock->gid_name)) {
+		ERROR("System does not support uid or gid authentication for sockets");
+		return -1;
+	}
+#endif
+
+	sock->magic = COMMAND_SOCKET_MAGIC;
+	sock->copy = NULL;
+	if (sock->path) sock->copy = talloc_typed_strdup(sock, sock->path);
+
+	if (sock->uid_name) {
+		struct passwd *pwd;
+
+		if (rad_getpwnam(cs, &pwd, sock->uid_name) < 0) {
+			ERROR("Failed getting uid for %s: %s", sock->uid_name, fr_strerror());
+			return -1;
+		}
+		sock->uid = pwd->pw_uid;
+		talloc_free(pwd);
+	} else {
+		sock->uid = -1;
+	}
+
+	if (sock->gid_name) {
+		if (rad_getgid(cs, &sock->gid, sock->gid_name) < 0) {
+			ERROR("Failed getting gid for %s: %s", sock->gid_name, fr_strerror());
+			return -1;
+		}
+	} else {
+		sock->gid = -1;
+	}
+
+	if (!sock->mode_name) {
+		sock->co.mode = FR_READ;
+	} else {
+		sock->co.mode = fr_str2int(mode_names, sock->mode_name, 0);
+		if (!sock->co.mode) {
+			ERROR("Invalid mode name \"%s\"",
+			       sock->mode_name);
+			return -1;
+		}
+	}
+
+	if (sock->peercred) {
+		this->fd = fr_server_domain_socket_peercred(sock->path, sock->uid, sock->gid);
+	} else {
+		uid_t uid = sock->uid;
+		gid_t gid = sock->gid;
+
+		if (uid == ((uid_t)-1)) uid = 0;
+		if (gid == ((gid_t)-1)) gid = 0;
+
+		this->fd = fr_server_domain_socket_perm(sock->path, uid, gid);
+	}
+
+	if (this->fd < 0) {
+		ERROR("Failed creating control socket \"%s\": %s", sock->path, fr_strerror());
+		if (sock->copy) talloc_free(sock->copy);
+		sock->copy = NULL;
+		return -1;
+	}
+
+	return 0;
+}
+
+static int command_socket_parse(CONF_SECTION *cs, rad_listen_t *this)
+{
+	int rcode;
+	CONF_PAIR const *cp;
+	listen_socket_t *sock;
+
+	cp = cf_pair_find(cs, "socket");
+	if (cp) return command_socket_parse_unix(cs, this);
+
+	rcode = common_socket_parse(cs, this);
+	if (rcode < 0) return -1;
+
+#ifdef WITH_TLS
+	if (this->tls) {
+		cf_log_err_cs(cs,
+			   "TLS is not supported for control sockets");
+		return -1;
+	}
+#endif
+
+	sock = this->data;
+	if (sock->proto != IPPROTO_TCP) {
+		cf_log_err_cs(cs,
+			   "UDP is not supported for control sockets");
+		return -1;
+	}
+
+	return 0;
+}
+
+static int command_socket_print(rad_listen_t const *this, char *buffer, size_t bufsize)
+{
+	fr_command_socket_t *sock = this->data;
+
+	if (sock->magic != COMMAND_SOCKET_MAGIC) {
+		return common_socket_print(this, buffer, bufsize);
+	}
+
+	snprintf(buffer, bufsize, "command file %s", sock->path);
+	return 1;
+}
+
+
+/*
+ *	String split routine.  Splits an input string IN PLACE
+ *	into pieces, based on spaces.
+ */
+static int str2argvX(char *str, char **argv, int max_argc)
+{
+	int argc = 0;
+
+	while (*str) {
+		if (argc >= max_argc) return argc;
+
+		/*
+		 *	Chop out comments early.
+		 */
+		if (*str == '#') {
+			*str = '\0';
+			break;
+		}
+
+		while ((*str == ' ') ||
+		       (*str == '\t') ||
+		       (*str == '\r') ||
+		       (*str == '\n')) *(str++) = '\0';
+
+		if (!*str) return argc;
+
+		argv[argc++] = str;
+
+		if ((*str == '\'') || (*str == '"')) {
+			char quote = *str;
+			char *p = str + 1;
+
+			while (true) {
+				if (!*p) return -1;
+
+				if (*p == quote) {
+					str = p + 1;
+					break;
+				}
+
+				/*
+				 *	Handle \" and nothing else.
+				 */
+				if (*p == '\\') {
+					p += 2;
+					continue;
+				}
+
+				p++;
+			}
+		}
+
+		while (*str &&
+		       (*str != ' ') &&
+		       (*str != '\t') &&
+		       (*str != '\r') &&
+		       (*str != '\n')) str++;
+	}
+
+	return argc;
+}
+
+static void print_help(rad_listen_t *listener, int argc, char *argv[],
+		       fr_command_table_t *table, int recursive)
+{
+	int i;
+
+	/* this should never happen, but if it does then just return gracefully */
+	if (!table) return;
+
+	for (i = 0; table[i].command != NULL; i++) {
+		if (argc > 0) {
+			if (strcmp(table[i].command, argv[0]) == 0) {
+				if (table[i].table) {
+					print_help(listener, argc - 1, argv + 1, table[i].table, recursive);
+				} else {
+					if (table[i].help) {
+						cprintf(listener, "%s\n", table[i].help);
+					}
+				}
+				return;
+			}
+
+			continue;
+		}
+
+		if (table[i].help) {
+			cprintf(listener, "%s\n",
+				table[i].help);
+		} else {
+			cprintf(listener, "%s <command> - do sub-command of %s\n",
+				table[i].command, table[i].command);
+		}
+
+		if (recursive && table[i].table) {
+			print_help(listener, 0, NULL, table[i].table, recursive);
+		}
+	}
+}
+
+#define MAX_ARGV (16)
+
+/*
+ *	Check if an incoming request is "ok"
+ *
+ *	It takes packets, not requests.  It sees if the packet looks
+ *	OK.  If so, it does a number of sanity checks on it.
+ */
+static int command_domain_recv_co(rad_listen_t *listener, fr_cs_buffer_t *co)
+{
+	int i;
+	uint32_t status;
+	ssize_t r, len;
+	int argc;
+	fr_channel_type_t channel;
+	char *my_argv[MAX_ARGV], **argv;
+	fr_command_table_t *table;
+	uint8_t *command;
+
+	r = fr_channel_drain(listener->fd, &channel, co->buffer, sizeof(co->buffer) - 1, &command, co->offset);
+
+	if (r <= 0) {
+	do_close:
+		command_close_socket(listener);
+		return 0;
+	}
+
+	/*
+	 *	We need more data.  Go read it.
+	 */
+	if (channel == FR_CHANNEL_WANT_MORE) {
+		co->offset = r;
+		return 0;
+	}
+
+	status = 0;
+	command[r] = '\0';
+	DEBUG("radmin> %s", command);
+
+	argc = str2argvX((char *) command, my_argv, MAX_ARGV);
+	if (argc == 0) goto do_next; /* empty strings are OK */
+
+	if (argc < 0) {
+		cprintf_error(listener, "Failed parsing command.\n");
+		goto do_next;
+	}
+
+	argv = my_argv;
+
+	for (len = 0; len <= co->offset; len++) {
+		if (command[len] < 0x20) {
+			command[len] = '\0';
+			break;
+		}
+	}
+
+	/*
+	 *	Hard-code exit && quit.
+	 */
+	if ((strcmp(argv[0], "exit") == 0) ||
+	    (strcmp(argv[0], "quit") == 0)) goto do_close;
+
+	table = command_table;
+ retry:
+	len = 0;
+	for (i = 0; table[i].command != NULL; i++) {
+		if (strcmp(table[i].command, argv[0]) == 0) {
+			/*
+			 *	Check permissions.
+			 */
+			if (((co->mode & FR_WRITE) == 0) &&
+			    ((table[i].mode & FR_WRITE) != 0)) {
+				cprintf_error(listener, "You do not have write permission.  See \"mode = rw\" in the \"listen\" section for this socket.\n");
+				goto do_next;
+			}
+
+			if (table[i].table) {
+				/*
+				 *	This is the last argument, but
+				 *	there's a sub-table.  Print help.
+				 *
+				 */
+				if (argc == 1) {
+					table = table[i].table;
+					goto do_help;
+				}
+
+				argc--;
+				argv++;
+				table = table[i].table;
+				goto retry;
+			}
+
+			if ((argc == 2) && (strcmp(argv[1], "?") == 0)) goto do_help;
+
+			if (!table[i].func) {
+				cprintf_error(listener, "Invalid command\n");
+				goto do_next;
+			}
+
+			status = table[i].func(listener, argc - 1, argv + 1);
+			goto do_next;
+		}
+	}
+
+	/*
+	 *	No such command
+	 */
+	if (!len) {
+		if ((strcmp(argv[0], "help") == 0) ||
+		    (strcmp(argv[0], "?") == 0)) {
+			int recursive;
+
+		do_help:
+			if ((argc > 1) && (strcmp(argv[1], "-r") == 0)) {
+				recursive = true;
+				argc--;
+				argv++;
+			} else {
+				recursive = false;
+			}
+
+			print_help(listener, argc - 1, argv + 1, table, recursive);
+			goto do_next;
+		}
+
+		cprintf_error(listener, "Unknown command \"%s\"\n",
+			      argv[0]);
+	}
+
+ do_next:
+	r = fr_channel_write(listener->fd, FR_CHANNEL_CMD_STATUS, &status, sizeof(status));
+	if (r <= 0) goto do_close;
+
+	return 0;
+}
+
+
+/*
+ *	Write 32-bit magic number && version information.
+ */
+static int command_write_magic(int newfd,
+#ifndef WITH_TCP
+			       UNUSED
+#endif
+			       listen_socket_t *sock
+	)
+{
+	ssize_t r;
+	uint32_t magic;
+	fr_channel_type_t channel;
+	char buffer[16];
+
+	r = fr_channel_read(newfd, &channel, buffer, 8);
+	if (r <= 0) {
+	do_close:
+		ERROR("Cannot talk to socket: %s",
+		      fr_syserror(errno));
+		return -1;
+	}
+
+	magic = htonl(0xf7eead16);
+	if ((r != 8) || (channel != FR_CHANNEL_INIT_ACK) ||
+	    (memcmp(&magic, &buffer, sizeof(magic)) != 0)) {
+		ERROR("Incompatible versions");
+		return -1;
+	}
+
+	r = fr_channel_write(newfd, FR_CHANNEL_INIT_ACK, buffer, 8);
+	if (r <= 0) goto do_close;
+
+#ifdef WITH_TCP
+	/*
+	 *	Write an initial challenge
+	 */
+	if (sock) {
+		int i;
+		fr_cs_buffer_t *co;
+
+		co = talloc_zero(sock, fr_cs_buffer_t);
+		sock->packet = (void *) co;
+
+		for (i = 0; i < 16; i++) {
+			co->buffer[i] = fr_rand();
+		}
+
+		r = fr_channel_write(newfd, FR_CHANNEL_AUTH_CHALLENGE, co->buffer, 16);
+		if (r <= 0) goto do_close;
+	}
+#endif
+
+	return 0;
+}
+
+#ifdef WITH_TCP
+static int command_tcp_recv(rad_listen_t *this)
+{
+	ssize_t r;
+	listen_socket_t *sock = this->data;
+	fr_cs_buffer_t *co = (void *) sock->packet;
+	fr_channel_type_t channel;
+
+	if (!co) {
+	do_close:
+		command_close_socket(this);
+		return 0;
+	}
+
+	if (!co->auth) {
+		uint8_t expected[16];
+
+		r = fr_channel_read(this->fd, &channel, co->buffer, 16);
+		if ((r != 16) || (channel != FR_CHANNEL_AUTH_RESPONSE)) {
+			goto do_close;
+		}
+
+		fr_hmac_md5(expected, (void const *) sock->client->secret,
+			    strlen(sock->client->secret),
+			    (uint8_t *) co->buffer, 16);
+
+		if (rad_digest_cmp(expected,
+				   (uint8_t *) co->buffer + 16, 16 != 0)) {
+			ERROR("radmin failed challenge: Closing socket");
+			goto do_close;
+		}
+
+		co->auth = true;
+		co->offset = 0;
+	}
+
+	return command_domain_recv_co(this, co);
+}
+
+
+/*
+ *	Should never be called.  The functions should just call write().
+ */
+static int command_tcp_send(UNUSED rad_listen_t *listener, UNUSED REQUEST *request)
+{
+	return 0;
+}
+#endif
+
+static int command_domain_recv(rad_listen_t *listener)
+{
+	fr_command_socket_t *sock = listener->data;
+
+	return command_domain_recv_co(listener, &sock->co);
+}
+
+static int command_domain_accept(rad_listen_t *listener)
+{
+	int newfd;
+	rad_listen_t *this;
+	socklen_t salen;
+	struct sockaddr_storage src;
+	fr_command_socket_t *sock = listener->data;
+
+	salen = sizeof(src);
+
+	DEBUG2(" ... new connection request on command socket");
+
+	newfd = accept(listener->fd, (struct sockaddr *) &src, &salen);
+	if (newfd < 0) {
+		/*
+		 *	Non-blocking sockets must handle this.
+		 */
+		if (errno == EWOULDBLOCK) {
+			return 0;
+		}
+
+		DEBUG2(" ... failed to accept connection");
+		return 0;
+	}
+
+#ifdef HAVE_GETPEEREID
+	/*
+	 *	Perform user authentication.
+	 */
+	if (sock->peercred && (sock->uid_name || sock->gid_name)) {
+		uid_t uid;
+		gid_t gid;
+
+		if (getpeereid(newfd, &uid, &gid) < 0) {
+			ERROR("Failed getting peer credentials for %s: %s",
+			       sock->path, fr_syserror(errno));
+			close(newfd);
+			return 0;
+		}
+
+		/*
+		 *	Only do UID checking if the caller is
+		 *	non-root.  The superuser can do anything, so
+		 *	we might as well let them.
+		 */
+		if (uid != 0) do {
+			/*
+			 *	Allow entry if UID or GID matches.
+			 */
+			if (sock->uid_name && (sock->uid == uid)) break;
+			if (sock->gid_name && (sock->gid == gid)) break;
+
+			if (sock->uid_name && (sock->uid != uid)) {
+				ERROR("Unauthorized connection to %s from uid %ld",
+
+				       sock->path, (long int) uid);
+				close(newfd);
+				return 0;
+			}
+
+			if (sock->gid_name && (sock->gid != gid)) {
+				ERROR("Unauthorized connection to %s from gid %ld",
+				       sock->path, (long int) gid);
+				close(newfd);
+				return 0;
+			}
+
+		} while (0);
+	}
+#endif
+
+	if (command_write_magic(newfd, NULL) < 0) {
+		close(newfd);
+		return 0;
+	}
+
+	/*
+	 *	Add the new listener.
+	 */
+	this = listen_alloc(listener, listener->type);
+	if (!this) return 0;
+
+	/*
+	 *	Copy everything, including the pointer to the socket
+	 *	information.
+	 */
+	sock = this->data;
+	memcpy(this, listener, sizeof(*this));
+	this->status = RAD_LISTEN_STATUS_INIT;
+	this->next = NULL;
+	this->data = sock;	/* fix it back */
+
+	sock->magic = COMMAND_SOCKET_MAGIC;
+	sock->user[0] = '\0';
+	sock->path = ((fr_command_socket_t *) listener->data)->path;
+	sock->co.offset = 0;
+	sock->co.mode = ((fr_command_socket_t *) listener->data)->co.mode;
+
+	this->fd = newfd;
+	this->recv = command_domain_recv;
+
+	/*
+	 *	Tell the event loop that we have a new FD
+	 */
+	radius_update_listener(this);
+
+	return 0;
+}
+
+
+/*
+ *	Send an authentication response packet
+ */
+static int command_domain_send(UNUSED rad_listen_t *listener,
+			       UNUSED REQUEST *request)
+{
+	return 0;
+}
+
+
+static int command_socket_encode(UNUSED rad_listen_t *listener,
+				 UNUSED REQUEST *request)
+{
+	return 0;
+}
+
+
+static int command_socket_decode(UNUSED rad_listen_t *listener,
+				 UNUSED REQUEST *request)
+{
+	return 0;
+}
+
+#endif /* WITH_COMMAND_SOCKET */
diff --git a/src/main/conffile.c b/src/main/conffile.c
new file mode 100644
index 0000000..7bb206c
--- /dev/null
+++ b/src/main/conffile.c
@@ -0,0 +1,3821 @@
+/*
+ * conffile.c	Read the radiusd.conf file.
+ *
+ *		Yep I should learn to use lex & yacc, or at least
+ *		write a decent parser. I know how to do that, really :)
+ *		miquels@cistron.nl
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2000,2006  The FreeRADIUS server project
+ * Copyright 2000  Miquel van Smoorenburg <miquels@cistron.nl>
+ * Copyright 2000  Alan DeKok <aland@ox.org>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/parser.h>
+#include <freeradius-devel/rad_assert.h>
+
+#ifdef HAVE_DIRENT_H
+#include <dirent.h>
+#endif
+
+#ifdef HAVE_SYS_STAT_H
+#include <sys/stat.h>
+#endif
+
+#include <ctype.h>
+
+bool check_config = false;
+
+typedef enum conf_property {
+	CONF_PROPERTY_INVALID = 0,
+	CONF_PROPERTY_NAME,
+	CONF_PROPERTY_INSTANCE,
+} CONF_PROPERTY;
+
+static const FR_NAME_NUMBER conf_property_name[] = {
+	{ "name",	CONF_PROPERTY_NAME},
+	{ "instance",	CONF_PROPERTY_INSTANCE},
+
+	{  NULL , -1 }
+};
+
+typedef enum conf_type {
+	CONF_ITEM_INVALID = 0,
+	CONF_ITEM_PAIR,
+	CONF_ITEM_SECTION,
+	CONF_ITEM_DATA
+} CONF_ITEM_TYPE;
+
+struct conf_item {
+	struct conf_item *next;		//!< Sibling.
+	struct conf_part *parent;	//!< Parent.
+	int lineno;			//!< The line number the config item began on.
+	char const *filename;		//!< The file the config item was parsed from.
+	CONF_ITEM_TYPE type;		//!< Whether the config item is a config_pair, conf_section or conf_data.
+};
+
+/** Configuration AVP similar to a VALUE_PAIR
+ *
+ */
+struct conf_pair {
+	CONF_ITEM	item;
+	char const	*attr;		//!< Attribute name
+	char const	*value;		//!< Attribute value
+	FR_TOKEN	op;		//!< Operator e.g. =, :=
+	FR_TOKEN	lhs_type;	//!< Name quoting style T_(DOUBLE|SINGLE|BACK)_QUOTE_STRING or T_BARE_WORD.
+	FR_TOKEN	rhs_type;	//!< Value Quoting style T_(DOUBLE|SINGLE|BACK)_QUOTE_STRING or T_BARE_WORD.
+	bool		pass2;		//!< do expansion in pass2.
+	bool		parsed;		//!< Was this item used during parsing?
+};
+
+/** Internal data that is associated with a configuration section
+ *
+ */
+struct conf_data {
+	CONF_ITEM  	item;
+	char const 	*name;
+	int	   	flag;
+	void	   	*data;		//!< User data
+	void       	(*free)(void *);	//!< Free user data function
+};
+
+struct conf_part {
+	CONF_ITEM	item;
+	char const	*name1;		//!< First name token.  Given ``foo bar {}`` would be ``foo``.
+	char const	*name2;		//!< Second name token. Given ``foo bar {}`` would be ``bar``.
+
+	FR_TOKEN	name2_type;	//!< The type of quoting around name2.
+
+	CONF_ITEM	*children;
+	CONF_ITEM	*tail;		//!< For speed.
+	CONF_SECTION	*template;
+
+	rbtree_t	*pair_tree;	//!< and a partridge..
+	rbtree_t	*section_tree;	//!< no jokes here.
+	rbtree_t	*name2_tree;	//!< for sections of the same name2
+	rbtree_t	*data_tree;
+
+	void		*base;
+	int		depth;
+
+	CONF_PARSER const *variables;
+};
+
+typedef struct cf_file_t {
+	char const	*filename;
+	CONF_SECTION	*cs;
+	struct stat	buf;
+	bool		from_dir;
+} cf_file_t;
+
+CONF_SECTION *root_config = NULL;
+bool cf_new_escape = true;
+
+
+static int		cf_data_add_internal(CONF_SECTION *cs, char const *name, void *data,
+					     void (*data_free)(void *), int flag);
+
+static void		*cf_data_find_internal(CONF_SECTION const *cs, char const *name, int flag);
+
+static char const 	*cf_expand_variables(char const *cf, int *lineno,
+					     CONF_SECTION *outercs,
+					     char *output, size_t outsize,
+					     char const *input, bool *soft_fail);
+
+static int cf_file_include(CONF_SECTION *cs, char const *filename_in, bool from_dir);
+
+
+
+/*
+ *	Isolate the scary casts in these tiny provably-safe functions
+ */
+
+/** Cast a CONF_ITEM to a CONF_PAIR
+ *
+ */
+CONF_PAIR *cf_item_to_pair(CONF_ITEM const *ci)
+{
+	CONF_PAIR *out;
+
+	if (ci == NULL) return NULL;
+
+	rad_assert(ci->type == CONF_ITEM_PAIR);
+
+	memcpy(&out, &ci, sizeof(out));
+	return out;
+}
+
+/** Cast a CONF_ITEM to a CONF_SECTION
+ *
+ */
+CONF_SECTION *cf_item_to_section(CONF_ITEM const *ci)
+{
+	CONF_SECTION *out;
+
+	if (ci == NULL) return NULL;
+
+	rad_assert(ci->type == CONF_ITEM_SECTION);
+
+	memcpy(&out, &ci, sizeof(out));
+	return out;
+}
+
+/** Cast a CONF_PAIR to a CONF_ITEM
+ *
+ */
+CONF_ITEM *cf_pair_to_item(CONF_PAIR const *cp)
+{
+	CONF_ITEM *out;
+
+	if (cp == NULL) return NULL;
+
+	memcpy(&out, &cp, sizeof(out));
+	return out;
+}
+
+/** Cast a CONF_SECTION to a CONF_ITEM
+ *
+ */
+CONF_ITEM *cf_section_to_item(CONF_SECTION const *cs)
+{
+	CONF_ITEM *out;
+
+	if (cs == NULL) return NULL;
+
+	memcpy(&out, &cs, sizeof(out));
+	return out;
+}
+
+/** Cast CONF_DATA to a CONF_ITEM
+ *
+ */
+static CONF_ITEM *cf_data_to_item(CONF_DATA const *cd)
+{
+	CONF_ITEM *out;
+
+	if (cd == NULL) {
+		return NULL;
+	}
+
+	memcpy(&out, &cd, sizeof(out));
+	return out;
+}
+
+static int _cf_data_free(CONF_DATA *cd)
+{
+	if (cd->free) cd->free(cd->data);
+
+	return 0;
+}
+
+/*
+ *	rbtree callback function
+ */
+static int pair_cmp(void const *a, void const *b)
+{
+	CONF_PAIR const *one = a;
+	CONF_PAIR const *two = b;
+
+	return strcmp(one->attr, two->attr);
+}
+
+
+/*
+ *	rbtree callback function
+ */
+static int section_cmp(void const *a, void const *b)
+{
+	CONF_SECTION const *one = a;
+	CONF_SECTION const *two = b;
+
+	return strcmp(one->name1, two->name1);
+}
+
+
+/*
+ *	rbtree callback function
+ */
+static int name2_cmp(void const *a, void const *b)
+{
+	CONF_SECTION const *one = a;
+	CONF_SECTION const *two = b;
+
+	rad_assert(strcmp(one->name1, two->name1) == 0);
+
+	if (!one->name2 && !two->name2) return 0;
+	if (one->name2 && !two->name2) return -1;
+	if (!one->name2 && two->name2) return +1;
+
+	return strcmp(one->name2, two->name2);
+}
+
+
+/*
+ *	rbtree callback function
+ */
+static int data_cmp(void const *a, void const *b)
+{
+	int rcode;
+
+	CONF_DATA const *one = a;
+	CONF_DATA const *two = b;
+
+	rcode = one->flag - two->flag;
+	if (rcode != 0) return rcode;
+
+	return strcmp(one->name, two->name);
+}
+
+/*
+ *	Functions for tracking filenames.
+ */
+static int filename_cmp(void const *a, void const *b)
+{
+	cf_file_t const *one = a;
+	cf_file_t const *two = b;
+
+	if (one->buf.st_dev < two->buf.st_dev) return -1;
+	if (one->buf.st_dev > two->buf.st_dev) return +1;
+
+	if (one->buf.st_ino < two->buf.st_ino) return -1;
+	if (one->buf.st_ino > two->buf.st_ino) return +1;
+
+	return 0;
+}
+
+static int cf_file_open(CONF_SECTION *cs, char const *filename, bool from_dir, FILE **fp_p)
+{
+	cf_file_t *file;
+	CONF_DATA *cd;
+	CONF_SECTION *top;
+	rbtree_t *tree;
+	int fd;
+	FILE *fp;
+
+	top = cf_top_section(cs);
+	cd = cf_data_find_internal(top, "filename", 0);
+	if (!cd) return -1;
+
+	tree = cd->data;
+
+	/*
+	 *	If we're including a wildcard directory, then ignore
+	 *	any files the users has already explicitly loaded in
+	 *	that directory.
+	 */
+	if (from_dir) {
+		cf_file_t my_file;
+
+		my_file.cs = cs;
+		my_file.filename = filename;
+
+		if (stat(filename, &my_file.buf) < 0) goto error;
+
+		file = rbtree_finddata(tree, &my_file);
+		if (file && !file->from_dir) return 0;
+	}
+
+	DEBUG2("including configuration file %s", filename);
+
+	fp = fopen(filename, "r");
+	if (!fp) {
+error:
+		ERROR("Unable to open file \"%s\": %s",
+		      filename, fr_syserror(errno));
+		return -1;
+	}
+
+	fd = fileno(fp);
+
+	file = talloc(tree, cf_file_t);
+	if (!file) {
+		fclose(fp);
+		return -1;
+	}
+
+	file->filename = filename;
+	file->cs = cs;
+	file->from_dir = from_dir;
+
+	if (fstat(fd, &file->buf) == 0) {
+#ifdef S_IWOTH
+		if ((file->buf.st_mode & S_IWOTH) != 0) {
+			ERROR("Configuration file %s is globally writable.  "
+			      "Refusing to start due to insecure configuration.", filename);
+
+			fclose(fp);
+			talloc_free(file);
+			return -1;
+		}
+#endif
+	}
+
+	/*
+	 *	We can include the same file twice.  e.g. when it
+	 *	contains common definitions, such as for SQL.
+	 *
+	 *	Though the admin should really use templates for that.
+	 */
+	if (!rbtree_insert(tree, file)) {
+		talloc_free(file);
+	}
+
+	*fp_p = fp;
+	return 1;
+}
+
+/*
+ *	Do some checks on the file
+ */
+static bool cf_file_check(CONF_SECTION *cs, char const *filename, bool check_perms)
+{
+	cf_file_t *file;
+	CONF_DATA *cd;
+	CONF_SECTION *top;
+	rbtree_t *tree;
+
+	top = cf_top_section(cs);
+	cd = cf_data_find_internal(top, "filename", 0);
+	if (!cd) return false;
+
+	tree = cd->data;
+
+	file = talloc(tree, cf_file_t);
+	if (!file) return false;
+
+	file->filename = filename;
+	file->cs = cs;
+
+	if (stat(filename, &file->buf) < 0) {
+		ERROR("Unable to check file \"%s\": %s", filename, fr_syserror(errno));
+		talloc_free(file);
+		return false;
+	}
+
+	if (!check_perms) {
+		talloc_free(file);
+		return true;
+	}
+
+#ifdef S_IWOTH
+	if ((file->buf.st_mode & S_IWOTH) != 0) {
+		ERROR("Configuration file %s is globally writable.  "
+		      "Refusing to start due to insecure configuration.", filename);
+		talloc_free(file);
+		return false;
+	}
+#endif
+
+	/*
+	 *	It's OK to include the same file twice...
+	 */
+	if (!rbtree_insert(tree, file)) {
+		talloc_free(file);
+	}
+
+	return true;
+
+}
+
+
+typedef struct cf_file_callback_t {
+	int		rcode;
+	rb_walker_t	callback;
+	CONF_SECTION	*modules;
+} cf_file_callback_t;
+
+
+/*
+ *	Return 0 for keep going, 1 for stop.
+ */
+static int file_callback(void *ctx, void *data)
+{
+	cf_file_callback_t *cb = ctx;
+	cf_file_t *file = data;
+	struct stat buf;
+
+	/*
+	 *	The file doesn't exist or we can no longer read it.
+	 */
+	if (stat(file->filename, &buf) < 0) {
+		cb->rcode = CF_FILE_ERROR;
+		return 1;
+	}
+
+	/*
+	 *	The file changed, we'll need to re-read it.
+	 */
+	if (file->buf.st_mtime != buf.st_mtime) {
+		if (cb->callback(cb->modules, file->cs)) {
+			cb->rcode |= CF_FILE_MODULE;
+			DEBUG3("HUP: Changed module file %s", file->filename);
+		} else {
+			DEBUG3("HUP: Changed config file %s", file->filename);
+			cb->rcode |= CF_FILE_CONFIG;
+		}
+
+		/*
+		 *	Presume that the file will be immediately
+		 *	re-read, so we update the mtime appropriately.
+		 */
+		file->buf.st_mtime = buf.st_mtime;
+	}
+
+	return 0;
+}
+
+
+/*
+ *	See if any of the files have changed.
+ */
+int cf_file_changed(CONF_SECTION *cs, rb_walker_t callback)
+{
+	CONF_DATA *cd;
+	CONF_SECTION *top;
+	cf_file_callback_t cb;
+	rbtree_t *tree;
+
+	top = cf_top_section(cs);
+	cd = cf_data_find_internal(top, "filename", 0);
+	if (!cd) return true;
+
+	tree = cd->data;
+
+	cb.rcode = CF_FILE_NONE;
+	cb.callback = callback;
+	cb.modules = cf_section_sub_find(cs, "modules");
+
+	(void) rbtree_walk(tree, RBTREE_IN_ORDER, file_callback, &cb);
+
+	return cb.rcode;
+}
+
+static int _cf_section_free(CONF_SECTION *cs)
+{
+	/*
+	 *	Name1 and name2 are allocated contiguous with
+	 *	cs.
+	 */
+	if (cs->pair_tree) {
+		rbtree_free(cs->pair_tree);
+		cs->pair_tree = NULL;
+	}
+	if (cs->section_tree) {
+		rbtree_free(cs->section_tree);
+		cs->section_tree = NULL;
+	}
+	if (cs->name2_tree) {
+		rbtree_free(cs->name2_tree);
+		cs->name2_tree = NULL;
+	}
+	if (cs->data_tree) {
+		rbtree_free(cs->data_tree);
+		cs->data_tree = NULL;
+	}
+
+	return 0;
+}
+
+/** Allocate a CONF_PAIR
+ *
+ * @param parent CONF_SECTION to hang this CONF_PAIR off of.
+ * @param attr name.
+ * @param value of CONF_PAIR.
+ * @param op T_OP_EQ, T_OP_SET etc.
+ * @param lhs_type T_BARE_WORD, T_DOUBLE_QUOTED_STRING, T_BACK_QUOTED_STRING
+ * @param rhs_type T_BARE_WORD, T_DOUBLE_QUOTED_STRING, T_BACK_QUOTED_STRING
+ * @return NULL on error, else a new CONF_SECTION parented by parent.
+ */
+CONF_PAIR *cf_pair_alloc(CONF_SECTION *parent, char const *attr, char const *value,
+			 FR_TOKEN op, FR_TOKEN lhs_type, FR_TOKEN rhs_type)
+{
+	CONF_PAIR *cp;
+
+	rad_assert(fr_equality_op[op] || fr_assignment_op[op]);
+	if (!attr) return NULL;
+
+	cp = talloc_zero(parent, CONF_PAIR);
+	if (!cp) return NULL;
+
+	cp->item.type = CONF_ITEM_PAIR;
+	cp->item.parent = parent;
+	cp->lhs_type = lhs_type;
+	cp->rhs_type = rhs_type;
+	cp->op = op;
+
+	cp->attr = talloc_typed_strdup(cp, attr);
+	if (!cp->attr) {
+	error:
+		talloc_free(cp);
+		return NULL;
+	}
+
+	if (value) {
+		cp->value = talloc_typed_strdup(cp, value);
+		if (!cp->value) goto error;
+	}
+
+	return cp;
+}
+
+/** Duplicate a CONF_PAIR
+ *
+ * @param parent to allocate new pair in.
+ * @param cp to duplicate.
+ * @return NULL on error, else a duplicate of the input pair.
+ */
+CONF_PAIR *cf_pair_dup(CONF_SECTION *parent, CONF_PAIR *cp)
+{
+	CONF_PAIR *new;
+
+	rad_assert(parent);
+	rad_assert(cp);
+
+	new = cf_pair_alloc(parent, cp->attr, cf_pair_value(cp),
+			    cp->op, cp->lhs_type, cp->rhs_type);
+	if (!new) return NULL;
+
+	new->parsed = cp->parsed;
+	new->item.lineno = cp->item.lineno;
+
+	/*
+	 *	Avoid mallocs if possible.
+	 */
+	if (!cp->item.filename || (parent->item.filename && !strcmp(parent->item.filename, cp->item.filename))) {
+		new->item.filename = parent->item.filename;
+	} else {
+		new->item.filename = talloc_strdup(new, cp->item.filename);
+	}
+
+	return new;
+}
+
+/** Add a configuration pair to a section
+ *
+ * @param parent section to add pair to.
+ * @param cp to add.
+ */
+void cf_pair_add(CONF_SECTION *parent, CONF_PAIR *cp)
+{
+	cf_item_add(parent, cf_pair_to_item(cp));
+}
+
+/** Allocate a CONF_SECTION
+ *
+ * @param parent CONF_SECTION to hang this CONF_SECTION off of.
+ * @param name1 Primary name.
+ * @param name2 Secondary name.
+ * @return NULL on error, else a new CONF_SECTION parented by parent.
+ */
+CONF_SECTION *cf_section_alloc(CONF_SECTION *parent, char const *name1, char const *name2)
+{
+	CONF_SECTION *cs;
+	char buffer[1024];
+
+	if (!name1) return NULL;
+
+	if (name2 && parent) {
+		if (strchr(name2, '$')) {
+			name2 = cf_expand_variables(parent->item.filename,
+						    &parent->item.lineno,
+						    parent,
+						    buffer, sizeof(buffer), name2, NULL);
+			if (!name2) {
+				ERROR("Failed expanding section name");
+				return NULL;
+			}
+		}
+	}
+
+	cs = talloc_zero(parent, CONF_SECTION);
+	if (!cs) return NULL;
+
+	cs->item.type = CONF_ITEM_SECTION;
+	cs->item.parent = parent;
+
+	cs->name1 = talloc_typed_strdup(cs, name1);
+	if (!cs->name1) {
+	error:
+		talloc_free(cs);
+		return NULL;
+	}
+
+	if (name2) {
+		cs->name2 = talloc_typed_strdup(cs, name2);
+		if (!cs->name2) goto error;
+	}
+
+	cs->pair_tree = rbtree_create(cs, pair_cmp, NULL, 0);
+	if (!cs->pair_tree) goto error;
+
+	talloc_set_destructor(cs, _cf_section_free);
+
+	/*
+	 *	Don't create a data tree, it may not be needed.
+	 */
+
+	/*
+	 *	Don't create the section tree here, it may not
+	 *	be needed.
+	 */
+
+	if (parent) cs->depth = parent->depth + 1;
+
+	return cs;
+}
+
+/** Duplicate a configuration section
+ *
+ * @note recursively duplicates any child sections.
+ * @note does not duplicate any data associated with a section, or its child sections.
+ *
+ * @param parent section (may be NULL).
+ * @param cs to duplicate.
+ * @param name1 of new section.
+ * @param name2 of new section.
+ * @param copy_meta Copy additional meta data for a section (like template, base, depth and variables).
+ * @return a duplicate of the existing section, or NULL on error.
+ */
+CONF_SECTION *cf_section_dup(CONF_SECTION *parent, CONF_SECTION const *cs,
+			     char const *name1, char const *name2, bool copy_meta)
+{
+	CONF_SECTION *new, *subcs;
+	CONF_PAIR *cp;
+	CONF_ITEM *ci;
+
+	new = cf_section_alloc(parent, name1, name2);
+
+	if (copy_meta) {
+		new->template = cs->template;
+		new->base = cs->base;
+		new->depth = cs->depth;
+		new->variables = cs->variables;
+	}
+
+	new->item.lineno = cs->item.lineno;
+
+	if (!cs->item.filename || (parent && (strcmp(parent->item.filename, cs->item.filename) == 0))) {
+		new->item.filename = parent->item.filename;
+	} else {
+		new->item.filename = talloc_strdup(new, cs->item.filename);
+	}
+
+	for (ci = cs->children; ci; ci = ci->next) {
+		switch (ci->type) {
+		case CONF_ITEM_SECTION:
+			subcs = cf_item_to_section(ci);
+			subcs = cf_section_dup(new, subcs,
+					       cf_section_name1(subcs), cf_section_name2(subcs),
+					       copy_meta);
+			if (!subcs) {
+				talloc_free(new);
+				return NULL;
+			}
+			cf_section_add(new, subcs);
+			break;
+
+		case CONF_ITEM_PAIR:
+			cp = cf_pair_dup(new, cf_item_to_pair(ci));
+			if (!cp) {
+				talloc_free(new);
+				return NULL;
+			}
+			cf_pair_add(new, cp);
+			break;
+
+		case CONF_ITEM_DATA: /* Skip data */
+			break;
+
+		case CONF_ITEM_INVALID:
+			rad_assert(0);
+		}
+	}
+
+	return new;
+}
+
+void cf_section_add(CONF_SECTION *parent, CONF_SECTION *cs)
+{
+	cf_item_add(parent, &(cs->item));
+}
+
+/** Replace pair in a given section with a new pair, of the given value.
+ *
+ * @param cs to replace pair in.
+ * @param cp to replace.
+ * @param value New value to assign to cp.
+ * @return 0 on success, -1 on failure.
+ */
+int cf_pair_replace(CONF_SECTION *cs, CONF_PAIR *cp, char const *value)
+{
+	CONF_PAIR *newp;
+	CONF_ITEM *ci, *cn, **last;
+
+	newp = cf_pair_alloc(cs, cp->attr, value, cp->op, cp->lhs_type, cp->rhs_type);
+	if (!newp) return -1;
+
+	ci = &(cp->item);
+	cn = &(newp->item);
+
+	/*
+	 *	Find the old one from the linked list, and replace it
+	 *	with the new one.
+	 */
+	for (last = &cs->children; (*last) != NULL; last = &(*last)->next) {
+		if (*last == ci) {
+			cn->next = (*last)->next;
+			*last = cn;
+			ci->next = NULL;
+			break;
+		}
+	}
+
+	rbtree_deletebydata(cs->pair_tree, ci);
+
+	rbtree_insert(cs->pair_tree, cn);
+
+	return 0;
+}
+
+
+/*
+ *	Add an item to a configuration section.
+ */
+void cf_item_add(CONF_SECTION *cs, CONF_ITEM *ci)
+{
+#ifndef NDEBUG
+	CONF_ITEM *first = ci;
+#endif
+
+	rad_assert((void *)cs != (void *)ci);
+
+	if (!cs || !ci) return;
+
+	if (!cs->children) {
+		rad_assert(cs->tail == NULL);
+		cs->children = ci;
+	} else {
+		rad_assert(cs->tail != NULL);
+		cs->tail->next = ci;
+	}
+
+	/*
+	 *	Update the trees (and tail) for each item added.
+	 */
+	for (/* nothing */; ci != NULL; ci = ci->next) {
+		rad_assert(ci->next != first);	/* simple cycle detection */
+
+		cs->tail = ci;
+
+		/*
+		 *	For fast lookups, pairs and sections get
+		 *	added to rbtree's.
+		 */
+		switch (ci->type) {
+		case CONF_ITEM_PAIR:
+			if (!rbtree_insert(cs->pair_tree, ci)) {
+				CONF_PAIR *cp = cf_item_to_pair(ci);
+
+				if (strcmp(cp->attr, "confdir") == 0) break;
+				if (!cp->value) break; /* module name, "ok", etc. */
+			}
+			break;
+
+		case CONF_ITEM_SECTION: {
+			CONF_SECTION *cs_new = cf_item_to_section(ci);
+			CONF_SECTION *name1_cs;
+
+			if (!cs->section_tree) {
+				cs->section_tree = rbtree_create(cs, section_cmp, NULL, 0);
+				if (!cs->section_tree) {
+					ERROR("Out of memory");
+					fr_exit_now(1);
+				}
+			}
+
+			name1_cs = rbtree_finddata(cs->section_tree, cs_new);
+			if (!name1_cs) {
+				if (!rbtree_insert(cs->section_tree, cs_new)) {
+					ERROR("Failed inserting section into tree");
+					fr_exit_now(1);
+				}
+				break;
+			}
+
+			/*
+			 *	We already have a section of
+			 *	this "name1".  Add a new
+			 *	sub-section based on name2.
+			 */
+			if (!name1_cs->name2_tree) {
+				name1_cs->name2_tree = rbtree_create(name1_cs, name2_cmp, NULL, 0);
+				if (!name1_cs->name2_tree) {
+					ERROR("Out of memory");
+					fr_exit_now(1);
+				}
+			}
+
+			/*
+			 *	We don't care if this fails.
+			 *	If the user tries to create
+			 *	two sections of the same
+			 *	name1/name2, the duplicate
+			 *	section is just silently
+			 *	ignored.
+			 */
+			rbtree_insert(name1_cs->name2_tree, cs_new);
+			break;
+		} /* was a section */
+
+		case CONF_ITEM_DATA:
+			if (!cs->data_tree) {
+				cs->data_tree = rbtree_create(cs, data_cmp, NULL, 0);
+			}
+			if (cs->data_tree) {
+				rbtree_insert(cs->data_tree, ci);
+			}
+			break;
+
+		default: /* FIXME: assert & error! */
+			break;
+
+		} /* switch over conf types */
+	} /* loop over ci */
+}
+
+
+CONF_ITEM *cf_reference_item(CONF_SECTION const *parentcs,
+			     CONF_SECTION *outercs,
+			     char const *ptr)
+{
+	CONF_PAIR *cp;
+	CONF_SECTION *next;
+	CONF_SECTION const *cs = outercs;
+	char name[8192];
+	char *p;
+
+	if (!cs) goto no_such_item;
+
+	strlcpy(name, ptr, sizeof(name));
+	p = name;
+
+	/*
+	 *	".foo" means "foo from the current section"
+	 */
+	if (*p == '.') {
+		p++;
+
+		/*
+		 *	Just '.' means the current section
+		 */
+		if (*p == '\0') {
+			return cf_section_to_item(cs);
+		}
+
+		/*
+		 *	..foo means "foo from the section
+		 *	enclosing this section" (etc.)
+		 */
+		while (*p == '.') {
+			if (cs->item.parent) {
+				cs = cs->item.parent;
+			}
+
+			/*
+			 *	.. means the section
+			 *	enclosing this section
+			 */
+			if (!*++p) {
+				return cf_section_to_item(cs);
+			}
+		}
+
+		/*
+		 *	"foo.bar.baz" means "from the root"
+		 */
+	} else if (strchr(p, '.') != NULL) {
+		if (!parentcs) goto no_such_item;
+
+		cs = parentcs;
+	}
+
+	while (*p) {
+		char *q, *r;
+
+		r = strchr(p, '[');
+		q = strchr(p, '.');
+		if (!r && !q) break;
+
+		if (r && q > r) q = NULL;
+		if (q && q < r) r = NULL;
+
+		/*
+		 *	Split off name2.
+		 */
+		if (r) {
+			q = strchr(r + 1, ']');
+			if (!q) return NULL; /* parse error */
+
+			/*
+			 *	Points to foo[bar]xx: parse error,
+			 *	it should be foo[bar] or foo[bar].baz
+			 */
+			if (q[1] && q[1] != '.') goto no_such_item;
+
+			*r = '\0';
+			*q = '\0';
+			next = cf_section_sub_find_name2(cs, p, r + 1);
+			*r = '[';
+			*q = ']';
+
+			/*
+			 *	Points to a named instance of a section.
+			 */
+			if (!q[1]) {
+				if (!next) goto no_such_item;
+				return &(next->item);
+			}
+
+			q++;	/* ensure we skip the ']' and '.' */
+
+		} else {
+			*q = '\0';
+			next = cf_section_sub_find(cs, p);
+			*q = '.';
+		}
+
+		if (!next) break; /* it MAY be a pair in this section! */
+
+		cs = next;
+		p = q + 1;
+	}
+
+	if (!*p) goto no_such_item;
+
+ retry:
+	/*
+	 *	Find it in the current referenced
+	 *	section.
+	 */
+	cp = cf_pair_find(cs, p);
+	if (cp) {
+		cp->parsed = true;	/* conf pairs which are referenced count as parsed */
+		return &(cp->item);
+	}
+
+	next = cf_section_sub_find(cs, p);
+	if (next) return &(next->item);
+
+	/*
+	 *	"foo" is "in the current section, OR in main".
+	 */
+	if ((p == name) && (parentcs != NULL) && (cs != parentcs)) {
+		cs = parentcs;
+		goto retry;
+	}
+
+no_such_item:
+	return NULL;
+}
+
+
+CONF_SECTION *cf_top_section(CONF_SECTION *cs)
+{
+	if (!cs) return NULL;
+
+	while (cs->item.parent != NULL) {
+		cs = cs->item.parent;
+	}
+
+	return cs;
+}
+
+
+/*
+ *	Expand the variables in an input string.
+ */
+static char const *cf_expand_variables(char const *cf, int *lineno,
+				       CONF_SECTION *outercs,
+				       char *output, size_t outsize,
+				       char const *input, bool *soft_fail)
+{
+	char *p;
+	char const *end, *ptr;
+	CONF_SECTION const *parentcs;
+	char name[8192];
+
+	if (soft_fail) *soft_fail = false;
+
+	/*
+	 *	Find the master parent conf section.
+	 *	We can't use main_config.config, because we're in the
+	 *	process of re-building it, and it isn't set up yet...
+	 */
+	parentcs = cf_top_section(outercs);
+
+	p = output;
+	ptr = input;
+	while (*ptr) {
+		/*
+		 *	Ignore anything other than "${"
+		 */
+		if ((*ptr == '$') && (ptr[1] == '{')) {
+			CONF_ITEM *ci;
+			CONF_PAIR *cp;
+			char *q;
+
+			/*
+			 *	FIXME: Add support for ${foo:-bar},
+			 *	like in xlat.c
+			 */
+
+			/*
+			 *	Look for trailing '}', and log a
+			 *	warning for anything that doesn't match,
+			 *	and exit with a fatal error.
+			 */
+			end = strchr(ptr, '}');
+			if (end == NULL) {
+				*p = '\0';
+				ERROR("%s[%d]: Variable expansion missing }",
+				       cf, *lineno);
+				return NULL;
+			}
+
+			ptr += 2;
+
+			/*
+			 *	Can't really happen because input lines are
+			 *	capped at 8k, which is sizeof(name)
+			 */
+			if ((size_t) (end - ptr) >= sizeof(name)) {
+				ERROR("%s[%d]: Reference string is too large",
+				      cf, *lineno);
+				return NULL;
+			}
+
+			memcpy(name, ptr, end - ptr);
+			name[end - ptr] = '\0';
+
+			q = strchr(name, ':');
+			if (q) {
+				*(q++) = '\0';
+			}
+
+			ci = cf_reference_item(parentcs, outercs, name);
+			if (!ci) {
+				if (soft_fail) *soft_fail = true;
+				ERROR("%s[%d]: Reference \"${%s}\" not found", cf, *lineno, name);
+				return NULL;
+			}
+
+			/*
+			 *	The expansion doesn't refer to another item or section
+			 *	it's the property of a section.
+			 */
+			if (q) {
+				CONF_SECTION *mycs = cf_item_to_section(ci);
+
+				if (ci->type != CONF_ITEM_SECTION) {
+					ERROR("%s[%d]: Can only reference properties of sections", cf, *lineno);
+					return NULL;
+				}
+
+				switch (fr_str2int(conf_property_name, q, CONF_PROPERTY_INVALID)) {
+				case CONF_PROPERTY_NAME:
+					strcpy(p, mycs->name1);
+					break;
+
+				case CONF_PROPERTY_INSTANCE:
+					strcpy(p, mycs->name2 ? mycs->name2 : mycs->name1);
+					break;
+
+				default:
+					ERROR("%s[%d]: Invalid property '%s'", cf, *lineno, q);
+					return NULL;
+				}
+				p += strlen(p);
+				ptr = end + 1;
+
+			} else if (ci->type == CONF_ITEM_PAIR) {
+				/*
+				 *  Substitute the value of the variable.
+				 */
+				cp = cf_item_to_pair(ci);
+
+				/*
+				 *	If the thing we reference is
+				 *	marked up as being expanded in
+				 *	pass2, don't expand it now.
+				 *	Let it be expanded in pass2.
+				 */
+				if (cp->pass2) {
+					if (soft_fail) *soft_fail = true;
+
+					ERROR("%s[%d]: Reference \"%s\" points to a variable which has not been expanded.",
+					      cf, *lineno, input);
+					return NULL;
+				}
+
+				/*
+				 *	Might as well make
+				 *	non-existent string be the
+				 *	empty string.
+				 */
+				if (!cp->value) {
+					*p = '\0';
+					goto skip_value;
+				}
+
+				if (p + strlen(cp->value) >= output + outsize) {
+					ERROR("%s[%d]: Reference \"%s\" is too long",
+					       cf, *lineno, input);
+					return NULL;
+				}
+
+				strcpy(p, cp->value);
+				p += strlen(p);
+			skip_value:
+				ptr = end + 1;
+
+			} else if (ci->type == CONF_ITEM_SECTION) {
+				CONF_SECTION *subcs;
+
+				/*
+				 *	Adding an entry again to a
+				 *	section is wrong.  We don't
+				 *	want an infinite loop.
+				 */
+				if (ci->parent == outercs) {
+					ERROR("%s[%d]: Cannot reference different item in same section", cf, *lineno);
+					return NULL;
+				}
+
+				/*
+				 *	Copy the section instead of
+				 *	referencing it.
+				 */
+				subcs = cf_item_to_section(ci);
+				subcs = cf_section_dup(outercs, subcs,
+						       cf_section_name1(subcs), cf_section_name2(subcs),
+						       false);
+				if (!subcs) {
+					ERROR("%s[%d]: Failed copying reference %s", cf, *lineno, name);
+					return NULL;
+				}
+
+				subcs->item.filename = ci->filename;
+				subcs->item.lineno = ci->lineno;
+				cf_item_add(outercs, &(subcs->item));
+
+				ptr = end + 1;
+
+			} else {
+				ERROR("%s[%d]: Reference \"%s\" type is invalid", cf, *lineno, input);
+				return NULL;
+			}
+		} else if (strncmp(ptr, "$ENV{", 5) == 0) {
+			char *env;
+
+			ptr += 5;
+
+			/*
+			 *	Look for trailing '}', and log a
+			 *	warning for anything that doesn't match,
+			 *	and exit with a fatal error.
+			 */
+			end = strchr(ptr, '}');
+			if (end == NULL) {
+				*p = '\0';
+				ERROR("%s[%d]: Environment variable expansion missing }",
+				       cf, *lineno);
+				return NULL;
+			}
+
+			/*
+			 *	Can't really happen because input lines are
+			 *	capped at 8k, which is sizeof(name)
+			 */
+			if ((size_t) (end - ptr) >= sizeof(name)) {
+				ERROR("%s[%d]: Environment variable name is too large",
+				       cf, *lineno);
+				return NULL;
+			}
+
+			memcpy(name, ptr, end - ptr);
+			name[end - ptr] = '\0';
+
+			/*
+			 *	Get the environment variable.
+			 *	If none exists, then make it an empty string.
+			 */
+			env = getenv(name);
+			if (env == NULL) {
+				*name = '\0';
+				env = name;
+			}
+
+			if (p + strlen(env) >= output + outsize) {
+				ERROR("%s[%d]: Reference \"%s\" is too long",
+				       cf, *lineno, input);
+				return NULL;
+			}
+
+			strcpy(p, env);
+			p += strlen(p);
+			ptr = end + 1;
+
+		} else {
+			/*
+			 *	Copy it over verbatim.
+			 */
+			*(p++) = *(ptr++);
+		}
+
+
+		if (p >= (output + outsize)) {
+			ERROR("%s[%d]: Reference \"%s\" is too long",
+			       cf, *lineno, input);
+			return NULL;
+		}
+	} /* loop over all of the input string. */
+
+	*p = '\0';
+
+	return output;
+}
+
+static char const parse_spaces[] = "                                                                                                                                                                                                                                                                ";
+
+/** Validation function for ipaddr conffile types
+ *
+ */
+static inline int fr_item_validate_ipaddr(CONF_SECTION *cs, char const *name, PW_TYPE type, char const *value,
+					  fr_ipaddr_t *ipaddr)
+{
+	char ipbuf[128];
+
+	if (strcmp(value, "*") == 0) {
+		cf_log_info(cs, "%.*s\t%s = *", cs->depth, parse_spaces, name);
+	} else if (strspn(value, ".0123456789abdefABCDEF:%[]/") == strlen(value)) {
+		cf_log_info(cs, "%.*s\t%s = %s", cs->depth, parse_spaces, name, value);
+	} else {
+		cf_log_info(cs, "%.*s\t%s = %s IPv%s address [%s]", cs->depth, parse_spaces, name, value,
+			    (ipaddr->af == AF_INET ? "4" : " 6"), ip_ntoh(ipaddr, ipbuf, sizeof(ipbuf)));
+	}
+
+	switch (type) {
+	case PW_TYPE_IPV4_ADDR:
+	case PW_TYPE_IPV6_ADDR:
+	case PW_TYPE_COMBO_IP_ADDR:
+		switch (ipaddr->af) {
+		case AF_INET:
+			if (ipaddr->prefix == 32) return 0;
+
+			cf_log_err(&(cs->item), "Invalid IPv4 mask length \"/%i\".  Only \"/32\" permitted for non-prefix types",
+				   ipaddr->prefix);
+			break;
+
+		case AF_INET6:
+			if (ipaddr->prefix == 128) return 0;
+
+			cf_log_err(&(cs->item), "Invalid IPv6 mask length \"/%i\".  Only \"/128\" permitted for non-prefix types",
+				   ipaddr->prefix);
+			break;
+
+
+		default:
+			cf_log_err(&(cs->item), "Unknown address (%d) family passed for parsing IP address.", ipaddr->af);
+			break;
+		}
+
+		return -1;
+
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+/** Parses a #CONF_PAIR into a C data type, with a default value.
+ *
+ * Takes fields from a #CONF_PARSER struct and uses them to parse the string value
+ * of a #CONF_PAIR into a C data type matching the type argument.
+ *
+ * The format of the types are the same as #value_data_t types.
+ *
+ * @note The dflt value will only be used if no matching #CONF_PAIR is found. Empty strings will not
+ *	 result in the dflt value being used.
+ *
+ * **PW_TYPE to data type mappings**
+ * | PW_TYPE                 | Data type          | Dynamically allocated  |
+ * | ----------------------- | ------------------ | ---------------------- |
+ * | PW_TYPE_TMPL            | ``vp_tmpl_t``      | Yes                    |
+ * | PW_TYPE_BOOLEAN         | ``bool``           | No                     |
+ * | PW_TYPE_INTEGER         | ``uint32_t``       | No                     |
+ * | PW_TYPE_SHORT           | ``uint16_t``       | No                     |
+ * | PW_TYPE_INTEGER64       | ``uint64_t``       | No                     |
+ * | PW_TYPE_SIGNED          | ``int32_t``        | No                     |
+ * | PW_TYPE_STRING          | ``char const *``   | Yes                    |
+ * | PW_TYPE_IPV4_ADDR       | ``fr_ipaddr_t``    | No                     |
+ * | PW_TYPE_IPV4_PREFIX     | ``fr_ipaddr_t``    | No                     |
+ * | PW_TYPE_IPV6_ADDR       | ``fr_ipaddr_t``    | No                     |
+ * | PW_TYPE_IPV6_PREFIX     | ``fr_ipaddr_t``    | No                     |
+ * | PW_TYPE_COMBO_IP_ADDR   | ``fr_ipaddr_t``    | No                     |
+ * | PW_TYPE_COMBO_IP_PREFIX | ``fr_ipaddr_t``    | No                     |
+ * | PW_TYPE_TIMEVAL         | ``struct timeval`` | No                     |
+ *
+ * @param cs to search for matching #CONF_PAIR in.
+ * @param name of #CONF_PAIR to search for.
+ * @param type Data type to parse #CONF_PAIR value as.
+ *	Should be one of the following ``data`` types, and one or more of the following ``flag`` types or'd together:
+ *	- ``data`` #PW_TYPE_TMPL 		- @copybrief PW_TYPE_TMPL
+ *					  	  Feeds the value into #tmpl_afrom_str. Value can be
+ *					  	  obtained when processing requests, with #tmpl_expand or #tmpl_aexpand.
+ *	- ``data`` #PW_TYPE_BOOLEAN		- @copybrief PW_TYPE_BOOLEAN
+ *	- ``data`` #PW_TYPE_INTEGER		- @copybrief PW_TYPE_INTEGER
+ *	- ``data`` #PW_TYPE_SHORT		- @copybrief PW_TYPE_SHORT
+ *	- ``data`` #PW_TYPE_INTEGER64		- @copybrief PW_TYPE_INTEGER64
+ *	- ``data`` #PW_TYPE_SIGNED		- @copybrief PW_TYPE_SIGNED
+ *	- ``data`` #PW_TYPE_STRING		- @copybrief PW_TYPE_STRING
+ *	- ``data`` #PW_TYPE_IPV4_ADDR		- @copybrief PW_TYPE_IPV4_ADDR (IPv4 address with prefix 32).
+ *	- ``data`` #PW_TYPE_IPV4_PREFIX		- @copybrief PW_TYPE_IPV4_PREFIX (IPv4 address with variable prefix).
+ *	- ``data`` #PW_TYPE_IPV6_ADDR		- @copybrief PW_TYPE_IPV6_ADDR (IPv6 address with prefix 128).
+ *	- ``data`` #PW_TYPE_IPV6_PREFIX		- @copybrief PW_TYPE_IPV6_PREFIX (IPv6 address with variable prefix).
+ *	- ``data`` #PW_TYPE_COMBO_IP_ADDR 	- @copybrief PW_TYPE_COMBO_IP_ADDR (IPv4/IPv6 address with
+ *						  prefix 32/128).
+ *	- ``data`` #PW_TYPE_COMBO_IP_PREFIX	- @copybrief PW_TYPE_COMBO_IP_PREFIX (IPv4/IPv6 address with
+ *						  variable prefix).
+ *	- ``data`` #PW_TYPE_TIMEVAL		- @copybrief PW_TYPE_TIMEVAL
+ *	- ``flag`` #PW_TYPE_DEPRECATED		- @copybrief PW_TYPE_DEPRECATED
+ *	- ``flag`` #PW_TYPE_REQUIRED		- @copybrief PW_TYPE_REQUIRED
+ *	- ``flag`` #PW_TYPE_ATTRIBUTE		- @copybrief PW_TYPE_ATTRIBUTE
+ *	- ``flag`` #PW_TYPE_SECRET		- @copybrief PW_TYPE_SECRET
+ *	- ``flag`` #PW_TYPE_FILE_INPUT		- @copybrief PW_TYPE_FILE_INPUT
+ *	- ``flag`` #PW_TYPE_NOT_EMPTY		- @copybrief PW_TYPE_NOT_EMPTY
+ * @param data Pointer to a global variable, or pointer to a field in the struct being populated with values.
+ * @param dflt value to use, if no #CONF_PAIR is found.
+ * @return
+ *	- 1 if default value was used.
+ *	- 0 on success.
+ *	- -1 on error.
+ *	- -2 if deprecated.
+ */
+int cf_item_parse(CONF_SECTION *cs, char const *name, unsigned int type, void *data, char const *dflt)
+{
+	int rcode;
+	bool deprecated, required, attribute, secret, file_input, cant_be_empty, tmpl, multi, file_exists;
+	char **q;
+	char const *value;
+	CONF_PAIR *cp = NULL;
+	fr_ipaddr_t *ipaddr;
+	char buffer[8192];
+	CONF_ITEM *c_item;
+
+	if (!cs) {
+		cf_log_err(&(cs->item), "No enclosing section for configuration item \"%s\"", name);
+		return -1;
+	}
+
+	c_item = &cs->item;
+
+	deprecated = (type & PW_TYPE_DEPRECATED);
+	required = (type & PW_TYPE_REQUIRED);
+	attribute = (type & PW_TYPE_ATTRIBUTE);
+	secret = (type & PW_TYPE_SECRET);
+	file_input = (type == PW_TYPE_FILE_INPUT);	/* check, not and */
+	file_exists = (type == PW_TYPE_FILE_EXISTS);	/* check, not and */
+	cant_be_empty = (type & PW_TYPE_NOT_EMPTY);
+	tmpl = (type & PW_TYPE_TMPL);
+	multi = (type & PW_TYPE_MULTI);
+
+	if (attribute) required = true;
+	if (required) cant_be_empty = true;	/* May want to review this in the future... */
+
+	/*
+	 *	Everything except templates must have a base type.
+	 */
+	if (!(type & 0xff) && !tmpl) {
+		cf_log_err(c_item, "Configuration item \"%s\" must have a data type", name);
+		return -1;
+	}
+
+	type &= 0xff;				/* normal types are small */
+
+	rcode = 0;
+
+	cp = cf_pair_find(cs, name);
+
+	/*
+	 *	No pairs match the configuration item name in the current
+	 *	section, use the default value.
+	 */
+	if (!cp) {
+		if (deprecated) return 0;	/* Don't set the default value */
+
+		rcode = 1;
+		value = dflt;
+	/*
+	 *	Something matched, used the CONF_PAIR value.
+	 */
+	} else {
+		CONF_PAIR *next = cp;
+
+		value = cp->value;
+		cp->parsed = true;
+		c_item = &cp->item;
+
+		if (deprecated) {
+			cf_log_err(c_item, "Configuration item \"%s\" is deprecated", name);
+			return -2;
+		}
+
+		/*
+		 *	A quick check to see if the next item is the same.
+		 */
+		if (!multi && cp->item.next && (cp->item.next->type == CONF_ITEM_PAIR)) {
+			next = cf_item_to_pair(cp->item.next);
+
+			if (strcmp(next->attr, name) == 0) {
+				WARN("%s[%d]: Ignoring duplicate configuration item '%s'",
+				     next->item.filename ? next->item.filename : "unknown",
+				     next->item.lineno, name);
+			}
+		}
+										   
+		if (multi) {
+			while ((next = cf_pair_find_next(cs, next, name)) != NULL) {
+				/*
+				 *	@fixme We should actually validate
+				 *	the value of the pairs too
+				 */
+				next->parsed = true;
+			};
+		}
+	}
+
+	if (!value) {
+		if (required) {
+			cf_log_err(c_item, "Configuration item \"%s\" must have a value", name);
+
+			return -1;
+		}
+		return rcode;
+	}
+
+	if ((value[0] == '\0') && cant_be_empty) {
+	cant_be_empty:
+		cf_log_err(c_item, "Configuration item \"%s\" must not be empty (zero length)", name);
+		if (!required) cf_log_err(c_item, "Comment item to silence this message");
+
+		return -1;
+	}
+
+
+	/*
+	 *	Process a value as a LITERAL template.  Once all of
+	 *	the attrs and xlats are defined, the pass2 code
+	 *	converts it to the appropriate type.
+	 */
+	if (tmpl) {
+		vp_tmpl_t *vpt;
+
+		if (!value) {
+			*(vp_tmpl_t **)data = NULL;
+			return 0;
+		}
+
+		rad_assert(!attribute);
+		vpt = tmpl_alloc(cs, TMPL_TYPE_LITERAL, value, strlen(value));
+		*(vp_tmpl_t **)data = vpt;
+
+		return 0;
+	}
+
+	switch (type) {
+	case PW_TYPE_BOOLEAN:
+		/*
+		 *	Allow yes/no, true/false, and on/off
+		 */
+		if ((strcasecmp(value, "yes") == 0) ||
+		    (strcasecmp(value, "true") == 0) ||
+		    (strcasecmp(value, "on") == 0)) {
+			*(bool *)data = true;
+		} else if ((strcasecmp(value, "no") == 0) ||
+			   (strcasecmp(value, "false") == 0) ||
+			   (strcasecmp(value, "off") == 0)) {
+			*(bool *)data = false;
+		} else {
+			*(bool *)data = false;
+			cf_log_err(&(cs->item), "Invalid value \"%s\" for boolean "
+			       "variable %s", value, name);
+			return -1;
+		}
+		cf_log_info(cs, "%.*s\t%s = %s",
+			    cs->depth, parse_spaces, name, value);
+		break;
+
+	case PW_TYPE_INTEGER:
+	{
+		unsigned long v = strtoul(value, 0, 0);
+
+		/*
+		 *	Restrict integer values to 0-INT32_MAX, this means
+		 *	it will always be safe to cast them to a signed type
+		 *	for comparisons, and imposes the same range limit as
+		 *	before we switched to using an unsigned type to
+		 *	represent config item integers.
+		 */
+		if (v > INT32_MAX) {
+			cf_log_err(&(cs->item), "Invalid value \"%s\" for variable %s, must be between 0-%u", value,
+				   name, INT32_MAX);
+			return -1;
+		}
+
+		*(uint32_t *)data = v;
+		cf_log_info(cs, "%.*s\t%s = %u", cs->depth, parse_spaces, name, *(uint32_t *)data);
+	}
+		break;
+
+	case PW_TYPE_BYTE:
+	{
+		unsigned long v = strtoul(value, 0, 0);
+
+		if (v > UINT8_MAX) {
+			cf_log_err(&(cs->item), "Invalid value \"%s\" for variable %s, must be between 0-%u", value,
+				   name, UINT8_MAX);
+			return -1;
+		}
+		*(uint8_t *)data = (uint8_t) v;
+		cf_log_info(cs, "%.*s\t%s = %u", cs->depth, parse_spaces, name, *(uint8_t *)data);
+	}
+		break;
+
+	case PW_TYPE_SHORT:
+	{
+		unsigned long v = strtoul(value, 0, 0);
+
+		if (v > UINT16_MAX) {
+			cf_log_err(&(cs->item), "Invalid value \"%s\" for variable %s, must be between 0-%u", value,
+				   name, UINT16_MAX);
+			return -1;
+		}
+		*(uint16_t *)data = (uint16_t) v;
+		cf_log_info(cs, "%.*s\t%s = %u", cs->depth, parse_spaces, name, *(uint16_t *)data);
+	}
+		break;
+
+	case PW_TYPE_INTEGER64:
+		*(uint64_t *)data = strtoull(value, 0, 0);
+		cf_log_info(cs, "%.*s\t%s = %" PRIu64, cs->depth, parse_spaces, name, *(uint64_t *)data);
+		break;
+
+	case PW_TYPE_SIGNED:
+		*(int32_t *)data = strtol(value, 0, 0);
+		cf_log_info(cs, "%.*s\t%s = %d", cs->depth, parse_spaces, name, *(int32_t *)data);
+		break;
+
+	case PW_TYPE_STRING:
+		q = (char **) data;
+		if (*q != NULL) {
+			talloc_free(*q);
+		}
+
+		/*
+		 *	Expand variables which haven't already been
+		 *	expanded automagically when the configuration
+		 *	file was read.
+		 */
+		if (value == dflt) {
+			int lineno = 0;
+
+			lineno = cs->item.lineno;
+
+			value = cf_expand_variables("<internal>",
+						    &lineno,
+						    cs, buffer, sizeof(buffer),
+						    value, NULL);
+			if (!value) {
+				cf_log_err(&(cs->item),"Failed expanding variable %s", name);
+				return -1;
+			}
+		}
+
+		if (cant_be_empty && (value[0] == '\0')) goto cant_be_empty;
+
+		if (attribute) {
+			if (!dict_attrbyname(value)) {
+				if (!cp) {
+					cf_log_err(&(cs->item), "No such attribute '%s' for configuration '%s'",
+						   value, name);
+				} else {
+					cf_log_err(&(cp->item), "No such attribute '%s'", value);
+				}
+				return -1;
+			}
+		}
+
+		/*
+		 *	Hide secrets when using "radiusd -X".
+		 */
+		if (secret && (rad_debug_lvl <= 2)) {
+			cf_log_info(cs, "%.*s\t%s = <<< secret >>>",
+				    cs->depth, parse_spaces, name);
+		} else {
+			cf_log_info(cs, "%.*s\t%s = \"%s\"",
+				    cs->depth, parse_spaces, name, value ? value : "(null)");
+		}
+		*q = value ? talloc_typed_strdup(cs, value) : NULL;
+
+		/*
+		 *	If there's data AND it's an input file, check
+		 *	that we can read it.  This check allows errors
+		 *	to be caught as early as possible, during
+		 *	server startup.
+		 */
+		if (*q && file_input && !cf_file_check(cs, *q, true)) {
+			cf_log_err(&(cs->item), "Failed parsing configuration item \"%s\"", name);
+			return -1;
+		}
+
+		if (*q && file_exists && !cf_file_check(cs, *q, false)) {
+			cf_log_err(&(cs->item), "Failed parsing configuration item \"%s\"", name);
+			return -1;
+		}
+		break;
+
+	case PW_TYPE_IPV4_ADDR:
+	case PW_TYPE_IPV4_PREFIX:
+		ipaddr = data;
+
+		if (fr_pton4(ipaddr, value, -1, true, false) < 0) {
+		failed:
+			cf_log_err(&(cs->item), "Failed parsing configuration item \"%s\" - %s", name, fr_strerror());
+			return -1;
+		}
+		if (fr_item_validate_ipaddr(cs, name, type, value, ipaddr) < 0) return -1;
+		break;
+
+	case PW_TYPE_IPV6_ADDR:
+	case PW_TYPE_IPV6_PREFIX:
+		ipaddr = data;
+
+		if (fr_pton6(ipaddr, value, -1, true, false) < 0) goto failed;
+		if (fr_item_validate_ipaddr(cs, name, type, value, ipaddr) < 0) return -1;
+		break;
+
+	case PW_TYPE_COMBO_IP_ADDR:
+	case PW_TYPE_COMBO_IP_PREFIX:
+		ipaddr = data;
+
+		if (fr_pton(ipaddr, value, -1, AF_UNSPEC, true) < 0) goto failed;
+		if (fr_item_validate_ipaddr(cs, name, type, value, ipaddr) < 0) return -1;
+		break;
+
+	case PW_TYPE_TIMEVAL: {
+		int sec;
+		char *end;
+		struct timeval tv;
+
+		sec = strtoul(value, &end, 10);
+		tv.tv_sec = sec;
+		tv.tv_usec = 0;
+		if (*end == '.') {
+			size_t len;
+
+			len = strlen(end + 1);
+
+			if (len > 6) {
+				cf_log_err(&(cs->item), "Too much precision for timeval");
+				return -1;
+			}
+
+			/*
+			 *	If they write "0.1", that means
+			 *	"10000" microseconds.
+			 */
+			sec = strtoul(end + 1, NULL, 10);
+			while (len < 6) {
+				sec *= 10;
+				len++;
+			}
+
+			tv.tv_usec = sec;
+		}
+		cf_log_info(cs, "%.*s\t%s = %d.%06d",
+			    cs->depth, parse_spaces, name, (int) tv.tv_sec, (int) tv.tv_usec);
+		memcpy(data, &tv, sizeof(tv));
+		}
+		break;
+
+	default:
+		/*
+		 *	If we get here, it's a sanity check error.
+		 *	It's not an error parsing the configuration
+		 *	file.
+		 */
+		rad_assert(type > PW_TYPE_INVALID);
+		rad_assert(type < PW_TYPE_MAX);
+
+		cf_log_err(&(cs->item), "type '%s' is not supported in the configuration files",
+		       fr_int2str(dict_attr_types, type, "?Unknown?"));
+		return -1;
+	} /* switch over variable type */
+
+	if (!cp) {
+		CONF_PAIR *cpn;
+
+		cpn = cf_pair_alloc(cs, name, value, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+		if (!cpn) return -1;
+		cpn->parsed = true;
+		cpn->item.filename = "<internal>";
+		cpn->item.lineno = 0;
+		cf_item_add(cs, &(cpn->item));
+	}
+
+	return rcode;
+}
+
+
+/*
+ *	A copy of cf_section_parse that initializes pointers before
+ *	parsing them.
+ */
+static void cf_section_parse_init(CONF_SECTION *cs, void *base,
+				  CONF_PARSER const *variables)
+{
+	int i;
+	void *data;
+
+	for (i = 0; variables[i].name != NULL; i++) {
+		if (variables[i].type == PW_TYPE_SUBSECTION) {
+			CONF_SECTION *subcs;
+
+			if (!variables[i].dflt) continue;
+
+			subcs = cf_section_sub_find(cs, variables[i].name);
+
+			/*
+			 *	If there's no subsection in the
+			 *	config, BUT the CONF_PARSER wants one,
+			 *	then create an empty one.  This is so
+			 *	that we can track the strings,
+			 *	etc. allocated in the subsection.
+			 */
+			if (!subcs) {
+				subcs = cf_section_alloc(cs, variables[i].name, NULL);
+				if (!subcs) return;
+
+				subcs->item.filename = cs->item.filename;
+				subcs->item.lineno = cs->item.lineno;
+				cf_item_add(cs, &(subcs->item));
+			}
+			if (base) {
+				data = ((uint8_t *)base) + variables[i].offset;
+			} else {
+				data = NULL;
+			}
+
+			cf_section_parse_init(subcs, data, (CONF_PARSER const *) variables[i].dflt);
+			continue;
+		}
+
+		if ((variables[i].type != PW_TYPE_STRING) &&
+		    (variables[i].type != PW_TYPE_FILE_INPUT) &&
+		    (variables[i].type != PW_TYPE_FILE_OUTPUT)) {
+			continue;
+		}
+
+		if (variables[i].data) {
+			*(char **) variables[i].data = NULL;
+		} else if (base) {
+			*(char **) (((char *)base) + variables[i].offset) = NULL;
+		} else {
+			continue;
+		}
+	} /* for all variables in the configuration section */
+}
+
+
+static void cf_section_parse_warn(CONF_SECTION *cs)
+{
+	CONF_ITEM *ci;
+
+	for (ci = cs->children; ci; ci = ci->next) {
+		/*
+		 *	Don't recurse on sections. We can only safely
+		 *	check conf pairs at the same level as the
+		 *	section that was just parsed.
+		 */
+		if (ci->type == CONF_ITEM_SECTION) continue;
+		if (ci->type == CONF_ITEM_PAIR) {
+			CONF_PAIR *cp;
+
+			cp = cf_item_to_pair(ci);
+			if (cp->parsed) continue;
+
+			WARN("%s[%d]: The item '%s' is defined, but is unused by the configuration",
+			     cp->item.filename ? cp->item.filename : "unknown",
+			     cp->item.lineno ? cp->item.lineno : 0,
+				cp->attr);
+		}
+
+		/*
+		 *	Skip everything else.
+		 */
+	}
+}
+
+/** Parse a configuration section into user-supplied variables
+ *
+ * @param cs to parse.
+ * @param base pointer to a struct to fill with data.  Any buffers will also be talloced
+ *	using this parent as a pointer.
+ * @param variables mappings between struct fields and #CONF_ITEM s.
+ * @return
+ *	- 0 on success.
+ *	- -1 on general error.
+ *	- -2 if a deprecated #CONF_ITEM was found.
+ */
+int cf_section_parse(CONF_SECTION *cs, void *base, CONF_PARSER const *variables)
+{
+	int ret = 0;
+	int i;
+	void *data;
+
+	cs->variables = variables; /* this doesn't hurt anything */
+
+	if (!cs->name2) {
+		cf_log_info(cs, "%.*s%s {", cs->depth, parse_spaces, cs->name1);
+	} else {
+		cf_log_info(cs, "%.*s%s %s {", cs->depth, parse_spaces, cs->name1, cs->name2);
+	}
+
+	cf_section_parse_init(cs, base, variables);
+
+	/*
+	 *	Handle the known configuration parameters.
+	 */
+	for (i = 0; variables[i].name != NULL; i++) {
+		/*
+		 *	Handle subsections specially
+		 */
+		if (variables[i].type == PW_TYPE_SUBSECTION) {
+			CONF_SECTION *subcs;
+
+			subcs = cf_section_sub_find(cs, variables[i].name);
+			/*
+			 *	Default in this case is overloaded to mean a pointer
+			 *	to the CONF_PARSER struct for the subsection.
+			 */
+			if (!variables[i].dflt || !subcs) {
+				ERROR("Internal sanity check 1 failed in cf_section_parse %s", variables[i].name);
+				ret = -1;
+				goto finish;
+			}
+
+			if (base) {
+				data = ((uint8_t *)base) + variables[i].offset;
+			} else {
+				data = NULL;
+			}
+
+			ret = cf_section_parse(subcs, data, (CONF_PARSER const *) variables[i].dflt);
+			if (ret < 0) goto finish;
+			continue;
+		} /* else it's a CONF_PAIR */
+
+		if (variables[i].data) {
+			data = variables[i].data; /* prefer this. */
+		} else if (base) {
+			data = ((char *)base) + variables[i].offset;
+		} else {
+			ERROR("Internal sanity check 2 failed in cf_section_parse");
+			ret = -1;
+			goto finish;
+		}
+
+		/*
+		 *	Parse the pair we found, or a default value.
+		 */
+		ret = cf_item_parse(cs, variables[i].name, variables[i].type, data, variables[i].dflt);
+		switch (ret) {
+		case 1:		/* Used default */
+			ret = 0;
+			break;
+
+		case 0:		/* OK */
+			break;
+
+		case -1:	/* Parse error */
+			goto finish;
+
+		case -2:	/* Deprecated CONF ITEM */
+			if ((variables[i + 1].offset == variables[i].offset) &&
+			    (variables[i + 1].data == variables[i].data)) {
+				cf_log_err(&(cs->item), "Replace \"%s\" with \"%s\"", variables[i].name,
+					   variables[i + 1].name);
+			} else {
+				cf_log_err(&(cs->item), "Cannot use deprecated configuration item \"%s\"", variables[i].name);
+			}
+			goto finish;
+		}
+	} /* for all variables in the configuration section */
+
+	/*
+	 *	Ensure we have a proper terminator, type so we catch
+	 *	missing terminators reliably
+	 */
+	rad_assert(variables[i].type == -1);
+
+	/*
+	 *	Warn about items in the configuration which weren't
+	 *	checked during parsing.
+	 */
+	if (rad_debug_lvl >= 3) cf_section_parse_warn(cs);
+
+	cs->base = base;
+
+	cf_log_info(cs, "%.*s}", cs->depth, parse_spaces);
+
+finish:
+	return ret;
+}
+
+
+/*
+ *	Check XLAT things in pass 2.  But don't cache the xlat stuff anywhere.
+ */
+int cf_section_parse_pass2(CONF_SECTION *cs, void *base, CONF_PARSER const *variables)
+{
+	int i;
+	ssize_t slen;
+	char const *error;
+	char *value = NULL;
+	xlat_exp_t *xlat;
+
+	/*
+	 *	Handle the known configuration parameters.
+	 */
+	for (i = 0; variables[i].name != NULL; i++) {
+		CONF_PAIR *cp;
+		void *data;
+
+		/*
+		 *	Handle subsections specially
+		 */
+		if (variables[i].type == PW_TYPE_SUBSECTION) {
+			CONF_SECTION *subcs;
+			subcs = cf_section_sub_find(cs, variables[i].name);
+
+			if (cf_section_parse_pass2(subcs, (uint8_t *)base + variables[i].offset,
+						   (CONF_PARSER const *) variables[i].dflt) < 0) {
+				return -1;
+			}
+			continue;
+		} /* else it's a CONF_PAIR */
+
+		/*
+		 *	Figure out which data we need to fix.
+		 */
+		if (variables[i].data) {
+			data = variables[i].data; /* prefer this. */
+		} else if (base) {
+			data = ((char *)base) + variables[i].offset;
+		} else {
+			data = NULL;
+		}
+
+		cp = cf_pair_find(cs, variables[i].name);
+		xlat = NULL;
+
+	redo:
+		if (!cp || !cp->value || !data) continue;
+
+		if ((cp->rhs_type != T_DOUBLE_QUOTED_STRING) &&
+		    (cp->rhs_type != T_BARE_WORD)) continue;
+
+		/*
+		 *	Non-xlat expansions shouldn't have xlat!
+		 */
+		if (((variables[i].type & PW_TYPE_XLAT) == 0) &&
+		    ((variables[i].type & PW_TYPE_TMPL) == 0)) {
+			/*
+			 *	Ignore %{... in shared secrets.
+			 *	They're never dynamically expanded.
+			 */
+			if ((variables[i].type & PW_TYPE_SECRET) != 0) continue;
+
+			if (strstr(cp->value, "%{") != NULL) {
+				WARN("%s[%d]: Found dynamic expansion in string which will not be dynamically expanded",
+				     cp->item.filename ? cp->item.filename : "unknown",
+				     cp->item.lineno ? cp->item.lineno : 0);
+			}
+			continue;
+		}
+
+		/*
+		 *	Parse (and throw away) the xlat string.
+		 *
+		 *	FIXME: All of these should be converted from PW_TYPE_XLAT
+		 *	to PW_TYPE_TMPL.
+		 */
+		if ((variables[i].type & PW_TYPE_XLAT) != 0) {
+			/*
+			 *	xlat expansions should be parseable.
+			 */
+			value = talloc_strdup(cs, cp->value); /* modified by xlat_tokenize */
+			xlat = NULL;
+
+			slen = xlat_tokenize(cs, value, &xlat, &error);
+			if (slen < 0) {
+				char *spaces, *text;
+
+			error:
+				fr_canonicalize_error(cs, &spaces, &text, slen, cp->value);
+
+				cf_log_err(&cp->item, "Failed parsing expanded string:");
+				cf_log_err(&cp->item, "%s", text);
+				cf_log_err(&cp->item, "%s^ %s", spaces, error);
+
+				talloc_free(spaces);
+				talloc_free(text);
+				talloc_free(value);
+				talloc_free(xlat);
+				return -1;
+			}
+
+			talloc_free(value);
+			talloc_free(xlat);
+		}
+
+		/*
+		 *	Convert the LITERAL template to the actual
+		 *	type.
+		 */
+		if ((variables[i].type & PW_TYPE_TMPL) != 0) {
+			vp_tmpl_t *vpt;
+
+			slen = tmpl_afrom_str(cs, &vpt, cp->value, talloc_array_length(cp->value) - 1,
+					      cp->rhs_type,
+					      REQUEST_CURRENT, PAIR_LIST_REQUEST, true);
+			if (slen < 0) {
+				error = fr_strerror();
+				goto error;
+			}
+
+			/*
+			 *	Sanity check
+			 *
+			 *	Don't add default - update with new types.
+			 */
+			switch (vpt->type) {
+			/*
+			 *	All attributes should have been defined by this point.
+			 */
+			case TMPL_TYPE_ATTR_UNDEFINED:
+				cf_log_err(&cp->item, "Unknown attribute '%s'", vpt->tmpl_unknown_name);
+				return -1;
+
+			case TMPL_TYPE_LITERAL:
+			case TMPL_TYPE_ATTR:
+			case TMPL_TYPE_LIST:
+			case TMPL_TYPE_DATA:
+			case TMPL_TYPE_EXEC:
+			case TMPL_TYPE_XLAT:
+			case TMPL_TYPE_XLAT_STRUCT:
+				break;
+
+			case TMPL_TYPE_UNKNOWN:
+			case TMPL_TYPE_REGEX:
+			case TMPL_TYPE_REGEX_STRUCT:
+			case TMPL_TYPE_NULL:
+				rad_assert(0);
+			}
+
+			talloc_free(*(vp_tmpl_t **)data);
+			*(vp_tmpl_t **)data = vpt;
+		}
+
+		/*
+		 *	If the "multi" flag is set, check all of them.
+		 */
+		if ((variables[i].type & PW_TYPE_MULTI) != 0) {
+			cp = cf_pair_find_next(cs, cp, cp->attr);
+			goto redo;
+		}
+	} /* for all variables in the configuration section */
+
+	return 0;
+}
+
+/*
+ *	Merge the template so everyting else "just works".
+ */
+static bool cf_template_merge(CONF_SECTION *cs, CONF_SECTION const *template)
+{
+	CONF_ITEM *ci;
+
+	if (!cs || !template) return true;
+
+	cs->template = NULL;
+
+	/*
+	 *	Walk over the template, adding its' entries to the
+	 *	current section.  But only if the entries don't
+	 *	already exist in the current section.
+	 */
+	for (ci = template->children; ci; ci = ci->next) {
+		if (ci->type == CONF_ITEM_PAIR) {
+			CONF_PAIR *cp1, *cp2;
+
+			/*
+			 *	It exists, don't over-write it.
+			 */
+			cp1 = cf_item_to_pair(ci);
+			if (cf_pair_find(cs, cp1->attr)) {
+				continue;
+			}
+
+			/*
+			 *	Create a new pair with all of the data
+			 *	of the old one.
+			 */
+			cp2 = cf_pair_dup(cs, cp1);
+			if (!cp2) return false;
+
+			cp2->item.filename = cp1->item.filename;
+			cp2->item.lineno = cp1->item.lineno;
+
+			cf_item_add(cs, &(cp2->item));
+			continue;
+		}
+
+		if (ci->type == CONF_ITEM_SECTION) {
+			CONF_SECTION *subcs1, *subcs2;
+
+			subcs1 = cf_item_to_section(ci);
+			rad_assert(subcs1 != NULL);
+
+			subcs2 = cf_section_sub_find_name2(cs, subcs1->name1, subcs1->name2);
+			if (subcs2) {
+				/*
+				 *	sub-sections get merged.
+				 */
+				if (!cf_template_merge(subcs2, subcs1)) {
+					return false;
+				}
+				continue;
+			}
+
+			/*
+			 *	Our section doesn't have a matching
+			 *	sub-section.  Copy it verbatim from
+			 *	the template.
+			 */
+			subcs2 = cf_section_dup(cs, subcs1,
+						cf_section_name1(subcs1), cf_section_name2(subcs1),
+						false);
+			if (!subcs2) return false;
+
+			subcs2->item.filename = subcs1->item.filename;
+			subcs2->item.lineno = subcs1->item.lineno;
+
+			cf_item_add(cs, &(subcs2->item));
+			continue;
+		}
+
+		/* ignore everything else */
+	}
+
+	return true;
+}
+
+static char const *cf_local_file(char const *base, char const *filename,
+				 char *buffer, size_t bufsize)
+{
+	size_t dirsize;
+	char *p;
+
+	strlcpy(buffer, base, bufsize);
+
+	p = strrchr(buffer, FR_DIR_SEP);
+	if (!p) return filename;
+	if (p[1]) {		/* ./foo */
+		p[1] = '\0';
+	}
+
+	dirsize = (p - buffer) + 1;
+
+	if ((dirsize + strlen(filename)) >= bufsize) {
+		return NULL;
+	}
+
+	strlcpy(p + 1, filename, bufsize - dirsize);
+
+	return buffer;
+}
+
+
+/*
+ *	Read a part of the config file.
+ */
+static int cf_section_read(char const *filename, int *lineno, FILE *fp,
+			   CONF_SECTION *current)
+
+{
+	CONF_SECTION *this, *css;
+	CONF_PAIR *cpn;
+	char const *ptr;
+	char const *value;
+	char buf[8192];
+	char buf1[8192];
+	char buf2[8192];
+	char buf3[8192];
+	char buf4[8192];
+	FR_TOKEN t1 = T_INVALID, t2, t3;
+	bool has_spaces = false;
+	bool pass2;
+	char *cbuf = buf;
+	size_t len;
+
+	this = current;		/* add items here */
+
+	/*
+	 *	Read, checking for line continuations ('\\' at EOL)
+	 */
+	for (;;) {
+		int at_eof;
+		css = NULL;
+
+		/*
+		 *	Get data, and remember if we are at EOF.
+		 */
+		at_eof = (fgets(cbuf, sizeof(buf) - (cbuf - buf), fp) == NULL);
+		(*lineno)++;
+
+		/*
+		 *	We read the entire 8k worth of data: complain.
+		 *	Note that we don't care if the last character
+		 *	is \n: it's still forbidden.  This means that
+		 *	the maximum allowed length of text is 8k-1, which
+		 *	should be plenty.
+		 */
+		len = strlen(cbuf);
+		if ((cbuf + len + 1) >= (buf + sizeof(buf))) {
+			ERROR("%s[%d]: Line too long",
+			       filename, *lineno);
+			return -1;
+		}
+
+		if (has_spaces) {
+			ptr = cbuf;
+			while (isspace((uint8_t) *ptr)) ptr++;
+
+			if (ptr > cbuf) {
+				memmove(cbuf, ptr, len - (ptr - cbuf));
+				len -= (ptr - cbuf);
+			}
+		}
+
+		/*
+		 *	Not doing continuations: check for edge
+		 *	conditions.
+		 */
+		if (cbuf == buf) {
+			if (at_eof) break;
+
+			ptr = buf;
+			while (*ptr && isspace((uint8_t) *ptr)) ptr++;
+
+			if (!*ptr || (*ptr == '#')) continue;
+
+		} else if (at_eof || (len == 0)) {
+			ERROR("%s[%d]: Continuation at EOF is illegal",
+			       filename, *lineno);
+			return -1;
+		}
+
+		/*
+		 *	See if there's a continuation.
+		 */
+		while ((len > 0) &&
+		       ((cbuf[len - 1] == '\n') || (cbuf[len - 1] == '\r'))) {
+			len--;
+			cbuf[len] = '\0';
+		}
+
+		if ((len > 0) && (cbuf[len - 1] == '\\')) {
+			/*
+			 *	Check for "suppress spaces" magic.
+			 */
+			if (!has_spaces && (len > 2) && (cbuf[len - 2] == '"')) {
+				has_spaces = true;
+			}
+
+			cbuf[len - 1] = '\0';
+			cbuf += len - 1;
+			continue;
+		}
+
+		ptr = cbuf = buf;
+		has_spaces = false;
+
+	get_more:
+		pass2 = false;
+
+		/*
+		 *	The parser is getting to be evil.
+		 */
+		while ((*ptr == ' ') || (*ptr == '\t')) ptr++;
+
+		if (((ptr[0] == '%') && (ptr[1] == '{')) ||
+		    (ptr[0] == '`')) {
+			int hack;
+
+			if (ptr[0] == '%') {
+				hack = rad_copy_variable(buf1, ptr);
+			} else {
+				hack = rad_copy_string(buf1, ptr);
+			}
+			if (hack < 0) {
+				ERROR("%s[%d]: Invalid expansion: %s",
+				       filename, *lineno, ptr);
+				return -1;
+			}
+
+			ptr += hack;
+
+			t2 = gettoken(&ptr, buf2, sizeof(buf2), true);
+			switch (t2) {
+			case T_EOL:
+			case T_HASH:
+				goto do_bare_word;
+
+			default:
+				ERROR("%s[%d]: Invalid expansion: %s",
+				       filename, *lineno, ptr);
+				return -1;
+			}
+		} else {
+			t1 = gettoken(&ptr, buf1, sizeof(buf1), true);
+		}
+
+		/*
+		 *	The caller eats "name1 name2 {", and calls us
+		 *	for the data inside of the section.  So if we
+		 *	receive a closing brace, then it must mean the
+		 *	end of the section.
+		 */
+	       if (t1 == T_RCBRACE) {
+		       if (this == current) {
+			       ERROR("%s[%d]: Too many closing braces",
+				      filename, *lineno);
+			       return -1;
+		       }
+
+		       /*
+			*	Merge the template into the existing
+			*	section.  This uses more memory, but
+			*	means that templates now work with
+			*	sub-sections, etc.
+			*/
+		       if (!cf_template_merge(this, this->template)) {
+			       return -1;
+		       }
+
+		       this = this->item.parent;
+		       goto check_for_more;
+	       }
+
+	       if (t1 != T_BARE_WORD) goto skip_keywords;
+
+		/*
+		 *	Allow for $INCLUDE files
+		 *
+		 *      This *SHOULD* work for any level include.
+		 *      I really really really hate this file.  -cparker
+		 */
+	       if ((strcasecmp(buf1, "$INCLUDE") == 0) ||
+		   (strcasecmp(buf1, "$-INCLUDE") == 0)) {
+			bool relative = true;
+
+			t2 = getword(&ptr, buf2, sizeof(buf2), true);
+			if (t2 != T_EOL) {
+			       ERROR("%s[%d]: Unexpected text after $INCLUDE",
+				     filename, *lineno);
+			       return -1;
+			}
+
+			if (buf2[0] == '$') relative = false;
+
+			value = cf_expand_variables(filename, lineno, this, buf4, sizeof(buf4), buf2, NULL);
+			if (!value) return -1;
+
+			if (!FR_DIR_IS_RELATIVE(value)) relative = false;
+
+			if (relative) {
+				value = cf_local_file(filename, value, buf3,
+						      sizeof(buf3));
+				if (!value) {
+					ERROR("%s[%d]: Directories too deep.",
+					       filename, *lineno);
+					return -1;
+				}
+			}
+
+
+#ifdef HAVE_DIRENT_H
+			/*
+			 *	$INCLUDE foo/
+			 *
+			 *	Include ALL non-"dot" files in the directory.
+			 *	careful!
+			 */
+			if (value[strlen(value) - 1] == '/') {
+				DIR		*dir;
+				struct dirent	*dp;
+				struct stat stat_buf;
+
+				DEBUG2("including files in directory %s", value );
+#ifdef S_IWOTH
+				/*
+				 *	Security checks.
+				 */
+				if (stat(value, &stat_buf) < 0) {
+					ERROR("%s[%d]: Failed reading directory %s: %s",
+					       filename, *lineno,
+					       value, fr_syserror(errno));
+					return -1;
+				}
+
+				if ((stat_buf.st_mode & S_IWOTH) != 0) {
+					ERROR("%s[%d]: Directory %s is globally writable.  Refusing to start due to "
+					      "insecure configuration", filename, *lineno, value);
+					return -1;
+				}
+#endif
+				dir = opendir(value);
+				if (!dir) {
+					ERROR("%s[%d]: Error reading directory %s: %s",
+					       filename, *lineno, value,
+					       fr_syserror(errno));
+					return -1;
+				}
+
+				/*
+				 *	Read the directory, ignoring "." files.
+				 */
+				while ((dp = readdir(dir)) != NULL) {
+					char const *p;
+					int slen;
+
+					if (dp->d_name[0] == '.') continue;
+
+					/*
+					 *	Check for valid characters
+					 */
+					for (p = dp->d_name; *p != '\0'; p++) {
+						if (isalpha((uint8_t)*p) ||
+						    isdigit((uint8_t)*p) ||
+						    (*p == '-') ||
+						    (*p == '_') ||
+						    (*p == '.')) continue;
+						break;
+					}
+					if (*p != '\0') continue;
+
+					slen = snprintf(buf2, sizeof(buf2), "%s%s",
+							value, dp->d_name);
+					if (slen >= (int) sizeof(buf2) || slen < 0) {
+						ERROR("%s: Full file path is too long.", dp->d_name);
+						return -1;
+					}
+					if ((stat(buf2, &stat_buf) != 0) ||
+					    S_ISDIR(stat_buf.st_mode)) continue;
+
+					/*
+					 *	Read the file into the current
+					 *	configuration section.
+					 */
+					if (cf_file_include(this, buf2, true) < 0) {
+						closedir(dir);
+						return -1;
+					}
+				}
+				closedir(dir);
+			}  else
+#endif
+			{ /* it was a normal file */
+				if (buf1[1] == '-') {
+					struct stat statbuf;
+
+					if (stat(value, &statbuf) < 0) {
+						WARN("Not including file %s: %s", value, fr_syserror(errno));
+						continue;
+					}
+				}
+
+				if (cf_file_include(this, value, false) < 0) {
+					return -1;
+				}
+			}
+			continue;
+		} /* we were in an include */
+
+	       if (strcasecmp(buf1, "$template") == 0) {
+		       CONF_ITEM *ci;
+		       CONF_SECTION *parentcs, *templatecs;
+		       t2 = getword(&ptr, buf2, sizeof(buf2), true);
+
+		       if (t2 != T_EOL) {
+			       ERROR("%s[%d]: Unexpected text after $TEMPLATE", filename, *lineno);
+			       return -1;
+		       }
+
+		       parentcs = cf_top_section(current);
+
+		       templatecs = cf_section_sub_find(parentcs, "templates");
+		       if (!templatecs) {
+				ERROR("%s[%d]: No \"templates\" section for reference \"%s\"", filename, *lineno, buf2);
+				return -1;
+		       }
+
+		       ci = cf_reference_item(parentcs, templatecs, buf2);
+		       if (!ci || (ci->type != CONF_ITEM_SECTION)) {
+				ERROR("%s[%d]: Reference \"%s\" not found", filename, *lineno, buf2);
+				return -1;
+		       }
+
+		       if (!this) {
+				ERROR("%s[%d]: Internal sanity check error in template reference", filename, *lineno);
+				return -1;
+		       }
+
+		       if (this->template) {
+				ERROR("%s[%d]: Section already has a template", filename, *lineno);
+				return -1;
+		       }
+
+		       this->template = cf_item_to_section(ci);
+		       continue;
+	       }
+
+		/*
+		 *	Ensure that the user can't add CONF_PAIRs
+		 *	with 'internal' names;
+		 */
+		if (buf1[0] == '_') {
+			ERROR("%s[%d]: Illegal configuration pair name \"%s\"", filename, *lineno, buf1);
+			return -1;
+		}
+
+		/*
+		 *	Handle if/elsif specially.
+		 */
+		if ((strcmp(buf1, "if") == 0) || (strcmp(buf1, "elsif") == 0)) {
+			ssize_t slen;
+			char const *error = NULL;
+			char *p;
+			CONF_SECTION *server;
+			fr_cond_t *cond = NULL;
+
+			/*
+			 *	if / elsif MUST be inside of a
+			 *	processing section, which MUST in turn
+			 *	be inside of a "server" directive.
+			 */
+			if (!this->item.parent) {
+			invalid_location:
+				ERROR("%s[%d]: Invalid location for '%s'",
+				       filename, *lineno, buf1);
+				return -1;
+			}
+
+			/*
+			 *	Can only have "if" in 3 named sections.
+			 */
+			server = this->item.parent;
+			while (server &&
+			       (strcmp(server->name1, "server") != 0) &&
+			       (strcmp(server->name1, "policy") != 0) &&
+			       (strcmp(server->name1, "instantiate") != 0)) {
+				server = server->item.parent;
+				if (!server) goto invalid_location;
+			}
+
+			/*
+			 *	Skip (...) to find the {
+			 */
+			slen = fr_condition_tokenize(this, cf_section_to_item(this), ptr, &cond,
+						     &error, FR_COND_TWO_PASS);
+			memcpy(&p, &ptr, sizeof(p));
+
+			if (slen < 0) {
+				if (p[-slen] != '{') goto cond_error;
+				slen = -slen;
+			}
+			TALLOC_FREE(cond);
+
+			/*
+			 *	This hack is so that the NEXT stage
+			 *	doesn't go "too far" in expanding the
+			 *	variable.  We can parse the conditions
+			 *	without expanding the ${...} stuff.
+			 *	BUT we don't want to expand all of the
+			 *	stuff AFTER the condition.  So we do
+			 *	two passes.
+			 *
+			 *	The first pass is to discover the end
+			 *	of the condition.  We then expand THAT
+			 *	string, and do a second pass parsing
+			 *	the expanded condition.
+			 */
+			p += slen;
+			*p = '\0';
+
+			/*
+			 *	If there's a ${...}.  If so, expand it.
+			 */
+			if (strchr(ptr, '$') != NULL) {
+				ptr = cf_expand_variables(filename, lineno,
+							  this,
+							  buf3, sizeof(buf3),
+							  ptr, NULL);
+				if (!ptr) {
+					ERROR("%s[%d]: Parse error expanding ${...} in condition",
+					      filename, *lineno);
+					return -1;
+				}
+			} /* else leave it alone */
+
+			css = cf_section_alloc(this, buf1, ptr);
+			if (!css) {
+				ERROR("%s[%d]: Failed allocating memory for section",
+				      filename, *lineno);
+				return -1;
+			}
+			css->item.filename = filename;
+			css->item.lineno = *lineno;
+
+			slen = fr_condition_tokenize(css, cf_section_to_item(css), ptr, &cond,
+						     &error, FR_COND_TWO_PASS);
+			*p = '{'; /* put it back */
+
+		cond_error:
+			if (slen < 0) {
+				char *spaces, *text;
+
+				fr_canonicalize_error(this, &spaces, &text, slen, ptr);
+
+				ERROR("%s[%d]: Parse error in condition",
+				      filename, *lineno);
+				ERROR("%s[%d]: %s", filename, *lineno, text);
+				ERROR("%s[%d]: %s^ %s", filename, *lineno, spaces, error);
+
+				talloc_free(spaces);
+				talloc_free(text);
+				talloc_free(css);
+				return -1;
+			}
+
+			if ((size_t) slen >= (sizeof(buf2) - 1)) {
+				talloc_free(css);
+				ERROR("%s[%d]: Condition is too large after \"%s\"",
+				       filename, *lineno, buf1);
+				return -1;
+			}
+
+			/*
+			 *	Copy the expanded and parsed condition
+			 *	into buf2.  Then, parse the text after
+			 *	the condition, which now MUST be a '{.
+			 *
+			 *	If it wasn't '{' it would have been
+			 *	caught in the first pass of
+			 *	conditional parsing, above.
+			 */
+			memcpy(buf2, ptr, slen);
+			buf2[slen] = '\0';
+			ptr = p;
+
+			if ((t3 = gettoken(&ptr, buf3, sizeof(buf3), true)) != T_LCBRACE) {
+				talloc_free(css);
+				ERROR("%s[%d]: Expected '{' %d",
+				      filename, *lineno, t3);
+				return -1;
+			}
+
+			/*
+			 *	Swap the condition with trailing stuff for
+			 *	the final condition.
+			 */
+			memcpy(&p, &css->name2, sizeof(css->name2));
+			talloc_free(p);
+			css->name2 = talloc_typed_strdup(css, buf2);
+
+			cf_item_add(this, &(css->item));
+			cf_data_add_internal(css, "if", cond, NULL, false);
+
+			/*
+			 *	The current section is now the child section.
+			 */
+			this = css;
+			css = NULL;
+			goto check_for_more;
+		}
+
+	skip_keywords:
+		/*
+		 *	Grab the next token.
+		 */
+		t2 = gettoken(&ptr, buf2, sizeof(buf2), !cf_new_escape);
+		switch (t2) {
+		case T_EOL:
+		case T_HASH:
+		case T_COMMA:
+		do_bare_word:
+			t3 = t2;
+			t2 = T_OP_EQ;
+			value = NULL;
+			goto do_set;
+
+		case T_OP_INCRM:
+		case T_OP_ADD:
+		case T_OP_CMP_EQ:
+		case T_OP_SUB:
+		case T_OP_LE:
+		case T_OP_GE:
+		case T_OP_CMP_FALSE:
+			if (!this || (strcmp(this->name1, "update") != 0)) {
+				ERROR("%s[%d]: Invalid operator in assignment",
+				       filename, *lineno);
+				return -1;
+			}
+			/* FALL-THROUGH */
+
+		case T_OP_EQ:
+		case T_OP_SET:
+		case T_OP_PREPEND:
+			while (isspace((uint8_t) *ptr)) ptr++;
+
+			/*
+			 *	Be a little more forgiving.
+			 */
+			if (*ptr == '#') {
+				t3 = T_HASH;
+			} else
+
+			/*
+			 *	New parser: non-quoted strings are
+			 *	bare words, and we parse everything
+			 *	until the next newline, or the next
+			 *	comma.  If they have { or } in a bare
+			 *	word, well... too bad.
+			 */
+			if (cf_new_escape && (*ptr != '"') && (*ptr != '\'')
+			    && (*ptr != '`') && (*ptr != '/')) {
+				const char *q = ptr;
+
+				t3 = T_BARE_WORD;
+				while (*q && (*q >= ' ') && (*q != ',') &&
+				       !isspace((uint8_t) *q)) q++;
+
+				if ((size_t) (q - ptr) >= sizeof(buf3)) {
+					ERROR("%s[%d]: Parse error: value too long",
+					      filename, *lineno);
+					return -1;
+				}
+
+				memcpy(buf3, ptr, (q - ptr));
+				buf3[q - ptr] = '\0';
+				ptr = q;
+
+			} else {
+				t3 = getstring(&ptr, buf3, sizeof(buf3), !cf_new_escape);
+			}
+
+			if (t3 == T_INVALID) {
+				ERROR("%s[%d]: Parse error: %s",
+				       filename, *lineno,
+				       fr_strerror());
+				return -1;
+			}
+
+			/*
+			 *	Allow "foo" by itself, or "foo = bar"
+			 */
+			switch (t3) {
+				bool soft_fail;
+
+			case T_BARE_WORD:
+			case T_DOUBLE_QUOTED_STRING:
+			case T_BACK_QUOTED_STRING:
+				value = cf_expand_variables(filename, lineno, this, buf4, sizeof(buf4), buf3, &soft_fail);
+				if (!value) {
+					if (!soft_fail) return -1;
+
+					/*
+					 *	References an item which doesn't exist,
+					 *	or which is already marked up as being
+					 *	expanded in pass2.  Wait for pass2 to
+					 *	do the expansions.
+					 */
+					pass2 = true;
+					value = buf3;
+				}
+				break;
+
+			case T_EOL:
+			case T_HASH:
+				value = NULL;
+				break;
+
+			default:
+				value = buf3;
+				break;
+			}
+
+			/*
+			 *	Add this CONF_PAIR to our CONF_SECTION
+			 */
+		do_set:
+			cpn = cf_pair_alloc(this, buf1, value, t2, t1, t3);
+			if (!cpn) return -1;
+			cpn->item.filename = filename;
+			cpn->item.lineno = *lineno;
+			cpn->pass2 = pass2;
+			cf_item_add(this, &(cpn->item));
+
+			/*
+			 *	Require a comma, unless there's a comment.
+			 */
+			while (isspace((uint8_t) *ptr)) ptr++;
+
+			if (*ptr == ',') {
+				ptr++;
+				break;
+			}
+
+			/*
+			 *	module # stuff!
+			 *	foo = bar # other stuff
+			 */
+			if ((t3 == T_HASH) || (t3 == T_COMMA) || (t3 == T_EOL) || (*ptr == '#')) continue;
+
+			if (!*ptr || (*ptr == '}')) break;
+
+			ERROR("%s[%d]: Syntax error: Expected comma after '%s': %s",
+			      filename, *lineno, value, ptr);
+			return -1;
+
+			/*
+			 *	No '=', must be a section or sub-section.
+			 */
+		case T_BARE_WORD:
+		case T_DOUBLE_QUOTED_STRING:
+		case T_SINGLE_QUOTED_STRING:
+			t3 = gettoken(&ptr, buf3, sizeof(buf3), true);
+			if (t3 != T_LCBRACE) {
+				ERROR("%s[%d]: Expecting section start brace '{' after \"%s %s\"",
+				       filename, *lineno, buf1, buf2);
+				return -1;
+			}
+			/* FALL-THROUGH */
+
+		case T_LCBRACE:
+			css = cf_section_alloc(this, buf1,
+					       t2 == T_LCBRACE ? NULL : buf2);
+			if (!css) {
+				ERROR("%s[%d]: Failed allocating memory for section",
+				      filename, *lineno);
+				return -1;
+			}
+
+			css->item.filename = filename;
+			css->item.lineno = *lineno;
+			cf_item_add(this, &(css->item));
+
+			/*
+			 *	There may not be a name2
+			 */
+			css->name2_type = (t2 == T_LCBRACE) ? T_INVALID : t2;
+
+			/*
+			 *	The current section is now the child section.
+			 */
+			this = css;
+			break;
+
+		case T_INVALID:
+			ERROR("%s[%d]: Syntax error in '%s': %s", filename, *lineno, ptr, fr_strerror());
+
+			return -1;
+
+		default:
+			ERROR("%s[%d]: Parse error after \"%s\": unexpected token \"%s\"",
+			      filename, *lineno, buf1, fr_int2str(fr_tokens, t2, "<INVALID>"));
+
+			return -1;
+		}
+
+	check_for_more:
+		/*
+		 *	Done parsing one thing.  Skip to EOL if possible.
+		 */
+		while (isspace((uint8_t) *ptr)) ptr++;
+
+		if (*ptr == '#') continue;
+
+		if (*ptr) {
+			goto get_more;
+		}
+
+	}
+
+	/*
+	 *	See if EOF was unexpected ..
+	 */
+	if (feof(fp) && (this != current)) {
+		ERROR("%s[%d]: EOF reached without closing brace for section %s starting at line %d",
+		      filename, *lineno, cf_section_name1(this), cf_section_lineno(this));
+		return -1;
+	}
+
+	return 0;
+}
+
+/*
+ *	Include one config file in another.
+ */
+static int cf_file_include(CONF_SECTION *cs, char const *filename_in, bool from_dir)
+{
+	FILE		*fp;
+	int		rcode;
+	int		lineno = 0;
+	char const	*filename;
+
+	/*
+	 *	So we only need to do this once.
+	 */
+	filename = talloc_strdup(cs, filename_in);
+
+	/*
+	 *	This may return "0" if we already loaded the file.
+	 */
+	rcode = cf_file_open(cs, filename, from_dir, &fp);
+	if (rcode <= 0) return rcode;
+
+	if (!cs->item.filename) cs->item.filename = filename;
+
+	/*
+	 *	Read the section.  It's OK to have EOF without a
+	 *	matching close brace.
+	 */
+	if (cf_section_read(filename, &lineno, fp, cs) < 0) {
+		fclose(fp);
+		return -1;
+	}
+
+	fclose(fp);
+	return 0;
+}
+
+
+/*
+ *	Do variable expansion in pass2.
+ *
+ *	This is a breadth-first expansion.  "deep
+ */
+static int cf_section_pass2(CONF_SECTION *cs)
+{
+	CONF_ITEM *ci;
+
+	for (ci = cs->children; ci; ci = ci->next) {
+		char const *value;
+		CONF_PAIR *cp;
+		char buffer[8192];
+
+		if (ci->type != CONF_ITEM_PAIR) continue;
+
+		cp = cf_item_to_pair(ci);
+		if (!cp->value || !cp->pass2) continue;
+
+		rad_assert((cp->rhs_type == T_BARE_WORD) ||
+			   (cp->rhs_type == T_DOUBLE_QUOTED_STRING) ||
+			   (cp->rhs_type == T_BACK_QUOTED_STRING));
+
+		value = cf_expand_variables(ci->filename, &ci->lineno, cs, buffer, sizeof(buffer), cp->value, NULL);
+		if (!value) return -1;
+
+		rad_const_free(cp->value);
+		cp->value = talloc_typed_strdup(cp, value);
+	}
+
+	for (ci = cs->children; ci; ci = ci->next) {
+		if (ci->type != CONF_ITEM_SECTION) continue;
+
+		if (cf_section_pass2(cf_item_to_section(ci)) < 0) return -1;
+	}
+
+	return 0;
+}
+
+
+/*
+ *	Bootstrap a config file.
+ */
+int cf_file_read(CONF_SECTION *cs, char const *filename)
+{
+	char *p;
+	CONF_PAIR *cp;
+	rbtree_t *tree;
+
+	cp = cf_pair_alloc(cs, "confdir", filename, T_OP_SET, T_BARE_WORD, T_SINGLE_QUOTED_STRING);
+	if (!cp) return -1;
+
+	p = strrchr(cp->value, FR_DIR_SEP);
+	if (p) *p = '\0';
+
+	cp->item.filename = "<internal>";
+	cp->item.lineno = -1;
+	cf_item_add(cs, &(cp->item));
+
+	tree = rbtree_create(cs, filename_cmp, NULL, 0);
+	if (!tree) return -1;
+
+	cf_data_add_internal(cs, "filename", tree, NULL, 0);
+
+	if (cf_file_include(cs, filename, false) < 0) return -1;
+
+	/*
+	 *	Now that we've read the file, go back through it and
+	 *	expand the variables.
+	 */
+	if (cf_section_pass2(cs) < 0) return -1;
+
+	return 0;
+}
+
+
+void cf_file_free(CONF_SECTION *cs)
+{
+	talloc_free(cs);
+}
+
+
+/*
+ * Return a CONF_PAIR within a CONF_SECTION.
+ */
+CONF_PAIR *cf_pair_find(CONF_SECTION const *cs, char const *name)
+{
+	CONF_PAIR *cp, mycp;
+
+	if (!cs || !name) return NULL;
+
+	mycp.attr = name;
+	cp = rbtree_finddata(cs->pair_tree, &mycp);
+	if (cp) return cp;
+
+	if (!cs->template) return NULL;
+
+	return rbtree_finddata(cs->template->pair_tree, &mycp);
+}
+
+/*
+ * Return the attr of a CONF_PAIR
+ */
+
+char const *cf_pair_attr(CONF_PAIR const *pair)
+{
+	return (pair ? pair->attr : NULL);
+}
+
+/*
+ * Return the value of a CONF_PAIR
+ */
+
+char const *cf_pair_value(CONF_PAIR const *pair)
+{
+	return (pair ? pair->value : NULL);
+}
+
+FR_TOKEN cf_pair_operator(CONF_PAIR const *pair)
+{
+	return (pair ? pair->op : T_INVALID);
+}
+
+/** Return the value (lhs) type
+ *
+ * @param pair to extract value type from.
+ * @return one of T_BARE_WORD, T_SINGLE_QUOTED_STRING, T_BACK_QUOTED_STRING
+ *	T_DOUBLE_QUOTED_STRING or T_INVALID if the pair is NULL.
+ */
+FR_TOKEN cf_pair_attr_type(CONF_PAIR const *pair)
+{
+	return (pair ? pair->lhs_type : T_INVALID);
+}
+
+/** Return the value (rhs) type
+ *
+ * @param pair to extract value type from.
+ * @return one of T_BARE_WORD, T_SINGLE_QUOTED_STRING, T_BACK_QUOTED_STRING
+ *	T_DOUBLE_QUOTED_STRING or T_INVALID if the pair is NULL.
+ */
+FR_TOKEN cf_pair_value_type(CONF_PAIR const *pair)
+{
+	return (pair ? pair->rhs_type : T_INVALID);
+}
+
+/*
+ * Turn a CONF_PAIR into a VALUE_PAIR
+ * For now, ignore the "value_type" field...
+ */
+VALUE_PAIR *cf_pairtovp(CONF_PAIR *pair)
+{
+	if (!pair) {
+		fr_strerror_printf("Internal error");
+		return NULL;
+	}
+
+	if (!pair->value) {
+		fr_strerror_printf("No value given for attribute %s", pair->attr);
+		return NULL;
+	}
+
+	/*
+	 *	false comparisons never match.  BUT if it's a "string"
+	 *	or `string`, then remember to expand it later.
+	 */
+	if ((pair->op != T_OP_CMP_FALSE) &&
+	    ((pair->rhs_type == T_DOUBLE_QUOTED_STRING) ||
+	     (pair->rhs_type == T_BACK_QUOTED_STRING))) {
+		VALUE_PAIR *vp;
+
+		vp = fr_pair_make(pair, NULL, pair->attr, NULL, pair->op);
+		if (!vp) {
+			return NULL;
+		}
+
+		if (fr_pair_mark_xlat(vp, pair->value) < 0) {
+			talloc_free(vp);
+
+			return NULL;
+		}
+
+		return vp;
+	}
+
+	return fr_pair_make(pair, NULL, pair->attr, pair->value, pair->op);
+}
+
+/*
+ * Return the first label of a CONF_SECTION
+ */
+
+char const *cf_section_name1(CONF_SECTION const *cs)
+{
+	return (cs ? cs->name1 : NULL);
+}
+
+/*
+ * Return the second label of a CONF_SECTION
+ */
+
+char const *cf_section_name2(CONF_SECTION const *cs)
+{
+	return (cs ? cs->name2 : NULL);
+}
+
+/** Return name2 if set, else name1
+ *
+ */
+char const *cf_section_name(CONF_SECTION const *cs)
+{
+	char const *name;
+
+	name = cf_section_name2(cs);
+	if (name) return name;
+
+	return cf_section_name1(cs);
+}
+
+/*
+ * Find a value in a CONF_SECTION
+ */
+char const *cf_section_value_find(CONF_SECTION const *cs, char const *attr)
+{
+	CONF_PAIR	*cp;
+
+	cp = cf_pair_find(cs, attr);
+
+	return (cp ? cp->value : NULL);
+}
+
+
+CONF_SECTION *cf_section_find_name2(CONF_SECTION const *cs,
+				    char const *name1, char const *name2)
+{
+	char const	*their2;
+	CONF_ITEM const *ci;
+
+	if (!cs || !name1) return NULL;
+
+	for (ci = &(cs->item); ci; ci = ci->next) {
+		if (ci->type != CONF_ITEM_SECTION)
+			continue;
+
+		if (strcmp(cf_item_to_section(ci)->name1, name1) != 0) {
+			continue;
+		}
+
+		their2 = cf_item_to_section(ci)->name2;
+
+		if ((!name2 && !their2) ||
+		    (name2 && their2 && (strcmp(name2, their2) == 0))) {
+			return cf_item_to_section(ci);
+		}
+	}
+
+	return NULL;
+}
+
+/** Find a pair with a name matching attr, after specified pair.
+ *
+ * @param cs to search in.
+ * @param pair to search from (may be NULL).
+ * @param attr to find (may be NULL in which case any attribute matches).
+ * @return the next matching CONF_PAIR or NULL if none matched.
+ */
+CONF_PAIR *cf_pair_find_next(CONF_SECTION const *cs,
+			     CONF_PAIR const *pair, char const *attr)
+{
+	CONF_ITEM	*ci;
+
+	if (!cs) return NULL;
+
+	/*
+	 *	If pair is NULL and we're trying to find a specific
+	 *	attribute this must be a first time run.
+	 *
+	 *	Find the pair with correct name.
+	 */
+	if (!pair && attr) return cf_pair_find(cs, attr);
+
+	/*
+	 *	Start searching from the next child, or from the head
+	 *	of the list of children (if no pair was provided).
+	 */
+	for (ci = pair ? pair->item.next : cs->children;
+	     ci;
+	     ci = ci->next) {
+		if (ci->type != CONF_ITEM_PAIR) continue;
+
+		if (!attr || strcmp(cf_item_to_pair(ci)->attr, attr) == 0) break;
+	}
+
+	return cf_item_to_pair(ci);
+}
+
+/*
+ * Find a CONF_SECTION, or return the root if name is NULL
+ */
+
+CONF_SECTION *cf_section_find(char const *name)
+{
+	if (name)
+		return cf_section_sub_find(root_config, name);
+	else
+		return root_config;
+}
+
+/** Find a sub-section in a section
+ *
+ *	This finds ANY section having the same first name.
+ *	The second name is ignored.
+ */
+CONF_SECTION *cf_section_sub_find(CONF_SECTION const *cs, char const *name)
+{
+	CONF_SECTION mycs;
+
+	if (!cs || !name) return NULL;	/* can't find an un-named section */
+
+	/*
+	 *	No sub-sections have been defined, so none exist.
+	 */
+	if (!cs->section_tree) return NULL;
+
+	mycs.name1 = name;
+	mycs.name2 = NULL;
+	return rbtree_finddata(cs->section_tree, &mycs);
+}
+
+
+/** Find a CONF_SECTION with both names.
+ *
+ */
+CONF_SECTION *cf_section_sub_find_name2(CONF_SECTION const *cs,
+					char const *name1, char const *name2)
+{
+	CONF_ITEM    *ci;
+
+	if (!cs) cs = root_config;
+	if (!cs) return NULL;
+
+	if (name1) {
+		CONF_SECTION mycs, *master_cs;
+
+		if (!cs->section_tree) return NULL;
+
+		mycs.name1 = name1;
+		mycs.name2 = name2;
+
+		master_cs = rbtree_finddata(cs->section_tree, &mycs);
+		if (!master_cs) return NULL;
+
+		/*
+		 *	Look it up in the name2 tree.  If it's there,
+		 *	return it.
+		 */
+		if (master_cs->name2_tree) {
+			CONF_SECTION *subcs;
+
+			subcs = rbtree_finddata(master_cs->name2_tree, &mycs);
+			if (subcs) return subcs;
+		}
+
+		/*
+		 *	We don't insert ourselves into the name2 tree.
+		 *	So if there's nothing in the name2 tree, maybe
+		 *	*we* are the answer.
+		 */
+		if (!master_cs->name2 && name2) return NULL;
+		if (master_cs->name2 && !name2) return NULL;
+		if (!master_cs->name2 && !name2) return master_cs;
+
+		if (strcmp(master_cs->name2, name2) == 0) {
+			return master_cs;
+		}
+
+		return NULL;
+	}
+
+	/*
+	 *	Else do it the old-fashioned way.
+	 */
+	for (ci = cs->children; ci; ci = ci->next) {
+		CONF_SECTION *subcs;
+
+		if (ci->type != CONF_ITEM_SECTION)
+			continue;
+
+		subcs = cf_item_to_section(ci);
+		if (!subcs->name2) {
+			if (strcmp(subcs->name1, name2) == 0) break;
+		} else {
+			if (strcmp(subcs->name2, name2) == 0) break;
+		}
+	}
+
+	return cf_item_to_section(ci);
+}
+
+/*
+ * Return the next subsection after a CONF_SECTION
+ * with a certain name1 (char *name1). If the requested
+ * name1 is NULL, any name1 matches.
+ */
+
+CONF_SECTION *cf_subsection_find_next(CONF_SECTION const *section,
+				      CONF_SECTION const *subsection,
+				      char const *name1)
+{
+	CONF_ITEM	*ci;
+
+	if (!section) return NULL;
+
+	/*
+	 * If subsection is NULL this must be a first time run
+	 * Find the subsection with correct name
+	 */
+
+	if (!subsection) {
+		ci = section->children;
+	} else {
+		ci = subsection->item.next;
+	}
+
+	for (; ci; ci = ci->next) {
+		if (ci->type != CONF_ITEM_SECTION)
+			continue;
+		if ((name1 == NULL) ||
+		    (strcmp(cf_item_to_section(ci)->name1, name1) == 0))
+			break;
+	}
+
+	return cf_item_to_section(ci);
+}
+
+
+/*
+ * Return the next section after a CONF_SECTION
+ * with a certain name1 (char *name1). If the requested
+ * name1 is NULL, any name1 matches.
+ */
+
+CONF_SECTION *cf_section_find_next(CONF_SECTION const *section,
+				   CONF_SECTION const *subsection,
+				   char const *name1)
+{
+	if (!section) return NULL;
+
+	if (!section->item.parent) return NULL;
+
+	return cf_subsection_find_next(section->item.parent, subsection, name1);
+}
+
+/** Return the next item after a CONF_ITEM.
+ *
+ */
+CONF_ITEM *cf_item_find_next(CONF_SECTION const *section, CONF_ITEM const *item)
+{
+	if (!section) return NULL;
+
+	/*
+	 *	If item is NULL this must be a first time run
+	 * 	Return the first item
+	 */
+	if (item == NULL) {
+		return section->children;
+	} else {
+		return item->next;
+	}
+}
+
+static void _pair_count(int *count, CONF_SECTION const *cs)
+{
+	CONF_ITEM const *ci;
+
+	for (ci = cf_item_find_next(cs, NULL);
+	     ci != NULL;
+	     ci = cf_item_find_next(cs, ci)) {
+
+		if (cf_item_is_section(ci)) {
+			_pair_count(count, cf_item_to_section(ci));
+			continue;
+		}
+
+		(*count)++;
+	}
+}
+
+/** Count the number of conf pairs beneath a section
+ *
+ * @param[in] cs to search for items in.
+ * @return number of pairs nested within section.
+ */
+int cf_pair_count(CONF_SECTION const *cs)
+{
+	int count = 0;
+
+	_pair_count(&count, cs);
+
+	return count;
+}
+
+CONF_SECTION *cf_item_parent(CONF_ITEM const *ci)
+{
+	if (!ci) return NULL;
+
+	return ci->parent;
+}
+
+int cf_section_lineno(CONF_SECTION const *section)
+{
+	return section->item.lineno;
+}
+
+char const *cf_pair_filename(CONF_PAIR const *pair)
+{
+	return pair->item.filename;
+}
+
+char const *cf_section_filename(CONF_SECTION const *section)
+{
+	return section->item.filename;
+}
+
+int cf_pair_lineno(CONF_PAIR const *pair)
+{
+	return pair->item.lineno;
+}
+
+bool cf_item_is_section(CONF_ITEM const *item)
+{
+	return item->type == CONF_ITEM_SECTION;
+}
+
+bool cf_item_is_pair(CONF_ITEM const *item)
+{
+	return item->type == CONF_ITEM_PAIR;
+}
+
+bool cf_item_is_data(CONF_ITEM const *item)
+{
+	return item->type == CONF_ITEM_DATA;
+}
+
+static CONF_DATA *cf_data_alloc(CONF_SECTION *parent, char const *name,
+				void *data, void (*data_free)(void *))
+{
+	CONF_DATA *cd;
+
+	cd = talloc_zero(parent, CONF_DATA);
+	if (!cd) return NULL;
+
+	cd->item.type = CONF_ITEM_DATA;
+	cd->item.parent = parent;
+	cd->name = talloc_typed_strdup(cd, name);
+	if (!cd->name) {
+		talloc_free(cd);
+		return NULL;
+	}
+
+	cd->data = data;
+	cd->free = data_free;
+
+	if (cd->free) {
+		talloc_set_destructor(cd, _cf_data_free);
+	}
+
+	return cd;
+}
+
+static void *cf_data_find_internal(CONF_SECTION const *cs, char const *name, int flag)
+{
+	if (!cs || !name) return NULL;
+
+	/*
+	 *	Find the name in the tree, for speed.
+	 */
+	if (cs->data_tree) {
+		CONF_DATA mycd;
+
+		mycd.name = name;
+		mycd.flag = flag;
+		return rbtree_finddata(cs->data_tree, &mycd);
+	}
+
+	return NULL;
+}
+
+/*
+ *	Find data from a particular section.
+ */
+void *cf_data_find(CONF_SECTION const *cs, char const *name)
+{
+	CONF_DATA *cd = cf_data_find_internal(cs, name, 0);
+
+	if (cd) return cd->data;
+	return NULL;
+}
+
+
+/*
+ *	Add named data to a configuration section.
+ */
+static int cf_data_add_internal(CONF_SECTION *cs, char const *name,
+				void *data, void (*data_free)(void *),
+				int flag)
+{
+	CONF_DATA *cd;
+
+	if (!cs || !name) return -1;
+
+	/*
+	 *	Already exists.  Can't add it.
+	 */
+	if (cf_data_find_internal(cs, name, flag) != NULL) return -1;
+
+	cd = cf_data_alloc(cs, name, data, data_free);
+	if (!cd) return -1;
+	cd->flag = flag;
+
+	cf_item_add(cs, cf_data_to_item(cd));
+
+	return 0;
+}
+
+/*
+ *	Add named data to a configuration section.
+ */
+int cf_data_add(CONF_SECTION *cs, char const *name,
+		void *data, void (*data_free)(void *))
+{
+	return cf_data_add_internal(cs, name, data, data_free, 0);
+}
+
+/** Remove named data from a configuration section
+ *
+ */
+void *cf_data_remove(CONF_SECTION *cs, char const *name)
+{
+	CONF_DATA mycd;
+	CONF_DATA *cd;
+	CONF_ITEM *ci, *it;
+	void *data;
+
+	if (!cs || !name) return NULL;
+	if (!cs->data_tree) return NULL;
+
+	/*
+	 *	Find the name in the tree, for speed.
+	 */
+	mycd.name = name;
+	mycd.flag = 0;
+	cd = rbtree_finddata(cs->data_tree, &mycd);
+	if (!cd) return NULL;
+
+	ci = cf_data_to_item(cd);
+	if (cs->children == ci) {
+		cs->children = ci->next;
+		if (cs->tail == ci) cs->tail = NULL;
+	} else {
+		for (it = cs->children; it; it = it->next) {
+			if (it->next == ci) {
+				it->next = ci->next;
+				if (cs->tail == ci) cs->tail = it;
+				break;
+			}
+		}
+	}
+
+	talloc_set_destructor(cd, NULL);	/* Disarm the destructor */
+	rbtree_deletebydata(cs->data_tree, &mycd);
+
+	data = cd->data;
+	talloc_free(cd);
+
+	return data;
+}
+
+/*
+ *	This is here to make the rest of the code easier to read.  It
+ *	ties conffile.c to log.c, but it means we don't have to
+ *	pollute every other function with the knowledge of the
+ *	configuration internals.
+ */
+void cf_log_err(CONF_ITEM const *ci, char const *fmt, ...)
+{
+	va_list ap;
+	char buffer[256];
+
+	va_start(ap, fmt);
+	vsnprintf(buffer, sizeof(buffer), fmt, ap);
+	va_end(ap);
+
+	if (ci) {
+		ERROR("%s[%d]: %s",
+		       ci->filename ? ci->filename : "unknown",
+		       ci->lineno ? ci->lineno : 0,
+		       buffer);
+	} else {
+		ERROR("<unknown>[*]: %s", buffer);
+	}
+}
+
+void cf_log_err_cs(CONF_SECTION const *cs, char const *fmt, ...)
+{
+	va_list ap;
+	char buffer[256];
+
+	va_start(ap, fmt);
+	vsnprintf(buffer, sizeof(buffer), fmt, ap);
+	va_end(ap);
+
+	rad_assert(cs != NULL);
+
+	ERROR("%s[%d]: %s",
+	       cs->item.filename ? cs->item.filename : "unknown",
+	       cs->item.lineno ? cs->item.lineno : 0,
+	       buffer);
+}
+
+void cf_log_err_cp(CONF_PAIR const *cp, char const *fmt, ...)
+{
+	va_list ap;
+	char buffer[256];
+
+	va_start(ap, fmt);
+	vsnprintf(buffer, sizeof(buffer), fmt, ap);
+	va_end(ap);
+
+	rad_assert(cp != NULL);
+
+	ERROR("%s[%d]: %s",
+	       cp->item.filename ? cp->item.filename : "unknown",
+	       cp->item.lineno ? cp->item.lineno : 0,
+	       buffer);
+}
+
+void cf_log_info(CONF_SECTION const *cs, char const *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	if ((rad_debug_lvl > 1) && cs) vradlog(L_DBG, fmt, ap);
+	va_end(ap);
+}
+
+/*
+ *	Wrapper to simplify the code.
+ */
+void cf_log_module(CONF_SECTION const *cs, char const *fmt, ...)
+{
+	va_list ap;
+	char buffer[256];
+
+	va_start(ap, fmt);
+	if (rad_debug_lvl > 1 && cs) {
+		vsnprintf(buffer, sizeof(buffer), fmt, ap);
+
+		DEBUG("%.*s# %s", cs->depth, parse_spaces, buffer);
+	}
+	va_end(ap);
+}
+
+const CONF_PARSER *cf_section_parse_table(CONF_SECTION *cs)
+{
+	if (!cs) return NULL;
+
+	return cs->variables;
+}
+
+/*
+ *	For "switch" and "case" statements.
+ */
+FR_TOKEN cf_section_name2_type(CONF_SECTION const *cs)
+{
+	if (!cs) return T_INVALID;
+
+	return cs->name2_type;
+}
diff --git a/src/main/connection.c b/src/main/connection.c
new file mode 100644
index 0000000..7ae4a2a
--- /dev/null
+++ b/src/main/connection.c
@@ -0,0 +1,1520 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * @file connection.c
+ * @brief Handle pools of connections (threads, sockets, etc.)
+ * @note This API must be used by all modules in the public distribution that
+ * maintain pools of connections.
+ *
+ * @copyright 2012  The FreeRADIUS server project
+ * @copyright 2012  Alan DeKok <aland@deployingradius.com>
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/heap.h>
+#include <freeradius-devel/modpriv.h>
+#include <freeradius-devel/rad_assert.h>
+
+typedef struct fr_connection fr_connection_t;
+
+static int fr_connection_pool_check(fr_connection_pool_t *pool);
+
+#ifndef NDEBUG
+#ifdef HAVE_PTHREAD_H
+/* #define PTHREAD_DEBUG (1) */
+#endif
+#endif
+
+/*
+ *	We don't need to pollute the logs with "open / close"
+ *	connection information.  Instead, only print these messages
+ *	when debugging.
+ */
+#undef INFO
+#define INFO(fmt, ...) if (rad_debug_lvl) radlog(L_INFO,  fmt, ## __VA_ARGS__)
+
+/** An individual connection within the connection pool
+ *
+ * Defines connection counters, timestamps, and holds a pointer to the
+ * connection handle itself.
+ *
+ * @see fr_connection_pool_t
+ */
+struct fr_connection {
+	fr_connection_t	*prev;			//!< Previous connection in list.
+	fr_connection_t	*next;			//!< Next connection in list.
+
+	time_t		created;		//!< Time connection was created.
+	struct timeval 	last_reserved;		//!< Last time the connection was reserved.
+
+	struct timeval	last_released;  	//!< Time the connection was released.
+
+	uint32_t	num_uses;		//!< Number of times the connection has been reserved.
+	uint64_t	number;			//!< Unique ID assigned when the connection is created,
+						//!< these will monotonically increase over the
+						//!< lifetime of the connection pool.
+	void		*connection;		//!< Pointer to whatever the module uses for a connection
+						//!< handle.
+	bool		in_use;			//!< Whether the connection is currently reserved.
+
+	int		heap;			//!< For the next connection heap.
+
+#ifdef PTHREAD_DEBUG
+	pthread_t	pthread_id;		//!< When 'in_use == true'.
+#endif
+};
+
+/** A connection pool
+ *
+ * Defines the configuration of the connection pool, all the counters and
+ * timestamps related to the connection pool, the mutex that stops multiple
+ * threads leaving the pool in an inconsistent state, and the callbacks
+ * required to open, close and check the status of connections within the pool.
+ *
+ * @see fr_connection
+ */
+struct fr_connection_pool_t {
+	int		ref;			//!< Reference counter to prevent connection
+						//!< pool being freed multiple times.
+	uint32_t       	start;			//!< Number of initial connections.
+	uint32_t       	min;			//!< Minimum number of concurrent connections to keep open.
+	uint32_t       	max;			//!< Maximum number of concurrent connections to allow.
+	uint32_t       	spare;			//!< Number of spare connections to try.
+	uint32_t	pending;		//!< Number of pending open connections.
+	uint32_t       	retry_delay;		//!< seconds to delay re-open after a failed open.
+	uint32_t	max_retries;		//!< Maximum number of retries to attempt for any given
+						//!< operation (e.g. query or bind)
+	uint32_t       	cleanup_interval; 	//!< Initial timer for how often we sweep the pool
+						//!< for free connections. (0 is infinite).
+	int		delay_interval;		//!< When we next do a cleanup.  Initialized to
+						//!< cleanup_interval, and increase from there based
+						//!< on the delay.
+	int		next_delay;    	 	//!< The next delay time.  cleanup.  Initialized to
+						//!< cleanup_interval, and decays from there.
+	uint64_t	max_uses;		//!< Maximum number of times a connection can be used
+						//!< before being closed.
+	uint32_t	lifetime;		//!< How long a connection can be open before being
+						//!< closed (irrespective of whether it's idle or not).
+	uint32_t       	idle_timeout;		//!< How long a connection can be idle before
+						//!< being closed.
+
+	uint32_t	max_pending;		//!< Max number of connections to open.
+
+	bool		spread;			//!< If true we spread requests over the connections,
+						//!< using the connection released longest ago, first.
+
+	fr_connection_pool_stats_t stats;		//!< various statistics
+
+	fr_heap_t	*heap;			//!< For the next connection heap
+
+	fr_connection_t	*head;			//!< Start of the connection list.
+	fr_connection_t *tail;			//!< End of the connection list.
+
+#ifdef HAVE_PTHREAD_H
+	pthread_mutex_t	mutex;			//!< Mutex used to keep consistent state when making
+						//!< modifications in threaded mode.
+#endif
+
+	CONF_SECTION	*cs;			//!< Configuration section holding the section of parsed
+						//!< config file that relates to this pool.
+	void		*opaque;		//!< Pointer to context data that will be passed to callbacks.
+
+	char const	*log_prefix;		//!< Log prefix to prepend to all log messages created
+						//!< by the connection pool code.
+
+	char const	*trigger_prefix;	//!< Prefix to prepend to names of all triggers
+						//!< fired by the connection pool code.
+
+	fr_connection_create_t	create;		//!< Function used to create new connections.
+	fr_connection_alive_t	alive;		//!< Function used to check status of connections.
+};
+
+#ifndef HAVE_PTHREAD_H
+#  define pthread_mutex_lock(_x)
+#  define pthread_mutex_unlock(_x)
+#endif
+
+static const CONF_PARSER connection_config[] = {
+	{ "start", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, start), "5" },
+	{ "min", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, min), "5" },
+	{ "max", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, max), "10" },
+	{ "spare", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, spare), "3" },
+	{ "uses", FR_CONF_OFFSET(PW_TYPE_INTEGER64, fr_connection_pool_t, max_uses), "0" },
+	{ "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, lifetime), "0" },
+	{ "cleanup_delay", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, cleanup_interval), NULL},
+	{ "cleanup_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, cleanup_interval), "30" },
+	{ "idle_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, idle_timeout), "60" },
+	{ "retry_delay", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, retry_delay), "1" },
+	{ "max_retries", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, max_retries), "5" },
+	{ "spread", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_connection_pool_t, spread), "no" },
+	CONF_PARSER_TERMINATOR
+};
+
+/** Order connections by reserved most recently
+ */
+static int last_reserved_cmp(void const *one, void const *two)
+{
+	fr_connection_t const *a = one;
+	fr_connection_t const *b = two;
+
+	if (a->last_reserved.tv_sec < b->last_reserved.tv_sec) return -1;
+	if (a->last_reserved.tv_sec > b->last_reserved.tv_sec) return +1;
+
+	if (a->last_reserved.tv_usec < b->last_reserved.tv_usec) return -1;
+	if (a->last_reserved.tv_usec > b->last_reserved.tv_usec) return +1;
+
+	return 0;
+}
+
+/** Order connections by released longest ago
+ */
+static int last_released_cmp(void const *one, void const *two)
+{
+	fr_connection_t const *a = one;
+	fr_connection_t const *b = two;
+
+	if (b->last_released.tv_sec < a->last_released.tv_sec) return -1;
+	if (b->last_released.tv_sec > a->last_released.tv_sec) return +1;
+
+	if (b->last_released.tv_usec < a->last_released.tv_usec) return -1;
+	if (b->last_released.tv_usec > a->last_released.tv_usec) return +1;
+
+	return 0;
+}
+
+/** Removes a connection from the connection list
+ *
+ * @note Must be called with the mutex held.
+ *
+ * @param[in,out] pool to modify.
+ * @param[in] this Connection to delete.
+ */
+static void fr_connection_unlink(fr_connection_pool_t *pool, fr_connection_t *this)
+{
+	if (this->prev) {
+		rad_assert(pool->head != this);
+		this->prev->next = this->next;
+	} else {
+		rad_assert(pool->head == this);
+		pool->head = this->next;
+	}
+	if (this->next) {
+		rad_assert(pool->tail != this);
+		this->next->prev = this->prev;
+	} else {
+		rad_assert(pool->tail == this);
+		pool->tail = this->prev;
+	}
+
+	this->prev = this->next = NULL;
+}
+
+/** Adds a connection to the head of the connection list
+ *
+ * @note Must be called with the mutex held.
+ *
+ * @param[in,out] pool to modify.
+ * @param[in] this Connection to add.
+ */
+static void fr_connection_link_head(fr_connection_pool_t *pool, fr_connection_t *this)
+{
+	rad_assert(pool != NULL);
+	rad_assert(this != NULL);
+	rad_assert(pool->head != this);
+	rad_assert(pool->tail != this);
+
+	if (pool->head) {
+		pool->head->prev = this;
+	}
+
+	this->next = pool->head;
+	this->prev = NULL;
+	pool->head = this;
+	if (!pool->tail) {
+		rad_assert(this->next == NULL);
+		pool->tail = this;
+	} else {
+		rad_assert(this->next != NULL);
+	}
+}
+
+/** Send a connection pool trigger.
+ *
+ * @param[in] pool to send trigger for.
+ * @param[in] name_suffix trigger name suffix.
+ */
+static void fr_connection_exec_trigger(fr_connection_pool_t *pool, char const *name_suffix)
+{
+	char name[64];
+	rad_assert(pool != NULL);
+	rad_assert(name_suffix != NULL);
+	snprintf(name, sizeof(name), "%s%s", pool->trigger_prefix, name_suffix);
+	exec_trigger(NULL, pool->cs, name, true);
+}
+
+/** Find a connection handle in the connection list
+ *
+ * Walks over the list of connections searching for a specified connection
+ * handle and returns the first connection that contains that pointer.
+ *
+ * @note Will lock mutex and only release mutex if connection handle
+ * is not found, so will usually return will mutex held.
+ * @note Must be called with the mutex free.
+ *
+ * @param[in] pool to search in.
+ * @param[in] conn handle to search for.
+ * @return
+ *	- Connection containing the specified handle.
+ *	- NULL if non if connection was found.
+ */
+static fr_connection_t *fr_connection_find(fr_connection_pool_t *pool, void *conn)
+{
+	fr_connection_t *this;
+
+	if (!pool || !conn) return NULL;
+
+	pthread_mutex_lock(&pool->mutex);
+
+	/*
+	 *	FIXME: This loop could be avoided if we passed a 'void
+	 *	**connection' instead.  We could use "offsetof" in
+	 *	order to find top of the parent structure.
+	 */
+	for (this = pool->head; this != NULL; this = this->next) {
+		if (this->connection == conn) {
+#ifdef PTHREAD_DEBUG
+			pthread_t pthread_id;
+
+			pthread_id = pthread_self();
+			rad_assert(pthread_equal(this->pthread_id, pthread_id) != 0);
+#endif
+
+			rad_assert(this->in_use == true);
+			return this;
+		}
+	}
+
+	pthread_mutex_unlock(&pool->mutex);
+	return NULL;
+}
+
+/** Spawns a new connection
+ *
+ * Spawns a new connection using the create callback, and returns it for
+ * adding to the connection list.
+ *
+ * @note Will call the 'open' trigger.
+ * @note Must be called with the mutex free.
+ *
+ * @param[in] pool to modify.
+ * @param[in] now Current time.
+ * @param[in] in_use whether the new connection should be "in_use" or not
+ * @return
+ *	- New connection struct.
+ *	- NULL on error.
+ */
+static fr_connection_t *fr_connection_spawn(fr_connection_pool_t *pool, time_t now, bool in_use)
+{
+	uint64_t	number;
+	uint32_t	max_pending;
+	TALLOC_CTX	*ctx;
+
+	fr_connection_t	*this;
+	void		*conn;
+
+	rad_assert(pool != NULL);
+
+	/*
+	 *	If we have NO connections, and we've previously failed
+	 *	opening connections, don't open multiple connections until
+	 *	we successfully open at least one.
+	 */
+	if ((pool->stats.num == 0) && pool->pending && pool->stats.last_failed) return NULL;
+
+	pthread_mutex_lock(&pool->mutex);
+	rad_assert(pool->stats.num <= pool->max);
+
+	/*
+	 *	Don't spawn too many connections at the same time.
+	 */
+	if ((pool->stats.num + pool->pending) >= pool->max) {
+		pthread_mutex_unlock(&pool->mutex);
+
+		ERROR("%s: Cannot open new connection, already at max", pool->log_prefix);
+		return NULL;
+	}
+
+	/*
+	 *	If the last attempt failed, wait a bit before
+	 *	retrying.
+	 */
+	if (pool->stats.last_failed && ((pool->stats.last_failed + pool->retry_delay) > now)) {
+		bool complain = false;
+
+		if (pool->stats.last_throttled != now) {
+			complain = true;
+
+			pool->stats.last_throttled = now;
+		}
+
+		pthread_mutex_unlock(&pool->mutex);
+
+		if (!RATE_LIMIT_ENABLED || complain) {
+			ERROR("%s: Last connection attempt failed, waiting %d seconds before retrying",
+			      pool->log_prefix, pool->retry_delay);
+		}
+
+		return NULL;
+	}
+
+	/*
+	 *	We limit the rate of new connections after a failed attempt.
+	 */
+	if (pool->pending > pool->max_pending) {
+		pthread_mutex_unlock(&pool->mutex);
+		RATE_LIMIT(WARN("%s: Cannot open a new connection due to rate limit after failure",
+				pool->log_prefix));
+		return NULL;
+	}
+
+	pool->pending++;
+	number = pool->stats.opened++;
+
+	/*
+	 *	Unlock the mutex while we try to open a new
+	 *	connection.  If there are issues with the back-end,
+	 *	opening a new connection may take a LONG time.  In
+	 *	that case, we want the other connections to continue
+	 *	to be used.
+	 */
+	pthread_mutex_unlock(&pool->mutex);
+
+	/*
+	 *	The true value for max_pending is the smaller of
+	 *	free connection slots, or pool->max_pending.
+	 */
+	max_pending = (pool->max - pool->stats.num);
+	if (pool->max_pending < max_pending) max_pending = pool->max_pending;
+	INFO("%s: Opening additional connection (%" PRIu64 "), %u of %u pending slots used",
+	     pool->log_prefix, number, pool->pending, max_pending);
+
+	/*
+	 *	Allocate a new top level ctx for the create callback
+	 *	to hang its memory off of.
+	 */
+	ctx = talloc_init("fr_connection_ctx");
+	if (!ctx) return NULL;
+
+	/*
+	 *	This may take a long time, which prevents other
+	 *	threads from releasing connections.  We don't care
+	 *	about other threads opening new connections, as we
+	 *	already have no free connections.
+	 */
+	conn = pool->create(ctx, pool->opaque);
+	if (!conn) {
+		ERROR("%s: Opening connection failed (%" PRIu64 ")", pool->log_prefix, number);
+
+		pool->stats.last_failed = now;
+		pthread_mutex_lock(&pool->mutex);
+		pool->max_pending = 1;
+		pool->pending--;
+		pool->stats.failed++;
+		pthread_mutex_unlock(&pool->mutex);
+
+		talloc_free(ctx);
+
+		return NULL;
+	}
+
+	/*
+	 *	And lock the mutex again while we link the new
+	 *	connection back into the pool.
+	 */
+	pthread_mutex_lock(&pool->mutex);
+
+	this = talloc_zero(pool, fr_connection_t);
+	if (!this) {
+		pthread_mutex_unlock(&pool->mutex);
+		talloc_free(ctx);
+
+		return NULL;
+	}
+	fr_link_talloc_ctx_free(this, ctx);
+
+	this->created = now;
+	this->connection = conn;
+	this->in_use = in_use;
+
+	this->number = number;
+	gettimeofday(&this->last_reserved, NULL);
+	this->last_released = this->last_reserved;
+
+	/*
+	 *	The connection pool is starting up.  Insert the
+	 *	connection into the heap.
+	 */
+	if (!in_use) fr_heap_insert(pool->heap, this);
+
+	fr_connection_link_head(pool, this);
+
+	/*
+	 *	Do NOT insert the connection into the heap.  That's
+	 *	done when the connection is released.
+	 */
+
+	pool->stats.num++;
+
+	rad_assert(pool->pending > 0);
+	pool->pending--;
+
+	/*
+	 *	We've successfully opened one more connection.  Allow
+	 *	more connections to open in parallel.
+	 */
+	if (pool->max_pending < pool->max) pool->max_pending++;
+
+	pool->stats.last_opened = time(NULL);
+	pool->delay_interval = pool->cleanup_interval;
+	pool->next_delay = pool->cleanup_interval;
+	pool->stats.last_failed = 0;
+
+	pthread_mutex_unlock(&pool->mutex);
+
+	fr_connection_exec_trigger(pool, "open");
+
+	return this;
+}
+
+/** Close an existing connection.
+ *
+ * Removes the connection from the list, calls the delete callback to close
+ * the connection, then frees memory allocated to the connection.
+ *
+ * @note Will call the 'close' trigger.
+ * @note Must be called with the mutex held.
+ *
+ * @param[in,out] pool to modify.
+ * @param[in] this Connection to delete.
+ * @param[in] reason to close the connection
+ * @param[in] msg optional message
+ */
+static void fr_connection_close_internal(fr_connection_pool_t *pool, fr_connection_t *this,
+					 char const *reason, char const *msg)
+{
+	if (!msg) {
+		INFO("%s: %s (%" PRIu64 ")", pool->log_prefix, reason, this->number);
+	} else {
+		INFO("%s: %s (%" PRIu64 ") - %s", pool->log_prefix, reason, this->number, msg);
+	}
+
+
+	/*
+	 *	If it's in use, release it.
+	 */
+	if (this->in_use) {
+#ifdef PTHREAD_DEBUG
+		pthread_t pthread_id = pthread_self();
+		rad_assert(pthread_equal(this->pthread_id, pthread_id) != 0);
+#endif
+
+		this->in_use = false;
+
+		rad_assert(pool->stats.active != 0);
+		pool->stats.active--;
+
+	} else {
+		/*
+		 *	Connection isn't used, remove it from the heap.
+		 */
+		fr_heap_extract(pool->heap, this);
+	}
+
+	fr_connection_exec_trigger(pool, "close");
+
+	fr_connection_unlink(pool, this);
+
+	rad_assert(pool->stats.num > 0);
+	pool->stats.num--;
+	pool->stats.closed++;
+	pool->stats.last_closed = time(NULL);
+	talloc_free(this);
+}
+
+/** Check whether a connection needs to be removed from the pool
+ *
+ * Will verify that the connection is within idle_timeout, max_uses, and
+ * lifetime values. If it is not, the connection will be closed.
+ *
+ * @note Will only close connections not in use.
+ * @note Must be called with the mutex held.
+ *
+ * @param[in,out] pool to modify.
+ * @param[in,out] this Connection to manage.
+ * @param[in] now Current time.
+ * @param[in] get whether we want to get a connection
+ * @return
+ *	- 0 if connection was closed.
+ *	- 1 if connection handle was left open.
+ */
+static int fr_connection_manage(fr_connection_pool_t *pool,
+				fr_connection_t *this,
+				time_t now, bool get)
+{
+	rad_assert(pool != NULL);
+	rad_assert(this != NULL);
+	char const *reason = "Closing expired connection";
+	char const *msg = NULL;
+
+	/*
+	 *	Don't terminate in-use connections
+	 */
+	if (this->in_use) return 1;
+
+	if ((pool->max_uses > 0) &&
+	    (this->num_uses >= pool->max_uses)) {
+		msg = "Hit max_uses limit";
+
+	do_delete:
+		if (pool->stats.num <= pool->min) {
+			DEBUG("%s: You probably need to lower \"min\"", pool->log_prefix);
+		}
+		fr_connection_close_internal(pool, this, reason, msg);
+		return 0;
+	}
+
+	if ((pool->lifetime > 0) &&
+	    ((this->created + pool->lifetime) < now)) {
+		msg = "Hit lifetime limit";
+		goto do_delete;
+	}
+
+	/*
+	 *	The connection WAS idle, but the caller is interested
+	 *	in getting a new one.  Instead of closing the old one
+	 *	and opening a new one, we just return the old one.
+	 */
+	if (get) return 1;
+
+	if ((pool->idle_timeout > 0) &&
+	    ((this->last_released.tv_sec + pool->idle_timeout) < now)) {
+		msg = "Hit idle_timeout limit";
+		goto do_delete;
+	}
+
+	return 1;
+}
+
+
+/** Check whether any connections need to be removed from the pool
+ *
+ * Maintains the number of connections in the pool as per the configuration
+ * parameters for the connection pool.
+ *
+ * @note Will only run checks the first time it's called in a given second,
+ * to throttle connection spawning/closing.
+ * @note Will only close connections not in use.
+ * @note Must be called with the mutex held, will release mutex before
+ * returning.
+ *
+ * @param[in,out] pool to manage.
+ * @return 1
+ */
+static int fr_connection_pool_check(fr_connection_pool_t *pool)
+{
+	uint32_t num, spare;
+	time_t now = time(NULL);
+	fr_connection_t *this, *next;
+
+	if (pool->stats.last_checked == now) {
+		pthread_mutex_unlock(&pool->mutex);
+		return 1;
+	}
+
+	/*
+	 *	Get "real" number of connections, and count pending
+	 *	connections as spare.
+	 */
+	num = pool->stats.num + pool->pending;
+	spare = pool->pending + (pool->stats.num - pool->stats.active);
+
+	/*
+	 *	The other end can close connections.  If so, we'll
+	 *	have fewer than "min".  When that happens, open more
+	 *	connections to enforce "min".
+	 *
+	 *	The code for spawning connections enforces that
+	 *	num + pending <= max.
+	 */
+	if (num < pool->min) {
+		INFO("Need %u more connections to reach min connections (%i)", pool->min - num, pool->min);
+		goto add_connection;
+	}
+
+	/*
+	 *	On the odd chance that we've opened too many
+	 *	connections, take care of that.
+	 */
+	if (num > pool->max) {
+		/*
+		 *	Pending connections don't get closed as "spare".
+		 */
+		if (pool->pending > 0) goto manage_connections;
+
+		/*
+		 *	Otherwise close one of the connections to
+		 *	bring us down to "max".
+		 */
+		goto close_connection;
+	}
+
+	/*
+	 *	Now that we've enforced min/max connections, try to
+	 *	keep the "spare" connections at the correct number.
+	 */
+
+	/*
+	 *	Nothing to do?  Go check all of the connections for
+	 *	timeouts, etc.
+	 */
+	if (spare == pool->spare) goto manage_connections;
+
+	/*
+	 *	Too many spare connections, delete some.
+	 */
+	if (spare > pool->spare) {
+		fr_connection_t *found;
+
+		/*
+		 *	Pending connections don't get closed as "spare".
+		 */
+		if (pool->pending > 0) goto manage_connections;
+
+		/*
+		 *	Don't close too many connections, even they
+		 *	are spare.
+		 */
+		if (num <= pool->min) goto manage_connections;
+
+		/*
+		 *	Too many spares, go close one.
+		 */
+
+	close_connection:
+		/*
+		 *	Don't close connections too often, in order to
+		 *	prevent flapping.
+		 */
+		if (now < (pool->stats.last_opened + pool->delay_interval)) goto manage_connections;
+
+		/*
+		 *	Find a connection to close.
+		 */
+		found = NULL;
+		for (this = pool->tail; this != NULL; this = this->prev) {
+			if (this->in_use) continue;
+
+			if (!found ||
+			    timercmp(&this->last_reserved, &found->last_reserved, <)) {
+				found = this;
+			}
+		}
+
+		rad_assert(found != NULL);
+
+		fr_connection_close_internal(pool, found, "Closing connection", "Too many unused connections.");
+
+		/*
+		 *	Decrease the delay for the next time we clean
+		 *	up.
+		 */
+		pool->next_delay >>= 1;
+		if (pool->next_delay == 0) pool->next_delay = 1;
+		pool->delay_interval += pool->next_delay;
+
+		goto manage_connections;
+	}
+
+	/*
+	 *	Too few connections, open some more.
+	 */
+	if (spare < pool->spare) {
+		/*
+		 *	Don't open too many pending connections.
+		 */
+		if (pool->pending >= pool->max_pending) goto manage_connections;
+
+		/*
+		 *	Don't open too many connections, even if we
+		 *	need more spares.
+		 */
+		if (num >= pool->max) goto manage_connections;
+
+		/*
+		 *	Too few spares, go add one.
+		 */
+
+	add_connection:
+		INFO("Need more connections to reach %i spares", pool->spare);
+
+		/*
+		 *	Only try to open spares if we're not already attempting to open
+		 *	a connection. Avoids spurious log messages.
+		 */
+		pthread_mutex_unlock(&pool->mutex);
+		fr_connection_spawn(pool, now, false); /* ignore return code */
+		pthread_mutex_lock(&pool->mutex);
+		goto manage_connections;
+	}
+
+	/*
+	 *	Pass over all of the connections in the pool, limiting
+	 *	lifetime, idle time, max requests, etc.
+	 */
+manage_connections:
+	for (this = pool->head; this != NULL; this = next) {
+		next = this->next;
+		fr_connection_manage(pool, this, now, false);
+	}
+
+	pool->stats.last_checked = now;
+	pthread_mutex_unlock(&pool->mutex);
+
+	return 1;
+}
+
+/** Get a connection from the connection pool
+ *
+ * @note Must be called with the mutex free.
+ *
+ * @param[in,out] pool to reserve the connection from.
+ * @param[in] spawn whether to spawn a new connection
+ * @return
+ *	- A pointer to the connection handle.
+ *	- NULL on error.
+ */
+static void *fr_connection_get_internal(fr_connection_pool_t *pool, bool spawn)
+{
+	time_t now;
+	fr_connection_t *this;
+
+	if (!pool) return NULL;
+
+	/*
+	 *	Allow CTRL-C to kill the server in debugging mode.
+	 */
+	if (main_config.exiting) return NULL;
+
+#ifdef HAVE_PTHREAD_H
+	if (spawn) pthread_mutex_lock(&pool->mutex);
+#endif
+
+	now = time(NULL);
+
+	/*
+	 *	Grab the link with the lowest latency, and check it
+	 *	for limits.  If "connection manage" says the link is
+	 *	no longer usable, go grab another one.
+	 */
+	do {
+		this = fr_heap_peek(pool->heap);
+		if (!this) break;
+
+		fr_assert(!this->in_use);
+	} while (!fr_connection_manage(pool, this, now, true));
+
+	/*
+	 *	We have a working connection.  Extract it from the
+	 *	heap and use it.
+	 */
+	if (this) {
+		fr_heap_extract(pool->heap, this);
+		goto do_return;
+	}
+
+	/*
+	 *	We were asked to avoid spawning a new connection, by
+	 *	fr_connection_reconnect_internal().  So we just return
+	 *	here.
+	 */
+	if (!spawn) return NULL;
+
+	if (pool->stats.num == pool->max) {
+		bool complain = false;
+
+		/*
+		 *	Rate-limit complaints.
+		 */
+		if (pool->stats.last_at_max != now) {
+			complain = true;
+			pool->stats.last_at_max = now;
+		}
+
+		pthread_mutex_unlock(&pool->mutex);
+
+		if (!RATE_LIMIT_ENABLED || complain) {
+			ERROR("%s: No connections available and at max connection limit", pool->log_prefix);
+		}
+
+		return NULL;
+	}
+
+	pthread_mutex_unlock(&pool->mutex);
+
+	DEBUG("%s: %i of %u connections in use.  You  may need to increase \"spare\"", pool->log_prefix,
+	      pool->stats.active, pool->stats.num);
+	this = fr_connection_spawn(pool, now, true); /* MY connection! */
+	if (!this) return NULL;
+
+	pthread_mutex_lock(&pool->mutex);
+
+do_return:
+	pool->stats.active++;
+	this->num_uses++;
+	gettimeofday(&this->last_reserved, NULL);
+	this->in_use = true;
+
+#ifdef PTHREAD_DEBUG
+	this->pthread_id = pthread_self();
+#endif
+
+#ifdef HAVE_PTHREAD_H
+	if (spawn) pthread_mutex_unlock(&pool->mutex);
+#endif
+
+	DEBUG("%s: Reserved connection (%" PRIu64 ")", pool->log_prefix, this->number);
+
+	return this->connection;
+}
+
+/** Reconnect a suspected inviable connection
+ *
+ * @note Must be called with the mutex held, will not release mutex.
+ *
+ * @see fr_connection_get
+ * @param[in,out] pool to reconnect the connection in.
+ * @param[in,out] conn to reconnect.
+ * @return new connection handle if successful else NULL.
+ */
+static fr_connection_t *fr_connection_reconnect_internal(fr_connection_pool_t *pool, fr_connection_t *conn)
+{
+	void		*new_conn;
+	uint64_t	conn_number;
+	TALLOC_CTX	*ctx;
+
+	conn_number = conn->number;
+
+	/*
+	 *	Destroy any handles associated with the fr_connection_t
+	 */
+	talloc_free_children(conn);
+
+	DEBUG("%s: Reconnecting (%" PRIu64 ")", pool->log_prefix, conn_number);
+
+	/*
+	 *	Allocate a new top level ctx for the create callback
+	 *	to hang its memory off of.
+	 */
+	ctx = talloc_init("fr_connection_ctx");
+	if (!ctx) return NULL;
+	fr_link_talloc_ctx_free(conn, ctx);
+
+	new_conn = pool->create(ctx, pool->opaque);
+	if (!new_conn) {
+		/*
+		 *	We can't create a new connection, so close the current one.
+		 */
+		fr_connection_close_internal(pool, conn, "Closing connection", "Failed to reconnect");
+
+		/*
+		 *	Maybe there's a connection which is unused and
+		 *	available.  If so, return it.
+		 */
+		new_conn = fr_connection_get_internal(pool, false);
+		if (new_conn) return new_conn;
+
+		RATE_LIMIT(ERROR("%s: Failed to reconnect (%" PRIu64 "), no free connections are available",
+				 pool->log_prefix, conn_number));
+
+		return NULL;
+	}
+
+	fr_connection_exec_trigger(pool, "close");
+	conn->connection = new_conn;
+
+	return new_conn;
+}
+
+/** Create a new connection pool
+ *
+ * Allocates structures used by the connection pool, initialises the various
+ * configuration options and counters, and sets the callback functions.
+ *
+ * Will also spawn the number of connections specified by the 'start'
+ * configuration options.
+ *
+ * @note Will call the 'start' trigger.
+ *
+ * @param[in] ctx Context to link pool's destruction to.
+ * @param[in] cs pool section.
+ * @param[in] opaque data pointer to pass to callbacks.
+ * @param[in] c Callback to create new connections.
+ * @param[in] a Callback to check the status of connections.
+ * @param[in] log_prefix prefix to prepend to all log messages.
+ * @param[in] trigger_prefix prefix to prepend to all trigger names.
+ * @return
+ *	- New connection pool.
+ *	- NULL on error.
+ */
+static fr_connection_pool_t *fr_connection_pool_init(TALLOC_CTX *ctx,
+						     CONF_SECTION *cs,
+						     void *opaque,
+						     fr_connection_create_t c,
+						     fr_connection_alive_t a,
+						     char const *log_prefix,
+						     char const *trigger_prefix)
+{
+	uint32_t i;
+	fr_connection_pool_t *pool;
+	fr_connection_t *this;
+	time_t now;
+
+	if (!cs || !opaque || !c) return NULL;
+
+	now = time(NULL);
+
+	/*
+	 *	Pool is allocated in the NULL context as
+	 *	threads are likely to allocate memory
+	 *	beneath the pool.
+	 */
+	pool = talloc_zero(NULL, fr_connection_pool_t);
+	if (!pool) return NULL;
+
+	/*
+	 *	Ensure the pool is freed at the same time
+	 *	as its parent.
+	 */
+	if (fr_link_talloc_ctx_free(ctx, pool) < 0) {
+		talloc_free(pool);
+
+		return NULL;
+	}
+
+	pool->cs = cs;
+	pool->opaque = opaque;
+	pool->create = c;
+	pool->alive = a;
+
+	pool->head = pool->tail = NULL;
+
+	/*
+	 *	We keep a heap of connections, sorted by the last time
+	 *	we STARTED using them.  Newly opened connections
+	 *	aren't in the heap.  They're only inserted in the list
+	 *	once they're released.
+	 *
+	 *	We do "most recently started" instead of "most
+	 *	recently used", because MRU is done as most recently
+	 *	*released*.  We want to order connections by
+	 *	responsiveness, and MRU prioritizes high latency
+	 *	connections.
+	 *
+	 *	We want most recently *started*, which gives
+	 *	preference to low latency links, and pushes high
+	 *	latency links down in the priority heap.
+	 *
+	 *	https://code.facebook.com/posts/1499322996995183/solving-the-mystery-of-link-imbalance-a-metastable-failure-state-at-scale/
+	 */
+	if (!pool->spread) {
+		pool->heap = fr_heap_create(last_reserved_cmp, offsetof(fr_connection_t, heap));
+	/*
+	 *	For some types of connections we need to used a different
+	 *	algorithm, because load balancing benefits are secondary
+	 *	to maintaining a cache of open connections.
+	 *
+	 *	With libcurl's multihandle, connections can only be reused
+	 *	if all handles that make up the multhandle are done processing
+	 *	their requests.
+	 *
+	 *	We can't tell when that's happened using libcurl, and even
+	 *	if we could, blocking until all servers had responded
+	 *	would have huge cost.
+	 *
+	 *	The solution is to order the heap so that the connection that
+	 *	was released longest ago is at the top.
+	 *
+	 *	That way we maximise time between connection use.
+	 */
+	} else {
+		pool->heap = fr_heap_create(last_released_cmp, offsetof(fr_connection_t, heap));
+	}
+	if (!pool->heap) {
+		talloc_free(pool);
+		return NULL;
+	}
+
+	pool->log_prefix = log_prefix ? talloc_typed_strdup(pool, log_prefix) : "core";
+	pool->trigger_prefix = trigger_prefix ? talloc_typed_strdup(pool, trigger_prefix) : "";
+
+#ifdef HAVE_PTHREAD_H
+	pthread_mutex_init(&pool->mutex, NULL);
+#endif
+
+	DEBUG("%s: Initialising connection pool", pool->log_prefix);
+
+	if (cf_section_parse(cs, pool, connection_config) < 0) goto error;
+
+	/*
+	 *	Some simple limits
+	 */
+	if (pool->max == 0) {
+		cf_log_err_cs(cs, "Cannot set 'max' to zero");
+		goto error;
+	}
+	pool->max_pending = pool->max; /* can open all connections now */
+
+	if (pool->min > pool->max) {
+		cf_log_err_cs(cs, "Cannot set 'min' to more than 'max'");
+		goto error;
+	}
+
+	FR_INTEGER_BOUND_CHECK("max", pool->max, <=, 1024);
+	FR_INTEGER_BOUND_CHECK("start", pool->start, <=, pool->max);
+	FR_INTEGER_BOUND_CHECK("spare", pool->spare, <=, (pool->max - pool->min));
+
+	if (pool->lifetime > 0) {
+		FR_INTEGER_COND_CHECK("idle_timeout", pool->idle_timeout, (pool->idle_timeout <= pool->lifetime), 0);
+	}
+
+	if (pool->idle_timeout > 0) {
+		FR_INTEGER_BOUND_CHECK("cleanup_interval", pool->cleanup_interval, <=, pool->idle_timeout);
+	}
+
+	/*
+	 *	Don't open any connections.  Instead, force the limits
+	 *	to only 1 connection.
+	 *
+	 */
+	if (check_config) {
+		pool->start = pool->min = pool->max = 1;
+		return pool;
+	}
+
+	/*
+	 *	Create all of the connections, unless the admin says
+	 *	not to.
+	 */
+	for (i = 0; i < pool->start; i++) {
+		this = fr_connection_spawn(pool, now, false);
+		if (!this) {
+		error:
+			fr_connection_pool_free(pool);
+			return NULL;
+		}
+	}
+
+	fr_connection_exec_trigger(pool, "start");
+
+	return pool;
+}
+
+/** Initialise a module specific connection pool
+ *
+ * @see fr_connection_pool_init
+ *
+ * @param[in] module section.
+ * @param[in] opaque data pointer to pass to callbacks.
+ * @param[in] c Callback to create new connections.
+ * @param[in] a Callback to check the status of connections.
+ * @param[in] log_prefix override, if NULL will be set automatically from the module CONF_SECTION.
+ * @return
+ *	- New connection pool.
+ *	- NULL on error.
+ */
+fr_connection_pool_t *fr_connection_pool_module_init(CONF_SECTION *module,
+						     void *opaque,
+						     fr_connection_create_t c,
+						     fr_connection_alive_t a,
+						     char const *log_prefix)
+{
+	CONF_SECTION *cs, *mycs;
+	char buff[128];
+	char trigger_prefix[64];
+
+	fr_connection_pool_t *pool;
+	char const *cs_name1, *cs_name2;
+
+	int ret;
+
+#define CONNECTION_POOL_CF_KEY "connection_pool"
+#define parent_name(_x) cf_section_name(cf_item_parent(cf_section_to_item(_x)))
+
+	cs_name1 = cf_section_name1(module);
+	cs_name2 = cf_section_name2(module);
+	if (!cs_name2) cs_name2 = cs_name1;
+
+	snprintf(trigger_prefix, sizeof(trigger_prefix), "modules.%s.", cs_name1);
+
+	if (!log_prefix) {
+		snprintf(buff, sizeof(buff), "rlm_%s (%s)", cs_name1, cs_name2);
+		log_prefix = buff;
+	}
+
+	/*
+	 *	Get sibling's pool config section
+	 */
+	ret = find_module_sibling_section(&cs, module, "pool");
+	switch (ret) {
+	case -1:
+		return NULL;
+
+	case 1:
+		DEBUG4("%s: Using pool section from \"%s\"", log_prefix, parent_name(cs));
+		break;
+
+	case 0:
+		DEBUG4("%s: Using local pool section", log_prefix);
+		break;
+	}
+
+	/*
+	 *	Get our pool config section
+	 */
+	mycs = cf_section_sub_find(module, "pool");
+	if (!mycs) {
+		DEBUG4("%s: Adding pool section to config item \"%s\" to store pool references", log_prefix,
+		       cf_section_name(module));
+
+		mycs = cf_section_alloc(module, "pool", NULL);
+		cf_section_add(module, mycs);
+	}
+
+	/*
+	 *	Sibling didn't have a pool config section
+	 *	Use our own local pool.
+	 */
+	if (!cs) {
+		DEBUG4("%s: \"%s.pool\" section not found, using \"%s.pool\"", log_prefix,
+		       parent_name(cs), parent_name(mycs));
+		cs = mycs;
+	}
+
+	/*
+	 *	If fr_connection_pool_init has already been called
+	 *	for this config section, reuse the previous instance.
+	 *
+	 *	This allows modules to pass in the config sections
+	 *	they would like to use the connection pool from.
+	 */
+	pool = cf_data_find(cs, CONNECTION_POOL_CF_KEY);
+	if (!pool) {
+		DEBUG4("%s: No pool reference found for config item \"%s.pool\"", log_prefix, parent_name(cs));
+		pool = fr_connection_pool_init(cs, cs, opaque, c, a, log_prefix, trigger_prefix);
+		if (!pool) return NULL;
+
+		DEBUG4("%s: Adding pool reference %p to config item \"%s.pool\"", log_prefix, pool, parent_name(cs));
+		cf_data_add(cs, CONNECTION_POOL_CF_KEY, pool, NULL);
+		return pool;
+	}
+	pool->ref++;
+
+	DEBUG4("%s: Found pool reference %p in config item \"%s.pool\"", log_prefix, pool, parent_name(cs));
+
+	/*
+	 *	We're reusing pool data add it to our local config
+	 *	section. This allows other modules to transitively
+	 *	re-use a pool through this module.
+	 */
+	if (mycs != cs) {
+		DEBUG4("%s: Copying pool reference %p from config item \"%s.pool\" to config item \"%s.pool\"",
+		       log_prefix, pool, parent_name(cs), parent_name(mycs));
+		cf_data_add(mycs, CONNECTION_POOL_CF_KEY, pool, NULL);
+	}
+
+	return pool;
+}
+
+/** Get the number of connections currently in the pool
+ *
+ * @param pool to count connections for.
+ * @return the number of connections in the pool
+ */
+int fr_connection_pool_get_num(fr_connection_pool_t *pool)
+{
+	return pool->stats.num;
+}
+
+/** Get the number of times an operation should be retried
+ *
+ * The lower of either the number of available connections or
+ * the configured max_retries.
+ *
+ * @param pool to get the retry count for.
+ * @return the number of times an operation can be retried.
+ */
+int fr_connection_pool_get_retries(fr_connection_pool_t *pool)
+{
+	return (pool->max_retries < pool->stats.num) ? pool->max_retries : pool->stats.num;
+}
+
+/** Get the number of connections currently in the pool
+ *
+ * @param module the module configuration which should contain the pool
+ * @return the stats, or NULL on "not found"
+ */
+fr_connection_pool_stats_t const *fr_connection_pool_stats(CONF_SECTION *module)
+{
+	fr_connection_pool_t *pool = NULL;
+	CONF_SECTION *cs;
+
+	cs = cf_section_sub_find(module, "pool");
+	if (!cs) {
+		CONF_PAIR *cp;
+		module_instance_t *mi;
+		char const *value;
+
+		/*
+		 *	This is the name of the module, not a
+		 *	reference.  <sigh>.
+		 */
+		cp = cf_pair_find(module, "pool");
+		if (!cp) return NULL;
+
+		value = cf_pair_value(cp);
+		if (!value) return NULL;
+
+		mi = module_find(cf_item_parent(cf_section_to_item(module)), value);
+		if (!mi) return NULL;
+
+		cs = cf_section_sub_find(mi->cs, "pool");
+		if (!cs) return NULL;
+	}
+
+	pool = cf_data_find(cs, CONNECTION_POOL_CF_KEY);
+	if (!pool) return NULL;
+
+	return &pool->stats;
+}
+
+
+/** Delete a connection pool
+ *
+ * Closes, unlinks and frees all connections in the connection pool, then frees
+ * all memory used by the connection pool.
+ *
+ * @note Will call the 'stop' trigger.
+ * @note Must be called with the mutex free.
+ *
+ * @param[in,out] pool to delete.
+ */
+void fr_connection_pool_free(fr_connection_pool_t *pool)
+{
+	fr_connection_t *this;
+
+	if (!pool) return;
+
+	/*
+	 *	More modules hold a reference to this pool, don't free
+	 *	it yet.
+	 */
+	if (pool->ref > 0) {
+		pool->ref--;
+		return;
+	}
+
+	DEBUG("%s: Removing connection pool", pool->log_prefix);
+
+	pthread_mutex_lock(&pool->mutex);
+
+	/*
+	 *	Don't loop over the list.  Just keep removing the head
+	 *	until they're all gone.
+	 */
+	while ((this = pool->head) != NULL) {
+		fr_connection_close_internal(pool, this, "Closing connection", "Shutting down connection pool");
+	}
+
+	fr_heap_delete(pool->heap);
+
+	fr_connection_exec_trigger(pool, "stop");
+
+	rad_assert(pool->head == NULL);
+	rad_assert(pool->tail == NULL);
+	rad_assert(pool->stats.num == 0);
+
+#ifdef HAVE_PTHREAD_H
+	pthread_mutex_destroy(&pool->mutex);
+#endif
+
+	talloc_free(pool);
+}
+
+/** Reserve a connection in the connection pool
+ *
+ * Will attempt to find an unused connection in the connection pool, if one is
+ * found, will mark it as in in use increment the number of active connections
+ * and return the connection handle.
+ *
+ * If no free connections are found will attempt to spawn a new one, conditional
+ * on a connection spawning not already being in progress, and not being at the
+ * 'max' connection limit.
+ *
+ * @note fr_connection_release must be called once the caller has finished
+ * using the connection.
+ *
+ * @see fr_connection_release
+ * @param[in,out] pool to reserve the connection from.
+ * @return
+ *	- A pointer to the connection handle.
+ *	- NULL on error.
+ */
+void *fr_connection_get(fr_connection_pool_t *pool)
+{
+	return fr_connection_get_internal(pool, true);
+}
+
+/** Release a connection
+ *
+ * Will mark a connection as unused and decrement the number of active
+ * connections.
+ *
+ * @see fr_connection_get
+ * @param[in,out] pool to release the connection in.
+ * @param[in,out] conn to release.
+ */
+void fr_connection_release(fr_connection_pool_t *pool, void *conn)
+{
+	fr_connection_t *this;
+
+	this = fr_connection_find(pool, conn);
+	if (!this) return;
+
+	this->in_use = false;
+
+	/*
+	 *	Record when the connection was last released
+	 */
+	gettimeofday(&this->last_released, NULL);
+
+	/*
+	 *	Insert the connection in the heap.
+	 *
+	 *	This will either be based on when we *started* using it
+	 *	(allowing fast links to be re-used, and slow links to be
+	 *	gradually expired), or when we released it (allowing
+	 *	the maximum amount of time between connection use).
+	 */
+	fr_heap_insert(pool->heap, this);
+
+	rad_assert(pool->stats.active != 0);
+	pool->stats.active--;
+
+	DEBUG("%s: Released connection (%" PRIu64 ")", pool->log_prefix, this->number);
+
+	/*
+	 *	We mirror the "spawn on get" functionality by having
+	 *	"delete on release".  If there are too many spare
+	 *	connections, go manage the pool && clean some up.
+	 */
+	fr_connection_pool_check(pool);
+}
+
+/** Reconnect a suspected inviable connection
+ *
+ * This should be called by the module if it suspects that a connection is
+ * not viable (e.g. the server has closed it).
+ *
+ * Will attempt to create a new connection handle using the create callback,
+ * and if this is successful the new handle will be assigned to the existing
+ * pool connection.
+ *
+ * If this is not successful, the connection will be removed from the pool.
+ *
+ * When implementing a module that uses the connection pool API, it is advisable
+ * to pass a pointer to the pointer to the handle (void **conn)
+ * to all functions which may call reconnect. This is so that if a new handle
+ * is created and returned, the handle pointer can be updated up the callstack,
+ * and a function higher up the stack doesn't attempt to use a now invalid
+ * connection handle.
+ *
+ * @note Will free any talloced memory hung off the context of the connection,
+ *	being reconnected.
+ *
+ * @warning After calling reconnect the caller *MUST NOT* attempt to use
+ *	the old handle in any other operations, as its memory will have been
+ *	freed.
+ *
+ * @see fr_connection_get
+ * @param[in,out] pool to reconnect the connection in.
+ * @param[in,out] conn to reconnect.
+ * @return new connection handle if successful else NULL.
+ */
+void *fr_connection_reconnect(fr_connection_pool_t *pool, void *conn)
+{
+	void		*new_conn;
+	fr_connection_t	*this;
+
+	if (!pool || !conn) return NULL;
+
+	/*
+	 *	Don't allow opening of new connections if we're trying
+	 *	to exit.
+	 */
+	if (main_config.exiting) {
+		fr_connection_release(pool, conn);
+		return NULL;
+	}
+
+	/*
+	 *	If fr_connection_find is successful the pool is now locked
+	 */
+	this = fr_connection_find(pool, conn);
+	if (!this) return NULL;
+
+	new_conn = fr_connection_reconnect_internal(pool, this);
+	pthread_mutex_unlock(&pool->mutex);
+
+	return new_conn;
+}
+
+/** Delete a connection from the connection pool.
+ *
+ * Resolves the connection handle to a connection, then (if found)
+ * closes, unlinks and frees that connection.
+ *
+ * @note Must be called with the mutex free.
+ *
+ * @param[in,out] pool Connection pool to modify.
+ * @param[in] conn to delete.
+ * @param[in] msg why the connection was closed.
+ * @return
+ *	- 0 If the connection could not be found.
+ *	- 1 if the connection was deleted.
+ */
+int fr_connection_close(fr_connection_pool_t *pool, void *conn, char const *msg)
+{
+	fr_connection_t *this;
+
+	this = fr_connection_find(pool, conn);
+	if (!this) return 0;
+
+	fr_connection_close_internal(pool, this, "Deleting connection", msg);
+	fr_connection_pool_check(pool);
+	return 1;
+}
diff --git a/src/main/crypt.c b/src/main/crypt.c
new file mode 100644
index 0000000..99c66d8
--- /dev/null
+++ b/src/main/crypt.c
@@ -0,0 +1,97 @@
+/*
+ * crypt.c	A thread-safe crypt wrapper
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2000-2006  The FreeRADIUS server project
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/libradius.h>
+
+#ifdef HAVE_CRYPT_H
+#include <crypt.h>
+#endif
+
+#ifdef HAVE_PTHREAD_H
+#include <pthread.h>
+
+/*
+ *  No pthreads, no mutex.
+ */
+static bool fr_crypt_init = false;
+static pthread_mutex_t fr_crypt_mutex;
+#endif
+
+
+/*
+ * performs a crypt password check in an thread-safe way.
+ *
+ * returns:  0 -- check succeeded
+ *	  -1 -- failed to crypt
+ *	   1 -- check failed
+ */
+int fr_crypt_check(char const *key, char const *crypted)
+{
+	char *passwd;
+	int cmp = 0;
+
+#ifdef HAVE_PTHREAD_H
+	/*
+	 *	Ensure we're thread-safe, as crypt() isn't.
+	 */
+	if (fr_crypt_init == false) {
+		pthread_mutex_init(&fr_crypt_mutex, NULL);
+		fr_crypt_init = true;
+	}
+
+	pthread_mutex_lock(&fr_crypt_mutex);
+#endif
+
+	passwd = crypt(key, crypted);
+
+	/*
+	 *	Got something, check it within the lock.  This is
+	 *	faster than copying it to a local buffer, and the
+	 *	time spent within the lock is critical.
+	 */
+	if (passwd) {
+		cmp = strcmp(crypted, passwd);
+	}
+
+#ifdef HAVE_PTHREAD_H
+	pthread_mutex_unlock(&fr_crypt_mutex);
+#endif
+
+	/*
+	 *	Error.
+	 */
+	if (!passwd) {
+		return -1;
+	}
+
+	/*
+	 *	OK, return OK.
+	 */
+	if (cmp == 0) {
+		return 0;
+	}
+
+	/*
+	 *	Comparison failed.
+	 */
+	return 1;
+}
diff --git a/src/main/detail.c b/src/main/detail.c
new file mode 100644
index 0000000..a5e8437
--- /dev/null
+++ b/src/main/detail.c
@@ -0,0 +1,1266 @@
+/*
+ * detail.c	Process the detail file
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2007  The FreeRADIUS server project
+ * Copyright 2007  Alan DeKok <aland@deployingradius.com>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/detail.h>
+#include <freeradius-devel/process.h>
+#include <freeradius-devel/rad_assert.h>
+
+#ifdef HAVE_SYS_STAT_H
+#include <sys/stat.h>
+#endif
+
+#ifdef HAVE_GLOB_H
+#include <glob.h>
+#endif
+
+#include <fcntl.h>
+
+#ifdef WITH_DETAIL
+
+#define USEC (1000000)
+
+static FR_NAME_NUMBER state_names[] = {
+	{ "unopened", STATE_UNOPENED },
+	{ "unlocked", STATE_UNLOCKED },
+	{ "header", STATE_HEADER },
+	{ "reading", STATE_READING },
+	{ "queued", STATE_QUEUED },
+	{ "running", STATE_RUNNING },
+	{ "no-reply", STATE_NO_REPLY },
+	{ "replied", STATE_REPLIED },
+
+	{ NULL, 0 }
+};
+
+
+/*
+ *	If we're limiting outstanding packets, then mark the response
+ *	as being sent.
+ */
+int detail_send(rad_listen_t *listener, REQUEST *request)
+{
+#ifdef WITH_DETAIL_THREAD
+	char c = 0;
+#endif
+	listen_detail_t *data = listener->data;
+
+	rad_assert(request->listener == listener);
+	rad_assert(listener->send == detail_send);
+
+	/*
+	 *	This request timed out.  Remember that, and tell the
+	 *	caller it's OK to read more "detail" file stuff.
+	 */
+	if (request->reply->code == 0) {
+		data->delay_time = data->retry_interval * USEC;
+		data->signal = 1;
+		data->state = STATE_NO_REPLY;
+
+		RDEBUG("detail (%s): No response to request.  Will retry in %d seconds",
+		       data->name, data->retry_interval);
+	} else {
+		int rtt;
+		struct timeval now;
+
+		RDEBUG("detail (%s): Done %s packet.", data->name, fr_packet_codes[request->packet->code]);
+
+		/*
+		 *	We call gettimeofday a lot.  But it should be OK,
+		 *	because there's nothing else to do.
+		 */
+		gettimeofday(&now, NULL);
+
+		/*
+		 *	If we haven't sent a packet in the last second, reset
+		 *	the RTT.
+		 */
+		now.tv_sec -= 1;
+		if (timercmp(&data->last_packet, &now, <)) {
+			data->has_rtt = false;
+		}
+		now.tv_sec += 1;
+
+		/*
+		 *	Only one detail packet may be outstanding at a time,
+		 *	so it's safe to update some entries in the detail
+		 *	structure.
+		 *
+		 *	We keep smoothed round trip time (SRTT), but not round
+		 *	trip timeout (RTO).  We use SRTT to calculate a rough
+		 *	load factor.
+		 */
+		rtt = now.tv_sec - request->packet->timestamp.tv_sec;
+		rtt *= USEC;
+		rtt += now.tv_usec;
+		rtt -= request->packet->timestamp.tv_usec;
+
+		/*
+		 *	If we're proxying, the RTT is our processing time,
+		 *	plus the network delay there and back, plus the time
+		 *	on the other end to process the packet.  Ideally, we
+		 *	should remove the network delays from the RTT, but we
+		 *	don't know what they are.
+		 *
+		 *	So, to be safe, we over-estimate the total cost of
+		 *	processing the packet.
+		 */
+		if (!data->has_rtt) {
+			data->has_rtt = true;
+			data->srtt = rtt;
+			data->rttvar = rtt / 2;
+
+		} else {
+			data->rttvar -= data->rttvar >> 2;
+			data->rttvar += (data->srtt - rtt);
+			data->srtt -= data->srtt >> 3;
+			data->srtt += rtt >> 3;
+		}
+
+		/*
+		 *	Calculate the time we wait before sending the next
+		 *	packet.
+		 *
+		 *	rtt / (rtt + delay) = load_factor / 100
+		 */
+		data->delay_time = (data->srtt * (100 - data->load_factor)) / (data->load_factor);
+
+		/*
+		 *	Cap delay at no less than 4 packets/s.  If the
+		 *	end system can't handle this, then it's very
+		 *	broken.
+		 */
+		if (data->delay_time > (USEC / 4)) data->delay_time= USEC / 4;
+
+		RDEBUG3("detail (%s): Received response for request %d.  Will read the next packet in %d seconds",
+			data->name, request->number, data->delay_time / USEC);
+
+		data->last_packet = now;
+		data->signal = 1;
+		data->state = STATE_REPLIED;
+		data->counter++;
+	}
+
+#ifdef WITH_DETAIL_THREAD
+	if (write(data->child_pipe[1], &c, 1) < 0) {
+		RERROR("detail (%s): Failed writing ack to reader thread: %s", data->name, fr_syserror(errno));
+	}
+#else
+	radius_signal_self(RADIUS_SIGNAL_SELF_DETAIL);
+#endif
+
+	return 0;
+}
+
+
+/*
+ *	Open the detail file, if we can.
+ *
+ *	FIXME: create it, if it's not already there, so that the main
+ *	server select() will wake us up if there's anything to read.
+ */
+static int detail_open(rad_listen_t *this)
+{
+	struct stat st;
+	listen_detail_t *data = this->data;
+
+	rad_assert(data->state == STATE_UNOPENED);
+	data->delay_time = USEC;
+
+	/*
+	 *	Open detail.work first, so we don't lose
+	 *	accounting packets.  It's probably better to
+	 *	duplicate them than to lose them.
+	 *
+	 *	Note that we're not writing to the file, but
+	 *	we've got to open it for writing in order to
+	 *	establish the lock, to prevent rlm_detail from
+	 *	writing to it.
+	 *
+	 *	This also means that if we're doing globbing,
+	 *	this file will be read && processed before the
+	 *	file globbing is done.
+	 */
+	data->fp = NULL;
+	data->work_fd = open(data->filename_work, O_RDWR);
+
+	/*
+	 *	Couldn't open it for a reason OTHER than "it doesn't
+	 *	exist".  Complain and tell the admin.
+	 */
+	if ((data->work_fd < 0) && (errno != ENOENT)) {
+		ERROR("Failed opening detail file %s: %s",
+		      data->filename_work, fr_syserror(errno));
+		return 0;
+	}
+
+	/*
+	 *	The file doesn't exist.  Poll for it again.
+	 */
+	if (data->work_fd < 0) {
+#ifndef HAVE_GLOB_H
+		return 0;
+#else
+		unsigned int	i;
+		int		found;
+		time_t		chtime;
+		char const	*filename;
+		glob_t		files;
+
+		DEBUG2("detail (%s): Polling for detail file", data->name);
+
+		memset(&files, 0, sizeof(files));
+		if (glob(data->filename, 0, NULL, &files) != 0) {
+		noop:
+			globfree(&files);
+			return 0;
+		}
+
+		/*
+		 *	Loop over the glob'd files, looking for the
+		 *	oldest one.
+		 */
+		chtime = 0;
+		found = -1;
+		for (i = 0; i < files.gl_pathc; i++) {
+			if (stat(files.gl_pathv[i], &st) < 0) continue;
+
+			if ((i == 0) || (st.st_ctime < chtime)) {
+				chtime = st.st_ctime;
+				found = i;
+			}
+		}
+
+		if (found < 0) goto noop;
+
+		/*
+		 *	Rename detail to detail.work
+		 */
+		filename = files.gl_pathv[found];
+
+		DEBUG("detail (%s): Renaming %s -> %s", data->name, filename, data->filename_work);
+		if (rename(filename, data->filename_work) < 0) {
+			ERROR("detail (%s): Failed renaming %s to %s: %s",
+			      data->name, filename, data->filename_work, fr_syserror(errno));
+			goto noop;
+		}
+
+		globfree(&files);	/* Shouldn't be using anything in files now */
+
+		/*
+		 *	And try to open the filename.
+		 */
+		data->work_fd = open(data->filename_work, O_RDWR);
+		if (data->work_fd < 0) {
+			ERROR("Failed opening detail file %s: %s",
+					data->filename_work, fr_syserror(errno));
+			return 0;
+		}
+#endif
+	} /* else detail.work existed, and we opened it */
+
+	rad_assert(data->vps == NULL);
+	rad_assert(data->fp == NULL);
+
+	data->state = STATE_UNLOCKED;
+
+	data->client_ip.af = AF_UNSPEC;
+	data->timestamp = 0;
+	data->offset = data->last_offset = data->timestamp_offset = 0;
+	data->packets = 0;
+	data->tries = 0;
+	data->done_entry = false;
+
+	return 1;
+}
+
+
+/*
+ *	FIXME: add a configuration "exit when done" so that the detail
+ *	file reader can be used as a one-off tool to update stuff.
+ *
+ *	The time sequence for reading from the detail file is:
+ *
+ *	t_0		signalled that the server is idle, and we
+ *			can read from the detail file.
+ *
+ *	t_rtt		the packet has been processed successfully,
+ *			wait for t_delay to enforce load factor.
+ *
+ *	t_rtt + t_delay wait for signal that the server is idle.
+ *
+ */
+#ifndef WITH_DETAIL_THREAD
+static RADIUS_PACKET *detail_poll(rad_listen_t *listener);
+
+int detail_recv(rad_listen_t *listener)
+{
+	RADIUS_PACKET *packet;
+	listen_detail_t *data = listener->data;
+	RAD_REQUEST_FUNP fun = NULL;
+
+	/*
+	 *	We may be in the main thread.  It needs to update the
+	 *	timers before we try to read from the file again.
+	 */
+	if (data->signal) return 0;
+
+	packet = detail_poll(listener);
+	if (!packet) return -1;
+
+	if (DEBUG_ENABLED2) {
+		VALUE_PAIR *vp;
+		vp_cursor_t cursor;
+
+		DEBUG2("detail (%s): Read packet from %s", data->name, data->filename_work);
+		for (vp = fr_cursor_init(&cursor, &packet->vps);
+		     vp;
+		     vp = fr_cursor_next(&cursor)) {
+			debug_pair(vp);
+		}
+	}
+
+	switch (packet->code) {
+	case PW_CODE_ACCOUNTING_REQUEST:
+		fun = rad_accounting;
+		break;
+
+	case PW_CODE_COA_REQUEST:
+	case PW_CODE_DISCONNECT_REQUEST:
+		fun = rad_coa_recv;
+		break;
+
+	default:
+		rad_free(&packet);
+		data->state = STATE_REPLIED;
+		return 0;
+	}
+
+	/*
+	 *	Don't bother doing limit checks, etc.
+	 */
+	if (!request_receive(NULL, listener, packet, &data->detail_client, fun)) {
+		rad_free(&packet);
+		data->state = STATE_NO_REPLY;	/* try again later */
+		return 0;
+	}
+
+	return 1;
+}
+#else
+int detail_recv(rad_listen_t *listener)
+{
+	char c = 0;
+	ssize_t rcode;
+	RADIUS_PACKET *packet;
+	listen_detail_t *data = listener->data;
+	RAD_REQUEST_FUNP fun = NULL;
+
+	/*
+	 *	Block until there's a packet ready.
+	 */
+	rcode = read(data->master_pipe[0], &packet, sizeof(packet));
+	if (rcode <= 0) return rcode;
+
+	rad_assert(packet != NULL);
+
+	if (DEBUG_ENABLED2) {
+		VALUE_PAIR *vp;
+		vp_cursor_t cursor;
+
+		DEBUG2("detail (%s): Read packet from %s", data->name, data->filename_work);
+		for (vp = fr_cursor_init(&cursor, &packet->vps);
+		     vp;
+		     vp = fr_cursor_next(&cursor)) {
+			debug_pair(vp);
+		}
+	}
+
+	switch (packet->code) {
+	case PW_CODE_ACCOUNTING_REQUEST:
+		fun = rad_accounting;
+		break;
+
+	case PW_CODE_COA_REQUEST:
+	case PW_CODE_DISCONNECT_REQUEST:
+		fun = rad_coa_recv;
+		break;
+
+	default:
+		data->state = STATE_REPLIED;
+		goto signal_thread;
+	}
+
+	if (!request_receive(NULL, listener, packet, &data->detail_client, fun)) {
+		data->state = STATE_NO_REPLY;	/* try again later */
+
+	signal_thread:
+		rad_free(&packet);
+		if (write(data->child_pipe[1], &c, 1) < 0) {
+			ERROR("detail (%s): Failed writing ack to reader thread: %s", data->name,
+			      fr_syserror(errno));
+		}
+	}
+
+	/*
+	 *	Wait for the child thread to write an answer to the pipe
+	 */
+	return 0;
+}
+#endif
+
+static RADIUS_PACKET *detail_poll(rad_listen_t *listener)
+{
+	int		y;
+	char		key[256], op[8], value[1024];
+	vp_cursor_t	cursor;
+	VALUE_PAIR	*vp;
+	RADIUS_PACKET	*packet;
+	char		buffer[2048];
+	listen_detail_t *data = listener->data;
+
+	switch (data->state) {
+	case STATE_UNOPENED:
+open_file:
+		rad_assert(data->work_fd < 0);
+
+		if (!detail_open(listener)) return NULL;
+
+		rad_assert(data->state == STATE_UNLOCKED);
+		rad_assert(data->work_fd >= 0);
+
+		/* FALL-THROUGH */
+
+	/*
+	 *	Try to lock fd.  If we can't, return.
+	 *	If we can, continue.  This means that
+	 *	the server doesn't block while waiting
+	 *	for the lock to open...
+	 */
+	case STATE_UNLOCKED:
+		/*
+		 *	Note that we do NOT block waiting for
+		 *	the lock.  We've re-named the file
+		 *	above, so we've already guaranteed
+		 *	that any *new* detail writer will not
+		 *	be opening this file.  The only
+		 *	purpose of the lock is to catch a race
+		 *	condition where the execution
+		 *	"ping-pongs" between radiusd &
+		 *	radrelay.
+		 */
+		if (rad_lockfd_nonblock(data->work_fd, 0) < 0) {
+			/*
+			 *	Close the FD.  The main loop
+			 *	will wake up in a second and
+			 *	try again.
+			 */
+			close(data->work_fd);
+			data->fp = NULL;
+			data->work_fd = -1;
+			data->state = STATE_UNOPENED;
+			return NULL;
+		}
+
+		/*
+		 *	Only open for writing if we're
+		 *	marking requests as completed.
+		 */
+		data->fp = fdopen(data->work_fd, data->track ? "r+" : "r");
+		if (!data->fp) {
+			ERROR("detail (%s): FATAL: Failed to re-open detail file: %s",
+			      data->name, fr_syserror(errno));
+			fr_exit(1);
+		}
+
+		/*
+		 *	Look for the header
+		 */
+		data->state = STATE_HEADER;
+		data->delay_time = USEC;
+		data->vps = NULL;
+
+		/* FALL-THROUGH */
+
+	case STATE_HEADER:
+	do_header:
+		rad_assert(data->ctx == NULL);
+		MEM(data->ctx = talloc_init("detail"));
+
+		data->done_entry = false;
+		data->timestamp_offset = 0;
+
+		data->tries = 0;
+		if (!data->fp) {
+			data->state = STATE_UNOPENED;
+			goto open_file;
+		}
+
+		{
+			struct stat buf;
+
+			if (fstat(data->work_fd, &buf) < 0) {
+				ERROR("detail (%s): Failed to stat detail file: %s",
+				      data->name, fr_syserror(errno));
+
+				goto cleanup;
+			}
+			if (((off_t) ftell(data->fp)) == buf.st_size) {
+				goto cleanup;
+			}
+		}
+
+		/*
+		 *	End of file.  Delete it, and re-set
+		 *	everything.
+		 */
+		if (feof(data->fp)) {
+		cleanup:
+			DEBUG("detail (%s): Unlinking %s", data->name, data->filename_work);
+			unlink(data->filename_work);
+			if (data->fp) fclose(data->fp);
+			TALLOC_FREE(data->ctx);
+			data->fp = NULL;
+			data->work_fd = -1;
+			data->state = STATE_UNOPENED;
+			rad_assert(data->vps == NULL);
+
+			if (data->one_shot) {
+				INFO("detail (%s): Finished reading \"one shot\" detail file - Exiting", data->name);
+				radius_signal_self(RADIUS_SIGNAL_SELF_EXIT);
+			}
+
+			return NULL;
+		}
+
+		/*
+		 *	Else go read something.
+		 */
+		if (!fgets(buffer, sizeof(buffer), data->fp)) {
+			DEBUG("detail (%s): Failed reading header from file - %s",
+			      data->name, data->filename_work);
+			goto cleanup;
+		}
+
+		/*
+		 *	Badly formatted file: delete it.
+		 */
+		if (!strchr(buffer, '\n')) {
+			DEBUG("detail (%s): Invalid line without trailing LF - %s", data->name, buffer);
+			goto cleanup;
+		}
+
+		if (!sscanf(buffer, "%*s %*s %*d %*d:%*d:%*d %d", &y)) {
+			DEBUG("detail (%s): Failed reading detail file header in line - %s", data->name, buffer);
+			goto cleanup;
+		}
+
+		data->state = STATE_READING;
+		/* FALL-THROUGH */
+
+
+	/*
+	 *	Read more value-pair's, unless we're
+	 *	at EOF.  In that case, queue whatever
+	 *	we have.
+	 */
+	case STATE_READING:
+		rad_assert(data->fp != NULL);
+
+		fr_cursor_init(&cursor, &data->vps);
+
+		/*
+		 *	Read a header, OR a value-pair.
+		 */
+		while (fgets(buffer, sizeof(buffer), data->fp)) {
+			data->last_offset = data->offset;
+			data->offset = ftell(data->fp); /* for statistics */
+
+			/*
+			 *	Badly formatted file: delete it.
+			 */
+			if (!strchr(buffer, '\n')) {
+				WARN("detail (%s): Skipping line without trailing LF - %s", data->name, buffer);
+				fr_pair_list_free(&data->vps);
+				goto cleanup;
+			}
+
+			/*
+			 *	We're reading VP's, and got a blank line.
+			 *	That indicates the end of an entry.  Queue the
+			 *	packet.
+			 */
+			if (buffer[0] == '\n') {
+				data->state = STATE_QUEUED;
+				data->tries = 0;
+				data->packets++;
+				goto alloc_packet;
+			}
+
+			/*
+			 *	We have a full "attribute = value" line.
+			 *	If it doesn't look reasonable, skip it.
+			 *
+			 *	FIXME: print an error for badly formatted attributes?
+			 */
+			if (sscanf(buffer, "%255s %7s %1023s", key, op, value) != 3) {
+				DEBUG("detail (%s): Skipping badly formatted line - %s", data->name, buffer);
+				continue;
+			}
+
+			/*
+			 *	Should be =, :=, +=, ...
+			 */
+			if (!strchr(op, '=')) {
+				DEBUG("detail (%s): Skipping line without operator - %s", data->name, buffer);
+				continue;
+			}
+
+			/*
+			 *	Skip non-protocol attributes.
+			 */
+			if (!strcasecmp(key, "Request-Authenticator")) continue;
+
+			/*
+			 *	Set the original client IP address, based on
+			 *	what's in the detail file.
+			 *
+			 *	Hmm... we don't set the server IP address.
+			 *	or port.  Oh well.
+			 */
+			if (!strcasecmp(key, "Client-IP-Address")) {
+				data->client_ip.af = AF_INET;
+				if (ip_hton(&data->client_ip, AF_INET, value, false) < 0) {
+					DEBUG("detail (%s): Failed parsing Client-IP-Address", data->name);
+					fr_pair_list_free(&data->vps);
+					goto cleanup;
+				}
+				continue;
+			}
+
+			/*
+			 *	The original time at which we received the
+			 *	packet.  We need this to properly calculate
+			 *	Acct-Delay-Time.
+			 */
+			if (!strcasecmp(key, "Timestamp")) {
+				data->timestamp = atoi(value);
+				data->timestamp_offset = data->last_offset;
+
+				vp = fr_pair_afrom_num(data->ctx, PW_PACKET_ORIGINAL_TIMESTAMP, 0);
+				if (vp) {
+					vp->vp_date = (uint32_t) data->timestamp;
+					vp->type = VT_DATA;
+					fr_cursor_insert(&cursor, vp);
+				}
+				continue;
+			}
+
+			if (!strcasecmp(key, "Donestamp")) {
+				data->timestamp = atoi(value);
+				data->done_entry = true;
+				continue;
+			}
+
+			DEBUG3("detail (%s): Trying to read VP from line - %s", data->name, buffer);
+
+			/*
+			 *	Read one VP.
+			 *
+			 *	FIXME: do we want to check for non-protocol
+			 *	attributes like radsqlrelay does?
+			 */
+			vp = NULL;
+			if ((fr_pair_list_afrom_str(data->ctx, buffer, &vp) > 0) &&
+			    (vp != NULL)) {
+				fr_cursor_merge(&cursor, vp);
+			} else {
+				DEBUG("detail (%s): Failed reading VP from line - %s", data->name, buffer);
+				goto cleanup;
+			}
+		}
+
+		/*
+		 *	The writer doesn't check that the
+		 *	record was completely written.  If the
+		 *	disk is full, this can result in a
+		 *	truncated record which has no trailing
+		 *	blank line.  When that happens, it's a
+		 *	bad record, and we ignore it.
+		 */
+		if (feof(data->fp)) {
+			DEBUG("detail (%s): Truncated record: treating it as EOF for detail file %s",
+			      data->name, data->filename_work);
+			fr_pair_list_free(&data->vps);
+			goto cleanup;
+		}
+
+		/*
+		 *	Some kind of non-eof error.
+		 *
+		 *	FIXME: Leave the file in-place, and warn the
+		 *	administrator?
+		 */
+		DEBUG("detail (%s): Unknown error, deleting detail file %s",
+		      data->name, data->filename_work);
+		goto cleanup;
+
+	case STATE_QUEUED:
+		goto alloc_packet;
+
+	/*
+	 *	Periodically check what's going on.
+	 *	If the request is taking too long,
+	 *	retry it.
+	 */
+	case STATE_RUNNING:
+		if (time(NULL) < (data->running + (int)data->retry_interval)) {
+			return NULL;
+		}
+
+		DEBUG("detail (%s): No response to detail request.  Retrying", data->name);
+		/* FALL-THROUGH */
+
+	/*
+	 *	If there's no reply, keep
+	 *	retransmitting the current packet
+	 *	forever.
+	 */
+	case STATE_NO_REPLY:
+		data->state = STATE_QUEUED;
+		goto alloc_packet;
+
+	/*
+	 *	We have a reply.  Clean up the old
+	 *	request, and go read another one.
+	 */
+	case STATE_REPLIED:
+		if (data->track) {
+			rad_assert(data->fp != NULL);
+
+			if (fseek(data->fp, data->timestamp_offset, SEEK_SET) < 0) {
+				DEBUG("detail (%s): Failed seeking to timestamp offset: %s",
+				     data->name, fr_syserror(errno));
+			} else if (fwrite("\tDone", 1, 5, data->fp) < 5) {
+				DEBUG("detail (%s): Failed marking request as done: %s",
+				     data->name, fr_syserror(errno));
+			} else if (fflush(data->fp) != 0) {
+				DEBUG("detail (%s): Failed flushing marked detail file to disk: %s",
+				     data->name, fr_syserror(errno));
+			}
+
+			if (fseek(data->fp, data->offset, SEEK_SET) < 0) {
+				DEBUG("detail (%s): Failed seeking to next detail request: %s",
+				     data->name, fr_syserror(errno));
+			}
+		}
+
+		fr_pair_list_free(&data->vps);
+		TALLOC_FREE(data->ctx);
+		data->state = STATE_HEADER;
+		goto do_header;
+	}
+
+	/*
+	 *	Process the packet.
+	 */
+ alloc_packet:
+	if (data->done_entry) {
+		DEBUG2("detail (%s): Skipping record for timestamp %lu", data->name, data->timestamp);
+		fr_pair_list_free(&data->vps);
+		TALLOC_FREE(data->ctx);
+		data->state = STATE_HEADER;
+		goto do_header;
+	}
+
+	data->tries++;
+
+	/*
+	 *	We're done reading the file, but we didn't read
+	 *	anything.  Clean up, and don't return anything.
+	 */
+	if (!data->vps) {
+		WARN("detail (%s): Read empty packet from file %s",
+		     data->name, data->filename_work);
+		data->state = STATE_HEADER;
+		return NULL;
+	}
+
+	/*
+	 *	Allocate the packet.  If we fail, it's a serious
+	 *	problem.
+	 */
+	packet = rad_alloc(NULL, true);
+	if (!packet) {
+		ERROR("detail (%s): FATAL: Failed allocating memory for detail", data->name);
+		fr_exit(1);
+	}
+
+	memset(packet, 0, sizeof(*packet));
+	packet->sockfd = -1;
+	packet->src_ipaddr.af = AF_INET;
+	packet->src_ipaddr.ipaddr.ip4addr.s_addr = htonl(INADDR_NONE);
+
+	/*
+	 *	If everything's OK, this is a waste of memory.
+	 *	Otherwise, it lets us re-send the original packet
+	 *	contents, unmolested.
+	 */
+	packet->vps = fr_pair_list_copy(packet, data->vps);
+
+	packet->code = PW_CODE_ACCOUNTING_REQUEST;
+	vp = fr_pair_find_by_num(packet->vps, PW_PACKET_TYPE, 0, TAG_ANY);
+	if (vp) packet->code = vp->vp_integer;
+
+	gettimeofday(&packet->timestamp, NULL);
+
+	/*
+	 *	Remember where it came from, so that we don't
+	 *	proxy it to the place it came from...
+	 */
+	if (data->client_ip.af != AF_UNSPEC) {
+		packet->src_ipaddr = data->client_ip;
+	}
+
+	vp = fr_pair_find_by_num(packet->vps, PW_PACKET_SRC_IP_ADDRESS, 0, TAG_ANY);
+	if (vp) {
+		packet->src_ipaddr.af = AF_INET;
+		packet->src_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+		packet->src_ipaddr.prefix = 32;
+	} else {
+		vp = fr_pair_find_by_num(packet->vps, PW_PACKET_SRC_IPV6_ADDRESS, 0, TAG_ANY);
+		if (vp) {
+			packet->src_ipaddr.af = AF_INET6;
+			memcpy(&packet->src_ipaddr.ipaddr.ip6addr,
+			       &vp->vp_ipv6addr, sizeof(vp->vp_ipv6addr));
+			packet->src_ipaddr.prefix = 128;
+		}
+	}
+
+	vp = fr_pair_find_by_num(packet->vps, PW_PACKET_DST_IP_ADDRESS, 0, TAG_ANY);
+	if (vp) {
+		packet->dst_ipaddr.af = AF_INET;
+		packet->dst_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+		packet->dst_ipaddr.prefix = 32;
+	} else {
+		vp = fr_pair_find_by_num(packet->vps, PW_PACKET_DST_IPV6_ADDRESS, 0, TAG_ANY);
+		if (vp) {
+			packet->dst_ipaddr.af = AF_INET6;
+			memcpy(&packet->dst_ipaddr.ipaddr.ip6addr,
+			       &vp->vp_ipv6addr, sizeof(vp->vp_ipv6addr));
+			packet->dst_ipaddr.prefix = 128;
+		}
+	}
+
+	/*
+	 *	Generate packet ID, ports, IP via a counter.
+	 */
+	packet->id = data->counter & 0xff;
+	packet->src_port = 1024 + ((data->counter >> 8) & 0xff);
+	packet->dst_port = 1024 + ((data->counter >> 16) & 0xff);
+
+	packet->dst_ipaddr.af = AF_INET;
+	packet->dst_ipaddr.ipaddr.ip4addr.s_addr = htonl((INADDR_LOOPBACK & ~0xffffff) | ((data->counter >> 24) & 0xff));
+
+	/*
+	 *	Create / update accounting attributes.
+	 */
+	if (packet->code == PW_CODE_ACCOUNTING_REQUEST) {
+		/*
+		 *	Prefer the Event-Timestamp in the packet, if it
+		 *	exists.  That is when the event occurred, whereas the
+		 *	"Timestamp" field is when we wrote the packet to the
+		 *	detail file, which could have been much later.
+		 */
+		vp = fr_pair_find_by_num(packet->vps, PW_EVENT_TIMESTAMP, 0, TAG_ANY);
+		if (vp) {
+			data->timestamp = vp->vp_integer;
+		}
+
+		/*
+		 *	Look for Acct-Delay-Time, and update
+		 *	based on Acct-Delay-Time += (time(NULL) - timestamp)
+		 */
+		vp = fr_pair_find_by_num(packet->vps, PW_ACCT_DELAY_TIME, 0, TAG_ANY);
+		if (!vp) {
+			vp = fr_pair_afrom_num(packet, PW_ACCT_DELAY_TIME, 0);
+			rad_assert(vp != NULL);
+			fr_pair_add(&packet->vps, vp);
+		}
+		if (data->timestamp != 0) {
+			vp->vp_integer += time(NULL) - data->timestamp;
+		}
+	}
+
+	/*
+	 *	Set the transmission count.
+	 */
+	vp = fr_pair_find_by_num(packet->vps, PW_PACKET_TRANSMIT_COUNTER, 0, TAG_ANY);
+	if (!vp) {
+		vp = fr_pair_afrom_num(packet, PW_PACKET_TRANSMIT_COUNTER, 0);
+		rad_assert(vp != NULL);
+		fr_pair_add(&packet->vps, vp);
+	}
+	vp->vp_integer = data->tries;
+
+	data->state = STATE_RUNNING;
+	data->running = packet->timestamp.tv_sec;
+
+	return packet;
+}
+
+/*
+ *	Free detail-specific stuff.
+ */
+void detail_free(rad_listen_t *this)
+{
+	listen_detail_t *data = this->data;
+
+#ifdef WITH_DETAIL_THREAD
+	if (!check_config) {
+		ssize_t ret;
+		void *arg = NULL;
+
+		/*
+		 *	Mark the child pipes as unusable
+		 */
+		close(data->child_pipe[0]);
+		close(data->child_pipe[1]);
+		data->child_pipe[0] = -1;
+
+		/*
+		 *	Tell it to stop (interrupting its sleep)
+		 */
+		pthread_kill(data->pthread_id, SIGTERM);
+
+		/*
+		 *	Wait for it to acknowledge that it's stopped.
+		 */
+		ret = read(data->master_pipe[0], &arg, sizeof(arg));
+		if (ret < 0) {
+			ERROR("detail (%s): Reader thread exited without informing the master: %s",
+			      data->name, fr_syserror(errno));
+		} else if (ret != sizeof(arg)) {
+			ERROR("detail (%s): Invalid thread pointer received from reader thread during exit",
+			      data->name);
+			ERROR("detail (%s): Expected %zu bytes, got %zi bytes", data->name, sizeof(arg), ret);
+		}
+
+		close(data->master_pipe[0]);
+		close(data->master_pipe[1]);
+
+		if (arg) pthread_join(data->pthread_id, &arg);
+	}
+#endif
+
+	if (data->fp != NULL) {
+		fclose(data->fp);
+		data->fp = NULL;
+	}
+}
+
+
+int detail_print(rad_listen_t const *this, char *buffer, size_t bufsize)
+{
+	if (!this->server) {
+		return snprintf(buffer, bufsize, "%s",
+				((listen_detail_t *)(this->data))->filename);
+	}
+
+	return snprintf(buffer, bufsize, "detail file %s as server %s",
+			((listen_detail_t *)(this->data))->filename,
+			this->server);
+}
+
+
+/*
+ *	Delay while waiting for a file to be ready
+ */
+static int detail_delay(listen_detail_t *data)
+{
+	int delay = (data->poll_interval - 1) * USEC;
+
+	/*
+	 *	Add +/- 0.25s of jitter
+	 */
+	delay += (USEC * 3) / 4;
+	delay += fr_rand() % (USEC / 2);
+
+	DEBUG2("detail (%s): Detail listener state %s waiting %d.%06d sec",
+	       data->name,
+	       fr_int2str(state_names, data->state, "?"),
+	       (delay / USEC), delay % USEC);
+
+	return delay;
+}
+
+/*
+ *	Overloaded to return delay times.
+ */
+int detail_encode(UNUSED rad_listen_t *this, UNUSED REQUEST *request)
+{
+#ifdef WITH_DETAIL_THREAD
+	return 0;
+#else
+	listen_detail_t *data = this->data;
+
+	/*
+	 *	We haven't sent a packet... delay things a bit.
+	 */
+	if (!data->signal) return detail_delay(data);
+
+	data->signal = 0;
+
+	DEBUG2("detail (%s): Detail listener state %s signalled %d waiting %d.%06d sec",
+	       data->name,
+	       fr_int2str(state_names, data->state, "?"),
+	       data->signal,
+	       data->delay_time / USEC,
+	       data->delay_time % USEC);
+
+	return data->delay_time;
+#endif
+}
+
+/*
+ *	Overloaded to return "should we fix delay times"
+ */
+int detail_decode(rad_listen_t *this, REQUEST *request)
+{
+#ifdef WITH_DETAIL_THREAD
+	listen_detail_t *data = this->data;
+
+	RDEBUG("Received %s from detail file %s",
+	       fr_packet_codes[request->packet->code], data->filename_work);
+
+	rdebug_pair_list(L_DBG_LVL_1, request, request->packet->vps, "\t");
+
+	return 0;
+#else
+	listen_detail_t *data = this->data;
+
+	RDEBUG("Received %s from detail file %s",
+	       fr_packet_codes[request->packet->code], data->filename_work);
+
+	rdebug_pair_list(L_DBG_LVL_1, request, request->packet->vps, "\t");
+
+	return data->signal;
+#endif
+}
+
+
+#ifdef WITH_DETAIL_THREAD
+static void *detail_handler_thread(void *arg)
+{
+	char c;
+	rad_listen_t *this = arg;
+	listen_detail_t *data = this->data;
+
+	while (true) {
+		RADIUS_PACKET *packet;
+
+		while ((packet = detail_poll(this)) == NULL) {
+			usleep(detail_delay(data));
+
+			/*
+			 *	If we're supposed to exit then tell
+			 *	the master thread we've exited.
+			 */
+			if (data->child_pipe[0] < 0) {
+				packet = NULL;
+				if (write(data->master_pipe[1], &packet, sizeof(packet)) < 0) {
+					ERROR("detail (%s): Failed writing exit status to master: %s",
+					      data->name, fr_syserror(errno));
+				}
+				return NULL;
+			}
+		}
+
+		/*
+		 *	Keep retrying forever.
+		 *
+		 *	FIXME: cap the retries.
+		 */
+		do {
+			if (write(data->master_pipe[1], &packet, sizeof(packet)) < 0) {
+				ERROR("detail (%s): Failed passing detail packet pointer to master: %s",
+				      data->name, fr_syserror(errno));
+			}
+
+			if (read(data->child_pipe[0], &c, 1) < 0) {
+				ERROR("detail (%s): Failed getting detail packet ack from master: %s",
+				      data->name, fr_syserror(errno));
+				break;
+			}
+
+			if (data->delay_time > 0) usleep(data->delay_time);
+
+			packet = detail_poll(this);
+			if (!packet) break;
+		} while (data->state != STATE_REPLIED);
+	}
+
+	return NULL;
+}
+#endif
+
+
+static const CONF_PARSER detail_config[] = {
+	{ "detail", FR_CONF_OFFSET(PW_TYPE_FILE_OUTPUT | PW_TYPE_DEPRECATED, listen_detail_t, filename), NULL },
+	{ "filename", FR_CONF_OFFSET(PW_TYPE_FILE_OUTPUT | PW_TYPE_REQUIRED, listen_detail_t, filename), NULL },
+	{ "load_factor", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_detail_t, load_factor), STRINGIFY(10) },
+	{ "poll_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_detail_t, poll_interval), STRINGIFY(1) },
+	{ "retry_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_detail_t, retry_interval), STRINGIFY(30) },
+	{ "one_shot", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, listen_detail_t, one_shot), "no" },
+	{ "track", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, listen_detail_t, track), "no" },
+	CONF_PARSER_TERMINATOR
+};
+
+/*
+ *	Parse a detail section.
+ */
+int detail_parse(CONF_SECTION *cs, rad_listen_t *this)
+{
+	int		rcode;
+	listen_detail_t *data;
+	RADCLIENT	*client;
+	char		buffer[2048];
+
+	data = this->data;
+
+	rcode = cf_section_parse(cs, data, detail_config);
+	if (rcode < 0) {
+		cf_log_err_cs(cs, "Failed parsing listen section");
+		return -1;
+	}
+
+	data->name = cf_section_name2(cs);
+	if (!data->name) data->name = data->filename;
+
+	/*
+	 *	We don't do duplicate detection for "detail" sockets.
+	 */
+	this->nodup = true;
+	this->synchronous = false;
+
+	if (!data->filename) {
+		cf_log_err_cs(cs, "No detail file specified in listen section");
+		return -1;
+	}
+
+	FR_INTEGER_BOUND_CHECK("load_factor", data->load_factor, >=, 1);
+	FR_INTEGER_BOUND_CHECK("load_factor", data->load_factor, <=, 100);
+
+	FR_INTEGER_BOUND_CHECK("poll_interval", data->poll_interval, >=, 1);
+	FR_INTEGER_BOUND_CHECK("poll_interval", data->poll_interval, <=, 60);
+
+	FR_INTEGER_BOUND_CHECK("retry_interval", data->retry_interval, >=, 4);
+	FR_INTEGER_BOUND_CHECK("retry_interval", data->retry_interval, <=, 3600);
+
+	/*
+	 *	Only checking the config.  Don't start threads or anything else.
+	 */
+	if (check_config) return 0;
+
+	/*
+	 *	If the filename is a glob, use "detail.work" as the
+	 *	work file name.
+	 */
+	if ((strchr(data->filename, '*') != NULL) ||
+	    (strchr(data->filename, '[') != NULL)) {
+		char *p;
+
+#ifndef HAVE_GLOB_H
+		WARN("detail (%s): File \"%s\" appears to use file globbing, but it is not supported on this system",
+		     data->name, data->filename);
+#endif
+		strlcpy(buffer, data->filename, sizeof(buffer));
+		p = strrchr(buffer, FR_DIR_SEP);
+		if (p) {
+			p[1] = '\0';
+		} else {
+			buffer[0] = '\0';
+		}
+
+		/*
+		 *	Globbing cannot be done across directories.
+		 */
+		if ((strchr(buffer, '*') != NULL) ||
+		    (strchr(buffer, '[') != NULL)) {
+			cf_log_err_cs(cs, "Wildcard directories are not supported");
+			return -1;
+		}
+
+		strlcat(buffer, "detail.work",
+			sizeof(buffer) - strlen(buffer));
+
+	} else {
+		snprintf(buffer, sizeof(buffer), "%s.work", data->filename);
+	}
+
+	data->filename_work = talloc_strdup(data, buffer);
+
+	data->work_fd = -1;
+	data->vps = NULL;
+	data->fp = NULL;
+	data->state = STATE_UNOPENED;
+	data->delay_time = data->poll_interval * USEC;
+	data->signal = 1;
+
+	/*
+	 *	Initialize the fake client.
+	 */
+	client = &data->detail_client;
+	memset(client, 0, sizeof(*client));
+	client->ipaddr.af = AF_INET;
+	client->ipaddr.ipaddr.ip4addr.s_addr = INADDR_NONE;
+	client->ipaddr.prefix = 0;
+	client->longname = client->shortname = data->filename;
+	client->secret = client->shortname;
+	client->nas_type = talloc_strdup(data, "none");	/* Part of 'data' not dynamically allocated */
+
+#ifdef WITH_DETAIL_THREAD
+	/*
+	 *	Create the communication pipes.
+	 */
+	if (pipe(data->master_pipe) < 0) {
+		ERROR("detail (%s): Error opening internal pipe: %s", data->name, fr_syserror(errno));
+		fr_exit(1);
+	}
+
+	if (pipe(data->child_pipe) < 0) {
+		ERROR("detail (%s): Error opening internal pipe: %s", data->name, fr_syserror(errno));
+		fr_exit(1);
+	}
+
+	pthread_create(&data->pthread_id, NULL, detail_handler_thread, this);
+
+	this->fd = data->master_pipe[0];
+#endif
+
+	return 0;
+}
+#endif
diff --git a/src/main/evaluate.c b/src/main/evaluate.c
new file mode 100644
index 0000000..c8585b6
--- /dev/null
+++ b/src/main/evaluate.c
@@ -0,0 +1,1144 @@
+/*
+ * evaluate.c	Evaluate complex conditions
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2007  The FreeRADIUS server project
+ * Copyright 2007  Alan DeKok <aland@deployingradius.com>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/parser.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <ctype.h>
+
+#ifdef WITH_UNLANG
+#ifdef WITH_EVAL_DEBUG
+#  define EVAL_DEBUG(fmt, ...) printf("EVAL: ");printf(fmt, ## __VA_ARGS__);printf("\n");fflush(stdout)
+#else
+#  define EVAL_DEBUG(...)
+#endif
+
+FR_NAME_NUMBER const modreturn_table[] = {
+	{ "reject",		RLM_MODULE_REJECT       },
+	{ "fail",		RLM_MODULE_FAIL	 	},
+	{ "ok",			RLM_MODULE_OK	   	},
+	{ "handled",		RLM_MODULE_HANDLED      },
+	{ "invalid",		RLM_MODULE_INVALID      },
+	{ "userlock",		RLM_MODULE_USERLOCK     },
+	{ "notfound", 		RLM_MODULE_NOTFOUND     },
+	{ "noop",		RLM_MODULE_NOOP	 	},
+	{ "updated",		RLM_MODULE_UPDATED      },
+	{ NULL, 0 }
+};
+
+
+static bool all_digits(char const *string)
+{
+	char const *p = string;
+
+	rad_assert(p != NULL);
+
+	if (*p == '\0') return false;
+
+	if (*p == '-') p++;
+
+	while (isdigit((uint8_t) *p)) p++;
+
+	return (*p == '\0');
+}
+
+/** Evaluate a template
+ *
+ * Converts a vp_tmpl_t to a boolean value.
+ *
+ * @param[in] request the REQUEST
+ * @param[in] modreturn the previous module return code
+ * @param[in] depth of the recursion (only used for debugging)
+ * @param[in] vpt the template to evaluate
+ * @return -1 on error, 0 for "no match", 1 for "match".
+ */
+int radius_evaluate_tmpl(REQUEST *request, int modreturn, UNUSED int depth, vp_tmpl_t const *vpt)
+{
+	int rcode;
+	int modcode;
+	value_data_t data;
+
+	switch (vpt->type) {
+	case TMPL_TYPE_LITERAL:
+		modcode = fr_str2int(modreturn_table, vpt->name, RLM_MODULE_UNKNOWN);
+		if (modcode != RLM_MODULE_UNKNOWN) {
+			rcode = (modcode == modreturn);
+			break;
+		}
+
+		/*
+		 *	Else it's a literal string.  Empty string is
+		 *	false, non-empty string is true.
+		 *
+		 *	@todo: Maybe also check for digits?
+		 *
+		 *	The VPT *doesn't* have a "bare word" type,
+		 *	which arguably it should.
+		 */
+		rcode = (*vpt->name != '\0');
+		break;
+
+	case TMPL_TYPE_ATTR:
+	case TMPL_TYPE_LIST:
+		if (tmpl_find_vp(NULL, request, vpt) == 0) {
+			rcode = true;
+		} else {
+			rcode = false;
+		}
+		break;
+
+	case TMPL_TYPE_XLAT_STRUCT:
+	case TMPL_TYPE_XLAT:
+	case TMPL_TYPE_EXEC:
+	{
+		char *p;
+
+		if (!*vpt->name) return false;
+		rcode = tmpl_aexpand(request, &p, request, vpt, NULL, NULL);
+		if (rcode < 0) {
+			EVAL_DEBUG("FAIL %d", __LINE__);
+			return -1;
+		}
+		data.strvalue = p;
+		rcode = (data.strvalue && (*data.strvalue != '\0'));
+		talloc_free(data.ptr);
+	}
+		break;
+
+	/*
+	 *	Can't have a bare ... (/foo/) ...
+	 */
+	case TMPL_TYPE_REGEX:
+	case TMPL_TYPE_REGEX_STRUCT:
+		rad_assert(0 == 1);
+		/* FALL-THROUGH */
+
+	default:
+		EVAL_DEBUG("FAIL %d", __LINE__);
+		rcode = -1;
+		break;
+	}
+
+	return rcode;
+}
+
+#ifdef HAVE_REGEX
+/** Perform a regular expressions comparison between two operands
+ *
+ * @return -1 on error, 0 for "no match", 1 for "match".
+ */
+static int cond_do_regex(REQUEST *request, fr_cond_t const *c,
+		         PW_TYPE lhs_type, value_data_t const *lhs, size_t lhs_len,
+		         PW_TYPE rhs_type, value_data_t const *rhs, size_t rhs_len)
+{
+	vp_map_t const *map = c->data.map;
+
+	ssize_t		slen;
+	int		ret;
+
+	regex_t		*preg, *rreg = NULL;
+	regmatch_t	rxmatch[REQUEST_MAX_REGEX + 1];	/* +1 for %{0} (whole match) capture group */
+	size_t		nmatch = sizeof(rxmatch) / sizeof(regmatch_t);
+
+	if (!lhs || (lhs_type != PW_TYPE_STRING)) return -1;
+
+	EVAL_DEBUG("CMP WITH REGEX %s %s",
+		   map->rhs->tmpl_iflag ? "CASE INSENSITIVE" : "CASE SENSITIVE",
+		   map->rhs->tmpl_mflag ? "MULTILINE" : "SINGLELINE");
+
+	switch (map->rhs->type) {
+	case TMPL_TYPE_REGEX_STRUCT: /* pre-compiled to a regex */
+		preg = map->rhs->tmpl_preg;
+#ifdef HAVE_PCRE
+		rad_assert(preg->precompiled);
+#endif
+		break;
+
+	default:
+		rad_assert(rhs_type == PW_TYPE_STRING);
+		rad_assert(rhs->strvalue);
+		slen = regex_compile(request, &rreg, rhs->strvalue, rhs_len,
+				     map->rhs->tmpl_iflag, map->rhs->tmpl_mflag, true, true);
+		if (slen <= 0) {
+			REMARKER(rhs->strvalue, -slen, fr_strerror());
+			EVAL_DEBUG("FAIL %d", __LINE__);
+
+			return -1;
+		}
+		preg = rreg;
+#ifdef HAVE_PCRE
+		rad_assert(!preg->precompiled);
+#endif
+		break;
+	}
+
+	ret = regex_exec(preg, lhs->strvalue, lhs_len, rxmatch, &nmatch);
+	switch (ret) {
+	case 0:
+		EVAL_DEBUG("CLEARING SUBCAPTURES");
+		regex_sub_to_request(request, NULL, NULL, 0, NULL, 0);	/* clear out old entries */
+		break;
+
+	case 1:
+		EVAL_DEBUG("SETTING SUBCAPTURES");
+		regex_sub_to_request(request, &preg, lhs->strvalue, lhs_len, rxmatch, nmatch);
+		break;
+
+	case -1:
+		EVAL_DEBUG("REGEX ERROR");
+		REDEBUG("regex failed: %s", fr_strerror());
+		break;
+
+	default:
+		break;
+	}
+
+	if (preg) talloc_free(rreg);
+
+	return ret;
+}
+#endif
+
+#ifdef WITH_EVAL_DEBUG
+static void cond_print_operands(REQUEST *request,
+			   	PW_TYPE lhs_type, value_data_t const *lhs, size_t lhs_len,
+			   	PW_TYPE rhs_type, value_data_t const *rhs, size_t rhs_len)
+{
+	if (lhs) {
+		if (lhs_type == PW_TYPE_STRING) {
+			EVAL_DEBUG("LHS: \"%s\" (%zu)" , lhs->strvalue, lhs_len);
+		} else {
+			char *lhs_hex;
+
+			lhs_hex = talloc_array(request, char, (lhs_len * 2) + 1);
+
+			if (lhs_type == PW_TYPE_OCTETS) {
+				fr_bin2hex(lhs_hex, lhs->octets, lhs_len);
+			} else {
+				fr_bin2hex(lhs_hex, (uint8_t const *)lhs, lhs_len);
+			}
+
+			EVAL_DEBUG("LHS: 0x%s (%zu)", lhs_hex, lhs_len);
+
+			talloc_free(lhs_hex);
+		}
+	} else {
+		EVAL_DEBUG("LHS: VIRTUAL");
+	}
+
+	if (rhs) {
+		if (rhs_type == PW_TYPE_STRING) {
+			EVAL_DEBUG("RHS: \"%s\" (%zu)" , rhs->strvalue, rhs_len);
+		} else {
+			char *rhs_hex;
+
+			rhs_hex = talloc_array(request, char, (rhs_len * 2) + 1);
+
+			if (rhs_type == PW_TYPE_OCTETS) {
+				fr_bin2hex(rhs_hex, rhs->octets, rhs_len);
+			} else {
+				fr_bin2hex(rhs_hex, (uint8_t const *)rhs, rhs_len);
+			}
+
+			EVAL_DEBUG("RHS: 0x%s (%zu)", rhs_hex, rhs_len);
+
+			talloc_free(rhs_hex);
+		}
+	} else {
+		EVAL_DEBUG("RHS: COMPILED");
+	}
+}
+#endif
+
+/** Call the correct data comparison function for the condition
+ *
+ * Deals with regular expression comparisons, virtual attribute
+ * comparisons, and data comparisons.
+ *
+ * @return -1 on error, 0 for "no match", 1 for "match".
+ */
+static int cond_cmp_values(REQUEST *request, fr_cond_t const *c,
+			   PW_TYPE lhs_type, value_data_t const *lhs, size_t lhs_len,
+			   PW_TYPE rhs_type, value_data_t const *rhs, size_t rhs_len)
+{
+	vp_map_t const *map = c->data.map;
+	int rcode;
+
+#ifdef WITH_EVAL_DEBUG
+		EVAL_DEBUG("CMP OPERANDS");
+		cond_print_operands(request, lhs_type, lhs, lhs_len, rhs_type, rhs, rhs_len);
+#endif
+
+#ifdef HAVE_REGEX
+	/*
+	 *	Regex comparison
+	 */
+	if (map->op == T_OP_REG_EQ) {
+		rcode = cond_do_regex(request, c, lhs_type, lhs, lhs_len, rhs_type, rhs, rhs_len);
+		goto finish;
+	}
+#endif
+	/*
+	 *	Virtual attribute comparison.
+	 */
+	if (c->pass2_fixup == PASS2_PAIRCOMPARE) {
+		VALUE_PAIR *vp;
+
+		EVAL_DEBUG("CMP WITH PAIRCOMPARE");
+		rad_assert(map->lhs->type == TMPL_TYPE_ATTR);
+
+		vp = fr_pair_afrom_da(request, map->lhs->tmpl_da);
+		vp->op = c->data.map->op;
+
+		value_data_copy(vp, &vp->data, rhs_type, rhs, rhs_len);
+		vp->vp_length = rhs_len;
+
+		rcode = paircompare(request, request->packet->vps, vp, NULL);
+		rcode = (rcode == 0) ? 1 : 0;
+		talloc_free(vp);
+		goto finish;
+	}
+
+	/*
+	 *	At this point both operands should have been normalised
+	 *	to the same type, and there's no special comparisons
+	 *	left.
+	 */
+	rad_assert(lhs_type == rhs_type);
+
+	EVAL_DEBUG("CMP WITH VALUE DATA");
+	rcode = value_data_cmp_op(map->op, lhs_type, lhs, lhs_len, rhs_type, rhs, rhs_len);
+finish:
+	switch (rcode) {
+	case 0:
+		EVAL_DEBUG("FALSE");
+		break;
+
+	case 1:
+		EVAL_DEBUG("TRUE");
+		break;
+
+	default:
+		EVAL_DEBUG("ERROR %i", rcode);
+		break;
+	}
+
+	return rcode;
+}
+
+
+static size_t regex_escape(UNUSED REQUEST *request, char *out, size_t outlen, char const *in, UNUSED void *arg)
+{
+	char *p = out;
+
+	while (*in && (outlen > 2)) {
+		switch (*in) {
+		case '\\':
+		case '.':
+		case '*':
+		case '+':
+		case '?':
+		case '|':
+		case '^':
+		case '$':
+		case '[':	/* we don't list close braces */
+		case '{':
+		case '(':
+			*(p++) = '\\';
+			outlen--;
+			/* FALL-THROUGH */
+
+		default:
+			*(p++) = *(in++);
+			outlen--;
+			break;
+		}
+	}
+
+	*(p++) = '\0';
+	return p - out;
+}
+
+
+/** Convert both operands to the same type
+ *
+ * If casting is successful, we call cond_cmp_values to do the comparison
+ *
+ * @return -1 on error, 0 for "no match", 1 for "match".
+ */
+static int cond_normalise_and_cmp(REQUEST *request, fr_cond_t const *c,
+				  PW_TYPE lhs_type, DICT_ATTR const *lhs_enumv,
+				  value_data_t const *lhs, size_t lhs_len)
+{
+	vp_map_t const *map = c->data.map;
+
+	DICT_ATTR const *cast = NULL;
+	PW_TYPE cast_type = PW_TYPE_INVALID;
+
+	int rcode;
+
+	PW_TYPE rhs_type = PW_TYPE_INVALID;
+	DICT_ATTR const *rhs_enumv = NULL;
+	value_data_t const *rhs = NULL;
+	size_t rhs_len;
+
+	value_data_t lhs_cast, rhs_cast;
+	void *lhs_cast_buff = NULL, *rhs_cast_buff = NULL;
+
+	xlat_escape_t escape = NULL;
+
+	/*
+	 *	Cast operand to correct type.
+	 *
+	 *	With hack for strings that look like integers, to cast them
+	 *	to 64 bit unsigned integers.
+	 *
+	 * @fixme For things like this it'd be useful to have a 64bit signed type.
+	 */
+#define CAST(_s) \
+do {\
+	if ((cast_type != PW_TYPE_INVALID) && (_s ## _type != PW_TYPE_INVALID) && (cast_type != _s ## _type)) {\
+		ssize_t r;\
+		EVAL_DEBUG("CASTING " #_s " FROM %s TO %s",\
+			   fr_int2str(dict_attr_types, _s ## _type, "<INVALID>"),\
+			   fr_int2str(dict_attr_types, cast_type, "<INVALID>"));\
+		r = value_data_cast(request, &_s ## _cast, cast_type, cast, _s ## _type, _s ## _enumv, _s, _s ## _len);\
+		if (r < 0) {\
+			REDEBUG("Failed casting " #_s " operand: %s", fr_strerror());\
+			rcode = -1;\
+			goto finish;\
+		}\
+		if (cast && cast->flags.is_pointer) _s ## _cast_buff = _s ## _cast.ptr;\
+		_s ## _type = cast_type;\
+		_s ## _len = (size_t)r;\
+		_s = &_s ## _cast;\
+	}\
+} while (0)
+
+#define CHECK_INT_CAST(_l, _r) \
+do {\
+	if ((cast_type == PW_TYPE_INVALID) &&\
+	    _l && (_l ## _type == PW_TYPE_STRING) &&\
+	    _r && (_r ## _type == PW_TYPE_STRING) &&\
+	    all_digits(lhs->strvalue) && all_digits(rhs->strvalue)) {\
+	    	cast_type = PW_TYPE_INTEGER64;\
+	    	EVAL_DEBUG("OPERANDS ARE NUMBER STRINGS, SETTING CAST TO integer64");\
+	}\
+} while (0)
+
+	/*
+	 *	Regular expressions need both operands to be strings
+	 */
+#ifdef HAVE_REGEX
+	if (map->op == T_OP_REG_EQ) {
+		cast_type = PW_TYPE_STRING;
+
+		if (map->rhs->type == TMPL_TYPE_XLAT_STRUCT) escape = regex_escape;
+	}
+	else
+#endif
+	/*
+	 *	If it's a pair comparison, data gets cast to the
+	 *	type of the pair comparison attribute.
+	 *
+	 *	Magic attribute is always the LHS.
+	 */
+	if (c->pass2_fixup == PASS2_PAIRCOMPARE) {
+		rad_assert(!c->cast);
+		rad_assert(map->lhs->type == TMPL_TYPE_ATTR);
+#ifndef NDEBUG
+		/* expensive assert */
+		rad_assert((map->rhs->type != TMPL_TYPE_ATTR) || !radius_find_compare(map->rhs->tmpl_da));
+#endif
+		cast = map->lhs->tmpl_da;
+		cast_type = cast->type;
+
+		EVAL_DEBUG("NORMALISATION TYPE %s (PAIRCMP TYPE)",
+			   fr_int2str(dict_attr_types, cast->type, "<INVALID>"));
+	/*
+	 *	Otherwise we use the explicit cast, or implicit
+	 *	cast (from an attribute reference).
+	 *	We already have the data for the lhs, so we convert
+	 *	it here.
+	 */
+	} else if (c->cast) {
+		cast = c->cast;
+		EVAL_DEBUG("NORMALISATION TYPE %s (EXPLICIT CAST)",
+			   fr_int2str(dict_attr_types, cast->type, "<INVALID>"));
+	} else if (map->lhs->type == TMPL_TYPE_ATTR) {
+		cast = map->lhs->tmpl_da;
+		EVAL_DEBUG("NORMALISATION TYPE %s (IMPLICIT FROM LHS REF)",
+			   fr_int2str(dict_attr_types, cast->type, "<INVALID>"));
+	} else if (map->rhs->type == TMPL_TYPE_ATTR) {
+		cast = map->rhs->tmpl_da;
+		EVAL_DEBUG("NORMALISATION TYPE %s (IMPLICIT FROM RHS REF)",
+			   fr_int2str(dict_attr_types, cast->type, "<INVALID>"));
+	} else if (map->lhs->type == TMPL_TYPE_DATA) {
+		cast_type = map->lhs->tmpl_data_type;
+		EVAL_DEBUG("NORMALISATION TYPE %s (IMPLICIT FROM LHS DATA)",
+			   fr_int2str(dict_attr_types, cast_type, "<INVALID>"));
+	} else if (map->rhs->type == TMPL_TYPE_DATA) {
+		cast_type = map->rhs->tmpl_data_type;
+		EVAL_DEBUG("NORMALISATION TYPE %s (IMPLICIT FROM RHS DATA)",
+			   fr_int2str(dict_attr_types, cast_type, "<INVALID>"));
+	}
+
+	if (cast) cast_type = cast->type;
+
+	switch (map->rhs->type) {
+	case TMPL_TYPE_ATTR:
+	{
+		VALUE_PAIR *vp;
+		vp_cursor_t cursor;
+
+		for (vp = tmpl_cursor_init(&rcode, &cursor, request, map->rhs);
+		     vp;
+	     	     vp = tmpl_cursor_next(&cursor, map->rhs)) {
+			rhs_type = vp->da->type;
+			rhs_enumv = vp->da;
+			rhs = &vp->data;
+			rhs_len = vp->vp_length;
+
+			CHECK_INT_CAST(lhs, rhs);
+			CAST(lhs);
+			CAST(rhs);
+
+			rcode = cond_cmp_values(request, c, lhs_type, lhs, lhs_len, rhs_type, rhs, rhs_len);
+			if (rcode != 0) break;
+
+			TALLOC_FREE(rhs_cast_buff);
+		}
+	}
+		break;
+
+	case TMPL_TYPE_DATA:
+		rhs_type = map->rhs->tmpl_data_type;
+		rhs = &map->rhs->tmpl_data_value;
+		rhs_len = map->rhs->tmpl_data_length;
+
+		CHECK_INT_CAST(lhs, rhs);
+		CAST(lhs);
+		CAST(rhs);
+
+		rcode = cond_cmp_values(request, c, lhs_type, lhs, lhs_len, rhs_type, rhs, rhs_len);
+		break;
+
+	/*
+	 *	Expanded types start as strings, then get converted
+	 *	to the type of the attribute or the explicit cast.
+	 */
+	case TMPL_TYPE_LITERAL:
+	case TMPL_TYPE_EXEC:
+	case TMPL_TYPE_XLAT:
+	case TMPL_TYPE_XLAT_STRUCT:
+	{
+		ssize_t ret;
+		value_data_t data;
+
+		if (map->rhs->type != TMPL_TYPE_LITERAL) {
+			char *p;
+
+			ret = tmpl_aexpand(request, &p, request, map->rhs, escape, NULL);
+			if (ret < 0) {
+				EVAL_DEBUG("FAIL [%i]", __LINE__);
+				rcode = -1;
+				goto finish;
+			}
+			data.strvalue = p;
+			rhs_len = ret;
+
+		} else {
+			data.strvalue = map->rhs->name;
+			rhs_len = map->rhs->len;
+		}
+		rad_assert(data.strvalue);
+
+		rhs_type = PW_TYPE_STRING;
+		rhs = &data;
+
+		CHECK_INT_CAST(lhs, rhs);
+		CAST(lhs);
+		CAST(rhs);
+
+		rcode = cond_cmp_values(request, c, lhs_type, lhs, lhs_len, rhs_type, rhs, rhs_len);
+		if (map->rhs->type != TMPL_TYPE_LITERAL)talloc_free(data.ptr);
+
+		break;
+	}
+
+	/*
+	 *	RHS is a compiled regex, we don't need to do anything with it.
+	 */
+	case TMPL_TYPE_REGEX_STRUCT:
+		CAST(lhs);
+		rcode = cond_cmp_values(request, c, lhs_type, lhs, lhs_len, PW_TYPE_INVALID, NULL, 0);
+		break;
+	/*
+	 *	Unsupported types (should have been parse errors)
+	 */
+	case TMPL_TYPE_NULL:
+	case TMPL_TYPE_LIST:
+	case TMPL_TYPE_UNKNOWN:
+	case TMPL_TYPE_ATTR_UNDEFINED:
+	case TMPL_TYPE_REGEX:	/* Should now be a TMPL_TYPE_REGEX_STRUCT or TMPL_TYPE_XLAT_STRUCT */
+		rad_assert(0);
+		rcode = -1;
+		break;
+	}
+
+finish:
+	talloc_free(lhs_cast_buff);
+	talloc_free(rhs_cast_buff);
+
+	return rcode;
+}
+
+
+/** Evaluate a map
+ *
+ * @param[in] request the REQUEST
+ * @param[in] modreturn the previous module return code
+ * @param[in] depth of the recursion (only used for debugging)
+ * @param[in] c the condition to evaluate
+ * @return -1 on error, 0 for "no match", 1 for "match".
+ */
+int radius_evaluate_map(REQUEST *request, UNUSED int modreturn, UNUSED int depth, fr_cond_t const *c)
+{
+	int rcode = 0;
+
+	vp_map_t const *map = c->data.map;
+
+	EVAL_DEBUG(">>> MAP TYPES LHS: %s, RHS: %s",
+		   fr_int2str(tmpl_names, map->lhs->type, "???"),
+		   fr_int2str(tmpl_names, map->rhs->type, "???"));
+
+	switch (map->lhs->type) {
+	/*
+	 *	LHS is an attribute or list
+	 */
+	case TMPL_TYPE_LIST:
+	case TMPL_TYPE_ATTR:
+	{
+		VALUE_PAIR *vp;
+		vp_cursor_t cursor;
+		/*
+		 *	Legacy paircompare call, skip processing the magic attribute
+		 *	if it's the LHS and cast RHS to the same type.
+		 */
+		if ((c->pass2_fixup == PASS2_PAIRCOMPARE) && (map->op != T_OP_REG_EQ)) {
+#ifndef NDEBUG
+			rad_assert(radius_find_compare(map->lhs->tmpl_da)); /* expensive assert */
+#endif
+			rcode = cond_normalise_and_cmp(request, c, PW_TYPE_INVALID, NULL, NULL, 0);
+			break;
+		}
+		for (vp = tmpl_cursor_init(&rcode, &cursor, request, map->lhs);
+		     vp;
+	     	     vp = tmpl_cursor_next(&cursor, map->lhs)) {
+			/*
+			 *	Evaluate all LHS values, condition evaluates to true
+			 *	if we get at least one set of operands that
+			 *	evaluates to true.
+			 */
+	     		rcode = cond_normalise_and_cmp(request, c, vp->da->type, vp->da, &vp->data, vp->vp_length);
+	     		if (rcode != 0) break;
+		}
+	}
+		break;
+
+	case TMPL_TYPE_DATA:
+		rcode = cond_normalise_and_cmp(request, c,
+					      map->lhs->tmpl_data_type, NULL, &map->lhs->tmpl_data_value,
+					      map->lhs->tmpl_data_length);
+		break;
+
+	case TMPL_TYPE_LITERAL:
+	case TMPL_TYPE_EXEC:
+	case TMPL_TYPE_XLAT:
+	case TMPL_TYPE_XLAT_STRUCT:
+	{
+		ssize_t ret;
+		value_data_t data;
+
+		if (map->lhs->type != TMPL_TYPE_LITERAL) {
+			char *p;
+
+			ret = tmpl_aexpand(request, &p, request, map->lhs, NULL, NULL);
+			if (ret < 0) {
+				EVAL_DEBUG("FAIL [%i]", __LINE__);
+				return ret;
+			}
+			data.strvalue = p;
+		} else {
+			data.strvalue = map->lhs->name;
+			ret = map->lhs->len;
+		}
+		rad_assert(data.strvalue);
+
+		rcode = cond_normalise_and_cmp(request, c, PW_TYPE_STRING, NULL, &data, ret);
+		if (map->lhs->type != TMPL_TYPE_LITERAL) talloc_free(data.ptr);
+	}
+		break;
+
+	/*
+	 *	Unsupported types (should have been parse errors)
+	 */
+	case TMPL_TYPE_NULL:
+	case TMPL_TYPE_ATTR_UNDEFINED:
+	case TMPL_TYPE_UNKNOWN:
+	case TMPL_TYPE_REGEX:		/* should now be a TMPL_TYPE_REGEX_STRUCT or TMPL_TYPE_XLAT_STRUCT */
+	case TMPL_TYPE_REGEX_STRUCT:	/* not allowed as LHS */
+		rad_assert(0);
+		rcode = -1;
+		break;
+	}
+
+	EVAL_DEBUG("<<<");
+
+	return rcode;
+}
+
+/** Evaluate a fr_cond_t;
+ *
+ * @param[in] request the REQUEST
+ * @param[in] modreturn the previous module return code
+ * @param[in] depth of the recursion (only used for debugging)
+ * @param[in] c the condition to evaluate
+ * @return -1 on failure, -2 on attribute not found, 0 for "no match", 1 for "match".
+ */
+int radius_evaluate_cond(REQUEST *request, int modreturn, int depth, fr_cond_t const *c)
+{
+	int rcode = -1;
+#ifdef WITH_EVAL_DEBUG
+	char buffer[1024];
+
+	fr_cond_sprint(buffer, sizeof(buffer), c);
+	EVAL_DEBUG("%s", buffer);
+#endif
+
+	while (c) {
+		switch (c->type) {
+		case COND_TYPE_EXISTS:
+			rcode = radius_evaluate_tmpl(request, modreturn, depth, c->data.vpt);
+			/* Existence checks are special, because we expect them to fail */
+			if (rcode < 0) rcode = 0;
+			break;
+
+		case COND_TYPE_MAP:
+			rcode = radius_evaluate_map(request, modreturn, depth, c);
+			break;
+
+		case COND_TYPE_CHILD:
+			rcode = radius_evaluate_cond(request, modreturn, depth + 1, c->data.child);
+			break;
+
+		case COND_TYPE_TRUE:
+			rcode = true;
+			break;
+
+		case COND_TYPE_FALSE:
+			rcode = false;
+			break;
+		default:
+			EVAL_DEBUG("FAIL %d", __LINE__);
+			return -1;
+		}
+
+		if (rcode < 0) return rcode;
+
+		if (c->negate) rcode = !rcode;
+
+		if (!c->next) break;
+
+		/*
+		 *	FALSE && ... = FALSE
+		 */
+		if (!rcode && (c->next_op == COND_AND)) return false;
+
+		/*
+		 *	TRUE || ... = TRUE
+		 */
+		if (rcode && (c->next_op == COND_OR)) return true;
+
+		c = c->next;
+	}
+
+	if (rcode < 0) {
+		EVAL_DEBUG("FAIL %d", __LINE__);
+	}
+	return rcode;
+}
+#endif
+
+
+/*
+ *	The fr_pair_list_move() function in src/lib/pair.c does all sorts of
+ *	extra magic that we don't want here.
+ *
+ *	FIXME: integrate this with the code calling it, so that we
+ *	only fr_pair_list_copy() those attributes that we're really going to
+ *	use.
+ */
+void radius_pairmove(REQUEST *request, VALUE_PAIR **to, VALUE_PAIR *from, bool do_xlat)
+{
+	int i, j, count, from_count, to_count, tailto;
+	vp_cursor_t cursor;
+	VALUE_PAIR *vp, *next, **last;
+	VALUE_PAIR **from_list, **to_list;
+	VALUE_PAIR *append, **append_tail;
+	VALUE_PAIR *prepend;
+	VALUE_PAIR *to_copy;
+	bool *edited = NULL;
+	REQUEST *fixup = NULL;
+	TALLOC_CTX *ctx;
+
+	/*
+	 *	Set up arrays for editing, to remove some of the
+	 *	O(N^2) dependencies.  This also makes it easier to
+	 *	insert and remove attributes.
+	 *
+	 *	It also means that the operators apply ONLY to the
+	 *	attributes in the original list.  With the previous
+	 *	implementation of fr_pair_list_move(), adding two attributes
+	 *	via "+=" and then "=" would mean that the second one
+	 *	wasn't added, because of the existence of the first
+	 *	one in the "to" list.  This implementation doesn't
+	 *	have that bug.
+	 *
+	 *	Also, the previous implementation did NOT implement
+	 *	"-=" correctly.  If two of the same attributes existed
+	 *	in the "to" list, and you tried to subtract something
+	 *	matching the *second* value, then the fr_pair_delete_by_num()
+	 *	function was called, and the *all* attributes of that
+	 *	number were deleted.  With this implementation, only
+	 *	the matching attributes are deleted.
+	 */
+	count = 0;
+	for (vp = fr_cursor_init(&cursor, &from); vp; vp = fr_cursor_next(&cursor)) count++;
+	from_list = talloc_array(request, VALUE_PAIR *, count);
+
+	for (vp = fr_cursor_init(&cursor, to); vp; vp = fr_cursor_next(&cursor)) count++;
+	to_list = talloc_array(request, VALUE_PAIR *, count);
+
+	prepend = NULL;
+
+	append = NULL;
+	append_tail = &append;
+
+	/*
+	 *	Move the lists to the arrays, and break the list
+	 *	chains.
+	 */
+	from_count = 0;
+	for (vp = from; vp != NULL; vp = next) {
+		next = vp->next;
+		from_list[from_count++] = vp;
+		vp->next = NULL;
+	}
+
+	to_count = 0;
+	ctx = talloc_parent(*to);
+	to_copy = fr_pair_list_copy(ctx, *to);
+	for (vp = to_copy; vp != NULL; vp = next) {
+		next = vp->next;
+		to_list[to_count++] = vp;
+		vp->next = NULL;
+	}
+	tailto = to_count;
+	edited = talloc_zero_array(request, bool, to_count);
+
+	RDEBUG4("::: FROM %d TO %d MAX %d", from_count, to_count, count);
+
+	/*
+	 *	Now that we have the lists initialized, start working
+	 *	over them.
+	 */
+	for (i = 0; i < from_count; i++) {
+		int found;
+
+		RDEBUG4("::: Examining %s", from_list[i]->da->name);
+
+		if (do_xlat) radius_xlat_do(request, from_list[i]);
+
+		/*
+		 *	Attribute should be appended, OR the "to" list
+		 *	is empty, and we're supposed to replace or
+		 *	"add if not existing".
+		 */
+		if (from_list[i]->op == T_OP_ADD) goto do_append;
+
+		/*
+		 *	The attribute needs to be prepended to the "to"
+		 *	list - store it in the prepend list
+		 */
+
+		if (from_list[i]->op == T_OP_PREPEND) {
+			RDEBUG4("::: PREPENDING %s FROM %d TO %d",
+			       from_list[i]->da->name, i, tailto);
+			from_list[i]->next = prepend;
+			prepend = from_list[i];
+			prepend->op = T_OP_EQ;
+			from_list[i] = NULL;
+			continue;
+		}
+		found = false;
+		for (j = 0; j < to_count; j++) {
+			if (edited[j] || !to_list[j] || !from_list[i]) continue;
+
+			/*
+			 *	Attributes aren't the same, skip them.
+			 */
+			if (from_list[i]->da != to_list[j]->da) {
+				continue;
+			}
+
+			/*
+			 *	We don't use a "switch" statement here
+			 *	because we want to break out of the
+			 *	"for" loop over 'j' in most cases.
+			 */
+
+			/*
+			 *	Over-write the FIRST instance of the
+			 *	matching attribute name.  We free the
+			 *	one in the "to" list, and move over
+			 *	the one in the "from" list.
+			 */
+			if (from_list[i]->op == T_OP_SET) {
+				RDEBUG4("::: OVERWRITING %s FROM %d TO %d",
+				       to_list[j]->da->name, i, j);
+				fr_pair_list_free(&to_list[j]);
+				to_list[j] = from_list[i];
+				from_list[i] = NULL;
+				edited[j] = true;
+				break;
+			}
+
+			/*
+			 *	Add the attribute only if it does not
+			 *	exist... but it exists, so we stop
+			 *	looking.
+			 */
+			if (from_list[i]->op == T_OP_EQ) {
+				found = true;
+				break;
+			}
+
+			/*
+			 *	Delete every attribute, independent
+			 *	of its value.
+			 */
+			if (from_list[i]->op == T_OP_CMP_FALSE) {
+				goto delete;
+			}
+
+			/*
+			 *	Delete all matching attributes from
+			 *	"to"
+			 */
+			if ((from_list[i]->op == T_OP_SUB) ||
+			    (from_list[i]->op == T_OP_CMP_EQ) ||
+			    (from_list[i]->op == T_OP_LE) ||
+			    (from_list[i]->op == T_OP_GE)) {
+				int rcode;
+				int old_op = from_list[i]->op;
+
+				/*
+				 *	Check for equality.
+				 */
+				from_list[i]->op = T_OP_CMP_EQ;
+
+				/*
+				 *	If equal, delete the one in
+				 *	the "to" list.
+				 */
+				rcode = radius_compare_vps(NULL, from_list[i],
+							   to_list[j]);
+				/*
+				 *	We may want to do more
+				 *	subtractions, so we re-set the
+				 *	operator back to it's original
+				 *	value.
+				 */
+				from_list[i]->op = old_op;
+
+				switch (old_op) {
+				case T_OP_CMP_EQ:
+					if (rcode != 0) goto delete;
+					break;
+
+				case T_OP_SUB:
+					if (rcode == 0) {
+					delete:
+						RDEBUG4("::: DELETING %s FROM %d TO %d",
+						       from_list[i]->da->name, i, j);
+						fr_pair_list_free(&to_list[j]);
+						to_list[j] = NULL;
+					}
+					break;
+
+					/*
+					 *	Enforce <=.  If it's
+					 *	>, replace it.
+					 */
+				case T_OP_LE:
+					if (rcode > 0) {
+						RDEBUG4("::: REPLACING %s FROM %d TO %d",
+						       from_list[i]->da->name, i, j);
+						fr_pair_list_free(&to_list[j]);
+						to_list[j] = from_list[i];
+						from_list[i] = NULL;
+						edited[j] = true;
+					}
+					break;
+
+				case T_OP_GE:
+					if (rcode < 0) {
+						RDEBUG4("::: REPLACING %s FROM %d TO %d",
+						       from_list[i]->da->name, i, j);
+						fr_pair_list_free(&to_list[j]);
+						to_list[j] = from_list[i];
+						from_list[i] = NULL;
+						edited[j] = true;
+					}
+					break;
+				}
+
+				continue;
+			}
+
+			rad_assert(0 == 1); /* panic! */
+		}
+
+		/*
+		 *	We were asked to add it if it didn't exist,
+		 *	and it doesn't exist.  Move it over to the
+		 *	tail of the "to" list, UNLESS it was already
+		 *	moved by another operator.
+		 */
+		if (!found && from_list[i]) {
+			if ((from_list[i]->op == T_OP_EQ) ||
+			    (from_list[i]->op == T_OP_LE) ||
+			    (from_list[i]->op == T_OP_GE) ||
+			    (from_list[i]->op == T_OP_SET)) {
+			do_append:
+				RDEBUG4("::: APPENDING %s FROM %d TO %d",
+				       from_list[i]->da->name, i, tailto);
+				*append_tail = from_list[i];
+				from_list[i]->op = T_OP_EQ;
+				from_list[i] = NULL;
+				append_tail = &(*append_tail)->next;
+			}
+		}
+	}
+
+	/*
+	 *	Delete attributes in the "from" list.
+	 */
+	for (i = 0; i < from_count; i++) {
+		if (!from_list[i]) continue;
+		fr_pair_list_free(&from_list[i]);
+	}
+	talloc_free(from_list);
+
+	RDEBUG4("::: TO in %d out %d", to_count, tailto);
+
+	/*
+	 *	Re-chain the "to" list.
+	 */
+	fr_pair_list_free(to);
+	last = to;
+
+	if (to == &request->packet->vps) {
+		fixup = request;
+	} else if (request->parent && (to == &request->parent->packet->vps)) {
+		fixup = request->parent;
+	}
+
+	/*
+	 *	Walk the list of "prepend" attributes first
+	 */
+	for (vp = prepend; vp != NULL; vp = vp->next) {
+		*last = vp;
+		last = &(*last)->next;
+	}
+
+	/*
+	 *	Next add on remaining items in the "to" list
+	 */
+	for (i = 0; i < tailto; i++) {
+		if (!to_list[i]) continue;
+
+		vp = to_list[i];
+		RDEBUG4("::: to[%d] = %s", i, vp->da->name);
+
+		/*
+		 *	Mash the operator to a simple '='.  The
+		 *	operators in the "to" list aren't used for
+		 *	anything.  BUT they're used in the "detail"
+		 *	file and debug output, where we don't want to
+		 *	see the operators.
+		 */
+		vp->op = T_OP_EQ;
+
+		*last = vp;
+		last = &(*last)->next;
+	}
+
+	/*
+	 *	And finally add in the attributes we're appending to
+	 *	the tail of the "to" list.
+	 */
+	*last = append;
+
+	/*
+	 *	Fix dumb cache issues
+	 */
+	if (fixup) {
+		fixup->username = NULL;
+		fixup->password = NULL;
+
+		for (vp = fixup->packet->vps; vp != NULL; vp = vp->next) {
+			if (vp->da->vendor) continue;
+
+			if ((vp->da->attr == PW_USER_NAME) && !fixup->username) {
+				fixup->username = vp;
+
+			} else if (vp->da->attr == PW_STRIPPED_USER_NAME) {
+				fixup->username = vp;
+
+			} else if (vp->da->attr == PW_USER_PASSWORD) {
+				fixup->password = vp;
+			}
+		}
+	}
+
+	rad_assert(request->packet != NULL);
+
+	talloc_free(to_list);
+	talloc_free(edited);
+}
diff --git a/src/main/exec.c b/src/main/exec.c
new file mode 100644
index 0000000..67243f7
--- /dev/null
+++ b/src/main/exec.c
@@ -0,0 +1,633 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/*
+ * $Id$
+ *
+ * @file exec.c
+ * @brief Execute external programs.
+ *
+ * @copyright 2000-2004,2006  The FreeRADIUS server project
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <sys/file.h>
+
+#include <fcntl.h>
+#include <ctype.h>
+
+#ifdef HAVE_SYS_WAIT_H
+#	include <sys/wait.h>
+#endif
+#ifndef WEXITSTATUS
+#	define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8)
+#endif
+#ifndef WIFEXITED
+#	define WIFEXITED(stat_val) (((stat_val) & 255) == 0)
+#endif
+
+#define MAX_ARGV (256)
+
+/** Start a process
+ *
+ * @param cmd Command to execute. This is parsed into argv[] parts,
+ * 	then each individual argv part is xlat'ed.
+ * @param request Current reuqest
+ * @param exec_wait set to 1 if you want to read from or write to child
+ * @param[in,out] input_fd pointer to int, receives the stdin file.
+ * 	descriptor. Set to NULL and the child will have /dev/null on stdin
+ * @param[in,out] output_fd pinter to int, receives the stdout file
+ * 	descriptor. Set to NULL and child will have /dev/null on stdout.
+ * @param input_pairs list of value pairs - these will be put into
+ * 	the environment variables of the child.
+ * @param shell_escape values before passing them as arguments.
+ * @return PID of the child process, -1 on error.
+ */
+pid_t radius_start_program(char const *cmd, REQUEST *request, bool exec_wait,
+			   int *input_fd, int *output_fd,
+			   VALUE_PAIR *input_pairs, bool shell_escape)
+{
+#ifndef __MINGW32__
+	VALUE_PAIR *vp;
+	int n;
+	int to_child[2] = {-1, -1};
+	int from_child[2] = {-1, -1};
+	pid_t pid;
+#endif
+	int		argc;
+	int		i;
+	char const	**argv_p;
+	char		*argv[MAX_ARGV], **argv_start = argv;
+	char		argv_buf[4096];
+#define MAX_ENVP 1024
+	char *envp[MAX_ENVP];
+	int envlen = 0;
+
+	/*
+	 *	Stupid array decomposition...
+	 *
+	 *	If we do memcpy(&argv_p, &argv, sizeof(argv_p)) src ends up being a char **
+	 *	pointing to the value of the first element.
+	 */
+	memcpy(&argv_p, &argv_start, sizeof(argv_p));
+	argc = rad_expand_xlat(request, cmd, MAX_ARGV, argv_p, true, sizeof(argv_buf), argv_buf);
+	if (argc <= 0) {
+		DEBUG("invalid command line '%s'.", cmd);
+		return -1;
+	}
+
+
+#ifndef NDEBUG
+	if (rad_debug_lvl > 2) {
+		DEBUG3("executing cmd %s", cmd);
+		for (i = 0; i < argc; i++) {
+			DEBUG3("\t[%d] %s", i, argv[i]);
+		}
+	}
+#endif
+
+#ifndef __MINGW32__
+	/*
+	 *	Open a pipe for child/parent communication, if necessary.
+	 */
+	if (exec_wait) {
+		if (input_fd) {
+			if (pipe(to_child) != 0) {
+				DEBUG("Couldn't open pipe to child: %s", fr_syserror(errno));
+				return -1;
+			}
+		}
+		if (output_fd) {
+			if (pipe(from_child) != 0) {
+				DEBUG("Couldn't open pipe from child: %s", fr_syserror(errno));
+				/* safe because these either need closing or are == -1 */
+				close(to_child[0]);
+				close(to_child[1]);
+				return -1;
+			}
+		}
+	}
+
+	envp[0] = NULL;
+
+	if (input_pairs) {
+		vp_cursor_t cursor;
+		char buffer[1024];
+
+		/*
+		 *	Set up the environment variables in the
+		 *	parent, so we don't call libc functions that
+		 *	hold mutexes.  They might be locked when we fork,
+		 *	and will remain locked in the child.
+		 */
+		for (vp = fr_cursor_init(&cursor, &input_pairs);
+		     vp;
+		     vp = fr_cursor_next(&cursor)) {
+			/*
+			 *	Hmm... maybe we shouldn't pass the
+			 *	user's password in an environment
+			 *	variable...
+			 */
+			snprintf(buffer, sizeof(buffer), "%s=", vp->da->name);
+			if (shell_escape) {
+				char *p;
+
+				for (p = buffer; *p != '='; p++) {
+					if (*p == '-') {
+						*p = '_';
+					} else if (isalpha((uint8_t) *p)) {
+						*p = toupper((uint8_t) *p);
+					}
+				}
+			}
+
+			n = strlen(buffer);
+			vp_prints_value(buffer + n, sizeof(buffer) - n, vp, shell_escape ? '"' : 0);
+
+			envp[envlen++] = strdup(buffer);
+
+			/*
+			 *	Don't add too many attributes.
+			 */
+			if (envlen == (MAX_ENVP - 1)) break;
+
+			/*
+			 *	NULL terminate for execve
+			 */
+			envp[envlen] = NULL;
+		}
+	}
+
+	if (exec_wait) {
+		pid = rad_fork();	/* remember PID */
+	} else {
+		pid = fork();		/* don't wait */
+	}
+
+	if (pid == 0) {
+		int devnull;
+
+		/*
+		 *	Child process.
+		 *
+		 *	We try to be fail-safe here. So if ANYTHING
+		 *	goes wrong, we exit with status 1.
+		 */
+
+		/*
+		 *	Open STDIN to /dev/null
+		 */
+		devnull = open("/dev/null", O_RDWR);
+		if (devnull < 0) {
+			DEBUG("Failed opening /dev/null: %s\n", fr_syserror(errno));
+
+			/*
+			 *	Where the status code is interpreted as a module rcode
+			 * 	one is subtracted from it, to allow 0 to equal success
+			 *
+			 *	2 is RLM_MODULE_FAIL + 1
+			 */
+			exit(2);
+		}
+
+		/*
+		 *	Only massage the pipe handles if the parent
+		 *	has created them.
+		 */
+		if (exec_wait) {
+			if (input_fd) {
+				close(to_child[1]);
+				dup2(to_child[0], STDIN_FILENO);
+			} else {
+				dup2(devnull, STDIN_FILENO);
+			}
+
+			if (output_fd) {
+				close(from_child[0]);
+				dup2(from_child[1], STDOUT_FILENO);
+			} else {
+				dup2(devnull, STDOUT_FILENO);
+			}
+
+		} else {	/* no pipe, STDOUT should be /dev/null */
+			dup2(devnull, STDIN_FILENO);
+			dup2(devnull, STDOUT_FILENO);
+		}
+
+		/*
+		 *	If we're not debugging, then we can't do
+		 *	anything with the error messages, so we throw
+		 *	them away.
+		 *
+		 *	If we are debugging, then we want the error
+		 *	messages to go to the STDERR of the server.
+		 */
+		if (rad_debug_lvl == 0) {
+			dup2(devnull, STDERR_FILENO);
+		}
+		close(devnull);
+
+		/*
+		 *	The server may have MANY FD's open.  We don't
+		 *	want to leave dangling FD's for the child process
+		 *	to play funky games with, so we close them.
+		 */
+		closefrom(3);
+
+		/*
+		 *	I swear the signature for execve is wrong and should
+		 *	take 'char const * const argv[]'.
+		 *
+		 *	Note: execve(), unlike system(), treats all the space
+		 *	delimited arguments as literals, so there's no need
+		 *	to perform additional escaping.
+		 */
+		execve(argv[0], argv, envp);
+		printf("Failed to execute \"%s\": %s", argv[0], fr_syserror(errno)); /* fork output will be captured */
+
+		/*
+		 *	Where the status code is interpreted as a module rcode
+		 * 	one is subtracted from it, to allow 0 to equal success
+		 *
+		 *	2 is RLM_MODULE_FAIL + 1
+		 */
+		exit(2);
+	}
+
+	/*
+	 *	Free child environment variables
+	 */
+	for (i = 0; i < envlen; i++) {
+		free(envp[i]);
+	}
+
+	/*
+	 *	Parent process.
+	 */
+	if (pid < 0) {
+		DEBUG("Couldn't fork %s: %s", argv[0], fr_syserror(errno));
+		if (exec_wait) {
+			/* safe because these either need closing or are == -1 */
+			close(to_child[0]);
+			close(to_child[1]);
+			close(from_child[0]);
+			close(from_child[1]);
+		}
+		return -1;
+	}
+
+	/*
+	 *	We're not waiting, exit, and ignore any child's status.
+	 */
+	if (exec_wait) {
+		/*
+		 *	Close the ends of the pipe(s) the child is using
+		 *	return the ends of the pipe(s) our caller wants
+		 *
+		 */
+		if (input_fd) {
+			*input_fd = to_child[1];
+			close(to_child[0]);
+		}
+		if (output_fd) {
+			*output_fd = from_child[0];
+			close(from_child[1]);
+		}
+	}
+
+	return pid;
+#else
+	if (exec_wait) {
+		DEBUG("Wait is not supported");
+		return -1;
+	}
+
+	{
+		/*
+		 *	The _spawn and _exec families of functions are
+		 *	found in Windows compiler libraries for
+		 *	portability from UNIX. There is a variety of
+		 *	functions, including the ability to pass
+		 *	either a list or array of parameters, to
+		 *	search in the PATH or otherwise, and whether
+		 *	or not to pass an environment (a set of
+		 *	environment variables). Using _spawn, you can
+		 *	also specify whether you want the new process
+		 *	to close your program (_P_OVERLAY), to wait
+		 *	until the new process is finished (_P_WAIT) or
+		 *	for the two to run concurrently (_P_NOWAIT).
+
+		 *	_spawn and _exec are useful for instances in
+		 *	which you have simple requirements for running
+		 *	the program, don't want the overhead of the
+		 *	Windows header file, or are interested
+		 *	primarily in portability.
+		 */
+
+		/*
+		 *	FIXME: check return code... what is it?
+		 */
+		_spawnve(_P_NOWAIT, argv[0], argv, envp);
+	}
+
+	return 0;
+#endif
+}
+
+/** Read from the child process.
+ *
+ * @param fd file descriptor to read from.
+ * @param pid pid of child, will be reaped if it dies.
+ * @param timeout amount of time to wait, in seconds.
+ * @param answer buffer to write into.
+ * @param left length of buffer.
+ * @return -1 on error, or length of output.
+ */
+int radius_readfrom_program(int fd, pid_t pid, int timeout,
+			    char *answer, int left)
+{
+	int done = 0;
+#ifndef __MINGW32__
+	int status;
+	struct timeval start;
+#ifdef O_NONBLOCK
+	bool nonblock = true;
+#endif
+
+#ifdef O_NONBLOCK
+	/*
+	 *	Try to set it non-blocking.
+	 */
+	do {
+		int flags;
+
+		if ((flags = fcntl(fd, F_GETFL, NULL)) < 0)  {
+			nonblock = false;
+			break;
+		}
+
+		flags |= O_NONBLOCK;
+		if( fcntl(fd, F_SETFL, flags) < 0) {
+			nonblock = false;
+			break;
+		}
+	} while (0);
+#endif
+
+
+	/*
+	 *	Read from the pipe until we doesn't get any more or
+	 *	until the message is full.
+	 */
+	gettimeofday(&start, NULL);
+	while (1) {
+		int rcode;
+		fd_set fds;
+		struct timeval when, elapsed, wake;
+
+		FD_ZERO(&fds);
+		FD_SET(fd, &fds);
+
+		gettimeofday(&when, NULL);
+		rad_tv_sub(&when, &start, &elapsed);
+		if (elapsed.tv_sec >= timeout) goto too_long;
+
+		when.tv_sec = timeout;
+		when.tv_usec = 0;
+		rad_tv_sub(&when, &elapsed, &wake);
+
+		rcode = select(fd + 1, &fds, NULL, NULL, &wake);
+		if (rcode == 0) {
+		too_long:
+			DEBUG("Child PID %u is taking too much time: forcing failure and killing child.", (unsigned int) pid);
+			kill(pid, SIGTERM);
+			close(fd); /* should give SIGPIPE to child, too */
+
+			/*
+			 *	Clean up the child entry.
+			 */
+			rad_waitpid(pid, &status);
+			return -1;
+		}
+		if (rcode < 0) {
+			if (errno == EINTR) continue;
+			break;
+		}
+
+#ifdef O_NONBLOCK
+		/*
+		 *	Read as many bytes as possible.  The kernel
+		 *	will return the number of bytes available.
+		 */
+		if (nonblock) {
+			status = read(fd, answer + done, left);
+		} else
+#endif
+			/*
+			 *	There's at least 1 byte ready: read it.
+			 */
+			status = read(fd, answer + done, 1);
+
+		/*
+		 *	Nothing more to read: stop.
+		 */
+		if (status == 0) {
+			break;
+		}
+
+		/*
+		 *	Error: See if we have to continue.
+		 */
+		if (status < 0) {
+			/*
+			 *	We were interrupted: continue reading.
+			 */
+			if (errno == EINTR) {
+				continue;
+			}
+
+			/*
+			 *	There was another error.  Most likely
+			 *	The child process has finished, and
+			 *	exited.
+			 */
+			break;
+		}
+
+		done += status;
+		left -= status;
+		if (left <= 0) break;
+	}
+#endif	/* __MINGW32__ */
+
+	/* Strip trailing new lines */
+	while ((done > 0) && (answer[done - 1] == '\n')) {
+		answer[--done] = '\0';
+	}
+
+	return done;
+}
+
+/** Execute a program.
+ *
+ * @param[in,out] ctx to allocate new VALUE_PAIR (s) in.
+ * @param[out] out buffer to append plaintext (non valuepair) output.
+ * @param[in] outlen length of out buffer.
+ * @param[out] output_pairs list of value pairs - child stdout will be parsed and added into this list
+ *	of value pairs.
+ * @param[in] request Current request (may be NULL).
+ * @param[in] cmd Command to execute. This is parsed into argv[] parts, then each individual argv part
+ *	is xlat'ed.
+ * @param[in] input_pairs list of value pairs - these will be available in the environment of the child.
+ * @param[in] exec_wait set to 1 if you want to read from or write to child.
+ * @param[in] shell_escape values before passing them as arguments.
+ * @param[in] timeout amount of time to wait, in seconds.
+
+ * @return 0 if exec_wait==0, exit code if exec_wait!=0, -1 on error.
+ */
+int radius_exec_program(TALLOC_CTX *ctx, char *out, size_t outlen, VALUE_PAIR **output_pairs,
+			REQUEST *request, char const *cmd, VALUE_PAIR *input_pairs,
+			bool exec_wait, bool shell_escape, int timeout)
+
+{
+	pid_t pid;
+	int from_child;
+#ifndef __MINGW32__
+	char *p;
+	pid_t child_pid;
+	int comma = 0;
+	int status, ret = 0;
+	ssize_t len;
+	char answer[4096];
+#endif
+
+	RDEBUG2("Executing: %s:", cmd);
+
+	if (out) *out = '\0';
+
+	pid = radius_start_program(cmd, request, exec_wait, NULL, &from_child, input_pairs, shell_escape);
+	if (pid < 0) {
+		return -1;
+	}
+
+	if (!exec_wait) {
+		return 0;
+	}
+
+#ifndef __MINGW32__
+	len = radius_readfrom_program(from_child, pid, timeout, answer, sizeof(answer));
+	if (len < 0) {
+		/*
+		 *	Failure - radius_readfrom_program will
+		 *	have called close(from_child) for us
+		 */
+		RERROR("Failed to read from child output");
+		return -1;
+
+	}
+	answer[len] = '\0';
+
+	/*
+	 *	Make sure that the writer can't block while writing to
+	 *	a pipe that no one is reading from anymore.
+	 */
+	close(from_child);
+
+	if (len == 0) {
+		goto wait;
+	}
+
+	/*
+	 *	Parse the output, if any.
+	 */
+	if (output_pairs) {
+		/*
+		 *	HACK: Replace '\n' with ',' so that
+		 *	fr_pair_list_afrom_str() can parse the buffer in
+		 *	one go (the proper way would be to
+		 *	fix fr_pair_list_afrom_str(), but oh well).
+		 */
+		for (p = answer; *p; p++) {
+			if (*p == '\n') {
+				*p = comma ? ' ' : ',';
+				p++;
+				comma = 0;
+			}
+			if (*p == ',') {
+				comma++;
+			}
+		}
+
+		/*
+		 *	Replace any trailing comma by a NUL.
+		 */
+		if (answer[len - 1] == ',') {
+			answer[--len] = '\0';
+		}
+
+		if (fr_pair_list_afrom_str(ctx, answer, output_pairs) == T_INVALID) {
+			RERROR("Failed parsing output from: %s: %s", cmd, fr_strerror());
+			if (out) strlcpy(out, answer, len);
+			ret = -1;
+		}
+
+		VERIFY_REQUEST(request);
+
+
+	/*
+	 *	We've not been told to extract output pairs,
+	 *	just copy the programs output to the out
+	 *	buffer.
+	 */
+
+	} else if (out) {
+		strlcpy(out, answer, outlen);
+	}
+
+	/*
+	 *	Call rad_waitpid (should map to waitpid on non-threaded
+	 *	or single-server systems).
+	 */
+wait:
+	child_pid = rad_waitpid(pid, &status);
+	if (child_pid == 0) {
+		RERROR("Timeout waiting for child");
+
+		return -2;
+	}
+
+	if (child_pid == pid) {
+		if (WIFEXITED(status)) {
+			status = WEXITSTATUS(status);
+			if ((status != 0) || (ret < 0)) {
+				RERROR("Program returned code (%d) and output '%s'", status, answer);
+			} else {
+				RDEBUG2("Program returned code (%d) and output '%s'", status, answer);
+			}
+
+			return ret < 0 ? ret : status;
+		}
+	}
+
+	RERROR("Abnormal child exit: %s", fr_syserror(errno));
+#endif	/* __MINGW32__ */
+
+	return -1;
+}
diff --git a/src/main/exfile.c b/src/main/exfile.c
new file mode 100644
index 0000000..59e6a05
--- /dev/null
+++ b/src/main/exfile.c
@@ -0,0 +1,547 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/*
+ * $Id$
+ *
+ * @file exfile.c
+ * @brief Allow multiple threads to write to the same set of files.
+ *
+ * @author Alan DeKok <aland@freeradius.org>
+ * @copyright 2014  The FreeRADIUS server project
+ */
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/exfile.h>
+
+#include <sys/stat.h>
+#include <fcntl.h>
+
+typedef struct exfile_entry_t {
+	int		fd;		//!< File descriptor associated with an entry.
+	uint32_t	hash;		//!< Hash for cheap comparison.
+	time_t		last_used;	//!< Last time the entry was used.
+	dev_t		st_dev;		//!< device inode
+	ino_t		st_ino;		//!< inode number
+	char		*filename;	//!< Filename.
+} exfile_entry_t;
+
+
+struct exfile_t {
+	uint32_t	max_entries;	//!< How many file descriptors we keep track of.
+	uint32_t	max_idle;	//!< Maximum idle time for a descriptor.
+	time_t		last_cleaned;
+
+#ifdef HAVE_PTHREAD_H
+	pthread_mutex_t mutex;
+#endif
+	exfile_entry_t *entries;
+	bool		locking;
+};
+
+
+#ifdef HAVE_PTHREAD_H
+#define PTHREAD_MUTEX_LOCK pthread_mutex_lock
+#define PTHREAD_MUTEX_UNLOCK pthread_mutex_unlock
+
+#else
+/*
+ *	This is easier than ifdef's throughout the code.
+ */
+#define PTHREAD_MUTEX_LOCK(_x)
+#define PTHREAD_MUTEX_UNLOCK(_x)
+#endif
+
+#define MAX_TRY_LOCK 4			//!< How many times we attempt to acquire a lock
+					//!< before giving up.
+
+static int _exfile_free(exfile_t *ef)
+{
+	uint32_t i;
+
+	PTHREAD_MUTEX_LOCK(&ef->mutex);
+
+	for (i = 0; i < ef->max_entries; i++) {
+		if (!ef->entries[i].filename) continue;
+
+		close(ef->entries[i].fd);
+	}
+
+	PTHREAD_MUTEX_UNLOCK(&ef->mutex);
+
+#ifdef HAVE_PTHREAD_H
+	pthread_mutex_destroy(&ef->mutex);
+#endif
+
+	return 0;
+}
+
+
+/** Initialize a way for multiple threads to log to one or more files.
+ *
+ * @param ctx The talloc context
+ * @param max_entries Max file descriptors to cache, and manage locks for.
+ * @param max_idle Maximum time a file descriptor can be idle before it's closed.
+ * @param locking whether or not to lock the files.
+ * @return the new context, or NULL on error.
+ */
+exfile_t *exfile_init(TALLOC_CTX *ctx, uint32_t max_entries, uint32_t max_idle, bool locking)
+{
+	exfile_t *ef;
+
+	ef = talloc_zero(ctx, exfile_t);
+	if (!ef) return NULL;
+
+	ef->max_entries = max_entries;
+	ef->max_idle = max_idle;
+	ef->locking = locking;
+
+	/*
+	 *	If we're not locking the files, just return the
+	 *	handle.  Each call to exfile_open() will just open a
+	 *	new file descriptor.
+	 */
+	if (!locking) return ef;
+
+	ef->entries = talloc_zero_array(ef, exfile_entry_t, max_entries);
+	if (!ef->entries) {
+		talloc_free(ef);
+		return NULL;
+	}
+
+#ifdef HAVE_PTHREAD_H
+	if (pthread_mutex_init(&ef->mutex, NULL) != 0) {
+		talloc_free(ef);
+		return NULL;
+	}
+#endif
+
+	talloc_set_destructor(ef, _exfile_free);
+
+	return ef;
+}
+
+
+static void exfile_cleanup_entry(exfile_entry_t *entry)
+{
+	TALLOC_FREE(entry->filename);
+
+	if (entry->fd >= 0) close(entry->fd);
+	entry->hash = 0;
+	entry->fd = -1;
+}
+
+
+/*
+ *	Try to open the file. If it doesn't exist, try to
+ *	create it's parent directories.
+ */
+static int exfile_open_mkdir(exfile_t *ef, char const *filename, mode_t permissions)
+{
+	int fd;
+
+	/*
+	 *	Files in /dev/ are special.  We don't try to create
+	 *	their parent directories, and we don't try to create
+	 *	the files.
+	 */
+	if (strncmp(filename, "/dev/", 5) == 0) {
+		int oflag;
+
+		if (((permissions & 0222) == 0) && (permissions & 0444) != 0) { /* !W + R */
+			oflag = O_RDONLY;
+
+		} else if (((permissions & 0222) != 0) && (permissions & 0444) == 0) { /* W + !R */
+			oflag = O_WRONLY;
+
+		} else {	/* unknown, make it R+W */
+			oflag = O_RDWR;
+		}
+
+		fd = open(filename, oflag, permissions);
+		if (fd < 0) {
+			fr_strerror_printf("Failed to open file %s: %s",
+					   filename, strerror(errno));
+			return -1;
+		}
+
+		return fd;
+	}
+
+	fd = open(filename, O_RDWR | O_CREAT, permissions);
+	if (fd < 0) {
+		mode_t dirperm;
+		char *p, *dir;
+
+		/*
+		 *	Maybe the directory doesn't exist.  Try to
+		 *	create it.
+		 */
+		dir = talloc_strdup(ef, filename);
+		if (!dir) return -1;
+		p = strrchr(dir, FR_DIR_SEP);
+		if (!p) {
+			fr_strerror_printf("No '/' in '%s'", filename);
+			talloc_free(dir);
+			return -1;
+		}
+		*p = '\0';
+
+		/*
+		 *	Ensure that the 'x' bit is set, so that we can
+		 *	read the directory.
+		 */
+		dirperm = permissions;
+		if ((dirperm & 0600) != 0) dirperm |= 0100;
+		if ((dirperm & 0060) != 0) dirperm |= 0010;
+		if ((dirperm & 0006) != 0) dirperm |= 0001;
+
+		if (rad_mkdir(dir, dirperm, -1, -1) < 0) {
+			fr_strerror_printf("Failed to create directory %s: %s",
+					   dir, strerror(errno));
+			talloc_free(dir);
+			return -1;
+		}
+		talloc_free(dir);
+
+		fd = open(filename, O_RDWR | O_CREAT, permissions);
+		if (fd < 0) {
+			fr_strerror_printf("Failed to open file %s: %s",
+					   filename, strerror(errno));
+			return -1;
+		}
+	}
+
+	return fd;
+}
+
+
+/** Open a new log file, or maybe an existing one.
+ *
+ * When multithreaded, the FD is locked via a mutex.  This way we're
+ * sure that no other thread is writing to the file.
+ *
+ * @param ef The logfile context returned from exfile_init().
+ * @param filename the file to open.
+ * @param permissions to use.
+ * @return an FD used to write to the file, or -1 on error.
+ */
+int exfile_open(exfile_t *ef, char const *filename, mode_t permissions, off_t *offset)
+{
+	int i, found, tries, unused, oldest;
+	uint32_t hash;
+	time_t now;
+	struct stat st;
+	off_t real_offset;
+
+	if (!ef || !filename) return -1;
+
+	/*
+	 *	No locking: just return a new FD.
+	 */
+	if (!ef->locking) {
+		found = exfile_open_mkdir(ef, filename, permissions);
+		if (found < 0) return -1;
+
+		real_offset = lseek(found, 0, SEEK_END);
+		if (offset) *offset = real_offset;
+		return found;
+	}
+
+	/*
+	 *	It's faster to do hash comparisons of a string than
+	 *	full string comparisons.
+	 */
+	hash = fr_hash_string(filename);
+	now = time(NULL);
+
+	PTHREAD_MUTEX_LOCK(&ef->mutex);
+
+	/*
+	 *	Clean up idle entries.
+	 */
+	if (now > (ef->last_cleaned + 1)) {
+		ef->last_cleaned = now;
+
+		for (i = 0; i < (int) ef->max_entries; i++) {
+			if (!ef->entries[i].filename) continue;
+
+			if ((ef->entries[i].last_used + ef->max_idle) >= now) continue;
+
+			/*
+			 *	This will block forever if a thread is
+			 *	doing something stupid.
+			 */
+			exfile_cleanup_entry(&ef->entries[i]);
+		}
+	}
+
+	/*
+	 *	Find the matching entry, or an unused one.
+	 *
+	 *	Also track which entry is the oldest, in case there
+	 *	are no unused entries.
+	 */
+	found = oldest = unused = -1;
+	for (i = 0; i < (int) ef->max_entries; i++) {
+		if (!ef->entries[i].filename) {
+			if (unused < 0) unused = i;
+			continue;
+		}
+
+		if ((oldest < 0) ||
+		    (ef->entries[i].last_used < ef->entries[oldest].last_used)) {
+			oldest = i;
+		}
+
+		/*
+		 *	Hash comparisons are fast.  String comparisons are slow.
+		 */
+		if (ef->entries[i].hash != hash) continue;
+
+		/*
+		 *	But we still need to do string comparisons if
+		 *	the hash matches, because 1/2^16 filenames
+		 *	will result in a hash collision.  And that's
+		 *	enough filenames in a long-running server to
+		 *	ensure that it happens.
+		 */
+		if (strcmp(ef->entries[i].filename, filename) != 0) continue;
+
+		found = i;
+		break;
+	}
+
+	/*
+	 *	If it wasn't found, create a new entry.
+	 */
+	if (found < 0) {
+		/*
+		 *	There are no unused entries.  Clean up the
+		 *	oldest one.
+		 */
+		if (unused < 0) {
+			exfile_cleanup_entry(&ef->entries[oldest]);
+			unused = oldest;
+		}
+
+		/*
+		 *	Create a new entry.
+		 */
+		i = unused;
+
+		ef->entries[i].hash = hash;
+		ef->entries[i].filename = talloc_strdup(ef->entries, filename);
+		ef->entries[i].fd = -1;
+
+		/*
+		 *	We've just created the entry.  Open the file
+		 *	and cache the FD.
+		 */
+	reopen:
+		ef->entries[i].fd = exfile_open_mkdir(ef, filename, permissions);
+		if (ef->entries[i].fd < 0) {
+		error:
+			exfile_cleanup_entry(&ef->entries[i]);
+			PTHREAD_MUTEX_UNLOCK(&(ef->mutex));
+			return -1;
+		}
+
+		if (fstat(ef->entries[i].fd, &st) < 0) goto error;
+
+		/*
+		 *	Remember which device and inode this file is
+		 *	for.
+		 */
+		ef->entries[i].st_dev = st.st_dev;
+		ef->entries[i].st_ino = st.st_ino;
+
+	} else {
+		i = found;
+
+		/*
+		 *	Stat the *filename*, not the file we opened.
+		 *	If that's not the file we opened, then go back
+		 *	and re-open the file.
+		 */
+		if (stat(ef->entries[i].filename, &st) == 0) {
+			if ((st.st_dev != ef->entries[i].st_dev) ||
+			    (st.st_ino != ef->entries[i].st_ino)) {
+				/*
+				 *	No longer the same file; reopen.
+				 */
+				close(ef->entries[i].fd);
+				goto reopen;
+			}
+		} else {
+			/*
+			 *	Error calling stat, likely the
+			 *	file has been moved. Reopen it.
+			 */
+			close(ef->entries[i].fd);
+			goto reopen;
+		}
+	}
+
+	/*
+	 *	Try to lock it.  If we can't lock it, it's because
+	 *	some reader has re-named the file to "foo.work" and
+	 *	locked it.  So, we close the current file, re-open it,
+	 *	and try again.
+	 */
+
+	/*
+	 *	Lock from the start of the file.  It's the
+	 *	only point in the file which is guaranteed to
+	 *	exist, and to be consistent across all threads
+	 *	and processes.
+	 */
+	if (lseek(ef->entries[i].fd, 0, SEEK_SET) < 0) {
+		fr_strerror_printf("Failed to seek in file %s: %s", filename, strerror(errno));
+		goto error;
+	}
+
+	/*
+	 *	Busy-loop trying to lock the file.
+	 */
+	for (tries = 0; tries < MAX_TRY_LOCK; tries++) {
+		if (rad_lockfd_nonblock(ef->entries[i].fd, 0) >= 0) break;
+
+		if (errno != EAGAIN) {
+			fr_strerror_printf("Failed to lock file %s: %s", filename, strerror(errno));
+			goto error;
+		}
+
+		/*
+		 *	Close the file and re-open it.  It may
+		 *	have been deleted.  If it was deleted,
+		 *	then the new file should now be unlocked.
+		 */
+		close(ef->entries[i].fd);
+		ef->entries[i].fd = open(filename, O_RDWR | O_CREAT, permissions);
+		if (ef->entries[i].fd < 0) {
+			fr_strerror_printf("Failed to open file %s: %s",
+					   filename, strerror(errno));
+			goto error;
+		}
+	}
+
+	if (tries >= MAX_TRY_LOCK) {
+		fr_strerror_printf("Failed to lock file %s: too many tries", filename);
+		goto error;
+	}
+
+	/*
+	 *	See which file it really is.
+	 */
+	if (fstat(ef->entries[i].fd, &st) < 0) {
+		fr_strerror_printf("Failed to stat file %s: %s", filename, strerror(errno));
+		goto error;
+	}
+
+	/*
+	 *	Maybe the file was unlinked from the file system, OR
+	 *	the file we opened is NOT the one we had cached.  If
+	 *	so, close the file and re-open it from scratch.
+	 */
+	if ((st.st_nlink == 0) ||
+	    (st.st_dev != ef->entries[i].st_dev) ||
+	    (st.st_ino != ef->entries[i].st_ino)) {
+		close(ef->entries[i].fd);
+		goto reopen;
+	}
+
+	/*
+	 *	Sometimes the file permissions are changed externally.
+	 *	just be sure to update the permission if necessary.
+	 */
+	if ((st.st_mode & ~S_IFMT) != permissions) {
+		char str_need[10], oct_need[5];
+		char str_have[10], oct_have[5];
+
+		rad_mode_to_oct(oct_need, permissions);
+		rad_mode_to_str(str_need, permissions);
+
+		rad_mode_to_oct(oct_have, st.st_mode & ~S_IFMT);
+		rad_mode_to_str(str_have, st.st_mode & ~S_IFMT);
+
+		WARN("File %s permissions are %s (%s) not %s (%s))", filename,
+		     oct_have, str_have, oct_need, str_need);
+
+		if (((st.st_mode | permissions) != st.st_mode) &&
+		    (fchmod(ef->entries[i].fd, (st.st_mode & ~S_IFMT) | permissions) < 0)) {
+			rad_mode_to_oct(oct_need, (st.st_mode & ~S_IFMT) | permissions);
+			rad_mode_to_str(str_need, (st.st_mode & ~S_IFMT) | permissions);
+			
+			WARN("Failed resetting file %s permissions to %s (%s): %s",
+			     filename, oct_need, str_need, fr_syserror(errno));
+		}
+	}
+
+	/*
+	 *	If we're appending, seek to the end of the file before
+	 *	returning the FD to the caller.
+	 */
+	real_offset = lseek(ef->entries[i].fd, 0, SEEK_END);
+	if (offset) *offset = real_offset;
+
+	/*
+	 *	Return holding the mutex for the entry.
+	 */
+	ef->entries[i].last_used = now;
+
+	return ef->entries[i].fd;
+}
+
+/** Close the log file.  Really just return it to the pool.
+ *
+ * When multithreaded, the FD is locked via a mutex.  This way we're
+ * sure that no other thread is writing to the file.  This function
+ * will unlock the mutex, so that other threads can write to the file.
+ *
+ * @param ef The logfile context returned from exfile_init()
+ * @param fd the FD to close (i.e. return to the pool)
+ * @return 0 on success, or -1 on error
+ */
+int exfile_close(exfile_t *ef, int fd)
+{
+	uint32_t i;
+
+	/*
+	 *	No locking: just close the file.
+	 */
+	if (!ef->locking) {
+		close(fd);
+		return 0;
+	}
+
+	/*
+	 *	Unlock the bytes that we had previously locked.
+	 */
+	for (i = 0; i < ef->max_entries; i++) {
+		if (ef->entries[i].fd == fd) {
+			(void) lseek(ef->entries[i].fd, 0, SEEK_SET);
+			(void) rad_unlockfd(ef->entries[i].fd, 0);
+
+			PTHREAD_MUTEX_UNLOCK(&(ef->mutex));
+			return 0;
+		}
+	}
+
+	PTHREAD_MUTEX_UNLOCK(&(ef->mutex));
+
+	fr_strerror_printf("Attempt to unlock file which is not tracked");
+	return -1;
+}
diff --git a/src/main/files.c b/src/main/files.c
new file mode 100644
index 0000000..25b6f0d
--- /dev/null
+++ b/src/main/files.c
@@ -0,0 +1,361 @@
+/*
+ * files.c	Read config files into memory.
+ *
+ * Version:     $Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2000,2006  The FreeRADIUS server project
+ * Copyright 2000  Miquel van Smoorenburg <miquels@cistron.nl>
+ * Copyright 2000  Alan DeKok <aland@ox.org>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <fcntl.h>
+
+/*
+ *	Debug code.
+ */
+#if 0
+static void debug_pair_list(PAIR_LIST *pl)
+{
+	VALUE_PAIR *vp;
+
+	while(pl) {
+		printf("Pair list: %s\n", pl->name);
+		printf("** Check:\n");
+		for(vp = pl->check; vp; vp = vp->next) {
+			printf("    ");
+			fprint_attr_val(stdout, vp);
+			printf("\n");
+		}
+		printf("** Reply:\n");
+		for(vp = pl->reply; vp; vp = vp->next) {
+			printf("    ");
+			fprint_attr_val(stdout, vp);
+			printf("\n");
+		}
+		pl = pl->next;
+	}
+}
+#endif
+
+/*
+ *	Free a PAIR_LIST
+ */
+void pairlist_free(PAIR_LIST **pl)
+{
+	talloc_free(*pl);
+	*pl = NULL;
+}
+
+
+#define FIND_MODE_NAME  0
+#define FIND_MODE_WANT_REPLY 1
+#define FIND_MODE_HAVE_REPLY 2
+
+/*
+ *	Read the users, huntgroups or hints file.
+ *	Return a PAIR_LIST.
+ */
+int pairlist_read(TALLOC_CTX *ctx, char const *file, PAIR_LIST **list, int complain)
+{
+	FILE *fp;
+	int mode = FIND_MODE_NAME;
+	char entry[256];
+	char buffer[8192];
+	char const *ptr;
+	VALUE_PAIR *check_tmp = NULL;
+	VALUE_PAIR *reply_tmp = NULL;
+	PAIR_LIST *pl = NULL, *t;
+	PAIR_LIST **last = &pl;
+	int order = 0;
+	int lineno = 0;
+	int entry_lineno = 0;
+	FR_TOKEN parsecode;
+#ifdef HAVE_REGEX_H
+	VALUE_PAIR *vp;
+	vp_cursor_t cursor;
+#endif
+	char newfile[8192];
+
+	DEBUG2("reading pairlist file %s", file);
+
+	/*
+	 *	Open the file.  The error message should be a little
+	 *	more useful...
+	 */
+	if ((fp = fopen(file, "r")) == NULL) {
+		if (!complain)
+			return -1;
+		ERROR("Couldn't open %s for reading: %s",
+				file, fr_syserror(errno));
+		return -1;
+	}
+
+	/*
+	 *	Read the entire file into memory for speed.
+	 */
+	while (fgets(buffer, sizeof(buffer), fp) != NULL) {
+		lineno++;
+
+		if (!feof(fp) && (strchr(buffer, '\n') == NULL)) {
+			fclose(fp);
+			ERROR("%s[%d]: line too long", file, lineno);
+			pairlist_free(&pl);
+			return -1;
+		}
+
+		/*
+		 *	If the line contains nothing but whitespace,
+		 *	ignore it.
+		 */
+		ptr = buffer;
+		while (isspace((uint8_t) *ptr)) ptr++;
+
+		if (*ptr == '#' || *ptr == '\n' || !*ptr) continue;
+
+parse_again:
+		if (mode == FIND_MODE_NAME) {
+			/*
+			 *	The user's name MUST be the first text on the line.
+			 */
+			if (isspace((uint8_t) buffer[0]))  {
+				ERROR("%s[%d]: Entry does not begin with a user name",
+				      file, lineno);
+				fclose(fp);
+				return -1;
+			}
+
+			/*
+			 *	Get the name.
+			 */		      
+			ptr = buffer;
+			getword(&ptr, entry, sizeof(entry), false);
+			entry_lineno = lineno;
+
+			/*
+			 *	Include another file if we see
+			 *	$INCLUDE filename
+			 */
+			if (strcasecmp(entry, "$INCLUDE") == 0) {
+				while (isspace((uint8_t) *ptr)) ptr++;
+
+				/*
+				 *	If it's an absolute pathname,
+				 *	then use it verbatim.
+				 *
+				 *	If not, then make the $include
+				 *	files *relative* to the current
+				 *	file.
+				 */
+				if (FR_DIR_IS_RELATIVE(ptr)) {
+					char *p;
+
+					strlcpy(newfile, file,
+						sizeof(newfile));
+					p = strrchr(newfile, FR_DIR_SEP);
+					if (!p) {
+						p = newfile + strlen(newfile);
+						*p = FR_DIR_SEP;
+					}
+					getword(&ptr, p + 1, sizeof(newfile) - 1 - (p - newfile), false);
+				} else {
+					getword(&ptr, newfile, sizeof(newfile), false);
+				}
+
+				t = NULL;
+
+				if (pairlist_read(ctx, newfile, &t, 0) != 0) {
+					pairlist_free(&pl);
+					ERROR("%s[%d]: Could not open included file %s: %s",
+					       file, lineno, newfile, fr_syserror(errno));
+					fclose(fp);
+					return -1;
+				}
+				*last = t;
+
+				/*
+				 *	t may be NULL, it may have one
+				 *	entry, or it may be a linked list
+				 *	of entries.  Go to the end of the
+				 *	list.
+				 */
+				while (*last) {
+					(*last)->order = order++;
+					last = &((*last)->next);
+				}
+				continue;
+			} /* $INCLUDE ... */
+
+			/*
+			 *	Parse the check values
+			 */
+			rad_assert(check_tmp == NULL);
+			rad_assert(reply_tmp == NULL);
+			parsecode = fr_pair_list_afrom_str(ctx, ptr, &check_tmp);
+			if (parsecode == T_INVALID) {
+				pairlist_free(&pl);
+				ERROR("%s[%d]: Parse error (check) for entry %s: %s",
+					file, lineno, entry, fr_strerror());
+				fclose(fp);
+				return -1;
+			}
+
+			if (parsecode != T_EOL) {
+				pairlist_free(&pl);
+				talloc_free(check_tmp);
+				ERROR("%s[%d]: Invalid text after check attributes for entry %s",
+				      file, lineno, entry);
+				fclose(fp);
+				return -1;
+			}
+
+#ifdef HAVE_REGEX_H
+			/*
+			 *	Do some more sanity checks.
+			 */
+			for (vp = fr_cursor_init(&cursor, &check_tmp);
+			     vp;
+			     vp = fr_cursor_next(&cursor)) {
+				if (((vp->op == T_OP_REG_EQ) ||
+				     (vp->op == T_OP_REG_NE)) &&
+				    (vp->da->type != PW_TYPE_STRING)) {
+					pairlist_free(&pl);
+					talloc_free(check_tmp);
+					ERROR("%s[%d]: Cannot use regular expressions for non-string attributes in entry %s",
+					      file, lineno, entry);
+					fclose(fp);
+					return -1;
+				}
+			}
+#endif
+
+			/*
+			 *	The reply MUST be on a new line.
+			 */
+			mode = FIND_MODE_WANT_REPLY;
+			continue;
+		}
+
+		/*
+		 *	We COULD have a reply, OR we could have a new entry.
+		 */
+		if (mode == FIND_MODE_WANT_REPLY) {
+			if (!isspace((uint8_t) buffer[0])) goto create_entry;
+
+			mode = FIND_MODE_HAVE_REPLY;
+		}
+
+		/*
+		 *	mode == FIND_MODE_HAVE_REPLY
+		 */
+
+		/*
+		 *	The previous line ended with a comma, and then
+		 *	we have the start of a new entry!
+		 */
+		if (!isspace((uint8_t) buffer[0])) {
+		trailing_comma:
+			pairlist_free(&pl);
+			talloc_free(check_tmp);
+			talloc_free(reply_tmp);
+			ERROR("%s[%d]: Invalid comma after the reply attributes.  Please delete it.",
+			      file, lineno);
+			fclose(fp);
+			return -1;
+		}
+
+		/*
+		 *	Parse the reply values.  If there's a trailing
+		 *	comma, keep parsing the reply values.
+		 */
+		parsecode = fr_pair_list_afrom_str(ctx, buffer, &reply_tmp);
+		if (parsecode == T_COMMA) {
+			continue;
+		}
+
+		/*
+		 *	We expect an EOL.  Anything else is an error.
+		 */
+		if (parsecode != T_EOL) {
+			pairlist_free(&pl);
+			talloc_free(check_tmp);
+			talloc_free(reply_tmp);
+			ERROR("%s[%d]: Parse error (reply) for entry %s: %s",
+			      file, lineno, entry, fr_strerror());
+			fclose(fp);
+			return -1;
+		}
+
+	create_entry:
+		/*
+		 *	Done with this entry...
+		 */
+		MEM(t = talloc_zero(ctx, PAIR_LIST));
+
+		if (check_tmp) fr_pair_steal(t, check_tmp);
+		if (reply_tmp) fr_pair_steal(t, reply_tmp);
+
+		t->check = check_tmp;
+		t->reply = reply_tmp;
+		t->lineno = entry_lineno;
+		t->order = order++;
+		check_tmp = NULL;
+		reply_tmp = NULL;
+
+		t->name = talloc_typed_strdup(t, entry);
+
+		*last = t;
+		last = &(t->next);
+
+		/*
+		 *	Look for a name.  If we came here because
+		 *	there were no reply attributes, then re-parse
+		 *	the current line, instead of reading another one.
+		 */
+		mode = FIND_MODE_NAME;
+		if (feof(fp)) break;
+		if (!isspace((uint8_t) buffer[0])) goto parse_again;
+	}
+
+	/*
+	 *	We're at EOF.  If we're supposed to read more, that's
+	 *	an error.
+	 */
+	if (mode == FIND_MODE_HAVE_REPLY) goto trailing_comma;
+
+	/*
+	 *	We had an entry, but no reply attributes.  That's OK.
+	 */
+	if (mode == FIND_MODE_WANT_REPLY) goto create_entry;
+
+	/*
+	 *	Else we were looking for an entry.  We didn't get one
+	 *	because we were at EOF, so that's OK.
+	 */
+
+	fclose(fp);
+
+	*list = pl;
+	return 0;
+}
diff --git a/src/main/libfreeradius-server.mk b/src/main/libfreeradius-server.mk
new file mode 100644
index 0000000..4495f72
--- /dev/null
+++ b/src/main/libfreeradius-server.mk
@@ -0,0 +1,22 @@
+TARGET	:= libfreeradius-server.a
+
+SOURCES	:=	conffile.c \
+		evaluate.c \
+		exec.c \
+		exfile.c \
+		log.c \
+		parser.c \
+		map.c \
+		regex.c \
+		tmpl.c \
+		util.c \
+		version.c \
+		pair.c \
+		xlat.c
+
+# This lets the linker determine which version of the SSLeay functions to use.
+TGT_LDLIBS      := $(OPENSSL_LIBS)
+
+ifneq ($(MAKECMDGOALS),scan)
+SRC_CFLAGS	+= -DBUILT_WITH_CPPFLAGS=\"$(CPPFLAGS)\" -DBUILT_WITH_CFLAGS=\"$(CFLAGS)\" -DBUILT_WITH_LDFLAGS=\"$(LDFLAGS)\" -DBUILT_WITH_LIBS=\"$(LIBS)\"
+endif
diff --git a/src/main/listen.c b/src/main/listen.c
new file mode 100644
index 0000000..ee73a57
--- /dev/null
+++ b/src/main/listen.c
@@ -0,0 +1,4486 @@
+/*
+ * listen.c	Handle socket stuff
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2005,2006  The FreeRADIUS server project
+ * Copyright 2005  Alan DeKok <aland@ox.org>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/rad_assert.h>
+#include <freeradius-devel/process.h>
+#include <freeradius-devel/protocol.h>
+#include <freeradius-devel/modpriv.h>
+
+#include <freeradius-devel/detail.h>
+
+#ifdef WITH_UDPFROMTO
+#include <freeradius-devel/udpfromto.h>
+#endif
+
+#ifdef HAVE_SYS_RESOURCE_H
+#include <sys/resource.h>
+#endif
+
+#ifdef HAVE_NET_IF_H
+#include <net/if.h>
+#endif
+
+#ifdef HAVE_FCNTL_H
+#include <fcntl.h>
+#endif
+
+#ifdef HAVE_SYS_STAT_H
+#include <sys/stat.h>
+#endif
+
+#ifdef WITH_TLS
+#include <netinet/tcp.h>
+
+#  ifdef __APPLE__
+#    if !defined(SOL_TCP) && defined(IPPROTO_TCP)
+#      define SOL_TCP IPPROTO_TCP
+#    endif
+#  endif
+
+#endif
+
+#ifdef DEBUG_PRINT_PACKET
+static void print_packet(RADIUS_PACKET *packet)
+{
+	char src[256], dst[256];
+
+	ip_ntoh(&packet->src_ipaddr, src, sizeof(src));
+	ip_ntoh(&packet->dst_ipaddr, dst, sizeof(dst));
+
+	fprintf(stderr, "ID %d: %s %d -> %s %d\n", packet->id,
+		src, packet->src_port, dst, packet->dst_port);
+
+}
+#endif
+
+
+static rad_listen_t *listen_alloc(TALLOC_CTX *ctx, RAD_LISTEN_TYPE type);
+
+#ifdef WITH_COMMAND_SOCKET
+#ifdef WITH_TCP
+static int command_tcp_recv(rad_listen_t *listener);
+static int command_tcp_send(rad_listen_t *listener, REQUEST *request);
+static int command_write_magic(int newfd, listen_socket_t *sock);
+#endif
+#endif
+
+#ifdef WITH_COA_TUNNEL
+static int listen_coa_init(void);
+#endif
+
+static fr_protocol_t master_listen[];
+
+#ifdef WITH_DYNAMIC_CLIENTS
+static void client_timer_free(void *ctx)
+{
+	RADCLIENT *client = ctx;
+
+	client_free(client);
+}
+#endif
+
+/*
+ *	Find a per-socket client.
+ */
+RADCLIENT *client_listener_find(rad_listen_t *listener,
+				fr_ipaddr_t const *ipaddr, uint16_t src_port)
+{
+#ifdef WITH_DYNAMIC_CLIENTS
+	int rcode;
+	REQUEST *request;
+	RADCLIENT *created;
+#endif
+	time_t now;
+	RADCLIENT *client;
+	RADCLIENT_LIST *clients;
+	listen_socket_t *sock;
+
+	rad_assert(listener != NULL);
+	rad_assert(ipaddr != NULL);
+
+	sock = listener->data;
+	clients = sock->clients;
+
+	/*
+	 *	This HAS to have been initialized previously.
+	 */
+	rad_assert(clients != NULL);
+
+	client = client_find(clients, ipaddr, sock->proto);
+	if (!client) {
+		char name[256], buffer[128];
+
+#ifdef WITH_DYNAMIC_CLIENTS
+	unknown:		/* used only for dynamic clients */
+#endif
+
+		/*
+		 *	DoS attack quenching, but only in daemon mode.
+		 *	If they're running in debug mode, show them
+		 *	every packet.
+		 */
+		if (rad_debug_lvl == 0) {
+			static time_t last_printed = 0;
+
+			now = time(NULL);
+			if (last_printed == now) return NULL;
+
+			last_printed = now;
+		}
+
+		listener->print(listener, name, sizeof(name));
+
+		radlog(L_ERR, "Ignoring request to %s from unknown client %s port %d"
+#ifdef WITH_TCP
+		       " proto %s"
+#endif
+		       , name, inet_ntop(ipaddr->af, &ipaddr->ipaddr,
+					 buffer, sizeof(buffer)), src_port
+#ifdef WITH_TCP
+		       , (sock->proto == IPPROTO_UDP) ? "udp" : "tcp"
+#endif
+		       );
+		return NULL;
+	}
+
+#ifndef WITH_DYNAMIC_CLIENTS
+	return client;		/* return the found client. */
+#else
+
+	/*
+	 *	No server defined, and it's not dynamic.  Return it.
+	 */
+	if (!client->client_server && !client->dynamic) return client;
+
+	now = time(NULL);
+
+	/*
+	 *	It's a dynamically generated client, check it.
+	 */
+	if (client->dynamic && (src_port != 0)) {
+#ifdef HAVE_SYS_STAT_H
+		char const *filename;
+#endif
+		fr_event_list_t *el;
+		struct timeval when;
+
+		/*
+		 *	Lives forever.  Return it.
+		 */
+		if (client->lifetime == 0) return client;
+
+		/*
+		 *	Rate-limit the deletion of known clients.
+		 *	This makes them last a little longer, but
+		 *	prevents the server from melting down if (say)
+		 *	10k clients all expire at once.
+		 */
+		if (now == client->last_new_client) return client;
+
+		/*
+		 *	It's not dead yet.  Return it.
+		 */
+		if ((client->created + client->lifetime) > now) return client;
+
+#ifdef HAVE_SYS_STAT_H
+		/*
+		 *	The client was read from a file, and the file
+		 *	hasn't changed since the client was created.
+		 *	Just renew the creation time, and continue.
+		 *	We don't need to re-load the same information.
+		 */
+		if (client->cs &&
+		    (filename = cf_section_filename(client->cs)) != NULL) {
+			struct stat buf;
+
+			if ((stat(filename, &buf) >= 0) &&
+			    (buf.st_mtime < client->created)) {
+				client->created = now;
+				return client;
+			}
+		}
+#endif
+
+
+		/*
+		 *	Delete the client from the known list.
+		 */
+		client_delete(clients, client);
+
+		/*
+		 *	Add a timer to free the client 20s after it's already timed out.
+		 */
+		el = radius_event_list_corral(EVENT_CORRAL_MAIN);
+
+		gettimeofday(&when, NULL);
+		when.tv_sec += main_config.max_request_time + 20;
+
+		/*
+		 *	If this fails, we leak memory.  That's better than crashing...
+		 */
+		(void) fr_event_insert(el, client_timer_free, client, &when, &client->ev);
+
+		/*
+		 *	Go find the enclosing network again.
+		 */
+		client = client_find(clients, ipaddr, sock->proto);
+
+		/*
+		 *	WTF?
+		 */
+		if (!client) goto unknown;
+		if (!client->client_server) goto unknown;
+
+		/*
+		 *	At this point, 'client' is the enclosing
+		 *	network that configures where dynamic clients
+		 *	can be defined.
+		 */
+		rad_assert(client->dynamic == 0);
+
+	} else if (!client->dynamic && client->rate_limit) {
+		/*
+		 *	The IP is unknown, so we've found an enclosing
+		 *	network.  Enable DoS protection.  We only
+		 *	allow one new client per second.  Known
+		 *	clients aren't subject to this restriction.
+		 */
+		if (now == client->last_new_client) goto unknown;
+	}
+
+	client->last_new_client = now;
+
+	request = request_alloc(NULL);
+	if (!request) goto unknown;
+
+	request->listener = listener;
+	request->client = client;
+	request->packet = rad_recv(NULL, listener->fd, 0x02); /* MSG_PEEK */
+	if (!request->packet) {				/* badly formed, etc */
+		talloc_free(request);
+		if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror());
+		goto unknown;
+	}
+	(void) talloc_steal(request, request->packet);
+	request->reply = rad_alloc_reply(request, request->packet);
+	if (!request->reply) {
+		talloc_free(request);
+		goto unknown;
+	}
+	gettimeofday(&request->packet->timestamp, NULL);
+	request->number = 0;
+	request->priority = listener->type;
+	request->server = client->client_server;
+	request->root = &main_config;
+
+	/*
+	 *	Run a fake request through the given virtual server.
+	 *	Look for FreeRADIUS-Client-IP-Address
+	 *		 FreeRADIUS-Client-Secret
+	 *		...
+	 *
+	 *	and create the RADCLIENT structure from that.
+	 */
+	RDEBUG("server %s {", request->server);
+
+	rcode = process_authorize(0, request);
+
+	RDEBUG("} # server %s", request->server);
+
+	switch (rcode) {
+	case RLM_MODULE_OK:
+	case RLM_MODULE_UPDATED:
+		break;
+
+	/*
+	 *	Likely a fatal error we want to warn the user about
+	 */
+	case RLM_MODULE_INVALID:
+	case RLM_MODULE_FAIL:
+		ERROR("Virtual-Server %s returned %s, creating dynamic client failed", request->server,
+		      fr_int2str(mod_rcode_table, rcode, "<INVALID>"));
+		talloc_free(request);
+		goto unknown;
+
+	/*
+	 *	Probably the result of policy, or the client not existing.
+	 */
+	default:
+		DEBUG("Virtual-Server %s returned %s, ignoring client", request->server,
+		      fr_int2str(mod_rcode_table, rcode, "<INVALID>"));
+		talloc_free(request);
+		goto unknown;
+	}
+
+	/*
+	 *	If the client was updated by rlm_dynamic_clients,
+	 *	don't create the client from attribute-value pairs.
+	 */
+	if (request->client == client) {
+		created = client_afrom_request(clients, request);
+	} else {
+		created = request->client;
+
+		/*
+		 *	This frees the client if it isn't valid.
+		 */
+		if (!client_add_dynamic(clients, client, created)) goto unknown;
+	}
+
+	request->server = client->server;
+	exec_trigger(request, NULL, "server.client.add", false);
+
+	talloc_free(request);
+
+	if (!created) goto unknown;
+
+	return created;
+#endif
+}
+
+static int listen_bind(rad_listen_t *this);
+
+#ifdef WITH_COA_TUNNEL
+static void listener_coa_update(rad_listen_t *this, VALUE_PAIR *vps);
+#endif
+
+/*
+ *	Process and reply to a server-status request.
+ *	Like rad_authenticate and rad_accounting this should
+ *	live in it's own file but it's so small we don't bother.
+ */
+int rad_status_server(REQUEST *request)
+{
+	int rcode = RLM_MODULE_OK;
+	DICT_VALUE *dval;
+
+#ifdef WITH_TLS
+	if (request->listener->tls) {
+		listen_socket_t *sock = request->listener->data;
+
+		if (sock->state == LISTEN_TLS_CHECKING) {
+			int autz_type = PW_AUTZ_TYPE;
+			char const *name = "Autz-Type";
+
+			if (request->listener->type == RAD_LISTEN_ACCT) {
+				autz_type = PW_ACCT_TYPE;
+				name = "Acct-Type";
+			}
+
+			RDEBUG("(TLS) Checking connection to see if it is authorized.");
+
+			dval = dict_valbyname(autz_type, 0, "New-TLS-Connection");
+			if (dval) {
+				rcode = process_authorize(dval->value, request);
+			} else {
+				rcode = RLM_MODULE_OK;
+				RWDEBUG("(TLS) Did not find '%s New-TLS-Connection' - defaulting to accept", name);
+			}
+
+			if ((rcode == RLM_MODULE_OK) || (rcode == RLM_MODULE_UPDATED)) {
+				RDEBUG("(TLS) Connection is authorized");
+				request->reply->code = PW_CODE_ACCESS_ACCEPT;
+			} else {
+				RWDEBUG("(TLS) Connection is not authorized - closing TCP socket.");
+				request->reply->code = PW_CODE_ACCESS_REJECT;
+			}
+
+			return 0;
+		}
+	}
+#endif
+
+#ifdef WITH_STATS
+	/*
+	 *	Full statistics are available only on a statistics
+	 *	socket.
+	 */
+	if (request->listener->type == RAD_LISTEN_NONE) {
+		request_stats_reply(request);
+	}
+#endif
+
+	switch (request->listener->type) {
+#ifdef WITH_STATS
+	case RAD_LISTEN_NONE:
+#endif
+	case RAD_LISTEN_AUTH:
+		dval = dict_valbyname(PW_AUTZ_TYPE, 0, "Status-Server");
+		if (dval) {
+			rcode = process_authorize(dval->value, request);
+		} else {
+			rcode = RLM_MODULE_OK;
+		}
+
+		switch (rcode) {
+		case RLM_MODULE_OK:
+		case RLM_MODULE_UPDATED:
+			request->reply->code = PW_CODE_ACCESS_ACCEPT;
+
+#ifdef WITH_COA_TUNNEL
+			if (request->listener->send_coa) listener_coa_update(request->listener, request->packet->vps);
+#endif
+			break;
+
+		case RLM_MODULE_FAIL:
+		case RLM_MODULE_HANDLED:
+			request->reply->code = 0; /* don't reply */
+			break;
+
+		default:
+		case RLM_MODULE_REJECT:
+			request->reply->code = PW_CODE_ACCESS_REJECT;
+			break;
+		}
+		break;
+
+#ifdef WITH_ACCOUNTING
+	case RAD_LISTEN_ACCT:
+		dval = dict_valbyname(PW_ACCT_TYPE, 0, "Status-Server");
+		if (dval) {
+			rcode = process_accounting(dval->value, request);
+		} else {
+			rcode = RLM_MODULE_OK;
+		}
+
+		switch (rcode) {
+		case RLM_MODULE_OK:
+		case RLM_MODULE_UPDATED:
+			request->reply->code = PW_CODE_ACCOUNTING_RESPONSE;
+
+#ifdef WITH_COA_TUNNEL
+			if (request->listener->send_coa) listener_coa_update(request->listener, request->packet->vps);
+#endif
+			break;
+
+		default:
+			request->reply->code = 0; /* don't reply */
+			break;
+		}
+		break;
+#endif
+
+#ifdef WITH_COA
+		/*
+		 *	This is a vendor extension.  Suggested by Glen
+		 *	Zorn in IETF 72, and rejected by the rest of
+		 *	the WG.  We like it, so it goes in here.
+		 */
+	case RAD_LISTEN_COA:
+		dval = dict_valbyname(PW_RECV_COA_TYPE, 0, "Status-Server");
+		if (dval) {
+			rcode = process_recv_coa(dval->value, request);
+		} else {
+			rcode = RLM_MODULE_OK;
+		}
+
+		switch (rcode) {
+		case RLM_MODULE_OK:
+		case RLM_MODULE_UPDATED:
+			request->reply->code = PW_CODE_COA_ACK;
+			break;
+
+		default:
+			request->reply->code = 0; /* don't reply */
+			break;
+		}
+		break;
+#endif
+
+	default:
+		return 0;
+	}
+
+	return 0;
+}
+
+#ifdef WITH_TCP
+static int dual_tcp_recv(rad_listen_t *listener)
+{
+	int rcode;
+	RADIUS_PACKET	*packet;
+	RAD_REQUEST_FUNP fun = NULL;
+	listen_socket_t *sock = listener->data;
+	RADCLIENT	*client = sock->client;
+
+	rad_assert(client != NULL);
+
+	if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0;
+
+	/*
+	 *	Allocate a packet for partial reads.
+	 */
+	if (!sock->packet) {
+		sock->packet = rad_alloc(sock, false);
+		if (!sock->packet) return 0;
+
+		sock->packet->sockfd = listener->fd;
+		sock->packet->src_ipaddr = sock->other_ipaddr;
+		sock->packet->src_port = sock->other_port;
+		sock->packet->dst_ipaddr = sock->my_ipaddr;
+		sock->packet->dst_port = sock->my_port;
+		sock->packet->proto = sock->proto;
+	}
+
+	/*
+	 *	Grab the packet currently being processed.
+	 */
+	packet = sock->packet;
+
+	rcode = fr_tcp_read_packet(packet, 0);
+
+	/*
+	 *	Still only a partial packet.  Put it back, and return,
+	 *	so that we'll read more data when it's ready.
+	 */
+	if (rcode == 0) {
+		return 0;
+	}
+
+	if (rcode == -1) {	/* error reading packet */
+		char buffer[256];
+
+		ERROR("Invalid packet from %s port %d, closing socket: %s",
+		       ip_ntoh(&packet->src_ipaddr, buffer, sizeof(buffer)),
+		       packet->src_port, fr_strerror());
+	}
+
+	if (rcode < 0) {	/* error or connection reset */
+		listener->status = RAD_LISTEN_STATUS_EOL;
+
+		/*
+		 *	Tell the event handler that an FD has disappeared.
+		 */
+		DEBUG("Client has closed connection");
+		radius_update_listener(listener);
+
+		/*
+		 *	Do NOT free the listener here.  It's in use by
+		 *	a request, and will need to hang around until
+		 *	all of the requests are done.
+		 *
+		 *	It is instead free'd in remove_from_request_hash()
+		 */
+		return 0;
+	}
+
+	/*
+	 *	Some sanity checks, based on the packet code.
+	 */
+	switch (packet->code) {
+	case PW_CODE_ACCESS_REQUEST:
+		if (listener->type != RAD_LISTEN_AUTH) goto bad_packet;
+		FR_STATS_INC(auth, total_requests);
+		fun = rad_authenticate;
+		break;
+
+#ifdef WITH_ACCOUNTING
+	case PW_CODE_ACCOUNTING_REQUEST:
+		if (listener->type != RAD_LISTEN_ACCT) {
+			/*
+			 *	Allow auth + dual.  Disallow
+			 *	everything else.
+			 */
+			if (!((listener->type == RAD_LISTEN_AUTH) &&
+			      (listener->dual))) {
+				    goto bad_packet;
+			}
+		}
+		FR_STATS_INC(acct, total_requests);
+		fun = rad_accounting;
+		break;
+#endif
+
+	case PW_CODE_STATUS_SERVER:
+		if (!main_config.status_server) {
+			FR_STATS_INC(auth, total_unknown_types);
+			WARN("Ignoring Status-Server request due to security configuration");
+			rad_free(&sock->packet);
+			return 0;
+		}
+		fun = rad_status_server;
+		break;
+
+	default:
+	bad_packet:
+		FR_STATS_INC(auth, total_unknown_types);
+
+		DEBUG("Invalid packet code %d sent from client %s port %d : IGNORED",
+		      packet->code, client->shortname, packet->src_port);
+		rad_free(&sock->packet);
+		return 0;
+	} /* switch over packet types */
+
+	if (!request_receive(NULL, listener, packet, client, fun)) {
+		FR_STATS_INC(auth, total_packets_dropped);
+		rad_free(&sock->packet);
+		return 0;
+	}
+
+	sock->packet = NULL;	/* we have no need for more partial reads */
+	return 1;
+}
+
+#ifdef WITH_TLS
+typedef struct {
+	char const	*name;
+	SSL_CTX		*ctx;
+} fr_realm_ctx_t;		/* hack from tls. */
+
+static int tls_sni_callback(SSL *ssl, UNUSED int *al, void *arg)
+{
+	fr_tls_server_conf_t *conf = arg;
+	char const *name, *p;
+	int type;
+	fr_realm_ctx_t my_r, *r;
+	REQUEST *request;
+	char buffer[PATH_MAX];
+
+	/*
+	 *	No SNI, that's fine.
+	 */
+	type = SSL_get_servername_type(ssl);
+	if (type < 0) return SSL_TLSEXT_ERR_OK;
+
+	/*
+	 *	No realms configured, just use the default context.
+	 */
+	if (!conf->realms) return SSL_TLSEXT_ERR_OK;
+
+	name = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+	if (!name) return SSL_TLSEXT_ERR_OK;
+
+	/*
+	 *	RFC Section 6066 Section 3 says that the names are
+	 *	ASCII, without a trailing dot.  i.e. punycode.
+	 */
+	for (p = name; *p != '\0'; p++) {
+		if (*p == '-') continue;
+		if (*p == '.') continue;
+		if ((*p >= 'A') && (*p <= 'Z')) continue;
+		if ((*p >= 'a') && (*p <= 'z')) continue;
+		if ((*p >= '0') && (*p <= '9')) continue;
+
+		/*
+		 *	Anything else, fail.
+		 */
+		return SSL_TLSEXT_ERR_ALERT_FATAL;
+	}
+
+	/*
+	 *	Too long, fail.
+	 */
+	if ((p - name) > 255) return SSL_TLSEXT_ERR_ALERT_FATAL;
+
+	snprintf(buffer, sizeof(buffer), "%s/%s.pem", conf->realm_dir, name);
+
+	my_r.name = buffer;
+	r = fr_hash_table_finddata(conf->realms, &my_r);
+
+	/*
+	 *	If found, switch certs.  Otherwise use the default
+	 *	one.
+	 */
+	if (r) (void) SSL_set_SSL_CTX(ssl, r->ctx);
+		
+	/*
+	 *	Set an attribute saying which server has been selected.
+	 */
+	request = (REQUEST *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST);
+	if (request) {
+		(void) pair_make_config("TLS-Server-Name-Indication", name, T_OP_SET);
+	}
+
+	return SSL_TLSEXT_ERR_OK;
+}
+#endif
+
+#ifdef WITH_RADIUSV11
+static const unsigned char radiusv11_alpn_protos[] = {
+	10, 'r', 'a', 'd', 'i', 'u', 's', '/', '1', '.', '1',
+};
+
+/*
+ *	On the server, get the ALPN list requested by the client.
+ */
+static int radiusv11_server_alpn_cb(SSL *ssl,
+				    const unsigned char **out,
+				    unsigned char *outlen,
+				    const unsigned char *in,
+				    unsigned int inlen,
+				    void *arg)
+{
+	rad_listen_t *this = arg;
+	listen_socket_t *sock = this->data;
+	unsigned char **hack;
+	const unsigned char *server;
+	unsigned int server_len, i;
+	int rcode;
+	REQUEST *request;
+
+	request = (REQUEST *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST);
+	fr_assert(request != NULL);
+
+	fr_assert(inlen > 0);
+
+	memcpy(&hack, &out, sizeof(out)); /* const issues */
+
+	/*
+	 *	The RADIUSv11 configuration for this socket is a combination of what we require, and what we
+	 *	require of the client.
+	 */
+	switch (this->radiusv11) {
+		/*
+		 *	If we forbid RADIUSv11, then we never advertised it via ALPN, and this callback should
+		 *	never have been registered.
+		 */
+	case FR_RADIUSV11_FORBID:
+		*out = NULL;
+		*outlen = 0;
+		return SSL_TLSEXT_ERR_OK;
+
+	case FR_RADIUSV11_ALLOW:
+	case FR_RADIUSV11_REQUIRE:
+		server = radiusv11_alpn_protos;
+		server_len = sizeof(radiusv11_alpn_protos);
+		break;
+	}
+
+	for (i = 0; i < inlen; i += in[0] + 1) {
+		RDEBUG("(TLS) ALPN sent by client is \"%.*s\"", in[i], &in[i + 1]);
+	}
+
+	/*
+	 *	Select the next protocol.
+	 */
+	rcode = SSL_select_next_proto(hack, outlen, server, server_len, in, inlen);
+	if (rcode == OPENSSL_NPN_NEGOTIATED) {
+		server = *out;
+
+		/*
+		 *	Tell our socket which protocol we negotiated.
+		 */
+		fr_assert(*outlen == 10);
+		sock->radiusv11 = (server[9] == '1');
+
+		RDEBUG("(TLS) ALPN server negotiated application protocol \"%.*s\"", (int) *outlen, server);
+		return SSL_TLSEXT_ERR_OK;
+	}
+
+	/*
+	 *	No common ALPN.
+	 */
+	RDEBUG("(TLS) ALPN failure - no protocols in common");
+	return SSL_TLSEXT_ERR_ALERT_FATAL;
+}
+
+int fr_radiusv11_client_init(fr_tls_server_conf_t *tls);
+int fr_radiusv11_client_get_alpn(rad_listen_t *listener);
+
+int fr_radiusv11_client_init(fr_tls_server_conf_t *tls)
+{
+	switch (tls->radiusv11) {
+	case FR_RADIUSV11_ALLOW:
+	case FR_RADIUSV11_REQUIRE:
+		if (SSL_CTX_set_alpn_protos(tls->ctx, radiusv11_alpn_protos, sizeof(radiusv11_alpn_protos)) != 0) {
+			ERROR("Failed setting RADIUSv11 negotiation flags");
+			return -1;
+		}
+		break;
+
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+int fr_radiusv11_client_get_alpn(rad_listen_t *listener)
+{
+	const unsigned char *data;
+	unsigned int len;
+	listen_socket_t *sock = listener->data;
+
+	SSL_get0_alpn_selected(sock->ssn->ssl, &data, &len);
+	if (!data) {
+		DEBUG("(TLS) ALPN home server did not send any application protocol");
+		if (listener->radiusv11 == FR_RADIUSV11_REQUIRE) {
+			DEBUG("(TLS) We have 'radiusv11 = require', but the home server has not negotiated it - closing socket");
+			return -1;
+		}
+
+		DEBUG("(TLS) ALPN assuming historical RADIUS");
+		return 0;
+	}
+
+	DEBUG("(TLS) ALPN home server sent application protocol \"%.*s\"", (int) len, data);
+
+	if (len != 10) {
+	radiusv11_unknown:
+		DEBUG("(TLS) ALPN home server sent unknown application protocol - closing connection");
+		return -1;
+	}
+
+	/*
+	 *	Should always be "radius/1.1".  The server MUST echo back one of the strings
+	 *	we sent.  If it doesn't, it's a bad server.
+	 */
+	if (memcmp(data, "radius/1.1", 10) != 0) goto radiusv11_unknown;
+
+	/*
+	 *	Double-check what the server sent us.  It SHOULD be sane, but it never hurts to check.
+	 */
+	switch (listener->radiusv11) {
+	case FR_RADIUSV11_FORBID:
+		DEBUG("(TLS) ALPN home server sent \"radius/v1.1\" but we forbid it - closing connection to home server");
+		return -1;
+
+	case FR_RADIUSV11_ALLOW:
+	case FR_RADIUSV11_REQUIRE:
+		DEBUG("(TLS) ALPN using \"radius/1.1\"");
+		sock->radiusv11 = true;
+		break;
+	}
+
+	sock->alpn_checked = true;
+	return 0;
+}
+#endif
+
+
+static int dual_tcp_accept(rad_listen_t *listener)
+{
+	int newfd;
+	uint16_t src_port;
+	rad_listen_t *this;
+	socklen_t salen;
+	struct sockaddr_storage src;
+	listen_socket_t *sock;
+	fr_ipaddr_t src_ipaddr;
+	RADCLIENT *client = NULL;
+
+	salen = sizeof(src);
+
+	DEBUG2(" ... new connection request on TCP socket");
+
+	newfd = accept(listener->fd, (struct sockaddr *) &src, &salen);
+	if (newfd < 0) {
+		/*
+		 *	Non-blocking sockets must handle this.
+		 */
+#ifdef EWOULDBLOCK
+		if (errno == EWOULDBLOCK) {
+			return 0;
+		}
+#endif
+
+		DEBUG2(" ... failed to accept connection");
+		return -1;
+	}
+
+	if (!fr_sockaddr2ipaddr(&src, salen, &src_ipaddr, &src_port)) {
+		close(newfd);
+		DEBUG2(" ... unknown address family");
+		return 0;
+	}
+
+	/*
+	 *	Enforce client IP address checks on accept, not on
+	 *	every packet.
+	 */
+	if ((client = client_listener_find(listener,
+					   &src_ipaddr, src_port)) == NULL) {
+		close(newfd);
+		FR_STATS_INC(auth, total_invalid_requests);
+		return 0;
+	}
+
+#ifdef WITH_TLS
+	/*
+	 *	Enforce security restrictions.
+	 *
+	 *	This shouldn't be necessary in practice.  However, it
+	 *	serves as a double-check on configurations.  Marking a
+	 *	client as "tls required" means that any accidental
+	 *	exposure of the client to non-TLS traffic is
+	 *	prevented.
+	 */
+	if (client->tls_required && !listener->tls) {
+		INFO("Ignoring connection to TLS socket from non-TLS client");
+		close(newfd);
+		return 0;
+	}
+
+#ifdef WITH_RADIUSV11
+	if (listener->tls) {
+		switch (listener->tls->radiusv11) {
+		case FR_RADIUSV11_FORBID:
+			if (client->radiusv11 == FR_RADIUSV11_REQUIRE) {
+				INFO("Ignoring new connection as client is marked as 'radiusv11 = require', and this socket has 'radiusv11 = forbid'");
+				close(newfd);
+				return 0;
+			}
+			break;
+
+		case FR_RADIUSV11_ALLOW:
+			/*
+			 *	We negotiate it as per the client recommendations (forbid, allow, require)
+			 */
+			break;
+
+		case FR_RADIUSV11_REQUIRE:
+			if (client->radiusv11 == FR_RADIUSV11_FORBID) {
+				INFO("Ignoring new connection as client is marked as 'radiusv11 = forbid', and this socket has 'radiusv11 = require'");
+				close(newfd);
+				return 0;
+			}
+			break;
+		}
+	}
+#endif
+
+#endif
+
+	/*
+	 *	Enforce max_connections on client && listen section.
+	 */
+	if ((client->limit.max_connections != 0) &&
+	    (client->limit.max_connections == client->limit.num_connections)) {
+		/*
+		 *	FIXME: Print client IP/port, and server IP/port.
+		 */
+		INFO("Ignoring new connection due to client max_connections (%d)", client->limit.max_connections);
+		close(newfd);
+		return 0;
+	}
+
+	sock = listener->data;
+	if ((sock->limit.max_connections != 0) &&
+	    (sock->limit.max_connections == sock->limit.num_connections)) {
+		/*
+		 *	FIXME: Print client IP/port, and server IP/port.
+		 */
+		INFO("Ignoring new connection due to socket max_connections");
+		close(newfd);
+		return 0;
+	}
+	client->limit.num_connections++;
+	sock->limit.num_connections++;
+
+	/*
+	 *	Add the new listener.  We require a new context here,
+	 *	because the allocations for the packet, etc. in the
+	 *	child listener will be done in a child thread.
+	 */
+	this = listen_alloc(NULL, listener->type);
+	if (!this) return -1;
+
+	/*
+	 *	Copy everything, including the pointer to the socket
+	 *	information.
+	 */
+	sock = this->data;
+	memcpy(this->data, listener->data, sizeof(*sock));
+	memcpy(this, listener, sizeof(*this));
+	this->next = NULL;
+	this->data = sock;	/* fix it back */
+
+	sock->parent = listener->data;
+	sock->other_ipaddr = src_ipaddr;
+	sock->other_port = src_port;
+	sock->client = client;
+	sock->opened = sock->last_packet = time(NULL);
+
+	/*
+	 *	Set the limits.  The defaults are the parent limits.
+	 *	Client limits on max_connections are enforced dynamically.
+	 *	Set the MINIMUM of client/socket idle timeout or lifetime.
+	 */
+	memcpy(&sock->limit, &sock->parent->limit, sizeof(sock->limit));
+
+	if (client->limit.idle_timeout &&
+	    ((sock->limit.idle_timeout == 0) ||
+	     (client->limit.idle_timeout < sock->limit.idle_timeout))) {
+		sock->limit.idle_timeout = client->limit.idle_timeout;
+	}
+
+	if (client->limit.lifetime &&
+	    ((sock->limit.lifetime == 0) ||
+	     (client->limit.lifetime < sock->limit.lifetime))) {
+		sock->limit.lifetime = client->limit.lifetime;
+	}
+
+	this->fd = newfd;
+	this->status = RAD_LISTEN_STATUS_INIT;
+
+	this->parent = listener;
+	if (!rbtree_insert(listener->children, this)) {
+		ERROR("Failed inserting TCP socket into parent list.");
+	}
+
+#ifdef WITH_COMMAND_SOCKET
+	if (this->type == RAD_LISTEN_COMMAND) {
+		this->recv = command_tcp_recv;
+		this->send = command_tcp_send;
+		command_write_magic(this->fd, sock);
+	} else
+#endif
+	{
+
+		this->recv = dual_tcp_recv;
+
+#ifdef WITH_TLS
+		if (this->tls) {
+			this->recv = dual_tls_recv;
+			this->send = dual_tls_send;
+
+			/*
+			 *	Set up SNI callback.  We don't do it
+			 *	in the main TLS code, because EAP
+			 *	doesn't need or use SNI.
+			 */
+			SSL_CTX_set_tlsext_servername_callback(this->tls->ctx, tls_sni_callback);
+			SSL_CTX_set_tlsext_servername_arg(this->tls->ctx, this->tls);
+#ifdef WITH_RADIUSV11
+			/*
+			 *      Default is "forbid" (0).  In which case we don't set any ALPN callbacks, and
+			 *      the ServerHello does not contain an ALPN section.
+			 */
+			if (client->radiusv11 != FR_RADIUSV11_FORBID) {
+				SSL_CTX_set_alpn_select_cb(this->tls->ctx, radiusv11_server_alpn_cb, this);
+				DEBUG("(TLS) ALPN radiusv11 = allow / require");
+			} else {
+				DEBUG("(TLS) ALPN radiusv11 = forbid");
+			}
+#endif
+		}
+#endif
+	}
+
+#ifdef WITH_COA_TUNNEL
+	/*
+	 *	Originate CoA requests to a NAS.
+	 */
+	if (this->send_coa) {
+		home_server_t *home;
+
+		rad_assert(this->type != RAD_LISTEN_PROXY);
+
+		this->proxy_send = dual_tls_send_coa_request;
+		this->proxy_encode = master_listen[RAD_LISTEN_PROXY].encode;
+		this->proxy_decode = master_listen[RAD_LISTEN_PROXY].decode;
+
+		/*
+		 *	Automatically create a home server for this
+		 *	client.  There MAY be one already one for that
+		 *	IP in the configuration files, but it will not
+		 *	have this particular port.
+		 */
+		sock->home = home = talloc_zero(this, home_server_t);
+		home->ipaddr = sock->other_ipaddr;
+		home->port = sock->other_port;
+		home->proto = sock->proto;
+		home->secret = sock->client->secret;
+
+		home->coa_irt = this->coa_irt;
+		home->coa_mrt = this->coa_mrt;
+		home->coa_mrc = this->coa_mrc;
+		home->coa_mrd = this->coa_mrd;
+		home->recv_coa_server = this->server;
+	}
+#endif
+
+	/*
+	 *	FIXME: set O_NONBLOCK on the accept'd fd.
+	 *	See djb's portability rants for details.
+	 */
+
+	/*
+	 *	Tell the event loop that we have a new FD.
+	 *	This can be called from a child thread...
+	 */
+	radius_update_listener(this);
+
+	return 0;
+}
+#endif
+
+/*
+ *	Ensure that we always keep the correct counters.
+ */
+#ifdef WITH_TCP
+static void common_socket_free(rad_listen_t *this)
+{
+	listen_socket_t *sock = this->data;
+
+	if (sock->proto != IPPROTO_TCP) return;
+
+	/*
+	 *      Decrement the number of connections.
+	 */
+	if (sock->parent && (sock->parent->limit.num_connections > 0)) {
+		sock->parent->limit.num_connections--;
+	}
+	if (sock->client && sock->client->limit.num_connections > 0) {
+		sock->client->limit.num_connections--;
+	}
+	if (sock->home && sock->home->limit.num_connections > 0) {
+		sock->home->limit.num_connections--;
+	}
+}
+#else
+#define common_socket_free NULL
+#endif
+
+/*
+ *	This function is stupid and complicated.
+ */
+int common_socket_print(rad_listen_t const *this, char *buffer, size_t bufsize)
+{
+	size_t len;
+	listen_socket_t *sock = this->data;
+	char const *name = master_listen[this->type].name;
+
+#define FORWARD len = strlen(buffer); if (len >= (bufsize + 1)) return 0;buffer += len;bufsize -= len
+#define ADDSTRING(_x) strlcpy(buffer, _x, bufsize);FORWARD
+
+	ADDSTRING(name);
+
+#ifdef WITH_TCP
+	if (this->dual) {
+		ADDSTRING("+acct");
+	}
+#endif
+
+#ifdef WITH_COA_TUNNEL
+	if (this->send_coa) {
+		ADDSTRING("+coa");
+	}
+#endif
+
+	if (sock->interface) {
+		ADDSTRING(" interface ");
+		ADDSTRING(sock->interface);
+	}
+
+#ifdef WITH_TCP
+	if (this->recv == dual_tcp_accept) {
+		ADDSTRING(" proto tcp");
+	}
+#endif
+
+#ifdef WITH_TCP
+	/*
+	 *	TCP sockets get printed a little differently, to make
+	 *	it clear what's going on.
+	 */
+	if (sock->client) {
+		ADDSTRING(" from client (");
+		ip_ntoh(&sock->other_ipaddr, buffer, bufsize);
+		FORWARD;
+
+		ADDSTRING(", ");
+		snprintf(buffer, bufsize, "%d", sock->other_port);
+		FORWARD;
+		ADDSTRING(") -> (");
+
+		if ((sock->my_ipaddr.af == AF_INET) &&
+		    (sock->my_ipaddr.ipaddr.ip4addr.s_addr == htonl(INADDR_ANY))) {
+			strlcpy(buffer, "*", bufsize);
+		} else {
+			ip_ntoh(&sock->my_ipaddr, buffer, bufsize);
+		}
+		FORWARD;
+
+		ADDSTRING(", ");
+		snprintf(buffer, bufsize, "%d", sock->my_port);
+		FORWARD;
+
+		if (this->server) {
+			ADDSTRING(", virtual-server=");
+			ADDSTRING(this->server);
+		}
+
+		ADDSTRING(")");
+
+		return 1;
+	}
+
+#ifdef WITH_PROXY
+	/*
+	 *	Maybe it's a socket that we opened to a home server.
+	 */
+	if ((sock->proto == IPPROTO_TCP) &&
+	    (this->type == RAD_LISTEN_PROXY)) {
+		ADDSTRING(" (");
+		ip_ntoh(&sock->my_ipaddr, buffer, bufsize);
+		FORWARD;
+
+		ADDSTRING(", ");
+		snprintf(buffer, bufsize, "%d", sock->my_port);
+		FORWARD;
+		ADDSTRING(") -> home_server (");
+
+		if ((sock->other_ipaddr.af == AF_INET) &&
+		    (sock->other_ipaddr.ipaddr.ip4addr.s_addr == htonl(INADDR_ANY))) {
+			strlcpy(buffer, "*", bufsize);
+		} else {
+			ip_ntoh(&sock->other_ipaddr, buffer, bufsize);
+		}
+		FORWARD;
+
+		ADDSTRING(", ");
+		snprintf(buffer, bufsize, "%d", sock->other_port);
+		FORWARD;
+
+		ADDSTRING(")");
+
+		return 1;
+	}
+#endif	/* WITH_PROXY */
+#endif	/* WITH_TCP */
+
+	ADDSTRING(" address ");
+
+	if ((sock->my_ipaddr.af == AF_INET) &&
+	    (sock->my_ipaddr.ipaddr.ip4addr.s_addr == htonl(INADDR_ANY))) {
+		strlcpy(buffer, "*", bufsize);
+	} else {
+		ip_ntoh(&sock->my_ipaddr, buffer, bufsize);
+	}
+	FORWARD;
+
+	ADDSTRING(" port ");
+	snprintf(buffer, bufsize, "%d", sock->my_port);
+	FORWARD;
+
+#ifdef WITH_TLS
+	if (this->tls) {
+		ADDSTRING(" (TLS)");
+		FORWARD;
+	}
+#endif
+
+	if (this->server) {
+		ADDSTRING(" bound to server ");
+		strlcpy(buffer, this->server, bufsize);
+	}
+
+#undef ADDSTRING
+#undef FORWARD
+
+	return 1;
+}
+
+static CONF_PARSER performance_config[] = {
+	{ "skip_duplicate_checks", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rad_listen_t, nodup), NULL },
+
+	{ "synchronous", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rad_listen_t, synchronous), NULL },
+
+	{ "workers", FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, workers), NULL },
+	CONF_PARSER_TERMINATOR
+};
+
+
+static CONF_PARSER limit_config[] = {
+	{ "max_pps", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_socket_t, max_rate), NULL },
+
+#ifdef WITH_TCP
+	{ "max_connections", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_socket_t, limit.max_connections), "16" },
+	{ "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_socket_t, limit.lifetime), "0" },
+	{ "idle_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_socket_t, limit.idle_timeout), STRINGIFY(30) },
+#endif
+	CONF_PARSER_TERMINATOR
+};
+
+#ifdef WITH_COA_TUNNEL
+static CONF_PARSER coa_config[] = {
+	{ "irt",  FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, coa_irt), STRINGIFY(2) },
+	{ "mrt",  FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, coa_mrt), STRINGIFY(16) },
+	{ "mrc",  FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, coa_mrc), STRINGIFY(5) },
+	{ "mrd",  FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, coa_mrd), STRINGIFY(30) },
+	CONF_PARSER_TERMINATOR
+};
+#endif
+
+#ifdef WITH_TCP
+/*
+ *	TLS requires child threads to handle the listeners.  Which
+ *	means that we need a separate talloc context per child thread.
+ *	Which means that we need to manually clean up the child
+ *	listeners.  Which means we need to manually track them.
+ *
+ *	All child thread linking/unlinking is done in the master
+ *	thread.  If we care, we can later add a mutex for the parent
+ *	listener.
+ */
+static int listener_cmp(void const *one, void const *two)
+{
+	if (one < two) return -1;
+	if (one > two) return +1;
+	return 0;
+}
+
+static int listener_unlink(UNUSED void *ctx, UNUSED void *data)
+{
+	return 2;		/* unlink this node from the tree */
+}
+#endif
+
+
+/*
+ *	Parse an authentication or accounting socket.
+ */
+int common_socket_parse(CONF_SECTION *cs, rad_listen_t *this)
+{
+	int		rcode;
+	uint16_t	listen_port;
+	fr_ipaddr_t	ipaddr;
+	listen_socket_t *sock = this->data;
+	char const	*section_name = NULL;
+	CONF_SECTION	*client_cs, *parentcs;
+	CONF_SECTION	*subcs;
+	CONF_PAIR	*cp;
+
+	this->cs = cs;
+
+	/*
+	 *	Try IPv4 first
+	 */
+	memset(&ipaddr, 0, sizeof(ipaddr));
+	ipaddr.ipaddr.ip4addr.s_addr = htonl(INADDR_NONE);
+
+	rcode = cf_item_parse(cs, "ipaddr", FR_ITEM_POINTER(PW_TYPE_COMBO_IP_ADDR, &ipaddr), NULL);
+	if (rcode < 0) return -1;
+	if (rcode != 0) rcode = cf_item_parse(cs, "ipv4addr", FR_ITEM_POINTER(PW_TYPE_IPV4_ADDR, &ipaddr), NULL);
+	if (rcode < 0) return -1;
+	if (rcode != 0) rcode = cf_item_parse(cs, "ipv6addr", FR_ITEM_POINTER(PW_TYPE_IPV6_ADDR, &ipaddr), NULL);
+	if (rcode < 0) return -1;
+	if (rcode != 0) {
+		cf_log_err_cs(cs, "No address specified in listen section");
+		return -1;
+	}
+
+	rcode = cf_item_parse(cs, "port", FR_ITEM_POINTER(PW_TYPE_SHORT, &listen_port), "0");
+	if (rcode < 0) return -1;
+
+	rcode = cf_item_parse(cs, "recv_buff", PW_TYPE_INTEGER, &sock->recv_buff, NULL);
+	if (rcode < 0) return -1;
+
+	sock->proto = IPPROTO_UDP;
+
+	if (cf_pair_find(cs, "proto")) {
+#ifndef WITH_TCP
+		cf_log_err_cs(cs,
+			   "System does not support the TCP protocol.  Delete this line from the configuration file");
+		return -1;
+#else
+		char const *proto = NULL;
+#ifdef WITH_TLS
+		CONF_SECTION *tls;
+#endif
+
+		rcode = cf_item_parse(cs, "proto", FR_ITEM_POINTER(PW_TYPE_STRING, &proto), "udp");
+		if (rcode < 0) return -1;
+
+		if (!proto || strcmp(proto, "udp") == 0) {
+			sock->proto = IPPROTO_UDP;
+
+		} else if (strcmp(proto, "tcp") == 0) {
+			sock->proto = IPPROTO_TCP;
+
+		} else {
+			cf_log_err_cs(cs,
+				   "Unknown proto name \"%s\"", proto);
+			return -1;
+		}
+
+		/*
+		 *	TCP requires a destination IP for sockets.
+		 *	UDP doesn't, so it's allowed.
+		 */
+#ifdef WITH_PROXY
+		if ((this->type == RAD_LISTEN_PROXY) &&
+		    (sock->proto != IPPROTO_UDP)) {
+			cf_log_err_cs(cs,
+				   "Proxy listeners can only listen on proto = udp");
+			return -1;
+		}
+#endif	/* WITH_PROXY */
+
+#ifdef WITH_TLS
+		tls = cf_section_sub_find(cs, "tls");
+
+		if (tls) {
+			/*
+			 *	Don't allow TLS configurations for UDP sockets.
+			 */
+			if (sock->proto != IPPROTO_TCP) {
+				cf_log_err_cs(cs,
+					      "TLS transport is not available for UDP sockets");
+				return -1;
+			}
+
+			/*
+			 *	Add support for http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+			 */
+			rcode = cf_item_parse(cs, "proxy_protocol", FR_ITEM_POINTER(PW_TYPE_BOOLEAN, &this->proxy_protocol), NULL);
+			if (rcode < 0) return -1;
+
+			/*
+			 *	Allow non-blocking for TLS sockets
+			 */
+			rcode = cf_item_parse(cs, "nonblock", FR_ITEM_POINTER(PW_TYPE_BOOLEAN, &this->nonblock), NULL);
+			if (rcode < 0) return -1;
+
+			/*
+			 *	If unset, set to default.
+			 */
+			if (listen_port == 0) listen_port = PW_RADIUS_TLS_PORT;
+
+			this->tls = tls_server_conf_parse(tls);
+			if (!this->tls) {
+				return -1;
+			}
+
+#ifdef HAVE_PTHREAD_H
+			if (pthread_mutex_init(&sock->mutex, NULL) < 0) {
+				rad_assert(0 == 1);
+				listen_free(&this);
+				return 0;
+			}
+#endif
+
+			rcode = cf_item_parse(cs, "check_client_connections", FR_ITEM_POINTER(PW_TYPE_BOOLEAN, &this->check_client_connections), "no");
+			if (rcode < 0) return -1;
+
+#ifdef WITH_RADIUSV11
+			if (this->tls->radiusv11_name) {
+				rcode = fr_str2int(radiusv11_types, this->tls->radiusv11_name, -1);
+				if (rcode < 0) {
+					cf_log_err_cs(cs, "Invalid value for 'radiusv11'");
+					return -1;
+				}
+
+				this->radiusv11 = this->tls->radiusv11 = rcode;
+			}
+#endif
+		}
+#else  /* WITH_TLS */
+		/*
+		 *	Built without TLS.  Disallow it.
+		 */
+		if (cf_section_sub_find(cs, "tls")) {
+			cf_log_err_cs(cs,
+				   "TLS transport is not available in this executable");
+			return -1;
+		}
+#endif	/* WITH_TLS */
+
+#endif	/* WITH_TCP */
+
+		/*
+		 *	No "proto" field.  Disallow TLS.
+		 */
+	} else if (cf_section_sub_find(cs, "tls")) {
+		cf_log_err_cs(cs,
+			   "TLS transport is not available in this \"listen\" section");
+		return -1;
+	}
+
+	/*
+	 *	Magical tuning methods!
+	 */
+	subcs = cf_section_sub_find(cs, "performance");
+	if (subcs) {
+		rcode = cf_section_parse(subcs, this,
+					 performance_config);
+		if (rcode < 0) return -1;
+
+		if (this->synchronous && sock->max_rate) {
+			WARN("Setting 'max_pps' is incompatible with 'synchronous'.  Disabling 'max_pps'");
+			sock->max_rate = 0;
+		}
+
+		if (!this->synchronous && this->workers) {
+			WARN("Setting 'workers' requires 'synchronous'.  Disabling 'workers'");
+			this->workers = 0;
+		}
+	}
+
+	subcs = cf_section_sub_find(cs, "limit");
+	if (subcs) {
+		rcode = cf_section_parse(subcs, sock,
+					 limit_config);
+		if (rcode < 0) return -1;
+
+		if (sock->max_rate && ((sock->max_rate < 10) || (sock->max_rate > 1000000))) {
+			cf_log_err_cs(cs,
+				      "Invalid value for \"max_pps\"");
+			return -1;
+		}
+
+#ifdef WITH_TCP
+		if ((sock->limit.idle_timeout > 0) && (sock->limit.idle_timeout < 5)) {
+			WARN("Setting idle_timeout to 5");
+			sock->limit.idle_timeout = 5;
+		}
+
+		if ((sock->limit.lifetime > 0) && (sock->limit.lifetime < 5)) {
+			WARN("Setting lifetime to 5");
+			sock->limit.lifetime = 5;
+		}
+
+		if ((sock->limit.lifetime > 0) && (sock->limit.idle_timeout > sock->limit.lifetime)) {
+			WARN("Setting idle_timeout to 0");
+			sock->limit.idle_timeout = 0;
+		}
+
+		/*
+		 *	Force no duplicate detection for TCP sockets.
+		 */
+		if (sock->proto == IPPROTO_TCP) {
+			this->nodup = true;
+		}
+
+	} else {
+		sock->limit.max_connections = 60;
+		sock->limit.idle_timeout = 30;
+		sock->limit.lifetime = 0;
+#endif
+	}
+
+	sock->my_ipaddr = ipaddr;
+	sock->my_port = listen_port;
+
+#ifdef WITH_PROXY
+	if (check_config) {
+		/*
+		 *	Until there is a side effects free way of forwarding a
+		 *	request to another virtual server, this check is invalid,
+		 *	and should be left disabled.
+		 */
+#if 0
+		if (home_server_find(&sock->my_ipaddr, sock->my_port, sock->proto)) {
+				char buffer[128];
+
+				ERROR("We have been asked to listen on %s port %d, which is also listed as a "
+				       "home server.  This can create a proxy loop",
+				       ip_ntoh(&sock->my_ipaddr, buffer, sizeof(buffer)), sock->my_port);
+				return -1;
+		}
+#endif
+		return 0;	/* don't do anything */
+	}
+#endif
+
+	/*
+	 *	If we can bind to interfaces, do so,
+	 *	else don't.
+	 */
+	cp = cf_pair_find(cs, "interface");
+	if (cp) {
+		char const *value = cf_pair_value(cp);
+		if (!value) {
+			cf_log_err_cs(cs,
+				   "No interface name given");
+			return -1;
+		}
+		sock->interface = value;
+	}
+
+#ifdef WITH_DHCP
+	/*
+	 *	If we can do broadcasts..
+	 */
+	cp = cf_pair_find(cs, "broadcast");
+	if (cp) {
+#ifndef SO_BROADCAST
+		cf_log_err_cs(cs,
+			   "System does not support broadcast sockets.  Delete this line from the configuration file");
+		return -1;
+#else
+		if (this->type != RAD_LISTEN_DHCP) {
+			cf_log_err_cp(cp,
+				   "Broadcast can only be set for DHCP listeners.  Delete this line from the configuration file");
+			return -1;
+		}
+
+		char const *value = cf_pair_value(cp);
+		if (!value) {
+			cf_log_err_cs(cs,
+				   "No broadcast value given");
+			return -1;
+		}
+
+		/*
+		 *	Hack... whatever happened to cf_section_parse?
+		 */
+		sock->broadcast = (strcmp(value, "yes") == 0);
+#endif
+	}
+#endif
+
+	/*
+	 *	And bind it to the port.
+	 */
+	if (listen_bind(this) < 0) {
+		char buffer[128];
+		cf_log_err_cs(cs,
+			   "Error binding to port for %s port %d",
+			   ip_ntoh(&sock->my_ipaddr, buffer, sizeof(buffer)),
+			   sock->my_port);
+		return -1;
+	}
+
+#ifdef WITH_PROXY
+	/*
+	 *	Proxy sockets don't have clients.
+	 */
+	if (this->type == RAD_LISTEN_PROXY) return 0;
+#endif
+
+	/*
+	 *	The more specific configurations are preferred to more
+	 *	generic ones.
+	 */
+	client_cs = NULL;
+	parentcs = cf_top_section(cs);
+	rcode = cf_item_parse(cs, "clients", FR_ITEM_POINTER(PW_TYPE_STRING, &section_name), NULL);
+	if (rcode < 0) return -1; /* bad string */
+	if (rcode == 0) {
+		/*
+		 *	Explicit list given: use it.
+		 */
+		client_cs = cf_section_sub_find_name2(parentcs, "clients", section_name);
+		if (!client_cs) {
+			client_cs = cf_section_find(section_name);
+		}
+		if (!client_cs) {
+			cf_log_err_cs(cs,
+				   "Failed to find clients %s {...}",
+				   section_name);
+			return -1;
+		}
+	} /* else there was no "clients = " entry. */
+
+	/*
+	 *	The "listen" section wasn't given an explicit client list.
+	 *	Look for (a) clients in this virtual server, or
+	 *	(b) the global client list.
+	 */
+	if (!client_cs) {
+		CONF_SECTION *server_cs;
+
+		server_cs = cf_section_sub_find_name2(parentcs,
+						      "server",
+						      this->server);
+		/*
+		 *	Found a "server foo" section.  If there are clients
+		 *	in it, use them.
+		 */
+		if (server_cs &&
+		    (cf_section_sub_find(server_cs, "client") != NULL)) {
+			client_cs = server_cs;
+		}
+	}
+
+	/*
+	 *	Still nothing.  Look for global clients.
+	 */
+	if (!client_cs) client_cs = parentcs;
+
+#ifdef WITH_TLS
+	sock->clients = client_list_parse_section(client_cs, (this->tls != NULL));
+#else
+	sock->clients = client_list_parse_section(client_cs, false);
+#endif
+	if (!sock->clients) {
+		cf_log_err_cs(cs,
+			   "Failed to load clients for this listen section");
+		return -1;
+	}
+
+#ifdef WITH_TCP
+	if (sock->proto == IPPROTO_TCP) {
+		/*
+		 *	Re-write the listener receive function to
+		 *	allow us to accept the socket.
+		 */
+		this->recv = dual_tcp_accept;
+
+		/*
+		 *	@todo - add a free function?  Though this only
+		 *	matters when we're tearing down the server, so
+		 *	perhaps it's less relevant.
+		 */
+		this->children = rbtree_create(this, listener_cmp, NULL, 0);
+		if (!this->children) {
+			cf_log_err_cs(cs, "Failed to create child list for TCP socket.");
+			return -1;
+		}
+	}
+#endif
+
+	return 0;
+}
+
+/*
+ *	Send a response packet
+ */
+static int common_socket_send(rad_listen_t *listener, REQUEST *request)
+{
+	rad_assert(request->listener == listener);
+	rad_assert(listener->send == common_socket_send);
+
+	if (request->reply->code == 0) return 0;
+
+#ifdef WITH_UDPFROMTO
+	/*
+	 *	Overwrite the src ip address on the outbound packet
+	 *	with the one specified by the client.
+	 *	This is useful to work around broken DSR implementations
+	 *	and other routing issues.
+	 */
+	if (request->client->src_ipaddr.af != AF_UNSPEC) {
+		request->reply->src_ipaddr = request->client->src_ipaddr;
+	}
+#endif
+
+	if (rad_send(request->reply, request->packet,
+		     request->client->secret) < 0) {
+		RERROR("Failed sending reply: %s",
+			       fr_strerror());
+		return -1;
+	}
+	return 0;
+}
+
+
+#ifdef WITH_PROXY
+/*
+ *	Send a packet to a home server.
+ *
+ *	FIXME: have different code for proxy auth & acct!
+ */
+static int proxy_socket_send(rad_listen_t *listener, REQUEST *request)
+{
+	rad_assert(request->proxy_listener == listener);
+	rad_assert(listener->proxy_send == proxy_socket_send);
+
+	if (rad_send(request->proxy, NULL,
+		     request->home_server->secret) < 0) {
+		RERROR("Failed sending proxied request: %s",
+			       fr_strerror());
+		return -1;
+	}
+
+	return 0;
+}
+#endif
+
+#ifdef WITH_STATS
+/*
+ *	Check if an incoming request is "ok"
+ *
+ *	It takes packets, not requests.  It sees if the packet looks
+ *	OK.  If so, it does a number of sanity checks on it.
+  */
+static int stats_socket_recv(rad_listen_t *listener)
+{
+	ssize_t		rcode;
+	int		code;
+	uint16_t	src_port;
+	RADIUS_PACKET	*packet;
+	RADCLIENT	*client = NULL;
+	fr_ipaddr_t	src_ipaddr;
+
+	rcode = rad_recv_header(listener->fd, &src_ipaddr, &src_port, &code);
+	if (rcode < 0) return 0;
+
+	FR_STATS_INC(auth, total_requests);
+
+	if (rcode < 20) {	/* RADIUS_HDR_LEN */
+		if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror());
+		FR_STATS_INC(auth, total_malformed_requests);
+		return 0;
+	}
+
+	if ((client = client_listener_find(listener,
+					   &src_ipaddr, src_port)) == NULL) {
+		rad_recv_discard(listener->fd);
+		FR_STATS_INC(auth, total_invalid_requests);
+		return 0;
+	}
+
+	FR_STATS_TYPE_INC(client->auth.total_requests);
+
+	/*
+	 *	We only understand Status-Server on this socket.
+	 */
+	if (code != PW_CODE_STATUS_SERVER) {
+		DEBUG("Ignoring packet code %d sent to Status-Server port",
+		      code);
+		rad_recv_discard(listener->fd);
+		FR_STATS_INC(auth, total_unknown_types);
+		return 0;
+	}
+
+	/*
+	 *	Now that we've sanity checked everything, receive the
+	 *	packet.
+	 */
+	packet = rad_recv(NULL, listener->fd, 1); /* require message authenticator */
+	if (!packet) {
+		FR_STATS_INC(auth, total_malformed_requests);
+		if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror());
+		return 0;
+	}
+
+	if (!request_receive(NULL, listener, packet, client, rad_status_server)) {
+		FR_STATS_INC(auth, total_packets_dropped);
+		rad_free(&packet);
+		return 0;
+	}
+
+	return 1;
+}
+#endif
+
+
+/*
+ *	Check if an incoming request is "ok"
+ *
+ *	It takes packets, not requests.  It sees if the packet looks
+ *	OK.  If so, it does a number of sanity checks on it.
+  */
+static int auth_socket_recv(rad_listen_t *listener)
+{
+	ssize_t		rcode;
+	int		code;
+	uint16_t	src_port;
+	RADIUS_PACKET	*packet;
+	RAD_REQUEST_FUNP fun = NULL;
+	RADCLIENT	*client = NULL;
+	fr_ipaddr_t	src_ipaddr;
+	TALLOC_CTX	*ctx;
+
+	rcode = rad_recv_header(listener->fd, &src_ipaddr, &src_port, &code);
+	if (rcode < 0) return 0;
+
+	FR_STATS_INC(auth, total_requests);
+
+	if (rcode < 20) {	/* RADIUS_HDR_LEN */
+		if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror());
+		FR_STATS_INC(auth, total_malformed_requests);
+		return 0;
+	}
+
+	if ((client = client_listener_find(listener,
+					   &src_ipaddr, src_port)) == NULL) {
+		rad_recv_discard(listener->fd);
+		FR_STATS_INC(auth, total_invalid_requests);
+		return 0;
+	}
+
+	/*
+	 *	Some sanity checks, based on the packet code.
+	 */
+	switch (code) {
+	case PW_CODE_ACCESS_REQUEST:
+		FR_STATS_TYPE_INC(client->auth.total_requests);
+		fun = rad_authenticate;
+		break;
+
+	case PW_CODE_STATUS_SERVER:
+		if (!main_config.status_server) {
+			rad_recv_discard(listener->fd);
+			FR_STATS_INC(auth, total_unknown_types);
+			WARN("Ignoring Status-Server request due to security configuration");
+			return 0;
+		}
+		fun = rad_status_server;
+		break;
+
+	default:
+		rad_recv_discard(listener->fd);
+		FR_STATS_INC(auth, total_unknown_types);
+
+		if (DEBUG_ENABLED) ERROR("Receive - Invalid packet code %d sent to authentication port from "
+					 "client %s port %d", code, client->shortname, src_port);
+		return 0;
+	} /* switch over packet types */
+
+	ctx = talloc_pool(NULL, main_config.talloc_pool_size);
+	if (!ctx) {
+		rad_recv_discard(listener->fd);
+		FR_STATS_INC(auth, total_packets_dropped);
+		return 0;
+	}
+	talloc_set_name_const(ctx, "auth_listener_pool");
+
+	/*
+	 *	Now that we've sanity checked everything, receive the
+	 *	packet.
+	 */
+	packet = rad_recv(ctx, listener->fd, client->message_authenticator);
+	if (!packet) {
+		FR_STATS_INC(auth, total_malformed_requests);
+		if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror());
+		talloc_free(ctx);
+		return 0;
+	}
+
+#ifdef __APPLE__
+#ifdef WITH_UDPFROMTO
+	/*
+	 *	This is a NICE Mac OSX bug.  Create an interface with
+	 *	two IP address, and then configure one listener for
+	 *	each IP address.  Send thousands of packets to one
+	 *	address, and some will show up on the OTHER socket.
+	 *
+	 *	This hack works ONLY if the clients are global.  If
+	 *	each listener has the same client IP, but with
+	 *	different secrets, then it will fail the rad_recv()
+	 *	check above, and there's nothing you can do.
+	 */
+	{
+		listen_socket_t *sock = listener->data;
+		rad_listen_t *other;
+
+		other = listener_find_byipaddr(&packet->dst_ipaddr,
+					       packet->dst_port, sock->proto);
+		if (other) listener = other;
+	}
+#endif
+#endif
+
+	if (!request_receive(ctx, listener, packet, client, fun)) {
+		FR_STATS_INC(auth, total_packets_dropped);
+		talloc_free(ctx);
+		return 0;
+	}
+
+	return 1;
+}
+
+
+#ifdef WITH_ACCOUNTING
+/*
+ *	Receive packets from an accounting socket
+ */
+static int acct_socket_recv(rad_listen_t *listener)
+{
+	ssize_t		rcode;
+	int		code;
+	uint16_t	src_port;
+	RADIUS_PACKET	*packet;
+	RAD_REQUEST_FUNP fun = NULL;
+	RADCLIENT	*client = NULL;
+	fr_ipaddr_t	src_ipaddr;
+	TALLOC_CTX	*ctx;
+
+	rcode = rad_recv_header(listener->fd, &src_ipaddr, &src_port, &code);
+	if (rcode < 0) return 0;
+
+	FR_STATS_INC(acct, total_requests);
+
+	if (rcode < 20) {	/* RADIUS_HDR_LEN */
+		if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror());
+		FR_STATS_INC(acct, total_malformed_requests);
+		return 0;
+	}
+
+	if ((client = client_listener_find(listener,
+					   &src_ipaddr, src_port)) == NULL) {
+		rad_recv_discard(listener->fd);
+		FR_STATS_INC(acct, total_invalid_requests);
+		return 0;
+	}
+
+	/*
+	 *	Some sanity checks, based on the packet code.
+	 */
+	switch (code) {
+	case PW_CODE_ACCOUNTING_REQUEST:
+		FR_STATS_TYPE_INC(client->acct.total_requests);
+		fun = rad_accounting;
+		break;
+
+	case PW_CODE_STATUS_SERVER:
+		if (!main_config.status_server) {
+			rad_recv_discard(listener->fd);
+			FR_STATS_INC(acct, total_unknown_types);
+
+			WARN("Ignoring Status-Server request due to security configuration");
+			return 0;
+		}
+		fun = rad_status_server;
+		break;
+
+	default:
+		rad_recv_discard(listener->fd);
+		FR_STATS_INC(acct, total_unknown_types);
+
+		DEBUG("Invalid packet code %d sent to a accounting port from client %s port %d : IGNORED",
+		      code, client->shortname, src_port);
+		return 0;
+	} /* switch over packet types */
+
+	ctx = talloc_pool(NULL, main_config.talloc_pool_size);
+	if (!ctx) {
+		rad_recv_discard(listener->fd);
+		FR_STATS_INC(acct, total_packets_dropped);
+		return 0;
+	}
+	talloc_set_name_const(ctx, "acct_listener_pool");
+
+	/*
+	 *	Now that we've sanity checked everything, receive the
+	 *	packet.
+	 */
+	packet = rad_recv(ctx, listener->fd, 0);
+	if (!packet) {
+		FR_STATS_INC(acct, total_malformed_requests);
+		if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror());
+		talloc_free(ctx);
+		return 0;
+	}
+
+	/*
+	 *	There can be no duplicate accounting packets.
+	 */
+	if (!request_receive(ctx, listener, packet, client, fun)) {
+		FR_STATS_INC(acct, total_packets_dropped);
+		rad_free(&packet);
+		talloc_free(ctx);
+		return 0;
+	}
+
+	return 1;
+}
+#endif
+
+
+#ifdef WITH_COA
+static int do_proxy(REQUEST *request)
+{
+	VALUE_PAIR *vp;
+
+	if (request->in_proxy_hash ||
+	    (request->proxy_reply && (request->proxy_reply->code != 0))) {
+		return 0;
+	}
+
+	vp = fr_pair_find_by_num(request->config, PW_HOME_SERVER_POOL, 0, TAG_ANY);
+
+	if (vp) {
+		if (!home_pool_byname(vp->vp_strvalue, HOME_TYPE_COA)) {
+			REDEBUG2("Cannot proxy to unknown pool %s",
+				 vp->vp_strvalue);
+			return -1;
+		}
+
+		return 1;
+	}
+
+	/*
+	 *	We have a destination IP address.  It will (later) proxied.
+	 */
+	vp = fr_pair_find_by_num(request->config, PW_PACKET_DST_IP_ADDRESS, 0, TAG_ANY);
+	if (!vp) vp = fr_pair_find_by_num(request->config, PW_PACKET_DST_IPV6_ADDRESS, 0, TAG_ANY);
+
+#ifdef WITH_COA_TUNNEL
+	if (!vp) vp = fr_pair_find_by_num(request->config, PW_PROXY_TO_ORIGINATING_REALM, 0, TAG_ANY);
+#endif
+
+	if (!vp) return 0;
+
+	return 1;
+}
+
+/*
+ *	Receive a CoA packet.
+ */
+int rad_coa_recv(REQUEST *request)
+{
+	int rcode = RLM_MODULE_OK;
+	int ack, nak;
+	int proxy_status;
+	VALUE_PAIR *vp;
+
+	/*
+	 *	Get the correct response
+	 */
+	switch (request->packet->code) {
+	case PW_CODE_COA_REQUEST:
+		ack = PW_CODE_COA_ACK;
+		nak = PW_CODE_COA_NAK;
+		break;
+
+	case PW_CODE_DISCONNECT_REQUEST:
+		ack = PW_CODE_DISCONNECT_ACK;
+		nak = PW_CODE_DISCONNECT_NAK;
+		break;
+
+	default:		/* shouldn't happen */
+		return RLM_MODULE_FAIL;
+	}
+
+#ifdef WITH_PROXY
+#define WAS_PROXIED (request->proxy)
+#else
+#define WAS_PROXIED (0)
+#endif
+
+	if (!WAS_PROXIED) {
+		/*
+		 *	RFC 5176 Section 3.3.  If we have a CoA-Request
+		 *	with Service-Type = Authorize-Only, it MUST
+		 *	have a State attribute in it.
+		 */
+		vp = fr_pair_find_by_num(request->packet->vps, PW_SERVICE_TYPE, 0, TAG_ANY);
+		if (request->packet->code == PW_CODE_COA_REQUEST) {
+			if (vp && (vp->vp_integer == PW_AUTHORIZE_ONLY)) {
+				vp = fr_pair_find_by_num(request->packet->vps, PW_STATE, 0, TAG_ANY);
+				if (!vp || (vp->vp_length == 0)) {
+					REDEBUG("CoA-Request with Service-Type = Authorize-Only MUST contain a State attribute");
+					request->reply->code = PW_CODE_COA_NAK;
+					return RLM_MODULE_FAIL;
+				}
+			}
+		} else if (vp) {
+			/*
+			 *	RFC 5176, Section 3.2.
+			 */
+			REDEBUG("Disconnect-Request MUST NOT contain a Service-Type attribute");
+			request->reply->code = PW_CODE_DISCONNECT_NAK;
+			return RLM_MODULE_FAIL;
+		}
+
+		rcode = process_recv_coa(0, request);
+		switch (rcode) {
+		case RLM_MODULE_FAIL:
+		case RLM_MODULE_INVALID:
+		case RLM_MODULE_REJECT:
+		case RLM_MODULE_USERLOCK:
+		default:
+			request->reply->code = nak;
+			break;
+
+		case RLM_MODULE_HANDLED:
+			return rcode;
+
+		case RLM_MODULE_NOOP:
+		case RLM_MODULE_NOTFOUND:
+		case RLM_MODULE_OK:
+		case RLM_MODULE_UPDATED:
+			proxy_status = do_proxy(request);
+			if (proxy_status == 1) return RLM_MODULE_OK;
+
+			if (proxy_status < 0) {
+				request->reply->code = nak;
+			} else {
+				request->reply->code = ack;
+			}
+			break;
+		}
+
+	}
+
+#ifdef WITH_PROXY
+	else if (request->proxy_reply) {
+		/*
+		 *	Start the reply code with the proxy reply
+		 *	code.
+		 */
+		request->reply->code = request->proxy_reply->code;
+	}
+#endif
+
+	/*
+	 *	Copy State from the request to the reply.
+	 *	See RFC 5176 Section 3.3.
+	 */
+	vp = fr_pair_list_copy_by_num(request->reply, request->packet->vps, PW_STATE, 0, TAG_ANY);
+	if (vp) fr_pair_add(&request->reply->vps, vp);
+
+	/*
+	 *	We may want to over-ride the reply.
+	 */
+	if (request->reply->code) {
+		rcode = process_send_coa(0, request);
+		switch (rcode) {
+			/*
+			 *	We need to send CoA-NAK back if Service-Type
+			 *	is Authorize-Only.  Rely on the user's policy
+			 *	to do that.  We're not a real NAS, so this
+			 *	restriction doesn't (ahem) apply to us.
+			 */
+		case RLM_MODULE_FAIL:
+		case RLM_MODULE_INVALID:
+		case RLM_MODULE_REJECT:
+		case RLM_MODULE_USERLOCK:
+		default:
+			/*
+			 *	Over-ride an ACK with a NAK
+			 */
+			request->reply->code = nak;
+			break;
+
+		case RLM_MODULE_HANDLED:
+			return rcode;
+
+		case RLM_MODULE_NOOP:
+		case RLM_MODULE_NOTFOUND:
+		case RLM_MODULE_OK:
+		case RLM_MODULE_UPDATED:
+			/*
+			 *	Do NOT over-ride a previously set value.
+			 *	Otherwise an "ok" here will re-write a
+			 *	NAK to an ACK.
+			 */
+			if (request->reply->code == 0) {
+				request->reply->code = ack;
+			}
+			break;
+		}
+	}
+
+	return RLM_MODULE_OK;
+}
+
+
+/*
+ *	Check if an incoming request is "ok"
+ *
+ *	It takes packets, not requests.  It sees if the packet looks
+ *	OK.  If so, it does a number of sanity checks on it.
+  */
+static int coa_socket_recv(rad_listen_t *listener)
+{
+	ssize_t		rcode;
+	int		code;
+	uint16_t	src_port;
+	RADIUS_PACKET	*packet;
+	RAD_REQUEST_FUNP fun = NULL;
+	RADCLIENT	*client = NULL;
+	fr_ipaddr_t	src_ipaddr;
+	TALLOC_CTX	*ctx;
+
+	rcode = rad_recv_header(listener->fd, &src_ipaddr, &src_port, &code);
+	if (rcode < 0) return 0;
+
+	if (rcode < 20) {	/* RADIUS_HDR_LEN */
+		if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror());
+		FR_STATS_INC(coa, total_malformed_requests);
+		return 0;
+	}
+
+	if ((client = client_listener_find(listener,
+					   &src_ipaddr, src_port)) == NULL) {
+		rad_recv_discard(listener->fd);
+		FR_STATS_INC(coa, total_requests);
+		FR_STATS_INC(coa, total_invalid_requests);
+		return 0;
+	}
+
+	/*
+	 *	Some sanity checks, based on the packet code.
+	 */
+	switch (code) {
+	case PW_CODE_COA_REQUEST:
+		FR_STATS_INC(coa, total_requests);
+		fun = rad_coa_recv;
+		break;
+
+	case PW_CODE_DISCONNECT_REQUEST:
+		FR_STATS_INC(dsc, total_requests);
+		fun = rad_coa_recv;
+		break;
+
+	default:
+		rad_recv_discard(listener->fd);
+		FR_STATS_INC(coa, total_unknown_types);
+		DEBUG("Invalid packet code %d sent to coa port from client %s port %d : IGNORED",
+		      code, client->shortname, src_port);
+		return 0;
+	} /* switch over packet types */
+
+	ctx = talloc_pool(NULL, main_config.talloc_pool_size);
+	if (!ctx) {
+		rad_recv_discard(listener->fd);
+		FR_STATS_INC(coa, total_packets_dropped);
+		return 0;
+	}
+	talloc_set_name_const(ctx, "coa_socket_recv_pool");
+
+	/*
+	 *	Now that we've sanity checked everything, receive the
+	 *	packet.
+	 */
+	packet = rad_recv(ctx, listener->fd, client->message_authenticator);
+	if (!packet) {
+		FR_STATS_INC(coa, total_malformed_requests);
+		if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror());
+		talloc_free(ctx);
+		return 0;
+	}
+
+	if (!request_receive(ctx, listener, packet, client, fun)) {
+		FR_STATS_INC(coa, total_packets_dropped);
+		rad_free(&packet);
+		talloc_free(ctx);
+		return 0;
+	}
+
+	return 1;
+}
+#endif
+
+#ifdef WITH_PROXY
+/*
+ *	Recieve packets from a proxy socket.
+ */
+static int proxy_socket_recv(rad_listen_t *listener)
+{
+	RADIUS_PACKET	*packet;
+#ifdef WITH_TCP
+	listen_socket_t *sock;
+#endif
+	char		buffer[128];
+
+	packet = rad_recv(NULL, listener->fd, 0);
+	if (!packet) {
+		if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror());
+		return 0;
+	}
+
+	switch (packet->code) {
+	case PW_CODE_ACCESS_ACCEPT:
+	case PW_CODE_ACCESS_CHALLENGE:
+	case PW_CODE_ACCESS_REJECT:
+		break;
+
+#ifdef WITH_ACCOUNTING
+	case PW_CODE_ACCOUNTING_RESPONSE:
+		break;
+#endif
+
+#ifdef WITH_COA
+	case PW_CODE_DISCONNECT_ACK:
+	case PW_CODE_DISCONNECT_NAK:
+	case PW_CODE_COA_ACK:
+	case PW_CODE_COA_NAK:
+		break;
+#endif
+
+	default:
+		/*
+		 *	FIXME: Update MIB for packet types?
+		 */
+		ERROR("Invalid packet code %d sent to a proxy port "
+		       "from home server %s port %d - ID %d : IGNORED",
+		       packet->code,
+		       ip_ntoh(&packet->src_ipaddr, buffer, sizeof(buffer)),
+		       packet->src_port, packet->id);
+#ifdef WITH_STATS
+		listener->stats.total_unknown_types++;
+#endif
+		rad_free(&packet);
+		return 0;
+	}
+
+#ifdef WITH_TCP
+	sock = listener->data;
+	packet->proto = sock->proto;
+#endif
+
+	if (!request_proxy_reply(packet)) {
+#ifdef WITH_STATS
+		listener->stats.total_packets_dropped++;
+#endif
+		rad_free(&packet);
+		return 0;
+	}
+
+	return 1;
+}
+
+#ifdef WITH_TCP
+/*
+ *	Recieve packets from a proxy socket.
+ */
+static int proxy_socket_tcp_recv(rad_listen_t *listener)
+{
+	int rcode;
+	RADIUS_PACKET	*packet;
+	listen_socket_t	*sock = listener->data;
+	char		buffer[256];
+
+	if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0;
+
+	if (!sock->packet) {
+		sock->packet = rad_alloc(sock, false);
+		if (!sock->packet) return 0;
+
+		sock->packet->sockfd = listener->fd;
+		sock->packet->src_ipaddr = sock->other_ipaddr;
+		sock->packet->src_port = sock->other_port;
+		sock->packet->dst_ipaddr = sock->my_ipaddr;
+		sock->packet->dst_port = sock->my_port;
+		sock->packet->proto = sock->proto;
+	}
+
+	packet = sock->packet;
+
+	rcode = fr_tcp_read_packet(packet, 0);
+
+	/*
+	 *	Still only a partial packet.  Put it back, and return,
+	 *	so that we'll read more data when it's ready.
+	 */
+	if (rcode == 0) {
+		return 0;
+	}
+
+	if (rcode == -1) {	/* error reading packet */
+		ERROR("Invalid packet from %s port %d, closing socket: %s",
+		       ip_ntoh(&packet->src_ipaddr, buffer, sizeof(buffer)),
+		       packet->src_port, fr_strerror());
+	}
+
+	if (rcode < 0) {	/* error or connection reset */
+		listener->status = RAD_LISTEN_STATUS_EOL;
+
+		/*
+		 *	Tell the event handler that an FD has disappeared.
+		 */
+		DEBUG("Home server %s port %d has closed connection",
+		      ip_ntoh(&packet->src_ipaddr, buffer, sizeof(buffer)),
+		      packet->src_port);
+
+		radius_update_listener(listener);
+
+		/*
+		 *	Do NOT free the listener here.  It's in use by
+		 *	a request, and will need to hang around until
+		 *	all of the requests are done.
+		 *
+		 *	It is instead free'd in remove_from_request_hash()
+		 */
+		return 0;
+	}
+
+	sock->packet = NULL;	/* we have no need for more partial reads */
+
+	/*
+	 *	FIXME: Client MIB updates?
+	 */
+	switch (packet->code) {
+	case PW_CODE_ACCESS_ACCEPT:
+	case PW_CODE_ACCESS_CHALLENGE:
+	case PW_CODE_ACCESS_REJECT:
+		break;
+
+#ifdef WITH_ACCOUNTING
+	case PW_CODE_ACCOUNTING_RESPONSE:
+		break;
+#endif
+
+	default:
+		/*
+		 *	FIXME: Update MIB for packet types?
+		 */
+		ERROR("Invalid packet code %d sent to a proxy port "
+		       "from home server %s port %d - ID %d : IGNORED",
+		       packet->code,
+		       ip_ntoh(&packet->src_ipaddr, buffer, sizeof(buffer)),
+		       packet->src_port, packet->id);
+		rad_free(&packet);
+		return 0;
+	}
+
+
+	/*
+	 *	FIXME: Have it return an indication of packets that
+	 *	are OK to ignore (dups, too late), versus ones that
+	 *	aren't OK to ignore (unknown response, spoofed, etc.)
+	 *
+	 *	Close the socket on bad packets...
+	 */
+	if (!request_proxy_reply(packet)) {
+		rad_free(&packet);
+		return 0;
+	}
+
+	sock->opened = sock->last_packet = time(NULL);
+
+	return 1;
+}
+#endif
+#endif
+
+#ifdef WITH_TLS
+#define TLS_UNUSED
+#else
+#define TLS_UNUSED UNUSED
+#endif
+
+static int client_socket_encode(TLS_UNUSED rad_listen_t *listener, REQUEST *request)
+{
+#ifdef WITH_TLS
+	/*
+	 *	Don't encode fake packets.
+	 */
+	listen_socket_t *sock = listener->data;
+	if (sock->state == LISTEN_TLS_CHECKING) return 0;
+
+#ifdef WITH_RADIUSV11
+	request->reply->radiusv11 = sock->radiusv11;
+#endif
+
+#endif
+
+	if (!request->reply->code) return 0;
+
+	if (request->reply->data) return 0; /* already encoded */
+
+	if (rad_encode(request->reply, request->packet, request->client->secret) < 0) {
+		RERROR("Failed encoding packet: %s", fr_strerror());
+
+		return -1;
+	}
+
+	if (request->reply->data_len > (MAX_PACKET_LEN - 100)) {
+		RWDEBUG("Packet is large, and possibly truncated - %zd vs max %d",
+		      request->reply->data_len, MAX_PACKET_LEN);
+	}
+
+	if (rad_sign(request->reply, request->packet, request->client->secret) < 0) {
+		RERROR("Failed signing packet: %s", fr_strerror());
+
+		return -1;
+	}
+
+	return 0;
+}
+
+
+static int client_socket_decode(UNUSED rad_listen_t *listener, REQUEST *request)
+{
+#ifdef WITH_TLS
+	listen_socket_t *sock = request->listener->data;
+
+#ifdef WITH_RADIUSV11
+	request->packet->radiusv11 = sock->radiusv11;
+#endif
+#endif
+
+	if (rad_verify(request->packet, NULL,
+		       request->client->secret) < 0) {
+		return -1;
+	}
+
+#ifdef WITH_TLS
+	/*
+	 *	FIXME: Add the rest of the TLS parameters, too?  But
+	 *	how do we separate EAP-TLS parameters from RADIUS/TLS
+	 *	parameters?
+	 */
+	if (sock->ssn && sock->ssn->ssl) {
+#ifdef PSK_MAX_IDENTITY_LEN
+		const char *identity = SSL_get_psk_identity(sock->ssn->ssl);
+		if (identity) {
+			RDEBUG("Retrieved psk identity: %s", identity);
+			pair_make_request("TLS-PSK-Identity", identity, T_OP_SET);
+		}
+#endif
+	}
+#endif
+
+	return rad_decode(request->packet, NULL,
+			  request->client->secret);
+}
+
+#ifdef WITH_PROXY
+#ifdef WITH_RADIUSV11
+#define RADIUSV11_UNUSED
+#else
+#define RADIUSV11_UNUSED UNUSED
+#endif
+
+static int proxy_socket_encode(RADIUSV11_UNUSED rad_listen_t *listener, REQUEST *request)
+{
+#ifdef WITH_RADIUSV11
+	listen_socket_t *sock = listener->data;
+
+	request->proxy->radiusv11 = sock->radiusv11;
+#endif
+
+	if (rad_encode(request->proxy, NULL, request->home_server->secret) < 0) {
+		RERROR("Failed encoding proxied packet: %s", fr_strerror());
+
+		return -1;
+	}
+
+	if (request->proxy->data_len > (MAX_PACKET_LEN - 100)) {
+		RWDEBUG("Packet is large, and possibly truncated - %zd vs max %d",
+		      request->proxy->data_len, MAX_PACKET_LEN);
+	}
+
+	if (rad_sign(request->proxy, NULL, request->home_server->secret) < 0) {
+		RERROR("Failed signing proxied packet: %s", fr_strerror());
+
+		return -1;
+	}
+
+	return 0;
+}
+
+
+static int proxy_socket_decode(UNUSED rad_listen_t *listener, REQUEST *request)
+{
+#ifdef WITH_RADIUSV11
+	listen_socket_t *sock = listener->data;
+
+	request->proxy_reply->radiusv11 = sock->radiusv11;
+#endif
+
+	/*
+	 *	rad_verify is run in event.c, received_proxy_response()
+	 */
+
+	return rad_decode(request->proxy_reply, request->proxy,
+			   request->home_server->secret);
+}
+#endif
+
+#include "command.c"
+
+/*
+ *	Temporarily NOT const!
+ */
+static fr_protocol_t master_listen[RAD_LISTEN_MAX] = {
+#ifdef WITH_STATS
+	{ RLM_MODULE_INIT, "status", sizeof(listen_socket_t), NULL,
+	  common_socket_parse, NULL,
+	  stats_socket_recv, common_socket_send,
+	  common_socket_print, client_socket_encode, client_socket_decode },
+#else
+	/*
+	 *	This always gets defined.
+	 */
+	{ RLM_MODULE_INIT, "status", 0, NULL,
+	  NULL, NULL, NULL, NULL, NULL, NULL, NULL},	/* RAD_LISTEN_NONE */
+#endif
+
+#ifdef WITH_PROXY
+	/* proxying */
+	{ RLM_MODULE_INIT, "proxy", sizeof(listen_socket_t), NULL,
+	  common_socket_parse, common_socket_free,
+	  proxy_socket_recv, proxy_socket_send,
+	  common_socket_print, proxy_socket_encode, proxy_socket_decode },
+#else
+	{ 0, "proxy", 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL },
+#endif
+
+	/* authentication */
+	{ RLM_MODULE_INIT, "auth", sizeof(listen_socket_t), NULL,
+	  common_socket_parse, common_socket_free,
+	  auth_socket_recv, common_socket_send,
+	  common_socket_print, client_socket_encode, client_socket_decode },
+
+#ifdef WITH_ACCOUNTING
+	/* accounting */
+	{ RLM_MODULE_INIT, "acct", sizeof(listen_socket_t), NULL,
+	  common_socket_parse, common_socket_free,
+	  acct_socket_recv, common_socket_send,
+	  common_socket_print, client_socket_encode, client_socket_decode},
+#else
+	{ 0, "acct", 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL },
+#endif
+
+#ifdef WITH_DETAIL
+	/* detail */
+	{ RLM_MODULE_INIT, "detail", sizeof(listen_detail_t), NULL,
+	  detail_parse, detail_free,
+	  detail_recv, detail_send,
+	  detail_print, detail_encode, detail_decode },
+#endif
+
+	/* vlan query protocol */
+	{ 0, "vmps", 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL },
+
+	/* dhcp query protocol */
+	{ 0, "dhcp", 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL },
+
+#ifdef WITH_COMMAND_SOCKET
+	/* TCP command socket */
+	{ RLM_MODULE_INIT, "control", sizeof(fr_command_socket_t), NULL,
+	  command_socket_parse, command_socket_free,
+	  command_domain_accept, command_domain_send,
+	  command_socket_print, command_socket_encode, command_socket_decode },
+#else
+	{ 0, "command", 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL },
+#endif
+
+#ifdef WITH_COA
+	/* Change of Authorization */
+	{ RLM_MODULE_INIT, "coa", sizeof(listen_socket_t), NULL,
+	  common_socket_parse, NULL,
+	  coa_socket_recv, common_socket_send,
+	  common_socket_print, client_socket_encode, client_socket_decode },
+#else
+	{ 0, "coa", 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL },
+#endif
+
+};
+
+
+
+/*
+ *	Binds a listener to a socket.
+ */
+static int listen_bind(rad_listen_t *this)
+{
+	int rcode;
+	struct sockaddr_storage salocal;
+	socklen_t	salen;
+	listen_socket_t *sock = this->data;
+#ifndef WITH_TCP
+#define proto_for_port "udp"
+#define sock_type SOCK_DGRAM
+#else
+	char const *proto_for_port = "udp";
+	int sock_type = SOCK_DGRAM;
+
+	if (sock->proto == IPPROTO_TCP) {
+#ifdef WITH_VMPS
+		if (this->type == RAD_LISTEN_VQP) {
+			ERROR("VQP does not support TCP transport");
+			return -1;
+		}
+#endif
+
+		proto_for_port = "tcp";
+		sock_type = SOCK_STREAM;
+	}
+#endif
+
+	/*
+	 *	If the port is zero, then it means the appropriate
+	 *	thing from /etc/services.
+	 */
+	if (sock->my_port == 0) {
+		struct servent	*svp;
+
+		switch (this->type) {
+		case RAD_LISTEN_AUTH:
+			svp = getservbyname ("radius", proto_for_port);
+			if (svp != NULL) {
+				sock->my_port = ntohs(svp->s_port);
+			} else {
+				sock->my_port = PW_AUTH_UDP_PORT;
+			}
+			break;
+
+#ifdef WITH_ACCOUNTING
+		case RAD_LISTEN_ACCT:
+			svp = getservbyname ("radacct", proto_for_port);
+			if (svp != NULL) {
+				sock->my_port = ntohs(svp->s_port);
+			} else {
+				sock->my_port = PW_ACCT_UDP_PORT;
+			}
+			break;
+#endif
+
+#ifdef WITH_PROXY
+		case RAD_LISTEN_PROXY:
+			/* leave it at zero */
+			break;
+#endif
+
+#ifdef WITH_VMPS
+		case RAD_LISTEN_VQP:
+			sock->my_port = 1589;
+			break;
+#endif
+
+#ifdef WITH_COMMAND_SOCKET
+		case RAD_LISTEN_COMMAND:
+			sock->my_port = PW_RADMIN_PORT;
+			break;
+#endif
+
+#ifdef WITH_COA
+		case RAD_LISTEN_COA:
+			svp = getservbyname ("radius-dynauth", "udp");
+			if (svp != NULL) {
+				sock->my_port = ntohs(svp->s_port);
+			} else {
+				sock->my_port = PW_COA_UDP_PORT;
+			}
+			break;
+#endif
+
+#ifdef WITH_DHCP
+		case RAD_LISTEN_DHCP:
+			svp = getservbyname ("bootps", "udp");
+			if (svp != NULL) {
+				sock->my_port = ntohs(svp->s_port);
+			} else {
+				sock->my_port = 67;
+			}
+			break;
+#endif
+
+		default:
+			WARN("Internal sanity check failed in binding to socket.  Ignoring problem");
+			return -1;
+		}
+	}
+
+	/*
+	 *	Don't open sockets if we're checking the config.
+	 */
+	if (check_config) {
+		this->fd = -1;
+		return 0;
+	}
+
+	/*
+	 *	Copy fr_socket() here, as we may need to bind to a device.
+	 */
+	this->fd = socket(sock->my_ipaddr.af, sock_type, 0);
+	if (this->fd < 0) {
+		char buffer[256];
+
+		this->print(this, buffer, sizeof(buffer));
+
+		ERROR("Failed opening %s: %s", buffer, fr_syserror(errno));
+		return -1;
+	}
+
+#ifdef FD_CLOEXEC
+	/*
+	 *	We don't want child processes inheriting these
+	 *	file descriptors.
+	 */
+	rcode = fcntl(this->fd, F_GETFD);
+	if (rcode >= 0) {
+		if (fcntl(this->fd, F_SETFD, rcode | FD_CLOEXEC) < 0) {
+			close(this->fd);
+			ERROR("Failed setting close on exec: %s", fr_syserror(errno));
+			return -1;
+		}
+	}
+#endif
+
+	/*
+	 *	Bind to a device BEFORE touching IP addresses.
+	 */
+	if (sock->interface) {
+#ifdef SO_BINDTODEVICE
+		struct ifreq ifreq;
+
+		memset(&ifreq, 0, sizeof(ifreq));
+		strlcpy(ifreq.ifr_name, sock->interface, sizeof(ifreq.ifr_name));
+
+		rad_suid_up();
+		rcode = setsockopt(this->fd, SOL_SOCKET, SO_BINDTODEVICE,
+				   (char *)&ifreq, sizeof(ifreq));
+		rad_suid_down();
+		if (rcode < 0) {
+			close(this->fd);
+			ERROR("Failed binding to interface %s: %s",
+			       sock->interface, fr_syserror(errno));
+			return -1;
+		} /* else it worked. */
+#else
+#ifdef HAVE_STRUCT_SOCKADDR_IN6
+#ifdef HAVE_NET_IF_H
+		/*
+		 *	Odds are that any system supporting "bind to
+		 *	device" also supports IPv6, so this next bit
+		 *	isn't necessary.  But it's here for
+		 *	completeness.
+		 *
+		 *	If we're doing IPv6, and the scope hasn't yet
+		 *	been defined, set the scope to the scope of
+		 *	the interface.
+		 */
+		if (sock->my_ipaddr.af == AF_INET6) {
+			if (sock->my_ipaddr.scope == 0) {
+				sock->my_ipaddr.scope = if_nametoindex(sock->interface);
+				if (sock->my_ipaddr.scope == 0) {
+					close(this->fd);
+					ERROR("Failed finding interface %s: %s",
+					       sock->interface, fr_syserror(errno));
+					return -1;
+				}
+			} /* else scope was defined: we're OK. */
+		} else
+#endif
+#endif
+				/*
+				 *	IPv4: no link local addresses,
+				 *	and no bind to device.
+				 */
+		{
+			close(this->fd);
+			ERROR("Failed binding to interface %s: \"bind to device\" is unsupported", sock->interface);
+			return -1;
+		}
+#endif
+	}
+
+#ifdef WITH_TCP
+	if (sock->proto == IPPROTO_TCP) {
+		int on = 1;
+
+		if (setsockopt(this->fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) {
+			close(this->fd);
+			ERROR("Failed to reuse address: %s", fr_syserror(errno));
+			return -1;
+		}
+	}
+#endif
+
+#if defined(WITH_TCP) && defined(WITH_UDPFROMTO)
+	else			/* UDP sockets get UDPfromto */
+#endif
+
+#ifdef WITH_UDPFROMTO
+	/*
+	 *	Initialize udpfromto for all sockets.
+	 */
+	if (udpfromto_init(this->fd) != 0) {
+		ERROR("Failed initializing udpfromto: %s",
+		       fr_syserror(errno));
+		close(this->fd);
+		return -1;
+	}
+#endif
+
+	/*
+	 *	Set up sockaddr stuff.
+	 */
+	if (!fr_ipaddr2sockaddr(&sock->my_ipaddr, sock->my_port, &salocal, &salen)) {
+		close(this->fd);
+		return -1;
+	}
+
+#ifdef HAVE_STRUCT_SOCKADDR_IN6
+	if (sock->my_ipaddr.af == AF_INET6) {
+		/*
+		 *	Listening on '::' does NOT get you IPv4 to
+		 *	IPv6 mapping.  You've got to listen on an IPv4
+		 *	address, too.  This makes the rest of the server
+		 *	design a little simpler.
+		 */
+#ifdef IPV6_V6ONLY
+
+		if (IN6_IS_ADDR_UNSPECIFIED(&sock->my_ipaddr.ipaddr.ip6addr)) {
+			int on = 1;
+
+			if (setsockopt(this->fd, IPPROTO_IPV6, IPV6_V6ONLY,
+				       (char *)&on, sizeof(on)) < 0) {
+				ERROR("Failed setting socket to IPv6 "
+				       "only: %s", fr_syserror(errno));
+
+				close(this->fd);
+				return -1;
+			}
+		}
+#endif /* IPV6_V6ONLY */
+	}
+#endif /* HAVE_STRUCT_SOCKADDR_IN6 */
+
+	if (sock->my_ipaddr.af == AF_INET) {
+#if (defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DONT)) || defined(IP_DONTFRAG)
+		int flag;
+#endif
+
+#if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DONT)
+
+		/*
+		 *	Disable PMTU discovery.  On Linux, this
+		 *	also makes sure that the "don't fragment"
+		 *	flag is zero.
+		 */
+		flag = IP_PMTUDISC_DONT;
+		if (setsockopt(this->fd, IPPROTO_IP, IP_MTU_DISCOVER,
+			       &flag, sizeof(flag)) < 0) {
+			ERROR("Failed disabling PMTU discovery: %s",
+			       fr_syserror(errno));
+
+			close(this->fd);
+			return -1;
+		}
+#endif
+
+#if defined(IP_DONTFRAG)
+		/*
+		 *	Ensure that the "don't fragment" flag is zero.
+		 */
+		flag = 0;
+		if (setsockopt(this->fd, IPPROTO_IP, IP_DONTFRAG,
+			       &flag, sizeof(flag)) < 0) {
+			ERROR("Failed setting don't fragment flag: %s",
+			       fr_syserror(errno));
+
+			close(this->fd);
+			return -1;
+		}
+#endif
+	}
+
+#ifdef WITH_DHCP
+#ifdef SO_BROADCAST
+	if (sock->broadcast) {
+		int on = 1;
+
+		if (setsockopt(this->fd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)) < 0) {
+			ERROR("Can't set broadcast option: %s",
+			       fr_syserror(errno));
+			return -1;
+		}
+	}
+#endif
+#endif
+
+#ifdef SO_RCVBUF
+	if (sock->recv_buff > 0) {
+		int opt;
+
+		opt = sock->recv_buff;
+		if (setsockopt(this->fd, SOL_SOCKET, SO_RCVBUF, &opt, sizeof(int)) < 0) {
+			WARN("Failed setting 'recv_buf': %s", fr_syserror(errno));
+		}
+	}
+#endif
+
+	/*
+	 *	May be binding to priviledged ports.
+	 */
+	if (sock->my_port != 0) {
+		rad_suid_up();
+		rcode = bind(this->fd, (struct sockaddr *) &salocal, salen);
+		rad_suid_down();
+		if (rcode < 0) {
+			char buffer[256];
+			close(this->fd);
+
+			this->print(this, buffer, sizeof(buffer));
+			ERROR("Failed binding to %s: %s\n",
+			       buffer, fr_syserror(errno));
+			return -1;
+		}
+
+		/*
+		 *	FreeBSD jail issues.  We bind to 0.0.0.0, but the
+		 *	kernel instead binds us to a 1.2.3.4.  If this
+		 *	happens, notice, and remember our real IP.
+		 */
+		{
+			struct sockaddr_storage	src;
+			socklen_t		sizeof_src = sizeof(src);
+
+			memset(&src, 0, sizeof_src);
+			if (getsockname(this->fd, (struct sockaddr *) &src,
+					&sizeof_src) < 0) {
+				ERROR("Failed getting socket name: %s",
+				       fr_syserror(errno));
+				return -1;
+			}
+
+			if (!fr_sockaddr2ipaddr(&src, sizeof_src,
+						&sock->my_ipaddr, &sock->my_port)) {
+				ERROR("Socket has unsupported address family");
+				return -1;
+			}
+		}
+	}
+
+#ifdef WITH_TCP
+	if (sock->proto == IPPROTO_TCP) {
+		/*
+		 *	Woker threads are blocking.
+		 *
+		 *	Otherwise, they're non-blocking.
+		 */
+		if (!this->workers) {
+			if (fr_nonblock(this->fd) < 0) {
+				close(this->fd);
+				ERROR("Failed setting non-blocking on socket: %s",
+				      fr_syserror(errno));
+				return -1;
+			}
+		}
+
+		/*
+		 *	Allow a backlog of 8 listeners, but only for incoming interfaces.
+		 */
+#ifdef WITH_PROXY
+		if (this->type != RAD_LISTEN_PROXY)
+#endif
+		if (listen(this->fd, 8) < 0) {
+			close(this->fd);
+			ERROR("Failed in listen(): %s", fr_syserror(errno));
+			return -1;
+		}
+	}
+#endif
+
+	/*
+	 *	Mostly for proxy sockets.
+	 */
+	sock->other_ipaddr.af = sock->my_ipaddr.af;
+
+/*
+ *	Don't screw up other people.
+ */
+#undef proto_for_port
+#undef sock_type
+
+	return 0;
+}
+
+
+static int _listener_free(rad_listen_t *this)
+{
+	/*
+	 *	Other code may have eaten the FD.
+	 */
+	if (this->fd >= 0) close(this->fd);
+
+	if (master_listen[this->type].free) {
+		master_listen[this->type].free(this);
+	}
+
+#ifdef WITH_TCP
+	if ((this->type == RAD_LISTEN_AUTH)
+#ifdef WITH_ACCT
+	    || (this->type == RAD_LISTEN_ACCT)
+#endif
+#ifdef WITH_PROXY
+	    || (this->type == RAD_LISTEN_PROXY)
+#endif
+#ifdef WITH_COMMAND_SOCKET
+	    || ((this->type == RAD_LISTEN_COMMAND) &&
+		(((fr_command_socket_t *) this->data)->magic != COMMAND_SOCKET_MAGIC))
+#endif
+		) {
+
+		/*
+		 *	Remove the child from the parent tree.
+		 */
+		if (this->parent) {
+			rbtree_deletebydata(this->parent->children, this);
+		}
+
+		/*
+		 *	Delete / close all of the children, too!
+		 */
+		if (this->children) {
+			rbtree_walk(this->children, RBTREE_DELETE_ORDER, listener_unlink, this);
+		}
+
+#ifdef WITH_TLS
+		/*
+		 *	Note that we do NOT free this->tls, as the
+		 *	pointer is parented by its CONF_SECTION.  It
+		 *	may be used by multiple listeners.
+		 */
+		if (this->tls) {
+			listen_socket_t *sock = this->data;
+
+			rad_assert(talloc_parent(sock) == this);
+			rad_assert(sock->ev == NULL);
+
+			rad_assert(!sock->ssn || (talloc_parent(sock->ssn) == sock));
+			rad_assert(!sock->request || (talloc_parent(sock->request) == sock));
+
+			if (sock->home && sock->home->listeners) (void) rbtree_deletebydata(sock->home->listeners, this);
+
+#ifdef HAVE_PTHREAD_H
+			pthread_mutex_destroy(&(sock->mutex));
+#endif
+
+		}
+#endif	/* WITH_TLS */
+	}
+#endif				/* WITH_TCP */
+
+	return 0;
+}
+
+
+/*
+ *	Allocate & initialize a new listener.
+ */
+static rad_listen_t *listen_alloc(TALLOC_CTX *ctx, RAD_LISTEN_TYPE type)
+{
+	rad_listen_t *this;
+
+	this = talloc_zero(ctx, rad_listen_t);
+
+	this->type = type;
+	this->recv = master_listen[this->type].recv;
+	this->send = master_listen[this->type].send;
+	this->print = master_listen[this->type].print;
+
+	if (type != RAD_LISTEN_PROXY) {
+		this->encode = master_listen[this->type].encode;
+		this->decode = master_listen[this->type].decode;
+	} else {
+		this->send = NULL; /* proxy packets shouldn't call this! */
+		this->proxy_send = master_listen[this->type].send;
+		this->proxy_encode = master_listen[this->type].encode;
+		this->proxy_decode = master_listen[this->type].decode;
+	}
+
+	talloc_set_destructor(this, _listener_free);
+
+	this->data = talloc_zero_array(this, uint8_t, master_listen[this->type].inst_size);
+
+	return this;
+}
+
+#ifdef WITH_PROXY
+
+/*
+ *	Externally visible function for creating a new proxy LISTENER.
+ *
+ *	Not thread-safe, but all calls to it are protected by the
+ *	proxy mutex in event.c
+ */
+rad_listen_t *proxy_new_listener(TALLOC_CTX *ctx, home_server_t *home, uint16_t src_port)
+{
+	time_t now;
+	rad_listen_t *this;
+	listen_socket_t *sock;
+	char buffer[256];
+
+	if (!home) return NULL;
+
+	rad_assert(home->virtual_server == NULL); /* we only open real sockets */
+
+	if ((home->limit.max_connections > 0) &&
+	    (home->limit.num_connections >= home->limit.max_connections)) {
+		RATE_LIMIT(INFO("Home server %s has too many open connections (%d)",
+				home->log_name, home->limit.max_connections));
+		return NULL;
+	}
+
+	now = time(NULL);
+	if (home->last_failed_open == now) {
+		WARN("Suppressing attempt to open socket to 'down' home server");
+		return NULL;
+	}
+
+	this = listen_alloc(ctx, RAD_LISTEN_PROXY);
+
+	sock = this->data;
+	sock->other_ipaddr = home->ipaddr;
+	sock->other_port = home->port;
+	sock->home = home;
+
+	sock->my_ipaddr = home->src_ipaddr;
+	sock->my_port = src_port;
+	sock->proto = home->proto;
+
+	/*
+	 *	For error messages.
+	 */
+	this->print(this, buffer, sizeof(buffer));
+
+#ifdef WITH_TCP
+	sock->opened = sock->last_packet = now;
+
+	if (home->proto == IPPROTO_TCP) {
+		this->recv = proxy_socket_tcp_recv;
+
+		/*
+		 *	FIXME: connect() is blocking!
+		 *	We do this with the proxy mutex locked, which may
+		 *	cause large delays!
+		 *
+		 *	http://www.developerweb.net/forum/showthread.php?p=13486
+		 */
+		this->fd = fr_socket_client_tcp(&home->src_ipaddr,
+						&home->ipaddr, home->port, false);
+
+		/*
+		 *	Set max_requests, lifetime, and idle_timeout from the home server.
+		 */
+		sock->limit = home->limit;
+	} else
+#endif
+		this->fd = fr_socket(&home->src_ipaddr, src_port);
+
+	if (this->fd < 0) {
+		this->print(this, buffer,sizeof(buffer));
+		ERROR("Failed opening new proxy socket '%s' : %s",
+		      buffer, fr_strerror());
+		home->last_failed_open = now;
+		listen_free(&this);
+		return NULL;
+	}
+
+
+#ifdef WITH_TCP
+#ifdef WITH_TLS
+	if ((home->proto == IPPROTO_TCP) && home->tls) {
+		DEBUG("(TLS) Trying new outgoing proxy connection to %s", buffer);
+
+		/*
+		 *	Set SNI, if configured.
+		 *
+		 *	The OpenSSL API says the filename is "char
+		 *	const *", but some versions have it as "void
+		 *	*", without the "const".  So we un-const it
+		 *	here through various C magic.
+		 */
+		if (home->tls->client_hostname) {
+			(void) SSL_set_tlsext_host_name(sock->ssn->ssl, (void *) (uintptr_t) home->tls->client_hostname);
+		}
+
+#ifdef WITH_RADIUSV11
+		this->radiusv11 = home->tls->radiusv11;
+#endif
+
+		this->nonblock |= home->nonblock;
+
+		/*
+		 *	Set non-blocking if it's configured.
+		 */
+		if (this->nonblock) {
+			if (fr_nonblock(this->fd) < 0) {
+				ERROR("(TLS) Failed setting nonblocking for proxy socket '%s' - %s", buffer, fr_strerror());
+				goto error;
+			}
+
+			rad_assert(home->listeners != NULL);
+
+			if (!rbtree_insert(home->listeners, this)) {
+				ERROR("(TLS) Failed adding tracking informtion for proxy socket '%s'", buffer);
+				goto error;
+			}
+
+#ifdef TCP_NODELAY
+			/*
+			 *	Also set TCP_NODELAY, to force the data to be written quickly.
+			 */
+			if (sock->proto == IPPROTO_TCP) {
+				int on = 1;
+
+				if (setsockopt(this->fd, SOL_TCP, TCP_NODELAY, &on, sizeof(on)) < 0) {
+					ERROR("(TLS) Failed to set TCP_NODELAY: %s", fr_syserror(errno));
+					goto error;
+				}
+			}
+#endif
+		}
+
+		/*
+		 *	This is blocking.  :(
+		 */
+		sock->ssn = tls_new_client_session(sock, home->tls, this->fd, &sock->certs);
+		if (!sock->ssn) {
+			ERROR("(TLS) Failed opening connection on proxy socket '%s'", buffer);
+			goto error;
+		}
+
+#ifdef WITH_RADIUSV11
+		/*
+		 *	Must not have alpn_checked yet.  This code only runs for blocking sockets.
+		 */
+		if (sock->ssn->connected && (fr_radiusv11_client_get_alpn(this) < 0)) {
+			goto error;
+		}
+#endif
+
+		sock->connect_timeout = home->connect_timeout;
+
+		this->recv = proxy_tls_recv;
+		this->proxy_send = proxy_tls_send;
+
+#ifdef HAVE_PTHREAD_H
+		if (pthread_mutex_init(&sock->mutex, NULL) < 0) {
+			rad_assert(0 == 1);
+			listen_free(&this);
+			return 0;
+		}
+#endif
+
+		/*
+		 *	Make sure that this listener is associated with the home server.
+		 *
+		 *	Since it's TCP+TLS, this socket can only be associated with one home server.
+		 */
+
+#ifdef WITH_COA_TUNNEL
+		if (home->recv_coa) {
+			RADCLIENT *client;
+
+			this->send_coa = true;
+
+			/*
+			 *	Don't set this->send_coa, as we are
+			 *	not sending CoA-Request packets to
+			 *	this home server.  Instead, we are
+			 *	receiving CoA packets from this home
+			 *	server.
+			 */
+			this->send = proxy_tls_send_reply;
+			this->encode = master_listen[RAD_LISTEN_AUTH].encode;
+			this->decode = master_listen[RAD_LISTEN_AUTH].decode;
+
+			/*
+			 *	Automatically create a client for this
+			 *	home server.  There MAY be one already
+			 *	one for that IP in the configuration
+			 *	files, but there's no guarantee that
+			 *	it exists.
+			 *
+			 *	The only real reason to use an
+			 *	existing client is to track various
+			 *	statistics.
+			 */
+			sock->client = client = talloc_zero(sock, RADCLIENT);
+			client->ipaddr = sock->other_ipaddr;
+			client->src_ipaddr = sock->my_ipaddr;
+			client->longname = client->shortname = talloc_typed_strdup(client, home->name);
+			client->secret = talloc_typed_strdup(client, home->secret);
+			client->nas_type = "none";
+			client->server = talloc_typed_strdup(client, home->recv_coa_server);
+		}
+#endif
+	}
+#endif
+#endif
+	/*
+	 *	Figure out which port we were bound to.
+	 */
+	if (sock->my_port == 0) {
+		struct sockaddr_storage	src;
+		socklen_t		sizeof_src = sizeof(src);
+
+		memset(&src, 0, sizeof_src);
+		if (getsockname(this->fd, (struct sockaddr *) &src,
+				&sizeof_src) < 0) {
+			ERROR("Failed getting socket name for '%s': %s",
+			      buffer, fr_syserror(errno));
+		error:
+			close(this->fd);
+			home->last_failed_open = now;
+			listen_free(&this);
+			return NULL;
+		}
+
+		if (!fr_sockaddr2ipaddr(&src, sizeof_src,
+					&sock->my_ipaddr, &sock->my_port)) {
+			ERROR("Socket has unsupported address family for '%s'", buffer);
+			goto error;
+		}
+
+		this->print(this, buffer, sizeof(buffer));
+	}
+
+	if (rad_debug_lvl >= 3) {
+		DEBUG("Opened new proxy socket '%s'", buffer);
+	}
+
+	home->limit.num_connections++;
+
+	return this;
+}
+#endif
+
+static const FR_NAME_NUMBER listen_compare[] = {
+#ifdef WITH_STATS
+	{ "status",	RAD_LISTEN_NONE },
+#endif
+	{ "auth",	RAD_LISTEN_AUTH },
+#ifdef WITH_COA_TUNNEL
+	{ "auth+coa",	RAD_LISTEN_AUTH },
+#endif
+#ifdef WITH_ACCOUNTING
+	{ "acct",	RAD_LISTEN_ACCT },
+	{ "auth+acct",	RAD_LISTEN_AUTH },
+#ifdef WITH_COA_TUNNEL
+	{ "auth+acct+coa",	RAD_LISTEN_AUTH },
+#endif
+#endif
+#ifdef WITH_DETAIL
+	{ "detail",	RAD_LISTEN_DETAIL },
+#endif
+#ifdef WITH_PROXY
+	{ "proxy",	RAD_LISTEN_PROXY },
+#endif
+#ifdef WITH_VMPS
+	{ "vmps",	RAD_LISTEN_VQP },
+#endif
+#ifdef WITH_DHCP
+	{ "dhcp",	RAD_LISTEN_DHCP },
+#endif
+#ifdef WITH_COMMAND_SOCKET
+	{ "control",	RAD_LISTEN_COMMAND },
+#endif
+#ifdef WITH_COA
+	{ "coa",	RAD_LISTEN_COA },
+#endif
+	{ NULL, 0 },
+};
+
+static int _free_proto_handle(fr_dlhandle *handle)
+{
+	dlclose(*handle);
+	return 0;
+}
+
+static rad_listen_t *listen_parse(CONF_SECTION *cs, char const *server)
+{
+	int		type, rcode;
+	char const	*listen_type;
+	rad_listen_t	*this;
+	CONF_PAIR	*cp;
+	char const	*value;
+	fr_dlhandle	handle;
+	CONF_SECTION	*server_cs;
+	char const	*p;
+	char		buffer[32];
+
+	cp = cf_pair_find(cs, "type");
+	if (!cp) {
+		cf_log_err_cs(cs,
+			   "No type specified in listen section");
+		return NULL;
+	}
+
+	value = cf_pair_value(cp);
+	if (!value) {
+		cf_log_err_cp(cp,
+			      "Type cannot be empty");
+		return NULL;
+	}
+
+	snprintf(buffer, sizeof(buffer), "proto_%s", value);
+	handle = fr_dlopenext(buffer);
+	if (handle) {
+		fr_protocol_t	*proto;
+		fr_dlhandle	*marker;
+
+		proto = dlsym(handle, buffer);
+		if (!proto) {
+#if 0
+			cf_log_err_cs(cs,
+				      "Failed linking to protocol %s : %s\n",
+				      value, dlerror());
+#endif
+			dlclose(handle);
+			return NULL;
+		}
+
+		type = fr_str2int(listen_compare, value, -1);
+		rad_assert(type >= 0); /* shouldn't be able to compile an invalid type */
+
+		memcpy(&master_listen[type], proto, sizeof(*proto));
+
+		/*
+		 *	Ensure handle gets closed if config section gets freed
+		 */
+		marker = talloc(cs, fr_dlhandle);
+		*marker = handle;
+		talloc_set_destructor(marker, _free_proto_handle);
+
+		if (master_listen[type].magic !=  RLM_MODULE_INIT) {
+			ERROR("Failed to load protocol '%s', it has the wrong version.",
+			       master_listen[type].name);
+			return NULL;
+		}
+	}
+
+	cf_log_info(cs, "listen {");
+
+	listen_type = NULL;
+	rcode = cf_item_parse(cs, "type", FR_ITEM_POINTER(PW_TYPE_STRING, &listen_type), "");
+	if (rcode < 0) return NULL;
+	if (rcode == 1) {
+		cf_log_err_cs(cs,
+			   "No type specified in listen section");
+		return NULL;
+	}
+
+	type = fr_str2int(listen_compare, listen_type, -1);
+	if (type < 0) {
+		cf_log_err_cs(cs,
+			   "Invalid type \"%s\" in listen section.",
+			   listen_type);
+		return NULL;
+	}
+
+	/*
+	 *	DHCP and VMPS *must* be loaded dynamically.
+	 */
+	if (master_listen[type].magic !=  RLM_MODULE_INIT) {
+		ERROR("Cannot load protocol '%s', as the required library does not exist",
+		      master_listen[type].name);
+		return NULL;
+	}
+
+	/*
+	 *	Allow listen sections in the default config to
+	 *	refer to a server.
+	 */
+	if (!server) {
+		rcode = cf_item_parse(cs, "virtual_server", FR_ITEM_POINTER(PW_TYPE_STRING, &server), NULL);
+		if (rcode < 0) return NULL;
+	}
+
+#ifdef WITH_PROXY
+	/*
+	 *	We were passed a virtual server, so the caller is
+	 *	defining a proxy listener inside of a virtual server.
+	 *	This isn't allowed right now.
+	 */
+	else if (type == RAD_LISTEN_PROXY) {
+		ERROR("Error: listen type \"proxy\" Cannot appear in a virtual server section");
+		return NULL;
+	}
+#endif
+
+	/*
+	 *	Set up cross-type data.
+	 */
+	this = listen_alloc(cs, type);
+	this->server = server;
+	this->fd = -1;
+
+#ifdef WITH_TCP
+	/*
+	 *	Add special flags '+' for "auth+acct".
+	 */
+	p = strchr(listen_type, '+');
+	if (p) {
+		if (strncmp(p + 1, "acct", 4) == 0) {
+			this->dual = true;
+#ifdef WITH_COA_TUNNEL
+			p += 5;
+		}
+
+		if (strcmp(p, "+coa") == 0) {
+			this->send_coa = true;
+#endif
+		}
+	}
+#endif
+
+	/*
+	 *	Call per-type parser.
+	 */
+	if (master_listen[type].parse(cs, this) < 0) {
+		listen_free(&this);
+		return NULL;
+	}
+
+	server_cs = cf_section_sub_find_name2(main_config.config, "server",
+					      this->server);
+	if (!server_cs && this->server) {
+		cf_log_err_cs(cs, "No such server \"%s\"", this->server);
+		listen_free(&this);
+		return NULL;
+	}
+
+#ifdef WITH_COA_TUNNEL
+	if (this->send_coa) {
+		CONF_SECTION	*coa;
+
+		if (!this->tls) {
+			cf_log_err_cs(cs, "TLS is required in order to use \"+coa\"");
+			listen_free(&this);
+			return NULL;
+		}
+
+		/*
+		 *	Parse the configuration if it exists.
+		 */
+		coa = cf_section_sub_find(cs, "coa");
+		if (coa) {
+			rcode = cf_section_parse(cs, this, coa_config);
+			if (rcode < 0) {
+				listen_free(&this);
+				return NULL;
+			}
+		}
+
+		/*
+		 *	Use the same boundary checks as for home
+		 *	server. See realm_home_server_sanitize().
+		 */
+		FR_INTEGER_BOUND_CHECK("coa_irt", this->coa_irt, >=, 1);
+		FR_INTEGER_BOUND_CHECK("coa_irt", this->coa_irt, <=, 5);
+
+		FR_INTEGER_BOUND_CHECK("coa_mrc", this->coa_mrc, <=, 20);
+
+		FR_INTEGER_BOUND_CHECK("coa_mrt", this->coa_mrt, <=, 30);
+
+		FR_INTEGER_BOUND_CHECK("coa_mrd", this->coa_mrd, >=, 5);
+		FR_INTEGER_BOUND_CHECK("coa_mrd", this->coa_mrd, <=, 60);
+	}
+#endif	/* WITH_COA_TUNNEL */
+
+	cf_log_info(cs, "}");
+
+	return this;
+}
+
+#ifdef HAVE_PTHREAD_H
+/*
+ *	A child thread which does NOTHING other than read and process
+ *	packets.
+ */
+static void *recv_thread(void *arg)
+{
+	rad_listen_t *this = arg;
+
+	while (1) {
+		this->recv(this);
+	}
+
+	return NULL;
+}
+#endif
+
+
+/*
+ *	Generate a list of listeners.  Takes an input list of
+ *	listeners, too, so we don't close sockets with waiting packets.
+ */
+int listen_init(CONF_SECTION *config, rad_listen_t **head, bool spawn_flag)
+{
+	bool		override = false;
+	CONF_SECTION	*cs = NULL;
+	rad_listen_t	**last;
+	rad_listen_t	*this;
+	fr_ipaddr_t	server_ipaddr;
+	uint16_t	auth_port = 0;
+
+	/*
+	 *	We shouldn't be called with a pre-existing list.
+	 */
+	rad_assert(head && (*head == NULL));
+
+	memset(&server_ipaddr, 0, sizeof(server_ipaddr));
+
+	last = head;
+	server_ipaddr.af = AF_UNSPEC;
+
+	/*
+	 *	If the port is specified on the command-line,
+	 *	it over-rides the configuration file.
+	 *
+	 *	FIXME: If argv[0] == "vmpsd", then don't listen on auth/acct!
+	 */
+	if (main_config.port > 0) {
+		auth_port = main_config.port;
+
+		/*
+		 *	-p X but no -i Y on the command-line.
+		 */
+		if (main_config.myip.af == AF_UNSPEC) {
+			ERROR("The command-line says \"-p %d\", but there is no associated IP address to use",
+			      main_config.port);
+			return -1;
+		}
+	}
+
+	/*
+	 *	If the IP address was configured on the command-line,
+	 *	use that as the "bind_address"
+	 */
+	if (main_config.myip.af != AF_UNSPEC) {
+		listen_socket_t *sock;
+
+		memcpy(&server_ipaddr, &main_config.myip,
+		       sizeof(server_ipaddr));
+		override = true;
+
+#ifdef WITH_VMPS
+		if (strcmp(main_config.name, "vmpsd") == 0) {
+			this = listen_alloc(config, RAD_LISTEN_VQP);
+			if (!auth_port) auth_port = 1589;
+		} else
+#endif
+			this = listen_alloc(config, RAD_LISTEN_AUTH);
+
+		sock = this->data;
+
+		sock->my_ipaddr = server_ipaddr;
+		sock->my_port = auth_port;
+
+		sock->clients = client_list_parse_section(config, false);
+		if (!sock->clients) {
+			cf_log_err_cs(config,
+				   "Failed to find any clients for this listen section");
+			listen_free(&this);
+			return -1;
+		}
+
+		if (listen_bind(this) < 0) {
+			listen_free(head);
+			ERROR("There appears to be another RADIUS server running on the authentication port %d", sock->my_port);
+			listen_free(&this);
+			return -1;
+		}
+		auth_port = sock->my_port;	/* may have been updated in listen_bind */
+		if (override) {
+			cs = cf_section_sub_find_name2(config, "server",
+						       main_config.name);
+			if (cs) this->server = main_config.name;
+		}
+
+		*last = this;
+		last = &(this->next);
+
+#ifdef WITH_VMPS
+		/*
+		 *	No acct for vmpsd
+		 */
+		if (strcmp(main_config.name, "vmpsd") == 0) goto add_sockets;
+#endif
+
+#ifdef WITH_ACCOUNTING
+		/*
+		 *	Open Accounting Socket.
+		 *
+		 *	If we haven't already gotten acct_port from
+		 *	/etc/services, then make it auth_port + 1.
+		 */
+		this = listen_alloc(config, RAD_LISTEN_ACCT);
+		sock = this->data;
+
+		/*
+		 *	Create the accounting socket.
+		 *
+		 *	The accounting port is always the
+		 *	authentication port + 1
+		 */
+		sock->my_ipaddr = server_ipaddr;
+		sock->my_port = auth_port + 1;
+
+		sock->clients = client_list_parse_section(config, false);
+		if (!sock->clients) {
+			cf_log_err_cs(config,
+				   "Failed to find any clients for this listen section");
+			return -1;
+		}
+
+		if (listen_bind(this) < 0) {
+			listen_free(&this);
+			listen_free(head);
+			ERROR("There appears to be another RADIUS server running on the accounting port %d", sock->my_port);
+			return -1;
+		}
+
+		if (override) {
+			cs = cf_section_sub_find_name2(config, "server",
+						       main_config.name);
+			if (cs) this->server = main_config.name;
+		}
+
+		*last = this;
+		last = &(this->next);
+#endif
+	}
+
+	/*
+	 *	They specified an IP on the command-line, ignore
+	 *	all listen sections except the one in '-n'.
+	 */
+	if (main_config.myip.af != AF_UNSPEC) {
+		CONF_SECTION *subcs;
+		char const *name2 = cf_section_name2(cs);
+
+		cs = cf_section_sub_find_name2(config, "server",
+					       main_config.name);
+		if (!cs) goto add_sockets;
+
+		/*
+		 *	Should really abstract this code...
+		 */
+		for (subcs = cf_subsection_find_next(cs, NULL, "listen");
+		     subcs != NULL;
+		     subcs = cf_subsection_find_next(cs, subcs, "listen")) {
+			this = listen_parse(subcs, name2);
+			if (!this) {
+				listen_free(head);
+				return -1;
+			}
+
+			*last = this;
+			last = &(this->next);
+		} /* loop over "listen" directives in server <foo> */
+
+		goto add_sockets;
+	}
+
+	/*
+	 *	Walk through the "listen" sections, if they exist.
+	 */
+	for (cs = cf_subsection_find_next(config, NULL, "listen");
+	     cs != NULL;
+	     cs = cf_subsection_find_next(config, cs, "listen")) {
+		this = listen_parse(cs, NULL);
+		if (!this) {
+			listen_free(head);
+			return -1;
+		}
+
+		*last = this;
+		last = &(this->next);
+	}
+
+	/*
+	 *	Check virtual servers for "listen" sections, too.
+	 *
+	 *	FIXME: Move to virtual server init?
+	 */
+	for (cs = cf_subsection_find_next(config, NULL, "server");
+	     cs != NULL;
+	     cs = cf_subsection_find_next(config, cs, "server")) {
+		CONF_SECTION *subcs;
+		char const *name2 = cf_section_name2(cs);
+
+		for (subcs = cf_subsection_find_next(cs, NULL, "listen");
+		     subcs != NULL;
+		     subcs = cf_subsection_find_next(cs, subcs, "listen")) {
+			this = listen_parse(subcs, name2);
+			if (!this) {
+				listen_free(head);
+				return -1;
+			}
+
+			*last = this;
+			last = &(this->next);
+		} /* loop over "listen" directives in virtual servers */
+	} /* loop over virtual servers */
+
+add_sockets:
+	/*
+	 *	No sockets to receive packets, this is an error.
+	 *	proxying is pointless.
+	 */
+	if (!*head) {
+		ERROR("The server is not configured to listen on any ports.  Cannot start");
+		return -1;
+	}
+
+	/*
+	 *	Print out which sockets we're listening on, and
+	 *	add them to the event list.
+	 */
+	for (this = *head; this != NULL; this = this->next) {
+#ifdef WITH_TLS
+		if (!check_config && !spawn_flag && this->tls) {
+			cf_log_err_cs(this->cs, "Threading must be enabled for TLS sockets to function properly");
+			cf_log_err_cs(this->cs, "You probably need to do '%s -fxx -l stdout' for debugging",
+				      main_config.name);
+			return -1;
+		}
+#endif
+		if (!check_config) {
+			if (this->workers && !spawn_flag) {
+				WARN("Setting 'workers' requires 'synchronous'.  Disabling 'workers'");
+				this->workers = 0;
+			}
+
+			if (this->workers) {
+#ifdef HAVE_PTHREAD_H
+				int rcode;
+				uint32_t i;
+				char buffer[256];
+
+				this->print(this, buffer, sizeof(buffer));
+
+				for (i = 0; i < this->workers; i++) {
+					pthread_t id;
+
+					/*
+					 *	FIXME: create detached?
+					 */
+					rcode = pthread_create(&id, 0, recv_thread, this);
+					if (rcode != 0) {
+						ERROR("Thread create failed: %s",
+						      fr_syserror(rcode));
+						fr_exit(1);
+					}
+
+					DEBUG("Thread %d for %s\n", i, buffer);
+				}
+#else
+				WARN("Setting 'workers' requires 'synchronous'.  Disabling 'workers'");
+				this->workers = 0;
+#endif
+
+			} else {
+				radius_update_listener(this);
+			}
+
+		}
+	}
+
+	/*
+	 *	Haven't defined any sockets.  Die.
+	 */
+	if (!*head) return -1;
+
+#ifdef WITH_COA_TUNNEL
+	if (listen_coa_init() < 0) return -1;
+#endif
+
+	return 0;
+}
+
+/*
+ *	Free a linked list of listeners;
+ */
+void listen_free(rad_listen_t **head)
+{
+	rad_listen_t *this;
+
+	if (!head || !*head) return;
+
+	this = *head;
+	while (this) {
+		rad_listen_t *next = this->next;
+		talloc_free(this);
+		this = next;
+	}
+
+	*head = NULL;
+}
+
+#ifdef WITH_STATS
+RADCLIENT_LIST *listener_find_client_list(fr_ipaddr_t const *ipaddr, uint16_t port, int proto)
+{
+	rad_listen_t *this;
+
+	for (this = main_config.listen; this != NULL; this = this->next) {
+		listen_socket_t *sock;
+
+		if ((this->type != RAD_LISTEN_AUTH)
+#ifdef WITH_ACCOUNTING
+		    && (this->type != RAD_LISTEN_ACCT)
+#endif
+#ifdef WITH_COA
+		    && (this->type != RAD_LISTEN_COA)
+#endif
+		    ) continue;
+
+		sock = this->data;
+
+		if (sock->my_port != port) continue;
+		if (sock->proto != proto) continue;
+		if (fr_ipaddr_cmp(ipaddr, &sock->my_ipaddr) != 0) continue;
+
+		return sock->clients;
+	}
+
+	return NULL;
+}
+#endif
+
+rad_listen_t *listener_find_byipaddr(fr_ipaddr_t const *ipaddr, uint16_t port, int proto)
+{
+	rad_listen_t *this;
+
+	for (this = main_config.listen; this != NULL; this = this->next) {
+		listen_socket_t *sock;
+
+		sock = this->data;
+
+		if (sock->my_port != port) continue;
+		if (sock->proto != proto) continue;
+		if (fr_ipaddr_cmp(ipaddr, &sock->my_ipaddr) != 0) continue;
+
+		return this;
+	}
+
+	/*
+	 *	Failed to find a specific one.  Find INADDR_ANY
+	 */
+	for (this = main_config.listen; this != NULL; this = this->next) {
+		listen_socket_t *sock;
+
+		sock = this->data;
+
+		if (sock->my_port != port) continue;
+		if (sock->proto != proto) continue;
+		if (!fr_inaddr_any(&sock->my_ipaddr)) continue;
+
+		return this;
+	}
+
+	return NULL;
+}
+
+#ifdef WITH_COA_TUNNEL
+/*
+ *	This is easier than putting ifdef's everywhere.  And
+ *	realistically, there aren't many systems which have OpenSSL,
+ *	but not pthreads.
+ */
+#ifndef HAVE_PTHREAD_H
+#error CoA tunnels require pthreads
+#endif
+
+#include <pthread.h>
+
+static rbtree_t *coa_tree = NULL;
+
+/*
+ *	We have an RB tree of keys, and within each key, a hash table
+ *	of one or more listeners associated with that key.
+ */
+typedef struct {
+	char const     	*key;
+	fr_hash_table_t	*ht;
+
+	pthread_mutex_t	mutex;		/* per key, to lower contention */
+} coa_key_t;
+
+typedef struct {
+	coa_key_t	*coa_key;
+	rad_listen_t	*listener;
+} coa_entry_t;
+
+static int coa_key_cmp(void const *one, void const *two)
+{
+	coa_key_t const *a = one;
+	coa_key_t const *b = two;
+
+	return strcmp(a->key, b->key);
+}
+
+static void coa_key_free(void *data)
+{
+	coa_key_t *coa_key = data;
+
+	pthread_mutex_destroy(&coa_key->mutex);
+	fr_hash_table_free(coa_key->ht);
+	talloc_free(coa_key);
+}
+
+static uint32_t coa_entry_hash(void const *data)
+{
+	coa_entry_t const *a = (coa_entry_t const *) data;
+
+	return fr_hash(&a->listener, sizeof(a->listener));
+}
+
+static int coa_entry_cmp(void const *one, void const *two)
+{
+	coa_entry_t const *a = one;
+	coa_entry_t const *b = two;
+
+	return memcmp(&a->listener, &b->listener, sizeof(a->listener));
+}
+
+/*
+ *	Delete the entry, without holding the parents lock.
+ */
+static void coa_entry_free(void *data)
+{
+	talloc_free(data);
+}
+
+static int coa_entry_destructor(coa_entry_t *entry)
+{
+	pthread_mutex_lock(&entry->coa_key->mutex);
+	fr_hash_table_delete(entry->coa_key->ht, entry);
+	pthread_mutex_unlock(&entry->coa_key->mutex);
+
+	return 0;
+}
+
+static int listen_coa_init(void)
+{
+	/*
+	 *	We will be looking up listeners by key.  Each key
+	 *	points us to a list of listeners.  Each key has it's
+	 *	own mutex, so that it's thread-safe.
+	 */
+	coa_tree = rbtree_create(NULL, coa_key_cmp, coa_key_free, RBTREE_FLAG_LOCK);
+	if (!coa_tree) {
+		ERROR("Failed creating internal tracking tree for Originating-Realm-Key");
+		return -1;
+	}
+
+	return 0;
+}
+
+void listen_coa_free(void)
+{
+	/*
+	 *	If we are freeing the tree, then all of the listeners
+	 *	must have been freed first.
+	 */
+	rad_assert(rbtree_num_elements(coa_tree) == 0);
+	rbtree_free(coa_tree);
+	coa_tree = NULL;
+}
+
+/*
+ *	Adds a listener to the hash of listeners, based on key.
+ */
+void listen_coa_add(rad_listen_t *this, char const *key)
+{
+	int tries = 0;
+	coa_key_t my_key, *coa_key;
+	coa_entry_t *entry;
+
+	rad_assert(this->send_coa);
+	rad_assert(this->parent);
+	rad_assert(!this->key);
+
+	/*
+	 *	Find the key.  If we can't find it, then create it.
+	 */
+	my_key.key = key;
+
+retry:
+	coa_key = rbtree_finddata(coa_tree, &my_key);
+	if (!coa_key) {
+		coa_key = talloc_zero(NULL, coa_key_t);
+		if (!coa_key) return;
+		coa_key->key = talloc_strdup(coa_key, key);
+		if (!coa_key->key) {
+		fail:
+			talloc_free(coa_key);
+			return;
+		}
+
+		/*
+		 *	Create the hash table of listeners.
+		 */
+		coa_key->ht = fr_hash_table_create(coa_entry_hash, coa_entry_cmp, coa_entry_free);
+		if (!coa_key->ht) goto fail;
+
+		if (!rbtree_insert(coa_tree, coa_key)) {
+			talloc_free(coa_key);
+
+			/*
+			 *	The lookups are mutex protected, but
+			 *	if there's time between the lookup and
+			 *	the insert, another thread may have
+			 *	created the node.  In which case we
+			 *	try again.
+			 */
+			if (tries < 3) goto retry;
+			tries++;
+			return;
+		}
+
+		(void) pthread_mutex_init(&coa_key->mutex, NULL);
+	}
+
+	/*
+	 *	No need to strdup() this, coa_key will only be removed
+	 *	after the listener has been removed.
+	 */
+	if (!this->key) this->key = coa_key->key;
+
+	entry = talloc_zero(this, coa_entry_t);
+	if (!entry) return;
+	talloc_set_destructor(entry, coa_entry_destructor);
+
+	entry->coa_key = coa_key;
+	entry->listener = this;
+
+	/*
+	 *	Insert the entry into the hash table.
+	 */
+	pthread_mutex_lock(&coa_key->mutex);
+	fr_hash_table_insert(coa_key->ht, entry);
+	pthread_mutex_unlock(&coa_key->mutex);
+}
+
+/*
+ *	Find an active listener by key.
+ *
+ *	This function will update request->home_server, and
+ *	request->proxy_listener.
+ */
+int listen_coa_find(REQUEST *request, char const *key)
+{
+	coa_key_t my_key, *coa_key;
+	rad_listen_t *this, *found;
+	listen_socket_t *sock;
+	fr_hash_iter_t iter;
+
+	/*
+	 *	Find the key.  If we can't find it, then error out.
+	 */
+	memcpy(&my_key.key, &key, sizeof(key)); /* const issues */
+	coa_key = rbtree_finddata(coa_tree, &my_key);
+	if (!coa_key) return -1;
+
+	/*
+	 *	We've found it.  Now find a listener which has free
+	 *	IDs.  i.e. where the number of used IDs is less tahn
+	 *	256.
+	 */
+	found = NULL;
+	pthread_mutex_lock(&coa_key->mutex);
+	for (this = fr_hash_table_iter_init(coa_key->ht, &iter);
+	     this != NULL;
+	     this = fr_hash_table_iter_next(coa_key->ht, &iter)) {
+		if (this->blocked) continue;
+
+		if (this->dead) continue;
+
+		if (!found) {
+			if (this->num_ids_used < 256) {
+				found = this;
+			}
+
+			/*
+			 *	Skip listeners which have all used IDs.
+			 */
+			continue;
+		}
+
+		/*
+		 *	Try to spread the load across all available
+		 *	sockets.
+		 */
+		if (found->num_ids_used > this->num_ids_used) {
+			found = this;
+			continue;
+		}
+
+		/*
+		 *	If they are equal, pick one at random.
+		 *
+		 *	@todo - pick one with equal probability from
+		 *	among the ones with the same IDs used.  This
+		 *	algorithm prefers the first one.
+		 */
+		if (found->num_ids_used == this->num_ids_used) {
+			if ((fr_rand() & 0x01) == 0) {
+				found = this;
+				continue;
+			}
+		}
+	}
+
+	pthread_mutex_unlock(&coa_key->mutex);
+	if (!found) return -1;
+
+	request->proxy_listener = found;
+
+	sock = found->data;
+	request->home_server = sock->home;
+	return 0;
+}
+
+/*
+ *	Check for an active listener by key.
+ */
+static bool listen_coa_exists(rad_listen_t *this, char const *key)
+{
+	coa_key_t my_key, *coa_key;
+	coa_entry_t my_entry, *entry;
+
+	/*
+	 *	Find the key.  If we can't find it, then error out.
+	 */
+	memcpy(&my_key.key, &key, sizeof(key)); /* const issues */
+	coa_key = rbtree_finddata(coa_tree, &my_key);
+	if (!coa_key) return false;
+
+	my_entry.listener = this;
+	pthread_mutex_lock(&coa_key->mutex);
+	entry = fr_hash_table_finddata(coa_key->ht, &my_entry);
+	pthread_mutex_unlock(&coa_key->mutex);
+
+	return (entry != NULL);
+}
+
+/*
+ *	Delete a listener entry.
+ */
+static void listen_coa_delete(rad_listen_t *this, char const *key)
+{
+	coa_key_t my_key, *coa_key;
+	coa_entry_t my_entry;
+
+	/*
+	 *	Find the key.  If we can't find it, then error out.
+	 */
+	memcpy(&my_key.key, &key, sizeof(key)); /* const issues */
+	coa_key = rbtree_finddata(coa_tree, &my_key);
+	if (!coa_key) return;
+
+	my_entry.listener = this;
+	pthread_mutex_lock(&coa_key->mutex);
+	(void) fr_hash_table_delete(coa_key->ht, &my_entry);
+	pthread_mutex_unlock(&coa_key->mutex);
+}
+
+
+static void listener_coa_update(rad_listen_t *this, VALUE_PAIR *vps)
+{
+	VALUE_PAIR *vp;
+	vp_cursor_t cursor;
+
+	fr_cursor_init(&cursor, &vps);
+
+	/*
+	 *	Add or delete Operator-Name realms
+	 */
+	while ((vp = fr_cursor_next_by_num(&cursor, PW_OPERATOR_NAME, 0, TAG_ANY)) != NULL) {
+		if (vp->vp_length <= 1) continue;
+
+		if (vp->vp_strvalue[0] == '+') {
+			if (listen_coa_exists(this, vp->vp_strvalue)) continue;
+
+			listen_coa_add(this, vp->vp_strvalue);
+			continue;
+		}
+
+		if (vp->vp_strvalue[0] == '-') {
+			listen_coa_delete(this, vp->vp_strvalue);
+			continue;
+		}
+	}
+}
+#endif
diff --git a/src/main/log.c b/src/main/log.c
new file mode 100644
index 0000000..1ca2f91
--- /dev/null
+++ b/src/main/log.c
@@ -0,0 +1,923 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @brief Logging functions used by the server core.
+ * @file main/log.c
+ *
+ * @copyright 2000,2006  The FreeRADIUS server project
+ * @copyright 2000  Miquel van Smoorenburg <miquels@cistron.nl>
+ * @copyright 2000  Alan DeKok <aland@ox.org>
+ * @copyright 2001  Chad Miller <cmiller@surfsouth.com>
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/rad_assert.h>
+
+#ifdef HAVE_SYS_STAT_H
+#  include <sys/stat.h>
+#endif
+
+#include <fcntl.h>
+
+#ifdef HAVE_SYSLOG_H
+#  include <syslog.h>
+#endif
+
+#include <sys/file.h>
+
+#ifdef HAVE_PTHREAD_H
+#include <pthread.h>
+#endif
+
+log_lvl_t	rad_debug_lvl = 0;		//!< Global debugging level
+static bool	rate_limit = true;		//!< Whether repeated log entries should be rate limited
+
+/** Maps log categories to message prefixes
+ */
+static const FR_NAME_NUMBER levels[] = {
+	{ ": Debug: ",		L_DBG		},
+	{ ": Auth: ",		L_AUTH		},
+	{ ": Proxy: ",		L_PROXY		},
+	{ ": Info: ",		L_INFO		},
+	{ ": Warning: ",	L_WARN		},
+	{ ": Acct: ",		L_ACCT		},
+	{ ": Error: ",		L_ERR		},
+	{ ": WARNING: ",	L_DBG_WARN	},
+	{ ": ERROR: ",		L_DBG_ERR	},
+	{ ": WARNING: ",	L_DBG_WARN_REQ	},
+	{ ": ERROR: ",		L_DBG_ERR_REQ	},
+	{ NULL, 0 }
+};
+
+/** @name VT100 escape sequences
+ *
+ * These sequences may be written to VT100 terminals to change the
+ * colour and style of the text.
+ *
+ @code{.c}
+   fprintf(stdout, VTC_RED "This text will be coloured red" VTC_RESET);
+ @endcode
+ * @{
+ */
+#define VTC_RED		"\x1b[31m"	//!< Colour following text red.
+#define VTC_YELLOW      "\x1b[33m"	//!< Colour following text yellow.
+#define VTC_BOLD	"\x1b[1m"	//!< Embolden following text.
+#define VTC_RESET	"\x1b[0m"	//!< Reset terminal text to default style/colour.
+/** @} */
+
+/** Maps log categories to VT100 style/colour escape sequences
+ */
+static const FR_NAME_NUMBER colours[] = {
+	{ "",			L_DBG		},
+	{ VTC_BOLD,		L_AUTH		},
+	{ VTC_BOLD,		L_PROXY		},
+	{ VTC_BOLD,		L_INFO		},
+	{ VTC_BOLD,		L_ACCT		},
+	{ VTC_RED,		L_ERR		},
+	{ VTC_BOLD VTC_YELLOW,	L_WARN		},
+	{ VTC_BOLD VTC_RED,	L_DBG_ERR	},
+	{ VTC_BOLD VTC_YELLOW,	L_DBG_WARN	},
+	{ VTC_BOLD VTC_RED,	L_DBG_ERR_REQ	},
+	{ VTC_BOLD VTC_YELLOW,	L_DBG_WARN_REQ	},
+	{ NULL, 0 }
+};
+
+/** Syslog facility table
+ *
+ * Maps syslog facility keywords, to the syslog facility macros defined
+ * in the system's syslog.h.
+ *
+ * @note Not all facilities are supported by every operating system.
+ *       If a facility is unavailable it will not appear in the table.
+ */
+const FR_NAME_NUMBER syslog_facility_table[] = {
+#ifdef LOG_KERN
+	{ "kern",		LOG_KERN	},
+#endif
+#ifdef LOG_USER
+	{ "user",		LOG_USER	},
+#endif
+#ifdef LOG_MAIL
+	{ "mail",		LOG_MAIL	},
+#endif
+#ifdef LOG_DAEMON
+	{ "daemon",		LOG_DAEMON	},
+#endif
+#ifdef LOG_AUTH
+	{ "auth",		LOG_AUTH	},
+#endif
+#ifdef LOG_LPR
+	{ "lpr",		LOG_LPR		},
+#endif
+#ifdef LOG_NEWS
+	{ "news",		LOG_NEWS	},
+#endif
+#ifdef LOG_UUCP
+	{ "uucp",		LOG_UUCP	},
+#endif
+#ifdef LOG_CRON
+	{ "cron",		LOG_CRON	},
+#endif
+#ifdef LOG_AUTHPRIV
+	{ "authpriv",		LOG_AUTHPRIV	},
+#endif
+#ifdef LOG_FTP
+	{ "ftp",		LOG_FTP		},
+#endif
+#ifdef LOG_LOCAL0
+	{ "local0",		LOG_LOCAL0	},
+#endif
+#ifdef LOG_LOCAL1
+	{ "local1",		LOG_LOCAL1	},
+#endif
+#ifdef LOG_LOCAL2
+	{ "local2",		LOG_LOCAL2	},
+#endif
+#ifdef LOG_LOCAL3
+	{ "local3",		LOG_LOCAL3	},
+#endif
+#ifdef LOG_LOCAL4
+	{ "local4",		LOG_LOCAL4	},
+#endif
+#ifdef LOG_LOCAL5
+	{ "local5",		LOG_LOCAL5	},
+#endif
+#ifdef LOG_LOCAL6
+	{ "local6",		LOG_LOCAL6	},
+#endif
+#ifdef LOG_LOCAL7
+	{ "local7",		LOG_LOCAL7	},
+#endif
+	{ NULL,			-1		}
+};
+
+/** Syslog severity table
+ *
+ * Maps syslog severity keywords, to the syslog severity macros defined
+ * in the system's syslog.h file.
+ *
+ */
+const FR_NAME_NUMBER syslog_severity_table[] = {
+#ifdef LOG_EMERG
+	{ "emergency",		LOG_EMERG	},
+#endif
+#ifdef LOG_ALERT
+	{ "alert",		LOG_ALERT	},
+#endif
+#ifdef LOG_CRIT
+	{ "critical",		LOG_CRIT	},
+#endif
+#ifdef LOG_ERR
+	{ "error",		LOG_ERR		},
+#endif
+#ifdef LOG_WARNING
+	{ "warning",		LOG_WARNING	},
+#endif
+#ifdef LOG_NOTICE
+	{ "notice",		LOG_NOTICE	},
+#endif
+#ifdef LOG_INFO
+	{ "info",		LOG_INFO	},
+#endif
+#ifdef LOG_DEBUG
+	{ "debug",		LOG_DEBUG	},
+#endif
+	{ NULL,			-1		}
+};
+
+const FR_NAME_NUMBER log_str2dst[] = {
+	{ "null",		L_DST_NULL	},
+	{ "files",		L_DST_FILES	},
+	{ "syslog",		L_DST_SYSLOG	},
+	{ "stdout",		L_DST_STDOUT	},
+	{ "stderr",		L_DST_STDERR	},
+	{ NULL,			L_DST_NUM_DEST	}
+};
+
+bool log_dates_utc = false;
+
+fr_log_t default_log = {
+	.colourise = false,	//!< Will be set later. Should be off before we do terminal detection.
+	.fd = STDOUT_FILENO,
+	.dst = L_DST_STDOUT,
+	.file = NULL,
+	.debug_file = NULL,
+};
+
+static int stderr_fd = -1;	//!< The original unmolested stderr file descriptor
+static int stdout_fd = -1;	//!< The original unmolested stdout file descriptor
+
+static char const spaces[] = "                                                                                                                        ";
+
+/** On fault, reset STDOUT and STDERR to something useful
+ *
+ * @return 0
+ */
+static int _restore_std(UNUSED int sig)
+{
+	if ((stderr_fd > 0) && (stdout_fd > 0)) {
+		dup2(stderr_fd, STDOUT_FILENO);
+		dup2(stdout_fd, STDERR_FILENO);
+		return 0;
+	}
+
+	if (default_log.fd > 0) {
+		dup2(default_log.fd, STDOUT_FILENO);
+		dup2(default_log.fd, STDERR_FILENO);
+		return 0;
+	}
+
+	return 0;
+}
+
+/** Initialise file descriptors based on logging destination
+ *
+ * @param log Logger to manipulate.
+ * @param daemonize Whether the server is starting as a daemon.
+ * @return 0 on success -1 on failure.
+ */
+int radlog_init(fr_log_t *log, bool daemonize)
+{
+	int devnull;
+
+	rate_limit = daemonize;
+
+	/*
+	 *	If we're running in foreground mode, save STDIN /
+	 *	STDERR as higher FDs, which won't get used by anyone
+	 *	else.  When we fork/exec a program, it's STD FDs will
+	 *	get set to pipes.  We later set STDOUT / STDERR to
+	 *	/dev/null, so that any library trying to write to them
+	 *	doesn't screw anything up.
+	 *
+	 *	Then, when something goes wrong, restore them so that
+	 *	any debugger called from the panic action has access
+	 *	to STDOUT / STDERR.
+	 */
+	if (!daemonize) {
+		fr_fault_set_cb(_restore_std);
+
+		stdout_fd = dup(STDOUT_FILENO);
+		stderr_fd = dup(STDERR_FILENO);
+	}
+
+	devnull = open("/dev/null", O_RDWR);
+	if (devnull < 0) {
+		fr_strerror_printf("Error opening /dev/null: %s", fr_syserror(errno));
+		return -1;
+	}
+
+	/*
+	 *	STDOUT & STDERR go to /dev/null, unless we have "-x",
+	 *	then STDOUT & STDERR go to the "-l log" destination.
+	 *
+	 *	The complexity here is because "-l log" can go to
+	 *	STDOUT or STDERR, too.
+	 */
+	if (log->dst == L_DST_STDOUT) {
+		setlinebuf(stdout);
+		log->fd = STDOUT_FILENO;
+
+		/*
+		 *	If we're debugging, allow STDERR to go to
+		 *	STDOUT too, for executed programs,
+		 */
+		if (rad_debug_lvl) {
+			dup2(STDOUT_FILENO, STDERR_FILENO);
+		} else {
+			dup2(devnull, STDERR_FILENO);
+		}
+
+	} else if (log->dst == L_DST_STDERR) {
+		setlinebuf(stderr);
+		log->fd = STDERR_FILENO;
+
+		/*
+		 *	If we're debugging, allow STDOUT to go to
+		 *	STDERR too, for executed programs,
+		 */
+		if (rad_debug_lvl) {
+			dup2(STDERR_FILENO, STDOUT_FILENO);
+		} else {
+			dup2(devnull, STDOUT_FILENO);
+		}
+
+	} else if (log->dst == L_DST_SYSLOG) {
+		/*
+		 *	Discard STDOUT and STDERR no matter what the
+		 *	status of debugging.  Syslog isn't a file
+		 *	descriptor, so we can't use it.
+		 */
+		dup2(devnull, STDOUT_FILENO);
+		dup2(devnull, STDERR_FILENO);
+
+	} else if (rad_debug_lvl) {
+		/*
+		 *	If we're debugging, allow STDOUT and STDERR to
+		 *	go to the log file.
+		 */
+		dup2(log->fd, STDOUT_FILENO);
+		dup2(log->fd, STDERR_FILENO);
+
+	} else {
+		/*
+		 *	Not debugging, and the log isn't STDOUT or
+		 *	STDERR.  Ensure that we move both of them to
+		 *	/dev/null, so that the calling terminal can
+		 *	exit, and the output from executed programs
+		 *	doesn't pollute STDOUT / STDERR.
+		 */
+		dup2(devnull, STDOUT_FILENO);
+		dup2(devnull, STDERR_FILENO);
+	}
+
+	close(devnull);
+
+	fr_fault_set_log_fd(log->fd);
+
+	return 0;
+}
+
+/** Send a server log message to its destination
+ *
+ * @param type of log message.
+ * @param msg with printf style substitution tokens.
+ * @param ap Substitution arguments.
+ */
+int vradlog(log_type_t type, char const *msg, va_list ap)
+{
+	unsigned char *p;
+	char buffer[10240];	/* The largest config item size, then extra for prefixes and suffixes */
+	char *unsan;
+	size_t len;
+	int colourise = default_log.colourise;
+
+	/*
+	 *	If we don't want any messages, then
+	 *	throw them away.
+	 */
+	if (default_log.dst == L_DST_NULL) {
+		return 0;
+	}
+
+	buffer[0] = '\0';
+	len = 0;
+
+	if (colourise) {
+		len += strlcpy(buffer + len, fr_int2str(colours, type, ""), sizeof(buffer) - len) ;
+		if (len == 0) {
+			colourise = false;
+		}
+	}
+
+	/*
+	 *	Mark the point where we treat the buffer as unsanitized.
+	 */
+	unsan = buffer + len;
+
+	/*
+	 *	Don't print timestamps to syslog, it does that for us.
+	 *	Don't print timestamps and error types for low levels
+	 *	of debugging.
+	 *
+	 *	Print timestamps for non-debugging, and for high levels
+	 *	of debugging.
+	 */
+	if (default_log.dst != L_DST_SYSLOG) {
+		if ((rad_debug_lvl != 1) && (rad_debug_lvl != 2)) {
+			time_t timeval;
+
+			timeval = time(NULL);
+			CTIME_R(&timeval, buffer + len, sizeof(buffer) - len - 1);
+
+			len = strlen(buffer);
+			len += strlcpy(buffer + len, fr_int2str(levels, type, ": "), sizeof(buffer) - len);
+		} else goto add_prefix;
+	} else {
+	add_prefix:
+		if (len < sizeof(buffer)) switch (type) {
+		case L_DBG_WARN:
+			len += strlcpy(buffer + len, "WARNING: ", sizeof(buffer) - len);
+			break;
+
+		case L_DBG_ERR:
+			len += strlcpy(buffer + len, "ERROR: ", sizeof(buffer) - len);
+			break;
+
+		default:
+			break;
+		}
+	}
+
+	if (len < sizeof(buffer)) {
+		vsnprintf(buffer + len, sizeof(buffer) - len - 1, msg, ap);
+		len += strlen(buffer + len);
+	}
+
+	/*
+	 *	Filter out control chars and non UTF8 chars
+	 */
+	for (p = (unsigned char *)unsan; *p != '\0'; p++) {
+		int clen;
+
+		switch (*p) {
+		case '\r':
+		case '\n':
+			*p = ' ';
+			break;
+
+		case '\t':
+			continue;
+
+		default:
+			clen = fr_utf8_char(p, -1);
+			if (!clen) {
+				*p = '?';
+				continue;
+			}
+			p += (clen - 1);
+			break;
+		}
+	}
+
+	if (colourise && (len < sizeof(buffer))) {
+		len += strlcpy(buffer + len, VTC_RESET, sizeof(buffer) - len);
+	}
+
+	if (len < (sizeof(buffer) - 2)) {
+		buffer[len]	= '\n';
+		buffer[len + 1] = '\0';
+	} else {
+		buffer[sizeof(buffer) - 2] = '\n';
+		buffer[sizeof(buffer) - 1] = '\0';
+	}
+
+	switch (default_log.dst) {
+
+#ifdef HAVE_SYSLOG_H
+	case L_DST_SYSLOG:
+		switch (type) {
+		case L_DBG:
+		case L_DBG_WARN:
+		case L_DBG_ERR:
+		case L_DBG_ERR_REQ:
+		case L_DBG_WARN_REQ:
+			type = LOG_DEBUG;
+			break;
+
+		case L_AUTH:
+		case L_PROXY:
+		case L_ACCT:
+			type = LOG_NOTICE;
+			break;
+
+		case L_INFO:
+			type = LOG_INFO;
+			break;
+
+		case L_WARN:
+			type = LOG_WARNING;
+			break;
+
+		case L_ERR:
+			type = LOG_ERR;
+			break;
+		}
+		syslog(type, "%s", buffer);
+		break;
+#endif
+
+	case L_DST_FILES:
+	case L_DST_STDOUT:
+	case L_DST_STDERR:
+		return write(default_log.fd, buffer, strlen(buffer));
+
+	default:
+	case L_DST_NULL:	/* should have been caught above */
+		break;
+	}
+
+	return 0;
+}
+
+/** Send a server log message to its destination
+ *
+ * @param type of log message.
+ * @param msg with printf style substitution tokens.
+ * @param ... Substitution arguments.
+ */
+int radlog(log_type_t type, char const *msg, ...)
+{
+	va_list ap;
+	int r = 0;
+
+	va_start(ap, msg);
+
+	/*
+	 *	Non-debug message, or debugging is enabled.  Log it.
+	 */
+	if (((type & L_DBG) == 0) || (rad_debug_lvl > 0)) {
+		r = vradlog(type, msg, ap);
+	}
+	va_end(ap);
+
+	return r;
+}
+
+/** Send a server log message to its destination without evaluating its debug level
+ *
+ * @param type of log message.
+ * @param msg with printf style substitution tokens.
+ * @param ... Substitution arguments.
+ */
+static int radlog_always(log_type_t type, char const *msg, ...) CC_HINT(format (printf, 2, 3));
+static int radlog_always(log_type_t type, char const *msg, ...)
+{
+	va_list ap;
+	int r;
+
+	va_start(ap, msg);
+	r = vradlog(type, msg, ap);
+	va_end(ap);
+
+	return r;
+}
+
+/** Whether a server debug message should be logged
+ *
+ * @param type of message.
+ * @param lvl of debugging this message should be logged at.
+ * @return true if message should be logged, else false.
+ */
+inline bool debug_enabled(log_type_t type, log_lvl_t lvl)
+{
+	if ((type & L_DBG) && (lvl <= rad_debug_lvl)) return true;
+
+	return false;
+}
+
+/** Whether rate limiting is enabled
+ */
+bool rate_limit_enabled(void)
+{
+	if (rate_limit || (rad_debug_lvl < 1)) return true;
+
+	return false;
+}
+
+/** Whether a request specific debug message should be logged
+ *
+ * @param type of message.
+ * @param lvl of debugging this message should be logged at.
+ * @param request The current request.
+ * @return true if message should be logged, else false.
+ */
+inline bool radlog_debug_enabled(log_type_t type, log_lvl_t lvl, REQUEST *request)
+{
+	/*
+	 *	It's a debug class message, note this doesn't mean it's a debug type message.
+	 *
+	 *	For example it could be a RIDEBUG message, which would be an informational message,
+	 *	instead of an RDEBUG message which would be a debug debug message.
+	 *
+	 *	There is log function, but the request debug level isn't high enough.
+	 *	OR, we're in debug mode, and the global debug level isn't high enough,
+	 *	then don't log the message.
+	 */
+	if ((type & L_DBG) &&
+	    ((request->log.func && (lvl <= request->log.lvl)) ||
+	     ((rad_debug_lvl != 0) && (lvl <= rad_debug_lvl)))) {
+		return true;
+	}
+
+	return false;
+}
+
+/** Send a log message to its destination, possibly including fields from the request
+ *
+ * @param type of log message, #L_ERR, #L_WARN, #L_INFO, #L_DBG.
+ * @param lvl Minimum required server or request level to output this message.
+ * @param request The current request.
+ * @param msg with printf style substitution tokens.
+ * @param ap Substitution arguments.
+ */
+void vradlog_request(log_type_t type, log_lvl_t lvl, REQUEST *request, char const *msg, va_list ap)
+{
+	size_t len = 0;
+	char const *filename = default_log.file;
+	FILE *fp = NULL;
+
+	char buffer[10240];	/* The largest config item size, then extra for prefixes and suffixes */
+
+	char *p;
+	char const *extra = "";
+	uint8_t indent;
+	va_list aq;
+
+	/*
+	 *	Debug messages get treated specially.
+	 */
+	if ((type & L_DBG) != 0) {
+
+		if (!radlog_debug_enabled(type, lvl, request)) {
+			return;
+		}
+
+		/*
+		 *	Use the debug output file, if specified,
+		 *	otherwise leave it as the default log file.
+		 */
+#ifdef WITH_COMMAND_SOCKET
+		filename = default_log.debug_file;
+		if (!filename)
+#endif
+		{
+			filename = default_log.file;
+		}
+	}
+
+	if (filename) {
+		radlog_func_t rl = request->log.func;
+
+		request->log.func = NULL;
+
+		/*
+		 *	This is SLOW!  Doing it for every log message
+		 *	in every request is NOT recommended!
+		 */
+		if (radius_xlat(buffer, sizeof(buffer), request, filename, rad_filename_escape, NULL) < 0) return;
+		request->log.func = rl;
+
+		/*
+		 *	Ensure the directory structure exists, for
+		 *	where we're going to write the log file.
+		 */
+		p = strrchr(buffer, FR_DIR_SEP);
+		if (p) {
+			*p = '\0';
+			if (rad_mkdir(buffer, S_IRWXU, -1, -1) < 0) {
+				ERROR("Failed creating %s: %s", buffer, fr_syserror(errno));
+				return;
+			}
+			*p = FR_DIR_SEP;
+		}
+
+		fp = fopen(buffer, "a");
+	}
+
+	/*
+	 *  If we don't copy the original ap we get a segfault from vasprintf. This is apparently
+	 *  due to ap sometimes being implemented with a stack offset which is invalidated if
+	 *  ap is passed into another function. See here:
+	 *  http://julipedia.meroh.net/2011/09/using-vacopy-to-safely-pass-ap.html
+	 *
+	 *  I don't buy that explanation, but doing a va_copy here does prevent SEGVs seen when
+	 *  running unit tests which generate errors under CI.
+	 */
+	va_copy(aq, ap);
+	vsnprintf(buffer + len, sizeof(buffer) - len, msg, aq);
+	va_end(aq);
+
+	/*
+	 *	Make sure the indent isn't set to something crazy
+	 */
+	indent = request->log.indent > sizeof(spaces) ?
+		 sizeof(spaces) :
+		 request->log.indent;
+
+	/*
+	 *	Logging to a file descriptor
+	 */
+	if (fp) {
+		char time_buff[64];	/* The current timestamp */
+
+		time_t timeval;
+		timeval = time(NULL);
+
+#ifdef HAVE_GMTIME_R
+		if (log_dates_utc) {
+			struct tm utc;
+			gmtime_r(&timeval, &utc);
+			ASCTIME_R(&utc, time_buff, sizeof(time_buff));
+		} else
+#endif
+		{
+			CTIME_R(&timeval, time_buff, sizeof(time_buff));
+		}
+
+		/*
+		 *	Strip trailing new lines
+		 */
+		p = strrchr(time_buff, '\n');
+		if (p) p[0] = '\0';
+
+		if (request->module && (request->module[0] != '\0')) {
+			fprintf(fp, "(%u) %s%s%s: %.*s%s\n",
+				request->number, time_buff, fr_int2str(levels, type, ""),
+				request->module, indent, spaces, buffer);
+		} else {
+			fprintf(fp, "(%u) %s%s%.*s%s\n",
+				request->number, time_buff, fr_int2str(levels, type, ""),
+				indent, spaces, buffer);
+		}
+		fclose(fp);
+		return;
+	}
+
+	/*
+	 *	Logging everywhere else
+	 */
+	if (!DEBUG_ENABLED3) switch (type) {
+	case L_DBG_WARN:
+		extra = "WARNING: ";
+		type = L_DBG_WARN_REQ;
+		break;
+
+	case L_DBG_ERR:
+		extra = "ERROR: ";
+		type = L_DBG_ERR_REQ;
+		break;
+	default:
+		break;
+	}
+
+	if (request->module && (request->module[0] != '\0')) {
+		radlog_always(type, "(%u) %s: %.*s%s%s", request->number,
+			      request->module, indent, spaces, extra, buffer);
+	} else {
+		radlog_always(type, "(%u) %.*s%s%s", request->number,
+			      indent, spaces, extra, buffer);
+	}
+}
+
+/** Martial variadic log arguments into a va_list and pass to normal logging functions
+ *
+ * @see radlog_request_error for more details.
+ *
+ * @param type the log category.
+ * @param lvl of debugging this message should be logged at.
+ * @param request The current request.
+ * @param msg with printf style substitution tokens.
+ * @param ... Substitution arguments.
+ */
+void radlog_request(log_type_t type, log_lvl_t lvl, REQUEST *request, char const *msg, ...)
+{
+	va_list ap;
+
+	if (!request->log.func && !(type & L_DBG)) return;
+
+	va_start(ap, msg);
+	if (request->log.func) request->log.func(type, lvl, request, msg, ap);
+	else if (!(type & L_DBG)) vradlog_request(type, lvl, request, msg, ap);
+	va_end(ap);
+}
+
+/** Martial variadic log arguments into a va_list and pass to error logging functions
+ *
+ * This could all be done in a macro, but it turns out some implementations of the
+ * variadic macros do not work at all well if the va_list being written to is further
+ * up the stack (which is required as you still need a function to convert the elipses
+ * into a va_list).
+ *
+ * So, we use this small wrapper function instead, which will hopefully guarantee
+ * consistent behaviour.
+ *
+ * @param type the log category.
+ * @param lvl of debugging this message should be logged at.
+ * @param request The current request.
+ * @param msg with printf style substitution tokens.
+ * @param ... Substitution arguments.
+ */
+void radlog_request_error(log_type_t type, log_lvl_t lvl, REQUEST *request, char const *msg, ...)
+{
+	va_list ap;
+
+	va_start(ap, msg);
+	if (request->log.func) request->log.func(type, lvl, request, msg, ap);
+	else if (!(type & L_DBG)) vradlog_request(type, lvl, request, msg, ap);
+	vmodule_failure_msg(request, msg, ap);
+	va_end(ap);
+}
+
+/** Write the string being parsed, and a marker showing where the parse error occurred
+ *
+ * @param type the log category.
+ * @param lvl of debugging this message should be logged at.
+ * @param request The current request.
+ * @param msg string we were parsing.
+ * @param idx The position of the marker relative to the string.
+ * @param error What the parse error was.
+ */
+void radlog_request_marker(log_type_t type, log_lvl_t lvl, REQUEST *request,
+			   char const *msg, size_t idx, char const *error)
+{
+	char const *prefix = "";
+	uint8_t indent;
+
+	if (idx >= sizeof(spaces)) {
+		size_t offset = (idx - (sizeof(spaces) - 1)) + (sizeof(spaces) * 0.75);
+		idx -= offset;
+		msg += offset;
+
+		prefix = "... ";
+	}
+
+	/*
+	 *  Don't want format markers being indented
+	 */
+	indent = request->log.indent;
+	request->log.indent = 0;
+
+	radlog_request(type, lvl, request, "%s%s", prefix, msg);
+	radlog_request(type, lvl, request, "%s%.*s^ %s", prefix, (int) idx, spaces, error);
+
+	request->log.indent = indent;
+}
+
+
+/** Canonicalize error strings, removing tabs, and generate spaces for error marker
+ *
+ * @note talloc_free must be called on the buffer returned in spaces and text
+ *
+ * Used to produce error messages such as this:
+ @verbatim
+  I'm a string with a parser # error
+                             ^ Unexpected character in string
+ @endverbatim
+ *
+ * With code resembling this:
+ @code{.c}
+   ERROR("%s", parsed_str);
+   ERROR("%s^ %s", space, text);
+ @endcode
+ *
+ * @todo merge with above function (radlog_request_marker)
+ *
+ * @param sp Where to write a dynamically allocated buffer of spaces used to indent the error text.
+ * @param text Where to write the canonicalized version of msg (the error text).
+ * @param ctx to allocate the spaces and text buffers in.
+ * @param slen of error marker. Expects negative integer value, as returned by parse functions.
+ * @param msg to canonicalize.
+ */
+void fr_canonicalize_error(TALLOC_CTX *ctx, char **sp, char **text, ssize_t slen, char const *msg)
+{
+	size_t offset, skip = 0;
+	char *spbuf, *p;
+	char *value;
+
+	offset = -slen;
+
+	/*
+	 *	Ensure that the error isn't indented
+	 *	too far.
+	 */
+	if (offset > 45) {
+		skip = offset - 40;
+		offset -= skip;
+		value = talloc_strdup(ctx, msg + skip);
+		memcpy(value, "...", 3);
+
+	} else {
+		value = talloc_strdup(ctx, msg);
+	}
+
+	spbuf = talloc_array(ctx, char, offset + 1);
+	memset(spbuf, ' ', offset);
+	spbuf[offset] = '\0';
+
+	/*
+	 *	Smash tabs to spaces for the input string.
+	 */
+	for (p = value; *p != '\0'; p++) {
+		if (*p == '\t') *p = ' ';
+	}
+
+
+	/*
+	 *	Ensure that there isn't too much text after the error.
+	 */
+	if (strlen(value) > 100) {
+		memcpy(value + 95, "... ", 5);
+	}
+
+	*sp = spbuf;
+	*text = value;
+}
+
diff --git a/src/main/mainconfig.c b/src/main/mainconfig.c
new file mode 100644
index 0000000..227ae4a
--- /dev/null
+++ b/src/main/mainconfig.c
@@ -0,0 +1,1420 @@
+/*
+ * mainconf.c	Handle the server's configuration.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2002,2006-2007  The FreeRADIUS server project
+ * Copyright 2002  Alan DeKok <aland@ox.org>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/modpriv.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <sys/stat.h>
+#include <pwd.h>
+#include <grp.h>
+
+#ifdef HAVE_SYSLOG_H
+#  include <syslog.h>
+#endif
+
+#ifdef HAVE_FCNTL_H
+#  include <fcntl.h>
+#endif
+
+#ifdef HAVE_SYSTEMD
+#  include <systemd/sd-daemon.h>
+#endif
+
+main_config_t		main_config;				//!< Main server configuration.
+extern fr_cond_t	*debug_condition;
+fr_cond_t		*debug_condition = NULL;			//!< Condition used to mark packets up for checking.
+bool			event_loop_started = false;		//!< Whether the main event loop has been started yet.
+
+typedef struct cached_config_t {
+	struct cached_config_t *next;
+	time_t		created;
+	CONF_SECTION	*cs;
+} cached_config_t;
+
+static cached_config_t	*cs_cache = NULL;
+
+/*
+ *	Temporary local variables for parsing the configuration
+ *	file.
+ */
+#ifdef HAVE_SETUID
+/*
+ *	Systems that have set/getresuid also have setuid.
+ */
+static uid_t server_uid = 0;
+static gid_t server_gid = 0;
+static char const *uid_name = NULL;
+static char const *gid_name = NULL;
+#endif
+static char const *chroot_dir = NULL;
+static bool allow_core_dumps = false;
+static char const *radlog_dest = NULL;
+
+/*
+ *	These are not used anywhere else..
+ */
+static char const	*localstatedir = NULL;
+static char const	*prefix = NULL;
+static char const	*my_name = NULL;
+static char const	*sbindir = NULL;
+static char const	*run_dir = NULL;
+static char const	*syslog_facility = NULL;
+static bool		do_colourise = false;
+
+static char const	*radius_dir = NULL;	//!< Path to raddb directory
+
+/**********************************************************************
+ *
+ *	We need to figure out where the logs go, before doing anything
+ *	else.  This is so that the log messages go to the correct
+ *	place.
+ *
+ *	BUT, we want the settings from the command line to over-ride
+ *	the ones in the configuration file.  So, these items are
+ *	parsed ONLY if there is no "-l foo" on the command line.
+ *
+ **********************************************************************/
+
+/*
+ *	Log destinations
+ */
+static const CONF_PARSER startup_log_config[] = {
+	{ "destination",  FR_CONF_POINTER(PW_TYPE_STRING, &radlog_dest), "files" },
+	{ "syslog_facility",  FR_CONF_POINTER(PW_TYPE_STRING, &syslog_facility), STRINGIFY(0) },
+
+	{ "localstatedir", FR_CONF_POINTER(PW_TYPE_STRING, &localstatedir), "${prefix}/var"},
+	{ "logdir", FR_CONF_POINTER(PW_TYPE_STRING, &radlog_dir), "${localstatedir}/log"},
+	{ "file",  FR_CONF_POINTER(PW_TYPE_STRING, &main_config.log_file), "${logdir}/radius.log" },
+	{ "requests",  FR_CONF_POINTER(PW_TYPE_STRING | PW_TYPE_DEPRECATED, &default_log.file), NULL },
+	CONF_PARSER_TERMINATOR
+};
+
+
+/*
+ *	Basic configuration for the server.
+ */
+static const CONF_PARSER startup_server_config[] = {
+	{ "log",  FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) startup_log_config },
+
+	{ "name", FR_CONF_POINTER(PW_TYPE_STRING, &my_name), "radiusd"},
+	{ "prefix", FR_CONF_POINTER(PW_TYPE_STRING, &prefix), "/usr/local"},
+
+	{ "log_file",  FR_CONF_POINTER(PW_TYPE_STRING, &main_config.log_file), NULL },
+	{ "log_destination", FR_CONF_POINTER(PW_TYPE_STRING, &radlog_dest), NULL },
+	{ "use_utc", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &log_dates_utc), NULL },
+	CONF_PARSER_TERMINATOR
+};
+
+
+/**********************************************************************
+ *
+ *	Now that we've parsed the log destination, AND the security
+ *	items, we can parse the rest of the configuration items.
+ *
+ **********************************************************************/
+static const CONF_PARSER log_config[] = {
+	{ "stripped_names", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &log_stripped_names),"no" },
+	{ "auth", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.log_auth), "no" },
+	{ "auth_accept", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.log_accept), NULL},
+	{ "auth_reject", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.log_reject), NULL},
+	{ "auth_badpass", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.log_auth_badpass), "no" },
+	{ "auth_goodpass", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.log_auth_goodpass), "no" },
+	{ "msg_badpass", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.auth_badpass_msg), NULL},
+	{ "msg_goodpass", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.auth_goodpass_msg), NULL},
+	{ "colourise",FR_CONF_POINTER(PW_TYPE_BOOLEAN, &do_colourise), NULL },
+	{ "use_utc", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &log_dates_utc), NULL },
+	{ "msg_denied", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.denied_msg), "You are already logged in - access denied" },
+	{ "suppress_secrets", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.suppress_secrets), NULL },
+	CONF_PARSER_TERMINATOR
+};
+
+
+/*
+ *  Security configuration for the server.
+ */
+static const CONF_PARSER security_config[] = {
+	{ "max_attributes",  FR_CONF_POINTER(PW_TYPE_INTEGER, &fr_max_attributes), STRINGIFY(0) },
+	{ "reject_delay",  FR_CONF_POINTER(PW_TYPE_TIMEVAL, &main_config.reject_delay), STRINGIFY(0) },
+	{ "status_server", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.status_server), "no"},
+#ifdef ENABLE_OPENSSL_VERSION_CHECK
+	{ "allow_vulnerable_openssl", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.allow_vulnerable_openssl), "no"},
+#endif
+	CONF_PARSER_TERMINATOR
+};
+
+static const CONF_PARSER resources[] = {
+	/*
+	 *	Don't set a default here.  It's set in the code, below.  This means that
+	 *	the config item will *not* get printed out in debug mode, so that no one knows
+	 *	it exists.
+	 */
+	{ "talloc_pool_size", FR_CONF_POINTER(PW_TYPE_INTEGER, &main_config.talloc_pool_size), NULL },
+	CONF_PARSER_TERMINATOR
+};
+
+static const CONF_PARSER server_config[] = {
+	/*
+	 *	FIXME: 'prefix' is the ONLY one which should be
+	 *	configured at compile time.  Hard-coding it here is
+	 *	bad.  It will be cleaned up once we clean up the
+	 *	hard-coded defines for the locations of the various
+	 *	files.
+	 */
+	{ "name", FR_CONF_POINTER(PW_TYPE_STRING, &my_name), "radiusd"},
+	{ "prefix", FR_CONF_POINTER(PW_TYPE_STRING, &prefix), "/usr/local"},
+	{ "localstatedir", FR_CONF_POINTER(PW_TYPE_STRING, &localstatedir), "${prefix}/var"},
+	{ "sbindir", FR_CONF_POINTER(PW_TYPE_STRING, &sbindir), "${prefix}/sbin"},
+	{ "logdir", FR_CONF_POINTER(PW_TYPE_STRING, &radlog_dir), "${localstatedir}/log"},
+	{ "run_dir", FR_CONF_POINTER(PW_TYPE_STRING, &run_dir), "${localstatedir}/run/${name}"},
+	{ "libdir", FR_CONF_POINTER(PW_TYPE_STRING, &radlib_dir), "${prefix}/lib"},
+	{ "radacctdir", FR_CONF_POINTER(PW_TYPE_STRING, &radacct_dir), "${logdir}/radacct" },
+	{ "panic_action", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.panic_action), NULL},
+	{ "hostname_lookups", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &fr_dns_lookups), "no" },
+	{ "max_request_time", FR_CONF_POINTER(PW_TYPE_INTEGER, &main_config.max_request_time), STRINGIFY(MAX_REQUEST_TIME) },
+	{ "cleanup_delay", FR_CONF_POINTER(PW_TYPE_INTEGER, &main_config.cleanup_delay), STRINGIFY(CLEANUP_DELAY) },
+	{ "max_requests", FR_CONF_POINTER(PW_TYPE_INTEGER, &main_config.max_requests), STRINGIFY(MAX_REQUESTS) },
+	{ "postauth_client_lost", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.postauth_client_lost), "no" },
+	{ "pidfile", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.pid_file), "${run_dir}/radiusd.pid"},
+	{ "checkrad", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.checkrad), "${sbindir}/checkrad" },
+
+	{ "debug_level", FR_CONF_POINTER(PW_TYPE_INTEGER, &main_config.debug_level), "0"},
+
+#ifdef WITH_PROXY
+	{ "proxy_requests", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.proxy_requests), "yes" },
+#endif
+	{ "log", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) log_config },
+
+	{ "resources", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) resources },
+
+	/*
+	 *	People with old configs will have these.  They are listed
+	 *	AFTER the "log" section, so if they exist in radiusd.conf,
+	 *	it will prefer "log_foo = bar" to "log { foo = bar }".
+	 *	They're listed with default values of NULL, so that if they
+	 *	DON'T exist in radiusd.conf, then the previously parsed
+	 *	values for "log { foo = bar}" will be used.
+	 */
+	{ "log_auth", FR_CONF_POINTER(PW_TYPE_BOOLEAN | PW_TYPE_DEPRECATED, &main_config.log_auth), NULL },
+	{ "log_auth_badpass", FR_CONF_POINTER(PW_TYPE_BOOLEAN | PW_TYPE_DEPRECATED, &main_config.log_auth_badpass), NULL },
+	{ "log_auth_goodpass", FR_CONF_POINTER(PW_TYPE_BOOLEAN | PW_TYPE_DEPRECATED, &main_config.log_auth_goodpass), NULL },
+	{ "log_stripped_names", FR_CONF_POINTER(PW_TYPE_BOOLEAN | PW_TYPE_DEPRECATED, &log_stripped_names), NULL },
+
+	{  "security", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) security_config },
+	CONF_PARSER_TERMINATOR
+};
+
+
+/**********************************************************************
+ *
+ *	The next few items are here as a "bootstrap" for security.
+ *	They allow the server to switch users, chroot, while still
+ *	opening the various output files with the correct permission.
+ *
+ *	It's rare (or impossible) to have parse errors here, so we
+ *	don't worry too much about that.  In contrast, when we parse
+ *	the rest of the configuration, we CAN get parse errors.  We
+ *	want THOSE parse errors to go to the log file, and we want the
+ *	log file to have the correct permissions.
+ *
+ **********************************************************************/
+static const CONF_PARSER bootstrap_security_config[] = {
+#ifdef HAVE_SETUID
+	{ "user",  FR_CONF_POINTER(PW_TYPE_STRING, &uid_name), NULL },
+	{ "group", FR_CONF_POINTER(PW_TYPE_STRING, &gid_name), NULL },
+#endif
+	{ "chroot",  FR_CONF_POINTER(PW_TYPE_STRING, &chroot_dir), NULL },
+	{ "allow_core_dumps", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &allow_core_dumps), "no" },
+	CONF_PARSER_TERMINATOR
+};
+
+static const CONF_PARSER bootstrap_config[] = {
+	{  "security", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) bootstrap_security_config },
+
+	{ "name", FR_CONF_POINTER(PW_TYPE_STRING, &my_name), "radiusd"},
+	{ "prefix", FR_CONF_POINTER(PW_TYPE_STRING, &prefix), "/usr/local"},
+	{ "localstatedir", FR_CONF_POINTER(PW_TYPE_STRING, &localstatedir), "${prefix}/var"},
+
+	{ "logdir", FR_CONF_POINTER(PW_TYPE_STRING, &radlog_dir), "${localstatedir}/log"},
+	{ "run_dir", FR_CONF_POINTER(PW_TYPE_STRING, &run_dir), "${localstatedir}/run/${name}"},
+
+	/*
+	 *	For backwards compatibility.
+	 */
+#ifdef HAVE_SETUID
+	{ "user",  FR_CONF_POINTER(PW_TYPE_STRING | PW_TYPE_DEPRECATED, &uid_name), NULL },
+	{ "group",  FR_CONF_POINTER(PW_TYPE_STRING | PW_TYPE_DEPRECATED, &gid_name), NULL },
+#endif
+	{ "chroot",  FR_CONF_POINTER(PW_TYPE_STRING | PW_TYPE_DEPRECATED, &chroot_dir), NULL },
+	{ "allow_core_dumps", FR_CONF_POINTER(PW_TYPE_BOOLEAN | PW_TYPE_DEPRECATED, &allow_core_dumps), NULL },
+	CONF_PARSER_TERMINATOR
+};
+
+
+static size_t config_escape_func(UNUSED REQUEST *request, char *out, size_t outlen, char const *in, UNUSED void *arg)
+{
+	size_t len = 0;
+	static char const disallowed[] = "%{}\\'\"`";
+
+	while (in[0]) {
+		/*
+		 *	Non-printable characters get replaced with their
+		 *	mime-encoded equivalents.
+		 */
+		if ((in[0] < 32)) {
+			if (outlen <= 3) break;
+
+			snprintf(out, outlen, "=%02X", (unsigned char) in[0]);
+			in++;
+			out += 3;
+			outlen -= 3;
+			len += 3;
+			continue;
+
+		} else if (strchr(disallowed, *in) != NULL) {
+			if (outlen <= 2) break;
+
+			out[0] = '\\';
+			out[1] = *in;
+			in++;
+			out += 2;
+			outlen -= 2;
+			len += 2;
+			continue;
+		}
+
+		/*
+		 *	Only one byte left.
+		 */
+		if (outlen <= 1) {
+			break;
+		}
+
+		/*
+		 *	Allowed character.
+		 */
+		*out = *in;
+		out++;
+		in++;
+		outlen--;
+		len++;
+	}
+	*out = '\0';
+	return len;
+}
+
+/*
+ *	Xlat for %{config:section.subsection.attribute}
+ */
+static ssize_t xlat_config(UNUSED void *instance, REQUEST *request, char const *fmt, char *out, size_t outlen)
+{
+	char const *value;
+	CONF_PAIR *cp;
+	CONF_ITEM *ci;
+	char buffer[1024];
+
+	/*
+	 *	Expand it safely.
+	 */
+	if (radius_xlat(buffer, sizeof(buffer), request, fmt, config_escape_func, NULL) < 0) {
+		return 0;
+	}
+
+	ci = cf_reference_item(request->root->config,
+			       request->root->config, buffer);
+	if (!ci || !cf_item_is_pair(ci)) {
+		REDEBUG("Config item \"%s\" does not exist", fmt);
+		*out = '\0';
+		return -1;
+	}
+
+	cp = cf_item_to_pair(ci);
+
+	/*
+	 *  Ensure that we only copy what's necessary.
+	 *
+	 *  If 'outlen' is too small, then the output is chopped to fit.
+	 */
+	value = cf_pair_value(cp);
+	if (!value) {
+		out[0] = '\0';
+		return 0;
+	}
+
+	if (outlen > strlen(value)) {
+		outlen = strlen(value) + 1;
+	}
+
+	strlcpy(out, value, outlen);
+
+	return strlen(out);
+}
+
+
+/*
+ *	Xlat for %{client:foo}
+ */
+static ssize_t xlat_client(UNUSED void *instance, REQUEST *request, char const *fmt, char *out, size_t outlen)
+{
+	char const *value = NULL;
+	CONF_PAIR *cp;
+
+	if (!fmt || !out || (outlen < 1)) return 0;
+
+	if (!request->client) {
+		RWDEBUG("No client associated with this request");
+		*out = '\0';
+		return 0;
+	}
+
+	cp = cf_pair_find(request->client->cs, fmt);
+	if (!cp || !(value = cf_pair_value(cp))) {
+		if (strcmp(fmt, "shortname") == 0 && request->client->shortname) {
+			value = request->client->shortname;
+		}
+		else if (strcmp(fmt, "nas_type") == 0 && request->client->nas_type) {
+			value = request->client->nas_type;
+		} else {
+			*out = '\0';
+			return 0;
+		}
+	}
+
+	strlcpy(out, value, outlen);
+
+	return strlen(out);
+}
+
+/*
+ *	Xlat for %{getclient:<ipaddr>.foo}
+ */
+static ssize_t xlat_getclient(UNUSED void *instance, REQUEST *request, char const *fmt, char *out, size_t outlen)
+{
+	char const *value = NULL;
+	char buffer[INET6_ADDRSTRLEN], *q;
+	char const *p = fmt;
+	fr_ipaddr_t ip;
+	CONF_PAIR *cp;
+	RADCLIENT *client = NULL;
+
+	if (!fmt || !out || (outlen < 1)) return 0;
+
+	q = strrchr(p, '.');
+	if (!q || (q == p) || (((size_t)(q - p)) > sizeof(buffer))) {
+		REDEBUG("Invalid client string");
+		goto error;
+	}
+
+	strlcpy(buffer, p, (q + 1) - p);
+	if (fr_pton(&ip, buffer, -1, AF_UNSPEC, false) < 0) {
+		REDEBUG("\"%s\" is not a valid IPv4 or IPv6 address", buffer);
+		goto error;
+	}
+
+	fmt = q + 1;
+
+	client = client_find(NULL, &ip, IPPROTO_IP);
+	if (!client) {
+		RDEBUG("No client found with IP \"%s\"", buffer);
+		*out = '\0';
+		return 0;
+	}
+
+	cp = cf_pair_find(client->cs, fmt);
+	if (!cp || !(value = cf_pair_value(cp))) {
+		if (strcmp(fmt, "shortname") == 0) {
+			strlcpy(out, request->client->shortname, outlen);
+			return strlen(out);
+		}
+		*out = '\0';
+		return 0;
+	}
+
+	strlcpy(out, value, outlen);
+	return strlen(out);
+
+	error:
+	*out = '\0';
+	return -1;
+}
+
+/*
+ *	Common xlat for listeners
+ */
+static ssize_t xlat_listen_common(REQUEST *request, rad_listen_t *listen,
+				  char const *fmt, char *out, size_t outlen)
+{
+	char const *value = NULL;
+	CONF_PAIR *cp;
+
+	if (!fmt || !out || (outlen < 1)) return 0;
+
+	if (!listen) {
+		RWDEBUG("No listener associated with this request");
+		*out = '\0';
+		return 0;
+	}
+
+	/*
+	 *	When TLS is configured, we *require* the use of TLS.
+	 */
+	if (strcmp(fmt, "tls") == 0) {
+#ifdef WITH_TLS
+		if (listen->tls) {
+			strlcpy(out, "yes", outlen);
+			return strlen(out);
+		}
+#endif
+
+		strlcpy(out, "no", outlen);
+		return strlen(out);
+	}
+
+#ifdef WITH_TLS
+	/*
+	 *	Look for TLS certificate data.
+	 */
+	if (strncmp(fmt, "TLS-", 4) == 0) {
+		VALUE_PAIR *vp;
+		listen_socket_t *sock = listen->data;
+
+		if (!listen->tls) {
+			RDEBUG("Listener is not using TLS.  TLS attributes are not available");
+			*out = '\0';
+			return 0;
+		}
+
+		for (vp = sock->certs; vp != NULL; vp = vp->next) {
+			if (strcmp(fmt, vp->da->name) == 0) {
+				return vp_prints_value(out, outlen, vp, 0);
+			}
+		}
+
+		RDEBUG("Unknown TLS attribute \"%s\"", fmt);
+		*out = '\0';
+		return 0;
+	}
+#else
+	if (strncmp(fmt, "TLS-", 4) == 0) {
+		RDEBUG("Server is not built with TLS support");
+		*out = '\0';
+		return 0;
+	}
+#endif
+
+#ifdef WITH_COA_TUNNEL
+	/*
+	 *      Look for RADSEC CoA tunnel key.
+	 */
+	if (listen->key && (strcmp(fmt, "Originating-Realm-Key") == 0)) {
+		strlcpy(out, listen->key, outlen);
+		return strlen(out);
+	}
+#endif
+
+	cp = cf_pair_find(listen->cs, fmt);
+	if (!cp || !(value = cf_pair_value(cp))) {
+		RDEBUG("Listener does not contain config item \"%s\"", fmt);
+		*out = '\0';
+		return 0;
+	}
+
+	strlcpy(out, value, outlen);
+
+	return strlen(out);
+}
+
+
+/*
+ *	Xlat for %{listen:foo}
+ */
+static ssize_t xlat_listen(UNUSED void *instance, REQUEST *request,
+			   char const *fmt, char *out, size_t outlen)
+{
+	return xlat_listen_common(request, request->listener, fmt, out, outlen);
+}
+
+/*
+ *	Xlat for %{proxy_listen:foo}
+ */
+static ssize_t xlat_proxy_listen(UNUSED void *instance, REQUEST *request,
+				 char const *fmt, char *out, size_t outlen)
+{
+	if (!request->proxy_listener) {
+		*out = '\0';
+		return 0;
+	}
+
+	return xlat_listen_common(request, request->proxy_listener, fmt, out, outlen);
+}
+
+#ifdef HAVE_SETUID
+/*
+ *  Do chroot, if requested.
+ *
+ *  Switch UID and GID to what is specified in the config file
+ */
+static int switch_users(CONF_SECTION *cs)
+{
+	bool do_suid = false;
+	bool do_sgid = false;
+
+	/*
+	 *	Get the current maximum for core files.  Do this
+	 *	before anything else so as to ensure it's properly
+	 *	initialized.
+	 */
+	if (fr_set_dumpable_init() < 0) {
+		return 0;
+	}
+
+	/*
+	 *	Don't do chroot/setuid/setgid if we're in debugging
+	 *	as non-root.
+	 */
+	if (rad_debug_lvl && (getuid() != 0)) return 1;
+
+	if (cf_section_parse(cs, NULL, bootstrap_config) < 0) {
+		fr_strerror_printf("Failed to parse user/group information.");
+		return 0;
+	}
+
+#ifdef HAVE_GRP_H
+	/*
+	 *	Get the correct GID for the server.
+	 */
+	server_gid = getgid();
+
+	if (gid_name) {
+		struct group *gr;
+
+		gr = getgrnam(gid_name);
+		if (!gr) {
+			fr_strerror_printf("Cannot get ID for group %s: %s",
+					   gid_name, fr_syserror(errno));
+			return 0;
+		}
+
+		if (server_gid != gr->gr_gid) {
+			server_gid = gr->gr_gid;
+			do_sgid = true;
+		}
+	}
+#endif
+
+	/*
+	 *	Get the correct UID for the server.
+	 */
+	server_uid = getuid();
+
+	if (uid_name) {
+		struct passwd *user;
+
+		if (rad_getpwnam(cs, &user, uid_name) < 0) {
+			fr_strerror_printf("Cannot get passwd entry for user %s: %s",
+					   uid_name, fr_strerror());
+			return 0;
+		}
+
+		/*
+		 *	We're not the correct user.  Go set that.
+		 */
+		if (server_uid != user->pw_uid) {
+			server_uid = user->pw_uid;
+			do_suid = true;
+#ifdef HAVE_INITGROUPS
+			if (initgroups(uid_name, server_gid) < 0) {
+				fr_strerror_printf("Cannot initialize supplementary group list for user %s: %s",
+						   uid_name, fr_syserror(errno));
+				talloc_free(user);
+				return 0;
+			}
+#endif
+		}
+
+		talloc_free(user);
+	}
+
+	/*
+	 *	Do chroot BEFORE changing UIDs.
+	 */
+	if (chroot_dir) {
+		if (chroot(chroot_dir) < 0) {
+			fr_strerror_printf("Failed to perform chroot to %s: %s",
+					   chroot_dir, fr_syserror(errno));
+			return 0;
+		}
+
+		/*
+		 *	Note that we leave chdir alone.  It may be
+		 *	OUTSIDE of the root.  This allows us to read
+		 *	the configuration from "-d ./etc/raddb", with
+		 *	the chroot as "./chroot/" for example.  After
+		 *	the server has been loaded, it does a "cd
+		 *	${logdir}" below, so that core files (if any)
+		 *	go to a logging directory.
+		 *
+		 *	This also allows the configuration of the
+		 *	server to be outside of the chroot.  If the
+		 *	server is statically linked, then the only
+		 *	things needed inside of the chroot are the
+		 *	logging directories.
+		 */
+	}
+
+#ifdef HAVE_GRP_H
+	/*
+	 *	Set the GID.  Don't bother checking it.
+	 */
+	if (do_sgid) {
+		if (setgid(server_gid) < 0){
+			fr_strerror_printf("Failed setting group to %s: %s",
+					   gid_name, fr_syserror(errno));
+			return 0;
+		}
+	}
+#endif
+
+	/*
+	 *	The directories for PID files and logs must exist.  We
+	 *	need to create them if we're told to write files to
+	 *	those directories.
+	 *
+	 *	Because this creation is new in 3.0.9, it's a soft
+	 *	fail.
+	 *
+	 */
+	if (main_config.write_pid) {
+		char *my_dir;
+
+		my_dir = talloc_strdup(NULL, run_dir);
+		if (rad_mkdir(my_dir, 0750, server_uid, server_gid) < 0) {
+			DEBUG("Failed to create run_dir %s: %s",
+			      my_dir, strerror(errno));
+		}
+		talloc_free(my_dir);
+	}
+
+	if (default_log.dst == L_DST_FILES) {
+		char *my_dir;
+
+		my_dir = talloc_strdup(NULL, radlog_dir);
+		if (rad_mkdir(my_dir, 0750, server_uid, server_gid) < 0) {
+			DEBUG("Failed to create logdir %s: %s",
+			      my_dir, strerror(errno));
+		}
+		talloc_free(my_dir);
+	}
+
+	/*
+	 *	If we don't already have a log file open, open one
+	 *	now.  We may not have been logging anything yet.  The
+	 *	server normally starts up fairly quietly.
+	 */
+	if ((default_log.dst == L_DST_FILES) &&
+	    (default_log.fd < 0)) {
+		default_log.fd = open(main_config.log_file,
+				      O_WRONLY | O_APPEND | O_CREAT, 0640);
+		if (default_log.fd < 0) {
+			fr_strerror_printf("Failed to open log file %s: %s\n",
+					   main_config.log_file, fr_syserror(errno));
+			return 0;
+		}
+	}
+
+	/*
+	 *	If we need to change UID, ensure that the log files
+	 *	have the correct owner && group.
+	 *
+	 *	We have to do this because some log files MAY already
+	 *	have been written as root.  We need to change them to
+	 *	have the correct ownership before proceeding.
+	 */
+	if ((do_suid || do_sgid) &&
+	    (default_log.dst == L_DST_FILES)) {
+		if (fchown(default_log.fd, server_uid, server_gid) < 0) {
+			fr_strerror_printf("Cannot change ownership of log file %s: %s\n",
+					   main_config.log_file, fr_syserror(errno));
+			return 0;
+		}
+	}
+
+	/*
+	 *	Once we're done with all of the privileged work,
+	 *	permanently change the UID.
+	 */
+	if (do_suid) {
+		rad_suid_set_down_uid(server_uid);
+		rad_suid_down();
+	}
+
+	/*
+	 *	This also clears the dumpable flag if core dumps
+	 *	aren't allowed.
+	 */
+	if (fr_set_dumpable(allow_core_dumps) < 0) {
+		WARN("Failed to allow core dumps - %s", fr_strerror());
+	}
+
+	if (allow_core_dumps) {
+		INFO("Core dumps are enabled");
+	}
+
+	return 1;
+}
+#endif	/* HAVE_SETUID */
+
+/** Set the global radius config directory.
+ *
+ * @param ctx Where to allocate the memory for the path string.
+ * @param path to config dir root e.g. /usr/local/etc/raddb
+ */
+void set_radius_dir(TALLOC_CTX *ctx, char const *path)
+{
+	if (radius_dir) {
+		char *p;
+
+		memcpy(&p, &radius_dir, sizeof(p));
+		talloc_free(p);
+		radius_dir = NULL;
+	}
+	if (path) radius_dir = talloc_strdup(ctx, path);
+}
+
+/** Get the global radius config directory.
+ *
+ * @return the global radius config directory.
+ */
+char const *get_radius_dir(void)
+{
+	return radius_dir;
+}
+
+static int _dlhandle_free(void **dl_handle)
+{
+	dlclose(*dl_handle);
+	return 0;
+}
+
+/*
+ *	Read config files.
+ *
+ *	This function can ONLY be called from the main server process.
+ */
+int main_config_init(void)
+{
+	char const *p = NULL;
+	CONF_SECTION *cs, *subcs;
+	struct stat statbuf;
+	cached_config_t *cc;
+	char buffer[1024];
+
+	if (stat(radius_dir, &statbuf) < 0) {
+		ERROR("Errors reading %s: %s",
+		       radius_dir, fr_syserror(errno));
+		return -1;
+	}
+
+#ifdef S_IWOTH
+	if ((statbuf.st_mode & S_IWOTH) != 0) {
+		ERROR("Configuration directory %s is globally writable.  Refusing to start due to insecure configuration.",
+		       radius_dir);
+	  return -1;
+	}
+#endif
+
+#if 0 && defined(S_IROTH)
+	if (statbuf.st_mode & S_IROTH != 0) {
+		ERROR("Configuration directory %s is globally readable.  Refusing to start due to insecure configuration.",
+		       radius_dir);
+		return -1;
+	}
+#endif
+	INFO("Starting - reading configuration files ...");
+
+	/*
+	 *	We need to load the dictionaries before reading the
+	 *	configuration files.  This is because of the
+	 *	pre-compilation in conffile.c.  That should probably
+	 *	be fixed to be done as a second stage.
+	 */
+	if (!main_config.dictionary_dir) {
+		main_config.dictionary_dir = DICTDIR;
+	}
+
+	/*
+	 *	About sizeof(REQUEST) + sizeof(RADIUS_PACKET) * 2 + sizeof(VALUE_PAIR) * 400
+	 *
+	 *	Which should be enough for many configurations.
+	 */
+	main_config.talloc_pool_size = 8 * 1024; /* default */
+
+	/*
+	 *	Read the distribution dictionaries first, then
+	 *	the ones in raddb.
+	 */
+	DEBUG2("including dictionary file %s/%s", main_config.dictionary_dir, RADIUS_DICTIONARY);
+	if (dict_init(main_config.dictionary_dir, RADIUS_DICTIONARY) != 0) {
+		ERROR("Errors reading dictionary: %s",
+		      fr_strerror());
+		return -1;
+	}
+
+#define DICT_READ_OPTIONAL(_d, _n) \
+do {\
+	switch (dict_read(_d, _n)) {\
+	case -1:\
+		ERROR("Errors reading %s/%s: %s", _d, _n, fr_strerror());\
+		return -1;\
+	case 0:\
+		DEBUG2("including dictionary file %s/%s", _d,_n);\
+		break;\
+	default:\
+		break;\
+	}\
+} while (0)
+
+	/*
+	 *	Try to load protocol-specific dictionaries.  It's OK
+	 *	if they don't exist.
+	 */
+#ifdef WITH_DHCP
+	DICT_READ_OPTIONAL(main_config.dictionary_dir, "dictionary.dhcp");
+#endif
+
+#ifdef WITH_VMPS
+	DICT_READ_OPTIONAL(main_config.dictionary_dir, "dictionary.vqp");
+#endif
+
+	/*
+	 *	It's OK if this one doesn't exist.
+	 */
+	DICT_READ_OPTIONAL(radius_dir, RADIUS_DICTIONARY);
+
+	cs = cf_section_alloc(NULL, "main", NULL);
+	if (!cs) return -1;
+
+	/*
+	 *	Add a 'feature' subsection off the main config
+	 *	We check if it's defined first, as the user may
+	 *	have defined their own feature flags, or want
+	 *	to manually override the ones set by modules
+	 *	or the server.
+	 */
+	subcs = cf_section_sub_find(cs, "feature");
+	if (!subcs) {
+		subcs = cf_section_alloc(cs, "feature", NULL);
+		if (!subcs) return -1;
+
+		cf_section_add(cs, subcs);
+	}
+	version_init_features(subcs);
+
+	/*
+	 *	Add a 'version' subsection off the main config
+	 *	We check if it's defined first, this is for
+	 *	backwards compatibility.
+	 */
+	subcs = cf_section_sub_find(cs, "version");
+	if (!subcs) {
+		subcs = cf_section_alloc(cs, "version", NULL);
+		if (!subcs) return -1;
+		cf_section_add(cs, subcs);
+	}
+	version_init_numbers(subcs);
+
+	/* Read the configuration file */
+	snprintf(buffer, sizeof(buffer), "%.200s/%.50s.conf", radius_dir, main_config.name);
+	if (cf_file_read(cs, buffer) < 0) {
+		ERROR("Errors reading or parsing %s", buffer);
+	failure:
+		talloc_free(cs);
+		return -1;
+	}
+
+	/*
+	 *	Parse environment variables first.
+	 */
+	subcs = cf_section_sub_find(cs, "ENV");
+	if (subcs) {
+		char const *attr, *value;
+		CONF_PAIR *cp;
+		CONF_ITEM *ci;
+
+		for (ci = cf_item_find_next(subcs, NULL);
+		     ci != NULL;
+		     ci = cf_item_find_next(subcs, ci)) {
+
+			if (cf_item_is_data(ci)) continue;
+
+			if (!cf_item_is_pair(ci)) {
+				cf_log_err(ci, "Unexpected item in ENV section");
+				goto failure;
+			}
+
+			cp = cf_item_to_pair(ci);
+			if (cf_pair_operator(cp) != T_OP_EQ) {
+				cf_log_err(ci, "Invalid operator for item in ENV section");
+				goto failure;
+			}
+
+			attr = cf_pair_attr(cp);
+			value = cf_pair_value(cp);
+			if (!value) {
+				if (unsetenv(attr) < 0) {
+					cf_log_err(ci, "Failed deleting environment variable %s: %s",
+						   attr, fr_syserror(errno));
+					goto failure;
+				}
+			} else {
+				void *handle;
+				void **handle_p;
+
+				if (setenv(attr, value, 1) < 0) {
+					cf_log_err(ci, "Failed setting environment variable %s: %s",
+						   attr, fr_syserror(errno));
+					goto failure;
+				}
+
+				/*
+				 *	Hacks for LD_PRELOAD.
+				 */
+				if (strcmp(attr, "LD_PRELOAD") != 0) continue;
+
+				handle = dlopen(value, RTLD_NOW | RTLD_GLOBAL);
+				if (!handle) {
+					cf_log_err(ci, "Failed loading library %s: %s", value, dlerror());
+					goto failure;
+				}
+
+				/*
+				 *	Wrap the pointer, so we can set a destructor.
+				 */
+				MEM(handle_p = talloc(NULL, void *));
+				*handle_p = handle;
+				talloc_set_destructor(handle_p, _dlhandle_free);
+
+				(void) cf_data_add(subcs, value, handle, NULL);
+			}
+		} /* loop over pairs in ENV */
+	} /* there's an ENV subsection */
+
+	/*
+	 *	If there was no log destination set on the command line,
+	 *	set it now.
+	 */
+	if (default_log.dst == L_DST_NULL) {
+		default_log.dst = L_DST_STDERR;
+		default_log.fd = STDERR_FILENO;
+
+		if (cf_section_parse(cs, NULL, startup_server_config) == -1) {
+			fprintf(stderr, "%s: Error: Failed to parse log{} section.\n",
+				main_config.name);
+			cf_file_free(cs);
+			return -1;
+		}
+
+		if (!radlog_dest) {
+			fprintf(stderr, "%s: Error: No log destination specified.\n",
+				main_config.name);
+			cf_file_free(cs);
+			return -1;
+		}
+
+		default_log.fd = -1;
+		default_log.dst = fr_str2int(log_str2dst, radlog_dest,
+					      L_DST_NUM_DEST);
+		if (default_log.dst == L_DST_NUM_DEST) {
+			fprintf(stderr, "%s: Error: Unknown log_destination %s\n",
+				main_config.name, radlog_dest);
+			cf_file_free(cs);
+			return -1;
+		}
+
+		if (default_log.dst == L_DST_SYSLOG) {
+			/*
+			 *	Make sure syslog_facility isn't NULL
+			 *	before using it
+			 */
+			if (!syslog_facility) {
+				fprintf(stderr, "%s: Error: Syslog chosen but no facility was specified\n",
+					main_config.name);
+				cf_file_free(cs);
+				return -1;
+			}
+			main_config.syslog_facility = fr_str2int(syslog_facility_table, syslog_facility, -1);
+			if (main_config.syslog_facility < 0) {
+				fprintf(stderr, "%s: Error: Unknown syslog_facility %s\n",
+					main_config.name, syslog_facility);
+				cf_file_free(cs);
+				return -1;
+			}
+
+#ifdef HAVE_SYSLOG_H
+			/*
+			 *	Call openlog only once, when the
+			 *	program starts.
+			 */
+			openlog(main_config.name, LOG_PID, main_config.syslog_facility);
+#endif
+
+		} else if (default_log.dst == L_DST_FILES) {
+			if (!main_config.log_file) {
+				fprintf(stderr, "%s: Error: Specified \"files\" as a log destination, but no log filename was given!\n",
+					main_config.name);
+				cf_file_free(cs);
+				return -1;
+			}
+		}
+	}
+
+#ifdef HAVE_SETUID
+	/*
+	 *	Switch users as early as possible.
+	 */
+	if (!switch_users(cs)) {
+		fprintf(stderr, "%s: ERROR - %s\n", main_config.name, fr_strerror());
+		fr_exit(1);
+	}
+#endif
+
+	/*
+	 *	This allows us to figure out where, relative to
+	 *	radiusd.conf, the other configuration files exist.
+	 */
+	if (cf_section_parse(cs, NULL, server_config) < 0) return -1;
+
+	/*
+	 *	Fix up log_auth, and log_accept and log_reject
+	 */
+	if (main_config.log_auth) {
+		main_config.log_accept = main_config.log_reject = true;
+	}
+
+	/*
+	 *	We ignore colourization of output until after the
+	 *	configuration files have been parsed.
+	 */
+	p = getenv("TERM");
+	if (do_colourise && p && isatty(default_log.fd) && strstr(p, "xterm")) {
+		default_log.colourise = true;
+	} else {
+		default_log.colourise = false;
+	}
+
+	/*
+	 *	Starting the server, WITHOUT "-x" on the
+	 *	command-line: use whatever is in the config
+	 *	file.
+	 */
+	if (rad_debug_lvl == 0) {
+		rad_debug_lvl = main_config.debug_level;
+	}
+	fr_debug_lvl = rad_debug_lvl;
+
+	FR_INTEGER_COND_CHECK("max_request_time", main_config.max_request_time,
+			      (main_config.max_request_time != 0), 100);
+
+	/*
+	 *	reject_delay can be zero.  OR 1 though 10.
+	 */
+	if ((main_config.reject_delay.tv_sec != 0) || (main_config.reject_delay.tv_usec != 0)) {
+		FR_TIMEVAL_BOUND_CHECK("reject_delay", &main_config.reject_delay, >=, 1, 0);
+	}
+	FR_TIMEVAL_BOUND_CHECK("reject_delay", &main_config.reject_delay, <=, 10, 0);
+
+	FR_INTEGER_BOUND_CHECK("cleanup_delay", main_config.cleanup_delay, <=, 30);
+
+	FR_INTEGER_BOUND_CHECK("resources.talloc_pool_size", main_config.talloc_pool_size, >=, 2 * 1024);
+	FR_INTEGER_BOUND_CHECK("resources.talloc_pool_size", main_config.talloc_pool_size, <=, 1024 * 1024);
+
+	/*
+	 * Set default initial request processing delay to 1/3 of a second.
+	 * Will be updated by the lowest response window across all home servers,
+	 * if it is less than this.
+	 */
+	main_config.init_delay.tv_sec = 0;
+	main_config.init_delay.tv_usec = 2* (1000000 / 3);
+
+	/*
+	 *	Free the old configuration items, and replace them
+	 *	with the new ones.
+	 *
+	 *	Note that where possible, we do atomic switch-overs,
+	 *	to ensure that the pointers are always valid.
+	 */
+	rad_assert(main_config.config == NULL);
+	root_config = main_config.config = cs;
+
+	DEBUG2("%s: #### Loading Realms and Home Servers ####", main_config.name);
+	if (!realms_init(cs)) {
+		return -1;
+	}
+
+	DEBUG2("%s: #### Loading Clients ####", main_config.name);
+	if (!client_list_parse_section(cs, false)) {
+		return -1;
+	}
+
+	/*
+	 *	Register the %{config:section.subsection} xlat function.
+	 */
+	xlat_register("config", xlat_config, NULL, NULL);
+	xlat_register("client", xlat_client, NULL, NULL);
+	xlat_register("getclient", xlat_getclient, NULL, NULL);
+	xlat_register("listen", xlat_listen, NULL, NULL);
+	xlat_register("proxy_listen", xlat_proxy_listen, NULL, NULL);
+
+	/*
+	 *  Go update our behaviour, based on the configuration
+	 *  changes.
+	 */
+
+	/*
+	 *	Sanity check the configuration for internal
+	 *	consistency.
+	 */
+	FR_TIMEVAL_BOUND_CHECK("reject_delay", &main_config.reject_delay, <=, main_config.cleanup_delay, 0);
+
+	if (chroot_dir) {
+		if (chdir(radlog_dir) < 0) {
+			ERROR("Failed to 'chdir %s' after chroot: %s",
+			       radlog_dir, fr_syserror(errno));
+			return -1;
+		}
+	}
+
+	cc = talloc_zero(NULL, cached_config_t);
+	if (!cc) return -1;
+
+	cc->cs = talloc_steal(cc ,cs);
+	rad_assert(cs_cache == NULL);
+	cs_cache = cc;
+
+	/* Clear any unprocessed configuration errors */
+	(void) fr_strerror();
+
+	return 0;
+}
+
+/*
+ *	Free the configuration.  Called only when the server is exiting.
+ */
+int main_config_free(void)
+{
+	virtual_servers_free(0);
+
+	/*
+	 *	Clean up the configuration data
+	 *	structures.
+	 */
+	client_list_free(NULL);
+	realms_free();
+	listen_free(&main_config.listen);
+
+	/*
+	 *	Frees current config and any previous configs.
+	 */
+	TALLOC_FREE(cs_cache);
+	dict_free();
+
+	return 0;
+}
+
+void hup_logfile(void)
+{
+	int fd, old_fd;
+
+	if (default_log.dst != L_DST_FILES) return;
+
+	fd = open(main_config.log_file,
+		  O_WRONLY | O_APPEND | O_CREAT, 0640);
+	if (fd >= 0) {
+		/*
+		 *	Atomic swap. We'd like to keep the old
+		 *	FD around so that callers don't
+		 *	suddenly find the FD closed, and the
+		 *	writes go nowhere.  But that's hard to
+		 *	do.  So... we have the case where a
+		 *	log message *might* be lost on HUP.
+		 */
+		old_fd = default_log.fd;
+		default_log.fd = fd;
+		close(old_fd);
+	}
+}
+
+static int hup_callback(void *ctx, void *data)
+{
+	CONF_SECTION *modules = ctx;
+	CONF_SECTION *cs = data;
+	CONF_SECTION *parent;
+	char const *name;
+	module_instance_t *mi;
+
+	/*
+	 *	Files may be defined in sub-sections of a module
+	 *	config.  Walk up the tree until we find the module
+	 *	definition.
+	 */
+	parent = cf_item_parent(cf_section_to_item(cs));
+	while (parent != modules) {
+		cs = parent;
+		parent = cf_item_parent(cf_section_to_item(cs));
+
+		/*
+		 *	Something went wrong.  Oh well...
+		 */
+		if (!parent) return 0;
+	}
+
+	name = cf_section_name2(cs);
+	if (!name) name = cf_section_name1(cs);
+
+	mi = module_find(modules, name);
+	if (!mi) return 0;
+
+	if ((mi->entry->module->type & RLM_TYPE_HUP_SAFE) == 0) return 0;
+
+	if (!module_hup_module(mi->cs, mi, time(NULL))) return 0;
+
+	return 1;
+}
+
+void main_config_hup(void)
+{
+	int rcode;
+	cached_config_t *cc;
+	CONF_SECTION *cs;
+	time_t when;
+	char buffer[1024];
+
+	static time_t last_hup = 0;
+
+	/*
+	 *	Re-open the log file.  If we can't, then keep logging
+	 *	to the old log file.
+	 *
+	 *	The "open log file" code is here rather than in log.c,
+	 *	because it makes that function MUCH simpler.
+	 */
+	hup_logfile();
+
+	/*
+	 *	Only check the config files every few seconds.
+	 */
+	when = time(NULL);
+	if ((last_hup + 2) >= when) {
+		INFO("HUP - Last HUP was too recent.  Ignoring");
+		return;
+	}
+	last_hup = when;
+
+	rcode = cf_file_changed(cs_cache->cs, hup_callback);
+	if (rcode == CF_FILE_NONE) {
+		INFO("HUP - No files changed.  Ignoring");
+		return;
+	}
+
+	if (rcode == CF_FILE_ERROR) {
+		INFO("HUP - Cannot read configuration files.  Ignoring");
+		return;
+	}
+
+	/*
+	 *	No config files have changed.
+	 */
+	if ((rcode & CF_FILE_CONFIG) == 0) {
+		if ((rcode & CF_FILE_MODULE) != 0) {
+			INFO("HUP - Files loaded by a module have changed.");
+
+			/*
+			 *	FIXME: reload the module.
+			 */
+
+		}
+		return;
+	}
+
+	cs = cf_section_alloc(NULL, "main", NULL);
+	if (!cs) return;
+
+#ifdef HAVE_SYSTEMD
+	sd_notify(0, "RELOADING=1");
+#endif
+
+	/* Read the configuration file */
+	snprintf(buffer, sizeof(buffer), "%.200s/%.50s.conf", radius_dir, main_config.name);
+
+	INFO("HUP - Re-reading configuration files");
+	if (cf_file_read(cs, buffer) < 0) {
+		ERROR("Failed to re-read or parse %s", buffer);
+		talloc_free(cs);
+		return;
+	}
+
+	cc = talloc_zero(cs_cache, cached_config_t);
+	if (!cc) {
+		ERROR("Out of memory");
+		return;
+	}
+
+	/*
+	 *	Save the current configuration.  Note that we do NOT
+	 *	free older ones.  We should probably do so at some
+	 *	point.  Doing so will require us to mark which modules
+	 *	are still in use, and which aren't.  Modules that
+	 *	can't be HUPed always use the original configuration.
+	 *	Modules that can be HUPed use one of the newer
+	 *	configurations.
+	 */
+	cc->created = time(NULL);
+	cc->cs = talloc_steal(cc, cs);
+	cc->next = cs_cache;
+	cs_cache = cc;
+
+	INFO("HUP - loading modules");
+
+	/*
+	 *	Prefer the new module configuration.
+	 */
+	modules_hup(cf_section_sub_find(cs, "modules"));
+
+	/*
+	 *	Load new servers BEFORE freeing old ones.
+	 */
+	virtual_servers_load(cs);
+
+	virtual_servers_free(cc->created - (main_config.max_request_time * 4));
+
+#ifdef HAVE_SYSTEMD
+	/*
+	 * If RELOADING=1 event is sent then it needed also a "READY=1" notification
+	 * when it completed reloading its configuration.
+	 */
+	sd_notify(0, "READY=1");
+#endif
+}
diff --git a/src/main/map.c b/src/main/map.c
new file mode 100644
index 0000000..e59fcec
--- /dev/null
+++ b/src/main/map.c
@@ -0,0 +1,1717 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/*
+ * $Id$
+ *
+ * @brief map / template functions
+ * @file main/map.c
+ *
+ * @ingroup AVP
+ *
+ * @copyright 2013 The FreeRADIUS server project
+ * @copyright 2013  Alan DeKok <aland@freeradius.org>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <ctype.h>
+
+#ifdef DEBUG_MAP
+static void map_dump(REQUEST *request, vp_map_t const *map)
+{
+	RDEBUG(">>> MAP TYPES LHS: %s, RHS: %s",
+	       fr_int2str(tmpl_names, map->lhs->type, "???"),
+	       fr_int2str(tmpl_names, map->rhs->type, "???"));
+
+	if (map->rhs) {
+		RDEBUG(">>> MAP NAMES %s %s", map->lhs->name, map->rhs->name);
+	}
+}
+#endif
+
+
+/** re-parse a map where the lhs is an unknown attribute.
+ *
+ *
+ * @param map to process.
+ * @param rhs_type quotation type around rhs.
+ * @param rhs string to re-parse.
+ */
+bool map_cast_from_hex(vp_map_t *map, FR_TOKEN rhs_type, char const *rhs)
+{
+	size_t len;
+	ssize_t rlen;
+	uint8_t *ptr;
+	char const *p;
+	pair_lists_t list;
+
+	DICT_ATTR const *da;
+	VALUE_PAIR *vp;
+	vp_tmpl_t *vpt;
+
+	rad_assert(map != NULL);
+
+	rad_assert(map->lhs != NULL);
+	rad_assert(map->lhs->type == TMPL_TYPE_ATTR);
+
+	rad_assert(map->rhs == NULL);
+	rad_assert(rhs != NULL);
+
+	VERIFY_MAP(map);
+
+	/*
+	 *	If the attribute is still unknown, go parse the RHS.
+	 */
+	da = dict_attrbyvalue(map->lhs->tmpl_da->attr, map->lhs->tmpl_da->vendor);
+	if (!da || da->flags.is_unknown) return false;
+
+	/*
+	 *	If the RHS is something OTHER than an octet
+	 *	string, go parse it as that.
+	 */
+	if (rhs_type != T_BARE_WORD) return false;
+	if ((rhs[0] != '0') || (tolower((uint8_t)rhs[1]) != 'x')) return false;
+	if (!rhs[2]) return false;
+
+	len = strlen(rhs + 2);
+
+	ptr = talloc_array(map, uint8_t, len >> 1);
+	if (!ptr) return false;
+
+	len = fr_hex2bin(ptr, len >> 1, rhs + 2, len);
+
+	/*
+	 *	If we can't parse it, or if it's malformed,
+	 *	it's still unknown.
+	 */
+	rlen = data2vp(NULL, NULL, NULL, NULL, da, ptr, len, len, &vp);
+	talloc_free(ptr);
+
+	if (rlen < 0) return false;
+
+	if ((size_t) rlen < len) {
+	free_vp:
+		fr_pair_list_free(&vp);
+		return false;
+	}
+
+	/*
+	 *	Was still parsed as an unknown attribute.
+	 */
+	if (vp->da->flags.is_unknown) goto free_vp;
+
+	/*
+	 *	Set the RHS to the PARSED name, not the crap octet
+	 *	string which was input.
+	 */
+	map->rhs = tmpl_alloc(map, TMPL_TYPE_DATA, NULL, 0);
+	if (!map->rhs) goto free_vp;
+
+	map->rhs->tmpl_data_type = da->type;
+	map->rhs->tmpl_data_length = vp->vp_length;
+	if (vp->da->flags.is_pointer) {
+		if (vp->da->type == PW_TYPE_STRING) {
+			map->rhs->tmpl_data_value.ptr = talloc_bstrndup(map->rhs, vp->data.ptr, vp->vp_length);
+		} else {
+			map->rhs->tmpl_data_value.ptr = talloc_memdup(map->rhs, vp->data.ptr, vp->vp_length);
+		}
+	} else {
+		memcpy(&map->rhs->tmpl_data_value, &vp->data, sizeof(map->rhs->tmpl_data_value));
+	}
+	map->rhs->name = vp_aprints_value(map->rhs, vp, '"');
+	map->rhs->len = talloc_array_length(map->rhs->name) - 1;
+
+	/*
+	 *	Set the LHS to the REAL attribute name.
+	 */
+	vpt = tmpl_alloc(map, TMPL_TYPE_ATTR, map->lhs->tmpl_da->name, -1);
+	memcpy(&vpt->data.attribute, &map->lhs->data.attribute, sizeof(vpt->data.attribute));
+	vpt->tmpl_da = da;
+
+	/*
+	 *	Be sure to keep the "&control:" or "control:" prefix.
+	 *	If it's there, we re-generate it from whatever was in
+	 *	the original name, including the '&'.
+	 */
+	p = map->lhs->name;
+	if (*p == '&') p++;
+	len = radius_list_name(&list, p, PAIR_LIST_UNKNOWN);
+
+	if (list != PAIR_LIST_UNKNOWN) {
+		rad_const_free(vpt->name);
+
+		vpt->name = talloc_asprintf(vpt, "%.*s:%s",
+					    (int) len, map->lhs->name,
+					    map->lhs->tmpl_da->name);
+		vpt->len = strlen(vpt->name);
+	}
+
+	talloc_free(map->lhs);
+	map->lhs = vpt;
+
+	fr_pair_list_free(&vp);
+
+	VERIFY_MAP(map);
+
+	return true;
+}
+
+/** Convert CONFIG_PAIR (which may contain refs) to vp_map_t.
+ *
+ * Treats the left operand as an attribute reference
+ * @verbatim<request>.<list>.<attribute>@endverbatim
+ *
+ * Treatment of left operand depends on quotation, barewords are treated as
+ * attribute references, double quoted values are treated as expandable strings,
+ * single quoted values are treated as literal strings.
+ *
+ * Return must be freed with talloc_free
+ *
+ * @param[in] ctx for talloc.
+ * @param[in] out Where to write the pointer to the new value_pair_map_struct.
+ * @param[in] cp to convert to map.
+ * @param[in] dst_request_def The default request to insert unqualified
+ *	attributes into.
+ * @param[in] dst_list_def The default list to insert unqualified attributes
+ *	into.
+ * @param[in] src_request_def The default request to resolve attribute
+ *	references in.
+ * @param[in] src_list_def The default list to resolve unqualified attributes
+ *	in.
+ * @return vp_map_t if successful or NULL on error.
+ */
+int map_afrom_cp(TALLOC_CTX *ctx, vp_map_t **out, CONF_PAIR *cp,
+		 request_refs_t dst_request_def, pair_lists_t dst_list_def,
+		 request_refs_t src_request_def, pair_lists_t src_list_def)
+{
+	vp_map_t *map;
+	char const *attr, *value;
+	ssize_t slen;
+	FR_TOKEN type;
+
+	*out = NULL;
+
+	if (!cp) return -1;
+
+	map = talloc_zero(ctx, vp_map_t);
+	map->op = cf_pair_operator(cp);
+	map->ci = cf_pair_to_item(cp);
+
+	attr = cf_pair_attr(cp);
+	value = cf_pair_value(cp);
+	if (!value) {
+		cf_log_err_cp(cp, "Missing attribute value");
+		goto error;
+	}
+
+	/*
+	 *	LHS may be an expansion (that expands to an attribute reference)
+	 *	or an attribute reference. Quoting determines which it is.
+	 */
+	type = cf_pair_attr_type(cp);
+	switch (type) {
+	case T_DOUBLE_QUOTED_STRING:
+	case T_BACK_QUOTED_STRING:
+		slen = tmpl_afrom_str(ctx, &map->lhs, attr, talloc_array_length(attr) - 1,
+				      type, dst_request_def, dst_list_def, true);
+		if (slen <= 0) {
+			char *spaces, *text;
+
+		marker:
+			fr_canonicalize_error(ctx, &spaces, &text, slen, attr);
+			cf_log_err_cp(cp, "%s", text);
+			cf_log_err_cp(cp, "%s^ %s", spaces, fr_strerror());
+
+			talloc_free(spaces);
+			talloc_free(text);
+			goto error;
+		}
+		break;
+
+	case T_BARE_WORD:
+		/*
+		 *	Foo = %{...}
+		 *
+		 *	Not allowed!
+		 */
+		if ((attr[0] == '%') && (attr[1] == '{')) {
+			cf_log_err_cp(cp, "Bare expansions are not permitted.  They must be in a double-quoted string.");
+			goto error;
+		}
+		/* FALL-THROUGH */
+
+	default:
+		slen = tmpl_afrom_attr_str(ctx, &map->lhs, attr, dst_request_def, dst_list_def, true, true);
+		if (slen <= 0) {
+			cf_log_err_cp(cp, "Failed parsing attribute reference");
+
+			goto marker;
+		}
+
+		if (tmpl_define_unknown_attr(map->lhs) < 0) {
+			cf_log_err_cp(cp, "Failed creating attribute %s: %s",
+				      map->lhs->name, fr_strerror());
+			goto error;
+		}
+
+		break;
+	}
+
+	/*
+	 *	RHS might be an attribute reference.
+	 */
+	type = cf_pair_value_type(cp);
+
+	if ((map->lhs->type == TMPL_TYPE_ATTR) &&
+	    map->lhs->tmpl_da->flags.is_unknown &&
+	    !map_cast_from_hex(map, type, value)) {
+		goto error;
+
+	} else {
+		slen = tmpl_afrom_str(map, &map->rhs, value, strlen(value), type, src_request_def, src_list_def, true);
+		if (slen < 0) goto marker;
+		if (tmpl_define_unknown_attr(map->rhs) < 0) {
+			cf_log_err_cp(cp, "Failed creating attribute %s: %s", map->rhs->name, fr_strerror());
+			goto error;
+		}
+	}
+	if (!map->rhs) {
+		cf_log_err_cp(cp, "%s", fr_strerror());
+		goto error;
+	}
+
+	if (map->rhs->type == TMPL_TYPE_ATTR) {
+		/*
+		 *	We cannot assign a count to an attribute.  That must
+		 *	be done in an xlat.
+		 */
+		if (map->rhs->tmpl_num == NUM_COUNT) {
+			cf_log_err_cp(cp, "Cannot assign from a count");
+			goto error;
+		}
+
+		if (map->rhs->tmpl_da->flags.virtual) {
+			cf_log_err_cp(cp, "Virtual attributes must be in an expansion such as \"%%{%s}\".", map->rhs->tmpl_da->name);
+			goto error;
+		}
+	}
+
+	VERIFY_MAP(map);
+
+	*out = map;
+
+	return 0;
+
+error:
+	talloc_free(map);
+	return -1;
+}
+
+/** Convert an 'update' config section into an attribute map.
+ *
+ * Uses 'name2' of section to set default request and lists.
+ *
+ * @param[in] cs the update section
+ * @param[out] out Where to store the head of the map.
+ * @param[in] dst_list_def The default destination list, usually dictated by
+ * 	the section the module is being called in.
+ * @param[in] src_list_def The default source list, usually dictated by the
+ *	section the module is being called in.
+ * @param[in] validate map using this callback (may be NULL).
+ * @param[in] ctx to pass to callback.
+ * @param[in] max number of mappings to process.
+ * @return -1 on error, else 0.
+ */
+int map_afrom_cs(vp_map_t **out, CONF_SECTION *cs,
+		 pair_lists_t dst_list_def, pair_lists_t src_list_def,
+		 map_validate_t validate, void *ctx,
+		 unsigned int max)
+{
+	char const *cs_list, *p;
+
+	request_refs_t request_def = REQUEST_CURRENT;
+
+	CONF_ITEM *ci;
+	CONF_PAIR *cp;
+
+	unsigned int total = 0;
+	vp_map_t **tail, *map;
+	TALLOC_CTX *parent;
+
+	*out = NULL;
+	tail = out;
+
+	/*
+	 *	The first map has cs as the parent.
+	 *	The rest have the previous map as the parent.
+	 */
+	parent = cs;
+
+	ci = cf_section_to_item(cs);
+
+	cs_list = p = cf_section_name2(cs);
+	if (cs_list) {
+		p += radius_request_name(&request_def, p, REQUEST_CURRENT);
+		if (request_def == REQUEST_UNKNOWN) {
+			cf_log_err(ci, "Default request specified in mapping section is invalid");
+			return -1;
+		}
+
+		dst_list_def = fr_str2int(pair_lists, p, PAIR_LIST_UNKNOWN);
+		if (dst_list_def == PAIR_LIST_UNKNOWN) {
+			cf_log_err(ci, "Default list \"%s\" specified "
+				   "in mapping section is invalid", p);
+			return -1;
+		}
+	}
+
+	for (ci = cf_item_find_next(cs, NULL);
+	     ci != NULL;
+	     ci = cf_item_find_next(cs, ci)) {
+		if (total++ == max) {
+			cf_log_err(ci, "Map size exceeded");
+		error:
+			TALLOC_FREE(*out);
+			return -1;
+		}
+
+		if (!cf_item_is_pair(ci)) {
+			cf_log_err(ci, "Entry is not in \"attribute = value\" format");
+			goto error;
+		}
+
+		cp = cf_item_to_pair(ci);
+		if (map_afrom_cp(parent, &map, cp, request_def, dst_list_def, REQUEST_CURRENT, src_list_def) < 0) {
+			goto error;
+		}
+
+		VERIFY_MAP(map);
+
+		/*
+		 *	Check the types in the map are valid
+		 */
+		if (validate && (validate(map, ctx) < 0)) goto error;
+
+		parent = *tail = map;
+		tail = &(map->next);
+	}
+
+	return 0;
+
+}
+
+
+/** Convert strings to vp_map_t
+ *
+ * Treatment of operands depends on quotation, barewords are treated
+ * as attribute references, double quoted values are treated as
+ * expandable strings, single quoted values are treated as literal
+ * strings.
+ *
+ * Return must be freed with talloc_free
+ *
+ * @param[in] ctx for talloc
+ * @param[out] out Where to store the head of the map.
+ * @param[in] lhs of the operation
+ * @param[in] lhs_type type of the LHS string
+ * @param[in] op the operation to perform
+ * @param[in] rhs of the operation
+ * @param[in] rhs_type type of the RHS string
+ * @param[in] dst_request_def The default request to insert unqualified
+ *	attributes into.
+ * @param[in] dst_list_def The default list to insert unqualified attributes
+ *	into.
+ * @param[in] src_request_def The default request to resolve attribute
+ *	references in.
+ * @param[in] src_list_def The default list to resolve unqualified attributes
+ *	in.
+ * @return vp_map_t if successful or NULL on error.
+ */
+int map_afrom_fields(TALLOC_CTX *ctx, vp_map_t **out, char const *lhs, FR_TOKEN lhs_type,
+		     FR_TOKEN op, char const *rhs, FR_TOKEN rhs_type,
+		     request_refs_t dst_request_def,
+		     pair_lists_t dst_list_def,
+		     request_refs_t src_request_def,
+		     pair_lists_t src_list_def)
+{
+	ssize_t slen;
+	vp_map_t *map;
+
+	map = talloc_zero(ctx, vp_map_t);
+
+	slen = tmpl_afrom_str(map, &map->lhs, lhs, strlen(lhs), lhs_type, dst_request_def, dst_list_def, true);
+	if (slen < 0) {
+	error:
+		talloc_free(map);
+		return -1;
+	}
+
+	map->op = op;
+
+	if ((map->lhs->type == TMPL_TYPE_ATTR) &&
+	    map->lhs->tmpl_da->flags.is_unknown &&
+	    map_cast_from_hex(map, rhs_type, rhs)) {
+		return 0;
+	}
+
+	slen = tmpl_afrom_str(map, &map->rhs, rhs, strlen(rhs), rhs_type, src_request_def, src_list_def, true);
+	if (slen < 0) goto error;
+
+	VERIFY_MAP(map);
+
+	*out = map;
+
+	return 0;
+}
+
+/** Convert a value pair string to valuepair map
+ *
+ * Takes a valuepair string with list and request qualifiers and converts it into a
+ * vp_map_t.
+ *
+ * @param ctx where to allocate the map.
+ * @param out Where to write the new map (must be freed with talloc_free()).
+ * @param vp_str string to parse.
+ * @param dst_request_def to use if attribute isn't qualified.
+ * @param dst_list_def to use if attribute isn't qualified.
+ * @param src_request_def to use if attribute isn't qualified.
+ * @param src_list_def to use if attribute isn't qualified.
+ * @return 0 on success, < 0 on error.
+ */
+int map_afrom_attr_str(TALLOC_CTX *ctx, vp_map_t **out, char const *vp_str,
+		       request_refs_t dst_request_def, pair_lists_t dst_list_def,
+		       request_refs_t src_request_def, pair_lists_t src_list_def)
+{
+	char const *p = vp_str;
+	FR_TOKEN quote;
+
+	VALUE_PAIR_RAW raw;
+	vp_map_t *map = NULL;
+
+	quote = gettoken(&p, raw.l_opand, sizeof(raw.l_opand), false);
+	switch (quote) {
+	case T_BARE_WORD:
+		break;
+
+	case T_INVALID:
+	error:
+		return -1;
+
+	default:
+		fr_strerror_printf("Left operand must be an attribute");
+		return -1;
+	}
+
+	raw.op = getop(&p);
+	if (raw.op == T_INVALID) goto error;
+
+	raw.quote = gettoken(&p, raw.r_opand, sizeof(raw.r_opand), false);
+	if (raw.quote == T_INVALID) goto error;
+	if (!fr_str_tok[raw.quote]) {
+		fr_strerror_printf("Right operand must be an attribute or string");
+		return -1;
+	}
+
+	if (map_afrom_fields(ctx, &map, raw.l_opand, T_BARE_WORD, raw.op, raw.r_opand, raw.quote,
+			     dst_request_def, dst_list_def, src_request_def, src_list_def) < 0) {
+		return -1;
+	}
+
+	rad_assert(map != NULL);
+	*out = map;
+
+	VERIFY_MAP(map);
+
+	return 0;
+}
+
+/** Compare map where LHS is #TMPL_TYPE_ATTR
+ *
+ * Compares maps by lhs->tmpl_da, lhs->tmpl_tag, lhs->tmpl_num
+ *
+ * @note both map->lhs must be #TMPL_TYPE_ATTR.
+ *
+ * @param a first map.
+ * @param b second map.
+ */
+int8_t map_cmp_by_lhs_attr(void const *a, void const *b)
+{
+	vp_tmpl_t const *my_a = ((vp_map_t const *)a)->lhs;
+	vp_tmpl_t const *my_b = ((vp_map_t const *)b)->lhs;
+
+	VERIFY_TMPL(my_a);
+	VERIFY_TMPL(my_b);
+
+	uint8_t cmp;
+
+	rad_assert(my_a->type == TMPL_TYPE_ATTR);
+	rad_assert(my_b->type == TMPL_TYPE_ATTR);
+
+	cmp = fr_pointer_cmp(my_a->tmpl_da, my_b->tmpl_da);
+	if (cmp != 0) return cmp;
+
+	if (my_a->tmpl_tag < my_b->tmpl_tag) return -1;
+
+	if (my_a->tmpl_tag > my_b->tmpl_tag) return 1;
+
+	if (my_a->tmpl_num < my_b->tmpl_num) return -1;
+
+	if (my_a->tmpl_num > my_b->tmpl_num) return 1;
+
+	return 0;
+}
+
+static void map_sort_split(vp_map_t *source, vp_map_t **front, vp_map_t **back)
+{
+	vp_map_t *fast;
+	vp_map_t *slow;
+
+	/*
+	 *	Stopping condition - no more elements left to split
+	 */
+	if (!source || !source->next) {
+		*front = source;
+		*back = NULL;
+
+		return;
+	}
+
+	/*
+	 *	Fast advances twice as fast as slow, so when it gets to the end,
+	 *	slow will point to the middle of the linked list.
+	 */
+	slow = source;
+	fast = source->next;
+
+	while (fast) {
+		fast = fast->next;
+		if (fast) {
+			slow = slow->next;
+			fast = fast->next;
+		}
+	}
+
+	*front = source;
+	*back = slow->next;
+	slow->next = NULL;
+}
+
+static vp_map_t *map_sort_merge(vp_map_t *a, vp_map_t *b, fr_cmp_t cmp)
+{
+	vp_map_t *result = NULL;
+
+	if (!a) return b;
+	if (!b) return a;
+
+	/*
+	 *	Compare things in the maps
+	 */
+	if (cmp(a, b) <= 0) {
+		result = a;
+		result->next = map_sort_merge(a->next, b, cmp);
+	} else {
+		result = b;
+		result->next = map_sort_merge(a, b->next, cmp);
+	}
+
+	return result;
+}
+
+/** Sort a linked list of #vp_map_t using merge sort
+ *
+ * @param[in,out] maps List of #vp_map_t to sort.
+ * @param[in] cmp to sort with
+ */
+void map_sort(vp_map_t **maps, fr_cmp_t cmp)
+{
+	vp_map_t *head = *maps;
+	vp_map_t *a;
+	vp_map_t *b;
+
+	/*
+	 *	If there's 0-1 elements it must already be sorted.
+	 */
+	if (!head || !head->next) {
+		return;
+	}
+
+	map_sort_split(head, &a, &b);	/* Split into sublists */
+	map_sort(&a, cmp);		/* Traverse left */
+	map_sort(&b, cmp);		/* Traverse right */
+
+	/*
+	 *	merge the two sorted lists together
+	 */
+	*maps = map_sort_merge(a, b, cmp);
+}
+
+/** Process map which has exec as a src
+ *
+ * Evaluate maps which specify exec as a src. This may be used by various sorts of update sections,
+ * and so has been broken out into it's own function.
+ *
+ * @param[in,out] ctx to allocate new #VALUE_PAIR (s) in.
+ * @param[out] out Where to write the #VALUE_PAIR (s).
+ * @param[in] request structure (used only for talloc).
+ * @param[in] map the map. The LHS (dst) must be TMPL_TYPE_ATTR or TMPL_TYPE_LIST. The RHS (src)
+ *	must be TMPL_TYPE_EXEC.
+ * @return -1 on failure, 0 on success.
+ */
+static int map_exec_to_vp(TALLOC_CTX *ctx, VALUE_PAIR **out, REQUEST *request, vp_map_t const *map)
+{
+	int result;
+	char *expanded = NULL;
+	char answer[1024];
+	VALUE_PAIR **input_pairs = NULL;
+	VALUE_PAIR *output_pairs = NULL;
+
+	*out = NULL;
+
+	VERIFY_MAP(map);
+
+	rad_assert(map->rhs->type == TMPL_TYPE_EXEC);
+	rad_assert((map->lhs->type == TMPL_TYPE_ATTR) || (map->lhs->type == TMPL_TYPE_LIST));
+
+	/*
+	 *	We always put the request pairs into the environment
+	 */
+	input_pairs = radius_list(request, PAIR_LIST_REQUEST);
+
+	/*
+	 *	Automagically switch output type depending on our destination
+	 *	If dst is a list, then we create attributes from the output of the program
+	 *	if dst is an attribute, then we create an attribute of that type and then
+	 *	call fr_pair_value_from_str on the output of the script.
+	 */
+	result = radius_exec_program(ctx, answer, sizeof(answer),
+				     (map->lhs->type == TMPL_TYPE_LIST) ? &output_pairs : NULL,
+				     request, map->rhs->name, input_pairs ? *input_pairs : NULL,
+				     true, true, EXEC_TIMEOUT);
+	talloc_free(expanded);
+	if (result != 0) {
+		talloc_free(output_pairs);
+		return -1;
+	}
+
+	switch (map->lhs->type) {
+	case TMPL_TYPE_LIST:
+		if (!output_pairs) {
+			REDEBUG("No valid attributes received from program");
+			return -2;
+		}
+		*out = output_pairs;
+		return 0;
+
+	case TMPL_TYPE_ATTR:
+	{
+		VALUE_PAIR *vp;
+
+		vp = fr_pair_afrom_da(ctx, map->lhs->tmpl_da);
+		if (!vp) return -1;
+		vp->op = map->op;
+		vp->tag = map->lhs->tmpl_tag;
+		if (fr_pair_value_from_str(vp, answer, -1) < 0) {
+			fr_pair_list_free(&vp);
+			return -2;
+		}
+		*out = vp;
+
+		return 0;
+	}
+
+	default:
+		rad_assert(0);
+	}
+
+	return -1;
+}
+
+/** Convert a map to a VALUE_PAIR.
+ *
+ * @param[in,out] ctx to allocate #VALUE_PAIR (s) in.
+ * @param[out] out Where to write the #VALUE_PAIR (s), which may be NULL if not found
+ * @param[in] request The current request.
+ * @param[in] map the map. The LHS (dst) has to be #TMPL_TYPE_ATTR or #TMPL_TYPE_LIST.
+ * @param[in] uctx unused.
+ * @return
+ *	- 0 on success.
+ *	- -1 on failure.
+ */
+int map_to_vp(TALLOC_CTX *ctx, VALUE_PAIR **out, REQUEST *request, vp_map_t const *map, UNUSED void *uctx)
+{
+	int rcode = 0;
+	ssize_t len;
+	VALUE_PAIR *vp = NULL, *new, *found = NULL;
+	REQUEST *context = request;
+	vp_cursor_t cursor;
+	ssize_t slen;
+	char *str;
+
+	*out = NULL;
+
+	VERIFY_MAP(map);
+	rad_assert(map->lhs != NULL);
+	rad_assert(map->rhs != NULL);
+
+	rad_assert((map->lhs->type == TMPL_TYPE_LIST) || (map->lhs->type == TMPL_TYPE_ATTR));
+
+	/*
+	 *	Special case for !*, we don't need to parse RHS as this is a unary operator.
+	 */
+	if (map->op == T_OP_CMP_FALSE) return 0;
+
+	/*
+	 *	List to list found, this is a special case because we don't need
+	 *	to allocate any attributes, just finding the current list, and change
+	 *	the op.
+	 */
+	if ((map->lhs->type == TMPL_TYPE_LIST) && (map->rhs->type == TMPL_TYPE_LIST)) {
+		VALUE_PAIR **from = NULL;
+
+		if (radius_request(&context, map->rhs->tmpl_request) == 0) {
+			from = radius_list(context, map->rhs->tmpl_list);
+		}
+		if (!from) return 0;
+
+		found = fr_pair_list_copy(ctx, *from);
+
+		/*
+		 *	List to list copy is empty if the src list has no attributes.
+		 */
+		if (!found) return 0;
+
+		for (vp = fr_cursor_init(&cursor, &found);
+		     vp;
+		     vp = fr_cursor_next(&cursor)) {
+			vp->op = T_OP_ADD;
+		}
+
+		*out = found;
+
+		return 0;
+	}
+
+	/*
+	 *	And parse the RHS
+	 */
+	switch (map->rhs->type) {
+	case TMPL_TYPE_XLAT_STRUCT:
+		rad_assert(map->lhs->type == TMPL_TYPE_ATTR);
+		rad_assert(map->lhs->tmpl_da);	/* We need to know which attribute to create */
+		rad_assert(map->rhs->tmpl_xlat != NULL);
+
+		new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da);
+		if (!new) return -1;
+
+		str = NULL;
+		slen = radius_axlat_struct(&str, request, map->rhs->tmpl_xlat, NULL, NULL);
+		if (slen < 0) {
+			rcode = slen;
+			goto error;
+		}
+
+		/*
+		 *	We do the debug printing because radius_axlat_struct
+		 *	doesn't have access to the original string.  It's been
+		 *	mangled during the parsing to xlat_exp_t
+		 */
+		RDEBUG2("EXPAND %s", map->rhs->name);
+		RDEBUG2("   --> %s", str);
+
+		rcode = fr_pair_value_from_str(new, str, -1);
+		talloc_free(str);
+		if (rcode < 0) {
+			fr_pair_list_free(&new);
+			goto error;
+		}
+		new->op = map->op;
+		new->tag = map->lhs->tmpl_tag;
+		*out = new;
+		break;
+
+	case TMPL_TYPE_XLAT:
+		rad_assert(map->lhs->type == TMPL_TYPE_ATTR);
+		rad_assert(map->lhs->tmpl_da);	/* We need to know which attribute to create */
+
+		new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da);
+		if (!new) return -1;
+
+		str = NULL;
+		slen = radius_axlat(&str, request, map->rhs->name, NULL, NULL);
+		if (slen < 0) {
+			rcode = slen;
+			goto error;
+		}
+
+		rcode = fr_pair_value_from_str(new, str, -1);
+		talloc_free(str);
+		if (rcode < 0) {
+			fr_pair_list_free(&new);
+			goto error;
+		}
+		new->op = map->op;
+		new->tag = map->lhs->tmpl_tag;
+		*out = new;
+		break;
+
+	case TMPL_TYPE_LITERAL:
+		rad_assert(map->lhs->type == TMPL_TYPE_ATTR);
+		rad_assert(map->lhs->tmpl_da);	/* We need to know which attribute to create */
+
+		new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da);
+		if (!new) return -1;
+
+		if (fr_pair_value_from_str(new, map->rhs->name, -1) < 0) {
+			rcode = 0;
+			goto error;
+		}
+		new->op = map->op;
+		new->tag = map->lhs->tmpl_tag;
+		*out = new;
+		break;
+
+	case TMPL_TYPE_ATTR:
+	{
+		vp_cursor_t from;
+
+		rad_assert(((map->lhs->type == TMPL_TYPE_ATTR) && map->lhs->tmpl_da) ||
+			   ((map->lhs->type == TMPL_TYPE_LIST) && !map->lhs->tmpl_da));
+
+		/*
+		 * @todo should log error, and return -1 for v3.1 (causes update to fail)
+		 */
+		if (tmpl_copy_vps(ctx, &found, request, map->rhs) < 0) return 0;
+
+		vp = fr_cursor_init(&from, &found);
+
+		/*
+		 *  Src/Dst attributes don't match, convert src attributes
+		 *  to match dst.
+		 */
+		if ((map->lhs->type == TMPL_TYPE_ATTR) &&
+		    (map->rhs->tmpl_da->type != map->lhs->tmpl_da->type)) {
+			vp_cursor_t to;
+
+			(void) fr_cursor_init(&to, out);
+			for (; vp; vp = fr_cursor_next(&from)) {
+				new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da);
+				if (!new) return -1;
+
+				len = value_data_cast(new, &new->data, new->da->type, new->da,
+						      vp->da->type, vp->da, &vp->data, vp->vp_length);
+				if (len < 0) {
+					REDEBUG("Attribute conversion failed: %s", fr_strerror());
+					fr_pair_list_free(&found);
+					fr_pair_list_free(&new);
+					return -1;
+				}
+
+				new->vp_length = len;
+				vp = fr_cursor_remove(&from);
+				talloc_free(vp);
+
+				if (new->da->type == PW_TYPE_STRING) {
+					rad_assert(new->vp_strvalue != NULL);
+				}
+
+				new->op = map->op;
+				new->tag = map->lhs->tmpl_tag;
+				fr_cursor_insert(&to, new);
+			}
+			return 0;
+		}
+
+		/*
+		 *   Otherwise we just need to fixup the attribute types
+		 *   and operators
+		 */
+		for (; vp; vp = fr_cursor_next(&from)) {
+			vp->da = map->lhs->tmpl_da;
+			vp->op = map->op;
+			vp->tag = map->lhs->tmpl_tag;
+		}
+		*out = found;
+	}
+		break;
+
+	case TMPL_TYPE_DATA:
+		rad_assert(map->lhs->tmpl_da);
+		rad_assert(map->lhs->type == TMPL_TYPE_ATTR);
+		rad_assert(map->lhs->tmpl_da->type == map->rhs->tmpl_data_type);
+
+		new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da);
+		if (!new) return -1;
+
+		len = value_data_copy(new, &new->data, new->da->type, &map->rhs->tmpl_data_value,
+				      map->rhs->tmpl_data_length);
+		if (len < 0) goto error;
+
+		new->vp_length = len;
+		new->op = map->op;
+		new->tag = map->lhs->tmpl_tag;
+		*out = new;
+
+		VERIFY_MAP(map);
+		break;
+
+	/*
+	 *	This essentially does the same as rlm_exec xlat, except it's non-configurable.
+	 *	It's only really here as a convenience for people who expect the contents of
+	 *	backticks to be executed in a shell.
+	 *
+	 *	exec string is xlat expanded and arguments are shell escaped.
+	 */
+	case TMPL_TYPE_EXEC:
+		return map_exec_to_vp(ctx, out, request, map);
+
+	default:
+		rad_assert(0);	/* Should have been caught at parse time */
+
+	error:
+		fr_pair_list_free(&vp);
+		return rcode;
+	}
+
+	return 0;
+}
+
+#define DEBUG_OVERWRITE(_old, _new) \
+do {\
+	if (RDEBUG_ENABLED3) {\
+		char *old = vp_aprints_value(request, _old, '"');\
+		char *new = vp_aprints_value(request, _new, '"');\
+		RDEBUG3("Overwriting value \"%s\" with \"%s\"", old, new);\
+		talloc_free(old);\
+		talloc_free(new);\
+	}\
+} while (0)
+
+/** Convert vp_map_t to VALUE_PAIR(s) and add them to a REQUEST.
+ *
+ * Takes a single vp_map_t, resolves request and list identifiers
+ * to pointers in the current request, then attempts to retrieve module
+ * specific value(s) using callback, and adds the resulting values to the
+ * correct request/list.
+ *
+ * @param request The current request.
+ * @param map specifying destination attribute and location and src identifier.
+ * @param func to retrieve module specific values and convert them to
+ *	VALUE_PAIRS.
+ * @param ctx to be passed to func.
+ * @return -1 if the operation failed, -2 in the source attribute wasn't valid, 0 on success.
+ */
+int map_to_request(REQUEST *request, vp_map_t const *map, radius_map_getvalue_t func, void *ctx)
+{
+	int rcode = 0;
+	int num;
+	VALUE_PAIR **list, *vp, *dst, *head = NULL;
+	bool found = false;
+	REQUEST *context;
+	TALLOC_CTX *parent;
+	vp_cursor_t dst_list, src_list;
+
+	vp_map_t	exp_map;
+	vp_tmpl_t	exp_lhs;
+
+	VERIFY_MAP(map);
+	rad_assert(map->lhs != NULL);
+	rad_assert(map->rhs != NULL);
+
+	/*
+	 *	Preprocessing of the LHS of the map.
+	 */
+	switch (map->lhs->type) {
+	/*
+	 *	Already in the correct form.
+	 */
+	case TMPL_TYPE_LIST:
+	case TMPL_TYPE_ATTR:
+		break;
+
+	/*
+	 *	Everything else gets expanded, then re-parsed as an
+	 *	attribute reference.
+	 */
+	case TMPL_TYPE_XLAT:
+	case TMPL_TYPE_XLAT_STRUCT:
+	case TMPL_TYPE_EXEC:
+	{
+		char *attr;
+		ssize_t slen;
+
+		slen = tmpl_aexpand(request, &attr, request, map->lhs, NULL, NULL);
+		if (slen <= 0) {
+			REDEBUG("Left side \"%.*s\" of map failed expansion", (int)map->lhs->len, map->lhs->name);
+			return -1;
+		}
+
+		slen = tmpl_from_attr_str(&exp_lhs, attr, REQUEST_CURRENT, PAIR_LIST_REQUEST, false, false) ;
+		if (slen <= 0) {
+			REDEBUG("Left side \"%.*s\" expansion not an attribute reference: %s",
+				(int)map->lhs->len, map->lhs->name, fr_strerror());
+			talloc_free(attr);
+			return -1;
+		}
+		rad_assert((exp_lhs.type == TMPL_TYPE_ATTR) || (exp_lhs.type == TMPL_TYPE_LIST));
+
+		memcpy(&exp_map, map, sizeof(exp_map));
+		exp_map.lhs = &exp_lhs;
+		map = &exp_map;
+	}
+		break;
+
+	default:
+		rad_assert(0);
+		break;
+	}
+
+
+	/*
+	 *	Sanity check inputs.  We can have a list or attribute
+	 *	as a destination.
+	 */
+	if ((map->lhs->type != TMPL_TYPE_LIST) &&
+	    (map->lhs->type != TMPL_TYPE_ATTR)) {
+		REDEBUG("Left side \"%.*s\" of map should be an attr or list but is an %s",
+			(int)map->lhs->len, map->lhs->name,
+			fr_int2str(tmpl_names, map->lhs->type, "<INVALID>"));
+		return -2;
+	}
+
+	context = request;
+	if (radius_request(&context, map->lhs->tmpl_request) < 0) {
+		REDEBUG("Mapping \"%.*s\" -> \"%.*s\" invalid in this context",
+			(int)map->rhs->len, map->rhs->name, (int)map->lhs->len, map->lhs->name);
+		return -2;
+	}
+
+	/*
+	 *	If there's no CoA packet and we're updating it,
+	 *	auto-allocate it.
+	 */
+	if (((map->lhs->tmpl_list == PAIR_LIST_COA) ||
+	     (map->lhs->tmpl_list == PAIR_LIST_DM)) && !request->coa) {
+		if (request->parent) {
+			REDEBUG("You can only do 'update coa' when processing a packet which was received from the network");
+			return -2;
+		}
+
+		if ((request->packet->code == PW_CODE_COA_REQUEST) ||
+		    (request->packet->code == PW_CODE_DISCONNECT_REQUEST)) {
+			REDEBUG("You cannot do 'update coa' when processing a CoA / Disconnect request.  Use 'update request' instead.");
+			return -2;
+		}
+
+		if (!request_alloc_coa(context)) {
+			REDEBUG("Failed to create a CoA/Disconnect Request message");
+			return -2;
+		}
+		context->coa->proxy->code = (map->lhs->tmpl_list == PAIR_LIST_COA) ?
+					    PW_CODE_COA_REQUEST :
+					    PW_CODE_DISCONNECT_REQUEST;
+	}
+
+	list = radius_list(context, map->lhs->tmpl_list);
+	if (!list) {
+		REDEBUG("Mapping \"%.*s\" -> \"%.*s\" invalid in this context",
+			(int)map->rhs->len, map->rhs->name, (int)map->lhs->len, map->lhs->name);
+
+		return -2;
+	}
+
+	parent = radius_list_ctx(context, map->lhs->tmpl_list);
+	if (!parent) {
+		REDEBUG("Unable to set parent list");
+		return -1;
+	}
+
+	/*
+	 *	The callback should either return -1 to signify operations error,
+	 *	-2 when it can't find the attribute or list being referenced, or
+	 *	0 to signify success. It may return "success", but still have no
+	 *	VPs to work with.
+	 */
+	if (map->rhs->type != TMPL_TYPE_NULL) {
+		rcode = func(parent, &head, request, map, ctx);
+		if (rcode < 0) {
+			rad_assert(!head);
+			return rcode;
+		}
+		if (!head) {
+			RDEBUG2("No attributes updated for RHS %s", map->rhs->name);
+			return rcode;
+		}
+	} else {
+		if (rad_debug_lvl) map_debug_log(request, map, NULL);
+	}
+
+	/*
+	 *	Print the VPs
+	 */
+	for (vp = fr_cursor_init(&src_list, &head);
+	     vp;
+	     vp = fr_cursor_next(&src_list)) {
+		VERIFY_VP(vp);
+
+		if (rad_debug_lvl) map_debug_log(request, map, vp);
+	}
+
+	/*
+	 *	The destination is a list (which is a completely different set of operations)
+	 */
+	if (map->lhs->type == TMPL_TYPE_LIST) {
+		switch (map->op) {
+		case T_OP_CMP_FALSE:
+			/* We don't need the src VPs (should just be 'ANY') */
+			rad_assert(!head);
+
+			/* Clear the entire dst list */
+			fr_pair_list_free(list);
+
+			if (map->lhs->tmpl_list == PAIR_LIST_REQUEST) {
+				context->username = NULL;
+				context->password = NULL;
+			}
+			return 0;
+
+		case T_OP_SET:
+			if (map->rhs->type == TMPL_TYPE_LIST) {
+				fr_pair_list_free(list);
+				*list = head;
+				head = NULL;
+			} else { /* FALL-THROUGH */
+		case T_OP_EQ:
+				rad_assert(map->rhs->type == TMPL_TYPE_EXEC);
+				/* FALL-THROUGH */
+		case T_OP_ADD:
+				fr_pair_list_move(parent, list, &head, map->op);
+				fr_pair_list_free(&head);
+			}
+			goto finish;
+		case T_OP_PREPEND:
+			fr_pair_list_move(parent, list, &head, T_OP_PREPEND);
+			fr_pair_list_free(&head);
+			goto finish;
+
+		default:
+			fr_pair_list_free(&head);
+			return -1;
+		}
+	}
+
+	/*
+	 *	Find the destination attribute.  We leave with either
+	 *	the dst_list and vp pointing to the attribute or the VP
+	 *	being NULL (no attribute at that index).
+	 */
+	num = map->lhs->tmpl_num;
+	(void) fr_cursor_init(&dst_list, list);
+	if ((num != NUM_ANY) && (num > 0)) {
+		while ((dst = fr_cursor_next_by_da(&dst_list, map->lhs->tmpl_da, map->lhs->tmpl_tag))) {
+			if (num <= 0) break;
+			num--;
+		}
+	} else {
+		dst = fr_cursor_next_by_da(&dst_list, map->lhs->tmpl_da, map->lhs->tmpl_tag);
+	}
+	rad_assert(!dst || (map->lhs->tmpl_da == dst->da));
+
+	/*
+	 *	The destination is an attribute
+	 */
+	switch (map->op) {
+	default:
+		break;
+	/*
+	 * 	!* - Remove all attributes which match dst in the specified list.
+	 *	This doesn't use attributes returned by the func(), and immediately frees them.
+	 */
+	case T_OP_CMP_FALSE:
+		/* We don't need the src VPs (should just be 'ANY') */
+		rad_assert(!head);
+		if (!dst) return 0;
+
+		/*
+		 *	Wildcard: delete all of the matching ones, based on tag.
+		 */
+		if (map->lhs->tmpl_num == NUM_ANY) {
+			fr_pair_delete_by_num(list, map->lhs->tmpl_da->attr, map->lhs->tmpl_da->vendor, map->lhs->tmpl_tag);
+			dst = NULL;
+		/*
+		 *	We've found the Nth one.  Delete it, and only it.
+		 */
+		} else {
+			dst = fr_cursor_remove(&dst_list);
+			fr_pair_list_free(&dst);
+		}
+
+		/*
+		 *	Check that the User-Name and User-Password
+		 *	caches point to the correct attribute.
+		 */
+		goto finish;
+
+	/*
+	 *	-= - Delete attributes in the dst list which match any of the
+	 *	src_list attributes.
+	 *
+	 *	This operation has two modes:
+	 *	- If map->lhs->tmpl_num > 0, we check each of the src_list attributes against
+	 *	  the dst attribute, to see if any of their values match.
+	 *	- If map->lhs->tmpl_num == NUM_ANY, we compare all instances of the dst attribute
+	 *	  against each of the src_list attributes.
+	 */
+	case T_OP_SUB:
+		/* We didn't find any attributes earlier */
+		if (!dst) {
+			fr_pair_list_free(&head);
+			return 0;
+		}
+
+		/*
+		 *	Instance specific[n] delete
+		 */
+		if (map->lhs->tmpl_num != NUM_ANY) {
+			for (vp = fr_cursor_first(&src_list);
+			     vp;
+			     vp = fr_cursor_next(&src_list)) {
+				head->op = T_OP_CMP_EQ;
+				rcode = radius_compare_vps(request, vp, dst);
+				if (rcode == 0) {
+					dst = fr_cursor_remove(&dst_list);
+					fr_pair_list_free(&dst);
+					found = true;
+				}
+			}
+			fr_pair_list_free(&head);
+			if (!found) return 0;
+			goto finish;
+		}
+
+		/*
+		 *	All instances[*] delete
+		 */
+		for (dst = fr_cursor_current(&dst_list);
+		     dst;
+		     dst = fr_cursor_next_by_da(&dst_list, map->lhs->tmpl_da, map->lhs->tmpl_tag)) {
+			for (vp = fr_cursor_first(&src_list);
+			     vp;
+			     vp = fr_cursor_next(&src_list)) {
+				head->op = T_OP_CMP_EQ;
+				rcode = radius_compare_vps(request, vp, dst);
+				if (rcode == 0) {
+					dst = fr_cursor_remove(&dst_list);
+					fr_pair_list_free(&dst);
+					found = true;
+				}
+			}
+		}
+		fr_pair_list_free(&head);
+		if (!found) return 0;
+		goto finish;
+	}
+
+	/*
+	 *	Another fixup pass to set tags on attributes were about to insert
+	 */
+	if (map->lhs->tmpl_tag != TAG_ANY) {
+		for (vp = fr_cursor_init(&src_list, &head);
+		     vp;
+		     vp = fr_cursor_next(&src_list)) {
+			vp->tag = map->lhs->tmpl_tag;
+		}
+	}
+
+	switch (map->op) {
+	/*
+	 *	= - Set only if not already set
+	 */
+	case T_OP_EQ:
+		if (dst) {
+			RDEBUG3("Refusing to overwrite (use :=)");
+			fr_pair_list_free(&head);
+			return 0;
+		}
+
+		/* Insert first instance (if multiple) */
+		fr_cursor_first(&src_list);
+		fr_cursor_insert(&dst_list, fr_cursor_remove(&src_list));
+		/* Free any we didn't insert */
+		fr_pair_list_free(&head);
+		break;
+
+	/*
+	 *	:= - Overwrite existing attribute with last src_list attribute
+	 */
+	case T_OP_SET:
+		/* Wind to last instance */
+		fr_cursor_last(&src_list);
+		if (dst) {
+			DEBUG_OVERWRITE(dst, fr_cursor_current(&src_list));
+			dst = fr_cursor_replace(&dst_list, fr_cursor_remove(&src_list));
+			fr_pair_list_free(&dst);
+		} else {
+			fr_cursor_insert(&dst_list, fr_cursor_remove(&src_list));
+		}
+		/* Free any we didn't insert */
+		fr_pair_list_free(&head);
+		break;
+
+	/*
+	 *	^= - Prepend src_list attributes to the destination
+	 */
+	case T_OP_PREPEND:
+		fr_pair_prepend(list, head);
+		head = NULL;
+		break;
+
+	/*
+	 *	+= - Add all src_list attributes to the destination
+	 */
+	case T_OP_ADD:
+		/* Insert all the instances! (if multiple) */
+		fr_pair_add(list, head);
+		head = NULL;
+		break;
+
+	/*
+	 *	Filter operators
+	 */
+	case T_OP_REG_NE:
+	case T_OP_NE:
+	case T_OP_REG_EQ:
+	case T_OP_CMP_EQ:
+	case T_OP_GE:
+	case T_OP_GT:
+	case T_OP_LE:
+	case T_OP_LT:
+	{
+		VALUE_PAIR *a, *b;
+
+		fr_pair_list_sort(&head, fr_pair_cmp_by_da_tag);
+		fr_pair_list_sort(list, fr_pair_cmp_by_da_tag);
+
+		fr_cursor_first(&dst_list);
+
+		for (b = fr_cursor_first(&src_list);
+		     b;
+		     b = fr_cursor_next(&src_list)) {
+			found = false;
+
+			for (a = fr_cursor_current(&dst_list);
+			     a;
+			     a = fr_cursor_next(&dst_list)) {
+				int8_t cmp;
+
+				cmp = fr_pair_cmp_by_da_tag(a, b);	/* attribute and tag match */
+				if (cmp > 0) break;
+				else if (cmp < 0) continue;
+
+				/*
+				 *	The LHS exists.  We need to
+				 *	limit it's value based on the
+				 *	operator, and on the value of
+				 *	the RHS.
+				 */
+				cmp = (value_data_cmp_op(map->op, a->da->type, &a->data, a->vp_length, b->da->type, &b->data, b->vp_length) == 0);
+				if (cmp == 1) switch (map->op) {
+
+					/*
+					 *	Keep only matching attributes.
+					 */
+				default:
+				case T_OP_REG_NE:
+				case T_OP_NE:
+				case T_OP_REG_EQ:
+				case T_OP_CMP_EQ:
+					a = fr_cursor_remove(&dst_list);
+					talloc_free(a);
+					break;
+
+					/*
+					 *	Keep matching
+					 *	attribute, and enforce
+					 *	matching values.
+					 */
+				case T_OP_GE:
+				case T_OP_GT:
+				case T_OP_LE:
+				case T_OP_LT:
+					DEBUG_OVERWRITE(a, b);
+					(void) value_data_copy(a, &a->data, a->da->type,
+							       &b->data, b->vp_length);
+					found = true;
+					break;
+				}
+			}
+
+			/*
+			 *	End of the dst list.
+			 */
+			if (!a) {
+				if (found) break;
+
+				switch (map->op) {
+				default:
+					break;
+
+					/*
+					 *	It wasn't found.  Insert it with the given value.
+					 */
+				case T_OP_GE:
+				case T_OP_GT:
+				case T_OP_LE:
+				case T_OP_LT:
+					(void) fr_cursor_insert(&dst_list, fr_pair_copy(parent, b));
+					break;
+				}
+				break;
+			}
+		}
+		fr_pair_list_free(&head);
+	}
+		break;
+
+	default:
+		rad_assert(0);	/* Should have been caught be the caller */
+		return -1;
+	}
+
+finish:
+	rad_assert(!head);
+
+	/*
+	 *	Update the cached username && password.  This is code
+	 *	we execute on EVERY update (sigh) so that SOME modules
+	 *	MIGHT NOT have to do the search themselves.
+	 *
+	 *	TBH, we should probably make each module just do the
+	 *	search themselves.
+	 */
+	if (map->lhs->tmpl_list == PAIR_LIST_REQUEST) {
+		context->username = NULL;
+		context->password = NULL;
+
+		for (vp = fr_cursor_init(&src_list, list);
+		     vp;
+		     vp = fr_cursor_next(&src_list)) {
+
+			if (vp->da->vendor != 0) continue;
+			if (vp->da->flags.has_tag) continue;
+
+			if (!context->username && (vp->da->attr == PW_USER_NAME)) {
+				context->username = vp;
+				continue;
+			}
+
+			if (vp->da->attr == PW_STRIPPED_USER_NAME) {
+				context->username = vp;
+				continue;
+			}
+
+			if (vp->da->attr == PW_USER_PASSWORD) {
+				context->password = vp;
+				continue;
+			}
+		}
+	}
+	return 0;
+}
+
+/** Check whether the destination of a map is currently valid
+ *
+ * @param request The current request.
+ * @param map to check.
+ * @return true if the map resolves to a request and list else false.
+ */
+bool map_dst_valid(REQUEST *request, vp_map_t const *map)
+{
+	REQUEST *context = request;
+
+	VERIFY_MAP(map);
+
+	if (radius_request(&context, map->lhs->tmpl_request) < 0) return false;
+	if (!radius_list(context, map->lhs->tmpl_list)) return false;
+
+	return true;
+}
+
+/**  Print a map to a string
+ *
+ * @param[out] buffer for the output string
+ * @param[in] bufsize of the buffer
+ * @param[in] map to print
+ * @return the size of the string printed
+ */
+size_t map_prints(char *buffer, size_t bufsize, vp_map_t const *map)
+{
+	size_t len;
+	DICT_ATTR const *da = NULL;
+	char *p = buffer;
+	char *end = buffer + bufsize;
+
+	VERIFY_MAP(map);
+
+	if (map->lhs->type == TMPL_TYPE_ATTR) da = map->lhs->tmpl_da;
+
+	len = tmpl_prints(buffer, bufsize, map->lhs, da);
+	p += len;
+
+	*(p++) = ' ';
+	strlcpy(p, fr_token_name(map->op), end - p);
+	p += strlen(p);
+	*(p++) = ' ';
+
+	/*
+	 *	The RHS doesn't matter for many operators
+	 */
+	if ((map->op == T_OP_CMP_TRUE) ||
+	    (map->op == T_OP_CMP_FALSE)) {
+		strlcpy(p, "ANY", (end - p));
+		p += strlen(p);
+		return p - buffer;
+	}
+
+	rad_assert(map->rhs != NULL);
+
+	if ((map->lhs->type == TMPL_TYPE_ATTR) &&
+	    (map->lhs->tmpl_da->type == PW_TYPE_STRING) &&
+	    (map->rhs->type == TMPL_TYPE_LITERAL)) {
+		*(p++) = '\'';
+		len = tmpl_prints(p, end - p, map->rhs, da);
+		p += len;
+		*(p++) = '\'';
+		*p = '\0';
+	} else {
+		len = tmpl_prints(p, end - p, map->rhs, da);
+		p += len;
+	}
+
+	return p - buffer;
+}
+
+/*
+ *	Debug print a map / VP
+ */
+void map_debug_log(REQUEST *request, vp_map_t const *map, VALUE_PAIR const *vp)
+{
+	char *value;
+	char buffer[1024];
+
+	VERIFY_MAP(map);
+	rad_assert(map->lhs != NULL);
+	rad_assert(map->rhs != NULL);
+
+	rad_assert(vp || (map->rhs->type == TMPL_TYPE_NULL));
+
+	switch (map->rhs->type) {
+	/*
+	 *	Just print the value being assigned
+	 */
+	default:
+	case TMPL_TYPE_LITERAL:
+		vp_prints_value(buffer, sizeof(buffer), vp, map->rhs->quote);
+		value = buffer;
+		break;
+
+	case TMPL_TYPE_XLAT:
+	case TMPL_TYPE_XLAT_STRUCT:
+		vp_prints_value(buffer, sizeof(buffer), vp, map->rhs->quote);
+		value = buffer;
+		break;
+
+	case TMPL_TYPE_DATA:
+		vp_prints_value(buffer, sizeof(buffer), vp, map->rhs->quote);
+		value = buffer;
+		break;
+
+	/*
+	 *	For the lists, we can't use the original name, and have to
+	 *	rebuild it using tmpl_prints, for each attribute we're
+	 *	copying.
+	 */
+	case TMPL_TYPE_LIST:
+	{
+		char		attr[256];
+		char		quote = '\0';
+		vp_tmpl_t	vpt;
+		/*
+		 *	Fudge a temporary tmpl that describes the attribute we're copying
+		 *	this is a combination of the original list tmpl, and values from
+		 *	the VALUE_PAIR. This way, we get tag info included.
+		 */
+		memcpy(&vpt, map->rhs, sizeof(vpt));
+		vpt.tmpl_da = vp->da;
+		vpt.tmpl_tag = vp->tag;
+		vpt.type = TMPL_TYPE_ATTR;
+
+		/*
+		 *	Not appropriate to use map->rhs->quote here, as that's the quoting
+		 *	around the list ref. The attribute value has no quoting, so we choose
+		 *	the quoting based on the data type, and whether it's printable.
+		 */
+		if (vp->da->type == PW_TYPE_STRING) quote = is_printable(vp->vp_strvalue,
+									 vp->vp_length) ? '\'' : '"';
+		vp_prints_value(buffer, sizeof(buffer), vp, quote);
+		tmpl_prints(attr, sizeof(attr), &vpt, vp->da);
+		value = talloc_typed_asprintf(request, "%s -> %s", attr, buffer);
+	}
+		break;
+
+	case TMPL_TYPE_ATTR:
+	{
+		char quote = '\0';
+
+		/*
+		 *	Not appropriate to use map->rhs->quote here, as that's the quoting
+		 *	around the attr ref. The attribute value has no quoting, so we choose
+		 *	the quoting based on the data type, and whether it's printable.
+		 */
+		if (vp->da->type == PW_TYPE_STRING) quote = is_printable(vp->vp_strvalue,
+									 vp->vp_length) ? '\'' : '"';
+		vp_prints_value(buffer, sizeof(buffer), vp, quote);
+		value = talloc_typed_asprintf(request, "%.*s -> %s", (int)map->rhs->len, map->rhs->name, buffer);
+	}
+		break;
+
+	case TMPL_TYPE_NULL:
+		strcpy(buffer, "ANY");
+		value = buffer;
+		break;
+	}
+
+	switch (map->lhs->type) {
+	case TMPL_TYPE_LIST:
+		RDEBUG("%.*s:%s %s %s", (int)map->lhs->len, map->lhs->name, vp ? vp->da->name : "",
+		       fr_int2str(fr_tokens, vp ? vp->op : map->op, "<INVALID>"), value);
+		break;
+
+	case TMPL_TYPE_ATTR:
+		RDEBUG("%s %s %s", map->lhs->name,
+		       fr_int2str(fr_tokens, vp ? vp->op : map->op, "<INVALID>"), value);
+		break;
+
+	default:
+		RDEBUG("map %s = %s", fr_int2str(tmpl_names, map->lhs->type, "???"), value);
+		break;
+	}
+
+	if (value != buffer) talloc_free(value);
+}
diff --git a/src/main/modcall.c b/src/main/modcall.c
new file mode 100644
index 0000000..aa6abf8
--- /dev/null
+++ b/src/main/modcall.c
@@ -0,0 +1,4041 @@
+/*
+ * @name modcall.c
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2000,2006  The FreeRADIUS server project
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modpriv.h>
+#include <freeradius-devel/modcall.h>
+#include <freeradius-devel/parser.h>
+#include <freeradius-devel/rad_assert.h>
+
+
+/* mutually-recursive static functions need a prototype up front */
+static modcallable *do_compile_modgroup(modcallable *,
+					rlm_components_t, CONF_SECTION *,
+					int, int, int);
+
+/* Actions may be a positive integer (the highest one returned in the group
+ * will be returned), or the keyword "return", represented here by
+ * MOD_ACTION_RETURN, to cause an immediate return.
+ * There's also the keyword "reject", represented here by MOD_ACTION_REJECT
+ * to cause an immediate reject. */
+#define MOD_ACTION_RETURN  (-1)
+#define MOD_ACTION_REJECT  (-2)
+
+/* Here are our basic types: modcallable, modgroup, and modsingle. For an
+ * explanation of what they are all about, see doc/configurable_failover.rst */
+struct modcallable {
+	modcallable *parent;
+	struct modcallable *next;
+	char const *name;
+	char const *debug_name;
+	enum { MOD_SINGLE = 1, MOD_GROUP, MOD_LOAD_BALANCE, MOD_REDUNDANT_LOAD_BALANCE,
+#ifdef WITH_UNLANG
+	       MOD_IF, MOD_ELSE, MOD_ELSIF, MOD_UPDATE, MOD_SWITCH, MOD_CASE,
+	       MOD_FOREACH, MOD_BREAK, MOD_RETURN,
+#endif
+	       MOD_POLICY, MOD_REFERENCE, MOD_XLAT } type;
+	rlm_components_t method;
+	int actions[RLM_MODULE_NUMCODES];
+};
+
+#define MOD_LOG_OPEN_BRACE RDEBUG2("%s {", c->debug_name)
+
+#define MOD_LOG_CLOSE_BRACE RDEBUG2("} # %s = %s", c->debug_name, fr_int2str(mod_rcode_table, result, "<invalid>"))
+
+typedef struct {
+	modcallable		mc;		/* self */
+	enum {
+		GROUPTYPE_SIMPLE = 0,
+		GROUPTYPE_REDUNDANT,
+		GROUPTYPE_COUNT
+	} grouptype;				/* after mc */
+	modcallable		*children;
+	modcallable		*tail;		/* of the children list */
+	CONF_SECTION		*cs;
+	vp_map_t	*map;		/* update */
+	vp_tmpl_t	*vpt;		/* switch */
+	fr_cond_t		*cond;		/* if/elsif */
+	bool			done_pass2;
+} modgroup;
+
+typedef struct {
+	modcallable mc;
+	module_instance_t *modinst;
+} modsingle;
+
+typedef struct {
+	modcallable mc;
+	char const *ref_name;
+	CONF_SECTION *ref_cs;
+} modref;
+
+typedef struct {
+	modcallable mc;
+	int exec;
+	char *xlat_name;
+} modxlat;
+
+/* Simple conversions: modsingle and modgroup are subclasses of modcallable,
+ * so we often want to go back and forth between them. */
+static modsingle *mod_callabletosingle(modcallable *p)
+{
+	rad_assert(p->type==MOD_SINGLE);
+	return (modsingle *)p;
+}
+static modgroup *mod_callabletogroup(modcallable *p)
+{
+	rad_assert((p->type > MOD_SINGLE) && (p->type <= MOD_POLICY));
+
+	return (modgroup *)p;
+}
+static modcallable *mod_singletocallable(modsingle *p)
+{
+	return (modcallable *)p;
+}
+static modcallable *mod_grouptocallable(modgroup *p)
+{
+	return (modcallable *)p;
+}
+
+static modref *mod_callabletoref(modcallable *p)
+{
+	rad_assert(p->type==MOD_REFERENCE);
+	return (modref *)p;
+}
+static modcallable *mod_reftocallable(modref *p)
+{
+	return (modcallable *)p;
+}
+
+static modxlat *mod_callabletoxlat(modcallable *p)
+{
+	rad_assert(p->type==MOD_XLAT);
+	return (modxlat *)p;
+}
+static modcallable *mod_xlattocallable(modxlat *p)
+{
+	return (modcallable *)p;
+}
+
+/* modgroups are grown by adding a modcallable to the end */
+static void add_child(modgroup *g, modcallable *c)
+{
+	if (!c) return;
+
+	(void) talloc_steal(g, c);
+
+	if (!g->children) {
+		g->children = g->tail = c;
+	} else {
+		rad_assert(g->tail->next == NULL);
+		g->tail->next = c;
+		g->tail = c;
+	}
+
+	c->parent = mod_grouptocallable(g);
+}
+
+/* Here's where we recognize all of our keywords: first the rcodes, then the
+ * actions */
+const FR_NAME_NUMBER mod_rcode_table[] = {
+	{ "reject",     RLM_MODULE_REJECT       },
+	{ "fail",       RLM_MODULE_FAIL	 },
+	{ "ok",	 	RLM_MODULE_OK	   },
+	{ "handled",    RLM_MODULE_HANDLED      },
+	{ "invalid",    RLM_MODULE_INVALID      },
+	{ "userlock",   RLM_MODULE_USERLOCK     },
+	{ "notfound",   RLM_MODULE_NOTFOUND     },
+	{ "noop",       RLM_MODULE_NOOP	 },
+	{ "updated",    RLM_MODULE_UPDATED      },
+	{ NULL, 0 }
+};
+
+
+/*
+ *	Compile action && rcode for later use.
+ */
+static int compile_action(modcallable *c, CONF_PAIR *cp)
+{
+	int action;
+	char const *attr, *value;
+
+	attr = cf_pair_attr(cp);
+	value = cf_pair_value(cp);
+	if (!value) return 0;
+
+	if (!strcasecmp(value, "return"))
+		action = MOD_ACTION_RETURN;
+
+	else if (!strcasecmp(value, "break"))
+		action = MOD_ACTION_RETURN;
+
+	else if (!strcasecmp(value, "reject"))
+		action = MOD_ACTION_REJECT;
+
+	else if (strspn(value, "0123456789")==strlen(value)) {
+		action = atoi(value);
+
+		/*
+		 *	Don't allow priority zero, for future use.
+		 */
+		if (action == 0) return 0;
+	} else {
+		cf_log_err_cp(cp, "Unknown action '%s'.\n",
+			   value);
+		return 0;
+	}
+
+	if (strcasecmp(attr, "default") != 0) {
+		int rcode;
+
+		rcode = fr_str2int(mod_rcode_table, attr, -1);
+		if (rcode < 0) {
+			cf_log_err_cp(cp,
+				   "Unknown module rcode '%s'.\n",
+				   attr);
+			return 0;
+		}
+		c->actions[rcode] = action;
+
+	} else {		/* set all unset values to the default */
+		int i;
+
+		for (i = 0; i < RLM_MODULE_NUMCODES; i++) {
+			if (!c->actions[i]) c->actions[i] = action;
+		}
+	}
+
+	return 1;
+}
+
+/* Some short names for debugging output */
+static char const * const comp2str[] = {
+	"authenticate",
+	"authorize",
+	"preacct",
+	"accounting",
+	"session",
+	"pre-proxy",
+	"post-proxy",
+	"post-auth"
+#ifdef WITH_COA
+	,
+	"recv-coa",
+	"send-coa"
+#endif
+};
+
+#ifdef HAVE_PTHREAD_H
+/*
+ *	Lock the mutex for the module
+ */
+static void safe_lock(module_instance_t *instance)
+{
+	if (instance->mutex)
+		pthread_mutex_lock(instance->mutex);
+}
+
+/*
+ *	Unlock the mutex for the module
+ */
+static void safe_unlock(module_instance_t *instance)
+{
+	if (instance->mutex)
+		pthread_mutex_unlock(instance->mutex);
+}
+#else
+/*
+ *	No threads: these functions become NULL's.
+ */
+#define safe_lock(foo)
+#define safe_unlock(foo)
+#endif
+
+static rlm_rcode_t CC_HINT(nonnull) call_modsingle(rlm_components_t component, modsingle *sp, REQUEST *request)
+{
+	int blocked;
+	int indent = request->log.indent;
+	char const *old;
+
+	/*
+	 *	If the request should stop, refuse to do anything.
+	 */
+	blocked = (request->master_state == REQUEST_STOP_PROCESSING);
+	if (blocked) return RLM_MODULE_NOOP;
+
+	RDEBUG3("modsingle[%s]: calling %s (%s)",
+		comp2str[component], sp->modinst->name,
+		sp->modinst->entry->name);
+	request->log.indent = 0;
+
+	if (sp->modinst->force) {
+		request->rcode = sp->modinst->code;
+		goto fail;
+	}
+
+	/*
+	 *	For logging unresponsive children.
+	 */
+	old = request->module;
+	request->module = sp->modinst->name;
+
+	safe_lock(sp->modinst);
+	request->rcode = sp->modinst->entry->module->methods[component](sp->modinst->insthandle, request);
+	safe_unlock(sp->modinst);
+
+	request->module = old;
+
+	/*
+	 *	Wasn't blocked, and now is.  Complain!
+	 */
+	blocked = (request->master_state == REQUEST_STOP_PROCESSING);
+	if (blocked) {
+		RWARN("Module %s became unblocked", sp->modinst->entry->name);
+	}
+
+ fail:
+	request->log.indent = indent;
+	RDEBUG3("modsingle[%s]: returned from %s (%s)",
+	       comp2str[component], sp->modinst->name,
+	       sp->modinst->entry->name);
+
+	return request->rcode;
+}
+
+static int default_component_results[MOD_COUNT] = {
+	RLM_MODULE_REJECT,	/* AUTH */
+	RLM_MODULE_NOTFOUND,	/* AUTZ */
+	RLM_MODULE_NOOP,	/* PREACCT */
+	RLM_MODULE_NOOP,	/* ACCT */
+	RLM_MODULE_FAIL,	/* SESS */
+	RLM_MODULE_NOOP,	/* PRE_PROXY */
+	RLM_MODULE_NOOP,	/* POST_PROXY */
+	RLM_MODULE_NOOP       	/* POST_AUTH */
+#ifdef WITH_COA
+	,
+	RLM_MODULE_NOOP,       	/* RECV_COA_TYPE */
+	RLM_MODULE_NOOP		/* SEND_COA_TYPE */
+#endif
+};
+
+
+extern char const *unlang_keyword[];
+
+char const *unlang_keyword[] = {
+	"",
+	"single",
+	"group",
+	"load-balance group",
+	"redundant-load-balance group",
+#ifdef WITH_UNLANG
+	"if",
+	"else",
+	"elsif",
+	"update",
+	"switch",
+	"case",
+	"foreach",
+	"break",
+	"return",
+#endif
+	"policy",
+	"reference",
+	"xlat",
+	NULL
+};
+
+static char const modcall_spaces[] = "                                                                ";
+
+#define MODCALL_STACK_MAX (32)
+
+/*
+ *	Don't call the modules recursively.  Instead, do them
+ *	iteratively, and manage the call stack ourselves.
+ */
+typedef struct modcall_stack_entry_t {
+	rlm_rcode_t result;
+	int priority;
+	int unwind;		/* unwind to this one if it exists */
+	modcallable *c;
+} modcall_stack_entry_t;
+
+
+static bool modcall_recurse(REQUEST *request, rlm_components_t component, int depth,
+			    modcall_stack_entry_t *entry, bool do_next_sibling);
+
+/*
+ *	Call a child of a block.
+ */
+static void modcall_child(REQUEST *request, rlm_components_t component, int depth,
+			  modcall_stack_entry_t *entry, modcallable *c,
+			  rlm_rcode_t *result, bool do_next_sibling)
+{
+	modcall_stack_entry_t *next;
+
+	if (depth >= MODCALL_STACK_MAX) {
+		ERROR("Internal sanity check failed: module stack is too deep");
+		fr_exit(1);
+	}
+
+	/*
+	 *	Initialize the childs stack frame.
+	 */
+	next = entry + 1;
+	next->c = c;
+	next->result = entry->result;
+	next->priority = 0;
+	next->unwind = 0;
+
+	if (!modcall_recurse(request, component,
+			     depth, next, do_next_sibling)) {
+		*result = RLM_MODULE_FAIL;
+		 return;
+	}
+
+	/*
+	 *	Unwind back up the stack
+	 */
+	if (next->unwind != 0) {
+		entry->unwind = next->unwind;
+	}
+
+	*result = next->result;
+
+	return;
+}
+
+
+/*
+ *	Interpret the various types of blocks.
+ */
+static bool modcall_recurse(REQUEST *request, rlm_components_t component, int depth,
+			    modcall_stack_entry_t *entry, bool do_next_sibling)
+{
+	bool if_taken, was_if;
+	modcallable *c;
+	int priority;
+	rlm_rcode_t result;
+
+	was_if = if_taken = false;
+	result = RLM_MODULE_UNKNOWN;
+	RINDENT();
+
+redo:
+	priority = -1;
+	c = entry->c;
+
+	/*
+	 *	Nothing more to do.  Return the code and priority
+	 *	which was set by the caller.
+	 */
+	if (!c) goto finish;
+
+	if (fr_debug_lvl >= 3) {
+		VERIFY_REQUEST(request);
+	}
+
+	rad_assert(c->debug_name != NULL); /* if this happens, all bets are off. */
+
+	/*
+	 *	We've been asked to stop.  Do so.
+	 */
+	if ((request->master_state == REQUEST_STOP_PROCESSING) ||
+	    (request->parent &&
+	     (request->parent->master_state == REQUEST_STOP_PROCESSING))) {
+		entry->result = RLM_MODULE_FAIL;
+		entry->priority = 9999;
+		goto finish;
+	}
+
+#ifdef WITH_UNLANG
+	/*
+	 *	Handle "if" conditions.
+	 */
+	if (c->type == MOD_IF) {
+		int condition;
+		modgroup *g;
+
+	mod_if:
+		g = mod_callabletogroup(c);
+		rad_assert(g->cond != NULL);
+
+		RDEBUG2("%s %s{", unlang_keyword[c->type], c->name);
+
+		/*
+		 *	Use "result" UNLESS it wasn't set, in which
+		 *	case we use the previous result on the stack.
+		 */
+		condition = radius_evaluate_cond(request, result != RLM_MODULE_UNKNOWN ? result : entry->result, 0, g->cond);
+		if (condition < 0) {
+			condition = false;
+			REDEBUG("Failed retrieving values required to evaluate condition");
+		} else {
+			RDEBUG2("%s %s -> %s",
+				unlang_keyword[c->type],
+				c->name, condition ? "TRUE" : "FALSE");
+		}
+
+		/*
+		 *	Didn't pass.  Remember that.
+		 */
+		if (!condition) {
+			was_if = true;
+			if_taken = false;
+			goto next_sibling;
+		}
+
+		/*
+		 *	We took the "if".  Go recurse into its' children.
+		 */
+		was_if = true;
+		if_taken = true;
+		goto do_children;
+	} /* MOD_IF */
+
+	/*
+	 *	"else" if the previous "if" was taken.
+	 *	"if" if the previous if wasn't taken.
+	 */
+	if (c->type == MOD_ELSIF) {
+		if (!was_if) goto elsif_error;
+
+		/*
+		 *	Like MOD_ELSE, but allow for a later "else"
+		 */
+		if (if_taken) {
+			RDEBUG2("... skipping %s: Preceding \"if\" was taken",
+				unlang_keyword[c->type]);
+			was_if = true;
+			if_taken = true;
+			goto next_sibling;
+		}
+
+		/*
+		 *	Check the "if" condition.
+		 */
+		goto mod_if;
+	} /* MOD_ELSIF */
+
+	/*
+	 *	"else" for a preceding "if".
+	 */
+	if (c->type == MOD_ELSE) {
+		if (!was_if) { /* error */
+		elsif_error:
+			RDEBUG2("... skipping %s: No preceding \"if\"",
+				unlang_keyword[c->type]);
+			goto next_sibling;
+		}
+
+		if (if_taken) {
+			RDEBUG2("... skipping %s: Preceding \"if\" was taken",
+				unlang_keyword[c->type]);
+			was_if = false;
+			if_taken = false;
+			goto next_sibling;
+		}
+
+		/*
+		 *	We need to process it.  Go do that.
+		 */
+		was_if = false;
+		if_taken = false;
+		goto do_children;
+	} /* MOD_ELSE */
+
+	/*
+	 *	We're no longer processing if/else/elsif.  Reset the
+	 *	trackers for those conditions.
+	 */
+	was_if = false;
+	if_taken = false;
+#endif	/* WITH_UNLANG */
+
+	if (c->type == MOD_SINGLE) {
+		modsingle *sp;
+
+		/*
+		 *	Process a stand-alone child, and fall through
+		 *	to dealing with it's parent.
+		 */
+		sp = mod_callabletosingle(c);
+
+		result = call_modsingle(c->method, sp, request);
+		RDEBUG2("[%s] = %s", c->name ? c->name : "",
+			fr_int2str(mod_rcode_table, result, "<invalid>"));
+		goto calculate_result;
+	} /* MOD_SINGLE */
+
+#ifdef WITH_UNLANG
+	/*
+	 *	Update attribute(s)
+	 */
+	if (c->type == MOD_UPDATE) {
+		int rcode;
+		modgroup *g = mod_callabletogroup(c);
+		vp_map_t *map;
+
+		MOD_LOG_OPEN_BRACE;
+		RINDENT();
+		for (map = g->map; map != NULL; map = map->next) {
+			rcode = map_to_request(request, map, map_to_vp, NULL);
+			if (rcode < 0) {
+				result = (rcode == -2) ? RLM_MODULE_INVALID : RLM_MODULE_FAIL;
+				REXDENT();
+				MOD_LOG_CLOSE_BRACE;
+				goto calculate_result;
+			}
+		}
+		REXDENT();
+		result = RLM_MODULE_NOOP;
+		MOD_LOG_CLOSE_BRACE;
+		goto calculate_result;
+	} /* MOD_IF */
+
+	/*
+	 *	Loop over a set of attributes.
+	 */
+	if (c->type == MOD_FOREACH) {
+		int i, foreach_depth = -1;
+		VALUE_PAIR *vps, *vp;
+		modcall_stack_entry_t *next = NULL;
+		vp_cursor_t copy;
+		modgroup *g = mod_callabletogroup(c);
+
+		if (depth >= MODCALL_STACK_MAX) {
+			ERROR("Internal sanity check failed: module stack is too deep");
+			fr_exit(1);
+		}
+
+		/*
+		 *	Figure out how deep we are in nesting by looking at request_data
+		 *	stored previously.
+		 */
+		for (i = 0; i < 8; i++) {
+			if (!request_data_reference(request, (void *)radius_get_vp, i)) {
+				foreach_depth = i;
+				break;
+			}
+		}
+
+		if (foreach_depth < 0) {
+			REDEBUG("foreach Nesting too deep!");
+			result = RLM_MODULE_FAIL;
+			goto calculate_result;
+		}
+
+		/*
+		 *	Copy the VPs from the original request, this ensures deterministic
+		 *	behaviour if someone decides to add or remove VPs in the set were
+		 *	iterating over.
+		 */
+		if (tmpl_copy_vps(request, &vps, request, g->vpt) < 0) {	/* nothing to loop over */
+			MOD_LOG_OPEN_BRACE;
+			result = RLM_MODULE_NOOP;
+			MOD_LOG_CLOSE_BRACE;
+			goto calculate_result;
+		}
+
+		rad_assert(vps != NULL);
+		fr_cursor_init(&copy, &vps);
+
+		RDEBUG2("foreach %s ", c->name);
+
+		/*
+		 *	This is the actual body of the foreach loop
+		 */
+		for (vp = fr_cursor_first(&copy);
+		     vp != NULL;
+		     vp = fr_cursor_next(&copy)) {
+#ifndef NDEBUG
+			if (fr_debug_lvl >= 2) {
+				char buffer[1024];
+
+				vp_prints_value(buffer, sizeof(buffer), vp, '"');
+				RDEBUG2("# Foreach-Variable-%d = %s", foreach_depth, buffer);
+			}
+#endif
+
+			/*
+			 *	Add the vp to the request, so that
+			 *	xlat.c, xlat_foreach() can find it.
+			 */
+			request_data_add(request, (void *)radius_get_vp, foreach_depth, &vp, false);
+
+			/*
+			 *	Initialize the childs stack frame.
+			 */
+			next = entry + 1;
+			next->c = g->children;
+			next->result = entry->result;
+			next->priority = 0;
+			next->unwind = 0;
+
+			if (!modcall_recurse(request, component, depth + 1, next, true)) {
+				break;
+			}
+
+			/*
+			 *	We've been asked to unwind to the
+			 *	enclosing "foreach".  We're here, so
+			 *	we can stop unwinding.
+			 */
+			if (next->unwind == MOD_BREAK) {
+				entry->unwind = 0;
+				break;
+			}
+
+			/*
+			 *	Unwind all the way.
+			 */
+			if (next->unwind == MOD_RETURN) {
+				entry->unwind = MOD_RETURN;
+				break;
+			}
+		} /* loop over VPs */
+
+		/*
+		 *	Free the copied vps and the request data
+		 *	If we don't remove the request data, something could call
+		 *	the xlat outside of a foreach loop and trigger a segv.
+		 */
+		fr_pair_list_free(&vps);
+		request_data_get(request, (void *)radius_get_vp, foreach_depth);
+
+		rad_assert(next != NULL);
+		result = next->result;
+		priority = next->priority;
+		MOD_LOG_CLOSE_BRACE;
+		goto calculate_result;
+	} /* MOD_FOREACH */
+
+	/*
+	 *	Break out of a "foreach" loop, or return from a nested
+	 *	group.
+	 */
+	if ((c->type == MOD_BREAK) || (c->type == MOD_RETURN)) {
+		int i;
+		VALUE_PAIR **copy_p;
+
+		RDEBUG2("%s", unlang_keyword[c->type]);
+
+		for (i = 8; i >= 0; i--) {
+			copy_p = request_data_get(request, (void *)radius_get_vp, i);
+			if (copy_p) {
+				if (c->type == MOD_BREAK) {
+					RDEBUG2("# break Foreach-Variable-%d", i);
+					break;
+				}
+			}
+		}
+
+		/*
+		 *	Leave result / priority on the stack, and stop processing the section.
+		 */
+		entry->unwind = c->type;
+		goto finish;
+	} /* MOD_BREAK */
+
+#endif	  /* WITH_UNLANG */
+
+	/*
+	 *	Child is a group that has children of it's own.
+	 */
+	if ((c->type == MOD_GROUP) || (c->type == MOD_POLICY)
+#ifdef WITH_UNLANG
+	    || (c->type == MOD_CASE)
+#endif
+		) {
+		modgroup *g;
+
+#ifdef WITH_UNLANG
+	do_children:
+#endif
+		g = mod_callabletogroup(c);
+
+		/*
+		 *	This should really have been caught in the
+		 *	compiler, and the node never generated.  But
+		 *	doing that requires changing it's API so that
+		 *	it returns a flag instead of the compiled
+		 *	MOD_GROUP.
+		 */
+		if (!g->children) {
+			if (c->type == MOD_CASE) {
+				result = RLM_MODULE_NOOP;
+				goto calculate_result;
+			}
+
+			RDEBUG2("%s { ... } # empty sub-section is ignored", c->name);
+			goto next_sibling;
+		}
+
+		MOD_LOG_OPEN_BRACE;
+		modcall_child(request, component,
+			      depth + 1, entry, g->children,
+			      &result, true);
+		MOD_LOG_CLOSE_BRACE;
+		goto calculate_result;
+	} /* MOD_GROUP */
+
+#ifdef WITH_UNLANG
+	if (c->type == MOD_SWITCH) {
+		modcallable *this, *found, *null_case;
+		modgroup *g, *h;
+		fr_cond_t cond;
+		value_data_t data;
+		vp_map_t map;
+		vp_tmpl_t vpt;
+
+		MOD_LOG_OPEN_BRACE;
+
+		g = mod_callabletogroup(c);
+
+		memset(&cond, 0, sizeof(cond));
+		memset(&map, 0, sizeof(map));
+
+		cond.type = COND_TYPE_MAP;
+		cond.data.map = &map;
+
+		map.op = T_OP_CMP_EQ;
+		map.ci = cf_section_to_item(g->cs);
+
+		rad_assert(g->vpt != NULL);
+
+		null_case = found = NULL;
+		data.ptr = NULL;
+
+		/*
+		 *	The attribute doesn't exist.  We can skip
+		 *	directly to the default 'case' statement.
+		 */
+		if ((g->vpt->type == TMPL_TYPE_ATTR) && (tmpl_find_vp(NULL, request, g->vpt) < 0)) {
+		find_null_case:
+			for (this = g->children; this; this = this->next) {
+				rad_assert(this->type == MOD_CASE);
+
+				h = mod_callabletogroup(this);
+				if (h->vpt) continue;
+
+				found = this;
+				break;
+			}
+
+			goto do_null_case;
+		}
+
+		/*
+		 *	Expand the template if necessary, so that it
+		 *	is evaluated once instead of for each 'case'
+		 *	statement.
+		 */
+		if ((g->vpt->type == TMPL_TYPE_XLAT_STRUCT) ||
+		    (g->vpt->type == TMPL_TYPE_XLAT) ||
+		    (g->vpt->type == TMPL_TYPE_EXEC)) {
+			char *p;
+			ssize_t len;
+
+			len = tmpl_aexpand(request, &p, request, g->vpt, NULL, NULL);
+			if (len < 0) goto find_null_case;
+			data.strvalue = p;
+			tmpl_init(&vpt, TMPL_TYPE_LITERAL, data.strvalue, len);
+		}
+
+		/*
+		 *	Find either the exact matching name, or the
+		 *	"case {...}" statement.
+		 */
+		for (this = g->children; this; this = this->next) {
+			rad_assert(this->type == MOD_CASE);
+
+			h = mod_callabletogroup(this);
+
+			/*
+			 *	Remember the default case
+			 */
+			if (!h->vpt) {
+				if (!null_case) null_case = this;
+				continue;
+			}
+
+			/*
+			 *	If we're switching over an attribute
+			 *	AND we haven't pre-parsed the data for
+			 *	the case statement, then cast the data
+			 *	to the type of the attribute.
+			 */
+			if ((g->vpt->type == TMPL_TYPE_ATTR) &&
+			    (h->vpt->type != TMPL_TYPE_DATA)) {
+				map.rhs = g->vpt;
+				map.lhs = h->vpt;
+				cond.cast = g->vpt->tmpl_da;
+
+				/*
+				 *	Remove unnecessary casting.
+				 */
+				if ((h->vpt->type == TMPL_TYPE_ATTR) &&
+				    (g->vpt->tmpl_da->type == h->vpt->tmpl_da->type)) {
+					cond.cast = NULL;
+				}
+
+				/*
+				 *	Use the pre-expanded string.
+				 */
+			} else if ((g->vpt->type == TMPL_TYPE_XLAT_STRUCT) ||
+				   (g->vpt->type == TMPL_TYPE_XLAT) ||
+				   (g->vpt->type == TMPL_TYPE_EXEC)) {
+				map.rhs = h->vpt;
+				map.lhs = &vpt;
+				cond.cast = NULL;
+
+				/*
+				 *	Else evaluate the 'switch' statement.
+				 */
+			} else {
+				map.rhs = h->vpt;
+				map.lhs = g->vpt;
+				cond.cast = NULL;
+			}
+
+			if (radius_evaluate_map(request, RLM_MODULE_UNKNOWN, 0,
+						&cond) == 1) {
+				found = this;
+				break;
+			}
+		}
+
+		if (!found) found = null_case;
+
+	do_null_case:
+		talloc_free(data.ptr);
+		modcall_child(request, component, depth + 1, entry, found, &result, true);
+		MOD_LOG_CLOSE_BRACE;
+		goto calculate_result;
+	} /* MOD_SWITCH */
+#endif
+
+	if ((c->type == MOD_LOAD_BALANCE) ||
+	    (c->type == MOD_REDUNDANT_LOAD_BALANCE)) {
+		uint32_t count = 0;
+		modcallable *this, *found;
+		modgroup *g;
+
+		MOD_LOG_OPEN_BRACE;
+
+		g = mod_callabletogroup(c);
+		found = g->children;
+		rad_assert(g->children != NULL);
+
+		/*
+		 *	Choose a child at random.
+		 */
+		for (this = g->children; this; this = this->next) {
+			count++;
+
+			if ((count * (fr_rand() & 0xffff)) < (uint32_t) 0x10000) {
+				found = this;
+			}
+		}
+
+		if (c->type == MOD_LOAD_BALANCE) {
+			modcall_child(request, component,
+				      depth + 1, entry, found,
+				      &result, false);
+
+		} else {
+			this = found;
+
+			do {
+				modcall_child(request, component,
+					      depth + 1, entry, this,
+					      &result, false);
+				if (this->actions[result] == MOD_ACTION_RETURN) {
+					priority = -1;
+					break;
+				}
+
+				this = this->next;
+				if (!this) this = g->children;
+			} while (this != found);
+		}
+		MOD_LOG_CLOSE_BRACE;
+		goto calculate_result;
+	} /* MOD_LOAD_BALANCE */
+
+	/*
+	 *	Reference another virtual server.
+	 *
+	 *	This should really be deleted, and replaced with a
+	 *	more abstracted / functional version.
+	 */
+	if (c->type == MOD_REFERENCE) {
+		modref *mr = mod_callabletoref(c);
+		char const *server = request->server;
+
+		if (server == mr->ref_name) {
+			RWDEBUG("Suppressing recursive call to server %s", server);
+			goto next_sibling;
+		}
+
+		request->server = mr->ref_name;
+		RDEBUG("server %s { # nested call", mr->ref_name);
+		result = indexed_modcall(component, 0, request);
+		RDEBUG("} # server %s with nested call", mr->ref_name);
+		request->server = server;
+		goto calculate_result;
+	} /* MOD_REFERENCE */
+
+	/*
+	 *	xlat a string without doing anything else
+	 *
+	 *	This should really be deleted, and replaced with a
+	 *	more abstracted / functional version.
+	 */
+	if (c->type == MOD_XLAT) {
+		modxlat *mx = mod_callabletoxlat(c);
+		char buffer[128];
+
+		if (!mx->exec) {
+			radius_xlat(buffer, sizeof(buffer), request, mx->xlat_name, NULL, NULL);
+		} else {
+			RDEBUG("`%s`", mx->xlat_name);
+			radius_exec_program(request, NULL, 0, NULL, request, mx->xlat_name, request->packet->vps,
+					    false, true, EXEC_TIMEOUT);
+		}
+
+		goto next_sibling;
+	} /* MOD_XLAT */
+
+	/*
+	 *	Add new module types here.
+	 */
+
+calculate_result:
+#if 0
+	RDEBUG("(%s, %d) ? (%s, %d)",
+	       fr_int2str(mod_rcode_table, result, "<invalid>"),
+	       priority,
+	       fr_int2str(mod_rcode_table, entry->result, "<invalid>"),
+	       entry->priority);
+#endif
+
+
+	rad_assert(result != RLM_MODULE_UNKNOWN);
+
+	/*
+	 *	The child's action says return.  Do so.
+	 */
+	if ((c->actions[result] == MOD_ACTION_RETURN) &&
+	    (priority <= 0)) {
+		entry->result = result;
+		goto finish;
+	}
+
+	/*
+	 *	If "reject", break out of the loop and return
+	 *	reject.
+	 */
+	if (c->actions[result] == MOD_ACTION_REJECT) {
+		entry->result = RLM_MODULE_REJECT;
+		goto finish;
+	}
+
+	/*
+	 *	The array holds a default priority for this return
+	 *	code.  Grab it in preference to any unset priority.
+	 */
+	if (priority < 0) {
+		priority = c->actions[result];
+	}
+
+	/*
+	 *	We're higher than any previous priority, remember this
+	 *	return code and priority.
+	 */
+	if (priority > entry->priority) {
+		entry->result = result;
+		entry->priority = priority;
+	}
+
+#ifdef WITH_UNLANG
+	/*
+	 *	If we're processing a "case" statement, we return once
+	 *	it's done, rather than going to the next "case" statement.
+	 */
+	if (c->type == MOD_CASE) goto finish;
+#endif
+
+	/*
+	 *	If we've been told to stop processing
+	 *	it, do so.
+	 */
+	if (entry->unwind == MOD_BREAK) {
+		RDEBUG2("# unwind to enclosing foreach");
+		goto finish;
+	}
+
+	if (entry->unwind == MOD_RETURN) {
+		goto finish;
+	}
+
+next_sibling:
+	if (do_next_sibling) {
+		entry->c = entry->c->next;
+
+		if (entry->c) goto redo;
+	}
+
+finish:
+	/*
+	 *	And we're done!
+	 */
+	REXDENT();
+	return true;
+}
+
+
+/** Call a module, iteratively, with a local stack, rather than recursively
+ *
+ * What did Paul Graham say about Lisp...?
+ */
+int modcall(rlm_components_t component, modcallable *c, REQUEST *request)
+{
+	modcall_stack_entry_t stack[MODCALL_STACK_MAX];
+
+#ifndef NDEBUG
+	memset(stack, 0, sizeof(stack));
+#endif
+	/*
+	 *	Set up the initial stack frame.
+	 */
+	stack[0].c = c;
+	stack[0].result = default_component_results[component];
+	stack[0].priority = 0;
+	stack[0].unwind = 0;
+
+	/*
+	 *	Call the main handler.
+	 */
+	if (!modcall_recurse(request, component, 0, &stack[0], true)) {
+		return RLM_MODULE_FAIL;
+	}
+
+	/*
+	 *	Return the result.
+	 */
+	return stack[0].result;
+}
+
+
+#if 0
+static char const *action2str(int action)
+{
+	static char buf[32];
+	if(action==MOD_ACTION_RETURN)
+		return "return";
+	if(action==MOD_ACTION_REJECT)
+		return "reject";
+	snprintf(buf, sizeof buf, "%d", action);
+	return buf;
+}
+
+/* If you suspect a bug in the parser, you'll want to use these dump
+ * functions. dump_tree should reproduce a whole tree exactly as it was found
+ * in radiusd.conf, but in long form (all actions explicitly defined) */
+static void dump_mc(modcallable *c, int indent)
+{
+	int i;
+
+	if(c->type==MOD_SINGLE) {
+		modsingle *single = mod_callabletosingle(c);
+		DEBUG("%.*s%s {", indent, "\t\t\t\t\t\t\t\t\t\t\t",
+			single->modinst->name);
+	} else if ((c->type > MOD_SINGLE) && (c->type <= MOD_POLICY)) {
+		modgroup *g = mod_callabletogroup(c);
+		modcallable *p;
+		DEBUG("%.*s%s {", indent, "\t\t\t\t\t\t\t\t\t\t\t",
+		      unlang_keyword[c->type]);
+		for(p = g->children;p;p = p->next)
+			dump_mc(p, indent+1);
+	} /* else ignore it for now */
+
+	for(i = 0; i<RLM_MODULE_NUMCODES; ++i) {
+		DEBUG("%.*s%s = %s", indent+1, "\t\t\t\t\t\t\t\t\t\t\t",
+		      fr_int2str(mod_rcode_table, i, "<invalid>"),
+		      action2str(c->actions[i]));
+	}
+
+	DEBUG("%.*s}", indent, "\t\t\t\t\t\t\t\t\t\t\t");
+}
+
+static void dump_tree(rlm_components_t comp, modcallable *c)
+{
+	DEBUG("[%s]", comp2str[comp]);
+	dump_mc(c, 0);
+}
+#else
+#define dump_tree(a, b)
+#endif
+
+/* These are the default actions. For each component, the group{} block
+ * behaves like the code from the old module_*() function. redundant{}
+ * are based on my guesses of what they will be used for. --Pac. */
+static const int
+defaultactions[MOD_COUNT][GROUPTYPE_COUNT][RLM_MODULE_NUMCODES] =
+{
+	/* authenticate */
+	{
+		/* group */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			1,			/* fail     */
+			MOD_ACTION_RETURN,	/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			1,			/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			MOD_ACTION_RETURN,	/* notfound */
+			1,			/* noop     */
+			1			/* updated  */
+		},
+		/* redundant */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			1,			/* fail     */
+			MOD_ACTION_RETURN,	/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			MOD_ACTION_RETURN,	/* notfound */
+			MOD_ACTION_RETURN,	/* noop     */
+			MOD_ACTION_RETURN	/* updated  */
+		}
+	},
+	/* authorize */
+	{
+		/* group */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			MOD_ACTION_RETURN,	/* fail     */
+			3,			/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			1,			/* notfound */
+			2,			/* noop     */
+			4			/* updated  */
+		},
+		/* redundant */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			1,			/* fail     */
+			MOD_ACTION_RETURN,	/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			MOD_ACTION_RETURN,	/* notfound */
+			MOD_ACTION_RETURN,	/* noop     */
+			MOD_ACTION_RETURN	/* updated  */
+		}
+	},
+	/* preacct */
+	{
+		/* group */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			MOD_ACTION_RETURN,	/* fail     */
+			2,			/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			MOD_ACTION_RETURN,	/* notfound */
+			1,			/* noop     */
+			3			/* updated  */
+		},
+		/* redundant */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			1,			/* fail     */
+			MOD_ACTION_RETURN,	/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			MOD_ACTION_RETURN,	/* notfound */
+			MOD_ACTION_RETURN,	/* noop     */
+			MOD_ACTION_RETURN	/* updated  */
+		}
+	},
+	/* accounting */
+	{
+		/* group */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			MOD_ACTION_RETURN,	/* fail     */
+			2,			/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			MOD_ACTION_RETURN,	/* notfound */
+			1,			/* noop     */
+			3			/* updated  */
+		},
+		/* redundant */
+		{
+			1,			/* reject   */
+			1,			/* fail     */
+			MOD_ACTION_RETURN,	/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			1,			/* invalid  */
+			1,			/* userlock */
+			1,			/* notfound */
+			2,			/* noop     */
+			4			/* updated  */
+		}
+	},
+	/* checksimul */
+	{
+		/* group */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			1,			/* fail     */
+			MOD_ACTION_RETURN,	/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			MOD_ACTION_RETURN,	/* notfound */
+			MOD_ACTION_RETURN,	/* noop     */
+			MOD_ACTION_RETURN	/* updated  */
+		},
+		/* redundant */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			1,			/* fail     */
+			MOD_ACTION_RETURN,	/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			MOD_ACTION_RETURN,	/* notfound */
+			MOD_ACTION_RETURN,	/* noop     */
+			MOD_ACTION_RETURN	/* updated  */
+		}
+	},
+	/* pre-proxy */
+	{
+		/* group */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			MOD_ACTION_RETURN,	/* fail     */
+			3,			/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			1,			/* notfound */
+			2,			/* noop     */
+			4			/* updated  */
+		},
+		/* redundant */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			1,			/* fail     */
+			MOD_ACTION_RETURN,	/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			MOD_ACTION_RETURN,	/* notfound */
+			MOD_ACTION_RETURN,	/* noop     */
+			MOD_ACTION_RETURN	/* updated  */
+		}
+	},
+	/* post-proxy */
+	{
+		/* group */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			MOD_ACTION_RETURN,	/* fail     */
+			3,			/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			1,			/* notfound */
+			2,			/* noop     */
+			4			/* updated  */
+		},
+		/* redundant */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			1,			/* fail     */
+			MOD_ACTION_RETURN,	/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			MOD_ACTION_RETURN,	/* notfound */
+			MOD_ACTION_RETURN,	/* noop     */
+			MOD_ACTION_RETURN	/* updated  */
+		}
+	},
+	/* post-auth */
+	{
+		/* group */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			MOD_ACTION_RETURN,	/* fail     */
+			3,			/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			1,			/* notfound */
+			2,			/* noop     */
+			4			/* updated  */
+		},
+		/* redundant */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			1,			/* fail     */
+			MOD_ACTION_RETURN,	/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			MOD_ACTION_RETURN,	/* notfound */
+			MOD_ACTION_RETURN,	/* noop     */
+			MOD_ACTION_RETURN	/* updated  */
+		}
+	}
+#ifdef WITH_COA
+	,
+	/* recv-coa */
+	{
+		/* group */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			MOD_ACTION_RETURN,	/* fail     */
+			3,			/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			1,			/* notfound */
+			2,			/* noop     */
+			4			/* updated  */
+		},
+		/* redundant */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			1,			/* fail     */
+			MOD_ACTION_RETURN,	/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			MOD_ACTION_RETURN,	/* notfound */
+			MOD_ACTION_RETURN,	/* noop     */
+			MOD_ACTION_RETURN	/* updated  */
+		}
+	},
+	/* send-coa */
+	{
+		/* group */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			MOD_ACTION_RETURN,	/* fail     */
+			3,			/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			1,			/* notfound */
+			2,			/* noop     */
+			4			/* updated  */
+		},
+		/* redundant */
+		{
+			MOD_ACTION_RETURN,	/* reject   */
+			1,			/* fail     */
+			MOD_ACTION_RETURN,	/* ok       */
+			MOD_ACTION_RETURN,	/* handled  */
+			MOD_ACTION_RETURN,	/* invalid  */
+			MOD_ACTION_RETURN,	/* userlock */
+			MOD_ACTION_RETURN,	/* notfound */
+			MOD_ACTION_RETURN,	/* noop     */
+			MOD_ACTION_RETURN	/* updated  */
+		}
+	}
+#endif
+};
+
+static const int authtype_actions[GROUPTYPE_COUNT][RLM_MODULE_NUMCODES] =
+{
+	/* group */
+	{
+		MOD_ACTION_RETURN,	/* reject   */
+		MOD_ACTION_RETURN,	/* fail     */
+		4,			/* ok       */
+		MOD_ACTION_RETURN,	/* handled  */
+		MOD_ACTION_RETURN,	/* invalid  */
+		MOD_ACTION_RETURN,	/* userlock */
+		1,			/* notfound */
+		2,			/* noop     */
+		3			/* updated  */
+	},
+	/* redundant */
+	{
+		MOD_ACTION_RETURN,	/* reject   */
+		1,			/* fail     */
+		MOD_ACTION_RETURN,	/* ok       */
+		MOD_ACTION_RETURN,	/* handled  */
+		MOD_ACTION_RETURN,	/* invalid  */
+		MOD_ACTION_RETURN,	/* userlock */
+		MOD_ACTION_RETURN,	/* notfound */
+		MOD_ACTION_RETURN,	/* noop     */
+		MOD_ACTION_RETURN	/* updated  */
+	}
+};
+
+/** Validate and fixup a map that's part of an update section.
+ *
+ * @param map to validate.
+ * @param ctx data to pass to fixup function (currently unused).
+ * @return 0 if valid else -1.
+ */
+int modcall_fixup_update(vp_map_t *map, UNUSED void *ctx)
+{
+	CONF_PAIR *cp = cf_item_to_pair(map->ci);
+	/*
+	 *	Anal-retentive checks.
+	 */
+	if (DEBUG_ENABLED3) {
+		if ((map->lhs->type == TMPL_TYPE_ATTR) && (map->lhs->name[0] != '&')) {
+			WARN("%s[%d]: Please change attribute reference to '&%s %s ...'",
+			     cf_pair_filename(cp), cf_pair_lineno(cp),
+			     map->lhs->name, fr_int2str(fr_tokens, map->op, "<INVALID>"));
+		}
+
+		if ((map->rhs->type == TMPL_TYPE_ATTR) && (map->rhs->name[0] != '&')) {
+			WARN("%s[%d]: Please change attribute reference to '... %s &%s'",
+			     cf_pair_filename(cp), cf_pair_lineno(cp),
+			     fr_int2str(fr_tokens, map->op, "<INVALID>"), map->rhs->name);
+		}
+	}
+
+	/*
+	 *	Values used by unary operators should be literal ANY
+	 *
+	 *	We then free the template and alloc a NULL one instead.
+	 */
+	if (map->op == T_OP_CMP_FALSE) {
+	 	if ((map->rhs->type != TMPL_TYPE_LITERAL) || (strcmp(map->rhs->name, "ANY") != 0)) {
+			WARN("%s[%d] Wildcard deletion MUST use '!* ANY'",
+			     cf_pair_filename(cp), cf_pair_lineno(cp));
+		}
+
+		TALLOC_FREE(map->rhs);
+
+		map->rhs = tmpl_alloc(map, TMPL_TYPE_NULL, NULL, 0);
+	}
+
+	/*
+	 *	Lots of sanity checks for insane people...
+	 */
+
+	/*
+	 *	What exactly where you expecting to happen here?
+	 */
+	if ((map->lhs->type == TMPL_TYPE_ATTR) &&
+	    (map->rhs->type == TMPL_TYPE_LIST)) {
+		cf_log_err(map->ci, "Can't copy list into an attribute");
+		return -1;
+	}
+
+	/*
+	 *	Depending on the attribute type, some operators are disallowed.
+	 */
+	if ((map->lhs->type == TMPL_TYPE_ATTR) && (!fr_assignment_op[map->op] && !fr_equality_op[map->op])) {
+		cf_log_err(map->ci, "Invalid operator \"%s\" in update section.  "
+			   "Only assignment or filter operators are allowed",
+			   fr_int2str(fr_tokens, map->op, "<INVALID>"));
+		return -1;
+	}
+
+	if (map->lhs->type == TMPL_TYPE_LIST) {
+		/*
+		 *	Can't copy an xlat expansion or literal into a list,
+		 *	we don't know what type of attribute we'd need
+		 *	to create.
+		 *
+		 *	The only exception is where were using a unary
+		 *	operator like !*.
+		 */
+	    	if (map->op != T_OP_CMP_FALSE) switch (map->rhs->type) {
+	    	case TMPL_TYPE_XLAT:
+	    	case TMPL_TYPE_LITERAL:
+			cf_log_err(map->ci, "Can't copy value into list (we don't know which attribute to create)");
+			return -1;
+
+		default:
+			break;
+		}
+
+		/*
+		 *	Only += and :=, and !*, and ^= operators are supported
+		 *	for lists.
+		 */
+		switch (map->op) {
+		case T_OP_CMP_FALSE:
+			break;
+
+		case T_OP_ADD:
+			if ((map->rhs->type != TMPL_TYPE_LIST) &&
+			    (map->rhs->type != TMPL_TYPE_EXEC)) {
+				cf_log_err(map->ci, "Invalid source for list assignment '%s += ...'", map->lhs->name);
+				return -1;
+			}
+			break;
+
+		case T_OP_SET:
+			if (map->rhs->type == TMPL_TYPE_EXEC) {
+				WARN("%s[%d]: Please change ':=' to '=' for list assignment",
+				     cf_pair_filename(cp), cf_pair_lineno(cp));
+			}
+
+			if (map->rhs->type != TMPL_TYPE_LIST) {
+				cf_log_err(map->ci, "Invalid source for list assignment '%s := ...'", map->lhs->name);
+				return -1;
+			}
+			break;
+
+		case T_OP_EQ:
+			if (map->rhs->type != TMPL_TYPE_EXEC) {
+				cf_log_err(map->ci, "Invalid source for list assignment '%s = ...'", map->lhs->name);
+				return -1;
+			}
+			break;
+
+		case T_OP_PREPEND:
+			if ((map->rhs->type != TMPL_TYPE_LIST) &&
+			    (map->rhs->type != TMPL_TYPE_EXEC)) {
+				cf_log_err(map->ci, "Invalid source for list assignment '%s ^= ...'", map->lhs->name);
+				return -1;
+			}
+			break;
+
+		default:
+			cf_log_err(map->ci, "Operator \"%s\" not allowed for list assignment",
+				   fr_int2str(fr_tokens, map->op, "<INVALID>"));
+			return -1;
+		}
+	}
+
+	/*
+	 *	If the map has a unary operator there's no further
+	 *	processing we need to, as RHS is unused.
+	 */
+	if (map->op == T_OP_CMP_FALSE) return 0;
+
+	/*
+	 *	If LHS is an attribute, and RHS is a literal, we can
+	 *	preparse the information into a TMPL_TYPE_DATA.
+	 *
+	 *	Unless it's a unary operator in which case we
+	 *	ignore map->rhs.
+	 */
+	if ((map->lhs->type == TMPL_TYPE_ATTR) && (map->rhs->type == TMPL_TYPE_LITERAL)) {
+		/*
+		 *	It's a literal string, just copy it.
+		 *	Don't escape anything.
+		 */
+		if (!cf_new_escape &&
+		    (map->lhs->tmpl_da->type == PW_TYPE_STRING) &&
+		    (cf_pair_value_type(cp) == T_SINGLE_QUOTED_STRING)) {
+			tmpl_cast_in_place_str(map->rhs);
+
+		} else {
+			/*
+			 *	RHS is hex, try to parse it as
+			 *	type-specific data.
+			 */
+			if (map->lhs->auto_converted &&
+			    (map->rhs->name[0] == '0') && (map->rhs->name[1] == 'x') &&
+			    (map->rhs->len > 2) && ((map->rhs->len & 0x01) == 0)) {
+				vp_tmpl_t *vpt = map->rhs;
+				map->rhs = NULL;
+
+				if (!map_cast_from_hex(map, T_BARE_WORD, vpt->name)) {
+					map->rhs = vpt;
+					cf_log_err(map->ci, "Cannot parse RHS hex as the data type of the attribute %s", map->lhs->tmpl_da->name);
+					return -1;
+				}
+				talloc_free(vpt);
+
+			} else if (tmpl_cast_in_place(map->rhs, map->lhs->tmpl_da->type, map->lhs->tmpl_da) < 0) {
+				cf_log_err(map->ci, "%s", fr_strerror());
+				return -1;
+			}
+
+			/*
+			 *	Fixup LHS da if it doesn't match the type
+			 *	of the RHS.
+			 */
+			if (map->lhs->tmpl_da->type != map->rhs->tmpl_data_type) {
+				DICT_ATTR const *da;
+
+				da = dict_attrbytype(map->lhs->tmpl_da->attr, map->lhs->tmpl_da->vendor,
+						     map->rhs->tmpl_data_type);
+				if (!da) {
+					cf_log_err(map->ci, "Cannot find %s variant of attribute \"%s\"",
+						   fr_int2str(dict_attr_types, map->rhs->tmpl_data_type,
+							      "<INVALID>"), map->lhs->tmpl_da->name);
+					return -1;
+				}
+				map->lhs->tmpl_da = da;
+			}
+		}
+	} /* else we can't precompile the data */
+
+	return 0;
+}
+
+
+#ifdef WITH_UNLANG
+static modcallable *do_compile_modupdate(modcallable *parent, rlm_components_t component,
+					 CONF_SECTION *cs, char const *name2)
+{
+	int rcode;
+	modgroup *g;
+	modcallable *csingle;
+
+	vp_map_t *head;
+
+	/*
+	 *	This looks at cs->name2 to determine which list to update
+	 */
+	rcode = map_afrom_cs(&head, cs, PAIR_LIST_REQUEST, PAIR_LIST_REQUEST, modcall_fixup_update, NULL, 128);
+	if (rcode < 0) return NULL; /* message already printed */
+	if (!head) {
+		cf_log_err_cs(cs, "'update' sections cannot be empty");
+		return NULL;
+	}
+
+	g = talloc_zero(parent, modgroup);
+	csingle = mod_grouptocallable(g);
+
+	csingle->parent = parent;
+	csingle->next = NULL;
+
+	if (name2) {
+		csingle->name = name2;
+	} else {
+		csingle->name = "";
+	}
+	csingle->type = MOD_UPDATE;
+	csingle->method = component;
+
+	memcpy(csingle->actions, defaultactions[component][GROUPTYPE_SIMPLE],
+	       sizeof(csingle->actions));
+
+	g->grouptype = GROUPTYPE_SIMPLE;
+	g->children = NULL;
+	g->cs = cs;
+	g->map = talloc_steal(g, head);
+
+	return csingle;
+}
+
+
+static modcallable *do_compile_modswitch (modcallable *parent, rlm_components_t component, CONF_SECTION *cs)
+{
+	CONF_ITEM *ci;
+	FR_TOKEN type;
+	char const *name2;
+	bool had_seen_default = false;
+	modcallable *csingle;
+	modgroup *g;
+	ssize_t slen;
+	vp_tmpl_t *vpt;
+
+	name2 = cf_section_name2(cs);
+	if (!name2) {
+		cf_log_err_cs(cs, "You must specify a variable to switch over for 'switch'");
+		return NULL;
+	}
+
+	if (!cf_item_find_next(cs, NULL)) {
+		cf_log_err_cs(cs, "'switch' statements cannot be empty");
+		return NULL;
+	}
+
+	/*
+	 *	Create the template.  If we fail, AND it's a bare word
+	 *	with &Foo-Bar, it MAY be an attribute defined by a
+	 *	module.  Allow it for now.  The pass2 checks below
+	 *	will fix it up.
+	 */
+	type = cf_section_name2_type(cs);
+	slen = tmpl_afrom_str(cs, &vpt, name2, strlen(name2), type, REQUEST_CURRENT, PAIR_LIST_REQUEST, true);
+	if ((slen < 0) && ((type != T_BARE_WORD) || (name2[0] != '&'))) {
+		char *spaces, *text;
+
+		fr_canonicalize_error(cs, &spaces, &text, slen, fr_strerror());
+
+		cf_log_err_cs(cs, "Syntax error");
+		cf_log_err_cs(cs, "%s", name2);
+		cf_log_err_cs(cs, "%s^ %s", spaces, text);
+
+		talloc_free(spaces);
+		talloc_free(text);
+
+		return NULL;
+	}
+
+	/*
+	 *	Otherwise a NULL vpt may refer to an attribute defined
+	 *	by a module.  That is checked in pass 2.
+	 */
+
+	if (vpt->type == TMPL_TYPE_LIST) {
+		cf_log_err_cs(cs, "Syntax error: Cannot switch over list '%s'", name2);
+		return NULL;
+	}
+
+	/*
+	 *	Warn about confusing things.
+	 */
+	if ((vpt->type == TMPL_TYPE_ATTR) && (*name2 != '&')) {
+		WARN("%s[%d]: Please change \"switch %s\" to \"switch &%s\"",
+		     cf_section_filename(cs), cf_section_lineno(cs),
+		     name2, name2);
+	}
+
+	/*
+	 *	Walk through the children of the switch section,
+	 *	ensuring that they're all 'case' statements
+	 */
+	for (ci = cf_item_find_next(cs, NULL);
+	     ci != NULL;
+	     ci = cf_item_find_next(cs, ci)) {
+		CONF_SECTION *subcs;
+		char const *name1;
+
+		if (!cf_item_is_section(ci)) {
+			if (!cf_item_is_pair(ci)) continue;
+
+			cf_log_err(ci, "\"switch\" sections can only have \"case\" subsections");
+			talloc_free(vpt);
+			return NULL;
+		}
+
+		subcs = cf_item_to_section(ci);	/* can't return NULL */
+		name1 = cf_section_name1(subcs);
+
+		if (strcmp(name1, "case") != 0) {
+			cf_log_err(ci, "\"switch\" sections can only have \"case\" subsections");
+			talloc_free(vpt);
+			return NULL;
+		}
+
+		name2 = cf_section_name2(subcs);
+		if (!name2) {
+			if (!had_seen_default) {
+				had_seen_default = true;
+				continue;
+			}
+
+			cf_log_err(ci, "Cannot have two 'default' case statements");
+			talloc_free(vpt);
+			return NULL;
+		}
+	}
+
+	csingle = do_compile_modgroup(parent, component, cs,
+				      GROUPTYPE_SIMPLE,
+				      GROUPTYPE_SIMPLE,
+				      MOD_SWITCH);
+	if (!csingle) {
+		talloc_free(vpt);
+		return NULL;
+	}
+
+	g = mod_callabletogroup(csingle);
+	g->vpt = talloc_steal(g, vpt);
+
+	return csingle;
+}
+
+static modcallable *do_compile_modcase(modcallable *parent, rlm_components_t component, CONF_SECTION *cs)
+{
+	int i;
+	char const *name2;
+	modcallable *csingle;
+	modgroup *g;
+	vp_tmpl_t *vpt;
+
+	if (!parent || (parent->type != MOD_SWITCH)) {
+		cf_log_err_cs(cs, "\"case\" statements may only appear within a \"switch\" section");
+		return NULL;
+	}
+
+	/*
+	 *	case THING means "match THING"
+	 *	case       means "match anything"
+	 */
+	name2 = cf_section_name2(cs);
+	if (name2) {
+		ssize_t slen;
+		FR_TOKEN type;
+
+		type = cf_section_name2_type(cs);
+
+		slen = tmpl_afrom_str(cs, &vpt, name2, strlen(name2), type, REQUEST_CURRENT, PAIR_LIST_REQUEST, true);
+		if ((slen < 0) && ((type != T_BARE_WORD) || (name2[0] != '&'))) {
+			char *spaces, *text;
+
+			fr_canonicalize_error(cs, &spaces, &text, slen, fr_strerror());
+
+			cf_log_err_cs(cs, "Syntax error");
+			cf_log_err_cs(cs, "%s", name2);
+			cf_log_err_cs(cs, "%s^ %s", spaces, text);
+
+			talloc_free(spaces);
+			talloc_free(text);
+
+			return NULL;
+		}
+
+		if (vpt->type == TMPL_TYPE_LIST) {
+			cf_log_err_cs(cs, "Syntax error: Cannot match list '%s'", name2);
+			return NULL;
+		}
+
+		/*
+		 *	Otherwise a NULL vpt may refer to an attribute defined
+		 *	by a module.  That is checked in pass 2.
+		 */
+
+	} else {
+		vpt = NULL;
+	}
+
+	csingle = do_compile_modgroup(parent, component, cs,
+				      GROUPTYPE_SIMPLE,
+				      GROUPTYPE_SIMPLE,
+				      MOD_CASE);
+	if (!csingle) {
+		talloc_free(vpt);
+		return NULL;
+	}
+
+	/*
+	 *	The interpretor expects this to be NULL for the
+	 *	default case.  do_compile_modgroup sets it to name2,
+	 *	unless name2 is NULL, in which case it sets it to name1.
+	 */
+	csingle->name = name2;
+
+	g = mod_callabletogroup(csingle);
+	g->vpt = talloc_steal(g, vpt);
+
+	/*
+	 *	Set all of it's codes to return, so that
+	 *	when we pick a 'case' statement, we don't
+	 *	fall through to processing the next one.
+	 */
+	for (i = 0; i < RLM_MODULE_NUMCODES; i++) {
+		csingle->actions[i] = MOD_ACTION_RETURN;
+	}
+
+	return csingle;
+}
+
+static modcallable *do_compile_modforeach(modcallable *parent,
+					  rlm_components_t component, CONF_SECTION *cs)
+{
+	FR_TOKEN type;
+	char const *name2;
+	modcallable *csingle;
+	modgroup *g;
+	ssize_t slen;
+	vp_tmpl_t *vpt;
+
+	name2 = cf_section_name2(cs);
+	if (!name2) {
+		cf_log_err_cs(cs,
+			   "You must specify an attribute to loop over in 'foreach'");
+		return NULL;
+	}
+
+	if (!cf_item_find_next(cs, NULL)) {
+		cf_log_err_cs(cs, "'foreach' blocks cannot be empty");
+		return NULL;
+	}
+
+	/*
+	 *	Create the template.  If we fail, AND it's a bare word
+	 *	with &Foo-Bar, it MAY be an attribute defined by a
+	 *	module.  Allow it for now.  The pass2 checks below
+	 *	will fix it up.
+	 */
+	type = cf_section_name2_type(cs);
+	slen = tmpl_afrom_str(cs, &vpt, name2, strlen(name2), type, REQUEST_CURRENT, PAIR_LIST_REQUEST, true);
+	if ((slen < 0) && ((type != T_BARE_WORD) || (name2[0] != '&'))) {
+		char *spaces, *text;
+
+		fr_canonicalize_error(cs, &spaces, &text, slen, fr_strerror());
+
+		cf_log_err_cs(cs, "Syntax error");
+		cf_log_err_cs(cs, "%s", name2);
+		cf_log_err_cs(cs, "%s^ %s", spaces, text);
+
+		talloc_free(spaces);
+		talloc_free(text);
+
+		return NULL;
+	}
+
+	/*
+	 *	If we don't have a negative return code, we must have a vpt
+	 *	(mostly to quiet coverity).
+	 */
+	rad_assert(vpt);
+
+	if ((vpt->type != TMPL_TYPE_ATTR) && (vpt->type != TMPL_TYPE_LIST)) {
+		cf_log_err_cs(cs, "MUST use attribute or list reference in 'foreach'");
+		return NULL;
+	}
+
+	/*
+	 *	Fix up the template to iterate over all instances of
+	 *	the attribute. In a perfect consistent world, users would do
+	 *	foreach &attr[*], but that's taking the consistency thing a bit far.
+	 */
+	vpt->tmpl_num = NUM_ALL;
+
+	csingle = do_compile_modgroup(parent, component, cs,
+				      GROUPTYPE_SIMPLE, GROUPTYPE_SIMPLE,
+				      MOD_FOREACH);
+
+	if (!csingle) {
+		talloc_free(vpt);
+		return NULL;
+	}
+
+	g = mod_callabletogroup(csingle);
+	g->vpt = vpt;
+
+	return csingle;
+}
+
+static modcallable *do_compile_modbreak(modcallable *parent,
+					rlm_components_t component, CONF_ITEM const *ci)
+{
+	CONF_SECTION const *cs = NULL;
+
+	for (cs = cf_item_parent(ci);
+	     cs != NULL;
+	     cs = cf_item_parent(cf_section_to_item(cs))) {
+		if (strcmp(cf_section_name1(cs), "foreach") == 0) {
+			break;
+		}
+	}
+
+	if (!cs) {
+		cf_log_err(ci, "'break' can only be used in a 'foreach' section");
+		return NULL;
+	}
+
+	return do_compile_modgroup(parent, component, NULL,
+				   GROUPTYPE_SIMPLE, GROUPTYPE_SIMPLE,
+				   MOD_BREAK);
+}
+#endif
+
+static modcallable *do_compile_modserver(modcallable *parent,
+					 rlm_components_t component, CONF_ITEM *ci,
+					 char const *name,
+					 CONF_SECTION *cs,
+					 char const *server)
+{
+	modcallable *csingle;
+	CONF_SECTION *subcs;
+	modref *mr;
+
+	subcs = cf_section_sub_find_name2(cs, comp2str[component], NULL);
+	if (!subcs) {
+		cf_log_err(ci, "Server %s has no %s section",
+			   server, comp2str[component]);
+		return NULL;
+	}
+
+	mr = talloc_zero(parent, modref);
+
+	csingle = mod_reftocallable(mr);
+	csingle->parent = parent;
+	csingle->next = NULL;
+	csingle->name = name;
+	csingle->type = MOD_REFERENCE;
+	csingle->method = component;
+
+	memcpy(csingle->actions, defaultactions[component][GROUPTYPE_SIMPLE],
+	       sizeof(csingle->actions));
+
+	mr->ref_name = strdup(server);
+	mr->ref_cs = cs;
+
+	return csingle;
+}
+
+static modcallable *do_compile_modxlat(modcallable *parent,
+				       rlm_components_t component, char const *fmt)
+{
+	modcallable *csingle;
+	modxlat *mx;
+
+	mx = talloc_zero(parent, modxlat);
+
+	csingle = mod_xlattocallable(mx);
+	csingle->parent = parent;
+	csingle->next = NULL;
+	csingle->name = "expand";
+	csingle->type = MOD_XLAT;
+	csingle->method = component;
+
+	memcpy(csingle->actions, defaultactions[component][GROUPTYPE_SIMPLE],
+	       sizeof(csingle->actions));
+
+	mx->xlat_name = talloc_strdup(mx, fmt);
+	if (!mx->xlat_name) {
+		talloc_free(mx);
+		return NULL;
+	}
+
+	if (fmt[0] != '%') {
+		char *p;
+		mx->exec = true;
+
+		strcpy(mx->xlat_name, fmt + 1);
+		p = strrchr(mx->xlat_name, '`');
+		if (p) *p = '\0';
+	}
+
+	return csingle;
+}
+
+/*
+ *	redundant, etc. can refer to modules or groups, but not much else.
+ */
+static int all_children_are_modules(CONF_SECTION *cs, char const *name)
+{
+	CONF_ITEM *ci;
+
+	for (ci=cf_item_find_next(cs, NULL);
+	     ci != NULL;
+	     ci=cf_item_find_next(cs, ci)) {
+		/*
+		 *	If we're a redundant, etc. group, then the
+		 *	intention is to call modules, rather than
+		 *	processing logic.  These checks aren't
+		 *	*strictly* necessary, but they keep the users
+		 *	from doing crazy things.
+		 */
+		if (cf_item_is_section(ci)) {
+			CONF_SECTION *subcs = cf_item_to_section(ci);
+			char const *name1 = cf_section_name1(subcs);
+
+			if ((strcmp(name1, "if") == 0) ||
+			    (strcmp(name1, "else") == 0) ||
+			    (strcmp(name1, "elsif") == 0) ||
+			    (strcmp(name1, "update") == 0) ||
+			    (strcmp(name1, "switch") == 0) ||
+			    (strcmp(name1, "case") == 0)) {
+				cf_log_err(ci, "%s sections cannot contain a \"%s\" statement",
+				       name, name1);
+				return 0;
+			}
+			continue;
+		}
+
+		if (cf_item_is_pair(ci)) {
+			CONF_PAIR *cp = cf_item_to_pair(ci);
+			if (cf_pair_value(cp) != NULL) {
+				cf_log_err(ci,
+					   "Entry with no value is invalid");
+				return 0;
+			}
+		}
+	}
+
+	return 1;
+}
+
+/** Load a named module from "instantiate" or "policy".
+ *
+ * If it's "foo.method", look for "foo", and return "method" as the method
+ * we wish to use, instead of the input component.
+ *
+ * @param[out] pcomponent Where to write the method we found, if any.  If no method is specified
+ *	will be set to MOD_COUNT.
+ * @param[in] real_name Complete name string e.g. foo.authorize.
+ * @param[in] virtual_name Virtual module name e.g. foo.
+ * @param[in] method_name Method override (may be NULL) or the method name e.g. authorize.
+ * @return the CONF_SECTION specifying the virtual module.
+ */
+static CONF_SECTION *virtual_module_find_cs(rlm_components_t *pcomponent,
+					    char const *real_name, char const *virtual_name, char const *method_name)
+{
+	CONF_SECTION *cs, *subcs;
+	rlm_components_t method = *pcomponent;
+	char buffer[256];
+
+	/*
+	 *	Turn the method name into a method enum.
+	 */
+	if (method_name) {
+		rlm_components_t i;
+
+		for (i = MOD_AUTHENTICATE; i < MOD_COUNT; i++) {
+			if (strcmp(comp2str[i], method_name) == 0) break;
+		}
+
+		if (i != MOD_COUNT) {
+			method = i;
+		} else {
+			method_name = NULL;
+			virtual_name = real_name;
+		}
+	}
+
+	/*
+	 *	Look for "foo" in the "instantiate" section.  If we
+	 *	find it, AND there's no method name, we've found the
+	 *	right thing.
+	 *
+	 *	Return it to the caller, with the updated method.
+	 */
+	cs = cf_section_find("instantiate");
+	if (cs) {
+		/*
+		 *	Found "foo".  Load it as "foo", or "foo.method".
+		 */
+		subcs = cf_section_sub_find_name2(cs, NULL, virtual_name);
+		if (subcs) {
+			*pcomponent = method;
+			return subcs;
+		}
+	}
+
+	/*
+	 *	Look for it in "policy".
+	 *
+	 *	If there's no policy section, we can't do anything else.
+	 */
+	cs = cf_section_find("policy");
+	if (!cs) return NULL;
+
+	/*
+	 *	"foo.authorize" means "load policy "foo" as method "authorize".
+	 *
+	 *	And bail out if there's no policy "foo".
+	 */
+	if (method_name) {
+		subcs = cf_section_sub_find_name2(cs, NULL, virtual_name);
+		if (subcs) *pcomponent = method;
+
+		return subcs;
+	}
+
+	/*
+	 *	"foo" means "look for foo.component" first, to allow
+	 *	method overrides.  If that's not found, just look for
+	 *	a policy "foo".
+	 *
+	 */
+	snprintf(buffer, sizeof(buffer), "%s.%s",
+		 virtual_name, comp2str[method]);
+	subcs = cf_section_sub_find_name2(cs, NULL, buffer);
+	if (subcs) return subcs;
+
+	return cf_section_sub_find_name2(cs, NULL, virtual_name);
+}
+
+
+/*
+ *	Compile one entry of a module call.
+ */
+static modcallable *do_compile_modsingle(modcallable *parent,
+					 rlm_components_t component, CONF_ITEM *ci,
+					 int grouptype,
+					 char const **modname)
+{
+	char const *modrefname, *p;
+	modsingle *single;
+	modcallable *csingle;
+	module_instance_t *this;
+	CONF_SECTION *cs, *subcs, *modules;
+	CONF_SECTION *loop;
+	char const *realname;
+	rlm_components_t method = component;
+
+	if (cf_item_is_section(ci)) {
+		char const *name2;
+
+		cs = cf_item_to_section(ci);
+		modrefname = cf_section_name1(cs);
+		name2 = cf_section_name2(cs);
+		if (!name2) name2 = "";
+
+		/*
+		 *	group{}, redundant{}, or append{} may appear
+		 *	where a single module instance was expected.
+		 *	In that case, we hand it off to
+		 *	compile_modgroup
+		 */
+		if (strcmp(modrefname, "group") == 0) {
+			*modname = name2;
+			return do_compile_modgroup(parent, component, cs,
+						   GROUPTYPE_SIMPLE,
+						   grouptype, MOD_GROUP);
+
+		} else if (strcmp(modrefname, "redundant") == 0) {
+			*modname = name2;
+
+			if (!all_children_are_modules(cs, modrefname)) {
+				return NULL;
+			}
+
+			return do_compile_modgroup(parent, component, cs,
+						   GROUPTYPE_REDUNDANT,
+						   grouptype, MOD_GROUP);
+
+		} else if (strcmp(modrefname, "load-balance") == 0) {
+			*modname = name2;
+
+			if (!all_children_are_modules(cs, modrefname)) {
+				return NULL;
+			}
+
+			return do_compile_modgroup(parent, component, cs,
+						   GROUPTYPE_SIMPLE,
+						   grouptype, MOD_LOAD_BALANCE);
+
+		} else if (strcmp(modrefname, "redundant-load-balance") == 0) {
+			*modname = name2;
+
+			if (!all_children_are_modules(cs, modrefname)) {
+				return NULL;
+			}
+
+			return do_compile_modgroup(parent, component, cs,
+						   GROUPTYPE_REDUNDANT,
+						   grouptype, MOD_REDUNDANT_LOAD_BALANCE);
+
+#ifdef WITH_UNLANG
+		} else 	if (strcmp(modrefname, "if") == 0) {
+			if (!cf_section_name2(cs)) {
+				cf_log_err(ci, "'if' without condition");
+				return NULL;
+			}
+
+			*modname = name2;
+			csingle= do_compile_modgroup(parent, component, cs,
+						     GROUPTYPE_SIMPLE,
+						     grouptype, MOD_IF);
+			if (!csingle) return NULL;
+			*modname = name2;
+
+			return csingle;
+
+		} else 	if (strcmp(modrefname, "elsif") == 0) {
+			if (parent &&
+			    ((parent->type == MOD_LOAD_BALANCE) ||
+			     (parent->type == MOD_REDUNDANT_LOAD_BALANCE))) {
+				cf_log_err(ci, "'elsif' cannot be used in this section");
+				return NULL;
+			}
+
+			if (!cf_section_name2(cs)) {
+				cf_log_err(ci, "'elsif' without condition");
+				return NULL;
+			}
+
+			*modname = name2;
+			return do_compile_modgroup(parent, component, cs,
+						   GROUPTYPE_SIMPLE,
+						   grouptype, MOD_ELSIF);
+
+		} else 	if (strcmp(modrefname, "else") == 0) {
+			if (parent &&
+			    ((parent->type == MOD_LOAD_BALANCE) ||
+			     (parent->type == MOD_REDUNDANT_LOAD_BALANCE))) {
+				cf_log_err(ci, "'else' cannot be used in this section section");
+				return NULL;
+			}
+
+			if (cf_section_name2(cs)) {
+				cf_log_err(ci, "Cannot have conditions on 'else'");
+				return NULL;
+			}
+
+			*modname = name2;
+			return  do_compile_modgroup(parent, component, cs,
+						    GROUPTYPE_SIMPLE,
+						    grouptype, MOD_ELSE);
+
+		} else 	if (strcmp(modrefname, "update") == 0) {
+			*modname = name2;
+
+			return do_compile_modupdate(parent, component, cs,
+						    name2);
+
+		} else 	if (strcmp(modrefname, "switch") == 0) {
+			*modname = name2;
+
+			return do_compile_modswitch (parent, component, cs);
+
+		} else 	if (strcmp(modrefname, "case") == 0) {
+			*modname = name2;
+
+			return do_compile_modcase(parent, component, cs);
+
+		} else 	if (strcmp(modrefname, "foreach") == 0) {
+			*modname = name2;
+
+			return do_compile_modforeach(parent, component, cs);
+
+#endif
+		} /* else it's something like sql { fail = 1 ...} */
+
+	} else if (!cf_item_is_pair(ci)) { /* CONF_DATA or some such */
+		return NULL;
+
+		/*
+		 *	Else it's a module reference, with updated return
+		 *	codes.
+		 */
+	} else {
+		CONF_PAIR *cp = cf_item_to_pair(ci);
+		modrefname = cf_pair_attr(cp);
+
+		/*
+		 *	Actions (ok = 1), etc. are orthogonal to just
+		 *	about everything else.
+		 */
+		if (cf_pair_value(cp) != NULL) {
+			cf_log_err(ci, "Entry is not a reference to a module");
+			return NULL;
+		}
+
+		/*
+		 *	In-place xlat's via %{...}.
+		 *
+		 *	This should really be removed from the server.
+		 */
+		if (((modrefname[0] == '%') && (modrefname[1] == '{')) ||
+		    (modrefname[0] == '`')) {
+			return do_compile_modxlat(parent, component,
+						  modrefname);
+		}
+	}
+
+#ifdef WITH_UNLANG
+	/*
+	 *	These can't be over-ridden.
+	 */
+	if (strcmp(modrefname, "break") == 0) {
+		if (!cf_item_is_pair(ci)) {
+			cf_log_err(ci, "Invalid use of 'break' as section name.");
+			return NULL;
+		}
+
+		return do_compile_modbreak(parent, component, ci);
+	}
+
+	if (strcmp(modrefname, "return") == 0) {
+		if (!cf_item_is_pair(ci)) {
+			cf_log_err(ci, "Invalid use of 'return' as section name.");
+			return NULL;
+		}
+
+		return do_compile_modgroup(parent, component, NULL,
+					   GROUPTYPE_SIMPLE, GROUPTYPE_SIMPLE,
+					   MOD_RETURN);
+	}
+#endif
+
+	/*
+	 *	Run a virtual server.  This is really terrible and
+	 *	should be deleted.
+	 */
+	if (strncmp(modrefname, "server[", 7) == 0) {
+		char buffer[256];
+
+		if (!cf_item_is_pair(ci)) {
+			cf_log_err(ci, "Invalid syntax");
+			return NULL;
+		}
+
+		strlcpy(buffer, modrefname + 7, sizeof(buffer));
+		p = strrchr(buffer, ']');
+		if (!p || p[1] != '\0' || (p == buffer)) {
+			cf_log_err(ci, "Invalid server reference in \"%s\".", modrefname);
+			return NULL;
+		}
+
+		buffer[p - buffer] = '\0';
+
+		cs = cf_section_sub_find_name2(NULL, "server", buffer);
+		if (!cs) {
+			cf_log_err(ci, "No such server \"%s\".", buffer);
+			return NULL;
+		}
+
+		/*
+		 *	Ignore stupid attempts to over-ride the return
+		 *	code.
+		 */
+		return do_compile_modserver(parent, component, ci,
+					    modrefname, cs, buffer);
+	}
+
+	/*
+	 *	We now have a name.  It can be one of two forms.  A
+	 *	bare module name, or a section named for the module,
+	 *	with over-rides for the return codes.
+	 *
+	 *	The name can refer to a real module, in the "modules"
+	 *	section.  In that case, the name will be either the
+	 *	first or second name of the sub-section of "modules".
+	 *
+	 *	Or, the name can refer to a policy, in the "policy"
+	 *	section.  In that case, the name will be first name of
+	 *	the sub-section of "policy".  Unless it's a "redudant"
+	 *	block...
+	 *
+	 *	Or, the name can refer to a "module.method", in which
+	 *	case we're calling a different method than normal for
+	 *	this section.
+	 *
+	 *	Or, the name can refer to a virtual module, in the
+	 *	"instantiate" section.  In that case, the name will be
+	 *	the first of the sub-section of "instantiate".  Unless
+	 *	it's a "redudant" block...
+	 *
+	 *	We try these in sequence, from the bottom up.  This is
+	 *	so that things in "instantiate" and "policy" can
+	 *	over-ride calls to real modules.
+	 */
+
+
+	/*
+	 *	Try:
+	 *
+	 *	instantiate { ... name { ...} ... }
+	 *	instantiate { ... name.method { ...} ... }
+	 *	policy { ... name { .. } .. }
+	 *	policy { ... name.method { .. } .. }
+	 *
+	 *	The only difference between things in "instantiate"
+	 *	and "policy" is that "instantiate" will cause modules
+	 *	to be instantiated in a particular order.
+	 */
+	subcs = NULL;
+	p = strrchr(modrefname, '.');
+	if (!p) {
+		subcs = virtual_module_find_cs(&method, modrefname, modrefname, NULL);
+	} else {
+		char buffer[256];
+
+		strlcpy(buffer, modrefname, sizeof(buffer));
+		buffer[p - modrefname] = '\0';
+
+		subcs = virtual_module_find_cs(&method, modrefname, buffer, buffer + (p - modrefname) + 1);
+	}
+
+	/*
+	 *	Check that we're not creating a loop.  We may
+	 *	be compiling an "sql" module reference inside
+	 *	of an "sql" policy.  If so, we allow the
+	 *	second "sql" to refer to the module.
+	 */
+	for (loop = cf_item_parent(ci);
+	     loop && subcs;
+	     loop = cf_item_parent(cf_section_to_item(loop))) {
+		if (loop == subcs) {
+			subcs = NULL;
+		}
+	}
+
+	/*
+	 *	We've found the relevant entry.  It MUST be a
+	 *	sub-section.
+	 *
+	 *	However, it can be a "redundant" block, or just a
+	 *	section name.
+	 */
+	if (subcs) {
+		/*
+		 *	modules.c takes care of ensuring that this is:
+		 *
+		 *	group foo { ...
+		 *	load-balance foo { ...
+		 *	redundant foo { ...
+		 *	redundant-load-balance foo { ...
+		 *
+		 *	We can just recurs to compile the section as
+		 *	if it was found here.
+		 */
+		if (cf_section_name2(subcs)) {
+			csingle = do_compile_modsingle(parent,
+						       method,
+						       cf_section_to_item(subcs),
+						       grouptype,
+						       modname);
+		} else {
+			/*
+			 *	We have:
+			 *
+			 *	foo { ...
+			 *
+			 *	So we compile it like it was:
+			 *
+			 *	group foo { ...
+			 */
+			csingle = do_compile_modgroup(parent,
+						      method,
+						      subcs,
+						      GROUPTYPE_SIMPLE,
+						      grouptype, MOD_GROUP);
+		}
+
+		/*
+		 *	Return the compiled thing if we can.
+		 */
+		if (!csingle) return NULL;
+		if (cf_item_is_pair(ci)) return csingle;
+
+		/*
+		 *	Else we have a reference to a policy, and that reference
+		 *	over-rides the return codes for the policy!
+		 */
+		goto action_override;
+	}
+
+	/*
+	 *	Not a virtual module.  It must be a real module.
+	 */
+	modules = cf_section_find("modules");
+	this = NULL;
+	realname = modrefname;
+
+	if (modules) {
+		/*
+		 *	Try to load the optional module.
+		 */
+		if (realname[0] == '-') realname++;
+
+		/*
+		 *	As of v3, the "modules" section contains
+		 *	modules we use.  Configuration for other
+		 *	modules belongs in raddb/mods-available/,
+		 *	which isn't loaded into the "modules" section.
+		 */
+		this = module_instantiate_method(modules, realname, &method);
+		if (this) goto allocate_csingle;
+
+		/*
+		 *	We were asked to MAYBE load it and it
+		 *	doesn't exist.  Return a soft error.
+		 */
+		if (realname != modrefname) {
+			*modname = modrefname;
+			return NULL;
+		}
+	}
+
+	/*
+	 *	Can't de-reference it to anything.  Ugh.
+	 */
+	*modname = NULL;
+	cf_log_err(ci, "Failed to find \"%s\" as a module or policy.", modrefname);
+	cf_log_err(ci, "Please verify that the configuration exists in %s/mods-enabled/%s.", get_radius_dir(), modrefname);
+	return NULL;
+
+	/*
+	 *	We know it's all OK, allocate the structures, and fill
+	 *	them in.
+	 */
+allocate_csingle:
+	/*
+	 *	Check if the module in question has the necessary
+	 *	component.
+	 */
+	if (!this->entry->module->methods[method]) {
+		cf_log_err(ci, "\"%s\" modules aren't allowed in '%s' sections -- they have no such method.", this->entry->module->name,
+			   comp2str[method]);
+		return NULL;
+	}
+
+	single = talloc_zero(parent, modsingle);
+	single->modinst = this;
+	*modname = this->entry->module->name;
+
+	csingle = mod_singletocallable(single);
+	csingle->parent = parent;
+	csingle->next = NULL;
+	if (!parent || (component != MOD_AUTHENTICATE)) {
+		memcpy(csingle->actions, defaultactions[component][grouptype],
+		       sizeof csingle->actions);
+	} else { /* inside Auth-Type has different rules */
+		memcpy(csingle->actions, authtype_actions[grouptype],
+		       sizeof csingle->actions);
+	}
+	rad_assert(modrefname != NULL);
+	csingle->name = realname;
+	csingle->type = MOD_SINGLE;
+	csingle->method = method;
+
+action_override:
+	/*
+	 *	Over-ride the default return codes of the module.
+	 */
+	if (cf_item_is_section(ci)) {
+		CONF_ITEM *csi;
+
+		cs = cf_item_to_section(ci);
+		for (csi=cf_item_find_next(cs, NULL);
+		     csi != NULL;
+		     csi=cf_item_find_next(cs, csi)) {
+
+			if (cf_item_is_section(csi)) {
+				cf_log_err(csi, "Subsection of module instance call not allowed");
+				talloc_free(csingle);
+				return NULL;
+			}
+
+			if (!cf_item_is_pair(csi)) continue;
+
+			if (!compile_action(csingle, cf_item_to_pair(csi))) {
+				talloc_free(csingle);
+				return NULL;
+			}
+		}
+	}
+
+	return csingle;
+}
+
+modcallable *compile_modsingle(TALLOC_CTX *ctx,
+			       modcallable **parent,
+			       rlm_components_t component, CONF_ITEM *ci,
+			       char const **modname)
+{
+	modcallable *ret;
+
+	if (!*parent) {
+		modcallable *c;
+		modgroup *g;
+		CONF_SECTION *parentcs;
+
+		g = talloc_zero(ctx, modgroup);
+		memset(g, 0, sizeof(*g));
+		g->grouptype = GROUPTYPE_SIMPLE;
+		c = mod_grouptocallable(g);
+		c->next = NULL;
+		memcpy(c->actions,
+		       defaultactions[component][GROUPTYPE_SIMPLE],
+		       sizeof(c->actions));
+
+		parentcs = cf_item_parent(ci);
+		c->name = cf_section_name2(parentcs);
+		if (!c->name) {
+			c->name = cf_section_name1(parentcs);
+		}
+
+		c->type = MOD_GROUP;
+		c->method = component;
+		g->children = NULL;
+
+		*parent = mod_grouptocallable(g);
+	}
+
+	ret = do_compile_modsingle(*parent, component, ci,
+				   GROUPTYPE_SIMPLE,
+				   modname);
+	dump_tree(component, ret);
+	return ret;
+}
+
+
+/*
+ *	Internal compile group code.
+ */
+static modcallable *do_compile_modgroup(modcallable *parent,
+					rlm_components_t component, CONF_SECTION *cs,
+					int grouptype, int parentgrouptype, int mod_type)
+{
+	int i;
+	modgroup *g;
+	modcallable *c;
+	CONF_ITEM *ci;
+
+	g = talloc_zero(parent, modgroup);
+	g->grouptype = grouptype;
+	g->children = NULL;
+	g->cs = cs;
+
+	c = mod_grouptocallable(g);
+	c->parent = parent;
+	c->type = mod_type;
+	c->next = NULL;
+	memset(c->actions, 0, sizeof(c->actions));
+
+	if (!cs) {		/* only for "break" and "return" */
+		c->name = "";
+		goto set_codes;
+	}
+
+	/*
+	 *	Remember the name for printing, etc.
+	 *
+	 *	FIXME: We may also want to put the names into a
+	 *	rbtree, so that groups can reference each other...
+	 */
+	c->name = cf_section_name2(cs);
+	if (!c->name) {
+		c->name = cf_section_name1(cs);
+		if ((strcmp(c->name, "group") == 0) ||
+		    (strcmp(c->name, "redundant") == 0)) {
+			c->name = "";
+		} else if (c->type == MOD_GROUP) {
+			c->type = MOD_POLICY;
+		}
+	}
+
+#ifdef WITH_UNLANG
+	/*
+	 *	Do load-time optimizations
+	 */
+	if ((c->type == MOD_IF) || (c->type == MOD_ELSIF) || (c->type == MOD_ELSE)) {
+		modgroup *f, *p;
+
+		rad_assert(parent != NULL);
+
+		if (c->type == MOD_IF) {
+			g->cond = cf_data_find(g->cs, "if");
+			rad_assert(g->cond != NULL);
+
+		check_if:
+			if (g->cond->type == COND_TYPE_FALSE) {
+				INFO(" # Skipping contents of '%s' as it is always 'false' -- %s:%d",
+				     unlang_keyword[g->mc.type],
+				     cf_section_filename(g->cs), cf_section_lineno(g->cs));
+				goto set_codes;
+			}
+
+		} else if (c->type == MOD_ELSIF) {
+
+			g->cond = cf_data_find(g->cs, "if");
+			rad_assert(g->cond != NULL);
+
+			rad_assert(parent != NULL);
+			p = mod_callabletogroup(parent);
+
+			if (!p->tail) goto elsif_fail;
+
+			/*
+			 *	We're in the process of compiling the
+			 *	section, so the parent's tail is the
+			 *	previous "if" statement.
+			 */
+			f = mod_callabletogroup(p->tail);
+			if ((f->mc.type != MOD_IF) &&
+			    (f->mc.type != MOD_ELSIF)) {
+			elsif_fail:
+				cf_log_err_cs(g->cs, "Invalid location for 'elsif'.  There is no preceding 'if' statement");
+				talloc_free(g);
+				return NULL;
+			}
+
+			/*
+			 *	If we took the previous condition, we
+			 *	don't need to take this one.
+			 *
+			 *	We reset our condition to 'true', so
+			 *	that subsequent sections can check
+			 *	that they don't need to be executed.
+			 */
+			if (f->cond->type == COND_TYPE_TRUE) {
+			skip_true:
+				INFO(" # Skipping contents of '%s' as previous '%s' is always  'true' -- %s:%d",
+				     unlang_keyword[g->mc.type],
+				     unlang_keyword[f->mc.type],
+				     cf_section_filename(g->cs), cf_section_lineno(g->cs));
+				g->cond = f->cond;
+				goto set_codes;
+			}
+			goto check_if;
+
+		} else {
+			rad_assert(c->type == MOD_ELSE);
+
+			rad_assert(parent != NULL);
+			p = mod_callabletogroup(parent);
+
+			if (!p->tail) goto else_fail;
+
+			f = mod_callabletogroup(p->tail);
+			if ((f->mc.type != MOD_IF) &&
+			    (f->mc.type != MOD_ELSIF)) {
+			else_fail:
+				cf_log_err_cs(g->cs, "Invalid location for 'else'.  There is no preceding 'if' statement");
+				talloc_free(g);
+				return NULL;
+			}
+
+			/*
+			 *	If we took the previous condition, we
+			 *	don't need to take this one.
+			 */
+			if (f->cond->type == COND_TYPE_TRUE) goto skip_true;
+		}
+
+		/*
+		 *	Else we need to compile this section
+		 */
+	}
+#endif
+
+	/*
+	 *	Loop over the children of this group.
+	 */
+	for (ci=cf_item_find_next(cs, NULL);
+	     ci != NULL;
+	     ci=cf_item_find_next(cs, ci)) {
+
+		/*
+		 *	Sections are references to other groups, or
+		 *	to modules with updated return codes.
+		 */
+		if (cf_item_is_section(ci)) {
+			char const *junk = NULL;
+			modcallable *single;
+			CONF_SECTION *subcs = cf_item_to_section(ci);
+
+			single = do_compile_modsingle(c, component, ci,
+						      grouptype, &junk);
+			if (!single) {
+				cf_log_err(ci, "Failed to parse \"%s\" subsection.",
+				       cf_section_name1(subcs));
+				talloc_free(c);
+				return NULL;
+			}
+			add_child(g, single);
+
+		} else if (!cf_item_is_pair(ci)) { /* CONF_DATA */
+			continue;
+
+		} else {
+			char const *attr, *value;
+			CONF_PAIR *cp = cf_item_to_pair(ci);
+
+			attr = cf_pair_attr(cp);
+			value = cf_pair_value(cp);
+
+			/*
+			 *	A CONF_PAIR is either a module
+			 *	instance with no actions
+			 *	specified ...
+			 */
+			if (!value) {
+				modcallable *single;
+				char const *junk = NULL;
+
+				single = do_compile_modsingle(c,
+							      component,
+							      ci,
+							      grouptype,
+							      &junk);
+				if (!single) {
+					if (cf_item_is_pair(ci) &&
+					    cf_pair_attr(cf_item_to_pair(ci))[0] == '-') {
+						continue;
+					}
+
+					cf_log_err(ci,
+						   "Failed to parse \"%s\" entry.",
+						   attr);
+					talloc_free(c);
+					return NULL;
+				}
+				add_child(g, single);
+
+				/*
+				 *	Or a module instance with action.
+				 */
+			} else if (!compile_action(c, cp)) {
+				talloc_free(c);
+				return NULL;
+			} /* else it worked */
+		}
+	}
+
+set_codes:
+	/*
+	 *	Set the default actions, if they haven't already been
+	 *	set.
+	 */
+	for (i = 0; i < RLM_MODULE_NUMCODES; i++) {
+		if (!c->actions[i]) {
+			if (!parent || (component != MOD_AUTHENTICATE)) {
+				c->actions[i] = defaultactions[component][parentgrouptype][i];
+			} else { /* inside Auth-Type has different rules */
+				c->actions[i] = authtype_actions[parentgrouptype][i];
+			}
+		}
+	}
+
+	switch (c->type) {
+	default:
+		break;
+
+	case MOD_GROUP:
+		if (grouptype != GROUPTYPE_REDUNDANT) break;
+		/* FALL-THROUGH */
+
+	case MOD_LOAD_BALANCE:
+	case MOD_REDUNDANT_LOAD_BALANCE:
+		if (!g->children) {
+			cf_log_err_cs(g->cs, "%s sections cannot be empty",
+				      cf_section_name1(g->cs));
+			talloc_free(c);
+			return NULL;
+		}
+	}
+
+	/*
+	 *	FIXME: If there are no children, return NULL?
+	 */
+	return mod_grouptocallable(g);
+}
+
+modcallable *compile_modgroup(modcallable *parent,
+			      rlm_components_t component, CONF_SECTION *cs)
+{
+	modcallable *ret = do_compile_modgroup(parent, component, cs,
+					       GROUPTYPE_SIMPLE,
+					       GROUPTYPE_SIMPLE, MOD_GROUP);
+
+	if (rad_debug_lvl > 3) {
+		modcall_debug(ret, 2);
+	}
+
+	return ret;
+}
+
+void add_to_modcallable(modcallable *parent, modcallable *this)
+{
+	modgroup *g;
+
+	rad_assert(this != NULL);
+	rad_assert(parent != NULL);
+
+	g = mod_callabletogroup(parent);
+
+	add_child(g, this);
+}
+
+
+#ifdef WITH_UNLANG
+static bool pass2_xlat_compile(CONF_ITEM const *ci, vp_tmpl_t **pvpt, bool convert,
+			       DICT_ATTR const *da)
+{
+	ssize_t slen;
+	char *fmt;
+	char const *error;
+	xlat_exp_t *head;
+	vp_tmpl_t *vpt;
+
+	vpt = *pvpt;
+
+	rad_assert(vpt->type == TMPL_TYPE_XLAT);
+
+	fmt = talloc_typed_strdup(vpt, vpt->name);
+	slen = xlat_tokenize(vpt, fmt, &head, &error);
+
+	if (slen < 0) {
+		char *spaces, *text;
+
+		fr_canonicalize_error(vpt, &spaces, &text, slen, vpt->name);
+
+		cf_log_err(ci, "Failed parsing expanded string:");
+		cf_log_err(ci, "%s", text);
+		cf_log_err(ci, "%s^ %s", spaces, error);
+
+		talloc_free(spaces);
+		talloc_free(text);
+		return false;
+	}
+
+	/*
+	 *	Convert %{Attribute-Name} to &Attribute-Name
+	 */
+	if (convert) {
+		vp_tmpl_t *attr;
+
+		attr = xlat_to_tmpl_attr(talloc_parent(vpt), head);
+		if (attr) {
+			/*
+			 *	If it's a virtual attribute, leave it
+			 *	alone.
+			 */
+			if (attr->tmpl_da->flags.virtual) {
+				talloc_free(attr);
+				return true;
+			}
+
+			/*
+			 *	If the attribute is of incompatible
+			 *	type, leave it alone.
+			 */
+			if (da && (da->type != attr->tmpl_da->type)) {
+				talloc_free(attr);
+				return true;
+			}
+
+			if (cf_item_is_pair(ci)) {
+				CONF_PAIR *cp = cf_item_to_pair(ci);
+
+				WARN("%s[%d]: Please change \"%%{%s}\" to &%s",
+				       cf_pair_filename(cp), cf_pair_lineno(cp),
+				       attr->name, attr->name);
+			} else {
+				CONF_SECTION *cs = cf_item_to_section(ci);
+
+				WARN("%s[%d]: Please change \"%%{%s}\" to &%s",
+				       cf_section_filename(cs), cf_section_lineno(cs),
+				       attr->name, attr->name);
+			}
+			TALLOC_FREE(*pvpt);
+			*pvpt = attr;
+			return true;
+		}
+	}
+
+	/*
+	 *	Re-write it to be a pre-parsed XLAT structure.
+	 */
+	vpt->type = TMPL_TYPE_XLAT_STRUCT;
+	vpt->tmpl_xlat = head;
+
+	return true;
+}
+
+
+#ifdef HAVE_REGEX
+static bool pass2_regex_compile(CONF_ITEM const *ci, vp_tmpl_t *vpt)
+{
+	ssize_t slen;
+	regex_t *preg;
+
+	rad_assert(vpt->type == TMPL_TYPE_REGEX);
+
+	/*
+	 *	It's a dynamic expansion.  We can't expand the string,
+	 *	but we can pre-parse it as an xlat struct.  In that
+	 *	case, we convert it to a pre-compiled XLAT.
+	 *
+	 *	This is a little more complicated than it needs to be
+	 *	because radius_evaluate_map() keys off of the src
+	 *	template type, instead of the operators.  And, the
+	 *	pass2_xlat_compile() function expects to get passed an
+	 *	XLAT instead of a REGEX.
+	 */
+	if (strchr(vpt->name, '%')) {
+		vpt->type = TMPL_TYPE_XLAT;
+		return pass2_xlat_compile(ci, &vpt, false, NULL);
+	}
+
+	slen = regex_compile(vpt, &preg, vpt->name, vpt->len,
+			     vpt->tmpl_iflag, vpt->tmpl_mflag, true, false);
+	if (slen <= 0) {
+		char *spaces, *text;
+
+		fr_canonicalize_error(vpt, &spaces, &text, slen, vpt->name);
+
+		cf_log_err(ci, "Invalid regular expression:");
+		cf_log_err(ci, "%s", text);
+		cf_log_err(ci, "%s^ %s", spaces, fr_strerror());
+
+		talloc_free(spaces);
+		talloc_free(text);
+
+		return false;
+	}
+
+	vpt->type = TMPL_TYPE_REGEX_STRUCT;
+	vpt->tmpl_preg = preg;
+
+	return true;
+}
+#endif
+
+static bool pass2_fixup_undefined(CONF_ITEM const *ci, vp_tmpl_t *vpt)
+{
+	DICT_ATTR const *da;
+
+	rad_assert(vpt->type == TMPL_TYPE_ATTR_UNDEFINED);
+
+	da = dict_attrbyname(vpt->tmpl_unknown_name);
+	if (!da) {
+		cf_log_err(ci, "Unknown attribute '%s'", vpt->tmpl_unknown_name);
+		return false;
+	}
+
+	vpt->tmpl_da = da;
+	vpt->type = TMPL_TYPE_ATTR;
+	return true;
+}
+
+static bool pass2_callback(void *ctx, fr_cond_t *c)
+{
+	vp_map_t *map;
+	vp_tmpl_t *vpt;
+
+	/*
+	 *	These don't get optimized.
+	 */
+	if ((c->type == COND_TYPE_TRUE) ||
+	    (c->type == COND_TYPE_FALSE)) {
+		return true;
+	}
+
+	/*
+	 *	Call children.
+	 */
+	if (c->type == COND_TYPE_CHILD) return pass2_callback(ctx, c->data.child);
+
+	/*
+	 *	A few simple checks here.
+	 */
+	if (c->type == COND_TYPE_EXISTS) {
+		if (c->data.vpt->type == TMPL_TYPE_XLAT) {
+			return pass2_xlat_compile(c->ci, &c->data.vpt, true, NULL);
+		}
+
+		rad_assert(c->data.vpt->type != TMPL_TYPE_REGEX);
+
+		/*
+		 *	The existence check might have been &Foo-Bar,
+		 *	where Foo-Bar is defined by a module.
+		 */
+		if (c->pass2_fixup == PASS2_FIXUP_ATTR) {
+			if (!pass2_fixup_undefined(c->ci, c->data.vpt)) return false;
+			c->pass2_fixup = PASS2_FIXUP_NONE;
+		}
+
+		/*
+		 *	Convert virtual &Attr-Foo to "%{Attr-Foo}"
+		 */
+		vpt = c->data.vpt;
+		if ((vpt->type == TMPL_TYPE_ATTR) && vpt->tmpl_da->flags.virtual) {
+			vpt->tmpl_xlat = xlat_from_tmpl_attr(vpt, vpt);
+			vpt->type = TMPL_TYPE_XLAT_STRUCT;
+		}
+
+		return true;
+	}
+
+	/*
+	 *	And tons of complicated checks.
+	 */
+	rad_assert(c->type == COND_TYPE_MAP);
+
+	map = c->data.map;	/* shorter */
+
+	/*
+	 *	Auth-Type := foo
+	 *
+	 *	Where "foo" is dynamically defined.
+	 */
+	if (c->pass2_fixup == PASS2_FIXUP_TYPE) {
+		if (!dict_valbyname(map->lhs->tmpl_da->attr,
+				    map->lhs->tmpl_da->vendor,
+				    map->rhs->name)) {
+			cf_log_err(map->ci, "Invalid reference to non-existent %s %s { ... }",
+				   map->lhs->tmpl_da->name,
+				   map->rhs->name);
+			return false;
+		}
+
+		/*
+		 *	These guys can't have a paircompare fixup applied.
+		 */
+		c->pass2_fixup = PASS2_FIXUP_NONE;
+		return true;
+	}
+
+	if (c->pass2_fixup == PASS2_FIXUP_ATTR) {
+		if (map->lhs->type == TMPL_TYPE_ATTR_UNDEFINED) {
+			if (!pass2_fixup_undefined(map->ci, map->lhs)) return false;
+		}
+
+		if (map->rhs->type == TMPL_TYPE_ATTR_UNDEFINED) {
+			if (!pass2_fixup_undefined(map->ci, map->rhs)) return false;
+		}
+
+		c->pass2_fixup = PASS2_FIXUP_NONE;
+	}
+
+	/*
+	 *	Just in case someone adds a new fixup later.
+	 */
+	rad_assert((c->pass2_fixup == PASS2_FIXUP_NONE) ||
+		   (c->pass2_fixup == PASS2_PAIRCOMPARE));
+
+	/*
+	 *	Precompile xlat's
+	 */
+	if (map->lhs->type == TMPL_TYPE_XLAT) {
+		/*
+		 *	Compile the LHS to an attribute reference only
+		 *	if the RHS is a literal.
+		 *
+		 *	@todo v3.1: allow anything anywhere.
+		 */
+		if (map->rhs->type != TMPL_TYPE_LITERAL) {
+			if (!pass2_xlat_compile(map->ci, &map->lhs, false, NULL)) {
+				return false;
+			}
+		} else {
+			if (!pass2_xlat_compile(map->ci, &map->lhs, true, NULL)) {
+				return false;
+			}
+
+			/*
+			 *	Attribute compared to a literal gets
+			 *	the literal cast to the data type of
+			 *	the attribute.
+			 *
+			 *	The code in parser.c did this for
+			 *
+			 *		&Attr == data
+			 *
+			 *	But now we've just converted "%{Attr}"
+			 *	to &Attr, so we've got to do it again.
+			 */
+			if ((map->lhs->type == TMPL_TYPE_ATTR) &&
+			    (map->rhs->type == TMPL_TYPE_LITERAL)) {
+				/*
+				 *	RHS is hex, try to parse it as
+				 *	type-specific data.
+				 */
+				if (map->lhs->auto_converted &&
+				    (map->rhs->name[0] == '0') && (map->rhs->name[1] == 'x') &&
+				    (map->rhs->len > 2) && ((map->rhs->len & 0x01) == 0)) {
+					vpt = map->rhs;
+					map->rhs = NULL;
+
+					if (!map_cast_from_hex(map, T_BARE_WORD, vpt->name)) {
+						map->rhs = vpt;
+						cf_log_err(map->ci, "Cannot parse RHS hex as the data type of the attribute %s", map->lhs->tmpl_da->name);
+						return -1;
+					}
+					talloc_free(vpt);
+
+				} else if ((map->rhs->len > 0) ||
+					   (map->op != T_OP_CMP_EQ) ||
+					   (map->lhs->tmpl_da->type == PW_TYPE_STRING) ||
+					   (map->lhs->tmpl_da->type == PW_TYPE_OCTETS)) {
+
+					if (tmpl_cast_in_place(map->rhs, map->lhs->tmpl_da->type, map->lhs->tmpl_da) < 0) {
+						cf_log_err(map->ci, "Failed to parse data type %s from string: %s",
+							   fr_int2str(dict_attr_types, map->lhs->tmpl_da->type, "<UNKNOWN>"),
+							   map->rhs->name);
+						return false;
+					} /* else the cast was successful */
+
+				} else {	/* RHS is empty, it's just a check for empty / non-empty string */
+					vpt = talloc_steal(c, map->lhs);
+					map->lhs = NULL;
+					talloc_free(c->data.map);
+
+					/*
+					 *	"%{Foo}" == '' ---> !Foo
+					 *	"%{Foo}" != '' ---> Foo
+					 */
+					c->type = COND_TYPE_EXISTS;
+					c->data.vpt = vpt;
+					c->negate = !c->negate;
+
+					WARN("%s[%d]: Please change (\"%%{%s}\" %s '') to %c&%s",
+					     cf_section_filename(cf_item_to_section(c->ci)),
+					     cf_section_lineno(cf_item_to_section(c->ci)),
+					     vpt->name, c->negate ? "==" : "!=",
+					     c->negate ? '!' : ' ', vpt->name);
+
+					/*
+					 *	No more RHS, so we can't do more optimizations
+					 */
+					return true;
+				}
+			}
+		}
+	}
+
+	if (map->rhs->type == TMPL_TYPE_XLAT) {
+		/*
+		 *	Convert the RHS to an attribute reference only
+		 *	if the LHS is an attribute reference, AND is
+		 *	of the same type as the RHS.
+		 *
+		 *	We can fix this when the code in evaluate.c
+		 *	can handle strings on the LHS, and attributes
+		 *	on the RHS.  For now, the code in parser.c
+		 *	forbids this.
+		 */
+		if (map->lhs->type == TMPL_TYPE_ATTR) {
+			DICT_ATTR const *da = c->cast;
+
+			if (!c->cast) da = map->lhs->tmpl_da;
+
+			if (!pass2_xlat_compile(map->ci, &map->rhs, true, da)) {
+				return false;
+			}
+
+		} else {
+			if (!pass2_xlat_compile(map->ci, &map->rhs, false, NULL)) {
+				return false;
+			}
+		}
+	}
+
+	/*
+	 *	Convert bare refs to %{Foreach-Variable-N}
+	 */
+	if ((map->lhs->type == TMPL_TYPE_LITERAL) &&
+	    (strncmp(map->lhs->name, "Foreach-Variable-", 17) == 0)) {
+		char *fmt;
+		ssize_t slen;
+
+		fmt = talloc_asprintf(map->lhs, "%%{%s}", map->lhs->name);
+		slen = tmpl_afrom_str(map, &vpt, fmt, talloc_array_length(fmt) - 1,
+				      T_DOUBLE_QUOTED_STRING, REQUEST_CURRENT, PAIR_LIST_REQUEST, true);
+		if (slen < 0) {
+			char *spaces, *text;
+
+			fr_canonicalize_error(map->ci, &spaces, &text, slen, fr_strerror());
+
+			cf_log_err(map->ci, "Failed converting %s to xlat", map->lhs->name);
+			cf_log_err(map->ci, "%s", fmt);
+			cf_log_err(map->ci, "%s^ %s", spaces, text);
+
+			talloc_free(spaces);
+			talloc_free(text);
+			talloc_free(fmt);
+
+			return false;
+		}
+		talloc_free(map->lhs);
+		map->lhs = vpt;
+	}
+
+#ifdef HAVE_REGEX
+	if (map->rhs->type == TMPL_TYPE_REGEX) {
+		if (!pass2_regex_compile(map->ci, map->rhs)) {
+			return false;
+		}
+	}
+	rad_assert(map->lhs->type != TMPL_TYPE_REGEX);
+#endif
+
+	/*
+	 *	Convert &Packet-Type to "%{Packet-Type}", because
+	 *	these attributes don't really exist.  The code to
+	 *	find an attribute reference doesn't work, but the
+	 *	xlat code does.
+	 */
+	vpt = c->data.map->lhs;
+	if ((vpt->type == TMPL_TYPE_ATTR) && vpt->tmpl_da->flags.virtual) {
+		if (!c->cast) c->cast = vpt->tmpl_da;
+		vpt->tmpl_xlat = xlat_from_tmpl_attr(vpt, vpt);
+		vpt->type = TMPL_TYPE_XLAT_STRUCT;
+	}
+
+	/*
+	 *	Convert RHS to expansions, too.
+	 */
+	vpt = c->data.map->rhs;
+	if ((vpt->type == TMPL_TYPE_ATTR) && vpt->tmpl_da->flags.virtual) {
+		vpt->tmpl_xlat = xlat_from_tmpl_attr(vpt, vpt);
+		vpt->type = TMPL_TYPE_XLAT_STRUCT;
+	}
+
+	/*
+	 *	@todo v3.1: do the same thing for the RHS...
+	 */
+
+	/*
+	 *	Only attributes can have a paircompare registered, and
+	 *	they can only be with the current REQUEST, and only
+	 *	with the request pairs.
+	 */
+	if ((map->lhs->type != TMPL_TYPE_ATTR) ||
+	    (map->lhs->tmpl_request != REQUEST_CURRENT) ||
+	    (map->lhs->tmpl_list != PAIR_LIST_REQUEST)) {
+		return true;
+	}
+
+	if (!radius_find_compare(map->lhs->tmpl_da)) return true;
+
+	if (map->rhs->type == TMPL_TYPE_REGEX) {
+		cf_log_err(map->ci, "Cannot compare virtual attribute %s via a regex",
+			   map->lhs->name);
+		return false;
+	}
+
+	if (c->cast) {
+		cf_log_err(map->ci, "Cannot cast virtual attribute %s",
+			   map->lhs->name);
+		return false;
+	}
+
+	if (map->op != T_OP_CMP_EQ) {
+		cf_log_err(map->ci, "Must use '==' for comparisons with virtual attribute %s",
+			   map->lhs->name);
+		return false;
+	}
+
+	/*
+	 *	Mark it as requiring a paircompare() call, instead of
+	 *	fr_pair_cmp().
+	 */
+	c->pass2_fixup = PASS2_PAIRCOMPARE;
+
+	return true;
+}
+
+
+/*
+ *	Compile the RHS of update sections to xlat_exp_t
+ */
+static bool modcall_pass2_update(modgroup *g)
+{
+	vp_map_t *map;
+
+	for (map = g->map; map != NULL; map = map->next) {
+		if (map->rhs->type == TMPL_TYPE_XLAT) {
+			rad_assert(map->rhs->tmpl_xlat == NULL);
+
+			/*
+			 *	FIXME: compile to attribute && handle
+			 *	the conversion in map_to_vp().
+			 */
+			if (!pass2_xlat_compile(map->ci, &map->rhs, false, NULL)) {
+				return false;
+			}
+		}
+
+		rad_assert(map->rhs->type != TMPL_TYPE_REGEX);
+
+		/*
+		 *	Deal with undefined attributes now.
+		 */
+		if (map->lhs->type == TMPL_TYPE_ATTR_UNDEFINED) {
+			if (!pass2_fixup_undefined(map->ci, map->lhs)) return false;
+		}
+
+		if (map->rhs->type == TMPL_TYPE_ATTR_UNDEFINED) {
+			if (!pass2_fixup_undefined(map->ci, map->rhs)) return false;
+		}
+	}
+
+	return true;
+}
+#endif
+
+/*
+ *	Do a second-stage pass on compiling the modules.
+ */
+bool modcall_pass2(modcallable *mc)
+{
+	ssize_t slen;
+	char const *name2;
+	modcallable *c;
+	modgroup *g;
+
+	for (c = mc; c != NULL; c = c->next) {
+		switch (c->type) {
+		default:
+			rad_assert(0 == 1);
+			break;
+
+#ifdef WITH_UNLANG
+		case MOD_UPDATE:
+			g = mod_callabletogroup(c);
+			if (g->done_pass2) goto do_next;
+
+			name2 = cf_section_name2(g->cs);
+			if (!name2) {
+				c->debug_name = unlang_keyword[c->type];
+			} else {
+				c->debug_name = talloc_asprintf(c, "update %s", name2);
+			}
+
+			if (!modcall_pass2_update(g)) {
+				return false;
+			}
+			g->done_pass2 = true;
+			break;
+
+		case MOD_XLAT:   /* @todo: pre-parse xlat's */
+		case MOD_REFERENCE:
+		case MOD_BREAK:
+		case MOD_RETURN:
+#endif
+
+		case MOD_SINGLE:
+			c->debug_name = c->name;
+			break;	/* do nothing */
+
+#ifdef WITH_UNLANG
+		case MOD_IF:
+		case MOD_ELSIF:
+			g = mod_callabletogroup(c);
+			if (g->done_pass2) goto do_next;
+
+			name2 = cf_section_name2(g->cs);
+			c->debug_name = talloc_asprintf(c, "%s %s", unlang_keyword[c->type], name2);
+
+			/*
+			 *	The compilation code takes care of
+			 *	simplifying 'true' and 'false'
+			 *	conditions.  For others, we have to do
+			 *	a second pass to parse && compile
+			 *	xlats.
+			 */
+			if (!((g->cond->type == COND_TYPE_TRUE) ||
+			      (g->cond->type == COND_TYPE_FALSE))) {
+				if (!fr_condition_walk(g->cond, pass2_callback, NULL)) {
+					return false;
+				}
+			}
+
+			if (!modcall_pass2(g->children)) return false;
+			g->done_pass2 = true;
+			break;
+#endif
+
+#ifdef WITH_UNLANG
+		case MOD_SWITCH:
+			g = mod_callabletogroup(c);
+			if (g->done_pass2) goto do_next;
+
+			name2 = cf_section_name2(g->cs);
+			c->debug_name = talloc_asprintf(c, "%s %s", unlang_keyword[c->type], name2);
+
+			/*
+			 *	We had &Foo-Bar, where Foo-Bar is
+			 *	defined by a module.
+			 */
+			if (!g->vpt) {
+				rad_assert(c->name != NULL);
+				rad_assert(c->name[0] == '&');
+				rad_assert(cf_section_name2_type(g->cs) == T_BARE_WORD);
+
+				slen = tmpl_afrom_str(g->cs, &g->vpt, c->name, strlen(c->name),
+						      cf_section_name2_type(g->cs),
+						      REQUEST_CURRENT, PAIR_LIST_REQUEST, true);
+				if (slen < 0) {
+					char *spaces, *text;
+
+				parse_error:
+					fr_canonicalize_error(g->cs, &spaces, &text, slen, fr_strerror());
+
+					cf_log_err_cs(g->cs, "Syntax error");
+					cf_log_err_cs(g->cs, "%s", c->name);
+					cf_log_err_cs(g->cs, "%s^ %s", spaces, text);
+
+					talloc_free(spaces);
+					talloc_free(text);
+
+					return false;
+				}
+
+				goto do_children;
+			}
+
+			/*
+			 *	Statically compile xlats
+			 */
+			if (g->vpt->type == TMPL_TYPE_XLAT) {
+				if (!pass2_xlat_compile(cf_section_to_item(g->cs),
+							&g->vpt, true, NULL)) {
+					return false;
+				}
+
+				goto do_children;
+			}
+
+			/*
+			 *	Convert virtual &Attr-Foo to "%{Attr-Foo}"
+			 */
+			if ((g->vpt->type == TMPL_TYPE_ATTR) && g->vpt->tmpl_da->flags.virtual) {
+				g->vpt->tmpl_xlat = xlat_from_tmpl_attr(g->vpt, g->vpt);
+				g->vpt->type = TMPL_TYPE_XLAT_STRUCT;
+			}
+
+			/*
+			 *	We may have: switch Foo-Bar {
+			 *
+			 *	where Foo-Bar is an attribute defined
+			 *	by a module.  Since there's no leading
+			 *	&, it's parsed as a literal.  But if
+			 *	we can parse it as an attribute,
+			 *	switch to using that.
+			 */
+			if (g->vpt->type == TMPL_TYPE_LITERAL) {
+				vp_tmpl_t *vpt;
+
+				slen = tmpl_afrom_str(g->cs, &vpt, c->name, strlen(c->name), cf_section_name2_type(g->cs),
+						      REQUEST_CURRENT, PAIR_LIST_REQUEST, true);
+				if (slen < 0) goto parse_error;
+				if (vpt->type == TMPL_TYPE_ATTR) {
+					talloc_free(g->vpt);
+					g->vpt = vpt;
+				}
+
+				goto do_children;
+			}
+
+			/*
+			 *	Warn about old-style configuration.
+			 *
+			 *	DEPRECATED: switch User-Name { ...
+			 *	ALLOWED   : switch &User-Name { ...
+			 */
+			if ((g->vpt->type == TMPL_TYPE_ATTR) &&
+			    (c->name[0] != '&')) {
+				WARN("%s[%d]: Please change %s to &%s",
+				     cf_section_filename(g->cs),
+				     cf_section_lineno(g->cs),
+				     c->name, c->name);
+			}
+
+		do_children:
+			if (!modcall_pass2(g->children)) return false;
+			g->done_pass2 = true;
+			break;
+
+		case MOD_CASE:
+			g = mod_callabletogroup(c);
+			if (g->done_pass2) goto do_next;
+
+			name2 = cf_section_name2(g->cs);
+			if (!name2) {
+				c->debug_name = unlang_keyword[c->type];
+			} else {
+				c->debug_name = talloc_asprintf(c, "%s %s", unlang_keyword[c->type], name2);
+			}
+
+			rad_assert(c->parent != NULL);
+			rad_assert(c->parent->type == MOD_SWITCH);
+
+			/*
+			 *	The statement may refer to an
+			 *	attribute which doesn't exist until
+			 *	all of the modules have been loaded.
+			 *	Check for that now.
+			 */
+			if (!g->vpt && c->name &&
+			    (c->name[0] == '&') &&
+			    (cf_section_name2_type(g->cs) == T_BARE_WORD)) {
+				slen = tmpl_afrom_str(g->cs, &g->vpt, c->name, strlen(c->name),
+						      cf_section_name2_type(g->cs),
+						      REQUEST_CURRENT, PAIR_LIST_REQUEST, true);
+				if (slen < 0) goto parse_error;
+			}
+
+			/*
+			 *	We have "case {...}".  There's no
+			 *	argument, so we don't need to check
+			 *	it.
+			 */
+			if (!g->vpt) goto do_children;
+
+			/*
+			 *	Do type-specific checks on the case statement
+			 */
+			if (g->vpt->type == TMPL_TYPE_LITERAL) {
+				modgroup *f;
+
+				f = mod_callabletogroup(mc->parent);
+				rad_assert(f->vpt != NULL);
+
+				/*
+				 *	We're switching over an
+				 *	attribute.  Check that the
+				 *	values match.
+				 */
+				if (f->vpt->type == TMPL_TYPE_ATTR) {
+					rad_assert(f->vpt->tmpl_da != NULL);
+
+					if (tmpl_cast_in_place(g->vpt, f->vpt->tmpl_da->type, f->vpt->tmpl_da) < 0) {
+						cf_log_err_cs(g->cs, "Invalid argument for case statement: %s",
+							      fr_strerror());
+						return false;
+					}
+				}
+
+				goto do_children;
+			}
+
+			if (g->vpt->type == TMPL_TYPE_ATTR_UNDEFINED) {
+				if (!pass2_fixup_undefined(cf_section_to_item(g->cs), g->vpt)) {
+					return false;
+				}
+			}
+
+			/*
+			 *	Compile and sanity check xlat
+			 *	expansions.
+			 */
+			if (g->vpt->type == TMPL_TYPE_XLAT) {
+				modgroup *f;
+
+				f = mod_callabletogroup(mc->parent);
+				rad_assert(f->vpt != NULL);
+
+				/*
+				 *	Don't expand xlat's into an
+				 *	attribute of a different type.
+				 */
+				if (f->vpt->type == TMPL_TYPE_ATTR) {
+					if (!pass2_xlat_compile(cf_section_to_item(g->cs),
+								&g->vpt, true, f->vpt->tmpl_da)) {
+						return false;
+					}
+				} else {
+					if (!pass2_xlat_compile(cf_section_to_item(g->cs),
+								&g->vpt, true, NULL)) {
+						return false;
+					}
+				}
+			}
+
+			/*
+			 *	Virtual attribute fixes for "case" statements, too.
+			 */
+			if ((g->vpt->type == TMPL_TYPE_ATTR) && g->vpt->tmpl_da->flags.virtual) {
+				g->vpt->tmpl_xlat = xlat_from_tmpl_attr(g->vpt, g->vpt);
+				g->vpt->type = TMPL_TYPE_XLAT_STRUCT;
+			}
+
+			if (!modcall_pass2(g->children)) return false;
+			g->done_pass2 = true;
+			break;
+
+		case MOD_FOREACH:
+			g = mod_callabletogroup(c);
+			if (g->done_pass2) goto do_next;
+
+			name2 = cf_section_name2(g->cs);
+			c->debug_name = talloc_asprintf(c, "%s %s", unlang_keyword[c->type], name2);
+
+			/*
+			 *	Already parsed, handle the children.
+			 */
+			if (g->vpt) goto check_children;
+
+			/*
+			 *	We had &Foo-Bar, where Foo-Bar is
+			 *	defined by a module.
+			 */
+			rad_assert(c->name != NULL);
+			rad_assert(c->name[0] == '&');
+			rad_assert(cf_section_name2_type(g->cs) == T_BARE_WORD);
+
+			/*
+			 *	The statement may refer to an
+			 *	attribute which doesn't exist until
+			 *	all of the modules have been loaded.
+			 *	Check for that now.
+			 */
+			slen = tmpl_afrom_str(g->cs, &g->vpt, c->name, strlen(c->name), cf_section_name2_type(g->cs),
+					      REQUEST_CURRENT, PAIR_LIST_REQUEST, true);
+			if (slen < 0) goto parse_error;
+
+		check_children:
+			rad_assert((g->vpt->type == TMPL_TYPE_ATTR) || (g->vpt->type == TMPL_TYPE_LIST));
+			if (g->vpt->tmpl_num != NUM_ALL) {
+				cf_log_err_cs(g->cs, "MUST NOT use instance selectors in 'foreach'");
+				return false;
+			}
+			if (!modcall_pass2(g->children)) return false;
+			g->done_pass2 = true;
+			break;
+
+		case MOD_ELSE:
+			c->debug_name = unlang_keyword[c->type];
+			goto do_recurse;
+
+		case MOD_POLICY:
+			g = mod_callabletogroup(c);
+			c->debug_name = talloc_asprintf(c, "%s %s", unlang_keyword[c->type], cf_section_name1(g->cs));
+			goto do_recurse;
+#endif
+
+		case MOD_GROUP:
+		case MOD_LOAD_BALANCE:
+		case MOD_REDUNDANT_LOAD_BALANCE:
+			c->debug_name = unlang_keyword[c->type];
+
+#ifdef WITH_UNLANG
+		do_recurse:
+#endif
+			g = mod_callabletogroup(c);
+			if (!g->cs) {
+				c->debug_name = mc->name; /* for authorize, etc. */
+
+			} else if (c->type == MOD_GROUP) { /* for Auth-Type, etc. */
+				char const *name1 = cf_section_name1(g->cs);
+
+				if (strcmp(name1, unlang_keyword[c->type]) != 0) {
+					name2 = cf_section_name2(g->cs);
+
+					if (!name2) {
+						c->debug_name = name1;
+					} else {
+						c->debug_name = talloc_asprintf(c, "%s %s", name1, name2);
+					}
+				}
+			}
+
+			if (g->done_pass2) goto do_next;
+			if (!modcall_pass2(g->children)) return false;
+			g->done_pass2 = true;
+			break;
+		}
+
+	do_next:
+		rad_assert(c->debug_name != NULL);
+	}
+
+	return true;
+}
+
+void modcall_debug(modcallable *mc, int depth)
+{
+	modcallable *this;
+	modgroup *g;
+	vp_map_t *map;
+	char buffer[1024];
+
+	for (this = mc; this != NULL; this = this->next) {
+		switch (this->type) {
+		default:
+			break;
+
+		case MOD_SINGLE: {
+			modsingle *single = mod_callabletosingle(this);
+
+			DEBUG("%.*s%s", depth, modcall_spaces,
+				single->modinst->name);
+			}
+			break;
+
+#ifdef WITH_UNLANG
+		case MOD_UPDATE:
+			g = mod_callabletogroup(this);
+			DEBUG("%.*s%s {", depth, modcall_spaces,
+				unlang_keyword[this->type]);
+
+			for (map = g->map; map != NULL; map = map->next) {
+				map_prints(buffer, sizeof(buffer), map);
+				DEBUG("%.*s%s", depth + 1, modcall_spaces, buffer);
+			}
+
+			DEBUG("%.*s}", depth, modcall_spaces);
+			break;
+
+		case MOD_ELSE:
+			g = mod_callabletogroup(this);
+			DEBUG("%.*s%s {", depth, modcall_spaces,
+				unlang_keyword[this->type]);
+			modcall_debug(g->children, depth + 1);
+			DEBUG("%.*s}", depth, modcall_spaces);
+			break;
+
+		case MOD_IF:
+		case MOD_ELSIF:
+			g = mod_callabletogroup(this);
+			fr_cond_sprint(buffer, sizeof(buffer), g->cond);
+			DEBUG("%.*s%s (%s) {", depth, modcall_spaces,
+				unlang_keyword[this->type], buffer);
+			modcall_debug(g->children, depth + 1);
+			DEBUG("%.*s}", depth, modcall_spaces);
+			break;
+
+		case MOD_SWITCH:
+		case MOD_CASE:
+			g = mod_callabletogroup(this);
+			tmpl_prints(buffer, sizeof(buffer), g->vpt, NULL);
+			DEBUG("%.*s%s %s {", depth, modcall_spaces,
+				unlang_keyword[this->type], buffer);
+			modcall_debug(g->children, depth + 1);
+			DEBUG("%.*s}", depth, modcall_spaces);
+			break;
+
+		case MOD_POLICY:
+		case MOD_FOREACH:
+			g = mod_callabletogroup(this);
+			DEBUG("%.*s%s %s {", depth, modcall_spaces,
+				unlang_keyword[this->type], this->name);
+			modcall_debug(g->children, depth + 1);
+			DEBUG("%.*s}", depth, modcall_spaces);
+			break;
+
+		case MOD_BREAK:
+			DEBUG("%.*sbreak", depth, modcall_spaces);
+			break;
+
+#endif
+		case MOD_GROUP:
+			g = mod_callabletogroup(this);
+			DEBUG("%.*s%s {", depth, modcall_spaces,
+			      unlang_keyword[this->type]);
+			modcall_debug(g->children, depth + 1);
+			DEBUG("%.*s}", depth, modcall_spaces);
+			break;
+
+
+		case MOD_LOAD_BALANCE:
+		case MOD_REDUNDANT_LOAD_BALANCE:
+			g = mod_callabletogroup(this);
+			DEBUG("%.*s%s {", depth, modcall_spaces,
+				unlang_keyword[this->type]);
+			modcall_debug(g->children, depth + 1);
+			DEBUG("%.*s}", depth, modcall_spaces);
+			break;
+		}
+	}
+}
+
+int modcall_pass2_condition(fr_cond_t *c)
+{
+	if (!fr_condition_walk(c, pass2_callback, NULL)) return -1;
+
+	return 0;
+}
diff --git a/src/main/modules.c b/src/main/modules.c
new file mode 100644
index 0000000..fd4334d
--- /dev/null
+++ b/src/main/modules.c
@@ -0,0 +1,2302 @@
+/*
+ * modules.c	Radius module support.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2003,2006  The FreeRADIUS server project
+ * Copyright 2000  Alan DeKok <aland@ox.org>
+ * Copyright 2000  Alan Curry <pacman@world.std.com>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modpriv.h>
+#include <freeradius-devel/modcall.h>
+#include <freeradius-devel/parser.h>
+#include <freeradius-devel/rad_assert.h>
+
+/** Path to search for modules in
+ *
+ */
+char const *radlib_dir = NULL;
+
+typedef struct indexed_modcallable {
+	rlm_components_t	comp;
+	int			idx;
+	modcallable		*modulelist;
+} indexed_modcallable;
+
+typedef struct virtual_server_t {
+	char const	*name;
+	time_t		created;
+	int		can_free;
+	CONF_SECTION	*cs;
+	rbtree_t	*components;
+	modcallable	*mc[MOD_COUNT];
+	CONF_SECTION	*subcs[MOD_COUNT];
+	struct virtual_server_t *next;
+} virtual_server_t;
+
+/*
+ *	Keep a hash of virtual servers, so that we can reload them.
+ */
+#define VIRTUAL_SERVER_HASH_SIZE (256)
+static virtual_server_t *virtual_servers[VIRTUAL_SERVER_HASH_SIZE];
+
+static rbtree_t *module_tree = NULL;
+
+static rbtree_t *instance_tree = NULL;
+
+struct fr_module_hup_t {
+	module_instance_t	*mi;
+	time_t			when;
+	void			*insthandle;
+	fr_module_hup_t		*next;
+};
+
+/*
+ *	Ordered by component
+ */
+const section_type_value_t section_type_value[MOD_COUNT] = {
+	{ "authenticate", "Auth-Type",       PW_AUTH_TYPE },
+	{ "authorize",    "Autz-Type",       PW_AUTZ_TYPE },
+	{ "preacct",      "Pre-Acct-Type",   PW_PRE_ACCT_TYPE },
+	{ "accounting",   "Acct-Type",       PW_ACCT_TYPE },
+	{ "session",      "Session-Type",    PW_SESSION_TYPE },
+	{ "pre-proxy",    "Pre-Proxy-Type",  PW_PRE_PROXY_TYPE },
+	{ "post-proxy",   "Post-Proxy-Type", PW_POST_PROXY_TYPE },
+	{ "post-auth",    "Post-Auth-Type",  PW_POST_AUTH_TYPE }
+#ifdef WITH_COA
+	,
+	{ "recv-coa",     "Recv-CoA-Type",   PW_RECV_COA_TYPE },
+	{ "send-coa",     "Send-CoA-Type",   PW_SEND_COA_TYPE }
+#endif
+};
+
+#ifndef RTLD_NOW
+#define RTLD_NOW (0)
+#endif
+#ifndef RTLD_LOCAL
+#define RTLD_LOCAL (0)
+#endif
+
+/** Check if the magic number in the module matches the one in the library
+ *
+ * This is used to detect potential ABI issues caused by running with modules which
+ * were built for a different version of the server.
+ *
+ * @param cs being parsed.
+ * @param module being loaded.
+ * @returns 0 on success, -1 if prefix mismatch, -2 if version mismatch, -3 if commit mismatch.
+ */
+static int check_module_magic(CONF_SECTION *cs, module_t const *module)
+{
+#ifdef HAVE_DLADDR
+	Dl_info dl_info;
+	dladdr(module, &dl_info);
+#endif
+
+	if (MAGIC_PREFIX(module->magic) != MAGIC_PREFIX(RADIUSD_MAGIC_NUMBER)) {
+#ifdef HAVE_DLADDR
+		cf_log_err_cs(cs, "Failed loading module rlm_%s from file %s", module->name, dl_info.dli_fname);
+#endif
+		cf_log_err_cs(cs, "Application and rlm_%s magic number (prefix) mismatch."
+			      "  application: %x module: %x", module->name,
+			      MAGIC_PREFIX(RADIUSD_MAGIC_NUMBER),
+			      MAGIC_PREFIX(module->magic));
+		return -1;
+	}
+
+	if (MAGIC_VERSION(module->magic) != MAGIC_VERSION(RADIUSD_MAGIC_NUMBER)) {
+#ifdef HAVE_DLADDR
+		cf_log_err_cs(cs, "Failed loading module rlm_%s from file %s", module->name, dl_info.dli_fname);
+#endif
+		cf_log_err_cs(cs, "Application and rlm_%s magic number (version) mismatch."
+			      "  application: %lx module: %lx", module->name,
+			      (unsigned long) MAGIC_VERSION(RADIUSD_MAGIC_NUMBER),
+			      (unsigned long) MAGIC_VERSION(module->magic));
+		return -2;
+	}
+
+	if (MAGIC_COMMIT(module->magic) != MAGIC_COMMIT(RADIUSD_MAGIC_NUMBER)) {
+#ifdef HAVE_DLADDR
+		cf_log_err_cs(cs, "Failed loading module rlm_%s from file %s", module->name, dl_info.dli_fname);
+#endif
+		cf_log_err_cs(cs, "Application and rlm_%s magic number (commit) mismatch."
+			      "  application: %lx module: %lx", module->name,
+			      (unsigned long) MAGIC_COMMIT(RADIUSD_MAGIC_NUMBER),
+			      (unsigned long) MAGIC_COMMIT(module->magic));
+		return -3;
+	}
+
+	return 0;
+}
+
+fr_dlhandle fr_dlopenext(char const *name)
+{
+	int		flags = RTLD_NOW;
+	void		*handle;
+	char		buffer[2048];
+	char		*env;
+	char const	*search_path;
+#ifdef RTLD_GLOBAL
+	if (strcmp(name, "rlm_perl") == 0) {
+		flags |= RTLD_GLOBAL;
+	} else
+#endif
+		flags |= RTLD_LOCAL;
+#if defined(RTLD_DEEPBIND) && !defined(__SANITIZE_ADDRESS__)
+		flags |= RTLD_DEEPBIND;
+#endif
+
+#ifndef NDEBUG
+	/*
+	 *	Bind all the symbols *NOW* so we don't hit errors later
+	 */
+	flags |= RTLD_NOW;
+#endif
+
+	/*
+	 *	Apple removed support for DYLD_LIBRARY_PATH in rootless mode.
+	 */
+	env = getenv("FR_LIBRARY_PATH");
+	if (env) {
+		DEBUG3("Ignoring libdir as FR_LIBRARY_PATH set.  Module search path will be: %s", env);
+		search_path = env;
+	} else {
+		search_path = radlib_dir;
+	}
+
+	/*
+	 *	Prefer loading our libraries by absolute path.
+	 */
+	if (search_path) {
+		char *error;
+		char *ctx, *paths, *path;
+		char *p;
+
+		fr_strerror();
+
+		ctx = paths = talloc_strdup(NULL, search_path);
+		while ((path = strsep(&paths, ":")) != NULL) {
+			/*
+			 *	Trim the trailing slash
+			 */
+			p = strrchr(path, '/');
+			if (p && ((p[1] == '\0') || (p[1] == ':'))) *p = '\0';
+
+			path = talloc_asprintf(ctx, "%s/%s%s", path, name, LT_SHREXT);
+
+			DEBUG4("Loading %s with path: %s", name, path);
+
+			handle = dlopen(path, flags);
+			if (handle) {
+				talloc_free(ctx);
+				return handle;
+			}
+			error = dlerror();
+
+			fr_strerror_printf("%s%s\n", fr_strerror(), error);
+			DEBUG4("Loading %s failed: %s - %s", name, error,
+			       (access(path, R_OK) < 0) ? fr_syserror(errno) : "No access errors");
+			talloc_free(path);
+		}
+		talloc_free(ctx);
+	}
+
+	DEBUG4("Loading library using linker search path(s)");
+	if (DEBUG_ENABLED4) {
+#ifdef __APPLE__
+		env = getenv("LD_LIBRARY_PATH");
+		if (env) {
+			DEBUG4("LD_LIBRARY_PATH            : %s", env);
+		}
+		env = getenv("DYLD_LIBRARY_PATH");
+		if (env) {
+			DEBUG4("DYLB_LIBRARY_PATH          : %s", env);
+		}
+		env = getenv("DYLD_FALLBACK_LIBRARY_PATH");
+		if (env) {
+			DEBUG4("DYLD_FALLBACK_LIBRARY_PATH : %s", env);
+		}
+		env = getcwd(buffer, sizeof(buffer));
+		if (env) {
+			DEBUG4("Current directory          : %s", env);
+		}
+#else
+		env = getenv("LD_LIBRARY_PATH");
+		if (env) {
+			DEBUG4("LD_LIBRARY_PATH  : %s", env);
+		}
+		DEBUG4("Defaults         : /lib:/usr/lib");
+#endif
+	}
+
+	strlcpy(buffer, name, sizeof(buffer));
+	/*
+	 *	FIXME: Make this configurable...
+	 */
+	strlcat(buffer, LT_SHREXT, sizeof(buffer));
+
+	handle = dlopen(buffer, flags);
+	if (!handle) {
+		char *error = dlerror();
+
+		DEBUG4("Failed with error: %s", error);
+		/*
+		 *	Don't overwrite the previous message
+		 *	It's likely to contain a better error.
+		 */
+		if (!radlib_dir) fr_strerror_printf("%s", dlerror());
+		return NULL;
+	}
+	return handle;
+}
+
+void *fr_dlsym(fr_dlhandle handle, char const *symbol)
+{
+	return dlsym(handle, symbol);
+}
+
+int fr_dlclose(fr_dlhandle handle)
+{
+	if (!handle) return 0;
+
+	return dlclose(handle);
+}
+
+char const *fr_dlerror(void)
+{
+	return dlerror();
+}
+
+static int virtual_server_idx(char const *name)
+{
+	uint32_t hash;
+
+	if (!name) return 0;
+
+	hash = fr_hash_string(name);
+
+	return hash & (VIRTUAL_SERVER_HASH_SIZE - 1);
+}
+
+static virtual_server_t *virtual_server_find(char const *name)
+{
+	rlm_rcode_t rcode;
+	virtual_server_t *server;
+
+	rcode = virtual_server_idx(name);
+	for (server = virtual_servers[rcode];
+	     server != NULL;
+	     server = server->next) {
+		if (!name && !server->name) break;
+
+		if ((name && server->name) &&
+		    (strcmp(name, server->name) == 0)) break;
+	}
+
+	return server;
+}
+
+static int _virtual_server_free(virtual_server_t *server)
+{
+	if (server->components) rbtree_free(server->components);
+	return 0;
+}
+
+void virtual_servers_free(time_t when)
+{
+	int i;
+	virtual_server_t **last;
+
+	for (i = 0; i < VIRTUAL_SERVER_HASH_SIZE; i++) {
+		virtual_server_t *server, *next;
+
+		last = &virtual_servers[i];
+		for (server = virtual_servers[i];
+		     server != NULL;
+		     server = next) {
+			next = server->next;
+
+			/*
+			 *	If we delete it, fix the links so that
+			 *	we don't orphan anything.  Also,
+			 *	delete it if it's old, AND a newer one
+			 *	was defined.
+			 *
+			 *	Otherwise, the last pointer gets set to
+			 *	the one we didn't delete.
+			 */
+			if ((when == 0) ||
+			    ((server->created < when) && server->can_free)) {
+				*last = server->next;
+				talloc_free(server);
+			} else {
+				last = &(server->next);
+			}
+		}
+	}
+}
+
+static int indexed_modcallable_cmp(void const *one, void const *two)
+{
+	indexed_modcallable const *a = one;
+	indexed_modcallable const *b = two;
+
+	if (a->comp < b->comp) return -1;
+	if (a->comp >  b->comp) return +1;
+
+	return a->idx - b->idx;
+}
+
+
+/*
+ *	Compare two module entries
+ */
+static int module_instance_cmp(void const *one, void const *two)
+{
+	module_instance_t const *a = one;
+	module_instance_t const *b = two;
+
+	return strcmp(a->name, b->name);
+}
+
+
+static void module_instance_free_old(UNUSED CONF_SECTION *cs, module_instance_t *node, time_t when)
+{
+	fr_module_hup_t *mh, **last;
+
+	/*
+	 *	Walk the list, freeing up old instances.
+	 */
+	last = &(node->mh);
+	while (*last) {
+		mh = *last;
+
+		/*
+		 *	Free only every 60 seconds.
+		 */
+		if ((when - mh->when) < 60) {
+			last = &(mh->next);
+			continue;
+		}
+
+		talloc_free(mh->insthandle);
+
+		*last = mh->next;
+		talloc_free(mh);
+	}
+}
+
+
+/*
+ *	Free a module instance.
+ */
+static void module_instance_free(void *data)
+{
+	module_instance_t *module = talloc_get_type_abort(data, module_instance_t);
+
+	module_instance_free_old(module->cs, module, time(NULL) + 100);
+
+#ifdef HAVE_PTHREAD_H
+	if (module->mutex) {
+		/*
+		 *	FIXME
+		 *	The mutex MIGHT be locked...
+		 *	we'll check for that later, I guess.
+		 */
+		pthread_mutex_destroy(module->mutex);
+		talloc_free(module->mutex);
+	}
+#endif
+
+	xlat_unregister(module->name, NULL, module->insthandle);
+
+	/*
+	 *	Remove all xlat's registered to module instance.
+	 */
+	if (module->insthandle) {
+		/*
+		 *	Remove any registered paircompares.
+		 */
+		paircompare_unregister_instance(module->insthandle);
+
+		xlat_unregister_module(module->insthandle);
+	}
+	talloc_free(module);
+}
+
+
+/*
+ *	Compare two module entries
+ */
+static int module_entry_cmp(void const *one, void const *two)
+{
+	module_entry_t const *a = one;
+	module_entry_t const *b = two;
+
+	return strcmp(a->name, b->name);
+}
+
+/*
+ *	Free a module entry.
+ */
+static int _module_entry_free(module_entry_t *this)
+{
+#ifndef NDEBUG
+	/*
+	 *	Don't dlclose() modules if we're doing memory
+	 *	debugging.  This removes the symbols needed by
+	 *	valgrind.
+	 */
+	if (!main_config.debug_memory)
+#endif
+		dlclose(this->handle);	/* ignore any errors */
+	return 0;
+}
+
+
+/*
+ *	Remove the module lists.
+ */
+int modules_free(void)
+{
+	rbtree_free(instance_tree);
+	rbtree_free(module_tree);
+
+	return 0;
+}
+
+
+/*
+ *	dlopen() a module.
+ */
+static module_entry_t *module_dlopen(CONF_SECTION *cs, char const *module_name)
+{
+	module_entry_t myentry;
+	module_entry_t *node;
+	void *handle = NULL;
+	module_t const *module;
+
+	strlcpy(myentry.name, module_name, sizeof(myentry.name));
+	node = rbtree_finddata(module_tree, &myentry);
+	if (node) return node;
+
+	/*
+	 *	Link to the module's rlm_FOO{} structure, the same as
+	 *	the module name.
+	 */
+
+#if !defined(WITH_LIBLTDL) && defined(HAVE_DLFCN_H) && defined(RTLD_SELF)
+	module = dlsym(RTLD_SELF, module_name);
+	if (module) goto open_self;
+#endif
+
+	/*
+	 *	Keep the handle around so we can dlclose() it.
+	 */
+	handle = fr_dlopenext(module_name);
+	if (!handle) {
+		cf_log_err_cs(cs, "Failed to link to module '%s': %s", module_name, fr_strerror());
+		return NULL;
+	}
+
+	DEBUG3("Loaded %s, checking if it's valid", module_name);
+
+	module = dlsym(handle, module_name);
+	if (!module) {
+		cf_log_err_cs(cs, "Failed linking to %s structure: %s", module_name, dlerror());
+		dlclose(handle);
+		return NULL;
+	}
+
+#if !defined(WITH_LIBLTDL) && defined (HAVE_DLFCN_H) && defined(RTLD_SELF)
+ open_self:
+#endif
+	/*
+	 *	Before doing anything else, check if it's sane.
+	 */
+	if (check_module_magic(cs, module) < 0) {
+		dlclose(handle);
+		return NULL;
+	}
+
+	/* make room for the module type */
+	node = talloc_zero(cs, module_entry_t);
+	talloc_set_destructor(node, _module_entry_free);
+	strlcpy(node->name, module_name, sizeof(node->name));
+	node->module = module;
+	node->handle = handle;
+
+	cf_log_module(cs, "Loaded module %s", module_name);
+
+	/*
+	 *	Add the module as "rlm_foo-version" to the configuration
+	 *	section.
+	 */
+	if (!rbtree_insert(module_tree, node)) {
+		ERROR("Failed to cache module %s", module_name);
+		dlclose(handle);
+		talloc_free(node);
+		return NULL;
+	}
+
+	return node;
+}
+
+/** Parse module's configuration section and setup destructors
+ *
+ */
+static int module_conf_parse(module_instance_t *node, void **handle)
+{
+	*handle = NULL;
+
+	/*
+	 *	If there is supposed to be instance data, allocate it now.
+	 *	Also parse the configuration data, if required.
+	 */
+	if (node->entry->module->inst_size) {
+		*handle = talloc_zero_array(node, uint8_t, node->entry->module->inst_size);
+		rad_assert(*handle);
+
+		talloc_set_name(*handle, "rlm_%s_t",
+				node->entry->module->name ? node->entry->module->name : "config");
+
+		if (node->entry->module->config &&
+		    (cf_section_parse(node->cs, *handle, node->entry->module->config) < 0)) {
+			cf_log_err_cs(node->cs,"Invalid configuration for module \"%s\"", node->name);
+			talloc_free(*handle);
+
+			return -1;
+		}
+
+		/*
+		 *	Set the destructor.
+		 */
+		if (node->entry->module->detach) {
+			talloc_set_destructor(*handle, node->entry->module->detach);
+		}
+	}
+
+	return 0;
+}
+
+/** Bootstrap a module.
+ *
+ *  Load the module shared library, allocate instance memory for it,
+ *  parse the module configuration, and call the modules "bootstrap" method.
+ */
+static module_instance_t *module_bootstrap(CONF_SECTION *cs)
+{
+	char const *name1, *name2, *askedname;
+	module_instance_t *node, myNode;
+	char module_name[256];
+
+	/*
+	 *	Figure out which module we want to load.
+	 */
+	name1 = cf_section_name1(cs);
+	askedname = name2 = cf_section_name2(cs);
+	if (!askedname) {
+		askedname = name1;
+		name2 = "";
+	}
+
+	strlcpy(myNode.name, askedname, sizeof(myNode.name));
+
+	/*
+	 *	See if the module already exists.
+	 */
+	node = rbtree_finddata(instance_tree, &myNode);
+	if (node) {
+		ERROR("Duplicate module \"%s %s { ... }\", in file %s:%d and file %s:%d",
+		      name1, name2,
+		      cf_section_filename(cs),
+		      cf_section_lineno(cs),
+		      cf_section_filename(node->cs),
+		      cf_section_lineno(node->cs));
+		return NULL;
+	}
+
+	/*
+	 *	Hang the node struct off of the configuration
+	 *	section. If the CS is free'd the instance will be
+	 *	free'd, too.
+	 */
+	node = talloc_zero(instance_tree, module_instance_t);
+	node->cs = cs;
+	strlcpy(node->name, askedname, sizeof(node->name));
+
+	/*
+	 *	Names in the "modules" section aren't prefixed
+	 *	with "rlm_", so we add it here.
+	 */
+	snprintf(module_name, sizeof(module_name), "rlm_%s", name1);
+
+	/*
+	 *	Load the module shared library.
+	 */
+	node->entry = module_dlopen(cs, module_name);
+	if (!node->entry) {
+		talloc_free(node);
+		return NULL;
+	}
+
+	cf_log_module(cs, "Loading module \"%s\" from file %s", node->name,
+		      cf_section_filename(cs));
+
+	/*
+	 *	Parse the modules configuration.
+	 */
+	if (module_conf_parse(node, &node->insthandle) < 0) {
+		talloc_free(node);
+		return NULL;
+	}
+
+	/*
+	 *	Bootstrap the module.
+	 */
+	if (node->entry->module->bootstrap &&
+	    ((node->entry->module->bootstrap)(cs, node->insthandle) < 0)) {
+		cf_log_err_cs(cs, "Instantiation failed for module \"%s\"", node->name);
+		talloc_free(node);
+		return NULL;
+	}
+
+	/*
+	 *	Remember the module for later.
+	 */
+	rbtree_insert(instance_tree, node);
+
+	return node;
+}
+
+
+/** Find an existing module instance.
+ *
+ */
+module_instance_t *module_find(CONF_SECTION *modules, char const *askedname)
+{
+	char const *instname;
+	module_instance_t myNode;
+
+	if (!modules) return NULL;
+
+	/*
+	 *	Look for the real name.  Ignore the first character,
+	 *	which tells the server "it's OK for this module to not
+	 *	exist."
+	 */
+	instname = askedname;
+	if (instname[0] == '-') instname++;
+
+	strlcpy(myNode.name, instname, sizeof(myNode.name));
+
+	return rbtree_finddata(instance_tree, &myNode);
+}
+
+
+/** Load a module, and instantiate it.
+ *
+ */
+module_instance_t *module_instantiate(CONF_SECTION *modules, char const *askedname)
+{
+	module_instance_t *node;
+
+	/*
+	 *	Find the module.  If it's not there, do nothing.
+	 */
+	node = module_find(modules, askedname);
+	if (!node) {
+		ERROR("Cannot find module \"%s\"", askedname);
+		return NULL;
+	}
+
+	/*
+	 *	The module is already instantiated.  Return it.
+	 */
+	if (node->instantiated) return node;
+
+	/*
+	 *	Now that ALL modules are instantiated, and ALL xlats
+	 *	are defined, go compile the config items marked as XLAT.
+	 */
+	if (node->entry->module->config &&
+	    (cf_section_parse_pass2(node->cs, node->insthandle,
+				    node->entry->module->config) < 0)) {
+		return NULL;
+	}
+
+	/*
+	 *	Call the instantiate method, if any.
+	 */
+	if (node->entry->module->instantiate) {
+		cf_log_module(node->cs, "Instantiating module \"%s\" from file %s", node->name,
+			      cf_section_filename(node->cs));
+
+		/*
+		 *	Call the module's instantiation routine.
+		 */
+		if ((node->entry->module->instantiate)(node->cs, node->insthandle) < 0) {
+			cf_log_err_cs(node->cs, "Instantiation failed for module \"%s\"", node->name);
+
+			return NULL;
+		}
+	}
+
+#ifdef HAVE_PTHREAD_H
+	/*
+	 *	If we're threaded, check if the module is thread-safe.
+	 *
+	 *	If it isn't, we create a mutex.
+	 */
+	if ((node->entry->module->type & RLM_TYPE_THREAD_UNSAFE) != 0) {
+		node->mutex = talloc_zero(node, pthread_mutex_t);
+
+		/*
+		 *	Initialize the mutex.
+		 */
+		pthread_mutex_init(node->mutex, NULL);
+	}
+#endif
+
+	node->instantiated = true;
+	node->last_hup = time(NULL); /* don't let us load it, then immediately hup it */
+
+	return node;
+}
+
+
+module_instance_t *module_instantiate_method(CONF_SECTION *modules, char const *name, rlm_components_t *method)
+{
+	char *p;
+	rlm_components_t i;
+	module_instance_t *mi;
+
+	/*
+	 *	If the module exists, ensure it's instantiated.
+	 *
+	 *	Doing it this way avoids complaints from
+	 *	module_instantiate()
+	 */
+	mi = module_find(modules, name);
+	if (mi) return module_instantiate(modules, name);
+
+	/*
+	 *	Find out which method is being used.
+	 */
+	p = strrchr(name, '.');
+	if (!p) return NULL;
+
+	p++;
+
+	/*
+	 *	Find the component.
+	 */
+	for (i = MOD_AUTHENTICATE; i < MOD_COUNT; i++) {
+		if (strcmp(p, section_type_value[i].section) == 0) {
+			char buffer[256];
+
+			strlcpy(buffer, name, sizeof(buffer));
+			buffer[p - name - 1] = '\0';
+
+			mi = module_find(modules, buffer);
+			if (mi) {
+				if (method) *method = i;
+				return module_instantiate(modules, buffer);
+			}
+		}
+	}
+
+	/*
+	 *	Not found.
+	 */
+	return NULL;
+}
+
+
+/** Resolve polymorphic item's from a module's CONF_SECTION to a subsection in another module
+ *
+ * This allows certain module sections to reference module sections in other instances
+ * of the same module and share CONF_DATA associated with them.
+ *
+ * @verbatim
+example {
+	data {
+		...
+	}
+}
+
+example inst {
+	data = example
+}
+ * @endverbatim
+ *
+ * @param out where to write the pointer to a module's config section.  May be NULL on success, indicating the config
+ *	  item was not found within the module CONF_SECTION, or the chain of module references was followed and the
+ *	  module at the end of the chain did not a subsection.
+ * @param module CONF_SECTION.
+ * @param name of the polymorphic sub-section.
+ * @return 0 on success with referenced section, 1 on success with local section, or -1 on failure.
+ */
+int find_module_sibling_section(CONF_SECTION **out, CONF_SECTION *module, char const *name)
+{
+	static bool loop = true;	/* not used, we just need a valid pointer to quiet static analysis */
+
+	CONF_PAIR *cp;
+	CONF_SECTION *cs;
+
+	module_instance_t *inst;
+	char const *inst_name;
+
+#define FIND_SIBLING_CF_KEY "find_sibling"
+
+	*out = NULL;
+
+	/*
+	 *	Is a real section (not referencing sibling module).
+	 */
+	cs = cf_section_sub_find(module, name);
+	if (cs) {
+		*out = cs;
+
+		return 0;
+	}
+
+	/*
+	 *	Item omitted completely from module config.
+	 */
+	cp = cf_pair_find(module, name);
+	if (!cp) return 0;
+
+	if (cf_data_find(module, FIND_SIBLING_CF_KEY)) {
+		cf_log_err_cp(cp, "Module reference loop found");
+
+		return -1;
+	}
+	cf_data_add(module, FIND_SIBLING_CF_KEY, &loop, NULL);
+
+	/*
+	 *	Item found, resolve it to a module instance.
+	 *	This triggers module loading, so we don't have
+	 *	instantiation order issues.
+	 */
+	inst_name = cf_pair_value(cp);
+	inst = module_instantiate(cf_item_parent(cf_section_to_item(module)), inst_name);
+
+	/*
+	 *	Remove the config data we added for loop
+	 *	detection.
+	 */
+	cf_data_remove(module, FIND_SIBLING_CF_KEY);
+	if (!inst) {
+		cf_log_err_cp(cp, "Unknown module instance \"%s\"", inst_name);
+
+		return -1;
+	}
+
+	/*
+	 *	Check the module instances are of the same type.
+	 */
+	if (strcmp(cf_section_name1(inst->cs), cf_section_name1(module)) != 0) {
+		cf_log_err_cp(cp, "Referenced module is a rlm_%s instance, must be a rlm_%s instance",
+			      cf_section_name1(inst->cs), cf_section_name1(module));
+
+		return -1;
+	}
+
+	*out = cf_section_sub_find(inst->cs, name);
+
+	return 1;
+}
+
+static indexed_modcallable *lookup_by_index(rbtree_t *components,
+					    rlm_components_t comp, int idx)
+{
+	indexed_modcallable myc;
+
+	myc.comp = comp;
+	myc.idx = idx;
+
+	return rbtree_finddata(components, &myc);
+}
+
+/*
+ *	Create a new sublist.
+ */
+static indexed_modcallable *new_sublist(CONF_SECTION *cs,
+					rbtree_t *components, rlm_components_t comp, int idx)
+{
+	indexed_modcallable *c;
+
+	c = lookup_by_index(components, comp, idx);
+
+	/* It is an error to try to create a sublist that already
+	 * exists. It would almost certainly be caused by accidental
+	 * duplication in the config file.
+	 *
+	 * index 0 is the exception, because it is used when we want
+	 * to collect _all_ listed modules under a single index by
+	 * default, which is currently the case in all components
+	 * except authenticate. */
+	if (c) {
+		if (idx == 0) {
+			return c;
+		}
+		return NULL;
+	}
+
+	c = talloc_zero(cs, indexed_modcallable);
+	c->modulelist = NULL;
+	c->comp = comp;
+	c->idx = idx;
+
+	if (!rbtree_insert(components, c)) {
+		talloc_free(c);
+		return NULL;
+	}
+
+	return c;
+}
+
+rlm_rcode_t indexed_modcall(rlm_components_t comp, int idx, REQUEST *request)
+{
+	rlm_rcode_t rcode;
+	modcallable *list = NULL;
+	virtual_server_t *server;
+
+	/*
+	 *	Hack to find the correct virtual server.
+	 */
+	server = virtual_server_find(request->server);
+	if (!server) {
+		RDEBUG("No such virtual server \"%s\"", request->server);
+		return RLM_MODULE_FAIL;
+	}
+
+	if (idx == 0) {
+		list = server->mc[comp];
+		if (!list) {
+			if (server->name) {
+				RDEBUG3("Empty %s section in virtual server \"%s\".  Using default return values.",
+					section_type_value[comp].section, server->name);
+			} else {
+				RDEBUG3("Empty %s section.  Using default return values.", section_type_value[comp].section);
+			}
+		}
+	} else {
+		indexed_modcallable *this;
+
+		this = lookup_by_index(server->components, comp, idx);
+		if (this) {
+			list = this->modulelist;
+		} else {
+			RDEBUG2("%s sub-section not found.  Ignoring.", section_type_value[comp].typename);
+		}
+	}
+
+	if (server->subcs[comp]) {
+		if (idx == 0) {
+			RDEBUG("# Executing section %s from file %s",
+			       section_type_value[comp].section,
+			       cf_section_filename(server->subcs[comp]));
+		} else {
+			RDEBUG("# Executing group from file %s",
+			       cf_section_filename(server->subcs[comp]));
+		}
+	}
+	request->component = section_type_value[comp].section;
+	rcode = modcall(comp, list, request);
+	request->component = "<core>";
+
+	return rcode;
+}
+
+/*
+ *	Load a sub-module list, as found inside an Auth-Type foo {}
+ *	block
+ */
+static int load_subcomponent_section(CONF_SECTION *cs,
+				     rbtree_t *components,
+				     DICT_ATTR const *da, rlm_components_t comp)
+{
+	indexed_modcallable *subcomp;
+	modcallable *ml;
+	DICT_VALUE *dval;
+	char const *name2 = cf_section_name2(cs);
+
+	/*
+	 *	Sanity check.
+	 */
+	if (!name2) {
+		return 1;
+	}
+
+	DEBUG("Compiling %s %s for attr %s", cf_section_name1(cs), name2, da->name);
+
+	/*
+	 *	Compile the group.
+	 */
+	ml = compile_modgroup(NULL, comp, cs);
+	if (!ml) {
+		return 0;
+	}
+
+	/*
+	 *	We must assign a numeric index to this subcomponent.
+	 *	It is generated and placed in the dictionary
+	 *	automatically.  If it isn't found, it's a serious
+	 *	error.
+	 */
+	dval = dict_valbyname(da->attr, da->vendor, name2);
+	if (!dval) {
+		talloc_free(ml);
+		cf_log_err_cs(cs,
+			   "The %s attribute has no VALUE defined for %s",
+			      section_type_value[comp].typename, name2);
+		return 0;
+	}
+
+	subcomp = new_sublist(cs, components, comp, dval->value);
+	if (!subcomp) {
+		talloc_free(ml);
+		return 1;
+	}
+
+	/*
+	 *	Link it into the talloc hierarchy.
+	 */
+	subcomp->modulelist = talloc_steal(subcomp, ml);
+	return 1;		/* OK */
+}
+
+/*
+ *	Don't complain too often.
+ */
+#define MAX_IGNORED (32)
+static int last_ignored = -1;
+static char const *ignored[MAX_IGNORED];
+
+static int load_component_section(CONF_SECTION *cs,
+				  rbtree_t *components, rlm_components_t comp)
+{
+	modcallable *this;
+	CONF_ITEM *modref;
+	int idx;
+	indexed_modcallable *subcomp;
+	char const *modname;
+	DICT_ATTR const *da;
+
+	/*
+	 *	Find the attribute used to store VALUEs for this section.
+	 */
+	da = dict_attrbyvalue(section_type_value[comp].attr, 0);
+	if (!da) {
+		cf_log_err_cs(cs,
+			   "No such attribute %s",
+			   section_type_value[comp].typename);
+		return -1;
+	}
+
+	/*
+	 *	Loop over the entries in the named section, loading
+	 *	the sections this time.
+	 */
+	for (modref = cf_item_find_next(cs, NULL);
+	     modref != NULL;
+	     modref = cf_item_find_next(cs, modref)) {
+		char const *name1;
+		CONF_PAIR *cp = NULL;
+		CONF_SECTION *scs = NULL;
+
+		if (cf_item_is_section(modref)) {
+			scs = cf_item_to_section(modref);
+
+			name1 = cf_section_name1(scs);
+
+			if (strcmp(name1,
+				   section_type_value[comp].typename) == 0) {
+				if (!load_subcomponent_section(scs,
+							       components,
+							       da,
+							       comp)) {
+
+					return -1; /* FIXME: memleak? */
+				}
+				continue;
+			}
+
+			cp = NULL;
+
+		} else if (cf_item_is_pair(modref)) {
+			cp = cf_item_to_pair(modref);
+
+		} else {
+			continue; /* ignore it */
+		}
+
+		/*
+		 *	Look for Auth-Type foo {}, which are special
+		 *	cases of named sections, and allowable ONLY
+		 *	at the top-level.
+		 *
+		 *	i.e. They're not allowed in a "group" or "redundant"
+		 *	subsection.
+		 */
+		if (comp == MOD_AUTHENTICATE) {
+			DICT_VALUE *dval;
+			char const *modrefname = NULL;
+
+			if (cp) {
+				modrefname = cf_pair_attr(cp);
+			} else {
+				modrefname = cf_section_name2(scs);
+				if (!modrefname) {
+					cf_log_err_cs(cs,
+						   "Errors parsing %s sub-section.\n",
+						   cf_section_name1(scs));
+					return -1;
+				}
+			}
+			if (*modrefname == '-') modrefname++;
+
+			dval = dict_valbyname(PW_AUTH_TYPE, 0, modrefname);
+			if (!dval) {
+				/*
+				 *	It's a section, but nothing we
+				 *	recognize.  Die!
+				 */
+				cf_log_err_cs(cs,
+					   "Unknown Auth-Type \"%s\" in %s sub-section.",
+					   modrefname, section_type_value[comp].section);
+				return -1;
+			}
+			idx = dval->value;
+		} else {
+			/* See the comment in new_sublist() for explanation
+			 * of the special index 0 */
+			idx = 0;
+		}
+
+		subcomp = new_sublist(cs, components, comp, idx);
+		if (!subcomp) continue;
+
+		/*
+		 *	Try to compile one entry.
+		 */
+		this = compile_modsingle(subcomp, &subcomp->modulelist, comp, modref, &modname);
+
+		/*
+		 *	It's OK for the module to not exist.
+		 */
+		if (!this && modname && (modname[0] == '-')) {
+			int i;
+
+			if (last_ignored < 0) {
+			save_complain:
+				last_ignored++;
+				ignored[last_ignored] = modname;
+
+			complain:
+				WARN("Ignoring \"%s\" (see raddb/mods-available/README.rst)", modname + 1);
+				continue;
+			}
+
+			if (last_ignored >= MAX_IGNORED) goto complain;
+
+			for (i = 0; i <= last_ignored; i++) {
+				if (strcmp(ignored[i], modname) == 0) {
+					break;
+				}
+			}
+
+			if (i > last_ignored) goto save_complain;
+			continue;
+		}
+
+		if (!this) {
+			cf_log_err_cs(cs,
+				   "Errors parsing %s section.\n",
+				   cf_section_name1(cs));
+			return -1;
+		}
+
+		if (rad_debug_lvl > 2) modcall_debug(this, 2);
+
+		add_to_modcallable(subcomp->modulelist, this);
+	}
+
+
+	return 0;
+}
+
+static int load_byserver(CONF_SECTION *cs)
+{
+	rlm_components_t comp;
+	bool found;
+	char const *name = cf_section_name2(cs);
+	rbtree_t *components;
+	virtual_server_t *server = NULL;
+	indexed_modcallable *c;
+	bool is_bare;
+
+	if (name) {
+		cf_log_info(cs, "server %s { # from file %s",
+			    name, cf_section_filename(cs));
+	} else {
+		cf_log_info(cs, "server { # from file %s",
+			    cf_section_filename(cs));
+	}
+
+	is_bare = (cf_item_parent(cf_section_to_item(cs)) == NULL);
+
+	server = talloc_zero(cs, virtual_server_t);
+	server->name = name;
+	server->created = time(NULL);
+	server->cs = cs;
+	server->components = components = rbtree_create(server, indexed_modcallable_cmp, NULL, 0);
+	if (!components) {
+		ERROR("Failed to initialize components");
+
+	error:
+		if (rad_debug_lvl == 0) {
+			ERROR("Failed to load virtual server %s",
+			      (name != NULL) ? name : "<default>");
+		}
+		return -1;
+	}
+	talloc_set_destructor(server, _virtual_server_free);
+
+	/*
+	 *	Loop over all of the known components, finding their
+	 *	configuration section, and loading it.
+	 */
+	found = false;
+	for (comp = 0; comp < MOD_COUNT; ++comp) {
+		CONF_SECTION *subcs;
+
+		subcs = cf_section_sub_find(cs,
+					    section_type_value[comp].section);
+		if (!subcs) continue;
+
+		if (is_bare) {
+			cf_log_err_cs(subcs, "The %s section should be inside of a 'server { ... }' block!",
+				      section_type_value[comp].section);
+		}
+
+		if (cf_item_find_next(subcs, NULL) == NULL) continue;
+
+		/*
+		 *	Skip pre/post-proxy sections if we're not
+		 *	proxying.
+		 */
+		if (
+#ifdef WITH_PROXY
+		    !main_config.proxy_requests &&
+#endif
+		    ((comp == MOD_PRE_PROXY) ||
+		     (comp == MOD_POST_PROXY))) {
+			continue;
+		}
+
+#ifndef WITH_ACCOUNTING
+		if (comp == MOD_ACCOUNTING) continue;
+#endif
+
+#ifndef WITH_SESSION_MGMT
+		if (comp == MOD_SESSION) continue;
+#endif
+
+		if (rad_debug_lvl <= 3) {
+			cf_log_module(cs, "Loading %s {...}",
+				      section_type_value[comp].section);
+		} else {
+			DEBUG(" %s {", section_type_value[comp].section);
+		}
+
+		if (load_component_section(subcs, components, comp) < 0) {
+			goto error;
+		}
+
+		if (rad_debug_lvl > 3) {
+			DEBUG(" } # %s", section_type_value[comp].section);
+		}
+
+		/*
+		 *	Cache a default, if it exists.  Some people
+		 *	put empty sections for some reason...
+		 */
+		c = lookup_by_index(components, comp, 0);
+		if (c) server->mc[comp] = c->modulelist;
+
+		server->subcs[comp] = subcs;
+
+		found = true;
+	} /* loop over components */
+
+	/*
+	 *	We haven't loaded any of the normal sections.  Maybe we're
+	 *	supposed to load the vmps section.
+	 *
+	 *	This is a bit of a hack...
+	 */
+	if (!found) do {
+#if defined(WITH_VMPS) || defined(WITH_DHCP) || defined(WITH_TLS)
+		CONF_SECTION *subcs;
+#endif
+#if defined(WITH_DHCP) || defined(WITH_TLS)
+		DICT_ATTR const *da;
+#endif
+
+#ifdef WITH_VMPS
+		subcs = cf_section_sub_find(cs, "vmps");
+		if (subcs) {
+			cf_log_module(cs, "Loading vmps {...}");
+			if (load_component_section(subcs, components,
+						   MOD_POST_AUTH) < 0) {
+				goto error;
+			}
+			c = lookup_by_index(components,
+					    MOD_POST_AUTH, 0);
+			if (c) server->mc[MOD_POST_AUTH] = c->modulelist;
+			break;
+		}
+#endif
+
+#ifdef WITH_TLS
+		/*
+		 *	It's OK to not have TLS cache sections.
+		 */
+		da = dict_attrbyname("TLS-Cache-Method");
+		subcs = cf_section_sub_find_name2(cs, "cache", "load");
+		if (subcs && !load_subcomponent_section(subcs,
+							components,
+							da,
+							MOD_POST_AUTH)) {
+			goto error; /* FIXME: memleak? */
+		}
+
+		subcs = cf_section_sub_find_name2(cs, "cache", "save");
+		if (subcs && !load_subcomponent_section(subcs,
+							components,
+							da,
+							MOD_POST_AUTH)) {
+			goto error; /* FIXME: memleak? */
+		}
+
+		subcs = cf_section_sub_find_name2(cs, "cache", "clear");
+		if (subcs && !load_subcomponent_section(subcs,
+							components,
+							da,
+							MOD_POST_AUTH)) {
+			goto error; /* FIXME: memleak? */
+		}
+
+		subcs = cf_section_sub_find_name2(cs, "cache", "refresh");
+		if (subcs && !load_subcomponent_section(subcs,
+							components,
+							da,
+							MOD_POST_AUTH)) {
+			goto error; /* FIXME: memleak? */
+		}
+#endif
+
+#ifdef WITH_DHCP
+		/*
+		 *	It's OK to not have DHCP.
+		 */
+		subcs = cf_subsection_find_next(cs, NULL, "dhcp");
+		if (!subcs) break;
+
+		da = dict_attrbyname("DHCP-Message-Type");
+
+		/*
+		 *	Handle each DHCP Message type separately.
+		 */
+		while (subcs) {
+			char const *name2 = cf_section_name2(subcs);
+
+			if (name2) {
+				cf_log_module(cs, "Loading dhcp %s {...}", name2);
+			} else {
+				cf_log_module(cs, "Loading dhcp {...}");
+			}
+			if (!load_subcomponent_section(subcs,
+						       components,
+						       da,
+						       MOD_POST_AUTH)) {
+				goto error; /* FIXME: memleak? */
+			}
+			c = lookup_by_index(components,
+					    MOD_POST_AUTH, 0);
+			if (c) server->mc[MOD_POST_AUTH] = c->modulelist;
+
+			subcs = cf_subsection_find_next(cs, subcs, "dhcp");
+		}
+#endif
+
+
+	} while (0);
+
+	if (name) {
+		cf_log_info(cs, "} # server %s", name);
+	} else {
+		cf_log_info(cs, "} # server");
+	}
+
+	if (rad_debug_lvl == 0) {
+		INFO("Loaded virtual server %s",
+		       (name != NULL) ? name : "<default>");
+	}
+
+	/*
+	 *	Now that it is OK, insert it into the list.
+	 *
+	 *	This is thread-safe...
+	 */
+	comp = virtual_server_idx(name);
+	server->next = virtual_servers[comp];
+	virtual_servers[comp] = server;
+
+	/*
+	 *	Mark OLDER ones of the same name as being unused.
+	 */
+	server = server->next;
+	while (server) {
+		if ((!name && !server->name) ||
+		    (name && server->name &&
+		     (strcmp(server->name, name) == 0))) {
+			server->can_free = true;
+			break;
+		}
+		server = server->next;
+	}
+
+	return 0;
+}
+
+
+static int pass2_cb(UNUSED void *ctx, void *data)
+{
+	indexed_modcallable *this = data;
+
+	if (!modcall_pass2(this->modulelist)) return -1;
+
+	return 0;
+}
+
+
+/*
+ *	Load all of the virtual servers.
+ */
+int virtual_servers_load(CONF_SECTION *config)
+{
+	CONF_SECTION *cs;
+	virtual_server_t *server;
+	static bool first_time = true;
+
+	DEBUG2("%s: #### Loading Virtual Servers ####", main_config.name);
+
+	/*
+	 *	If we have "server { ...}", then there SHOULD NOT be
+	 *	bare "authorize", etc. sections.  if there is no such
+	 *	server, then try to load the old-style sections first.
+	 *
+	 *	In either case, load the "default" virtual server first.
+	 *	this matches better with users expectations.
+	 */
+	cs = cf_section_find_name2(cf_subsection_find_next(config, NULL,
+							   "server"),
+				   "server", NULL);
+	if (cs) {
+		if (load_byserver(cs) < 0) {
+			return -1;
+		}
+	} else {
+		if (load_byserver(config) < 0) {
+			return -1;
+		}
+	}
+
+	/*
+	 *	Load all of the virtual servers.
+	 */
+	for (cs = cf_subsection_find_next(config, NULL, "server");
+	     cs != NULL;
+	     cs = cf_subsection_find_next(config, cs, "server")) {
+		char const *name2;
+
+		name2 = cf_section_name2(cs);
+		if (!name2) continue; /* handled above */
+
+		server = virtual_server_find(name2);
+		if (server &&
+		    (cf_top_section(server->cs) == config)) {
+			ERROR("Duplicate virtual server \"%s\" in file %s:%d and file %s:%d",
+			       server->name,
+			       cf_section_filename(server->cs),
+			       cf_section_lineno(server->cs),
+			       cf_section_filename(cs),
+			       cf_section_lineno(cs));
+			return -1;
+		}
+
+		if (load_byserver(cs) < 0) {
+			/*
+			 *	Once we successfully started once,
+			 *	continue loading the OTHER servers,
+			 *	even if one fails.
+			 */
+			if (!first_time) continue;
+			return -1;
+		}
+	}
+
+	/*
+	 *	Try to compile the "authorize", etc. sections which
+	 *	aren't in a virtual server.
+	 */
+	server = virtual_server_find(NULL);
+	if (server) {
+		int i;
+
+		for (i = MOD_AUTHENTICATE; i < MOD_COUNT; i++) {
+			if (!modcall_pass2(server->mc[i])) return -1;
+		}
+
+		if (server->components &&
+		    (rbtree_walk(server->components, RBTREE_IN_ORDER,
+				 pass2_cb, NULL) != 0)) {
+			return -1;
+		}
+	}
+
+	/*
+	 *	Now that we've loaded everything, run pass 2 over the
+	 *	conditions and xlats.
+	 */
+	for (cs = cf_subsection_find_next(config, NULL, "server");
+	     cs != NULL;
+	     cs = cf_subsection_find_next(config, cs, "server")) {
+		int i;
+		char const *name2;
+
+		name2 = cf_section_name2(cs);
+
+		server = virtual_server_find(name2);
+		if (!server) continue;
+
+		for (i = MOD_AUTHENTICATE; i < MOD_COUNT; i++) {
+			if (!modcall_pass2(server->mc[i])) return -1;
+		}
+
+		if (server->components &&
+		    (rbtree_walk(server->components, RBTREE_IN_ORDER,
+				 pass2_cb, NULL) != 0)) {
+			return -1;
+		}
+	}
+
+	/*
+	 *	If we succeed the first time around, remember that.
+	 */
+	first_time = false;
+
+	return 0;
+}
+
+int module_hup_module(CONF_SECTION *cs, module_instance_t *node, time_t when)
+{
+	void *insthandle;
+	fr_module_hup_t *mh;
+
+	if (!node ||
+	    node->entry->module->bootstrap ||
+	    !node->entry->module->instantiate ||
+	    ((node->entry->module->type & RLM_TYPE_HUP_SAFE) == 0)) {
+		return 1;
+	}
+
+	/*
+	 *	Silently ignore multiple HUPs within a short time period.
+	 */
+	if ((node->last_hup + 2) >= when) return 1;
+	node->last_hup = when;
+
+	/*
+	 *	Clear any old instances before attempting to reload
+	 */
+	module_instance_free_old(cs, node, when);
+
+	cf_log_module(cs, "Trying to reload module \"%s\"", node->name);
+
+	/*
+	 *	Parse the module configuration, and setup destructors so the
+	 *	module's detach method is called when it's instance data is
+	 *	about to be freed.
+	 */
+	if (module_conf_parse(node, &insthandle) < 0) {
+		cf_log_err_cs(cs, "HUP failed for module \"%s\" (parsing config failed). "
+			      "Using old configuration", node->name);
+
+		return 0;
+	}
+
+	if ((node->entry->module->instantiate)(cs, insthandle) < 0) {
+		cf_log_err_cs(cs, "HUP failed for module \"%s\".  Using old configuration.", node->name);
+		talloc_free(insthandle);
+
+		return 0;
+	}
+
+	INFO(" Module: Reloaded module \"%s\"", node->name);
+
+	/*
+	 *	Save the old instance handle for later deletion.
+	 */
+	mh = talloc_zero(cs, fr_module_hup_t);
+	mh->mi = node;
+	mh->when = when;
+	mh->insthandle = node->insthandle;
+	mh->next = node->mh;
+	node->mh = mh;
+
+	/*
+	 *	Replace the instance handle while the module is running.
+	 */
+	node->insthandle = insthandle;
+
+	/*
+	 *	FIXME: Set a timeout to come back in 60s, so that
+	 *	we can pro-actively clean up the old instances.
+	 */
+
+	return 1;
+}
+
+
+int modules_hup(CONF_SECTION *modules)
+{
+	time_t when;
+	CONF_ITEM *ci;
+	CONF_SECTION *cs;
+	module_instance_t *node;
+
+	if (!modules) return 0;
+
+	when = time(NULL);
+
+	/*
+	 *	Loop over the modules
+	 */
+	for (ci=cf_item_find_next(modules, NULL);
+	     ci != NULL;
+	     ci=cf_item_find_next(modules, ci)) {
+		char const *instname;
+		module_instance_t myNode;
+
+		/*
+		 *	If it's not a section, ignore it.
+		 */
+		if (!cf_item_is_section(ci)) continue;
+
+		cs = cf_item_to_section(ci);
+		instname = cf_section_name2(cs);
+		if (!instname) instname = cf_section_name1(cs);
+
+		strlcpy(myNode.name, instname, sizeof(myNode.name));
+		node = rbtree_finddata(instance_tree, &myNode);
+
+		module_hup_module(cs, node, when);
+	}
+
+	return 1;
+}
+
+
+static int define_type(CONF_SECTION *cs, DICT_ATTR const *da, char const *name)
+{
+	uint32_t value;
+	DICT_VALUE *dval;
+
+	/*
+	 *	Allow for conditionally loaded types
+	 */
+	if (*name == '-') name++;
+
+	/*
+	 *	If the value already exists, don't
+	 *	create it again.
+	 */
+	dval = dict_valbyname(da->attr, da->vendor, name);
+	if (dval) {
+		if (dval->value == 0) {
+			ERROR("The dictionaries must not define VALUE %s %s 0",
+			      da->name, name);
+			return 0;
+		}
+		return 1;
+	}
+
+	/*
+	 *	Create a new unique value with a
+	 *	meaningless number.  You can't look at
+	 *	it from outside of this code, so it
+	 *	doesn't matter.  The only requirement
+	 *	is that it's unique.
+	 */
+	do {
+		value = (fr_rand() & 0x00ffffff) + 1;
+	} while (dict_valbyattr(da->attr, da->vendor, value));
+
+	cf_log_module(cs, "Creating %s = %s", da->name, name);
+	if (dict_addvalue(name, da->name, value) < 0) {
+		ERROR("%s", fr_strerror());
+		return 0;
+	}
+
+	return 1;
+}
+
+/*
+ *	Define Auth-Type, etc. in a server.
+ */
+static bool server_define_types(CONF_SECTION *cs)
+{
+	rlm_components_t	comp;
+
+	/*
+	 *	Loop over all of the components
+	 */
+	for (comp = 0; comp < MOD_COUNT; ++comp) {
+		CONF_SECTION *subcs, *type_cs;
+		DICT_ATTR const *da;
+
+		subcs = cf_section_sub_find(cs,
+					    section_type_value[comp].section);
+		if (!subcs) continue;
+
+		if (cf_item_find_next(subcs, NULL) == NULL) continue;
+
+		/*
+		 *	Find the attribute used to store VALUEs for this section.
+		 */
+		da = dict_attrbyvalue(section_type_value[comp].attr, 0);
+		if (!da) {
+			cf_log_err_cs(subcs,
+				   "No such attribute %s",
+				   section_type_value[comp].typename);
+			return false;
+		}
+
+		/*
+		 *	Define dynamic types, so that others can reference
+		 *	them.
+		 *
+		 *	First, bare modules for 'authenticate'.
+		 *	Second, Auth-Type, etc.
+		 */
+		if (section_type_value[comp].attr == PW_AUTH_TYPE) {
+			CONF_ITEM *modref;
+
+			for (modref = cf_item_find_next(subcs, NULL);
+			     modref != NULL;
+			     modref = cf_item_find_next(subcs, modref)) {
+				CONF_PAIR *cp;
+
+				if (!cf_item_is_pair(modref)) continue;
+
+				cp = cf_item_to_pair(modref);
+				if (!define_type(cs, da, cf_pair_attr(cp))) {
+					return false;
+				}
+
+				/*
+				 *	Check for duplicates
+				 */
+				if (rad_debug_lvl) {
+					CONF_PAIR *cp2;
+					CONF_SECTION *cs2;
+
+					cp2 = cf_pair_find(subcs, cf_pair_attr(cp));
+					rad_assert(cp2 != NULL);
+					if (cp2 != cp) {
+						WARN("%s[%d]: Duplicate module '%s'",
+						     cf_pair_filename(cp2),
+						     cf_pair_lineno(cp2),
+						     cf_pair_attr(cp));
+					}
+
+					cs2 = cf_section_sub_find_name2(subcs, section_type_value[comp].typename, cf_pair_attr(cp));
+					if (cs2) {
+						WARN("%s[%d]: Duplicate Auth-Type '%s'",
+						     cf_section_filename(cs2),
+						     cf_section_lineno(cs2),
+						     cf_pair_attr(cp));
+					}
+				}
+
+			}
+		}
+
+		/*
+		 *	And loop over the type names
+		 */
+		for (type_cs = cf_subsection_find_next(subcs, NULL, section_type_value[comp].typename);
+		     type_cs != NULL;
+		     type_cs = cf_subsection_find_next(subcs, type_cs, section_type_value[comp].typename)) {
+			if (!define_type(cs, da, cf_section_name2(type_cs))) {
+				return false;
+			}
+
+			if (rad_debug_lvl) {
+				CONF_SECTION *cs2;
+
+				cs2 = cf_section_sub_find_name2(subcs, section_type_value[comp].typename, cf_section_name2(type_cs));
+				rad_assert(cs2 != NULL);
+				if (cs2 != type_cs) {
+					WARN("%s[%d]: Duplicate Auth-Type '%s'",
+					     cf_section_filename(cs2),
+					     cf_section_lineno(cs2),
+					     cf_section_name2(cs2));
+				}
+			}
+		}
+	} /* loop over components */
+
+	return true;
+}
+
+extern char const *unlang_keyword[];
+
+static bool is_reserved_word(const char *name)
+{
+	int i;
+
+	if (!name || !*name) return false;
+
+	for (i = 1; unlang_keyword[i] != NULL; i++) {
+		if (strcmp(name, unlang_keyword[i]) == 0) return true;
+	}
+
+	return false;
+}
+
+
+/*
+ *	Parse the module config sections, and load
+ *	and call each module's init() function.
+ */
+int modules_init(CONF_SECTION *config)
+{
+	CONF_ITEM	*ci, *next;
+	CONF_SECTION	*cs, *modules;
+
+	/*
+	 *	Set up the internal module struct.
+	 */
+	module_tree = rbtree_create(NULL, module_entry_cmp, NULL, 0);
+	if (!module_tree) {
+		ERROR("Failed to initialize modules\n");
+		return -1;
+	}
+
+	instance_tree = rbtree_create(NULL, module_instance_cmp,
+				      module_instance_free, 0);
+	if (!instance_tree) {
+		ERROR("Failed to initialize modules\n");
+		return -1;
+	}
+
+	memset(virtual_servers, 0, sizeof(virtual_servers));
+
+	/*
+	 *	Remember where the modules were stored.
+	 */
+	modules = cf_section_sub_find(config, "modules");
+	if (!modules) {
+		WARN("Cannot find a \"modules\" section in the configuration file!");
+	}
+
+	/*
+	 *	Load dictionaries.
+	 */
+	for (cs = cf_subsection_find_next(config, NULL, "server");
+	     cs != NULL;
+	     cs = cf_subsection_find_next(config, cs, "server")) {
+#if defined(WITH_DHCP) || defined(WITH_VMPS)
+		CONF_SECTION *subcs;
+		DICT_ATTR const *da;
+#endif
+
+#ifdef WITH_VMPS
+		/*
+		 *	Auto-load the VMPS/VQP dictionary.
+		 */
+		subcs = cf_section_sub_find(cs, "vmps");
+		if (subcs) {
+			da = dict_attrbyname("VQP-Packet-Type");
+			if (!da) {
+				if (dict_read(main_config.dictionary_dir, "dictionary.vqp") < 0) {
+					ERROR("Failed reading dictionary.vqp: %s",
+					      fr_strerror());
+					return -1;
+				}
+				cf_log_module(cs, "Loading dictionary.vqp");
+
+				da = dict_attrbyname("VQP-Packet-Type");
+				if (!da) {
+					ERROR("No VQP-Packet-Type in dictionary.vqp");
+					return -1;
+				}
+			}
+		}
+#endif
+
+#ifdef WITH_DHCP
+		/*
+		 *	Auto-load the DHCP dictionary.
+		 */
+		subcs = cf_subsection_find_next(cs, NULL, "dhcp");
+		if (subcs) {
+			da = dict_attrbyname("DHCP-Message-Type");
+			if (!da) {
+				cf_log_module(cs, "Loading dictionary.dhcp");
+				if (dict_read(main_config.dictionary_dir, "dictionary.dhcp") < 0) {
+					ERROR("Failed reading dictionary.dhcp: %s",
+					      fr_strerror());
+					return -1;
+				}
+
+				da = dict_attrbyname("DHCP-Message-Type");
+				if (!da) {
+					ERROR("No DHCP-Message-Type in dictionary.dhcp");
+					return -1;
+				}
+			}
+		}
+#endif
+		/*
+		 *	Else it's a RADIUS virtual server, and the
+		 *	dictionaries are already loaded.
+		 */
+
+		/*
+		 *	Root through each virtual server, defining
+		 *	Autz-Type and Auth-Type.  This is so that the
+		 *	modules can reference a particular type.
+		 */
+		if (!server_define_types(cs)) return -1;
+	}
+
+	DEBUG2("%s: #### Instantiating modules ####", main_config.name);
+
+	cf_log_info(config, " modules {");
+
+	/*
+	 *	Loop over module definitions, looking for duplicates.
+	 *
+	 *	This is O(N^2) in the number of modules, but most
+	 *	systems should have less than 100 modules.
+	 */
+	for (ci = cf_item_find_next(modules, NULL);
+	     ci != NULL;
+	     ci = next) {
+		char const *name1;
+		CONF_SECTION *subcs;
+		module_instance_t *node;
+
+		next = cf_item_find_next(modules, ci);
+
+		if (!cf_item_is_section(ci)) continue;
+
+		subcs = cf_item_to_section(ci);
+
+		node = module_bootstrap(subcs);
+		if (!node) return -1;
+
+		if (!next || !cf_item_is_section(next)) continue;
+
+		name1 = cf_section_name1(subcs);
+
+		if (is_reserved_word(name1)) {
+			cf_log_err_cs(subcs, "Module cannot be named for an 'unlang' keyword");
+			return -1;
+		}
+	}
+
+	/*
+	 *  Look for the 'instantiate' section, which tells us
+	 *  the instantiation order of the modules, and also allows
+	 *  us to load modules with no authorize/authenticate/etc.
+	 *  sections.
+	 */
+	cs = cf_section_sub_find(config, "instantiate");
+	if (cs) {
+		CONF_PAIR *cp;
+		module_instance_t *module;
+		char const *name;
+
+		cf_log_info(cs, "  instantiate {");
+
+		/*
+		 *  Loop over the items in the 'instantiate' section.
+		 */
+		for (ci=cf_item_find_next(cs, NULL);
+		     ci != NULL;
+		     ci=cf_item_find_next(cs, ci)) {
+			/*
+			 *	Skip sections and "other" stuff.
+			 *	Sections will be handled later, if
+			 *	they're referenced at all...
+			 */
+			if (cf_item_is_pair(ci)) {
+				cp = cf_item_to_pair(ci);
+				name = cf_pair_attr(cp);
+
+				module = module_instantiate(modules, name);
+				if (!module && (name[0] != '-')) {
+					return -1;
+				}
+			}
+
+			/*
+			 *	Can only be "redundant" or
+			 *	"load-balance" or
+			 *	"redundant-load-balance"
+			 */
+			if (cf_item_is_section(ci)) {
+				bool all_same = true;
+				module_t const *last = NULL;
+				CONF_SECTION *subcs;
+				CONF_ITEM *subci;
+
+				subcs = cf_item_to_section(ci);
+				name = cf_section_name1(subcs);
+
+				/*
+				 *	Groups, etc. must have a name.
+				 */
+				if (((strcmp(name, "group") == 0) ||
+				     (strcmp(name, "redundant") == 0) ||
+				     (strcmp(name, "redundant-load-balance") == 0) ||
+				     strcmp(name, "load-balance") == 0)) {
+					name = cf_section_name2(subcs);
+					if (!name) {
+						cf_log_err_cs(subcs, "Subsection must have a name");
+						return -1;
+					}
+
+					if (is_reserved_word(name)) {
+						cf_log_err_cs(subcs, "Instantiate sections cannot be named for an 'unlang' keyword");
+						return -1;
+					}
+				} else {
+					if (is_reserved_word(name)) {
+						cf_log_err_cs(subcs, "Instantiate sections cannot be named for an 'unlang' keyword");
+						return -1;
+					}
+				}
+
+				/*
+				 *	Ensure that the modules we reference here exist.
+				 */
+				for (subci=cf_item_find_next(subcs, NULL);
+				     subci != NULL;
+				     subci=cf_item_find_next(subcs, subci)) {
+					if (cf_item_is_pair(subci)) {
+						cp = cf_item_to_pair(subci);
+						if (cf_pair_value(cp)) {
+							cf_log_err(subci, "Cannot set return codes in a %s block",
+								   cf_section_name1(subcs));
+							return -1;
+						}
+
+						/*
+						 *	Allow "foo.authorize" in subsections.
+						 */
+						module = module_instantiate_method(modules, cf_pair_attr(cp), NULL);
+						if (!module) {
+							return -1;
+						}
+
+						if (all_same) {
+							if (!last) {
+								last = module->entry->module;
+							} else if (last != module->entry->module) {
+								last = NULL;
+								all_same = false;
+							}
+						}
+					} else {
+						all_same = false;
+					}
+
+					/*
+					 *	Don't check subsections for now.
+					 */
+				} /* loop over modules in a "redundant foo" section */
+
+				/*
+				 *	Register a redundant xlat
+				 */
+				if (all_same) {
+					if (!xlat_register_redundant(cf_item_to_section(ci))) {
+						WARN("%s[%d] Not registering expansions for %s",
+						     cf_section_filename(subcs), cf_section_lineno(subcs),
+						     cf_section_name2(subcs));
+					}
+				}
+			}  /* handle subsections */
+		} /* loop over the "instantiate" section */
+
+		cf_log_info(cs, "  }");
+	} /* if there's an 'instantiate' section. */
+
+	/*
+	 *	Now that we've loaded the explicitly ordered modules,
+	 *	load everything in the "modules" section.  This is
+	 *	because we've now split up the modules into
+	 *	mods-enabled.
+	 */
+	for (ci=cf_item_find_next(modules, NULL);
+	     ci != NULL;
+	     ci=next) {
+		char const *name;
+		module_instance_t *module;
+		CONF_SECTION *subcs;
+
+		next = cf_item_find_next(modules, ci);
+
+		if (!cf_item_is_section(ci)) continue;
+
+		subcs = cf_item_to_section(ci);
+		name = cf_section_name2(subcs);
+		if (!name) name = cf_section_name1(subcs);
+
+		module = module_instantiate(modules, name);
+		if (!module) return -1;
+	}
+	cf_log_info(config, " } # modules");
+
+	if (virtual_servers_load(config) < 0) return -1;
+
+	return 0;
+}
+
+/*
+ *	Call all authorization modules until one returns
+ *	somethings else than RLM_MODULE_OK
+ */
+rlm_rcode_t process_authorize(int autz_type, REQUEST *request)
+{
+	return indexed_modcall(MOD_AUTHORIZE, autz_type, request);
+}
+
+/*
+ *	Authenticate a user/password with various methods.
+ */
+rlm_rcode_t process_authenticate(int auth_type, REQUEST *request)
+{
+	return indexed_modcall(MOD_AUTHENTICATE, auth_type, request);
+}
+
+#ifdef WITH_ACCOUNTING
+/*
+ *	Do pre-accounting for ALL configured sessions
+ */
+rlm_rcode_t module_preacct(REQUEST *request)
+{
+	return indexed_modcall(MOD_PREACCT, 0, request);
+}
+
+/*
+ *	Do accounting for ALL configured sessions
+ */
+rlm_rcode_t process_accounting(int acct_type, REQUEST *request)
+{
+	return indexed_modcall(MOD_ACCOUNTING, acct_type, request);
+}
+#endif
+
+#ifdef WITH_SESSION_MGMT
+/*
+ *	See if a user is already logged in.
+ *
+ *	Returns: 0 == OK, 1 == double logins, 2 == multilink attempt
+ */
+int process_checksimul(int sess_type, REQUEST *request, int maxsimul)
+{
+	rlm_rcode_t rcode;
+
+	if(!request->username)
+		return 0;
+
+	request->simul_count = 0;
+	request->simul_max = maxsimul;
+	request->simul_mpp = 1;
+
+	rcode = indexed_modcall(MOD_SESSION, sess_type, request);
+
+	if (rcode != RLM_MODULE_OK) {
+		/* FIXME: Good spot for a *rate-limited* warning to the log */
+		return 0;
+	}
+
+	return (request->simul_count < maxsimul) ? 0 : request->simul_mpp;
+}
+#endif
+
+#ifdef WITH_PROXY
+/*
+ *	Do pre-proxying for ALL configured sessions
+ */
+rlm_rcode_t process_pre_proxy(int type, REQUEST *request)
+{
+	return indexed_modcall(MOD_PRE_PROXY, type, request);
+}
+
+/*
+ *	Do post-proxying for ALL configured sessions
+ */
+rlm_rcode_t process_post_proxy(int type, REQUEST *request)
+{
+	return indexed_modcall(MOD_POST_PROXY, type, request);
+}
+#endif
+
+/*
+ *	Do post-authentication for ALL configured sessions
+ */
+rlm_rcode_t process_post_auth(int postauth_type, REQUEST *request)
+{
+	return indexed_modcall(MOD_POST_AUTH, postauth_type, request);
+}
+
+#ifdef WITH_COA
+rlm_rcode_t process_recv_coa(int recv_coa_type, REQUEST *request)
+{
+	return indexed_modcall(MOD_RECV_COA, recv_coa_type, request);
+}
+
+rlm_rcode_t process_send_coa(int send_coa_type, REQUEST *request)
+{
+	return indexed_modcall(MOD_SEND_COA, send_coa_type, request);
+}
+#endif
diff --git a/src/main/pair.c b/src/main/pair.c
new file mode 100644
index 0000000..3725ba1
--- /dev/null
+++ b/src/main/pair.c
@@ -0,0 +1,911 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @brief Valuepair functions that are radiusd-specific and as such do not
+ * 	  belong in the library.
+ * @file main/pair.c
+ *
+ * @ingroup AVP
+ *
+ * @copyright 2000,2006  The FreeRADIUS server project
+ * @copyright 2000  Alan DeKok <aland@ox.org>
+ */
+
+RCSID("$Id$")
+
+#include <ctype.h>
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/rad_assert.h>
+
+struct cmp {
+	DICT_ATTR const *attribute;
+	DICT_ATTR const *from;
+	bool	first_only;
+	void *instance; /* module instance */
+	RAD_COMPARE_FUNC compare;
+	struct cmp *next;
+};
+static struct cmp *cmp;
+
+/** Compares check and vp by value.
+ *
+ * Does not call any per-attribute comparison function, but does honour
+ * check.operator. Basically does "vp.value check.op check.value".
+ *
+ * @param request Current request.
+ * @param check rvalue, and operator.
+ * @param vp lvalue.
+ * @return 0 if check and vp are equal, -1 if vp value is less than check value, 1 is vp value is more than check
+ *	value, -2 on error.
+ */
+#ifdef HAVE_REGEX
+int radius_compare_vps(REQUEST *request, VALUE_PAIR *check, VALUE_PAIR *vp)
+#else
+int radius_compare_vps(UNUSED REQUEST *request, VALUE_PAIR *check, VALUE_PAIR *vp)
+#endif
+{
+	int ret = 0;
+
+	/*
+	 *      Check for =* and !* and return appropriately
+	 */
+	if (check->op == T_OP_CMP_TRUE)  return 0;
+	if (check->op == T_OP_CMP_FALSE) return 1;
+
+#ifdef HAVE_REGEX
+	if ((check->op == T_OP_REG_EQ) || (check->op == T_OP_REG_NE)) {
+		ssize_t		slen;
+		regex_t		*preg = NULL;
+		regmatch_t	rxmatch[REQUEST_MAX_REGEX + 1];	/* +1 for %{0} (whole match) capture group */
+		size_t		nmatch = sizeof(rxmatch) / sizeof(regmatch_t);
+
+		char *expr = NULL, *value = NULL;
+		char const *expr_p, *value_p;
+
+		if (!vp) return -2;
+
+		if (check->da->type == PW_TYPE_STRING) {
+			expr_p = check->vp_strvalue;
+		} else {
+			expr_p = expr = vp_aprints_value(request, check, '\0');
+		}
+
+		if (vp->da->type == PW_TYPE_STRING) {
+			value_p = vp->vp_strvalue;
+		} else {
+			value_p = value = vp_aprints_value(request, vp, '\0');
+		}
+
+		if (!expr_p || !value_p) {
+			REDEBUG("Error stringifying operand for regular expression");
+
+		regex_error:
+			talloc_free(preg);
+			talloc_free(expr);
+			talloc_free(value);
+			return -2;
+		}
+
+		/*
+		 *	Include substring matches.
+		 */
+		slen = regex_compile(request, &preg, expr_p, talloc_array_length(expr_p) - 1, false, false, true, true);
+		if (slen <= 0) {
+			REMARKER(expr_p, -slen, fr_strerror());
+
+			goto regex_error;
+		}
+
+		slen = regex_exec(preg, value_p, talloc_array_length(value_p) - 1, rxmatch, &nmatch);
+		if (slen < 0) {
+			RERROR("%s", fr_strerror());
+
+			goto regex_error;
+		}
+
+		if (check->op == T_OP_REG_EQ) {
+			/*
+			 *	Add in %{0}. %{1}, etc.
+			 */
+			regex_sub_to_request(request, &preg, value_p, talloc_array_length(value_p) - 1,
+					     rxmatch, nmatch);
+			ret = (slen == 1) ? 0 : -1;
+		} else {
+			ret = (slen != 1) ? 0 : -1;
+		}
+
+		talloc_free(preg);
+		talloc_free(expr);
+		talloc_free(value);
+		goto finish;
+	}
+#endif
+
+	/*
+	 *	Attributes must be of the same type.
+	 *
+	 *	FIXME: deal with type mismatch properly if one side contain
+	 *	ABINARY, OCTETS or STRING by converting the other side to
+	 *	a string
+	 *
+	 */
+	if (vp->da->type != check->da->type) return -1;
+
+	/*
+	 *	Tagged attributes are equal if and only if both the
+	 *	tag AND value match.
+	 */
+	if (check->da->flags.has_tag && !TAG_EQ(check->tag, vp->tag)) {
+		ret = ((int) vp->tag) - ((int) check->tag);
+		if (ret != 0) goto finish;
+	}
+
+	/*
+	 *	Not a regular expression, compare the types.
+	 */
+	switch (check->da->type) {
+#ifdef WITH_ASCEND_BINARY
+		/*
+		 *	Ascend binary attributes can be treated
+		 *	as opaque objects, I guess...
+		 */
+		case PW_TYPE_ABINARY:
+#endif
+		case PW_TYPE_OCTETS:
+			if (vp->vp_length != check->vp_length) {
+				ret = 1; /* NOT equal */
+				break;
+			}
+			ret = memcmp(vp->vp_strvalue, check->vp_strvalue,
+				     vp->vp_length);
+			break;
+
+		case PW_TYPE_STRING:
+			ret = strcmp(vp->vp_strvalue,
+				     check->vp_strvalue);
+			break;
+
+		case PW_TYPE_BYTE:
+			ret = vp->vp_byte - check->vp_byte;
+			break;
+		case PW_TYPE_SHORT:
+			ret = vp->vp_short - check->vp_short;
+			break;
+		case PW_TYPE_INTEGER:
+			ret = vp->vp_integer - check->vp_integer;
+			break;
+
+		case PW_TYPE_INTEGER64:
+			/*
+			 *	Don't want integer overflow!
+			 */
+			if (vp->vp_integer64 < check->vp_integer64) {
+				ret = -1;
+			} else if (vp->vp_integer64 > check->vp_integer64) {
+				ret = +1;
+			} else {
+				ret = 0;
+			}
+			break;
+
+		case PW_TYPE_SIGNED:
+			if (vp->vp_signed < check->vp_signed) {
+				ret = -1;
+			} else if (vp->vp_signed > check->vp_signed) {
+				ret = +1;
+			} else {
+				ret = 0;
+			}
+			break;
+
+		case PW_TYPE_DATE:
+			ret = vp->vp_date - check->vp_date;
+			break;
+
+		case PW_TYPE_IPV4_ADDR:
+			ret = ntohl(vp->vp_ipaddr) - ntohl(check->vp_ipaddr);
+			break;
+
+		case PW_TYPE_IPV6_ADDR:
+			ret = memcmp(&vp->vp_ipv6addr, &check->vp_ipv6addr, sizeof(vp->vp_ipv6addr));
+			break;
+
+		case PW_TYPE_IPV4_PREFIX:
+		case PW_TYPE_IPV6_PREFIX:
+			ret = fr_pair_cmp_op(check->op, vp, check);
+			if (ret == -1) return -2;   // error
+			if (check->op == T_OP_LT || check->op == T_OP_LE)
+				ret = (ret == 1) ? -1 : 1;
+			else if (check->op == T_OP_GT || check->op == T_OP_GE)
+				ret = (ret == 1) ? 1 : -1;
+			else if (check->op == T_OP_CMP_EQ)
+				ret = (ret == 1) ? 0 : -1;
+			break;
+
+		case PW_TYPE_IFID:
+			ret = memcmp(vp->vp_ifid, check->vp_ifid, sizeof(vp->vp_ifid));
+			break;
+
+		default:
+			break;
+	}
+
+finish:
+	if (ret > 0) return 1;
+	if (ret < 0) return -1;
+	return 0;
+}
+
+
+/** Compare check and vp. May call the attribute comparison function.
+ *
+ * Unlike radius_compare_vps() this function will call any attribute-specific
+ * comparison functions registered.
+ *
+ * @param request Current request.
+ * @param req list pairs.
+ * @param check item to compare.
+ * @param check_pairs list.
+ * @param reply_pairs list.
+ * @return 0 if check and vp are equal, -1 if vp value is less than check value, 1 is vp value is more than check
+ *	value.
+ */
+int radius_callback_compare(REQUEST *request, VALUE_PAIR *req,
+			    VALUE_PAIR *check, VALUE_PAIR *check_pairs,
+			    VALUE_PAIR **reply_pairs)
+{
+	struct cmp *c;
+
+	/*
+	 *      Check for =* and !* and return appropriately
+	 */
+	if (check->op == T_OP_CMP_TRUE)  return 0;
+	if (check->op == T_OP_CMP_FALSE) return 1;
+
+	/*
+	 *	See if there is a special compare function.
+	 *
+	 *	FIXME: use new RB-Tree code.
+	 */
+	for (c = cmp; c; c = c->next) {
+		if (c->attribute == check->da) {
+			return (c->compare)(c->instance, request, req, check,
+				check_pairs, reply_pairs);
+		}
+	}
+
+	if (!req) return -1; /* doesn't exist, don't compare it */
+
+	return radius_compare_vps(request, check, req);
+}
+
+
+/** Find a comparison function for two attributes.
+ *
+ * @todo this should probably take DA's.
+ * @param attribute to find comparison function for.
+ * @return true if a comparison function was found, else false.
+ */
+int radius_find_compare(DICT_ATTR const *attribute)
+{
+	struct cmp *c;
+
+	for (c = cmp; c; c = c->next) {
+		if (c->attribute == attribute) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+
+/** See what attribute we want to compare with.
+ *
+ * @param attribute to find comparison function for.
+ * @param from reference to compare with
+ * @return true if the comparison callback require a matching attribue in the request, else false.
+ */
+static bool otherattr(DICT_ATTR const *attribute, DICT_ATTR const **from)
+{
+	struct cmp *c;
+
+	for (c = cmp; c; c = c->next) {
+		if (c->attribute == attribute) {
+			*from = c->from;
+			return c->first_only;
+		}
+	}
+
+	*from = attribute;
+	return false;
+}
+
+/** Register a function as compare function.
+ *
+ * @param name the attribute comparison to register
+ * @param from the attribute we want to compare with. Normally this is the same as attribute.
+ *  If null call the comparison function on every attributes in the request if first_only is false
+ * @param first_only will decide if we loop over the request attributes or stop on the first one
+ * @param func comparison function
+ * @param instance argument to comparison function
+ * @return 0
+ */
+int paircompare_register_byname(char const *name, DICT_ATTR const *from,
+				bool first_only, RAD_COMPARE_FUNC func, void *instance)
+{
+	ATTR_FLAGS flags;
+	DICT_ATTR const *da;
+
+	memset(&flags, 0, sizeof(flags));
+	flags.compare = 1;
+
+	da = dict_attrbyname(name);
+	if (da) {
+		if (!da->flags.compare) {
+			fr_strerror_printf("Attribute '%s' already exists.", name);
+			return -1;
+		}
+	} else if (from) {
+		if (dict_addattr(name, -1, 0, from->type, flags) < 0) {
+			fr_strerror_printf("Failed creating attribute '%s'", name);
+			return -1;
+		}
+
+		da = dict_attrbyname(name);
+		if (!da) {
+			fr_strerror_printf("Failed finding attribute '%s'", name);
+			return -1;
+		}
+
+		DEBUG("Creating attribute %s", name);
+	}
+
+	return paircompare_register(da, from, first_only, func, instance);
+}
+
+/** Register a function as compare function.
+ *
+ * @param attribute to register comparison function for.
+ * @param from the attribute we want to compare with. Normally this is the same as attribute.
+ *  If null call the comparison function on every attributes in the request if first_only is false
+ * @param first_only will decide if we loop over the request attributes or stop on the first one
+ * @param func comparison function
+ * @param instance argument to comparison function
+ * @return 0
+ */
+int paircompare_register(DICT_ATTR const *attribute, DICT_ATTR const *from,
+			 bool first_only, RAD_COMPARE_FUNC func, void *instance)
+{
+	struct cmp *c;
+
+	rad_assert(attribute != NULL);
+
+	paircompare_unregister(attribute, func);
+
+	c = rad_malloc(sizeof(struct cmp));
+
+	c->compare   = func;
+	c->attribute = attribute;
+	c->from = from;
+	c->first_only = first_only;
+	c->instance  = instance;
+	c->next      = cmp;
+	cmp = c;
+
+	return 0;
+}
+
+/** Unregister comparison function for an attribute
+ *
+ * @param attribute dict reference to unregister for.
+ * @param func comparison function to remove.
+ */
+void paircompare_unregister(DICT_ATTR const *attribute, RAD_COMPARE_FUNC func)
+{
+	struct cmp *c, *last;
+
+	last = NULL;
+	for (c = cmp; c; c = c->next) {
+		if (c->attribute == attribute && c->compare == func) {
+			break;
+		}
+		last = c;
+	}
+
+	if (c == NULL) return;
+
+	if (last != NULL) {
+		last->next = c->next;
+	} else {
+		cmp = c->next;
+	}
+
+	free(c);
+}
+
+/** Unregister comparison function for a module
+ *
+ *  All paircompare() functions for this module will be unregistered.
+ *
+ * @param instance the module instance
+ */
+void paircompare_unregister_instance(void *instance)
+{
+	struct cmp *c, **tail;
+
+	tail = &cmp;
+	while ((c = *tail) != NULL) {
+		if (c->instance == instance) {
+			*tail = c->next;
+			free(c);
+			continue;
+		}
+
+		tail = &(c->next);
+	}
+}
+
+/** Compare two pair lists except for the password information.
+ *
+ * For every element in "check" at least one matching copy must be present
+ * in "reply".
+ *
+ * @param[in] request Current request.
+ * @param[in] req_list request valuepairs.
+ * @param[in] check Check/control valuepairs.
+ * @param[in,out] rep_list Reply value pairs.
+ *
+ * @return 0 on match.
+ */
+int paircompare(REQUEST *request, VALUE_PAIR *req_list, VALUE_PAIR *check,
+		VALUE_PAIR **rep_list)
+{
+	vp_cursor_t cursor;
+	VALUE_PAIR *check_item;
+	VALUE_PAIR *auth_item = NULL;
+	DICT_ATTR const *from;
+
+	int result = 0;
+	int compare;
+	bool first_only;
+
+	for (check_item = fr_cursor_init(&cursor, &check);
+	     check_item;
+	     check_item = fr_cursor_next(&cursor)) {
+		/*
+		 *	If the user is setting a configuration value,
+		 *	then don't bother comparing it to any attributes
+		 *	sent to us by the user.  It ALWAYS matches.
+		 */
+		if ((check_item->op == T_OP_SET) ||
+		    (check_item->op == T_OP_ADD)) {
+			continue;
+		}
+
+		if (!check_item->da->vendor) switch (check_item->da->attr) {
+		/*
+		 *	Attributes we skip during comparison.
+		 *	These are "server" check items.
+		 */
+		case PW_CRYPT_PASSWORD:
+		case PW_AUTH_TYPE:
+		case PW_AUTZ_TYPE:
+		case PW_ACCT_TYPE:
+		case PW_SESSION_TYPE:
+		case PW_STRIP_USER_NAME:
+			continue;
+
+		/*
+		 *	IF the password attribute exists, THEN
+		 *	we can do comparisons against it.  If not,
+		 *	then the request did NOT contain a
+		 *	User-Password attribute, so we CANNOT do
+		 *	comparisons against it.
+		 *
+		 *	This hack makes CHAP-Password work..
+		 */
+		case PW_USER_PASSWORD:
+			if (check_item->op == T_OP_CMP_EQ) {
+				WARN("Found User-Password == \"...\"");
+				WARN("Are you sure you don't mean Cleartext-Password?");
+				WARN("See \"man rlm_pap\" for more information");
+			}
+			if (fr_pair_find_by_num(req_list, PW_USER_PASSWORD, 0, TAG_ANY) == NULL) {
+				continue;
+			}
+			break;
+		}
+
+		/*
+		 *	See if this item is present in the request.
+		 */
+		first_only = otherattr(check_item->da, &from);
+
+		auth_item = req_list;
+	try_again:
+		if (!first_only) {
+			while (auth_item != NULL) {
+				VERIFY_VP(auth_item);
+				if ((auth_item->da == from) || (!from)) {
+					break;
+				}
+				auth_item = auth_item->next;
+			}
+		}
+
+		/*
+		 *	Not found, it's not a match.
+		 */
+		if (auth_item == NULL) {
+			/*
+			 *	Didn't find it.  If we were *trying*
+			 *	to not find it, then we succeeded.
+			 */
+			if (check_item->op == T_OP_CMP_FALSE) {
+				continue;
+			} else {
+				return -1;
+			}
+		}
+
+		/*
+		 *	Else we found it, but we were trying to not
+		 *	find it, so we failed.
+		 */
+		if (check_item->op == T_OP_CMP_FALSE) {
+			return -1;
+		}
+
+		/*
+		 *	We've got to xlat the string before doing
+		 *	the comparison.
+		 */
+		radius_xlat_do(request, check_item);
+
+		/*
+		 *	OK it is present now compare them.
+		 */
+		compare = radius_callback_compare(request, auth_item,
+						  check_item, check, rep_list);
+
+		switch (check_item->op) {
+		case T_OP_EQ:
+		default:
+			RWDEBUG("Invalid operator '%s' for item %s: reverting to '=='",
+				fr_int2str(fr_tokens, check_item->op, "<INVALID>"), check_item->da->name);
+			/* FALL-THROUGH */
+		case T_OP_CMP_TRUE:
+		case T_OP_CMP_FALSE:
+		case T_OP_CMP_EQ:
+			if (compare != 0) result = -1;
+			break;
+
+		case T_OP_NE:
+			if (compare == 0) result = -1;
+			break;
+
+		case T_OP_LT:
+			if (compare >= 0) result = -1;
+			break;
+
+		case T_OP_GT:
+			if (compare <= 0) result = -1;
+			break;
+
+		case T_OP_LE:
+			if (compare > 0) result = -1;
+			break;
+
+		case T_OP_GE:
+			if (compare < 0) result = -1;
+			break;
+
+#ifdef HAVE_REGEX
+		case T_OP_REG_EQ:
+		case T_OP_REG_NE:
+			if (compare != 0) result = -1;
+			break;
+#endif
+		} /* switch over the operator of the check item */
+
+		/*
+		 *	This attribute didn't match, but maybe there's
+		 *	another of the same attribute, which DOES match.
+		 */
+		if ((result != 0) && (!first_only)) {
+			fr_assert(auth_item != NULL);
+			auth_item = auth_item->next;
+			result = 0;
+			goto try_again;
+		}
+
+	} /* for every entry in the check item list */
+
+	return result;
+}
+
+/** Expands an attribute marked with fr_pair_mark_xlat
+ *
+ * Writes the new value to the vp.
+ *
+ * @param request Current request.
+ * @param vp to expand.
+ * @return 0 if successful else -1 (on xlat failure) or -2 (on parse failure).
+ *	On failure pair will still no longer be marked for xlat expansion.
+ */
+int radius_xlat_do(REQUEST *request, VALUE_PAIR *vp)
+{
+	ssize_t slen;
+
+	char *expanded = NULL;
+	if (vp->type != VT_XLAT) return 0;
+
+	vp->type = VT_DATA;
+
+	slen = radius_axlat(&expanded, request, vp->value.xlat, NULL, NULL);
+	rad_const_free(vp->value.xlat);
+	vp->value.xlat = NULL;
+	if (slen < 0) {
+		return -1;
+	}
+
+	/*
+	 *	Parse the string into a new value.
+	 *
+	 *	If the VALUE_PAIR is being used in a regular expression
+	 *	then we just want to copy the new value in unmolested.
+	 */
+	if ((vp->op == T_OP_REG_EQ) || (vp->op == T_OP_REG_NE)) {
+		fr_pair_value_strsteal(vp, expanded);
+		return 0;
+	}
+
+	if (fr_pair_value_from_str(vp, expanded, -1) < 0){
+		talloc_free(expanded);
+		return -2;
+	}
+
+	talloc_free(expanded);
+
+	return 0;
+}
+
+/** Create a VALUE_PAIR and add it to a list of VALUE_PAIR s
+ *
+ * @note This function ALWAYS returns. If we're OOM, then it causes the
+ * @note server to exit, so you don't need to check the return value.
+ *
+ * @param[in] ctx for talloc
+ * @param[out] vps List to add new VALUE_PAIR to, if NULL will just
+ *	return VALUE_PAIR.
+ * @param[in] attribute number.
+ * @param[in] vendor number.
+ * @return a new VLAUE_PAIR or causes server to exit on error.
+ */
+VALUE_PAIR *radius_pair_create(TALLOC_CTX *ctx, VALUE_PAIR **vps,
+			      unsigned int attribute, unsigned int vendor)
+{
+	VALUE_PAIR *vp;
+
+	vp = fr_pair_afrom_num(ctx, attribute, vendor);
+	if (!vp) {
+		ERROR("No memory!");
+		rad_assert("No memory" == NULL);
+		fr_exit_now(1);
+	}
+
+	if (vps) fr_pair_add(vps, vp);
+
+	return vp;
+}
+
+/** Print a single valuepair to stderr or error log.
+ *
+ * @param[in] vp list to print.
+ */
+void debug_pair(VALUE_PAIR *vp)
+{
+	if (!vp || !rad_debug_lvl || !fr_log_fp) return;
+
+	vp_print(fr_log_fp, vp);
+}
+
+/** Print a single valuepair to stderr or error log.
+ *
+ * @param[in] level Debug level (1-4).
+ * @param[in] request to read logging params from.
+ * @param[in] vp to print.
+ * @param[in] prefix (optional).
+ */
+void rdebug_pair(log_lvl_t level, REQUEST *request, VALUE_PAIR *vp, char const *prefix)
+{
+	char buffer[768];
+	if (!vp || !request || !request->log.func) return;
+
+	if (!radlog_debug_enabled(L_DBG, level, request)) return;
+
+	if (vp->da->flags.secret && request->root && request->root->suppress_secrets && (rad_debug_lvl < 3)) {
+		RDEBUGX(level, "%s%s = <<< secret >>>", prefix ? prefix : "", vp->da->name);
+		return;
+	}
+
+	vp_prints(buffer, sizeof(buffer), vp);
+	RDEBUGX(level, "%s%s", prefix ? prefix : "",  buffer);
+}
+
+/** Print a list of VALUE_PAIRs.
+ *
+ * @param[in] level Debug level (1-4).
+ * @param[in] request to read logging params from.
+ * @param[in] vp to print.
+ * @param[in] prefix (optional).
+ */
+void rdebug_pair_list(log_lvl_t level, REQUEST *request, VALUE_PAIR *vp, char const *prefix)
+{
+	vp_cursor_t cursor;
+	char buffer[768];
+	if (!vp || !request || !request->log.func) return;
+
+	if (!radlog_debug_enabled(L_DBG, level, request)) return;
+
+	RINDENT();
+	for (vp = fr_cursor_init(&cursor, &vp);
+	     vp;
+	     vp = fr_cursor_next(&cursor)) {
+		VERIFY_VP(vp);
+
+		if (vp->da->flags.secret && request->root && request->root->suppress_secrets && (rad_debug_lvl < 3)) {
+			RDEBUGX(level, "%s%s = <<< secret >>>", prefix ? prefix : "", vp->da->name);
+			continue;
+		}
+
+		vp_prints(buffer, sizeof(buffer), vp);
+		RDEBUGX(level, "%s%s", prefix ? prefix : "",  buffer);
+	}
+	REXDENT();
+}
+
+/** Print a list of protocol VALUE_PAIRs.
+ *
+ * @param[in] level Debug level (1-4).
+ * @param[in] request to read logging params from.
+ * @param[in] vp to print.
+ */
+void rdebug_proto_pair_list(log_lvl_t level, REQUEST *request, VALUE_PAIR *vp)
+{
+	vp_cursor_t cursor;
+	char buffer[768];
+	if (!vp || !request || !request->log.func) return;
+
+	if (!radlog_debug_enabled(L_DBG, level, request)) return;
+
+	RINDENT();
+	for (vp = fr_cursor_init(&cursor, &vp);
+	     vp;
+	     vp = fr_cursor_next(&cursor)) {
+		VERIFY_VP(vp);
+		if ((vp->da->vendor == 0) &&
+		    ((vp->da->attr & 0xFFFF) > 0xff)) continue;
+
+		if (vp->da->flags.secret && request->root && request->root->suppress_secrets && (rad_debug_lvl < 3)) {
+			RDEBUGX(level, "%s = <<< secret >>>", vp->da->name);
+			continue;
+		}
+
+		vp_prints(buffer, sizeof(buffer), vp);
+		RDEBUGX(level, "%s", buffer);
+	}
+	REXDENT();
+}
+
+/** Return a VP from the specified request.
+ *
+ * @param out where to write the pointer to the resolved VP.
+ *	Will be NULL if the attribute couldn't be resolved.
+ * @param request current request.
+ * @param name attribute name including qualifiers.
+ * @return -4 if either the attribute or qualifier were invalid, and the same error codes as tmpl_find_vp for other
+ *	error conditions.
+ */
+int radius_get_vp(VALUE_PAIR **out, REQUEST *request, char const *name)
+{
+	int rcode;
+	vp_tmpl_t vpt;
+
+	*out = NULL;
+
+	if (tmpl_from_attr_str(&vpt, name, REQUEST_CURRENT, PAIR_LIST_REQUEST, false, false) <= 0) {
+		return -4;
+	}
+
+	rcode = tmpl_find_vp(out, request, &vpt);
+
+	return rcode;
+}
+
+/** Copy VP(s) from the specified request.
+ *
+ * @param ctx to alloc new VALUE_PAIRs in.
+ * @param out where to write the pointer to the copied VP.
+ *	Will be NULL if the attribute couldn't be resolved.
+ * @param request current request.
+ * @param name attribute name including qualifiers.
+ * @return -4 if either the attribute or qualifier were invalid, and the same error codes as tmpl_find_vp for other
+ *	error conditions.
+ */
+int radius_copy_vp(TALLOC_CTX *ctx, VALUE_PAIR **out, REQUEST *request, char const *name)
+{
+	int rcode;
+	vp_tmpl_t vpt;
+
+	*out = NULL;
+
+	if (tmpl_from_attr_str(&vpt, name, REQUEST_CURRENT, PAIR_LIST_REQUEST, false, false) <= 0) {
+		return -4;
+	}
+
+	rcode = tmpl_copy_vps(ctx, out, request, &vpt);
+
+	return rcode;
+}
+
+void module_failure_msg(REQUEST *request, char const *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	vmodule_failure_msg(request, fmt, ap);
+	va_end(ap);
+}
+
+/** Add a module failure message VALUE_PAIR to the request
+ */
+void vmodule_failure_msg(REQUEST *request, char const *fmt, va_list ap)
+{
+	char *p;
+	VALUE_PAIR *vp;
+	va_list aq;
+
+	if (!fmt || !request || !request->packet) {
+		return;
+	}
+
+	/*
+	 *  If we don't copy the original ap we get a segfault from vasprintf. This is apparently
+	 *  due to ap sometimes being implemented with a stack offset which is invalidated if
+	 *  ap is passed into another function. See here:
+	 *  http://julipedia.meroh.net/2011/09/using-vacopy-to-safely-pass-ap.html
+	 *
+	 *  I don't buy that explanation, but doing a va_copy here does prevent SEGVs seen when
+	 *  running unit tests which generate errors under CI.
+	 */
+	va_copy(aq, ap);
+	p = talloc_vasprintf(request, fmt, aq);
+	va_end(aq);
+
+	MEM(vp = pair_make_request("Module-Failure-Message", NULL, T_OP_ADD));
+	if (request->module && (request->module[0] != '\0')) {
+		fr_pair_value_sprintf(vp, "%s: %s", request->module, p);
+	} else {
+		fr_pair_value_sprintf(vp, "%s", p);
+	}
+	talloc_free(p);
+}
diff --git a/src/main/parser.c b/src/main/parser.c
new file mode 100644
index 0000000..e337b94
--- /dev/null
+++ b/src/main/parser.c
@@ -0,0 +1,1809 @@
+/*
+ * parser.c	Parse various things
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2013  Alan DeKok <aland@freeradius.org>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/parser.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <ctype.h>
+
+#define PW_CAST_BASE (1850)
+
+static const FR_NAME_NUMBER allowed_return_codes[] = {
+	{ "reject",     1 },
+	{ "fail",       1 },
+	{ "ok",	 	1 },
+	{ "handled",    1 },
+	{ "invalid",    1 },
+	{ "userlock",   1 },
+	{ "notfound",   1 },
+	{ "noop",       1 },
+	{ "updated",    1 },
+	{ NULL, 0 }
+};
+
+/*
+ *	This file shouldn't use any functions from the server core.
+ */
+
+size_t fr_cond_sprint(char *buffer, size_t bufsize, fr_cond_t const *in)
+{
+	size_t len;
+	char *p = buffer;
+	char *end = buffer + bufsize - 1;
+	fr_cond_t const *c = in;
+
+	rad_assert(bufsize > 0);
+
+next:
+	rad_assert(p < end);
+
+	if (!c) {
+		p[0] = '\0';
+		return 0;
+	}
+
+	/*
+	 *	Don't overflow the output buffer.
+	 */
+	if ((end - p) < 2) {
+		p[0] = '\0';
+		return 0;
+	}
+
+	if (c->negate) {
+		*(p++) = '!';	/* FIXME: only allow for child? */
+	}
+
+	switch (c->type) {
+	case COND_TYPE_EXISTS:
+		rad_assert(c->data.vpt != NULL);
+		if (c->cast) {
+			snprintf(p, end - p, "<%s>", fr_int2str(dict_attr_types,
+								c->cast->type, "??"));
+			p += strlen(p);
+		}
+
+		len = tmpl_prints(p, end - p, c->data.vpt, NULL);
+		p += len;
+		break;
+
+	case COND_TYPE_MAP:
+		rad_assert(c->data.map != NULL);
+#if 0
+		*(p++) = '[';	/* for extra-clear debugging */
+#endif
+		if (c->cast) {
+			snprintf(p, end - p, "<%s>", fr_int2str(dict_attr_types,
+								c->cast->type, "??"));
+			p += strlen(p);
+		}
+
+		len = map_prints(p, end - p, c->data.map);
+		p += len;
+#if 0
+		*(p++) = ']';
+#endif
+		break;
+
+	case COND_TYPE_CHILD:
+		rad_assert(c->data.child != NULL);
+		*(p++) = '(';
+		len = fr_cond_sprint(p, end - p, c->data.child);
+		p += len;
+		*(p++) = ')';
+		break;
+
+	case COND_TYPE_TRUE:
+		strlcpy(buffer, "true", bufsize);
+		return strlen(buffer);
+
+	case COND_TYPE_FALSE:
+		strlcpy(buffer, "false", bufsize);
+		return strlen(buffer);
+
+	default:
+		*buffer = '\0';
+		return 0;
+	}
+
+	if (c->next_op == COND_NONE) {
+		rad_assert(c->next == NULL);
+		*p = '\0';
+		return p - buffer;
+	}
+
+	if (c->next_op == COND_AND) {
+		strlcpy(p, " && ", end - p);
+		p += strlen(p);
+
+	} else if (c->next_op == COND_OR) {
+		strlcpy(p, " || ", end - p);
+		p += strlen(p);
+
+	} else {
+		rad_assert(0 == 1);
+	}
+
+	c = c->next;
+	goto next;
+}
+
+
+static ssize_t condition_tokenize_string(TALLOC_CTX *ctx, char **out,  char const **error, char const *start,
+					 FR_TOKEN *op)
+{
+	char const *p = start;
+	char *q;
+
+	switch (*p++) {
+	default:
+		return -1;
+
+	case '"':
+		*op = T_DOUBLE_QUOTED_STRING;
+		break;
+
+	case '\'':
+		*op = T_SINGLE_QUOTED_STRING;
+		break;
+
+	case '`':
+		*op = T_BACK_QUOTED_STRING;
+		break;
+
+	case '/':
+		*op = T_OP_REG_EQ; /* a bit of a hack. */
+		break;
+
+	}
+
+	*out = talloc_array(ctx, char, strlen(start) - 1); /* + 2 - 1 */
+	if (!*out) return -1;
+
+	q = *out;
+
+	while (*p) {
+		if (*p == *start) {
+			/*
+			 *	Call the STANDARD parse function to figure out what the string is.
+			 */
+			if (cf_new_escape) {
+				ssize_t slen;
+				value_data_t data;
+				char quote = *start;
+				PW_TYPE src_type = PW_TYPE_STRING;
+
+				/*
+				 *	Regex compilers can handle escapes.  So we don't do it.
+				 */
+				if (quote == '/') quote = '\0';
+
+				slen = value_data_from_str(ctx, &data, &src_type, NULL, start + 1, p - (start + 1), quote);
+				if (slen < 0) {
+					*error = "error parsing string";
+					return slen - 1;
+				}
+
+				talloc_free(*out);
+				*out = talloc_steal(ctx, data.ptr);
+				data.strvalue = NULL;
+			} else {
+				char *out2;
+
+				*(q++) = '\0'; /* terminate the output string */
+
+				out2 = talloc_realloc(ctx, *out, char, (q - *out));
+				if (!out2) {
+					*error = "Out of memory";
+					return -1;
+				}
+				*out = out2;
+			}
+
+			p++;
+			return (p - start);
+		}
+
+		if (*p == '\\') {
+			if (!p[1]) {
+				p++;
+				*error = "End of string after escape";
+				return -(p - start);
+			}
+
+			/*
+			 *	Hacks for backwards compatibility
+			 */
+			if (cf_new_escape) {
+				if (p[1] == start[0]) { /* Convert '\'' --> ' */
+					p++;
+				} else {
+					*(q++) = *(p++);
+				}
+
+			} else {
+				switch (p[1]) {
+				case 'r':
+					*q++ = '\r';
+					break;
+				case 'n':
+					*q++ = '\n';
+					break;
+				case 't':
+					*q++ = '\t';
+					break;
+				default:
+					*q++ = p[1];
+					break;
+				}
+				p += 2;
+				continue;
+			}
+
+		}
+		*(q++) = *(p++);
+	}
+
+	*error = "Unterminated string";
+	return -1;
+}
+
+static ssize_t condition_tokenize_word(TALLOC_CTX *ctx, char const *start, char **out,
+				       FR_TOKEN *op, char const **error)
+{
+	size_t len;
+	char const *p = start;
+
+	if ((*p == '"') || (*p == '\'') || (*p == '`') || (*p == '/')) {
+		return condition_tokenize_string(ctx, out, error, start, op);
+	}
+
+	*op = T_BARE_WORD;
+	if (*p == '&') p++;	/* special-case &User-Name */
+
+	while (*p) {
+		/*
+		 *	The LHS should really be limited to only a few
+		 *	things.  For now, we allow pretty much anything.
+		 */
+		if (*p == '\\') {
+			*error = "Unexpected escape";
+			return -(p - start);
+		}
+
+		/*
+		 *	("foo") is valid.
+		 */
+		if (*p == ')') {
+			break;
+		}
+
+		/*
+		 *	Spaces or special characters delineate the word
+		 */
+		if (isspace((uint8_t) *p) || (*p == '&') || (*p == '|') ||
+		    (*p == '!') || (*p == '=') || (*p == '<') || (*p == '>')) {
+			break;
+		}
+
+		if ((*p == '"') || (*p == '\'') || (*p == '`')) {
+			*error = "Unexpected start of string";
+			return -(p - start);
+		}
+
+		p++;
+	}
+
+	len = p - start;
+	if (!len) {
+		*error = "Empty string is invalid";
+		return 0;
+	}
+
+	*out = talloc_array(ctx, char, len + 1);
+	memcpy(*out, start, len);
+	(*out)[len] = '\0';
+	return len;
+}
+
+
+static ssize_t condition_tokenize_cast(char const *start, DICT_ATTR const **pda, char const **error)
+{
+	char const *p = start;
+	char const *q;
+	PW_TYPE cast;
+
+	while (isspace((uint8_t) *p)) p++; /* skip spaces before condition */
+
+	if (*p != '<') return 0;
+	p++;
+
+	q = p;
+	while (*q && *q != '>') q++;
+
+	cast = fr_substr2int(dict_attr_types, p, PW_TYPE_INVALID, q - p);
+	if (cast == PW_TYPE_INVALID) {
+		*error = "Invalid data type in cast";
+		return -(p - start);
+	}
+
+	/*
+	 *	We can only cast to basic data types.  Complex ones
+	 *	are forbidden.
+	 */
+	switch (cast) {
+#ifdef WITH_ASCEND_BINARY
+	case PW_TYPE_ABINARY:
+#endif
+	case PW_TYPE_COMBO_IP_ADDR:
+	case PW_TYPE_TLV:
+	case PW_TYPE_EXTENDED:
+	case PW_TYPE_LONG_EXTENDED:
+	case PW_TYPE_EVS:
+	case PW_TYPE_VSA:
+		*error = "Forbidden data type in cast";
+		return -(p - start);
+
+	default:
+		break;
+	}
+
+	*pda = dict_attrbyvalue(PW_CAST_BASE + cast, 0);
+	if (!*pda) {
+		*error = "Cannot cast to this data type";
+		return -(p - start);
+	}
+
+	q++;
+
+	while (isspace((uint8_t) *q)) q++; /* skip spaces after cast */
+
+	return q - start;
+}
+
+static bool condition_check_types(fr_cond_t *c, PW_TYPE lhs_type)
+{
+	/*
+	 *	SOME integer mismatch is OK.  If the LHS has a large type,
+	 *	and the RHS has a small type, it's OK.
+	 *
+	 *	If the LHS has a small type, and the RHS has a large type,
+	 *	then add a cast to the LHS.
+	 */
+	if (lhs_type == PW_TYPE_INTEGER64) {
+		if ((c->data.map->rhs->tmpl_da->type == PW_TYPE_INTEGER) ||
+		    (c->data.map->rhs->tmpl_da->type == PW_TYPE_SHORT) ||
+		    (c->data.map->rhs->tmpl_da->type == PW_TYPE_BYTE)) {
+			c->cast = NULL;
+			return true;
+		}
+	}
+
+	if (lhs_type == PW_TYPE_INTEGER) {
+		if ((c->data.map->rhs->tmpl_da->type == PW_TYPE_SHORT) ||
+		    (c->data.map->rhs->tmpl_da->type == PW_TYPE_BYTE)) {
+			c->cast = NULL;
+			return true;
+		}
+
+		if (c->data.map->rhs->tmpl_da->type == PW_TYPE_INTEGER64) {
+			c->cast = c->data.map->rhs->tmpl_da;
+			return true;
+		}
+	}
+
+	if (lhs_type == PW_TYPE_SHORT) {
+		if (c->data.map->rhs->tmpl_da->type == PW_TYPE_BYTE) {
+			c->cast = NULL;
+			return true;
+		}
+
+		if ((c->data.map->rhs->tmpl_da->type == PW_TYPE_INTEGER64) ||
+		    (c->data.map->rhs->tmpl_da->type == PW_TYPE_INTEGER)) {
+			c->cast = c->data.map->rhs->tmpl_da;
+			return true;
+		}
+	}
+
+	if (lhs_type == PW_TYPE_BYTE) {
+		if ((c->data.map->rhs->tmpl_da->type == PW_TYPE_INTEGER64) ||
+		    (c->data.map->rhs->tmpl_da->type == PW_TYPE_INTEGER) ||
+		    (c->data.map->rhs->tmpl_da->type == PW_TYPE_SHORT)) {
+			c->cast = c->data.map->rhs->tmpl_da;
+			return true;
+		}
+	}
+
+	if ((lhs_type == PW_TYPE_IPV4_PREFIX) &&
+	    (c->data.map->rhs->tmpl_da->type == PW_TYPE_IPV4_ADDR)) {
+		return true;
+	}
+
+	if ((lhs_type == PW_TYPE_IPV6_PREFIX) &&
+	    (c->data.map->rhs->tmpl_da->type == PW_TYPE_IPV6_ADDR)) {
+		return true;
+	}
+
+	/*
+	 *	Same checks as above, but with the types swapped, and
+	 *	with explicit cast for the interpretor.
+	 */
+	if ((lhs_type == PW_TYPE_IPV4_ADDR) &&
+	    (c->data.map->rhs->tmpl_da->type == PW_TYPE_IPV4_PREFIX)) {
+		c->cast = c->data.map->rhs->tmpl_da;
+		return true;
+	}
+
+	if ((lhs_type == PW_TYPE_IPV6_ADDR) &&
+	    (c->data.map->rhs->tmpl_da->type == PW_TYPE_IPV6_PREFIX)) {
+		c->cast = c->data.map->rhs->tmpl_da;
+		return true;
+	}
+
+	return false;
+}
+
+
+/*
+ *	Less code means less bugs
+ */
+#define return_P(_x) *error = _x;goto return_p
+#define return_0(_x) *error = _x;goto return_0
+#define return_lhs(_x) *error = _x;goto return_lhs
+#define return_rhs(_x) *error = _x;goto return_rhs
+#define return_SLEN goto return_slen
+
+
+/** Tokenize a conditional check
+ *
+ *  @param[in] ctx for talloc
+ *  @param[in] ci for CONF_ITEM
+ *  @param[in] start the start of the string to process.  Should be "(..."
+ *  @param[in] brace look for a closing brace
+ *  @param[out] pcond pointer to the returned condition structure
+ *  @param[out] error the parse error (if any)
+ *  @param[in] flags do one/two pass
+ *  @return length of the string skipped, or when negative, the offset to the offending error
+ */
+static ssize_t condition_tokenize(TALLOC_CTX *ctx, CONF_ITEM *ci, char const *start, bool brace,
+				  fr_cond_t **pcond, char const **error, int flags)
+{
+	ssize_t slen, tlen;
+	char const *p = start;
+	char const *lhs_p, *rhs_p;
+	fr_cond_t *c;
+	char *lhs, *rhs;
+	FR_TOKEN op, lhs_type, rhs_type;
+
+	c = talloc_zero(ctx, fr_cond_t);
+
+	rad_assert(c != NULL);
+	lhs = rhs = NULL;
+	lhs_type = rhs_type = T_INVALID;
+
+	while (isspace((uint8_t) *p)) p++; /* skip spaces before condition */
+
+	if (!*p) {
+		return_P("Empty condition is invalid");
+	}
+
+	/*
+	 *	!COND
+	 */
+	if (*p == '!') {
+		p++;
+		c->negate = true;
+		while (isspace((uint8_t) *p)) p++; /* skip spaces after negation */
+
+		/*
+		 *  Just for stupidity
+		 */
+		if (*p == '!') {
+			return_P("Double negation is invalid");
+		}
+	}
+
+	/*
+	 *	(COND)
+	 */
+	if (*p == '(') {
+		p++;
+
+		/*
+		 *	We've already eaten one layer of
+		 *	brackets.  Go recurse to get more.
+		 */
+		c->type = COND_TYPE_CHILD;
+		c->ci = ci;
+		slen = condition_tokenize(c, ci, p, true, &c->data.child, error, flags);
+		if (slen <= 0) {
+			return_SLEN;
+		}
+
+		if (!c->data.child) {
+			return_P("Empty condition is invalid");
+		}
+
+		p += slen;
+		while (isspace((uint8_t) *p)) p++; /* skip spaces after (COND)*/
+
+	} else { /* it's a bare FOO==BAR */
+		/*
+		 *	We didn't see anything special.  The condition must be one of
+		 *
+		 *	FOO
+		 *	FOO OP BAR
+		 */
+
+		/*
+		 *	Grab the LHS
+		 */
+		if (*p == '/') {
+			return_P("Conditional check cannot begin with a regular expression");
+		}
+
+		slen = condition_tokenize_cast(p, &c->cast, error);
+		if (slen < 0) {
+			return_SLEN;
+		}
+		p += slen;
+
+#ifndef __clang_analyzer__
+		lhs_p = p;
+#endif
+		slen = condition_tokenize_word(c, p, &lhs, &lhs_type, error);
+		if (slen <= 0) {
+			return_SLEN;
+		}
+		p += slen;
+
+
+#ifdef __clang_analyzer__
+		if (!lhs) return_P("Internal error");
+#endif
+
+		/*
+		 *	If the LHS is 0xabcdef... automatically cast it to octets
+		 */
+		if (!c->cast && (lhs_type == T_BARE_WORD) &&
+		    (lhs[0] == '0') && (lhs[1] == 'x') &&
+		    ((slen & 0x01) == 0)) {
+			if (slen == 2) {
+				return_P("Empty octet string is invalid");
+			}
+
+			c->cast = dict_attrbyvalue(PW_CAST_BASE + PW_TYPE_OCTETS, 0);
+		}
+
+		while (isspace((uint8_t)*p)) p++; /* skip spaces after LHS */
+
+		/*
+		 *	We may (or not) have an operator
+		 */
+
+
+		/*
+		 *	(FOO)
+		 */
+		if (*p == ')') {
+			/*
+			 *	don't skip the brace.  We'll look for it later.
+			 */
+			goto exists;
+
+			/*
+			 *	FOO
+			 */
+		} else if (!*p) {
+			if (brace) {
+				return_P("No closing brace at end of string");
+			}
+
+			goto exists;
+
+			/*
+			 *	FOO && ...
+			 */
+		} else if (((p[0] == '&') && (p[1] == '&')) ||
+			   ((p[0] == '|') && (p[1] == '|'))) {
+
+		exists:
+			if (c->cast) {
+				return_0("Cannot do cast for existence check");
+			}
+
+			c->type = COND_TYPE_EXISTS;
+			c->ci = ci;
+
+			tlen = tmpl_afrom_str(c, &c->data.vpt, lhs, talloc_array_length(lhs) - 1,
+					      lhs_type, REQUEST_CURRENT, PAIR_LIST_REQUEST, false);
+			if (tlen < 0) {
+				p = lhs_p - tlen;
+				return_P(fr_strerror());
+			}
+
+			rad_assert(c->data.vpt->type != TMPL_TYPE_REGEX);
+
+			if (c->data.vpt->type == TMPL_TYPE_ATTR_UNDEFINED) {
+				c->pass2_fixup = PASS2_FIXUP_ATTR;
+			}
+
+		} else { /* it's an operator */
+#ifdef HAVE_REGEX
+			bool regex = false;
+			bool iflag = false;
+			bool mflag = false;
+#endif
+			vp_map_t *map;
+
+			/*
+			 *	The next thing should now be a comparison operator.
+			 */
+			c->type = COND_TYPE_MAP;
+			c->ci = ci;
+
+			switch (*p) {
+			default:
+				return_P("Invalid text. Expected comparison operator");
+
+			case '!':
+				if (p[1] == '=') {
+					op = T_OP_NE;
+					p += 2;
+
+#ifdef HAVE_REGEX
+				} else if (p[1] == '~') {
+					regex = true;
+
+					op = T_OP_REG_NE;
+					p += 2;
+#endif
+
+				} else if (p[1] == '*') {
+					if (lhs_type != T_BARE_WORD) {
+						return_P("Cannot use !* on a string");
+					}
+
+					op = T_OP_CMP_FALSE;
+					p += 2;
+
+				} else {
+					goto invalid_operator;
+				}
+				break;
+
+			case '=':
+				if (p[1] == '=') {
+					op = T_OP_CMP_EQ;
+					p += 2;
+
+#ifdef HAVE_REGEX
+				} else if (p[1] == '~') {
+					regex = true;
+
+					op = T_OP_REG_EQ;
+					p += 2;
+#endif
+
+				} else if (p[1] == '*') {
+					if (lhs_type != T_BARE_WORD) {
+						return_P("Cannot use =* on a string");
+					}
+
+					op = T_OP_CMP_TRUE;
+					p += 2;
+
+				} else {
+				invalid_operator:
+					return_P("Invalid operator");
+				}
+
+				break;
+
+			case '<':
+				if (p[1] == '=') {
+					op = T_OP_LE;
+					p += 2;
+
+				} else {
+					op = T_OP_LT;
+					p++;
+				}
+				break;
+
+			case '>':
+				if (p[1] == '=') {
+					op = T_OP_GE;
+					p += 2;
+
+				} else {
+					op = T_OP_GT;
+					p++;
+				}
+				break;
+			}
+
+			while (isspace((uint8_t) *p)) p++; /* skip spaces after operator */
+
+			if (!*p) {
+				return_P("Expected text after operator");
+			}
+
+			/*
+			 *	Cannot have a cast on the RHS.
+			 *	But produce good errors, too.
+			 */
+			if (*p == '<') {
+				DICT_ATTR const *cast_da;
+
+				slen = condition_tokenize_cast(p, &cast_da, error);
+				if (slen < 0) {
+					return_SLEN;
+				}
+
+#ifdef __clang_analyzer__
+				if (!cast_da) return_P("Internal error");
+#endif
+
+				if (!c->cast) {
+					return_P("Unexpected cast");
+				}
+
+				if (c->cast != cast_da) {
+					return_P("Cannot cast to a different data type");
+				}
+
+				return_P("Unnecessary cast");
+			}
+
+			/*
+			 *	Grab the RHS
+			 */
+			rhs_p = p;
+			slen = condition_tokenize_word(c, p, &rhs, &rhs_type, error);
+			if (slen <= 0) {
+				return_SLEN;
+			}
+
+#ifdef HAVE_REGEX
+			/*
+			 *	Sanity checks for regexes.
+			 */
+			if (regex) {
+				if (*p != '/') {
+					return_P("Expected regular expression");
+				}
+				for (;;) {
+					switch (p[slen]) {
+					/*
+					 *	/foo/i
+					 */
+					case 'i':
+						iflag = true;
+						slen++;
+						continue;
+
+					/*
+					 *	/foo/m
+					 */
+					case 'm':
+						mflag = true;
+						slen++;
+						continue;
+
+					default:
+						break;
+					}
+					break;
+				}
+			} else if (!regex && (*p == '/')) {
+				return_P("Unexpected regular expression");
+			}
+
+#endif
+			/*
+			 *	Duplicate map_from_fields here, as we
+			 *	want to separate parse errors in the
+			 *	LHS from ones in the RHS.
+			 */
+			c->data.map = map = talloc_zero(c, vp_map_t);
+
+			tlen = tmpl_afrom_str(map, &map->lhs, lhs, talloc_array_length(lhs) - 1,
+					      lhs_type, REQUEST_CURRENT, PAIR_LIST_REQUEST, false);
+			if (tlen < 0) {
+				p = lhs_p - tlen;
+				return_P(fr_strerror());
+			}
+
+			if (tmpl_define_unknown_attr(map->lhs) < 0) {
+				return_lhs("Failed defining attribute");
+			return_lhs:
+				if (lhs) talloc_free(lhs);
+				if (rhs) talloc_free(rhs);
+				talloc_free(c);
+				return -(lhs_p - start);
+			}
+
+			map->op = op;
+
+			/*
+			 *	If the RHS is 0xabcdef... automatically cast it to octets
+			 *	unless the LHS is an attribute of type octets, or an
+			 *	integer type.
+			 */
+			if (!c->cast && (rhs_type == T_BARE_WORD) &&
+			    (rhs[0] == '0') && (rhs[1] == 'x') &&
+			    ((slen & 0x01) == 0)) {
+				if (slen == 2) {
+					return_P("Empty octet string is invalid");
+				}
+
+				if ((map->lhs->type != TMPL_TYPE_ATTR) ||
+				    !((map->lhs->tmpl_da->type == PW_TYPE_OCTETS) ||
+				      (map->lhs->tmpl_da->type == PW_TYPE_BYTE) ||
+				      (map->lhs->tmpl_da->type == PW_TYPE_SHORT) ||
+				      (map->lhs->tmpl_da->type == PW_TYPE_INTEGER) ||
+				      (map->lhs->tmpl_da->type == PW_TYPE_INTEGER64))) {
+					c->cast = dict_attrbyvalue(PW_CAST_BASE + PW_TYPE_OCTETS, 0);
+				}
+			}
+
+			if ((map->lhs->type == TMPL_TYPE_ATTR) &&
+			    map->lhs->tmpl_da->flags.is_unknown &&
+			    map_cast_from_hex(map, rhs_type, rhs)) {
+				/* do nothing */
+
+			} else {
+				tlen = tmpl_afrom_str(map, &map->rhs, rhs, talloc_array_length(rhs) - 1, rhs_type,
+						      REQUEST_CURRENT, PAIR_LIST_REQUEST, false);
+				if (tlen < 0) {
+					p = rhs_p - tlen;
+					return_P(fr_strerror());
+				}
+
+				if (tmpl_define_unknown_attr(map->rhs) < 0) {
+					return_rhs("Failed defining attribute");
+				}
+			}
+
+			/*
+			 *	Unknown attributes get marked up for pass2.
+			 */
+			if ((c->data.map->lhs->type == TMPL_TYPE_ATTR_UNDEFINED) ||
+			    (c->data.map->rhs->type == TMPL_TYPE_ATTR_UNDEFINED)) {
+				c->pass2_fixup = PASS2_FIXUP_ATTR;
+			}
+
+#ifdef HAVE_REGEX
+			if (c->data.map->rhs->type == TMPL_TYPE_REGEX) {
+				c->data.map->rhs->tmpl_iflag = iflag;
+				c->data.map->rhs->tmpl_mflag = mflag;
+			}
+#endif
+
+			/*
+			 *	Save the CONF_ITEM for later.
+			 */
+			c->data.map->ci = ci;
+
+			/*
+			 *	@todo: check LHS and RHS separately, to
+			 *	get better errors
+			 */
+			if ((c->data.map->rhs->type == TMPL_TYPE_LIST) ||
+			    (c->data.map->lhs->type == TMPL_TYPE_LIST)) {
+				return_0("Cannot use list references in condition");
+			}
+
+			/*
+			 *	Check cast type.  We can have the RHS
+			 *	a string if the LHS has a cast.  But
+			 *	if the RHS is an attr, it MUST be the
+			 *	same type as the LHS.
+			 */
+			if (c->cast) {
+				if ((c->data.map->rhs->type == TMPL_TYPE_ATTR) &&
+				    (c->cast->type != c->data.map->rhs->tmpl_da->type)) {
+					if (condition_check_types(c, c->cast->type)) {
+						goto keep_going;
+					}
+
+					goto same_type;
+				}
+
+#ifdef HAVE_REGEX
+				if (c->data.map->rhs->type == TMPL_TYPE_REGEX) {
+					return_0("Cannot use cast with regex comparison");
+				}
+#endif
+
+				/*
+				 *	The LHS is a literal which has been cast to a data type.
+				 *	Cast it to the appropriate data type.
+				 */
+				if ((c->data.map->lhs->type == TMPL_TYPE_LITERAL) &&
+				    (tmpl_cast_in_place(c->data.map->lhs, c->cast->type, c->cast) < 0)) {
+					*error = "Failed to parse field";
+					if (lhs) talloc_free(lhs);
+					if (rhs) talloc_free(rhs);
+					talloc_free(c);
+					return -(lhs_p - start);
+				}
+
+				/*
+				 *	The RHS is a literal, and the LHS has been cast to a data
+				 *	type.
+				 */
+				if ((c->data.map->lhs->type == TMPL_TYPE_DATA) &&
+				    (c->data.map->rhs->type == TMPL_TYPE_LITERAL) &&
+				    (tmpl_cast_in_place(c->data.map->rhs, c->cast->type, c->cast) < 0)) {
+					return_rhs("Failed to parse field");
+				}
+
+				/*
+				 *	We may be casting incompatible
+				 *	types.  We check this based on
+				 *	their size.
+				 */
+				if (c->data.map->lhs->type == TMPL_TYPE_ATTR) {
+					/*
+					 *      dst.min == src.min
+					 *	dst.max == src.max
+					 */
+					if ((dict_attr_sizes[c->cast->type][0] == dict_attr_sizes[c->data.map->lhs->tmpl_da->type][0]) &&
+					    (dict_attr_sizes[c->cast->type][1] == dict_attr_sizes[c->data.map->lhs->tmpl_da->type][1])) {
+						goto cast_ok;
+					}
+
+					/*
+					 *	Run-time parsing of strings.
+					 *	Run-time copying of octets.
+					 */
+					if ((c->data.map->lhs->tmpl_da->type == PW_TYPE_STRING) ||
+					    (c->data.map->lhs->tmpl_da->type == PW_TYPE_OCTETS)) {
+						goto cast_ok;
+					}
+
+					/*
+					 *	ifid to integer64 is OK
+					 */
+					if ((c->data.map->lhs->tmpl_da->type == PW_TYPE_IFID) &&
+					    (c->cast->type == PW_TYPE_INTEGER64)) {
+						goto cast_ok;
+					}
+
+					/*
+					 *	ipaddr to ipv4prefix is OK
+					 */
+					if ((c->data.map->lhs->tmpl_da->type == PW_TYPE_IPV4_ADDR) &&
+					    (c->cast->type == PW_TYPE_IPV4_PREFIX)) {
+						goto cast_ok;
+					}
+
+					/*
+					 *	ipv6addr to ipv6prefix is OK
+					 */
+					if ((c->data.map->lhs->tmpl_da->type == PW_TYPE_IPV6_ADDR) &&
+					    (c->cast->type == PW_TYPE_IPV6_PREFIX)) {
+						goto cast_ok;
+					}
+
+					/*
+					 *	integer64 to ethernet is OK.
+					 */
+					if ((c->data.map->lhs->tmpl_da->type == PW_TYPE_INTEGER64) &&
+					    (c->cast->type == PW_TYPE_ETHERNET)) {
+						goto cast_ok;
+					}
+
+					/*
+					 *	dst.max < src.min
+					 *	dst.min > src.max
+					 */
+					if ((dict_attr_sizes[c->cast->type][1] < dict_attr_sizes[c->data.map->lhs->tmpl_da->type][0]) ||
+					    (dict_attr_sizes[c->cast->type][0] > dict_attr_sizes[c->data.map->lhs->tmpl_da->type][1])) {
+						return_0("Cannot cast to attribute of incompatible size");
+					}
+				}
+
+			cast_ok:
+				/*
+				 *	Casting to a redundant type means we don't need the cast.
+				 *
+				 *	Do this LAST, as the rest of the code above assumes c->cast
+				 *	is not NULL.
+				 */
+				if ((c->data.map->lhs->type == TMPL_TYPE_ATTR) &&
+				    (c->cast->type == c->data.map->lhs->tmpl_da->type)) {
+					c->cast = NULL;
+				}
+
+			} else {
+				vp_tmpl_t *vpt;
+
+				/*
+				 *	Two attributes?  They must be of the same type
+				 */
+				if ((c->data.map->rhs->type == TMPL_TYPE_ATTR) &&
+				    (c->data.map->lhs->type == TMPL_TYPE_ATTR) &&
+				    (c->data.map->lhs->tmpl_da->type != c->data.map->rhs->tmpl_da->type)) {
+					if (condition_check_types(c, c->data.map->lhs->tmpl_da->type)) {
+						goto keep_going;
+					}
+
+				same_type:
+					return_0("Attribute comparisons must be of the same data type");
+				}
+
+				/*
+				 *	Without a cast, we can't compare "foo" to User-Name,
+				 *	it has to be done the other way around.
+				 */
+				if ((c->data.map->rhs->type == TMPL_TYPE_ATTR) &&
+				    (c->data.map->lhs->type != TMPL_TYPE_ATTR)) {
+					*error = "Cannot use attribute reference on right side of condition";
+				return_0:
+					if (lhs) talloc_free(lhs);
+					if (rhs) talloc_free(rhs);
+					talloc_free(c);
+					return 0;
+				}
+
+				/*
+				 *	Invalid: User-Name == bob
+				 *	Valid:   User-Name == "bob"
+				 *
+				 *	There's no real reason for
+				 *	this, other than consistency.
+				 */
+				if ((c->data.map->lhs->type == TMPL_TYPE_ATTR) &&
+				    (c->data.map->rhs->type != TMPL_TYPE_ATTR) &&
+				    (c->data.map->lhs->tmpl_da->type == PW_TYPE_STRING) &&
+				    (c->data.map->op != T_OP_CMP_TRUE) &&
+				    (c->data.map->op != T_OP_CMP_FALSE) &&
+				    (rhs_type == T_BARE_WORD)) {
+					return_rhs("Must have string as value for attribute");
+				}
+
+				/*
+				 *	Quotes around non-string
+				 *	attributes mean that it's
+				 *	either xlat, or an exec.
+				 */
+				if ((c->data.map->lhs->type == TMPL_TYPE_ATTR) &&
+				    (c->data.map->rhs->type != TMPL_TYPE_ATTR) &&
+				    (c->data.map->lhs->tmpl_da->type != PW_TYPE_STRING) &&
+				    (c->data.map->lhs->tmpl_da->type != PW_TYPE_OCTETS) &&
+				    (c->data.map->lhs->tmpl_da->type != PW_TYPE_DATE) &&
+				    (rhs_type == T_SINGLE_QUOTED_STRING)) {
+					*error = "Value must be an unquoted string";
+				return_rhs:
+					if (lhs) talloc_free(lhs);
+					if (rhs) talloc_free(rhs);
+					talloc_free(c);
+					return -(rhs_p - start);
+				}
+
+				/*
+				 *	The LHS has been cast to a data type, and the RHS is a
+				 *	literal.  Cast the RHS to the type of the cast.
+				 */
+				if (c->cast && (c->data.map->rhs->type == TMPL_TYPE_LITERAL) &&
+				    (tmpl_cast_in_place(c->data.map->rhs, c->cast->type, c->cast) < 0)) {
+					return_rhs("Failed to parse field");
+				}
+
+				/*
+				 *	The LHS is an attribute, and the RHS is a literal.  Cast the
+				 *	RHS to the data type of the LHS.
+				 *
+				 *	Note: There's a hack in here to always parse RHS as the
+				 *	equivalent prefix type if the LHS is an IP address.
+				 *
+				 *	This allows Framed-IP-Address < 192.168.0.0./24
+				 */
+				if ((c->data.map->lhs->type == TMPL_TYPE_ATTR) &&
+				    (c->data.map->rhs->type == TMPL_TYPE_LITERAL)) {
+					PW_TYPE type = c->data.map->lhs->tmpl_da->type;
+
+					switch (c->data.map->lhs->tmpl_da->type) {
+					case PW_TYPE_IPV4_ADDR:
+						if (strchr(c->data.map->rhs->name, '/') != NULL) {
+							type = PW_TYPE_IPV4_PREFIX;
+							c->cast = dict_attrbyvalue(PW_CAST_BASE + type, 0);
+						}
+						break;
+
+					case PW_TYPE_IPV6_ADDR:
+						if (strchr(c->data.map->rhs->name, '/') != NULL) {
+							type = PW_TYPE_IPV6_PREFIX;
+							c->cast = dict_attrbyvalue(PW_CAST_BASE + type, 0);
+						}
+						break;
+
+					default:
+						break;
+					}
+
+					if (tmpl_cast_in_place(c->data.map->rhs, type, c->data.map->lhs->tmpl_da) < 0) {
+						DICT_ATTR const *da = c->data.map->lhs->tmpl_da;
+
+						if ((da->vendor == 0) &&
+						    ((da->attr == PW_AUTH_TYPE) ||
+						     (da->attr == PW_AUTZ_TYPE) ||
+						     (da->attr == PW_ACCT_TYPE) ||
+						     (da->attr == PW_SESSION_TYPE) ||
+						     (da->attr == PW_POST_AUTH_TYPE) ||
+						     (da->attr == PW_PRE_PROXY_TYPE) ||
+						     (da->attr == PW_POST_PROXY_TYPE) ||
+						     (da->attr == PW_PRE_ACCT_TYPE) ||
+						     (da->attr == PW_RECV_COA_TYPE) ||
+						     (da->attr == PW_SEND_COA_TYPE))) {
+							/*
+							 *	The types for these attributes are dynamically allocated
+							 *	by modules.c, so we can't enforce strictness here.
+							 */
+							c->pass2_fixup = PASS2_FIXUP_TYPE;
+						} else {
+							return_rhs("Failed to parse value for attribute");
+						}
+					}
+
+					/*
+					 *	Stupid WiMAX shit.
+					 *	Cast the LHS to the
+					 *	type of the RHS.
+					 */
+					if (c->data.map->lhs->tmpl_da->type == PW_TYPE_COMBO_IP_ADDR) {
+						DICT_ATTR const *da;
+
+						da = dict_attrbytype(c->data.map->lhs->tmpl_da->attr,
+								     c->data.map->lhs->tmpl_da->vendor,
+								     c->data.map->rhs->tmpl_data_type);
+						if (!da) {
+							return_rhs("Cannot find type for attribute");
+						}
+						c->data.map->lhs->tmpl_da = da;
+					}
+				} /* attr to literal comparison */
+
+				/*
+				 *	The RHS will turn into... something.  Allow for prefixes
+				 *	there, too.
+				 */
+				if ((c->data.map->lhs->type == TMPL_TYPE_ATTR) &&
+				    ((c->data.map->rhs->type == TMPL_TYPE_XLAT) ||
+				     (c->data.map->rhs->type == TMPL_TYPE_XLAT_STRUCT) ||
+				     (c->data.map->rhs->type == TMPL_TYPE_EXEC))) {
+					if (c->data.map->lhs->tmpl_da->type == PW_TYPE_IPV4_ADDR) {
+						c->cast = dict_attrbyvalue(PW_CAST_BASE + PW_TYPE_IPV4_PREFIX, 0);
+					}
+
+					if (c->data.map->lhs->tmpl_da->type == PW_TYPE_IPV6_ADDR) {
+						c->cast = dict_attrbyvalue(PW_CAST_BASE + PW_TYPE_IPV6_PREFIX, 0);
+					}
+				}
+
+				/*
+				 *	If the LHS is a bare word, AND it looks like
+				 *	an attribute, try to parse it as such.
+				 *
+				 *	This allows LDAP-Group and SQL-Group to work.
+				 *
+				 *	The real fix is to just read the config files,
+				 *	and do no parsing until after all of the modules
+				 *	are loaded.  But that has issues, too.
+				 */
+				if ((c->data.map->lhs->type == TMPL_TYPE_LITERAL) && (lhs_type == T_BARE_WORD)) {
+					int hyphens = 0;
+					bool may_be_attr = true;
+					size_t i;
+					ssize_t attr_slen;
+
+					/*
+					 *	Backwards compatibility: Allow Foo-Bar,
+					 *	e.g. LDAP-Group and SQL-Group.
+					 */
+					for (i = 0; i < c->data.map->lhs->len; i++) {
+						if (!dict_attr_allowed_chars[(unsigned char) c->data.map->lhs->name[i]]) {
+							may_be_attr = false;
+							break;
+						}
+
+						if (c->data.map->lhs->name[i] == '-') {
+							hyphens++;
+						}
+					}
+
+					if (!hyphens || (hyphens > 3)) may_be_attr = false;
+
+					if (may_be_attr) {
+						attr_slen = tmpl_afrom_attr_str(c->data.map, &vpt, lhs,
+										REQUEST_CURRENT, PAIR_LIST_REQUEST,
+										true, true);
+						if ((attr_slen > 0) && (vpt->len == c->data.map->lhs->len)) {
+							talloc_free(c->data.map->lhs);
+							c->data.map->lhs = vpt;
+							c->pass2_fixup = PASS2_FIXUP_ATTR;
+						}
+					}
+				}
+			} /* we didn't have a cast */
+
+		keep_going:
+			p += slen;
+
+			while (isspace((uint8_t) *p)) p++; /* skip spaces after RHS */
+		} /* parse OP RHS */
+	} /* parse a condition (COND) or FOO OP BAR*/
+
+	/*
+	 *	...COND)
+	 */
+	if (*p == ')') {
+		if (!brace) {
+			return_P("Unexpected closing brace");
+		}
+
+		p++;
+		while (isspace((uint8_t) *p)) p++; /* skip spaces after closing brace */
+		goto done;
+	}
+
+	/*
+	 *	End of string is now allowed.
+	 */
+	if (!*p) {
+		if (brace) {
+			return_P("No closing brace at end of string");
+		}
+
+		goto done;
+	}
+
+	if (!(((p[0] == '&') && (p[1] == '&')) ||
+	      ((p[0] == '|') && (p[1] == '|')))) {
+		*error = "Unexpected text after condition";
+	return_p:
+		if (lhs) talloc_free(lhs);
+		if (rhs) talloc_free(rhs);
+		talloc_free(c);
+		return -(p - start);
+	}
+
+	/*
+	 *	Recurse to parse the next condition.
+	 */
+	c->next_op = p[0];
+	p += 2;
+
+	/*
+	 *	May still be looking for a closing brace.
+	 */
+	slen = condition_tokenize(c, ci, p, brace, &c->next, error, flags);
+	if (slen <= 0) {
+	return_slen:
+		if (lhs) talloc_free(lhs);
+		if (rhs) talloc_free(rhs);
+		talloc_free(c);
+		return slen - (p - start);
+	}
+	p += slen;
+
+done:
+	/*
+	 *	Normalize the condition before returning.
+	 *
+	 *	We collapse multiple levels of braces to one.  Then
+	 *	convert maps to literals.  Then literals to true/false
+	 *	statements.  Then true/false ||/&& followed by other
+	 *	conditions to just conditions.
+	 *
+	 *	Order is important.  The more complex cases are
+	 *	converted to simpler ones, from the most complex cases
+	 *	to the simplest ones.
+	 */
+
+	/*
+	 *	(FOO)     --> FOO
+	 *	(FOO) ... --> FOO ...
+	 */
+	if ((c->type == COND_TYPE_CHILD) && !c->data.child->next) {
+		fr_cond_t *child;
+
+		child = talloc_steal(ctx, c->data.child);
+		c->data.child = NULL;
+
+		child->next = talloc_steal(child, c->next);
+		c->next = NULL;
+
+		child->next_op = c->next_op;
+
+		/*
+		 *	Set the negation properly
+		 */
+		if ((c->negate && !child->negate) ||
+		    (!c->negate && child->negate)) {
+			child->negate = true;
+		} else {
+			child->negate = false;
+		}
+
+		lhs = rhs = NULL;
+		talloc_free(c);
+		c = child;
+	}
+
+	/*
+	 *	(FOO ...) --> FOO ...
+	 *
+	 *	But don't do !(FOO || BAR) --> !FOO || BAR
+	 *	Because that's different.
+	 */
+	if ((c->type == COND_TYPE_CHILD) &&
+	    !c->next && !c->negate) {
+		fr_cond_t *child;
+
+		child = talloc_steal(ctx, c->data.child);
+		c->data.child = NULL;
+
+		lhs = rhs = NULL;
+		talloc_free(c);
+		c = child;
+	}
+
+	/*
+	 *	Convert maps to literals.  Convert one form of map to
+	 *	a standardized form.  This doesn't make any
+	 *	theoretical difference, but it does mean that the
+	 *	run-time evaluation has fewer cases to check.
+	 */
+	if (c->type == COND_TYPE_MAP) do {
+		VERIFY_MAP(c->data.map);
+
+		/*
+		 *	!FOO !~ BAR --> FOO =~ BAR
+		 */
+		if (c->negate && (c->data.map->op == T_OP_REG_NE)) {
+			c->negate = false;
+			c->data.map->op = T_OP_REG_EQ;
+		}
+
+		/*
+		 *	FOO !~ BAR --> !FOO =~ BAR
+		 */
+		if (!c->negate && (c->data.map->op == T_OP_REG_NE)) {
+			c->negate = true;
+			c->data.map->op = T_OP_REG_EQ;
+		}
+
+		/*
+		 *	!FOO != BAR --> FOO == BAR
+		 */
+		if (c->negate && (c->data.map->op == T_OP_NE)) {
+			c->negate = false;
+			c->data.map->op = T_OP_CMP_EQ;
+		}
+
+		/*
+		 *	This next one catches "LDAP-Group != foo",
+		 *	which doesn't work as-is, but this hack fixes
+		 *	it.
+		 *
+		 *	FOO != BAR --> !FOO == BAR
+		 */
+		if (!c->negate && (c->data.map->op == T_OP_NE)) {
+			c->negate = true;
+			c->data.map->op = T_OP_CMP_EQ;
+		}
+
+		/*
+		 *	FOO =* BAR --> FOO
+		 *	FOO !* BAR --> !FOO
+		 *
+		 *	FOO may be a string, or a delayed attribute
+		 *	reference.
+		 */
+		if ((c->data.map->op == T_OP_CMP_TRUE) ||
+		    (c->data.map->op == T_OP_CMP_FALSE)) {
+			vp_tmpl_t *vpt;
+
+			vpt = talloc_steal(c, c->data.map->lhs);
+			c->data.map->lhs = NULL;
+
+			/*
+			 *	Invert the negation bit.
+			 */
+			if (c->data.map->op == T_OP_CMP_FALSE) {
+				c->negate = !c->negate;
+			}
+
+			TALLOC_FREE(c->data.map);
+
+			c->type = COND_TYPE_EXISTS;
+			c->data.vpt = vpt;
+			break;	/* it's no longer a map */
+		}
+
+		/*
+		 *	Both are data (IP address, integer, etc.)
+		 *
+		 *	We can do the evaluation here, so that it
+		 *	doesn't need to be done at run time
+		 */
+		if ((c->data.map->lhs->type == TMPL_TYPE_DATA) &&
+		    (c->data.map->rhs->type == TMPL_TYPE_DATA)) {
+			int rcode;
+
+			rad_assert(c->cast != NULL);
+
+			rcode = radius_evaluate_map(NULL, 0, 0, c);
+			TALLOC_FREE(c->data.map);
+			c->cast = NULL;
+			if (rcode) {
+				c->type = COND_TYPE_TRUE;
+			} else {
+				c->type = COND_TYPE_FALSE;
+			}
+
+			break;	/* it's no longer a map */
+		}
+
+		/*
+		 *	Both are literal strings.  They're not parsed
+		 *	as TMPL_TYPE_DATA because there's no cast to an
+		 *	attribute.
+		 *
+		 *	We can do the evaluation here, so that it
+		 *	doesn't need to be done at run time
+		 */
+		if ((c->data.map->rhs->type == TMPL_TYPE_LITERAL) &&
+		    (c->data.map->lhs->type == TMPL_TYPE_LITERAL) &&
+		    !c->pass2_fixup) {
+			int rcode;
+
+			rad_assert(c->cast == NULL);
+
+			rcode = radius_evaluate_map(NULL, 0, 0, c);
+			if (rcode) {
+				c->type = COND_TYPE_TRUE;
+			} else {
+				DEBUG3("OPTIMIZING (%s %s %s) --> FALSE",
+				       c->data.map->lhs->name,
+				       fr_int2str(fr_tokens, c->data.map->op, "??"),
+				       c->data.map->rhs->name);
+				c->type = COND_TYPE_FALSE;
+			}
+
+			/*
+			 *	Free map after using it above.
+			 */
+			TALLOC_FREE(c->data.map);
+			break;
+		}
+
+		/*
+		 *	<ipaddr>"foo" CMP &Attribute-Name The cast may
+		 *	not be necessary, and we can re-write it so
+		 *	that the attribute reference is on the LHS.
+		 */
+		if (c->cast &&
+		    (c->data.map->rhs->type == TMPL_TYPE_ATTR) &&
+		    (c->cast->type == c->data.map->rhs->tmpl_da->type) &&
+		    (c->data.map->lhs->type != TMPL_TYPE_ATTR)) {
+			vp_tmpl_t *tmp;
+
+			tmp = c->data.map->rhs;
+			c->data.map->rhs = c->data.map->lhs;
+			c->data.map->lhs = tmp;
+
+			c->cast = NULL;
+
+			switch (c->data.map->op) {
+			case T_OP_CMP_EQ:
+				/* do nothing */
+				break;
+
+			case T_OP_LE:
+				c->data.map->op = T_OP_GE;
+				break;
+
+			case T_OP_LT:
+				c->data.map->op = T_OP_GT;
+				break;
+
+			case T_OP_GE:
+				c->data.map->op = T_OP_LE;
+				break;
+
+			case T_OP_GT:
+				c->data.map->op = T_OP_LT;
+				break;
+
+			default:
+				return_0("Internal sanity check failed 1");
+			}
+
+			/*
+			 *	This must have been parsed into TMPL_TYPE_DATA.
+			 */
+			rad_assert(c->data.map->rhs->type != TMPL_TYPE_LITERAL);
+		}
+
+	} while (0);
+
+	/*
+	 *	Existence checks.  We short-circuit static strings,
+	 *	too.
+	 *
+	 *	FIXME: the data types should be in the template, too.
+	 *	So that we know where a literal came from.
+	 *
+	 *	"foo" is NOT the same as 'foo' or a bare foo.
+	 */
+	if (c->type == COND_TYPE_EXISTS) {
+		VERIFY_TMPL(c->data.vpt);
+
+		switch (c->data.vpt->type) {
+		case TMPL_TYPE_XLAT:
+		case TMPL_TYPE_ATTR:
+		case TMPL_TYPE_ATTR_UNDEFINED:
+		case TMPL_TYPE_LIST:
+		case TMPL_TYPE_EXEC:
+			break;
+
+			/*
+			 *	'true' and 'false' are special strings
+			 *	which mean themselves.
+			 *
+			 *	For integers, 0 is false, all other
+			 *	integers are true.
+			 *
+			 *	For strings, '' and "" are false.
+			 *	'foo' and "foo" are true.
+			 *
+			 *	The str2tmpl function takes care of
+			 *	marking "%{foo}" as TMPL_TYPE_XLAT, so
+			 *	the strings here are fixed at compile
+			 *	time.
+			 *
+			 *	`exec` and "%{...}" are left alone.
+			 *
+			 *	Bare words must be module return
+			 *	codes.
+			 */
+		case TMPL_TYPE_LITERAL:
+			if ((strcmp(c->data.vpt->name, "true") == 0) ||
+			    (strcmp(c->data.vpt->name, "1") == 0)) {
+				c->type = COND_TYPE_TRUE;
+				TALLOC_FREE(c->data.vpt);
+
+			} else if ((strcmp(c->data.vpt->name, "false") == 0) ||
+				   (strcmp(c->data.vpt->name, "0") == 0)) {
+				c->type = COND_TYPE_FALSE;
+				TALLOC_FREE(c->data.vpt);
+
+			} else if (!*c->data.vpt->name) {
+				c->type = COND_TYPE_FALSE;
+				TALLOC_FREE(c->data.vpt);
+
+			} else if ((lhs_type == T_SINGLE_QUOTED_STRING) ||
+				   (lhs_type == T_DOUBLE_QUOTED_STRING)) {
+				c->type = COND_TYPE_TRUE;
+				TALLOC_FREE(c->data.vpt);
+
+			} else if (lhs_type == T_BARE_WORD) {
+				int rcode;
+				bool zeros = true;
+				char const *q;
+
+				for (q = c->data.vpt->name;
+				     *q != '\0';
+				     q++) {
+					if (!isdigit((uint8_t) *q)) {
+						break;
+					}
+					if (*q != '0') zeros = false;
+				}
+
+				/*
+				 *	It's all digits, and therefore
+				 *	'false' if zero, and 'true' otherwise.
+				 */
+				if (!*q) {
+					if (zeros) {
+						c->type = COND_TYPE_FALSE;
+					} else {
+						c->type = COND_TYPE_TRUE;
+					}
+					TALLOC_FREE(c->data.vpt);
+					break;
+				}
+
+				/*
+				 *	Allow &Foo-Bar where Foo-Bar is an attribute
+				 *	defined by a module.
+				 */
+				if (c->pass2_fixup == PASS2_FIXUP_ATTR) {
+					break;
+				}
+
+				rcode = fr_str2int(allowed_return_codes,
+						   c->data.vpt->name, 0);
+				if (!rcode) {
+					return_0("Expected a module return code");
+				}
+			}
+
+			/*
+			 *	Else lhs_type==T_INVALID, and this
+			 *	node was made by promoting a child
+			 *	which had already been normalized.
+			 */
+			break;
+
+		case TMPL_TYPE_DATA:
+			return_0("Cannot use data here");
+
+		default:
+			return_0("Internal sanity check failed 2");
+		}
+	}
+
+	/*
+	 *	!TRUE -> FALSE
+	 */
+	if (c->type == COND_TYPE_TRUE) {
+		if (c->negate) {
+			c->negate = false;
+			c->type = COND_TYPE_FALSE;
+		}
+	}
+
+	/*
+	 *	!FALSE -> TRUE
+	 */
+	if (c->type == COND_TYPE_FALSE) {
+		if (c->negate) {
+			c->negate = false;
+			c->type = COND_TYPE_TRUE;
+		}
+	}
+
+	/*
+	 *	true && FOO --> FOO
+	 */
+	if ((c->type == COND_TYPE_TRUE) &&
+	    (c->next_op == COND_AND)) {
+		fr_cond_t *next;
+
+		next = talloc_steal(ctx, c->next);
+		c->next = NULL;
+
+		lhs = rhs = NULL;
+		talloc_free(c);
+		c = next;
+	}
+
+	/*
+	 *	false && FOO --> false
+	 */
+	if ((c->type == COND_TYPE_FALSE) &&
+	    (c->next_op == COND_AND)) {
+		talloc_free(c->next);
+		c->next = NULL;
+		c->next_op = COND_NONE;
+	}
+
+	/*
+	 *	false || FOO --> FOO
+	 */
+	if ((c->type == COND_TYPE_FALSE) &&
+	    (c->next_op == COND_OR)) {
+		fr_cond_t *next;
+
+		next = talloc_steal(ctx, c->next);
+		c->next = NULL;
+
+		lhs = rhs = NULL;
+		talloc_free(c);
+		c = next;
+	}
+
+	/*
+	 *	true || FOO --> true
+	 */
+	if ((c->type == COND_TYPE_TRUE) &&
+	    (c->next_op == COND_OR)) {
+		talloc_free(c->next);
+		c->next = NULL;
+		c->next_op = COND_NONE;
+	}
+
+	if (lhs) talloc_free(lhs);
+	if (rhs) talloc_free(rhs);
+
+	*pcond = c;
+	return p - start;
+}
+
+/** Tokenize a conditional check
+ *
+ *  @param[in] ctx for talloc
+ *  @param[in] ci for CONF_ITEM
+ *  @param[in] start the start of the string to process.  Should be "(..."
+ *  @param[out] head the parsed condition structure
+ *  @param[out] error the parse error (if any)
+ *  @param[in] flags do one/two pass
+ *  @return length of the string skipped, or when negative, the offset to the offending error
+ */
+ssize_t fr_condition_tokenize(TALLOC_CTX *ctx, CONF_ITEM *ci, char const *start, fr_cond_t **head, char const **error, int flags)
+{
+	return condition_tokenize(ctx, ci, start, false, head, error, flags);
+}
+
+/*
+ *	Walk in order.
+ */
+bool fr_condition_walk(fr_cond_t *c, bool (*callback)(void *, fr_cond_t *), void *ctx)
+{
+	while (c) {
+		/*
+		 *	Process this one, exit on error.
+		 */
+		if (!callback(ctx, c)) return false;
+
+		switch (c->type) {
+		case COND_TYPE_INVALID:
+			return false;
+
+		case COND_TYPE_EXISTS:
+		case COND_TYPE_MAP:
+		case COND_TYPE_TRUE:
+		case COND_TYPE_FALSE:
+			break;
+
+		case COND_TYPE_CHILD:
+			/*
+			 *	Walk over the child.
+			 */
+			if (!fr_condition_walk(c->data.child, callback, ctx)) {
+				return false;
+			}
+		}
+
+		/*
+		 *	No sibling, stop.
+		 */
+		if (c->next_op == COND_NONE) break;
+
+		/*
+		 *	process the next sibling
+		 */
+		c = c->next;
+	}
+
+	return true;
+}
diff --git a/src/main/process.c b/src/main/process.c
new file mode 100644
index 0000000..ed77839
--- /dev/null
+++ b/src/main/process.c
@@ -0,0 +1,6457 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @file process.c
+ * @brief Defines the state machines that control how requests are processed.
+ *
+ * @copyright 2012  The FreeRADIUS server project
+ * @copyright 2012  Alan DeKok <aland@deployingradius.com>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/process.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/state.h>
+
+#include <freeradius-devel/rad_assert.h>
+
+#ifdef WITH_DETAIL
+#include <freeradius-devel/detail.h>
+#endif
+
+#include <signal.h>
+#include <fcntl.h>
+
+#ifdef HAVE_SYS_WAIT_H
+#	include <sys/wait.h>
+#endif
+
+#ifdef HAVE_SYSTEMD_WATCHDOG
+#  include <systemd/sd-daemon.h>
+#endif
+
+extern pid_t radius_pid;
+extern fr_cond_t *debug_condition;
+
+#ifdef HAVE_SYSTEMD_WATCHDOG
+struct timeval sd_watchdog_interval;
+static fr_event_t *sd_watchdog_ev;
+#endif
+
+static bool spawn_flag = false;
+static bool just_started = true;
+time_t fr_start_time = (time_t)-1;
+static rbtree_t *pl = NULL;
+static fr_event_list_t *el = NULL;
+
+fr_event_list_t *radius_event_list_corral(UNUSED event_corral_t hint) {
+	/* Currently we do not run a second event loop for modules. */
+	return el;
+}
+
+static char const *action_codes[] = {
+	"INVALID",
+	"run",
+	"done",
+	"dup",
+	"timer",
+#ifdef WITH_PROXY
+	"proxy-reply",
+#endif
+	"request was cancelled",
+	"conflicting packet was received",
+	"max_time was reached",
+	"internal failure",
+	"cleanup_delay was reached",
+	"CoA packet was cancelled, and not sent",
+};
+
+#ifdef DEBUG_STATE_MACHINE
+#  define TRACE_STATE_MACHINE \
+if (rad_debug_lvl) do { \
+	struct timeval debug_tv; \
+	gettimeofday(&debug_tv, NULL); \
+	debug_tv.tv_sec -= fr_start_time; \
+	printf("(%u) %d.%06d ********\tSTATE %s action %s live M-%s C-%s\t********\n",\
+	       request->number, (int) debug_tv.tv_sec, (int) debug_tv.tv_usec, \
+	       __FUNCTION__, action_codes[action], master_state_names[request->master_state], \
+	       child_state_names[request->child_state]); \
+} while (0)
+
+static char const *master_state_names[REQUEST_MASTER_NUM_STATES] = {
+	"?",
+	"active",
+	"stop-processing",
+	"counted"
+};
+
+static char const *child_state_names[REQUEST_CHILD_NUM_STATES] = {
+	"?",
+	"queued",
+	"running",
+	"proxied",
+	"reject-delay",
+	"cleanup-delay",
+	"done"
+};
+
+#else
+#  define TRACE_STATE_MACHINE {}
+#endif
+
+static NEVER_RETURNS void _rad_panic(char const *file, unsigned int line, char const *msg)
+{
+	ERROR("%s[%u]: %s", file, line, msg);
+	fr_exit_now(1);
+}
+
+#define rad_panic(x) _rad_panic(__FILE__, __LINE__, x)
+
+/** Declare a state in the state machine
+ *
+ * Expands to the start of a function definition for a given state.
+ *
+ * @param _x the name of the state.
+ */
+#define STATE_MACHINE_DECL(_x) static void _x(REQUEST *request, int action)
+
+static void request_timer(void *ctx);
+
+/** Insert #REQUEST back into the event heap, to continue executing at a future time
+ *
+ * @param file the state machine timer call occurred in.
+ * @param line the state machine timer call occurred on.
+ * @param request to set add the timer event for.
+ * @param when the event should fine.
+ * @param action to perform when we resume processing the request.
+ */
+static inline void state_machine_timer(char const *file, int line, REQUEST *request,
+				       struct timeval *when, fr_state_action_t action)
+{
+	request->timer_action = action;
+	if (!fr_event_insert(el, request_timer, request, when, &request->ev)) {
+		_rad_panic(file, line, "Failed to insert event");
+	}
+}
+
+/** @copybrief state_machine_timer
+ *
+ * @param _x the action to perform when we resume processing the request.
+ */
+#define STATE_MACHINE_TIMER(_x) state_machine_timer(__FILE__, __LINE__, request, &when, _x)
+
+/*
+ *	We need a different VERIFY_REQUEST macro in process.c
+ *	To avoid the race conditions with the master thread
+ *	checking the REQUEST whilst it's being worked on by
+ *	the child.
+ */
+#if defined(WITH_VERIFY_PTR) && defined(HAVE_PTHREAD_H)
+#  undef VERIFY_REQUEST
+#  define VERIFY_REQUEST(_x) if (pthread_equal(pthread_self(), _x->child_pid) != 0) verify_request(__FILE__, __LINE__, _x)
+#endif
+
+/**
+ * @section request_timeline
+ *
+ *	Time sequence of a request
+ * @code
+ *
+ *	RQ-----------------P=============================Y-J-C
+ *	 ::::::::::::::::::::::::::::::::::::::::::::::::::::::::M
+ * @endcode
+ *
+ * -	R: received.  Duplicate detection is done, and request is
+ * 	   cached.
+ *
+ * -	Q: Request is placed onto a queue for child threads to pick up.
+ *	   If there are no child threads, the request goes immediately
+ *	   to P.
+ *
+ * -	P: Processing the request through the modules.
+ *
+ * -	Y: Reply is ready.  Rejects MAY be delayed here.  All other
+ *	   replies are sent immediately.
+ *
+ * -	J: Reject is sent "response_delay" after the reply is ready.
+ *
+ * -	C: For Access-Requests, After "cleanup_delay", the request is
+ *	   deleted.  Accounting-Request packets go directly from Y to C.
+ *
+ * -	M: Max request time.  If the request hits this timer, it is
+ *	   forcibly stopped.
+ *
+ *	Other considerations include duplicate and conflicting
+ *	packets.  When a dupicate packet is received, it is ignored
+ *	until we've reached Y, as no response is ready.  If the reply
+ *	is a reject, duplicates are ignored until J, when we're ready
+ *	to send the reply.  In between the reply being sent (Y or J),
+ *	and C, the server responds to duplicates by sending the cached
+ *	reply.
+ *
+ *	Conflicting packets are sent in 2 situations.
+ *
+ *	The first is in between R and Y.  In that case, we consider
+ *	it as a hint that we're taking too long, and the NAS has given
+ *	up on the request.  We then behave just as if the M timer was
+ *	reached, and we discard the current request.  This allows us
+ *	to process the new one.
+ *
+ *	The second case is when we're at Y, but we haven't yet
+ *	finished processing the request.  This is a race condition in
+ *	the threading code (avoiding locks is faster).  It means that
+ *	a thread has actually encoded and sent the reply, and that the
+ *	NAS has responded with a new packet.  The server can then
+ *	safely mark the current request as "OK to delete", and behaves
+ *	just as if the M timer was reached.  This usually happens only
+ *	in high-load situations.
+ *
+ *	Duplicate packets are sent when the NAS thinks we're taking
+ *	too long, and wants a reply.  From R-Y, duplicates are
+ *	ignored.  From Y-J (for Access-Rejects), duplicates are also
+ *	ignored.  From Y-C, duplicates get a duplicate reply.  *And*,
+ *	they cause the "cleanup_delay" time to be extended.  This
+ *	extension means that we're more likely to send a duplicate
+ *	reply (if we have one), or to suppress processing the packet
+ *	twice if we didn't reply to it.
+ *
+ *	All functions in this file should be thread-safe, and should
+ *	assume thet the REQUEST structure is being accessed
+ *	simultaneously by the main thread, and by the child worker
+ *	threads.  This means that timers, etc. cannot be updated in
+ *	the child thread.
+ *
+ *	Instead, the master thread periodically calls request->process
+ *	with action TIMER.  It's up to the individual functions to
+ *	determine how to handle that.  They need to check if they're
+ *	being called from a child thread or the master, and then do
+ *	different things based on that.
+ */
+#ifdef WITH_PROXY
+static fr_packet_list_t *proxy_list = NULL;
+static TALLOC_CTX *proxy_ctx = NULL;
+#endif
+
+#ifdef HAVE_PTHREAD_H
+#  ifdef WITH_PROXY
+static pthread_mutex_t proxy_mutex;
+static bool proxy_no_new_sockets = false;
+#  endif
+
+#  define PTHREAD_MUTEX_LOCK if (spawn_flag) pthread_mutex_lock
+#  define PTHREAD_MUTEX_UNLOCK if (spawn_flag) pthread_mutex_unlock
+
+static pthread_t NO_SUCH_CHILD_PID;
+#  define NO_CHILD_THREAD request->child_pid = NO_SUCH_CHILD_PID
+
+#else
+/*
+ *	This is easier than ifdef's throughout the code.
+ */
+#  define PTHREAD_MUTEX_LOCK(_x)
+#  define PTHREAD_MUTEX_UNLOCK(_x)
+#  define NO_CHILD_THREAD
+#endif
+
+#ifdef HAVE_PTHREAD_H
+static bool we_are_master(void)
+{
+	if (spawn_flag &&
+	    (pthread_equal(pthread_self(), NO_SUCH_CHILD_PID) == 0)) {
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ *	Assertions are debug checks.
+ */
+#  ifndef NDEBUG
+#    define ASSERT_MASTER 	if (!we_are_master()) rad_panic("We are not master")
+#    endif
+#else
+
+/*
+ *	No threads: we're always master.
+ */
+#  define we_are_master(_x) (1)
+#endif	/* HAVE_PTHREAD_H */
+
+#ifndef ASSERT_MASTER
+#  define ASSERT_MASTER
+#endif
+
+/*
+ *	Make state transitions simpler.
+ */
+#define FINAL_STATE(_x) NO_CHILD_THREAD; request->component = "<" #_x ">"; request->module = ""; request->child_state = _x
+
+
+static void event_new_fd(rad_listen_t *this);
+
+/*
+ *	We need mutexes around the event FD list *only* in certain
+ *	cases.
+ */
+#if defined (HAVE_PTHREAD_H) && (defined(WITH_PROXY) || defined(WITH_TCP))
+static rad_listen_t *new_listeners = NULL;
+
+static pthread_mutex_t	fd_mutex;
+#  define FD_MUTEX_LOCK if (spawn_flag) pthread_mutex_lock
+#  define FD_MUTEX_UNLOCK if (spawn_flag) pthread_mutex_unlock
+
+void radius_update_listener(rad_listen_t *this)
+{
+	/*
+	 *	Just do it ourselves.
+	 */
+	if (we_are_master()) {
+		event_new_fd(this);
+		return;
+	}
+
+	FD_MUTEX_LOCK(&fd_mutex);
+
+	/*
+	 *	If it's already in the list, don't add it again.
+	 */
+	if (this->next) {
+		FD_MUTEX_UNLOCK(&fd_mutex);
+		return;
+	}
+
+	/*
+	 *	Otherwise, add it to the list
+	 */
+	this->next = new_listeners;
+	new_listeners = this;
+	FD_MUTEX_UNLOCK(&fd_mutex);
+	radius_signal_self(RADIUS_SIGNAL_SELF_NEW_FD);
+}
+#else
+void radius_update_listener(rad_listen_t *this)
+{
+	/*
+	 *	No threads.  Just insert it.
+	 */
+	event_new_fd(this);
+}
+/*
+ *	This is easier than ifdef's throughout the code.
+ */
+#  define FD_MUTEX_LOCK(_x)
+#  define FD_MUTEX_UNLOCK(_x)
+#endif
+
+/*
+ *	Emit a systemd watchdog notification and reschedule the event.
+ */
+#ifdef HAVE_SYSTEMD_WATCHDOG
+typedef struct {
+	fr_event_list_t *el;
+	struct timeval when;
+} sd_watchdog_data_t;
+
+static sd_watchdog_data_t sdwd;
+
+static void sd_watchdog_event(void *ctx)
+{
+	sd_watchdog_data_t *s = (sd_watchdog_data_t *)ctx;
+
+	DEBUG("Emitting systemd watchdog notification");
+	sd_notify(0, "WATCHDOG=1");
+
+	timeradd(&s->when, &sd_watchdog_interval, &s->when);
+	if (!fr_event_insert(s->el, sd_watchdog_event, ctx, &s->when, &sd_watchdog_ev)) {
+		rad_panic("Failed to insert event");
+	}
+}
+#endif
+
+static int request_num_counter = 1;
+#ifdef WITH_PROXY
+static int request_will_proxy(REQUEST *request) CC_HINT(nonnull);
+static int request_proxy(REQUEST *request) CC_HINT(nonnull);
+STATE_MACHINE_DECL(request_ping) CC_HINT(nonnull);
+
+STATE_MACHINE_DECL(request_response_delay) CC_HINT(nonnull);
+STATE_MACHINE_DECL(request_cleanup_delay) CC_HINT(nonnull);
+STATE_MACHINE_DECL(request_running) CC_HINT(nonnull);
+STATE_MACHINE_DECL(request_done) CC_HINT(nonnull);
+
+STATE_MACHINE_DECL(proxy_no_reply) CC_HINT(nonnull);
+STATE_MACHINE_DECL(proxy_running) CC_HINT(nonnull);
+STATE_MACHINE_DECL(proxy_wait_for_reply) CC_HINT(nonnull);
+
+static int process_proxy_reply(REQUEST *request, RADIUS_PACKET *reply) CC_HINT(nonnull (1));
+static void remove_from_proxy_hash(REQUEST *request) CC_HINT(nonnull);
+static void remove_from_proxy_hash_nl(REQUEST *request, bool yank) CC_HINT(nonnull);
+static int insert_into_proxy_hash(REQUEST *request) CC_HINT(nonnull);
+static int setup_post_proxy_fail(REQUEST *request);
+#endif
+
+static REQUEST *request_setup(TALLOC_CTX *ctx, rad_listen_t *listener, RADIUS_PACKET *packet,
+			      RADCLIENT *client, RAD_REQUEST_FUNP fun);
+static int request_pre_handler(REQUEST *request, UNUSED int action) CC_HINT(nonnull);
+
+#ifdef WITH_COA
+static void request_coa_originate(REQUEST *request) CC_HINT(nonnull);
+STATE_MACHINE_DECL(coa_wait_for_reply) CC_HINT(nonnull);
+STATE_MACHINE_DECL(coa_no_reply) CC_HINT(nonnull);
+STATE_MACHINE_DECL(coa_running) CC_HINT(nonnull);
+static void coa_separate(REQUEST *request, bool retransmit) CC_HINT(nonnull);
+#  define COA_SEPARATE if (request->coa) coa_separate(request->coa, true);
+#else
+#  define COA_SEPARATE
+#endif
+
+#define CHECK_FOR_STOP do { if (request->master_state == REQUEST_STOP_PROCESSING) {request_done(request, FR_ACTION_CANCELLED);return;}} while (0)
+
+#undef USEC
+#define USEC (1000000)
+
+#define INSERT_EVENT(_function, _ctx) if (!fr_event_insert(el, _function, _ctx, &((_ctx)->when), &((_ctx)->ev))) { _rad_panic(__FILE__, __LINE__, "Failed to insert event"); }
+
+static void tv_add(struct timeval *tv, int usec_delay)
+{
+	if (usec_delay >= USEC) {
+		tv->tv_sec += usec_delay / USEC;
+		usec_delay %= USEC;
+	}
+	tv->tv_usec += usec_delay;
+
+	if (tv->tv_usec >= USEC) {
+		tv->tv_sec += tv->tv_usec / USEC;
+		tv->tv_usec %= USEC;
+	}
+}
+
+/*
+ *	Debug the packet if requested.
+ */
+static void debug_packet(REQUEST *request, RADIUS_PACKET *packet, bool received)
+{
+	char src_ipaddr[128];
+	char dst_ipaddr[128];
+
+	if (!packet) return;
+	if (!RDEBUG_ENABLED) return;
+
+#ifdef WITH_DETAIL
+	/*
+	 *	Don't print IP addresses for detail files.
+	 */
+	if (request->listener &&
+	    (request->listener->type == RAD_LISTEN_DETAIL)) return;
+
+#endif
+	/*
+	 *	Client-specific debugging re-prints the input
+	 *	packet into the client log.
+	 *
+	 *	This really belongs in a utility library
+	 */
+	if (is_radius_code(packet->code)) {
+		RDEBUG("%s %s Id %u from %s%s%s:%i to %s%s%s:%i length %zu",
+		       received ? "Received" : "Sent",
+		       fr_packet_codes[packet->code],
+		       packet->id,
+		       packet->src_ipaddr.af == AF_INET6 ? "[" : "",
+		       inet_ntop(packet->src_ipaddr.af,
+				 &packet->src_ipaddr.ipaddr,
+				 src_ipaddr, sizeof(src_ipaddr)),
+		       packet->src_ipaddr.af == AF_INET6 ? "]" : "",
+		       packet->src_port,
+		       packet->dst_ipaddr.af == AF_INET6 ? "[" : "",
+		       inet_ntop(packet->dst_ipaddr.af,
+				 &packet->dst_ipaddr.ipaddr,
+				 dst_ipaddr, sizeof(dst_ipaddr)),
+		       packet->dst_ipaddr.af == AF_INET6 ? "]" : "",
+		       packet->dst_port,
+		       packet->data_len);
+	} else {
+		RDEBUG("%s code %u Id %u from %s%s%s:%i to %s%s%s:%i length %zu\n",
+		       received ? "Received" : "Sent",
+		       packet->code,
+		       packet->id,
+		       packet->src_ipaddr.af == AF_INET6 ? "[" : "",
+		       inet_ntop(packet->src_ipaddr.af,
+				 &packet->src_ipaddr.ipaddr,
+				 src_ipaddr, sizeof(src_ipaddr)),
+		       packet->src_ipaddr.af == AF_INET6 ? "]" : "",
+		       packet->src_port,
+		       packet->dst_ipaddr.af == AF_INET6 ? "[" : "",
+		       inet_ntop(packet->dst_ipaddr.af,
+				 &packet->dst_ipaddr.ipaddr,
+				 dst_ipaddr, sizeof(dst_ipaddr)),
+		       packet->dst_ipaddr.af == AF_INET6 ? "]" : "",
+		       packet->dst_port,
+		       packet->data_len);
+	}
+
+	if (received) {
+		rdebug_pair_list(L_DBG_LVL_1, request, packet->vps, NULL);
+	} else {
+		rdebug_proto_pair_list(L_DBG_LVL_1, request, packet->vps);
+	}
+}
+
+
+/***********************************************************************
+ *
+ *	Start of RADIUS server state machine.
+ *
+ ***********************************************************************/
+
+static struct timeval *request_response_window(REQUEST *request)
+{
+	VERIFY_REQUEST(request);
+
+	rad_assert(request->home_server != NULL);
+
+	if (request->client) {
+		/*
+		 *	The client hasn't set the response window.  Return
+		 *	either the home server one, if set, or the global one.
+		 */
+		if (!timerisset(&request->client->response_window)) {
+			return &request->home_server->response_window;
+		}
+
+		if (timercmp(&request->client->response_window,
+			     &request->home_server->response_window, <)) {
+			return &request->client->response_window;
+		}
+	}
+
+	return &request->home_server->response_window;
+}
+
+/*
+ * Determine initial request processing delay.
+ */
+static int request_init_delay(REQUEST *request)
+{
+	struct timeval half_response_window;
+
+	VERIFY_REQUEST(request);
+
+	/* Allow client response window to lower initial delay */
+	if (timerisset(&request->client->response_window)) {
+		half_response_window.tv_sec = request->client->response_window.tv_sec >> 1;
+		half_response_window.tv_usec =
+			((request->client->response_window.tv_sec & 1) * USEC +
+				request->client->response_window.tv_usec) >> 1;
+		if (timercmp(&half_response_window, &request->root->init_delay, <))
+			return (int)half_response_window.tv_sec * USEC +
+				(int)half_response_window.tv_usec;
+	}
+
+	return (int)request->root->init_delay.tv_sec * USEC +
+		(int)request->root->init_delay.tv_usec;
+}
+
+/*
+ *	Callback for ALL timer events related to the request.
+ */
+static void request_timer(void *ctx)
+{
+	REQUEST *request = talloc_get_type_abort(ctx, REQUEST);
+	int action;
+
+	action = request->timer_action;
+
+	TRACE_STATE_MACHINE;
+
+	request->process(request, action);
+}
+
+/*
+ *	Wrapper for talloc pools.  If there's no parent, just free the
+ *	request.  If there is a parent, free the parent INSTEAD of the
+ *	request.
+ */
+static void request_free(REQUEST *request)
+{
+	void *ptr;
+
+	rad_assert(request->ev == NULL);
+	rad_assert(!request->in_request_hash);
+	rad_assert(!request->in_proxy_hash);
+
+	if ((request->options & RAD_REQUEST_OPTION_CTX) == 0) {
+		talloc_free(request);
+		return;
+	}
+
+	ptr = talloc_parent(request);
+	rad_assert(ptr != NULL);
+	talloc_free(ptr);
+}
+
+
+#ifdef WITH_PROXY
+#ifdef WITH_TLS
+void proxy_listener_freeze(rad_listen_t *listener, fr_event_fd_handler_t write_handler)
+{
+	PTHREAD_MUTEX_LOCK(&proxy_mutex);
+	if (!fr_packet_list_socket_freeze(proxy_list,
+					  listener->fd)) {
+		ERROR("Fatal error freezing socket: %s", fr_strerror());
+		fr_exit(1);
+	}
+
+	listener->blocked = true;
+
+	if (fr_event_fd_write_handler(el, 0, listener->fd, write_handler, listener) < 0) {
+		ERROR("Fatal error freezing socket: %s", fr_strerror());
+		fr_exit(1);
+	}
+
+	PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+}
+
+void proxy_listener_thaw(rad_listen_t *listener)
+{
+	PTHREAD_MUTEX_LOCK(&proxy_mutex);
+	if (!fr_packet_list_socket_thaw(proxy_list,
+					  listener->fd)) {
+		ERROR("Fatal error freezing socket: %s", fr_strerror());
+		fr_exit(1);
+	}
+
+	listener->blocked = false;
+
+	if (fr_event_fd_write_handler(el, 0, listener->fd, NULL, listener) < 0) {
+		ERROR("Fatal error freezing socket: %s", fr_strerror());
+		fr_exit(1);
+	}
+
+	PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+}
+#endif	/* WITH_TLS */
+
+static void proxy_reply_too_late(REQUEST *request)
+{
+	char buffer[128];
+
+	RDEBUG2("Reply from home server %s port %d  - ID: %d arrived too late.  Try increasing 'retry_delay' or 'max_request_time'",
+		inet_ntop(request->proxy->dst_ipaddr.af,
+			  &request->proxy->dst_ipaddr.ipaddr,
+			  buffer, sizeof(buffer)),
+		request->proxy->dst_port, request->proxy->id);
+}
+#endif
+
+
+/** Mark a request DONE and clean it up.
+ *
+ *  When a request is DONE, it can have ties to a number of other
+ *  portions of the server.  The request hash, proxy hash, events,
+ *  child threads, etc.  This function takes care of either cleaning
+ *  up the request, or managing the timers to wait for the ties to be
+ *  removed.
+ *
+ *  \dot
+ *	digraph done {
+ *		stopped -> done
+ *		done -> done [ label = "still running" ];
+ *	}
+ *  \enddot
+ */
+static void request_done(REQUEST *request, int original)
+{
+	struct timeval now, when;
+	int action = original;
+
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+
+	/*
+	 *	Force this no matter what.
+	 */
+	request->process = request_done;
+
+#ifdef WITH_DETAIL
+	/*
+	 *	Tell the detail listener that we're done.
+	 */
+	if (request->listener &&
+	    (request->listener->type == RAD_LISTEN_DETAIL) &&
+	    (request->simul_max != 1)) {
+		request->simul_max = 1;
+		request->listener->send(request->listener,
+					request);
+	}
+#endif
+
+#ifdef HAVE_PTHREAD_H
+	/*
+	 *	If called from a child thread, mark ourselves as done,
+	 *	and wait for the master thread timer to clean us up.
+	 */
+	if (!we_are_master()) {
+		FINAL_STATE(REQUEST_DONE);
+		return;
+	}
+#endif
+
+	/*
+	 *	Mark the request as STOP.
+	 */
+	request->master_state = REQUEST_STOP_PROCESSING;
+
+#ifdef WITH_PROXY
+	/*
+	 *	Walk through the server pool to see if we need to mark
+	 *	connections as dead.
+	 */
+	if (request->home_pool) {
+		fr_event_now(el, &now);
+		if (request->home_pool->last_serviced < now.tv_sec) {
+			int i;
+
+			request->home_pool->last_serviced = now.tv_sec;
+
+			for (i = 0; i < request->home_pool->num_home_servers; i++) {
+				home_server_t *home = request->home_pool->servers[i];
+
+				if (home->state == HOME_STATE_CONNECTION_FAIL) {
+					mark_home_server_dead(home, &now, false);
+				}
+			}
+		}
+	}
+#endif
+
+	/*
+	 *	If it was administratively canceled, then it's done.
+	 */
+	if (action >= FR_ACTION_CANCELLED) {
+		action = FR_ACTION_DONE;
+
+#ifdef WITH_COA
+		/*
+		 *	Don't touch request->coa, it's in the middle
+		 *	of being processed...
+		 */
+	} else {
+		/*
+		 *	Move the CoA request to its own handler, but
+		 *	only if the request finished normally, and was
+		 *	not administratively canceled.
+		 */
+		if (request->coa) {
+			coa_separate(request->coa, true);
+		} else if (request->parent && (request->parent->coa == request)) {
+			coa_separate(request, true);
+		}
+#endif
+	}
+
+	/*
+	 *	It doesn't hurt to send duplicate replies.  All other
+	 *	signals are ignored, as the request will be cleaned up
+	 *	soon anyways.
+	 */
+	switch (action) {
+	case FR_ACTION_DUP:
+#ifdef WITH_DETAIL
+		rad_assert(request->listener != NULL);
+#endif
+		if (request->reply->code != 0) {
+			request->listener->send(request->listener, request);
+			return;
+		} else {
+			RDEBUG("No reply.  Ignoring retransmit");
+		}
+		break;
+
+		/*
+		 *	Mark the request as done.
+		 */
+	case FR_ACTION_DONE:
+#ifdef HAVE_PTHREAD_H
+		/*
+		 *	If the child is still running, leave it alone.
+		 */
+		if (spawn_flag && (request->child_state <= REQUEST_RUNNING)) {
+			break;
+		}
+#endif
+
+#ifdef DEBUG_STATE_MACHINE
+		if (rad_debug_lvl) printf("(%u) ********\tSTATE %s C-%s -> C-%s\t********\n",
+				       request->number, __FUNCTION__,
+				       child_state_names[request->child_state],
+				       child_state_names[REQUEST_DONE]);
+#endif
+		request->child_state = REQUEST_DONE;
+		break;
+
+		/*
+		 *	Called when the child is taking too long to
+		 *	finish.  We've already marked it "please
+		 *	stop", so we don't complain any more.
+		 */
+	case FR_ACTION_TIMER:
+		break;
+
+#ifdef WITH_PROXY
+	case FR_ACTION_PROXY_REPLY:
+		proxy_reply_too_late(request);
+		break;
+#endif
+
+	default:
+		break;
+	}
+
+	/*
+	 *	Remove it from the request hash.
+	 */
+	if (request->in_request_hash) {
+		if (!rbtree_deletebydata(pl, &request->packet)) {
+			rad_assert(0 == 1);
+		}
+		request->in_request_hash = false;
+	}
+
+#ifdef WITH_PROXY
+	/*
+	 *	Wait for the proxy ID to expire.  This allows us to
+	 *	avoid re-use of proxy IDs for a while.
+	 */
+	if (request->in_proxy_hash) {
+		rad_assert(request->proxy != NULL);
+
+		fr_event_now(el, &now);
+		when = request->proxy->timestamp;
+
+#ifdef WITH_COA
+		if (((request->proxy->code == PW_CODE_COA_REQUEST) ||
+		     (request->proxy->code == PW_CODE_DISCONNECT_REQUEST)) &&
+		    (request->packet->code != request->proxy->code)) {
+			when.tv_sec += request->home_server->coa_mrd;
+		} else
+#endif
+			timeradd(&when, request_response_window(request), &when);
+
+		/*
+		 *	We haven't received all responses, AND there's still
+		 *	time to wait.  Do so.
+		 */
+		if ((request->num_proxied_requests > request->num_proxied_responses) &&
+#ifdef WITH_TCP
+		    (request->home_server->proto != IPPROTO_TCP) &&
+#endif
+		    timercmp(&now, &when, <)) {
+			RDEBUG("Waiting for more responses from the home server");
+			goto wait_some_more;
+		}
+
+		/*
+		 *	Time to remove it.
+		 */
+		remove_from_proxy_hash(request);
+	}
+#endif
+
+#ifdef HAVE_PTHREAD_H
+	/*
+	 *	If there's no children, we can mark the request as done.
+	 */
+	if (!spawn_flag) request->child_state = REQUEST_DONE;
+#endif
+
+	/*
+	 *	If the child is still running, wait for it to be finished.
+	 */
+	if (request->child_state <= REQUEST_RUNNING) {
+		gettimeofday(&now, NULL);
+#ifdef WITH_PROXY
+	wait_some_more:
+#endif
+		when = now;
+		if (request->delay < (USEC / 3)) request->delay = USEC / 3;
+		tv_add(&when, request->delay);
+		request->delay += request->delay >> 1;
+		if (request->delay > (10 * USEC)) request->delay = 10 * USEC;
+
+		STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+		return;
+	}
+
+#ifdef HAVE_PTHREAD_H
+	rad_assert(request->child_pid == NO_SUCH_CHILD_PID);
+#endif
+
+#ifdef WITH_COA
+	/*
+	 *	Now that the child is done, free the CoA packet.  If
+	 *	the CoA is running, it's already been separated.
+	 */
+	if (request->coa) TALLOC_FREE(request->coa);
+#endif
+
+
+	/*
+	 *	@todo: do final states for TCP sockets, too?
+	 */
+	request_stats_final(request);
+#ifdef WITH_TCP
+	if (request->listener) {
+		request->listener->count--;
+
+		/*
+		 *	If we're the last one, remove the listener now.
+		 */
+		if ((request->listener->count == 0) &&
+		    (request->listener->status >= RAD_LISTEN_STATUS_FROZEN)) {
+			event_new_fd(request->listener);
+		}
+	}
+#endif
+
+	if (request->packet) {
+		RDEBUG2("Cleaning up request packet ID %u with timestamp +%d due to %s",
+			request->packet->id,
+			(unsigned int) (request->timestamp - fr_start_time),
+			action_codes[original]);
+	} /* else don't print anything */
+
+	ASSERT_MASTER;
+	fr_event_delete(el, &request->ev);
+	request_free(request);
+}
+
+
+static void request_cleanup_delay_init(REQUEST *request)
+{
+	struct timeval now, when;
+
+	VERIFY_REQUEST(request);
+
+	/*
+	 *	Do cleanup delay ONLY for RADIUS packets from a real
+	 *	client.  Everything else just gets cleaned up
+	 *	immediately.
+	 */
+	if (request->packet->dst_port == 0) goto done;
+
+	/*
+	 *	Accounting packets shouldn't be retransmitted.  They
+	 *	should always be updated with Acct-Delay-Time.
+	 */
+#ifdef WITH_ACCOUNTING
+	if (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) goto done;
+#endif
+
+#ifdef WITH_DHCP
+	if (request->listener->type == RAD_LISTEN_DHCP) goto done;
+#endif
+
+#ifdef WITH_VMPS
+	if (request->listener->type == RAD_LISTEN_VQP) goto done;
+#endif
+
+	if (!request->root->cleanup_delay) goto done;
+
+	gettimeofday(&now, NULL);
+
+	rad_assert(request->reply->timestamp.tv_sec != 0);
+	when = request->reply->timestamp;
+
+	request->delay = request->root->cleanup_delay;
+	when.tv_sec += request->delay;
+
+	/*
+	 *	Set timer for when we need to clean it up.
+	 */
+	if (timercmp(&when, &now, >)) {
+#ifdef DEBUG_STATE_MACHINE
+		if (rad_debug_lvl) printf("(%u) ********\tNEXT-STATE %s -> %s\n", request->number, __FUNCTION__, "request_cleanup_delay");
+#endif
+		request->process = request_cleanup_delay;
+
+		if (!we_are_master()) {
+			FINAL_STATE(REQUEST_CLEANUP_DELAY);
+			return;
+		}
+
+		/*
+		 *	Update this if we can, otherwise let the timers pick it up.
+		 */
+		request->child_state = REQUEST_CLEANUP_DELAY;
+#ifdef HAVE_PTHREAD_H
+		rad_assert(request->child_pid == NO_SUCH_CHILD_PID);
+#endif
+		STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+		return;
+	}
+
+	/*
+	 *	Otherwise just clean it up.
+	 */
+done:
+	request_done(request, FR_ACTION_DONE);
+}
+
+
+/*
+ *	Enforce max_request_time.
+ */
+static bool request_max_time(REQUEST *request)
+{
+	struct timeval now, when;
+	rad_assert(request->magic == REQUEST_MAGIC);
+#ifdef DEBUG_STATE_MACHINE
+	int action = FR_ACTION_TIMER;
+#endif
+
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+	ASSERT_MASTER;
+
+	/*
+	 *	The child thread has acknowledged it's done.
+	 *	Transition to the DONE state.
+	 *
+	 *	If the request was marked STOP, then the "check for
+	 *	stop" macro already took care of it.
+	 */
+	if (request->child_state == REQUEST_DONE) {
+		request_done(request, FR_ACTION_DONE);
+		return true;
+	}
+
+	/*
+	 *	The request is still running.  Enforce max_request_time.
+	 */
+	fr_event_now(el, &now);
+	when = request->packet->timestamp;
+	when.tv_sec += request->root->max_request_time;
+
+	/*
+	 *	Taking too long: tell it to die.
+	 */
+	if (timercmp(&now, &when, >=)) {
+#ifdef HAVE_PTHREAD_H
+		/*
+		 *	If there's a child thread processing it,
+		 *	complain.
+		 */
+		if (spawn_flag &&
+		    (pthread_equal(request->child_pid, NO_SUCH_CHILD_PID) == 0)) {
+			ERROR("Unresponsive child for request %u, in component %s module %s",
+			      request->number,
+			      request->component ? request->component : "<core>",
+			      request->module ? request->module : "<core>");
+			request->max_time = true;
+
+			exec_trigger(request, NULL, "server.thread.unresponsive", true);
+		}
+#endif
+		/*
+		 *	Tell the request that it's done.
+		 */
+		request_done(request, FR_ACTION_MAX_TIME);
+		return true;
+	}
+
+	/*
+	 *	Sleep for some more.  We HOPE that the child will
+	 *	become responsive at some point in the future.  We do
+	 *	this by adding 50% to the current timer.
+	 */
+	when = now;
+	tv_add(&when, request->delay);
+	request->delay += request->delay >> 1;
+	STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+	return false;
+}
+
+static void request_queue_or_run(REQUEST *request,
+				 fr_request_process_t process)
+{
+#ifdef DEBUG_STATE_MACHINE
+	int action = FR_ACTION_TIMER;
+#endif
+
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+
+	/*
+	 *	Do this here so that fewer other functions need to do
+	 *	it.
+	 */
+	if (request->master_state == REQUEST_STOP_PROCESSING) {
+#ifdef DEBUG_STATE_MACHINE
+		if (rad_debug_lvl) printf("(%u) ********\tSTATE %s M-%s causes C-%s-> C-%s\t********\n",
+				       request->number, __FUNCTION__,
+				       master_state_names[request->master_state],
+				       child_state_names[request->child_state],
+				       child_state_names[REQUEST_DONE]);
+#endif
+		request_done(request, FR_ACTION_CANCELLED);
+		return;
+	}
+
+	request->process = process;
+
+	if (we_are_master()) {
+		struct timeval when;
+
+		/*
+		 *	(re) set the initial delay.
+		 */
+		request->delay = request_init_delay(request);
+		if (request->delay > USEC) request->delay = USEC;
+		gettimeofday(&when, NULL);
+		tv_add(&when, request->delay);
+		request->delay += request->delay >> 1;
+
+		STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+
+#ifdef HAVE_PTHREAD_H
+		if (spawn_flag) {
+			/*
+			 *	A child thread will eventually pick it up.
+			 */
+			if (request_enqueue(request)) return;
+
+			/*
+			 *	Otherwise we're not going to do anything with
+			 *	it...
+			 */
+			request_done(request, FR_ACTION_INTERNAL_FAILURE);
+			return;
+		}
+#endif
+	}
+
+	request->child_state = REQUEST_RUNNING;
+	request->process(request, FR_ACTION_RUN);
+
+#ifdef WNOHANG
+	/*
+	 *	Requests that care about child process exit
+	 *	codes have already either called
+	 *	rad_waitpid(), or they've given up.
+	 */
+	while (waitpid(-1, NULL, WNOHANG) > 0);
+#endif
+}
+
+void request_inject(REQUEST *request)
+{
+	request_queue_or_run(request, request_running);
+}
+
+
+static void request_dup(REQUEST *request)
+{
+	ERROR("(%u) Ignoring duplicate packet from "
+	      "client %s port %d - ID: %u due to unfinished request "
+	      "in component %s module %s",
+	      request->number, request->client->shortname,
+	      request->packet->src_port,request->packet->id,
+	      request->component, request->module);
+}
+
+
+/** Sit on a request until it's time to clean it up.
+ *
+ *  A NAS may not see a response from the server.  When the NAS
+ *  retransmits, we want to be able to send a cached reply back.  The
+ *  alternative is to re-process the packet, which does bad things for
+ *  EAP, among others.
+ *
+ *  IF we do see a NAS retransmit, we extend the cleanup delay,
+ *  because the NAS might miss our cached reply.
+ *
+ *  Otherwise, once we reach cleanup_delay, we transition to DONE.
+ *
+ *  \dot
+ *	digraph cleanup_delay {
+ *		cleanup_delay;
+ *		send_reply [ label = "send_reply\nincrease cleanup delay" ];
+ *
+ *		cleanup_delay -> send_reply [ label = "DUP" ];
+ *		send_reply -> cleanup_delay;
+ *		cleanup_delay -> proxy_reply_too_late [ label = "PROXY_REPLY", arrowhead = "none" ];
+ *		cleanup_delay -> cleanup_delay [ label = "TIMER < timeout" ];
+ *		cleanup_delay -> done [ label = "TIMER >= timeout" ];
+ *	}
+ *  \enddot
+ */
+static void request_cleanup_delay(REQUEST *request, int action)
+{
+	struct timeval when, now;
+
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+	ASSERT_MASTER;
+	COA_SEPARATE;
+	CHECK_FOR_STOP;
+
+	switch (action) {
+	case FR_ACTION_DUP:
+		if (request->reply->code != 0) {
+			DEBUG("(%u) Sending duplicate reply to "
+			      "client %s port %d - ID: %u",
+			      request->number, request->client->shortname,
+			      request->packet->src_port,request->packet->id);
+			request->listener->send(request->listener, request);
+		} else {
+			RDEBUG("No reply.  Ignoring retransmit");
+		}
+
+		/*
+		 *	Double the cleanup_delay to catch retransmits.
+		 */
+		when = request->reply->timestamp;
+		request->delay += request->delay;
+		when.tv_sec += request->delay;
+
+		STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+		break;
+
+#ifdef WITH_PROXY
+	case FR_ACTION_PROXY_REPLY:
+		proxy_reply_too_late(request);
+		break;
+#endif
+
+	case FR_ACTION_TIMER:
+		fr_event_now(el, &now);
+
+		rad_assert(request->root->cleanup_delay > 0);
+
+		when = request->reply->timestamp;
+		when.tv_sec += request->root->cleanup_delay;
+
+		if (timercmp(&when, &now, >)) {
+#ifdef DEBUG_STATE_MACHINE
+			if (rad_debug_lvl) printf("(%u) ********\tNEXT-STATE %s -> %s\n", request->number, __FUNCTION__, "request_cleanup_delay");
+#endif
+			STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+			return;
+		} /* else it's time to clean up */
+
+		request_done(request, FR_ACTION_CLEANUP_DELAY);
+		break;
+
+	default:
+		RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]);
+		break;
+	}
+}
+
+
+/** Sit on a request until it's time to respond to it.
+ *
+ *  For security reasons, rejects (and maybe some other) packets are
+ *  delayed for a while before we respond.  This delay means that
+ *  badly behaved NASes don't hammer the server with authentication
+ *  attempts.
+ *
+ *  Otherwise, once we reach response_delay, we send the reply, and
+ *  transition to cleanup_delay.
+ *
+ *  \dot
+ *	digraph response_delay {
+ *		response_delay -> proxy_reply_too_late [ label = "PROXY_REPLY", arrowhead = "none" ];
+ *		response_delay -> response_delay [ label = "DUP, TIMER < timeout" ];
+ *		response_delay -> send_reply [ label = "TIMER >= timeout" ];
+ *		send_reply -> cleanup_delay;
+ *	}
+ *  \enddot
+ */
+static void request_response_delay(REQUEST *request, int action)
+{
+	struct timeval when, now;
+
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+	ASSERT_MASTER;
+	COA_SEPARATE;
+	CHECK_FOR_STOP;
+
+	switch (action) {
+	case FR_ACTION_DUP:
+		RDEBUG("(%u) Discarding duplicate request from "
+		      "client %s port %d - ID: %u due to delayed response",
+		      request->number, request->client->shortname,
+		      request->packet->src_port,request->packet->id);
+		break;
+
+#ifdef WITH_PROXY
+	case FR_ACTION_PROXY_REPLY:
+		proxy_reply_too_late(request);
+		break;
+#endif
+
+	case FR_ACTION_TIMER:
+		fr_event_now(el, &now);
+
+		/*
+		 *	See if it's time to send the reply.  If not,
+		 *	we wait some more.
+		 */
+		when = request->reply->timestamp;
+
+		tv_add(&when, request->response_delay.tv_sec * USEC);
+		tv_add(&when, request->response_delay.tv_usec);
+
+		if (timercmp(&when, &now, >)) {
+#ifdef DEBUG_STATE_MACHINE
+			if (rad_debug_lvl) printf("(%u) ********\tNEXT-STATE %s -> %s\n", request->number, __FUNCTION__, "request_response_delay");
+#endif
+			STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+			return;
+		} /* else it's time to send the reject */
+
+		RDEBUG2("Sending delayed response");
+		request->listener->encode(request->listener, request);
+		debug_packet(request, request->reply, false);
+		request->listener->send(request->listener, request);
+
+		/*
+		 *	Clean up the request.
+		 */
+		request_cleanup_delay_init(request);
+		break;
+
+	default:
+		RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]);
+		break;
+	}
+}
+
+
+static int request_pre_handler(REQUEST *request, UNUSED int action)
+{
+	int rcode;
+
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+
+	if (request->master_state == REQUEST_STOP_PROCESSING) return 0;
+
+	/*
+	 *	Don't decode the packet if it's an internal "fake"
+	 *	request.  Instead, just return so that the caller can
+	 *	process it.
+	 */
+	if (request->packet->dst_port == 0) {
+		request->username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY);
+		request->password = fr_pair_find_by_num(request->packet->vps, PW_USER_PASSWORD, 0, TAG_ANY);
+		return 1;
+	}
+
+	if (!request->packet->vps) { /* FIXME: check for correct state */
+		rcode = request->listener->decode(request->listener, request);
+
+#ifdef WITH_UNLANG
+		if (debug_condition) {
+			/*
+			 *	Ignore parse errors.
+			 */
+			if (radius_evaluate_cond(request, RLM_MODULE_OK, 0, debug_condition) == 1) {
+				request->log.lvl = L_DBG_LVL_2;
+				request->log.func = vradlog_request;
+			}
+		}
+#endif
+
+		debug_packet(request, request->packet, true);
+	} else {
+		rcode = 0;
+	}
+
+	if (rcode < 0) {
+		RATE_LIMIT(INFO("Dropping packet without response because of error: %s (from client %s)", fr_strerror(), request->client->shortname));
+		request->reply->offset = -2; /* bad authenticator */
+		return 0;
+	}
+
+	if (!request->username) {
+		request->username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY);
+	}
+
+	return 1;
+}
+
+
+/**  Do the final processing of a request before we reply to the NAS.
+ *
+ *  Various cleanups, suppress responses, copy Proxy-State, and set
+ *  response_delay or cleanup_delay;
+ */
+static void request_finish(REQUEST *request, int action)
+{
+	VALUE_PAIR *vp;
+
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+	CHECK_FOR_STOP;
+
+	(void) action;	/* -Wunused */
+
+#ifdef WITH_COA
+	/*
+	 *	Don't do post-auth if we're a CoA request originated
+	 *	from an Access-Request.  See request_alloc_coa() for
+	 *	details.
+	 */
+	if ((request->options & RAD_REQUEST_OPTION_COA) != 0) goto done;
+#endif
+
+	/*
+	 *	Override the response code if a control:Response-Packet-Type attribute is present.
+	 */
+	vp = fr_pair_find_by_num(request->config, PW_RESPONSE_PACKET_TYPE, 0, TAG_ANY);
+	if (vp) {
+		if (vp->vp_integer == 256) {
+			RDEBUG2("Not responding to request");
+			fr_pair_delete_by_num(&request->reply->vps, PW_RESPONSE_PACKET_TYPE, 0, TAG_ANY);
+			request->reply->code = 0;
+		} else {
+			request->reply->code = vp->vp_integer;
+		}
+	}
+	/*
+	 *	Catch Auth-Type := Reject BEFORE proxying the packet.
+	 */
+	else if (request->packet->code == PW_CODE_ACCESS_REQUEST) {
+		if (request->reply->code == 0) {
+			vp = fr_pair_find_by_num(request->config, PW_AUTH_TYPE, 0, TAG_ANY);
+			if (!vp || (vp->vp_integer != 5)) {
+				RDEBUG2("There was no response configured: "
+					"rejecting request");
+			}
+
+			request->reply->code = PW_CODE_ACCESS_REJECT;
+		}
+	}
+
+	/*
+	 *	Copy Proxy-State from the request to the reply.
+	 */
+	vp = fr_pair_list_copy_by_num(request->reply, request->packet->vps,
+		       PW_PROXY_STATE, 0, TAG_ANY);
+	if (vp) fr_pair_add(&request->reply->vps, vp);
+
+	/*
+	 *	Call Post-Auth for Access-Request packets.
+	 */
+	if (request->packet->code == PW_CODE_ACCESS_REQUEST) {
+		rad_postauth(request);
+
+		vp = fr_pair_find_by_num(request->config, PW_RESPONSE_PACKET_TYPE, 0, TAG_ANY);
+		if (vp && (vp->vp_integer == 256)) {
+			RDEBUG2("Not responding to request");
+			request->reply->code = 0;
+		}
+	}
+
+#ifdef WITH_COA
+	/*
+	 *	Maybe originate a CoA request.
+	 */
+	if ((action == FR_ACTION_RUN) && !request->proxy && request->coa) {
+		request_coa_originate(request);
+	}
+#endif
+
+	/*
+	 *	Clean up.  These are no longer needed.
+	 */
+	gettimeofday(&request->reply->timestamp, NULL);
+
+	/*
+	 *	Fake packets get marked as "done", and have the
+	 *	proxy-reply section deal with the reply attributes.
+	 *	We therefore don't free the reply attributes.
+	 */
+	if (request->packet->dst_port == 0) {
+		RDEBUG("Finished internally proxied request.");
+		FINAL_STATE(REQUEST_DONE);
+		return;
+	}
+
+#ifdef WITH_DETAIL
+	/*
+	 *	Always send the reply to the detail listener.
+	 */
+	if (request->listener->type == RAD_LISTEN_DETAIL) {
+		request->simul_max = 1;
+
+		/*
+		 *	But only print the reply if there is one.
+		 */
+		if (request->reply->code != 0) {
+			debug_packet(request, request->reply, false);
+		}
+
+		request->listener->send(request->listener, request);
+		goto done;
+	}
+#endif
+
+	/*
+	 *	Ignore all "do not respond" packets.
+	 *	Except for the detail ones, which need to ping
+	 *	the detail file reader so that it will retransmit.
+	 */
+	if (!request->reply->code) {
+		RDEBUG("Not sending reply to client.");
+		goto done;
+	}
+
+	/*
+	 *	If it's not in the request hash, we MIGHT not want to
+	 *	send a reply.
+	 *
+	 *	If duplicate packets are allowed, then then only
+	 *	reason to NOT be in the request hash is because we
+	 *	don't want to send a reply.
+	 *
+	 *	FIXME: this is crap.  The rest of the state handling
+	 *	should use a different field so that we don't have two
+	 *	meanings for it.
+	 *
+	 *	Otherwise duplicates are forbidden, and the request is
+	 *	SUPPOSED to avoid the request hash.
+	 *
+	 *	In that case, we need to send a reply.
+	 */
+	if (!request->in_request_hash &&
+	    !request->listener->nodup) {
+		RDEBUG("Suppressing reply to client.");
+		goto done;
+	}
+
+	/*
+	 *	See if we need to delay an Access-Reject packet.
+	 */
+	if ((request->packet->code == PW_CODE_ACCESS_REQUEST) &&
+	    (request->reply->code == PW_CODE_ACCESS_REJECT) &&
+	    (request->root->reject_delay.tv_sec > 0)) {
+		request->response_delay = request->root->reject_delay;
+
+		vp = fr_pair_find_by_num(request->reply->vps, PW_FREERADIUS_RESPONSE_DELAY, 0, TAG_ANY);
+		if (vp) {
+			if (vp->vp_integer <= 10) {
+				request->response_delay.tv_sec = vp->vp_integer;
+			} else {
+				request->response_delay.tv_sec = 10;
+			}
+			request->response_delay.tv_usec = 0;
+		} else {
+			vp = fr_pair_find_by_num(request->reply->vps, PW_FREERADIUS_RESPONSE_DELAY_USEC, 0, TAG_ANY);
+			if (vp) {
+				if (vp->vp_integer <= 10 * USEC) {
+					request->response_delay.tv_sec = vp->vp_integer / USEC;
+					request->response_delay.tv_usec = vp->vp_integer % USEC;
+				} else {
+					request->response_delay.tv_sec = 10;
+					request->response_delay.tv_usec = 0;
+				}
+			}
+		}
+
+#ifdef WITH_PROXY
+		/*
+		 *	If we timed out a proxy packet, don't delay
+		 *	the reject any more.
+		 */
+		if (request->proxy && !request->proxy_reply) {
+			request->response_delay.tv_sec = 0;
+			request->response_delay.tv_usec = 0;
+		}
+#endif
+	}
+
+	/*
+	 *	Send the reply.
+	 */
+	if ((request->response_delay.tv_sec == 0) &&
+	    (request->response_delay.tv_usec == 0)) {
+
+		/*
+		 *	Don't print a reply if there's none to send.
+		 */
+		if (request->reply->code != 0) {
+			if (rad_debug_lvl && request->state &&
+			    (request->reply->code == PW_CODE_ACCESS_ACCEPT)) {
+				if (!fr_pair_find_by_num(request->packet->vps, PW_STATE, 0, TAG_ANY)) {
+					RWDEBUG2("Unused attributes found in &session-state:");
+				}
+			}
+
+			request->listener->encode(request->listener, request);
+			debug_packet(request, request->reply, false);
+			request->listener->send(request->listener, request);
+		}
+
+	done:
+		RDEBUG2("Finished request");
+		request_cleanup_delay_init(request);
+
+	} else {
+		/*
+		 *	Encode and sign it here, so that the master
+		 *	thread can just send the encoded data, which
+		 *	means it does less work.
+		 */
+		RDEBUG2("Delaying response for %d.%06d seconds",
+			(int) request->response_delay.tv_sec, (int) request->response_delay.tv_usec);
+		request->listener->encode(request->listener, request);
+		request->process = request_response_delay;
+
+		FINAL_STATE(REQUEST_RESPONSE_DELAY);
+	}
+}
+
+/** Process a request from a client.
+ *
+ *  The outcome might be that the request is proxied.
+ *
+ *  \dot
+ *	digraph running {
+ *		running -> running [ label = "TIMER < max_request_time" ];
+ *		running -> done [ label = "TIMER >= max_request_time" ];
+ *		running -> proxy [ label = "proxied" ];
+ *		running -> dup [ label = "DUP", arrowhead = "none" ];
+ *	}
+ *  \enddot
+ */
+static void request_running(REQUEST *request, int action)
+{
+	int rcode;
+
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+	CHECK_FOR_STOP;
+
+	switch (action) {
+	case FR_ACTION_TIMER:
+		(void) request_max_time(request);
+		break;
+
+	case FR_ACTION_DUP:
+		request_dup(request);
+		break;
+
+	case FR_ACTION_RUN:
+		if (!request_pre_handler(request, action)) {
+#ifdef DEBUG_STATE_MACHINE
+			if (rad_debug_lvl) printf("(%u) ********\tSTATE %s failed in pre-handler C-%s -> C-%s\t********\n",
+					       request->number, __FUNCTION__,
+					       child_state_names[request->child_state],
+					       child_state_names[REQUEST_DONE]);
+#endif
+			FINAL_STATE(REQUEST_DONE);
+			break;
+		}
+
+		rad_assert(request->handle != NULL);
+		request->handle(request);
+
+#ifdef WITH_PROXY
+		/*
+		 *	We may need to send a proxied request.
+		 */
+		rcode = request_will_proxy(request);
+		if (rcode == 1) {
+#ifdef DEBUG_STATE_MACHINE
+			if (rad_debug_lvl) printf("(%u) ********\tWill Proxy\t********\n", request->number);
+#endif
+			/*
+			 *	If this fails, it
+			 *	takes care of setting
+			 *	up the post proxy fail
+			 *	handler.
+			 */
+		retry_proxy:
+			if (request_proxy(request) < 0) {
+				if (request->home_server && request->home_server->virtual_server) goto req_finished;
+
+				if (request->home_pool && request->home_server &&
+				    HOME_SERVER_IS_DEAD(request->home_server)) {
+					VALUE_PAIR *vp;
+					REALM *realm = NULL;
+					home_server_t *home = NULL;
+
+					vp = fr_pair_find_by_num(request->config, PW_PROXY_TO_REALM, 0, TAG_ANY);
+					if (vp) realm = realm_find2(vp->vp_strvalue);
+
+					/*
+					 *	Since request->home_server is dead,
+					 *	this function won't pick the same home server as before.
+					 */
+					if (realm) home = home_server_ldb(vp->vp_strvalue, request->home_pool, request);
+					if (home) {
+						home_server_update_request(home, request);
+						goto retry_proxy;
+					}
+				}
+
+				(void) setup_post_proxy_fail(request);
+				process_proxy_reply(request, NULL);
+				goto req_finished;
+			}
+
+		} else if (rcode < 0) {
+			/*
+			 *	No live home servers, run Post-Proxy-Type Fail.
+			 */
+			(void) setup_post_proxy_fail(request);
+			process_proxy_reply(request, NULL);
+			goto req_finished;
+		} else
+#endif
+		{
+#ifdef DEBUG_STATE_MACHINE
+			if (rad_debug_lvl) printf("(%u) ********\tFinished\t********\n", request->number);
+#endif
+
+#ifdef WITH_PROXY
+		req_finished:
+#endif
+			request_finish(request, action);
+		}
+		break;
+
+	default:
+		RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]);
+		break;
+	}
+}
+
+int request_receive(TALLOC_CTX *ctx, rad_listen_t *listener, RADIUS_PACKET *packet,
+		    RADCLIENT *client, RAD_REQUEST_FUNP fun)
+{
+	uint32_t count;
+	RADIUS_PACKET **packet_p;
+	REQUEST *request = NULL;
+	struct timeval now;
+	listen_socket_t *sock = NULL;
+
+	VERIFY_PACKET(packet);
+
+	/*
+	 *	Set the last packet received.
+	 */
+	gettimeofday(&now, NULL);
+
+	packet->timestamp = now;
+
+#ifdef WITH_ACCOUNTING
+	if (listener->type != RAD_LISTEN_DETAIL)
+#endif
+
+#ifdef WITH_TCP
+	{
+		sock = listener->data;
+		sock->last_packet = now.tv_sec;
+
+		packet->proto = sock->proto;
+	}
+#endif
+
+	/*
+	 *	Skip everything if required.
+	 */
+	if (listener->nodup) goto skip_dup;
+
+	packet_p = rbtree_finddata(pl, &packet);
+	if (packet_p) {
+		rad_child_state_t child_state;
+		char const *old_module;
+
+		request = fr_packet2myptr(REQUEST, packet, packet_p);
+		rad_assert(request->in_request_hash);
+		child_state = request->child_state;
+		old_module = request->module;
+
+		/*
+		 *	Same src/dst ip/port, length, and
+		 *	authentication vector: must be a duplicate.
+		 */
+		if ((request->packet->data_len == packet->data_len) &&
+		    (memcmp(request->packet->vector, packet->vector,
+			    sizeof(packet->vector)) == 0)) {
+
+#ifdef WITH_STATS
+			switch (packet->code) {
+			case PW_CODE_ACCESS_REQUEST:
+				FR_STATS_INC(auth, total_dup_requests);
+				break;
+
+#ifdef WITH_ACCOUNTING
+			case PW_CODE_ACCOUNTING_REQUEST:
+				FR_STATS_INC(acct, total_dup_requests);
+				break;
+#endif
+#ifdef WITH_COA
+			case PW_CODE_COA_REQUEST:
+				FR_STATS_INC(coa, total_dup_requests);
+				break;
+
+			case PW_CODE_DISCONNECT_REQUEST:
+				FR_STATS_INC(dsc, total_dup_requests);
+				break;
+#endif
+
+			default:
+				break;
+			}
+#endif	/* WITH_STATS */
+
+			/*
+			 *	Tell the state machine that there's a
+			 *	duplicate request.
+			 */
+			request->process(request, FR_ACTION_DUP);
+			return 0; /* duplicate of live request */
+		}
+
+		/*
+		 *	Mark the request as done ASAP, and before we
+		 *	log anything.  The child may stop processing
+		 *	the request just as we're logging the
+		 *	complaint.
+		 */
+		request_done(request, FR_ACTION_CONFLICT);
+		request = NULL;
+
+		/*
+		 *	It's a new request, not a duplicate.  If the
+		 *	old one is done, then we can clean it up.
+		 */
+		if (child_state <= REQUEST_RUNNING) {
+			/*
+			 *	The request is still QUEUED or RUNNING.  That's a problem.
+			 */
+			ERROR("Received conflicting packet from "
+			      "client %s port %d - ID: %u due to "
+			      "unfinished request in module %s.  Giving up on old request.",
+			      client->shortname,
+			      packet->src_port, packet->id,
+			      old_module);
+
+#ifdef WITH_STATS
+			switch (packet->code) {
+			case PW_CODE_ACCESS_REQUEST:
+				FR_STATS_INC(auth, total_conflicts);
+				break;
+
+#ifdef WITH_ACCOUNTING
+			case PW_CODE_ACCOUNTING_REQUEST:
+				FR_STATS_INC(acct, total_conflicts);
+				break;
+#endif
+#ifdef WITH_COA
+			case PW_CODE_COA_REQUEST:
+				FR_STATS_INC(coa, total_conflicts);
+				break;
+
+			case PW_CODE_DISCONNECT_REQUEST:
+				FR_STATS_INC(dsc, total_conflicts);
+				break;
+#endif
+
+			default:
+				break;
+			}
+#endif	/* WITH_STATS */
+		}
+	} /* else the new packet is unique */
+
+	/*
+	 *	Quench maximum number of outstanding requests.
+	 */
+	if (main_config.max_requests &&
+	    ((count = rbtree_num_elements(pl)) > main_config.max_requests)) {
+		RATE_LIMIT(ERROR("Dropping request (%d is too many): from client %s port %d - ID: %d", count,
+				 client->shortname,
+				 packet->src_port, packet->id);
+			   WARN("Please check the configuration file.\n"
+				"\tThe value for 'max_requests' is probably set too low.\n"));
+
+		exec_trigger(NULL, NULL, "server.max_requests", true);
+		return 0;
+	}
+
+skip_dup:
+	/*
+	 *	Rate-limit the incoming packets
+	 */
+	if (sock && sock->max_rate) {
+		uint32_t pps;
+
+		pps = rad_pps(&sock->rate_pps_old, &sock->rate_pps_now, &sock->rate_time, &now);
+		if (pps > sock->max_rate) {
+			DEBUG("Dropping request due to rate limiting");
+			return 0;
+		}
+		sock->rate_pps_now++;
+	}
+
+	/*
+	 *	Allocate a pool for the request.
+	 */
+	if (!ctx) {
+		ctx = talloc_pool(NULL, main_config.talloc_pool_size);
+		if (!ctx) return 0;
+		talloc_set_name_const(ctx, "request_receive_pool");
+
+		/*
+		 *	The packet is still allocated from a different
+		 *	context, but oh well.
+		 */
+		(void) talloc_steal(ctx, packet);
+	}
+
+	request = request_setup(ctx, listener, packet, client, fun);
+	if (!request) {
+		talloc_free(ctx);
+		return 1;
+	}
+
+	/*
+	 *	Mark it as a "real" request with a context.
+	 */
+	request->options |= RAD_REQUEST_OPTION_CTX;
+
+	/*
+	 *	Remember the request in the list.
+	 */
+	if (!listener->nodup) {
+		if (!rbtree_insert(pl, &request->packet)) {
+			RERROR("Failed to insert request in the list of live requests: discarding it");
+			request_done(request, FR_ACTION_INTERNAL_FAILURE);
+			return 1;
+		}
+
+		request->in_request_hash = true;
+	}
+
+	/*
+	 *	Process it.  Send a response, and free it.
+	 */
+	if (listener->synchronous) {
+#ifdef WITH_DETAIL
+		rad_assert(listener->type != RAD_LISTEN_DETAIL);
+#endif
+
+		request->listener->decode(request->listener, request);
+		request->username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY);
+		request->password = fr_pair_find_by_num(request->packet->vps, PW_USER_PASSWORD, 0, TAG_ANY);
+
+		fun(request);
+
+		if (request->reply->code != 0) {
+			request->listener->send(request->listener, request);
+		} else {
+			RDEBUG("Not sending reply");
+		}
+
+		/*
+		 *	Don't do delayed reject.  Oh well.
+		 */
+		request_free(request);
+		return 1;
+	}
+
+	/*
+	 *	Otherwise, insert it into the state machine.
+	 *	The child threads will take care of processing it.
+	 */
+	request_queue_or_run(request, request_running);
+
+	return 1;
+}
+
+
+static REQUEST *request_setup(TALLOC_CTX *ctx, rad_listen_t *listener, RADIUS_PACKET *packet,
+			      RADCLIENT *client, RAD_REQUEST_FUNP fun)
+{
+	REQUEST *request;
+
+	/*
+	 *	Create and initialize the new request.
+	 */
+	request = request_alloc(ctx);
+	if (!request) {
+		ERROR("No memory");
+		return NULL;
+	}
+	request->reply = rad_alloc_reply(request, packet);
+	if (!request->reply) {
+		ERROR("No memory");
+		talloc_free(request);
+		return NULL;
+	}
+
+	request->listener = listener;
+	request->client = client;
+	request->packet = talloc_steal(request, packet);
+	request->number = request_num_counter++;
+	request->priority = listener->type;
+	request->master_state = REQUEST_ACTIVE;
+	request->child_state = REQUEST_RUNNING;
+#ifdef DEBUG_STATE_MACHINE
+	if (rad_debug_lvl) printf("(%u) ********\tSTATE %s C-%s -> C-%s\t********\n",
+			       request->number, __FUNCTION__,
+			       child_state_names[request->child_state],
+			       child_state_names[REQUEST_RUNNING]);
+#endif
+	request->handle = fun;
+	NO_CHILD_THREAD;
+
+#ifdef WITH_STATS
+	request->listener->stats.last_packet = request->packet->timestamp.tv_sec;
+	if (packet->code == PW_CODE_ACCESS_REQUEST) {
+		request->client->auth.last_packet = request->packet->timestamp.tv_sec;
+		radius_auth_stats.last_packet = request->packet->timestamp.tv_sec;
+#ifdef WITH_ACCOUNTING
+	} else if (packet->code == PW_CODE_ACCOUNTING_REQUEST) {
+		request->client->acct.last_packet = request->packet->timestamp.tv_sec;
+		radius_acct_stats.last_packet = request->packet->timestamp.tv_sec;
+#endif
+	}
+#endif	/* WITH_STATS */
+
+	/*
+	 *	Status-Server packets go to the head of the queue.
+	 */
+	if (request->packet->code == PW_CODE_STATUS_SERVER) request->priority = 0;
+
+	/*
+	 *	Set virtual server identity
+	 */
+	if (client->server) {
+		request->server = client->server;
+	} else if (listener->server) {
+		request->server = listener->server;
+	} else {
+		request->server = NULL;
+	}
+
+	request->root = &main_config;
+#ifdef WITH_TCP
+	request->listener->count++;
+#endif
+
+	/*
+	 *	The request passes many of our sanity checks.
+	 *	From here on in, if anything goes wrong, we
+	 *	send a reject message, instead of dropping the
+	 *	packet.
+	 */
+
+	/*
+	 *	Build the reply template from the request.
+	 */
+
+	request->reply->sockfd = request->packet->sockfd;
+	request->reply->dst_ipaddr = request->packet->src_ipaddr;
+	request->reply->src_ipaddr = request->packet->dst_ipaddr;
+	request->reply->dst_port = request->packet->src_port;
+	request->reply->src_port = request->packet->dst_port;
+	request->reply->id = request->packet->id;
+	request->reply->code = 0; /* UNKNOWN code */
+	memcpy(request->reply->vector, request->packet->vector,
+	       sizeof(request->reply->vector));
+	request->reply->vps = NULL;
+	request->reply->data = NULL;
+	request->reply->data_len = 0;
+
+	return request;
+}
+
+#ifdef WITH_TCP
+/***********************************************************************
+ *
+ *	TCP Handlers.
+ *
+ ***********************************************************************/
+
+/*
+ *	Timer function for all TCP sockets.
+ */
+static void tcp_socket_timer(void *ctx)
+{
+	rad_listen_t *listener = talloc_get_type_abort(ctx, rad_listen_t);
+	listen_socket_t *sock = listener->data;
+	struct timeval end, now;
+	char buffer[256];
+	fr_socket_limit_t *limit;
+
+	ASSERT_MASTER;
+
+	if (listener->status != RAD_LISTEN_STATUS_KNOWN) return;
+
+	fr_event_now(el, &now);
+
+	limit = &sock->limit;
+
+	/*
+	 *	If we enforce a lifetime, do it now.
+	 */
+	if (limit->lifetime > 0) {
+		end.tv_sec = sock->opened + limit->lifetime;
+		end.tv_usec = 0;
+
+		if (timercmp(&end, &now, <=)) {
+			listener->print(listener, buffer, sizeof(buffer));
+			DEBUG("Reached maximum lifetime on socket %s", buffer);
+
+		do_close:
+
+#ifdef WITH_PROXY
+			/*
+			 *	Proxy sockets get frozen, so that we don't use
+			 *	them for new requests.  But we do keep them
+			 *	open to listen for replies to requests we had
+			 *	previously sent.
+			 */
+			if (listener->type == RAD_LISTEN_PROXY
+#ifdef WITH_COA_TUNNEL
+			    || listener->send_coa
+#endif
+				) {
+				PTHREAD_MUTEX_LOCK(&proxy_mutex);
+				if (!fr_packet_list_socket_freeze(proxy_list,
+								  listener->fd)) {
+					ERROR("Fatal error freezing socket: %s", fr_strerror());
+					fr_exit(1);
+				}
+				PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+			}
+#endif
+
+			/*
+			 *	Mark the socket as "don't use if at all possible".
+			 */
+			listener->status = RAD_LISTEN_STATUS_FROZEN;
+
+			/*
+			 *	If it's blocked, then push all of the requests to other sockets.
+			 */
+#ifdef WITH_TLS
+			if (listener->blocked) {
+				listener->status = RAD_LISTEN_STATUS_REMOVE_NOW;
+			}
+#endif
+
+			event_new_fd(listener);
+			return;
+		}
+	} else {
+		end = now;
+		end.tv_sec += 3600;
+	}
+
+	/*
+	 *	Enforce an idle timeout.
+	 */
+	if (limit->idle_timeout > 0) {
+		struct timeval idle;
+
+		rad_assert(sock->last_packet != 0);
+		idle.tv_sec = sock->last_packet + limit->idle_timeout;
+		idle.tv_usec = 0;
+
+		if (timercmp(&idle, &now, <=)) {
+			listener->print(listener, buffer, sizeof(buffer));
+			DEBUG("Reached idle timeout on socket %s", buffer);
+			goto do_close;
+		}
+
+		/*
+		 *	Enforce the minimum of idle timeout or lifetime.
+		 */
+		if (timercmp(&idle, &end, <)) {
+			end = idle;
+		}
+	}
+
+	/*
+	 *	Wake up at t + 0.5s.  The code above checks if the timers
+	 *	are <= t.  This addition gives us a bit of leeway.
+	 */
+	end.tv_usec = USEC / 2;
+
+	ASSERT_MASTER;
+	if (!fr_event_insert(el, tcp_socket_timer, listener, &end, &sock->ev)) {
+		rad_panic("Failed to insert event");
+	}
+}
+
+
+#ifdef WITH_PROXY
+/*
+ *	Called by socket_del to remove requests with this socket
+ */
+static int eol_proxy_listener(void *ctx, void *data)
+{
+	rad_listen_t *this = talloc_get_type_abort(ctx, rad_listen_t);
+	RADIUS_PACKET **proxy_p = data;
+	REQUEST *request;
+
+	request = fr_packet2myptr(REQUEST, proxy, proxy_p);
+	if (request->proxy_listener != this) return 0;
+
+	/*
+	 *	The normal "remove_from_proxy_hash" tries to grab the
+	 *	proxy mutex.  We already have it held, so grabbing it
+	 *	again will cause a deadlock.  Instead, call the "no
+	 *	lock" version of the function.
+	 */
+	rad_assert(request->in_proxy_hash == true);
+	remove_from_proxy_hash_nl(request, false);
+
+	/*
+	 *	Don't mark it as DONE.  The client can retransmit, and
+	 *	the packet SHOULD be re-proxied somewhere else.
+	 *
+	 *	Return "2" means that the rbtree code will remove it
+	 *	from the tree, and we don't need to do it ourselves.
+	 */
+	return 2;
+}
+#endif	/* WITH_PROXY */
+
+static int eol_listener(void *ctx, void *data)
+{
+	rad_listen_t *this = talloc_get_type_abort(ctx, rad_listen_t);
+	RADIUS_PACKET **packet_p = data;
+	REQUEST *request;
+
+	request = fr_packet2myptr(REQUEST, packet, packet_p);
+	if (request->listener != this) return 0;
+
+	request->master_state = REQUEST_STOP_PROCESSING;
+	request->process = request_done;
+
+	return 0;
+}
+#endif	/* WITH_TCP */
+
+#ifdef WITH_PROXY
+/***********************************************************************
+ *
+ *	Proxy handlers for the state machine.
+ *
+ ***********************************************************************/
+
+/*
+ *	Called with the proxy mutex held
+ */
+static void remove_from_proxy_hash_nl(REQUEST *request, bool yank)
+{
+	VERIFY_REQUEST(request);
+
+	if (!request->in_proxy_hash) return;
+
+#ifdef COA_TUNNEL
+	/*
+	 *	Track how many IDs are used.  This information
+	 *	helps the listen_coa_find() function get a
+	 *	listener which has free IDs.
+	 */
+	rad_assert(request->proxy_listener->num_ids_used > 0);
+	request->proxy_listener->num_ids_used--;
+#endif
+
+	fr_packet_list_id_free(proxy_list, request->proxy, yank);
+	request->in_proxy_hash = false;
+
+	/*
+	 *	On the FIRST reply, decrement the count of outstanding
+	 *	requests.  Note that this is NOT the count of sent
+	 *	packets, but whether or not the home server has
+	 *	responded at all.
+	 */
+	if (request->home_server &&
+	    request->home_server->currently_outstanding) {
+		request->home_server->currently_outstanding--;
+
+		/*
+		 *	If we're NOT sending it packets, AND it's been
+		 *	a while since we got a response, then we don't
+		 *	know if it's alive or dead.
+		 */
+		if ((request->home_server->currently_outstanding == 0) &&
+		    (request->home_server->state == HOME_STATE_ALIVE)) {
+			struct timeval when, now;
+
+			when.tv_sec = request->home_server->last_packet_recv ;
+			when.tv_usec = 0;
+
+			timeradd(&when, request_response_window(request), &when);
+			gettimeofday(&now, NULL);
+
+			/*
+			 *	last_packet + response_window
+			 *
+			 *	We *administratively* mark the home
+			 *	server as "unknown" state, because we
+			 *	haven't seen a packet for a while.
+			 */
+			if (timercmp(&now, &when, >)) {
+				request->home_server->state = HOME_STATE_UNKNOWN;
+				request->home_server->last_packet_sent = 0;
+				request->home_server->last_packet_recv = 0;
+			}
+		}
+	}
+
+	if (request->proxy_listener) {
+		request->proxy_listener->count--;
+	}
+	request->proxy_listener = NULL;
+
+	/*
+	 *	Got from YES in hash, to NO, not in hash while we hold
+	 *	the mutex.  This guarantees that when another thread
+	 *	grabs the mutex, the "not in hash" flag is correct.
+	 */
+}
+
+static void remove_from_proxy_hash(REQUEST *request)
+{
+	VERIFY_REQUEST(request);
+
+	/*
+	 *	Check this without grabbing the mutex because it's a
+	 *	lot faster that way.
+	 */
+	if (!request->in_proxy_hash) return;
+
+#ifdef WITH_TCP
+	/*
+	 *	Status-Server packets aren't removed from the proxy hash.  They're reused.
+	 *
+	 *	Unless we're tearing down the listener.
+	 */
+	if ((request->proxy->proto == IPPROTO_TCP) && (request->proxy->code == PW_CODE_STATUS_SERVER) &&
+	    request->proxy_listener && (request->proxy_listener->status < RAD_LISTEN_STATUS_EOL)) {
+		return;
+	}
+#endif
+
+	/*
+	 *	The "not in hash" flag is definitive.  However, if the
+	 *	flag says that it IS in the hash, there might still be
+	 *	a race condition where it isn't.
+	 */
+	PTHREAD_MUTEX_LOCK(&proxy_mutex);
+
+	if (!request->in_proxy_hash) {
+		PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+		return;
+	}
+
+	remove_from_proxy_hash_nl(request, true);
+
+	PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+}
+
+static int insert_into_proxy_hash(REQUEST *request)
+{
+	char buf[128];
+	int tries;
+	bool success = false;
+	void *proxy_listener;
+#ifdef WITH_COA_TUNNEL
+	bool reverse_coa = request->proxy_listener && (request->proxy_listener->type != RAD_LISTEN_PROXY);
+#endif
+
+	VERIFY_REQUEST(request);
+
+	rad_assert(request->proxy != NULL);
+	rad_assert(request->home_server != NULL);
+	rad_assert(proxy_list != NULL);
+
+
+	PTHREAD_MUTEX_LOCK(&proxy_mutex);
+	proxy_listener = request->proxy_listener; /* may or may not be NULL */
+	request->num_proxied_requests = 1;
+	request->num_proxied_responses = 0;
+
+	for (tries = 0; tries < 2; tries++) {
+		rad_listen_t *this;
+		listen_socket_t *sock;
+
+		RDEBUG3("proxy: Trying to allocate ID (%d/2)", tries);
+		success = fr_packet_list_id_alloc(proxy_list,
+						  request->home_server->proto,
+						  &request->proxy, &proxy_listener);
+		if (success) break;
+
+#ifdef WITH_COA_TUNNEL
+		/*
+		 *	Can't allocate an ID here, try to grab another
+		 *	listener by key.
+		 */
+		if (reverse_coa) {
+			int rcode;
+			VALUE_PAIR *vp;
+
+			/*
+			 *	Find the Originating-Realm key, which
+			 *	might not be the same as
+			 *	proxy_listener->key.
+			 */
+			vp = fr_pair_find_by_num(request->config, PW_PROXY_TO_ORIGINATING_REALM, 0, TAG_ANY);
+			if (!vp) break;
+
+			/*
+			 *	We don't want to hold multiple mutexes
+			 *	at the same time.
+			 */
+			PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+			rcode = listen_coa_find(request, vp->vp_strvalue);
+			PTHREAD_MUTEX_LOCK(&proxy_mutex);
+			if (rcode < 0) continue;
+			break;
+		}
+#endif
+
+		if (tries > 0) continue; /* try opening new socket only once */
+
+#ifdef HAVE_PTHREAD_H
+		if (proxy_no_new_sockets) break;
+#endif
+
+		RDEBUG3("proxy: Trying to open a new listener to the home server");
+		this = proxy_new_listener(proxy_ctx, request->home_server, 0);
+		if (!this) {
+			request->home_server->state = HOME_STATE_CONNECTION_FAIL;
+			PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+			goto fail;
+		}
+
+		request->proxy->src_port = 0; /* Use any new socket */
+		proxy_listener = this;
+
+		sock = this->data;
+		if (!fr_packet_list_socket_add(proxy_list, this->fd,
+					       sock->proto,
+#ifdef WITH_RADIUSV11
+					       sock->radiusv11,
+#endif
+					       &sock->other_ipaddr, sock->other_port,
+					       this)) {
+
+#ifdef HAVE_PTHREAD_H
+			proxy_no_new_sockets = true;
+#endif
+			PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+
+			/*
+			 *	This is bad.  However, the
+			 *	packet list now supports 256
+			 *	open sockets, which should
+			 *	minimize this problem.
+			 */
+			ERROR("Failed adding proxy socket: %s",
+			      fr_strerror());
+			goto fail;
+		}
+
+#ifdef COA_TUNNEL
+		/*
+		 *	Track how many IDs are used.  This information
+		 *	helps the listen_coa_find() function get a
+		 *	listener which has free IDs.
+		 */
+		request->proxy_listener->num_ids_used++;
+#endif
+
+		/*
+		 *	Add it to the event loop.  Ensure that we have
+		 *	only one mutex locked at a time.
+		 */
+		PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+		radius_update_listener(this);
+		PTHREAD_MUTEX_LOCK(&proxy_mutex);
+	}
+
+	if (!proxy_listener || !success) {
+		PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+		REDEBUG2("proxy: Failed allocating Id for proxied request");
+	fail:
+		request->proxy_listener = NULL;
+		request->in_proxy_hash = false;
+		return 0;
+	}
+
+#ifndef WITH_RADIUSV11
+	rad_assert(request->proxy->id >= 0);
+#endif
+
+	request->proxy_listener = proxy_listener;
+	request->in_proxy_hash = true;
+	RDEBUG3("proxy: request is now in proxy hash");
+
+	/*
+	 *	Keep track of maximum outstanding requests to a
+	 *	particular home server.  'max_outstanding' is
+	 *	enforced in home_server_ldb(), in realms.c.
+	 */
+	request->home_server->currently_outstanding++;
+
+	request->proxy_listener->count++;
+
+	PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+
+	RDEBUG3("proxy: allocating destination %s port %d - Id %d",
+	       inet_ntop(request->proxy->dst_ipaddr.af,
+			 &request->proxy->dst_ipaddr.ipaddr, buf, sizeof(buf)),
+	       request->proxy->dst_port,
+	       request->proxy->id);
+
+	return 1;
+}
+
+static int process_proxy_reply(REQUEST *request, RADIUS_PACKET *reply)
+{
+	int rcode;
+	int post_proxy_type = 0;
+	VALUE_PAIR *vp;
+	char const *old_server;
+#ifdef WITH_COA_TUNNEL
+	bool reverse_coa = false;
+#endif
+
+	VERIFY_REQUEST(request);
+
+	/*
+	 *	There may be a proxy reply, but it may be too late.
+	 */
+	if ((request->home_server && !request->home_server->virtual_server) && !request->proxy_listener) return 0;
+
+	/*
+	 *	Delete any reply we had accumulated until now.
+	 */
+	RDEBUG2("Clearing existing &reply: attributes");
+	fr_pair_list_free(&request->reply->vps);
+
+	/*
+	 *	Run the packet through the post-proxy stage,
+	 *	BEFORE playing games with the attributes.
+	 */
+	vp = fr_pair_find_by_num(request->config, PW_POST_PROXY_TYPE, 0, TAG_ANY);
+	if (vp) {
+		post_proxy_type = vp->vp_integer;
+	/*
+	 *	If we have a proxy_reply, and it was a reject, or a NAK
+	 *	setup Post-Proxy <type>.
+	 *
+	 *	If the <type> doesn't have a section, then the Post-Proxy
+	 *	section is ignored.
+	 */
+	} else if (reply) {
+		DICT_VALUE *dval = NULL;
+
+		switch (reply->code) {
+		case PW_CODE_ACCESS_REJECT:
+			dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, "Reject");
+			if (dval) post_proxy_type = dval->value;
+			break;
+
+		case PW_CODE_DISCONNECT_NAK:
+			dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, fr_packet_codes[reply->code]);
+			if (dval) post_proxy_type = dval->value;
+			break;
+
+		case PW_CODE_COA_NAK:
+			dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, fr_packet_codes[reply->code]);
+			if (dval) post_proxy_type = dval->value;
+			break;
+
+		default:
+			break;
+		}
+
+		/*
+		 *	Create config:Post-Proxy-Type
+		 */
+		if (dval) {
+			vp = radius_pair_create(request, &request->config, PW_POST_PROXY_TYPE, 0);
+			vp->vp_integer = dval->value;
+		}
+	}
+
+	if (post_proxy_type > 0) RDEBUG2("Found Post-Proxy-Type %s",
+					 dict_valnamebyattr(PW_POST_PROXY_TYPE, 0, post_proxy_type));
+
+#ifdef WITH_COA_TUNNEL
+	/*
+	 *	Cache this, as request->proxy_listener will be
+	 *	NULL after removing the request from the proxy
+	 *	hash.
+	 */
+	if (request->proxy_listener) reverse_coa = request->proxy_listener->type != RAD_LISTEN_PROXY;
+#endif
+
+	if (reply) {
+		VERIFY_PACKET(reply);
+
+		/*
+		 *	Decode the packet if required.
+		 */
+		if (request->proxy_listener) {
+			rcode = request->proxy_listener->proxy_decode(request->proxy_listener, request);
+			debug_packet(request, reply, true);
+
+			/*
+			 *	Pro-actively remove it from the proxy hash.
+			 *	This is later than in 2.1.x, but it means that
+			 *	the replies are authenticated before being
+			 *	removed from the hash.
+			 */
+			if ((rcode == 0) &&
+			    (request->num_proxied_requests <= request->num_proxied_responses)) {
+				remove_from_proxy_hash(request);
+			}
+		} else {
+			rad_assert(!request->in_proxy_hash);
+		}
+	} else if (request->in_proxy_hash) {
+		remove_from_proxy_hash(request);
+	}
+
+
+	/*
+	 *	Run the request through the virtual server for the
+	 *	home server, OR through the virtual server for the
+	 *	home server pool.
+	 */
+	old_server = request->server;
+	if (request->home_server && request->home_server->virtual_server) {
+		request->server = request->home_server->virtual_server;
+
+#ifdef WITH_COA_TUNNEL
+	} else if (reverse_coa) {
+		rad_assert((request->proxy->code == PW_CODE_COA_REQUEST) ||
+			   (request->proxy->code == PW_CODE_DISCONNECT_REQUEST));
+		rad_assert(request->home_server != NULL);
+		rad_assert(request->home_server->recv_coa_server != NULL);
+		request->server = request->home_server->recv_coa_server;
+#endif
+
+	} else if (request->home_pool && request->home_pool->virtual_server) {
+			request->server = request->home_pool->virtual_server;
+	}
+
+	/*
+	 *	Run the request through the given virtual server.
+	 */
+	RDEBUG2("server %s {", request->server);
+	RINDENT();
+	rcode = process_post_proxy(post_proxy_type, request);
+	REXDENT();
+	RDEBUG2("}");
+	request->server = old_server;
+
+#ifdef WITH_COA
+	if (request->proxy && request->packet->code == request->proxy->code) {
+	  /*
+	   *	Don't run the next bit if we originated a CoA
+	   *	packet, after receiving an Access-Request or
+	   *	Accounting-Request.
+	   */
+#endif
+
+		/*
+		 *	There may NOT be a proxy reply, as we may be
+		 *	running Post-Proxy-Type = Fail.
+		 */
+		if (reply) {
+			fr_pair_add(&request->reply->vps, fr_pair_list_copy(request->reply, reply->vps));
+
+			/*
+			 *	Delete the Proxy-State Attributes from
+			 *	the reply.  These include Proxy-State
+			 *	attributes from us and remote server.
+			 */
+			fr_pair_delete_by_num(&request->reply->vps, PW_PROXY_STATE, 0, TAG_ANY);
+
+		} else {
+			vp = fr_pair_find_by_num(request->config, PW_RESPONSE_PACKET_TYPE, 0, TAG_ANY);
+			if (vp && (vp->vp_integer != 256)) {
+				request->proxy_reply = rad_alloc_reply(request, request->proxy);
+				request->proxy_reply->code = vp->vp_integer;
+			}
+		}
+#ifdef WITH_COA
+	}
+#endif
+	switch (rcode) {
+	default:  /* Don't do anything */
+		break;
+	case RLM_MODULE_FAIL:
+		return 0;
+
+	case RLM_MODULE_HANDLED:
+		return 0;
+	}
+
+	return 1;
+}
+
+static void mark_home_server_alive(REQUEST *request, home_server_t *home)
+{
+	char buffer[128];
+
+	home->state = HOME_STATE_ALIVE;
+	home->response_timeouts = 0;
+	exec_trigger(request, home->cs, "home_server.alive", false);
+	home->currently_outstanding = 0;
+	home->num_sent_pings = 0;
+	home->num_received_pings = 0;
+	gettimeofday(&home->revive_time, NULL);
+
+	fr_event_delete(el, &home->ev);
+
+	RPROXY("Marking home server %s port %d alive",
+	       inet_ntop(request->proxy->dst_ipaddr.af,
+			 &request->proxy->dst_ipaddr.ipaddr,
+			 buffer, sizeof(buffer)),
+	       request->proxy->dst_port);
+}
+
+
+int request_proxy_reply(RADIUS_PACKET *packet)
+{
+	RADIUS_PACKET **proxy_p;
+	REQUEST *request;
+	struct timeval now;
+	char buffer[128];
+
+	VERIFY_PACKET(packet);
+
+	PTHREAD_MUTEX_LOCK(&proxy_mutex);
+	proxy_p = fr_packet_list_find_byreply(proxy_list, packet);
+
+	if (!proxy_p) {
+		PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+		PROXY("No outstanding request was found for %s packet from host %s port %d - ID %u",
+		       fr_packet_codes[packet->code],
+		       inet_ntop(packet->src_ipaddr.af,
+				 &packet->src_ipaddr.ipaddr,
+				 buffer, sizeof(buffer)),
+		       packet->src_port, packet->id);
+		return 0;
+	}
+
+	request = fr_packet2myptr(REQUEST, proxy, proxy_p);
+
+	PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+
+	/*
+	 *	No reply, BUT the current packet fails verification:
+	 *	ignore it.  This does the MD5 calculations in the
+	 *	server core, but I guess we can fix that later.
+	 */
+	if (!request->proxy_reply) {
+		if (!request->home_server) {
+			proxy_reply_too_late(request);
+			return 0;
+		}
+
+		if (rad_verify(packet, request->proxy,
+			       request->home_server->secret) != 0) {
+			DEBUG("Ignoring spoofed proxy reply.  Signature is invalid");
+			return 0;
+		}
+	}
+
+	/*
+	 *	The home server sent us a packet which doesn't match
+	 *	something we have: ignore it.  This is done only to
+	 *	catch the case of broken systems.
+	 */
+	if (request->proxy_reply &&
+	    (memcmp(request->proxy_reply->vector,
+		    packet->vector,
+		    sizeof(request->proxy_reply->vector)) != 0)) {
+		RDEBUG2("Ignoring conflicting proxy reply");
+		return 0;
+	}
+
+	/*
+	 *	This shouldn't happen, but threads and race
+	 *	conditions.
+	 */
+	if (!request->proxy_listener || !request->proxy_listener->data) {
+		proxy_reply_too_late(request);
+		return 0;
+	}
+
+	gettimeofday(&now, NULL);
+
+	/*
+	 *	Status-Server packets don't count as real packets.
+	 */
+	if (request->proxy->code != PW_CODE_STATUS_SERVER) {
+#ifdef WITH_TCP
+		listen_socket_t *sock = request->proxy_listener->data;
+
+		sock->last_packet = now.tv_sec;
+#endif
+		request->home_server->last_packet_recv = now.tv_sec;
+	}
+
+	request->num_proxied_responses++;
+
+	/*
+	 *	If we have previously seen a reply, ignore the
+	 *	duplicate.
+	 */
+	if (request->proxy_reply) {
+		RDEBUG2("Discarding duplicate reply from host %s port %d  - ID: %d",
+			inet_ntop(packet->src_ipaddr.af,
+				  &packet->src_ipaddr.ipaddr,
+				  buffer, sizeof(buffer)),
+			packet->src_port, packet->id);
+		return 0;
+	}
+
+	/*
+	 *	Call the state machine to do something useful with the
+	 *	request.
+	 */
+	request->proxy_reply = talloc_steal(request, packet);
+	packet->timestamp = now;
+	request->priority = RAD_LISTEN_PROXY;
+
+#ifdef WITH_STATS
+	/*
+	 *	Update the proxy listener stats here, because only one
+	 *	thread accesses that at a time.  The home_server and
+	 *	main proxy_*_stats structures are updated once the
+	 *	request is cleaned up.
+	 */
+	request->proxy_listener->stats.total_responses++;
+
+	request->home_server->stats.last_packet = packet->timestamp.tv_sec;
+	request->proxy_listener->stats.last_packet = packet->timestamp.tv_sec;
+
+	switch (request->proxy->code) {
+	case PW_CODE_ACCESS_REQUEST:
+		proxy_auth_stats.last_packet = packet->timestamp.tv_sec;
+
+		if (request->proxy_reply->code == PW_CODE_ACCESS_ACCEPT) {
+			request->proxy_listener->stats.total_access_accepts++;
+
+		} else if (request->proxy_reply->code == PW_CODE_ACCESS_REJECT) {
+			request->proxy_listener->stats.total_access_rejects++;
+
+		} else if (request->proxy_reply->code == PW_CODE_ACCESS_CHALLENGE) {
+			request->proxy_listener->stats.total_access_challenges++;
+		}
+		break;
+
+#ifdef WITH_ACCOUNTING
+	case PW_CODE_ACCOUNTING_REQUEST:
+		request->proxy_listener->stats.total_responses++;
+		proxy_acct_stats.last_packet = packet->timestamp.tv_sec;
+		break;
+
+#endif
+
+#ifdef WITH_COA
+	case PW_CODE_COA_REQUEST:
+		request->proxy_listener->stats.total_responses++;
+		proxy_coa_stats.last_packet = packet->timestamp.tv_sec;
+		break;
+
+	case PW_CODE_DISCONNECT_REQUEST:
+		request->proxy_listener->stats.total_responses++;
+		proxy_dsc_stats.last_packet = packet->timestamp.tv_sec;
+		break;
+
+#endif
+	default:
+		break;
+	}
+#endif
+
+	/*
+	 *	If we hadn't been sending the home server packets for
+	 *	a while, just mark it alive.  Or, if it was zombie,
+	 *	it's now responded, and is therefore alive.
+	 */
+	if ((request->home_server->state == HOME_STATE_UNKNOWN) ||
+	    (request->home_server->state == HOME_STATE_ZOMBIE)) {
+		mark_home_server_alive(request, request->home_server);
+	}
+
+	/*
+	 *	Tell the request state machine that we have a proxy
+	 *	reply.  Depending on the function, this should either
+	 *	ignore it, or process it.
+	 */
+	request->process(request, FR_ACTION_PROXY_REPLY);
+
+	return 1;
+}
+
+
+static int setup_post_proxy_fail(REQUEST *request)
+{
+	DICT_VALUE const *dval = NULL;
+	VALUE_PAIR *vp;
+	RADIUS_PACKET *packet;
+
+	VERIFY_REQUEST(request);
+
+	packet = request->proxy ? request->proxy : request->packet;
+
+	if (packet->code == PW_CODE_ACCESS_REQUEST) {
+		dval = dict_valbyname(PW_POST_PROXY_TYPE, 0,
+				      "Fail-Authentication");
+#ifdef WITH_ACCOUNTING
+	} else if (packet->code == PW_CODE_ACCOUNTING_REQUEST) {
+		dval = dict_valbyname(PW_POST_PROXY_TYPE, 0,
+				      "Fail-Accounting");
+#endif
+
+#ifdef WITH_COA
+	} else if (packet->code == PW_CODE_COA_REQUEST) {
+		dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, "Fail-CoA");
+
+	} else if (packet->code == PW_CODE_DISCONNECT_REQUEST) {
+		dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, "Fail-Disconnect");
+#endif
+	} else {
+		WARN("Unknown packet type in Post-Proxy-Type Fail: ignoring");
+		return 0;
+	}
+
+	if (!dval) dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, "Fail");
+
+	if (!dval) {
+		fr_pair_delete_by_num(&request->config, PW_POST_PROXY_TYPE, 0, TAG_ANY);
+		return 0;
+	}
+
+	vp = fr_pair_find_by_num(request->config, PW_POST_PROXY_TYPE, 0, TAG_ANY);
+	if (!vp) vp = radius_pair_create(request, &request->config,
+					PW_POST_PROXY_TYPE, 0);
+	vp->vp_integer = dval->value;
+
+	return 1;
+}
+
+
+/** Process a request after the proxy has timed out.
+ *
+ *  Run the packet through Post-Proxy-Type Fail
+ *
+ *  \dot
+ *	digraph proxy_no_reply {
+ *		proxy_no_reply;
+ *
+ *		proxy_no_reply -> dup [ label = "DUP", arrowhead = "none" ];
+ *		proxy_no_reply -> timer [ label = "TIMER < max_request_time" ];
+ *		proxy_no_reply -> proxy_reply_too_late [ label = "PROXY_REPLY" arrowhead = "none"];
+ *		proxy_no_reply -> process_proxy_reply [ label = "RUN" ];
+ *		proxy_no_reply -> done [ label = "TIMER >= timeout" ];
+ *	}
+ *  \enddot
+ */
+static void proxy_no_reply(REQUEST *request, int action)
+{
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+	CHECK_FOR_STOP;
+
+	switch (action) {
+	case FR_ACTION_DUP:
+		request_dup(request);
+		break;
+
+	case FR_ACTION_TIMER:
+		(void) request_max_time(request);
+		break;
+
+	case FR_ACTION_PROXY_REPLY:
+		proxy_reply_too_late(request);
+		break;
+
+	case FR_ACTION_RUN:
+		if (process_proxy_reply(request, NULL)) {
+			request->handle(request);
+		}
+		request_finish(request, action);
+		break;
+
+	default:
+		RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]);
+		break;
+	}
+}
+
+/** Process the request after receiving a proxy reply.
+ *
+ *  Throught the post-proxy section, and the through the handler
+ *  function.
+ *
+ *  \dot
+ *	digraph proxy_running {
+ *		proxy_running;
+ *
+ *		proxy_running -> dup [ label = "DUP", arrowhead = "none" ];
+ *		proxy_running -> timer [ label = "TIMER < max_request_time" ];
+ *		proxy_running -> process_proxy_reply [ label = "RUN" ];
+ *		proxy_running -> done [ label = "TIMER >= timeout" ];
+ *	}
+ *  \enddot
+ */
+static void proxy_running(REQUEST *request, int action)
+{
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+	CHECK_FOR_STOP;
+
+	switch (action) {
+	case FR_ACTION_DUP:
+		request_dup(request);
+		break;
+
+	case FR_ACTION_TIMER:
+		(void) request_max_time(request);
+		break;
+
+	case FR_ACTION_RUN:
+		if (process_proxy_reply(request, request->proxy_reply)) {
+			request->handle(request);
+		}
+		request_finish(request, action);
+		break;
+
+	default:		/* duplicate proxy replies are suppressed */
+		RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]);
+		break;
+	}
+}
+
+/** Determine if a #REQUEST needs to be proxied, and perform pre-proxy operations
+ *
+ * Whether a request will be proxied is determined by the attributes present
+ * in request->config. If any of the following attributes are found, the
+ * request may be proxied.
+ *
+ * The key attributes are:
+ *   - PW_PROXY_TO_REALM          - Specifies a realm the request should be proxied to.
+ *   - PW_HOME_SERVER_POOL        - Specifies a specific home server pool to proxy to.
+ *   - PW_HOME_SERVER_NAME        - Specifies a home server by name
+ *   - PW_PACKET_DST_IP_ADDRESS   - Specifies a home server by IPv4 address
+ *   - PW_PACKET_DST_IPV6_ADDRESS - Specifies a home server by IPv5 address
+ *
+ * Certain packet types such as #PW_CODE_STATUS_SERVER will never be proxied.
+ *
+ * If request should be proxied, will:
+ *   - Add request:Proxy-State
+ *   - Strip the current username value of its realm (depending on config)
+ *   - Create a CHAP-Challenge from the original request vector, if one doesn't already
+ *     exist.
+ *   - Call the pre-process section in the current server, or in the virtual server
+ *     associated with the home server pool we're proxying to.
+ *
+ * @todo A lot of this logic is RADIUS specific, and should be moved out into a protocol
+ *	specific function.
+ *
+ * @param request The #REQUEST to evaluate for proxying.
+ * @return 0 if not proxying, 1 if request should be proxied, -1 on error.
+ */
+static int request_will_proxy(REQUEST *request)
+{
+	int rcode, pre_proxy_type = 0;
+	char const *realmname = NULL;
+	VALUE_PAIR *vp, *strippedname;
+	home_server_t *home;
+	REALM *realm = NULL;
+	home_pool_t *pool = NULL;
+	char const *old_server;
+
+	VERIFY_REQUEST(request);
+
+	if (!request->root->proxy_requests) {
+		return 0;
+	}
+	if (request->packet->dst_port == 0) return 0;
+	if (request->packet->code == PW_CODE_STATUS_SERVER) return 0;
+	if (request->in_proxy_hash) return 0;
+
+	/*
+	 *	FIXME: for 3.0, allow this only for rejects?
+	 */
+	if (request->reply->code != 0) return 0;
+
+	vp = fr_pair_find_by_num(request->config, PW_PROXY_TO_REALM, 0, TAG_ANY);
+	if (vp) {
+		realm = realm_find2(vp->vp_strvalue);
+		if (!realm) {
+			REDEBUG2("Cannot proxy to unknown realm %s",
+				vp->vp_strvalue);
+			return 0;
+		}
+
+		realmname = vp->vp_strvalue;
+
+		/*
+		 *	Figure out which pool to use.
+		 */
+		if (request->packet->code == PW_CODE_ACCESS_REQUEST) {
+			DEBUG3("Using home pool auth for realm %s", realm->name);
+			pool = realm->auth_pool;
+
+#ifdef WITH_ACCOUNTING
+		} else if (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) {
+			DEBUG3("Using home pool acct for realm %s", realm->name);
+			pool = realm->acct_pool;
+#endif
+
+#ifdef WITH_COA
+		} else if ((request->packet->code == PW_CODE_COA_REQUEST) ||
+			   (request->packet->code == PW_CODE_DISCONNECT_REQUEST)) {
+			DEBUG3("Using home pool coa for realm %s", realm->name);
+			pool = realm->coa_pool;
+#endif
+
+		} else {
+			return 0;
+		}
+
+	} else if ((vp = fr_pair_find_by_num(request->config, PW_HOME_SERVER_POOL, 0, TAG_ANY)) != NULL) {
+		int pool_type;
+
+		DEBUG3("Using Home-Server-Pool %s", vp->vp_strvalue);
+
+		switch (request->packet->code) {
+		case PW_CODE_ACCESS_REQUEST:
+			pool_type = HOME_TYPE_AUTH;
+			break;
+
+#ifdef WITH_ACCOUNTING
+		case PW_CODE_ACCOUNTING_REQUEST:
+			pool_type = HOME_TYPE_ACCT;
+			break;
+#endif
+
+#ifdef WITH_COA
+		case PW_CODE_COA_REQUEST:
+		case PW_CODE_DISCONNECT_REQUEST:
+			pool_type = HOME_TYPE_COA;
+			break;
+#endif
+
+		default:
+			return 0;
+		}
+
+		pool = home_pool_byname(vp->vp_strvalue, pool_type);
+
+		/*
+		 *	Send it directly to a home server (i.e. NAS)
+		 */
+	} else if (((vp = fr_pair_find_by_num(request->config, PW_PACKET_DST_IP_ADDRESS, 0, TAG_ANY)) != NULL) ||
+		   ((vp = fr_pair_find_by_num(request->config, PW_PACKET_DST_IPV6_ADDRESS, 0, TAG_ANY)) != NULL)) {
+		uint16_t dst_port;
+		fr_ipaddr_t dst_ipaddr;
+
+		memset(&dst_ipaddr, 0, sizeof(dst_ipaddr));
+
+		if (vp->da->attr == PW_PACKET_DST_IP_ADDRESS) {
+			dst_ipaddr.af = AF_INET;
+			dst_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+			dst_ipaddr.prefix = 32;
+		} else {
+			dst_ipaddr.af = AF_INET6;
+			memcpy(&dst_ipaddr.ipaddr.ip6addr, &vp->vp_ipv6addr, sizeof(vp->vp_ipv6addr));
+			dst_ipaddr.prefix = 128;
+		}
+
+		vp = fr_pair_find_by_num(request->config, PW_PACKET_DST_PORT, 0, TAG_ANY);
+		if (!vp) {
+			if (request->packet->code == PW_CODE_ACCESS_REQUEST) {
+				dst_port = PW_AUTH_UDP_PORT;
+
+#ifdef WITH_ACCOUNTING
+			} else if (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) {
+				dst_port = PW_ACCT_UDP_PORT;
+#endif
+
+#ifdef WITH_COA
+			} else if ((request->packet->code == PW_CODE_COA_REQUEST) ||
+				   (request->packet->code == PW_CODE_DISCONNECT_REQUEST)) {
+				dst_port = PW_COA_UDP_PORT;
+#endif
+			} else { /* shouldn't happen for RADIUS... */
+				return 0;
+			}
+
+		} else {
+			dst_port = vp->vp_integer;
+		}
+
+		/*
+		 *	Find the home server.
+		 */
+		home = home_server_find(&dst_ipaddr, dst_port, IPPROTO_UDP);
+		if (!home) home = home_server_find(&dst_ipaddr, dst_port, IPPROTO_TCP);
+		if (!home) {
+			char buffer[256];
+
+			RWDEBUG("No such home server %s port %u",
+				inet_ntop(dst_ipaddr.af, &dst_ipaddr.ipaddr, buffer, sizeof(buffer)),
+				(unsigned int) dst_port);
+			return 0;
+		}
+
+		/*
+		 *	The home server is alive (or may be alive).
+		 *	Send the packet to the IP.
+		 */
+		if (!HOME_SERVER_IS_DEAD(home)) goto do_home;
+
+		/*
+		 *	The home server is dead.  If you wanted
+		 *	fail-over, you should have proxied to a pool.
+		 *	Sucks to be you.
+		 */
+
+		return 0;
+
+	} else if ((vp = fr_pair_find_by_num(request->config, PW_HOME_SERVER_NAME, 0, TAG_ANY)) != NULL) {
+		int type;
+
+		switch (request->packet->code) {
+		case PW_CODE_ACCESS_REQUEST:
+			type = HOME_TYPE_AUTH;
+			break;
+
+#ifdef WITH_ACCOUNTING
+		case PW_CODE_ACCOUNTING_REQUEST:
+			type = HOME_TYPE_ACCT;
+			break;
+#endif
+
+#ifdef WITH_COA
+		case PW_CODE_COA_REQUEST:
+		case PW_CODE_DISCONNECT_REQUEST:
+			type = HOME_TYPE_COA;
+			break;
+#endif
+
+		default:
+			return 0;
+		}
+
+		/*
+		 *	Find the home server by name.
+		 */
+		home = home_server_byname(vp->vp_strvalue, type);
+		if (!home) {
+			RWDEBUG("No such home server %s", vp->vp_strvalue);
+			return 0;
+		}
+
+		/*
+		 *	The home server is alive (or may be alive).
+		 *	Send the packet to the IP.
+		 */
+		if (!HOME_SERVER_IS_DEAD(home)) goto do_home;
+
+		/*
+		 *	The home server is dead.  If you wanted
+		 *	fail-over, you should have proxied to a pool.
+		 *	Sucks to be you.
+		 */
+
+		return 0;
+
+#ifdef WITH_COA_TUNNEL
+	} else if (((request->packet->code == PW_CODE_COA_REQUEST) ||
+		    (request->packet->code == PW_CODE_DISCONNECT_REQUEST)) &&
+		   ((vp = fr_pair_find_by_num(request->config, PW_PROXY_TO_ORIGINATING_REALM, 0, TAG_ANY)) != NULL)) {
+
+		/*
+		 *	This function will set request->home_server,
+		 *	and also request->proxy_listener.
+		 */
+		if (listen_coa_find(request, vp->vp_strvalue) < 0) {
+			vp_cursor_t cursor;
+
+			(void) fr_cursor_init(&cursor, &request->config); /* already checked it above */
+
+			while ((vp = fr_cursor_next(&cursor)) != NULL) {
+				if (listen_coa_find(request, vp->vp_strvalue) == 0) break;
+			}
+
+			/*
+			 *	Not found.
+			 */
+			return 0;
+		}
+
+		/*
+		 *	Initialise request->proxy, and copy VPs over.
+		 */
+		home_server_update_request(request->home_server, request);
+		goto add_proxy_state;
+#endif
+	} else {
+
+		return 0;
+	}
+
+	if (!pool) {
+		RWDEBUG2("Cancelling proxy as no home pool exists");
+		return 0;
+	}
+
+	if (request->listener->synchronous) {
+		WARN("Cannot proxy a request which is from a 'synchronous' socket");
+		return 0;
+	}
+
+	request->home_pool = pool;
+
+	home = home_server_ldb(realmname, pool, request);
+
+	if (!home) {
+		REDEBUG2("Failed to find live home server: Cancelling proxy");
+		return -1;
+	}
+
+do_home:
+	home_server_update_request(home, request);
+
+#ifdef WITH_COA
+	/*
+	 *	Once we've decided to proxy a request, we cannot send
+	 *	a CoA packet.  So we free up any CoA packet here.
+	 */
+	if (request->coa) request_done(request->coa, FR_ACTION_COA_CANCELLED);
+#endif
+
+	/*
+	 *	Remember that we sent the request to a Realm.
+	 */
+	if (realmname && !fr_pair_find_by_num(request->packet->vps, PW_REALM, 0, TAG_ANY)) {
+		pair_make_request("Realm", realmname, T_OP_EQ);
+	}
+
+	/*
+	 *	Strip the name, if told to.
+	 *
+	 *	Doing it here catches the case of proxied tunneled
+	 *	requests.
+	 */
+	if (realm && (realm->strip_realm == true) &&
+	   (strippedname = fr_pair_find_by_num(request->proxy->vps, PW_STRIPPED_USER_NAME, 0, TAG_ANY)) != NULL) {
+		/*
+		 *	If there's a Stripped-User-Name attribute in
+		 *	the request, then use THAT as the User-Name
+		 *	for the proxied request, instead of the
+		 *	original name.
+		 *
+		 *	This is done by making a copy of the
+		 *	Stripped-User-Name attribute, turning it into
+		 *	a User-Name attribute, deleting the
+		 *	Stripped-User-Name and User-Name attributes
+		 *	from the vps list, and making the new
+		 *	User-Name the head of the vps list.
+		 */
+		vp = fr_pair_find_by_num(request->proxy->vps, PW_USER_NAME, 0, TAG_ANY);
+		if (!vp) {
+			vp_cursor_t cursor;
+			vp = radius_pair_create(NULL, NULL,
+					       PW_USER_NAME, 0);
+			rad_assert(vp != NULL);	/* handled by above function */
+			/* Insert at the START of the list */
+			/* FIXME: Can't make assumptions about ordering */
+			fr_cursor_init(&cursor, &vp);
+			fr_cursor_merge(&cursor, request->proxy->vps);
+			request->proxy->vps = vp;
+		}
+		fr_pair_value_strcpy(vp, strippedname->vp_strvalue);
+
+		/*
+		 *	Do NOT delete Stripped-User-Name.
+		 */
+	}
+
+	/*
+	 *	If there is no PW_CHAP_CHALLENGE attribute but
+	 *	there is a PW_CHAP_PASSWORD we need to add it
+	 *	since we can't use the request authenticator
+	 *	anymore - we changed it.
+	 */
+	if ((request->packet->code == PW_CODE_ACCESS_REQUEST) &&
+	    fr_pair_find_by_num(request->proxy->vps, PW_CHAP_PASSWORD, 0, TAG_ANY) &&
+	    fr_pair_find_by_num(request->proxy->vps, PW_CHAP_CHALLENGE, 0, TAG_ANY) == NULL) {
+		vp = radius_pair_create(request->proxy, &request->proxy->vps, PW_CHAP_CHALLENGE, 0);
+		fr_pair_value_memcpy(vp, request->packet->vector, sizeof(request->packet->vector));
+	}
+
+	/*
+	 *	The RFC's say we have to do this, but FreeRADIUS
+	 *	doesn't need it.
+	 */
+#ifdef WITH_COA_TUNNEL
+add_proxy_state:
+#endif
+
+	vp = radius_pair_create(request->proxy, &request->proxy->vps, PW_PROXY_STATE, 0);
+	fr_pair_value_sprintf(vp, "%u", request->packet->id);
+
+	/*
+	 *	Should be done BEFORE inserting into proxy hash, as
+	 *	pre-proxy may use this information, or change it.
+	 */
+	request->proxy->code = request->packet->code;
+
+	/*
+	 *	Call the pre-proxy routines.
+	 */
+	vp = fr_pair_find_by_num(request->config, PW_PRE_PROXY_TYPE, 0, TAG_ANY);
+	if (vp) {
+		DICT_VALUE const *dval = dict_valbyattr(vp->da->attr, vp->da->vendor, vp->vp_integer);
+		/* Must be a validation issue */
+		rad_assert(dval);
+		RDEBUG2("Found Pre-Proxy-Type %s", dval->name);
+		pre_proxy_type = vp->vp_integer;
+	}
+
+	/*
+	 *	Run the request through the virtual server for the
+	 *	home server, OR through the virtual server for the
+	 *	home server pool.
+	 */
+	old_server = request->server;
+	if (request->home_server && request->home_server->virtual_server) {
+		request->server = request->home_server->virtual_server;
+
+#ifdef WITH_COA_TUNNEL
+	} else if (request->proxy_listener && (request->proxy_listener->type != RAD_LISTEN_PROXY)) {
+		rad_assert((request->packet->code == PW_CODE_COA_REQUEST) ||
+			   (request->packet->code == PW_CODE_DISCONNECT_REQUEST));
+		rad_assert(request->home_server != NULL);
+		rad_assert(request->home_server->recv_coa_server != NULL);
+		request->server = request->home_server->recv_coa_server;
+#endif
+
+	} else {
+		char buffer[128];
+
+		RDEBUG2("Starting proxy to home server %s port %d",
+			inet_ntop(request->proxy->dst_ipaddr.af,
+				  &request->proxy->dst_ipaddr.ipaddr,
+				  buffer, sizeof(buffer)),
+			request->proxy->dst_port);
+
+		if (request->home_pool && request->home_pool->virtual_server) {
+			request->server = request->home_pool->virtual_server;
+		}
+	}
+
+	/*
+	 *	Run the request through the given virtual server.
+	 */
+	RDEBUG2("server %s {", request->server);
+	RINDENT();
+	rcode = process_pre_proxy(pre_proxy_type, request);
+	REXDENT();
+	RDEBUG2("}");
+	request->server = old_server;
+
+	switch (rcode) {
+	case RLM_MODULE_FAIL:
+	case RLM_MODULE_INVALID:
+	case RLM_MODULE_NOTFOUND:
+	case RLM_MODULE_USERLOCK:
+	default:
+		/* FIXME: debug print failed stuff */
+		return -1;
+
+	case RLM_MODULE_REJECT:
+	case RLM_MODULE_HANDLED:
+		return 0;
+
+	/*
+	 *	Only proxy the packet if the pre-proxy code succeeded.
+	 */
+	case RLM_MODULE_NOOP:
+	case RLM_MODULE_OK:
+	case RLM_MODULE_UPDATED:
+		return 1;
+	}
+}
+
+static int proxy_to_virtual_server(REQUEST *request)
+{
+	REQUEST *fake;
+
+	if (request->packet->dst_port == 0) {
+		WARN("Cannot proxy an internal request");
+		return 0;
+	}
+
+	DEBUG("Proxying to virtual server %s",
+	      request->home_server->virtual_server);
+
+	/*
+	 *	Packets to virtual servers don't get
+	 *	retransmissions sent to them.  And the virtual
+	 *	server is run ONLY if we have no child
+	 *	threads, or we're running in a child thread.
+	 */
+	rad_assert(!spawn_flag || !we_are_master());
+
+	fake = request_alloc_fake(request);
+
+	fake->packet->vps = fr_pair_list_copy(fake->packet, request->packet->vps);
+	talloc_free(request->proxy);
+
+	fake->server = request->home_server->virtual_server;
+	fake->handle = request->handle;
+	fake->process = NULL; /* should never be run for anything */
+
+	/*
+	 *	Run the virtual server.
+	 */
+	request_running(fake, FR_ACTION_RUN);
+
+	request->proxy = talloc_steal(request, fake->packet);
+	fake->packet = NULL;
+	request->proxy_reply = talloc_steal(request, fake->reply);
+	fake->reply = NULL;
+
+	talloc_free(fake);
+
+	/*
+	 *	No reply code, toss the reply we have,
+	 *	and do post-proxy-type Fail.
+	 */
+	if (!request->proxy_reply->code) {
+		TALLOC_FREE(request->proxy_reply);
+		setup_post_proxy_fail(request);
+	}
+
+	/*
+	 *	Do the proxy reply (if any)
+	 */
+	if (process_proxy_reply(request, request->proxy_reply)) {
+		request->handle(request);
+	}
+
+	return -1;	/* so we call request_finish */
+}
+
+
+static int request_proxy(REQUEST *request)
+{
+	char buffer[128];
+
+	VERIFY_REQUEST(request);
+
+	rad_assert(request->parent == NULL);
+
+	if (request->master_state == REQUEST_STOP_PROCESSING) return 0;
+
+#ifdef WITH_COA
+	if (request->coa) {
+		RWDEBUG("Cannot proxy and originate CoA packets at the same time.  Cancelling CoA request");
+		request_done(request->coa, FR_ACTION_COA_CANCELLED);
+	}
+#endif
+
+	if (!request->home_server) {
+		RWDEBUG("No home server selected");
+		return -1;
+	}
+
+	/*
+	 *	The request may need sending to a virtual server.
+	 *	This code is more than a little screwed up.  The rest
+	 *	of the state machine doesn't handle parent / child
+	 *	relationships well.  i.e. if the child request takes
+	 *	too long, the core will mark the *parent* as "stop
+	 *	processing".  And the child will continue without
+	 *	knowing anything...
+	 *
+	 *	So, we have some horrible hacks to get around that.
+	 */
+	if (request->home_server->virtual_server) return proxy_to_virtual_server(request);
+
+	/*
+	 *	We're actually sending a proxied packet.  Do that now.
+	 */
+	if (!request->in_proxy_hash && !insert_into_proxy_hash(request)) {
+		RPROXY("Failed to insert request into the proxy list");
+		return -1;
+	}
+
+#ifndef WITH_RADIUSV11
+	rad_assert(request->proxy->id >= 0);
+#endif
+
+	if (rad_debug_lvl) {
+		struct timeval *response_window;
+
+		response_window = request_response_window(request);
+
+#ifdef WITH_TLS
+		if (request->home_server->tls) {
+#ifdef WITH_RADIUSV11
+			listen_socket_t *sock = request->proxy_listener->data;
+
+			if (sock->radiusv11) {
+				fr_pair_delete_by_num(&request->proxy->vps, PW_MESSAGE_AUTHENTICATOR, 0, TAG_ANY);
+			}
+#endif
+
+			RDEBUG2("Proxying request to home server %s port %d (TLS) timeout %d.%06d",
+				inet_ntop(request->proxy->dst_ipaddr.af,
+					  &request->proxy->dst_ipaddr.ipaddr,
+					  buffer, sizeof(buffer)),
+				request->proxy->dst_port,
+				(int) response_window->tv_sec, (int) response_window->tv_usec);
+		} else
+#endif
+			RDEBUG2("Proxying request to home server %s port %d timeout %d.%06d",
+				inet_ntop(request->proxy->dst_ipaddr.af,
+					  &request->proxy->dst_ipaddr.ipaddr,
+					  buffer, sizeof(buffer)),
+				request->proxy->dst_port,
+				(int) response_window->tv_sec, (int) response_window->tv_usec);
+
+
+	}
+
+	gettimeofday(&request->proxy->timestamp, NULL);
+	request->home_server->last_packet_sent = request->proxy->timestamp.tv_sec;
+
+	/*
+	 *	Encode the packet before we do anything else.
+	 */
+	request->proxy_listener->proxy_encode(request->proxy_listener, request);
+	debug_packet(request, request->proxy, false);
+
+	/*
+	 *	Set the state function, then the state, no child, and
+	 *	send the packet.
+	 *
+	 *	The order here is different from other state changes
+	 *	due to race conditions with replies from the home
+	 *	server.
+	 */
+	request->process = proxy_wait_for_reply;
+	request->child_state = REQUEST_PROXIED;
+	request->component = "<REQUEST_PROXIED>";
+	request->module = "";
+	NO_CHILD_THREAD;
+
+	/*
+	 *	And send the packet.
+	 */
+	request->proxy_listener->proxy_send(request->proxy_listener, request);
+	return 1;
+}
+
+/*
+ *	Proxy the packet as if it was new.
+ */
+static int request_proxy_anew(REQUEST *request)
+{
+	home_server_t *home;
+
+	VERIFY_REQUEST(request);
+
+	/*
+	 *	Delete the request from the proxy list.
+	 *
+	 *	The packet list code takes care of ensuring that IDs
+	 *	aren't reused until all 256 IDs have been used.  So
+	 *	there's a 1/256 chance of re-using the same ID when
+	 *	we're sending to the same home server.  Which is
+	 *	acceptable.
+	 */
+	remove_from_proxy_hash(request);
+
+	/*
+	 *	Find a live home server for the request.
+	 */
+	home = home_server_ldb(NULL, request->home_pool, request);
+	if (!home) {
+		REDEBUG2("Failed to find live home server for request");
+	post_proxy_fail:
+		if (setup_post_proxy_fail(request)) {
+			request_queue_or_run(request, proxy_running);
+		} else {
+			gettimeofday(&request->reply->timestamp, NULL);
+			request_cleanup_delay_init(request);
+		}
+		return 0;
+	}
+
+#ifdef WITH_ACCOUNTING
+	/*
+	 *	Update the Acct-Delay-Time attribute, since the LAST
+	 *	time we tried to retransmit this packet.
+	 */
+	if (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) {
+		VALUE_PAIR *vp;
+
+		vp = fr_pair_find_by_num(request->proxy->vps, PW_ACCT_DELAY_TIME, 0, TAG_ANY);
+		if (!vp) vp = radius_pair_create(request->proxy,
+						&request->proxy->vps,
+						PW_ACCT_DELAY_TIME, 0);
+		if (vp) {
+			struct timeval now;
+
+			gettimeofday(&now, NULL);
+			vp->vp_integer += now.tv_sec - request->proxy->timestamp.tv_sec;
+		}
+	}
+#endif
+
+	/*
+	 *	May have failed over to a "fallback" virtual server.
+	 *	If so, run that instead of doing proxying to a real
+	 *	server.
+	 */
+	if (home->virtual_server) {
+		request->home_server = home;
+		TALLOC_FREE(request->proxy);
+
+		(void) proxy_to_virtual_server(request);
+		return 0;
+	}
+
+	home_server_update_request(home, request);
+
+	if (!insert_into_proxy_hash(request)) {
+		RPROXY("Failed to insert retransmission into the proxy list");
+		goto post_proxy_fail;
+	}
+
+	/*
+	 *	Free the old packet, to force re-encoding
+	 */
+	talloc_free(request->proxy->data);
+	request->proxy->data = NULL;
+	request->proxy->data_len = 0;
+
+	if (request_proxy(request) != 1) goto post_proxy_fail;
+
+	return 1;
+}
+
+
+/** Ping a home server.
+ *
+ */
+static void request_ping(REQUEST *request, int action)
+{
+	home_server_t *home = request->home_server;
+	char buffer[128];
+
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+	ASSERT_MASTER;
+
+	switch (action) {
+	case FR_ACTION_TIMER:
+		ERROR("No response to status check %d ID %u for home server %s port %d",
+		       request->number,
+		       request->proxy->id,
+		       inet_ntop(request->proxy->dst_ipaddr.af,
+				 &request->proxy->dst_ipaddr.ipaddr,
+				 buffer, sizeof(buffer)),
+		       request->proxy->dst_port);
+		remove_from_proxy_hash(request);
+		break;
+
+	case FR_ACTION_PROXY_REPLY:
+		rad_assert(request->in_proxy_hash);
+
+		request->home_server->num_received_pings++;
+		RPROXY("Received response to status check %d ID %u (%d in current sequence)",
+		       request->number, request->proxy->id, home->num_received_pings);
+
+		/*
+		 *	Remove the request from any hashes
+		 */
+		fr_event_delete(el, &request->ev);
+		remove_from_proxy_hash(request);
+
+		/*
+		 *	The control socket may have marked the home server as
+		 *	alive.  OR, it may have suddenly started responding to
+		 *	requests again.  If so, don't re-do the "make alive"
+		 *	work.
+		 */
+		if (home->state == HOME_STATE_ALIVE) break;
+
+		/*
+		 *	It's dead, and we haven't received enough ping
+		 *	responses to mark it "alive".  Wait a bit.
+		 *
+		 *	If it's zombie, we mark it alive immediately.
+		 */
+		if (HOME_SERVER_IS_DEAD(home) &&
+		    (home->num_received_pings < home->num_pings_to_alive)) {
+			return;
+		}
+
+		/*
+		 *	Mark it alive and delete any outstanding
+		 *	pings.
+		 */
+		mark_home_server_alive(request, home);
+		break;
+
+	default:
+		RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]);
+		break;
+	}
+
+	rad_assert(!request->in_request_hash);
+	rad_assert(!request->in_proxy_hash);
+	rad_assert(request->ev == NULL);
+	NO_CHILD_THREAD;
+	request_done(request, FR_ACTION_DONE);
+}
+
+/*
+ *	Add +/- 2s of jitter, as suggested in RFC 3539
+ *	and in RFC 5080.
+ */
+static void add_jitter(struct timeval *when)
+{
+	uint32_t jitter;
+
+	when->tv_sec -= 2;
+
+	jitter = fr_rand();
+	jitter ^= (jitter >> 10);
+	jitter &= ((1 << 22) - 1); /* 22 bits of 1 */
+
+	/*
+	 *	Add in ~ (4 * USEC) of jitter.
+	 */
+	tv_add(when, jitter);
+}
+
+/*
+ *	Called from start of zombie period, OR after control socket
+ *	marks the home server dead.
+ */
+static void ping_home_server(void *ctx)
+{
+	home_server_t *home = talloc_get_type_abort(ctx, home_server_t);
+	REQUEST *request;
+	VALUE_PAIR *vp;
+	struct timeval when, now;
+
+	if ((home->state == HOME_STATE_ALIVE) ||
+	    (home->ev != NULL)) {
+		return;
+	}
+
+	gettimeofday(&now, NULL);
+	ASSERT_MASTER;
+
+	/*
+	 *	We've run out of zombie time.  Mark it dead.
+	 */
+	if (home->state == HOME_STATE_ZOMBIE) {
+		when = home->zombie_period_start;
+		when.tv_sec += home->zombie_period;
+
+		if (timercmp(&when, &now, <)) {
+			DEBUG("PING: Zombie period is over for home server %s", home->log_name);
+			mark_home_server_dead(home, &now, false);
+		}
+	}
+
+	/*
+	 *	We're not supposed to be pinging it.  Just wake up
+	 *	when we're supposed to mark it dead.
+	 */
+	if (home->ping_check == HOME_PING_CHECK_NONE) {
+		if (home->state == HOME_STATE_ZOMBIE) {
+			home->when = home->zombie_period_start;
+			home->when.tv_sec += home->zombie_period;
+			INSERT_EVENT(ping_home_server, home);
+		}
+
+		/*
+		 *	Else mark_home_server_dead will set a timer
+		 *	for revive_interval.
+		 */
+		return;
+	}
+
+
+	request = request_alloc(NULL);
+	if (!request) return;
+	request->number = request_num_counter++;
+	NO_CHILD_THREAD;
+
+	request->proxy = rad_alloc(request, true);
+	request->root = &main_config;
+	rad_assert(request->proxy != NULL);
+
+	if (home->ping_check == HOME_PING_CHECK_STATUS_SERVER) {
+		request->proxy->code = PW_CODE_STATUS_SERVER;
+
+		fr_pair_make(request->proxy, &request->proxy->vps,
+			 "Message-Authenticator", "0x00", T_OP_SET);
+
+	} else if ((home->type == HOME_TYPE_AUTH) ||
+		   (home->type == HOME_TYPE_AUTH_ACCT)) {
+		request->proxy->code = PW_CODE_ACCESS_REQUEST;
+
+		fr_pair_make(request->proxy, &request->proxy->vps,
+			 "User-Name", home->ping_user_name, T_OP_SET);
+		fr_pair_make(request->proxy, &request->proxy->vps,
+			 "User-Password", home->ping_user_password, T_OP_SET);
+		fr_pair_make(request->proxy, &request->proxy->vps,
+			 "Service-Type", "Authenticate-Only", T_OP_SET);
+		fr_pair_make(request->proxy, &request->proxy->vps,
+			 "Message-Authenticator", "0x00", T_OP_SET);
+
+#ifdef WITH_ACCOUNTING
+	} else if (home->type == HOME_TYPE_ACCT) {
+		request->proxy->code = PW_CODE_ACCOUNTING_REQUEST;
+
+		fr_pair_make(request->proxy, &request->proxy->vps,
+			 "User-Name", home->ping_user_name, T_OP_SET);
+		fr_pair_make(request->proxy, &request->proxy->vps,
+			 "Acct-Status-Type", "Stop", T_OP_SET);
+		fr_pair_make(request->proxy, &request->proxy->vps,
+			 "Acct-Session-Id", "00000000", T_OP_SET);
+		vp = fr_pair_make(request->proxy, &request->proxy->vps,
+			      "Event-Timestamp", "0", T_OP_SET);
+		vp->vp_date = now.tv_sec;
+#endif
+
+	} else {
+		/*
+		 *	Unkown home server type.
+		 */
+		talloc_free(request);
+		return;
+	}
+
+	vp = fr_pair_make(request->proxy, &request->proxy->vps,
+		      "NAS-Identifier", "", T_OP_SET);
+	if (vp) {
+		fr_pair_value_sprintf(vp, "Status Check %u. Are you alive?",
+			    home->num_sent_pings);
+	}
+
+#ifdef WITH_TCP
+	request->proxy->proto = home->proto;
+#endif
+	request->proxy->src_ipaddr = home->src_ipaddr;
+	request->proxy->dst_ipaddr = home->ipaddr;
+	request->proxy->dst_port = home->port;
+	request->home_server = home;
+#ifdef DEBUG_STATE_MACHINE
+	if (rad_debug_lvl) printf("(%u) ********\tSTATE %s C-%s -> C-%s\t********\n", request->number, __FUNCTION__,
+			       child_state_names[request->child_state],
+			       child_state_names[REQUEST_DONE]);
+	if (rad_debug_lvl) printf("(%u) ********\tNEXT-STATE %s -> %s\n", request->number, __FUNCTION__, "request_ping");
+#endif
+#ifdef HAVE_PTHREAD_H
+	rad_assert(request->child_pid == NO_SUCH_CHILD_PID);
+#endif
+	request->child_state = REQUEST_PROXIED;
+	request->process = request_ping;
+
+	rad_assert(request->proxy_listener == NULL);
+
+	if (!insert_into_proxy_hash(request)) {
+		RPROXY("Failed to insert status check %d into proxy list.  Discarding it.",
+		       request->number);
+
+		rad_assert(!request->in_request_hash);
+		rad_assert(!request->in_proxy_hash);
+		rad_assert(request->ev == NULL);
+		talloc_free(request);
+		return;
+	}
+
+	/*
+	 *	Set up the timer callback.
+	 */
+	when = now;
+	when.tv_sec += home->ping_timeout;
+
+	DEBUG("PING: Waiting %u seconds for response to ping",
+	      home->ping_timeout);
+
+	STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+	home->num_sent_pings++;
+
+	rad_assert(request->proxy_listener != NULL);
+	request->proxy_listener->proxy_encode(request->proxy_listener, request);
+	debug_packet(request, request->proxy, false);
+	request->proxy_listener->proxy_send(request->proxy_listener,
+					    request);
+
+	/*
+	 *	Add +/- 2s of jitter, as suggested in RFC 3539
+	 *	and in the Issues and Fixes draft.
+	 */
+	home->when = now;
+	home->when.tv_sec += home->ping_interval;
+
+	add_jitter(&home->when);
+
+	DEBUG("PING: Next status packet in %u seconds", home->ping_interval);
+	INSERT_EVENT(ping_home_server, home);
+}
+
+static void home_trigger(home_server_t *home, char const *trigger)
+{
+	REQUEST *my_request;
+	RADIUS_PACKET *my_packet;
+
+	my_request = talloc_zero(NULL, REQUEST);
+	my_packet = talloc_zero(my_request, RADIUS_PACKET);
+	my_request->proxy = my_packet;
+	my_packet->dst_ipaddr = home->ipaddr;
+	my_packet->src_ipaddr = home->src_ipaddr;
+
+	exec_trigger(my_request, home->cs, trigger, false);
+	talloc_free(my_request);
+}
+
+static void mark_home_server_zombie(home_server_t *home, struct timeval *now, struct timeval *response_window)
+{
+	time_t start;
+	char buffer[128];
+
+	ASSERT_MASTER;
+
+	rad_assert((home->state == HOME_STATE_ALIVE) ||
+		   (home->state == HOME_STATE_UNKNOWN));
+
+	/*
+	 *	We've received a real packet recently.  Don't mark the
+	 *	server as zombie until we've received NO packets for a
+	 *	while.  The "1/4" of zombie period was chosen rather
+	 *	arbitrarily.  It's a balance between too short, which
+	 *	gives quick fail-over and fail-back, or too long,
+	 *	where the proxy still sends packets to an unresponsive
+	 *	home server.
+	 */
+	start = now->tv_sec - ((home->zombie_period + 3) / 4);
+	if (home->last_packet_recv >= start) {
+		DEBUG("Received reply from home server %d seconds ago.  Might not be zombie.",
+		      (int) (now->tv_sec - home->last_packet_recv));
+		return;
+	}
+
+	home->state = HOME_STATE_ZOMBIE;
+	home_trigger(home, "home_server.zombie");
+
+	/*
+	 *	Set the home server to "zombie", as of the time
+	 *	calculated above.
+	 */
+	home->zombie_period_start.tv_sec = start;
+	home->zombie_period_start.tv_usec = USEC / 2;
+
+	fr_event_delete(el, &home->ev);
+
+	home->num_sent_pings = 0;
+	home->num_received_pings = 0;
+
+	PROXY( "Marking home server %s port %d as zombie (it has not responded in %d.%06d seconds).",
+	       inet_ntop(home->ipaddr.af, &home->ipaddr.ipaddr,
+			 buffer, sizeof(buffer)),
+	       home->port, (int) response_window->tv_sec, (int) response_window->tv_usec);
+
+	ping_home_server(home);
+}
+
+
+void revive_home_server(void *ctx)
+{
+	home_server_t *home = talloc_get_type_abort(ctx, home_server_t);
+	char buffer[128];
+
+	home->state = HOME_STATE_ALIVE;
+	home->response_timeouts = 0;
+	home_trigger(home, "home_server.alive");
+	home->currently_outstanding = 0;
+	gettimeofday(&home->revive_time, NULL);
+
+	/*
+	 *	Delete any outstanding events.
+	 */
+	ASSERT_MASTER;
+	if (home->ev) fr_event_delete(el, &home->ev);
+
+	PROXY( "Marking home server %s port %d alive again... we have no idea if it really is alive or not.",
+	       inet_ntop(home->ipaddr.af, &home->ipaddr.ipaddr,
+			 buffer, sizeof(buffer)),
+	       home->port);
+}
+
+#ifdef WITH_TLS
+static int eol_home_listener(UNUSED void *ctx, void *data)
+{
+	rad_listen_t *this = talloc_get_type_abort(data, rad_listen_t);
+
+	/*
+	 *	The socket isn't blocked, we can still use it.
+	 *
+	 *	i.e. the home server is dead for a reason OTHER than
+	 *	"all available sockets are blocked".
+	 *
+	 *	We can still ping the home server via sockets which
+	 *	are writable.
+	 */
+	if (!this->blocked) return 0;
+
+	this->status = RAD_LISTEN_STATUS_EOL;
+
+	FD_MUTEX_LOCK(&fd_mutex);
+	this->next = new_listeners;
+	new_listeners = this;
+	FD_MUTEX_UNLOCK(&fd_mutex);
+
+	return 1;		/* alway delete from this tree */
+}
+#endif
+
+void mark_home_server_dead(home_server_t *home, struct timeval *when, bool down)
+{
+	int previous_state = home->state;
+	char buffer[128];
+
+	PROXY( "Marking home server %s port %d as dead.",
+	       inet_ntop(home->ipaddr.af, &home->ipaddr.ipaddr,
+			 buffer, sizeof(buffer)),
+	       home->port);
+
+	home->state = HOME_STATE_IS_DEAD;
+	home_trigger(home, "home_server.dead");
+
+#ifdef WITH_TLS
+	/*
+	 *	If the home server is dead, then close all of the sockets associated with it.
+	 *
+	 *	Note that the "EOL listener" code expects to _also_
+	 *	delete the listeners.  At which point we end up with a
+	 *	mutex locked twice, and bad things happen.  The
+	 *	solution is to move the listeners to the global
+	 *	"waiting for update" list, and then notify ourselves
+	 *	that there are listeners waiting to be updated.
+	 */
+	if (home->listeners) {
+		ASSERT_MASTER;
+
+		rbtree_walk(home->listeners, RBTREE_DELETE_ORDER, eol_home_listener, NULL);
+		radius_signal_self(RADIUS_SIGNAL_SELF_NEW_FD);
+	}
+#endif
+
+	/*
+	 *	Administratively down - don't do anything to bring it
+	 *	up.
+	 */
+	if (down) {
+		home->state = HOME_STATE_ADMIN_DOWN;
+		return;
+	}
+
+	/*
+	 *	Ping it if configured, AND we can ping it.
+	 */
+	if ((home->ping_check != HOME_PING_CHECK_NONE) &&
+	    (previous_state != HOME_STATE_CONNECTION_FAIL)) {
+		/*
+		 *	If the control socket marks us dead, start
+		 *	pinging.  Otherwise, we already started
+		 *	pinging when it was marked "zombie".
+		 */
+		if (previous_state == HOME_STATE_ALIVE) {
+			ping_home_server(home);
+		} else {
+			DEBUG("PING: Already pinging home server %s", home->log_name);
+		}
+
+	} else {
+		/*
+		 *	Revive it after a fixed period of time.  This
+		 *	is very, very, bad.
+		 */
+		home->when = *when;
+		home->when.tv_sec += home->revive_interval;
+
+		DEBUG("PING: Reviving home server %s in %u seconds", home->log_name, home->revive_interval);
+		ASSERT_MASTER;
+		INSERT_EVENT(revive_home_server, home);
+	}
+}
+
+/** Wait for a reply after proxying a request.
+ *
+ *  Retransmit the proxied packet, or time out and go to
+ *  proxy_no_reply.  Mark the home server unresponsive, etc.
+ *
+ *  If we do receive a reply, we transition to proxy_running.
+ *
+ *  \dot
+ *	digraph proxy_wait_for_reply {
+ *		proxy_wait_for_reply;
+ *
+ *		proxy_wait_for_reply -> retransmit_proxied_request [ label = "DUP", arrowhead = "none" ];
+ *		proxy_wait_for_reply -> proxy_no_reply [ label = "TIMER >= response_window" ];
+ *		proxy_wait_for_reply -> timer [ label = "TIMER < max_request_time" ];
+ *		proxy_wait_for_reply -> proxy_running [ label = "PROXY_REPLY" arrowhead = "none"];
+ *		proxy_wait_for_reply -> done [ label = "TIMER >= max_request_time" ];
+ *	}
+ *  \enddot
+ */
+static void proxy_wait_for_reply(REQUEST *request, int action)
+{
+	struct timeval now, when;
+	struct timeval *response_window = NULL;
+	home_server_t *home = request->home_server;
+	char buffer[128];
+
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+	CHECK_FOR_STOP;
+
+	rad_assert(request->packet->code != PW_CODE_STATUS_SERVER);
+	rad_assert(request->home_server != NULL);
+
+	gettimeofday(&now, NULL);
+
+	switch (action) {
+	case FR_ACTION_DUP:
+		/*
+		 *	The request was proxied to a virtual server.
+		 *	Ignore the retransmit.
+		 */
+		if (request->home_server->virtual_server) return;
+
+		/*
+		 *	Failed connections get the home server marked
+		 *	as dead.
+		 */
+		if (home->state == HOME_STATE_CONNECTION_FAIL) {
+			mark_home_server_dead(home, &now, false);
+		}
+
+		/*
+		 *	We have a reply, ignore the retransmit.
+		 */
+		if (request->proxy_reply) return;
+
+		/*
+		 *	Use a new connection when the home server is
+		 *	dead, or when there's no proxy listener, or
+		 *	when the listener is failed or dead.
+		 *
+		 *	If the listener is known or frozen, use it for
+		 *	retransmits.
+		 */
+		if (HOME_SERVER_IS_DEAD(home) ||
+		    !request->proxy_listener ||
+		    (request->proxy_listener->status >= RAD_LISTEN_STATUS_EOL)) {
+			request_proxy_anew(request);
+			return;
+		}
+
+#ifdef WITH_TCP
+		/*
+		 *	The home server is still alive, but TCP.  We
+		 *	rely on TCP to get the request and reply back.
+		 *	So there's no need to retransmit.
+		 */
+		if (home->proto == IPPROTO_TCP) {
+			DEBUG2("Suppressing duplicate proxied request (tcp) to home server %s port %d proto TCP - ID: %d",
+			       inet_ntop(request->proxy->dst_ipaddr.af,
+					 &request->proxy->dst_ipaddr.ipaddr,
+					 buffer, sizeof(buffer)),
+			       request->proxy->dst_port,
+			       request->proxy->id);
+			return;
+		}
+#endif
+
+		/*
+		 *	More than one retransmit a second is stupid,
+		 *	and should be suppressed by the proxy.
+		 */
+		when = request->proxy->timestamp;
+		when.tv_sec++;
+
+		if (timercmp(&now, &when, <)) {
+			DEBUG2("Suppressing duplicate proxied request (too fast) to home server %s port %d proto TCP - ID: %d",
+			       inet_ntop(request->proxy->dst_ipaddr.af,
+					 &request->proxy->dst_ipaddr.ipaddr,
+					 buffer, sizeof(buffer)),
+			       request->proxy->dst_port,
+			       request->proxy->id);
+			return;
+		}
+
+#ifdef WITH_ACCOUNTING
+		/*
+		 *	If we update the Acct-Delay-Time, we need to
+		 *	get a new ID.
+		 */
+		if ((request->packet->code == PW_CODE_ACCOUNTING_REQUEST) &&
+		    fr_pair_find_by_num(request->proxy->vps, PW_ACCT_DELAY_TIME, 0, TAG_ANY)) {
+			request_proxy_anew(request);
+			return;
+		}
+#endif
+
+		RDEBUG2("Sending duplicate proxied request to home server %s port %d - ID: %d",
+			inet_ntop(request->proxy->dst_ipaddr.af,
+				  &request->proxy->dst_ipaddr.ipaddr,
+				  buffer, sizeof(buffer)),
+			request->proxy->dst_port,
+			request->proxy->id);
+		request->num_proxied_requests++;
+
+		rad_assert(request->proxy_listener != NULL);
+		FR_STATS_TYPE_INC(home->stats.total_requests);
+		home->last_packet_sent = now.tv_sec;
+		request->proxy->timestamp = now;
+		debug_packet(request, request->proxy, false);
+		request->proxy_listener->proxy_send(request->proxy_listener, request);
+		break;
+
+	case FR_ACTION_TIMER:
+		/*
+		 *	Failed connections get the home server marked
+		 *	as dead.
+		 */
+		if (home->state == HOME_STATE_CONNECTION_FAIL) {
+			mark_home_server_dead(home, &now, false);
+		}
+
+		response_window = request_response_window(request);
+
+#ifdef WITH_TCP
+		if (!request->proxy_listener ||
+		    (request->proxy_listener->status >= RAD_LISTEN_STATUS_EOL)) {
+			remove_from_proxy_hash(request);
+
+			when = request->packet->timestamp;
+			when.tv_sec += request->root->max_request_time;
+
+			if (timercmp(&when, &now, >)) {
+				RDEBUG("Waiting for client retransmission in order to do a proxy retransmit");
+				STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+				return;
+			}
+		} else
+#endif
+		{
+			/*
+			 *	Wake up "response_window" time in the future.
+			 *	i.e. when MY packet hasn't received a response.
+			 *
+			 *	Note that we DO NOT mark the home server as
+			 *	zombie if it doesn't respond to us.  It may be
+			 *	responding to other (better looking) packets.
+			 */
+			when = request->proxy->timestamp;
+			timeradd(&when, response_window, &when);
+
+			/*
+			 *	Not at the response window.  Set the timer for
+			 *	that.
+			 */
+			if (timercmp(&when, &now, >)) {
+				struct timeval diff;
+				timersub(&when, &now, &diff);
+
+				RDEBUG("Expecting proxy response no later than %d.%06d seconds from now",
+				       (int) diff.tv_sec, (int) diff.tv_usec);
+				STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+				return;
+			}
+		}
+
+		RDEBUG("No proxy response, giving up on request and marking it done");
+
+		/*
+		 *	If we haven't received any packets for
+		 *	"response_window", then mark the home server
+		 *	as zombie.
+		 *
+		 *	This check should really be part of a home
+		 *	server state machine.
+		 */
+		if ((home->state == HOME_STATE_ALIVE) ||
+		     (home->state == HOME_STATE_UNKNOWN)) {
+			home->response_timeouts++;
+			if (home->response_timeouts >= home->max_response_timeouts)
+				mark_home_server_zombie(home, &now, response_window);
+		}
+
+		FR_STATS_TYPE_INC(home->stats.total_timeouts);
+		if (home->type == HOME_TYPE_AUTH) {
+			if (request->proxy_listener) FR_STATS_TYPE_INC(request->proxy_listener->stats.total_timeouts);
+			FR_STATS_TYPE_INC(proxy_auth_stats.total_timeouts);
+		}
+#ifdef WITH_ACCT
+		else if (home->type == HOME_TYPE_ACCT) {
+			if (request->proxy_listener) FR_STATS_TYPE_INC(request->proxy_listener->stats.total_timeouts);
+			FR_STATS_TYPE_INC(proxy_acct_stats.total_timeouts);
+		}
+#endif
+#ifdef WITH_COA
+		else if (home->type == HOME_TYPE_COA) {
+			if (request->proxy_listener) FR_STATS_TYPE_INC(request->proxy_listener->stats.total_timeouts);
+
+			if (request->packet->code == PW_CODE_COA_REQUEST) {
+				FR_STATS_TYPE_INC(proxy_coa_stats.total_timeouts);
+			} else {
+				FR_STATS_TYPE_INC(proxy_dsc_stats.total_timeouts);
+			}
+		}
+#endif
+
+		/*
+		 *	There was no response within the window.  Stop
+		 *	the request.  If the client retransmitted, it
+		 *	may have failed over to another home server.
+		 *	But that one may be dead, too.
+		 *
+		 * 	The extra verbose message if we have a username,
+		 *	is extremely useful if the proxy is part of a chain
+		 *	and the final home server, is not the one we're
+		 *	proxying to.
+		 */
+		if (request->username) {
+			RERROR("Failing proxied request for user \"%s\", due to lack of any response from home "
+			       "server %s port %d",
+			       request->username->vp_strvalue,
+			       inet_ntop(request->proxy->dst_ipaddr.af,
+					 &request->proxy->dst_ipaddr.ipaddr,
+					 buffer, sizeof(buffer)),
+			       request->proxy->dst_port);
+		} else {
+			RERROR("Failing proxied request, due to lack of any response from home server %s port %d",
+			       inet_ntop(request->proxy->dst_ipaddr.af,
+					 &request->proxy->dst_ipaddr.ipaddr,
+					 buffer, sizeof(buffer)),
+			       request->proxy->dst_port);
+		}
+
+		if (setup_post_proxy_fail(request)) {
+			request_queue_or_run(request, proxy_no_reply);
+		} else {
+			gettimeofday(&request->reply->timestamp, NULL);
+			request_cleanup_delay_init(request);
+		}
+		break;
+
+		/*
+		 *	We received a new reply.  Go process it.
+		 */
+	case FR_ACTION_PROXY_REPLY:
+		request_queue_or_run(request, proxy_running);
+		break;
+
+	default:
+		RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]);
+		break;
+	}
+}
+#endif	/* WITH_PROXY */
+
+
+/***********************************************************************
+ *
+ *  CoA code
+ *
+ ***********************************************************************/
+#ifdef WITH_COA
+/*
+ *	See if we need to originate a CoA request.
+ */
+static void request_coa_originate(REQUEST *request)
+{
+	int rcode, pre_proxy_type = 0;
+	VALUE_PAIR *vp;
+	REQUEST *coa;
+	fr_ipaddr_t ipaddr;
+	char const *old_server;
+	char buffer[256];
+
+	VERIFY_REQUEST(request);
+
+	rad_assert(request->coa != NULL);
+	rad_assert(request->proxy == NULL);
+	rad_assert(!request->in_proxy_hash);
+	rad_assert(request->proxy_reply == NULL);
+
+	/*
+	 *	Check whether we want to originate one, or cancel one.
+	 */
+	vp = fr_pair_find_by_num(request->config, PW_SEND_COA_REQUEST, 0, TAG_ANY);
+	if (!vp) {
+		vp = fr_pair_find_by_num(request->coa->proxy->vps, PW_SEND_COA_REQUEST, 0, TAG_ANY);
+	}
+
+	if (vp) {
+		if (vp->vp_integer == 0) {
+		fail:
+			TALLOC_FREE(request->coa);
+			return;
+		}
+	}
+
+	if (!main_config.proxy_requests) {
+		RWDEBUG("Cannot originate CoA packets unless 'proxy_requests = yes'");
+		TALLOC_FREE(request->coa);
+		return;
+	}
+
+	coa = request->coa;
+	coa->listener = NULL;	/* copied here by request_alloc_fake(), but not needed */
+
+#ifdef WITH_COA_TUNNEL
+	/*
+	 *	Proxy-To-Originating-Realm is preferred to any other
+	 *	method of originating CoA requests.
+	 */
+	vp = fr_pair_find_by_num(coa->proxy->vps, PW_PROXY_TO_ORIGINATING_REALM, 0, TAG_ANY);
+	if (vp) {
+		/*
+		 *	This function will set request->home_server,
+		 *	and also request->proxy_listener.
+		 */
+		if (listen_coa_find(coa, vp->vp_strvalue) < 0) {
+			RWDEBUG("Unknown Originating realm '%s'", vp->vp_strvalue);
+			return;
+		}
+
+		goto set_packet_type;
+	}
+#endif
+
+	/*
+	 *	src_ipaddr will be set up in proxy_encode.
+	 */
+	memset(&ipaddr, 0, sizeof(ipaddr));
+	vp = fr_pair_find_by_num(coa->proxy->vps, PW_PACKET_DST_IP_ADDRESS, 0, TAG_ANY);
+	if (vp) {
+		ipaddr.af = AF_INET;
+		ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+		ipaddr.prefix = 32;
+	} else if ((vp = fr_pair_find_by_num(coa->proxy->vps, PW_PACKET_DST_IPV6_ADDRESS, 0, TAG_ANY)) != NULL) {
+		ipaddr.af = AF_INET6;
+		ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr;
+		ipaddr.prefix = 128;
+	} else if ((vp = fr_pair_find_by_num(coa->proxy->vps, PW_HOME_SERVER_POOL, 0, TAG_ANY)) != NULL) {
+		coa->home_pool = home_pool_byname(vp->vp_strvalue,
+						  HOME_TYPE_COA);
+		if (!coa->home_pool) {
+			RWDEBUG2("No such home_server_pool %s",
+			       vp->vp_strvalue);
+			goto fail;
+		}
+
+		/*
+		 *	Prefer the pool to one server
+		 */
+	} else if (request->client->coa_home_pool) {
+		coa->home_pool = request->client->coa_home_pool;
+
+	} else if (request->client->coa_home_server) {
+		coa->home_server = request->client->coa_home_server;
+
+	} else {
+		/*
+		 *	If all else fails, send it to the client that
+		 *	originated this request.
+		 */
+		memcpy(&ipaddr, &request->packet->src_ipaddr, sizeof(ipaddr));
+	}
+
+	/*
+	 *	Use the pool, if it exists.
+	 */
+	if (coa->home_pool) {
+		coa->home_server = home_server_ldb(NULL, coa->home_pool, coa);
+		if (!coa->home_server) {
+			RWDEBUG("No live home server for home_server_pool %s", coa->home_pool->name);
+			goto fail;
+		}
+		home_server_update_request(coa->home_server, coa);
+
+	} else if (!coa->home_server) {
+		uint16_t port = PW_COA_UDP_PORT;
+
+		vp = fr_pair_find_by_num(coa->proxy->vps, PW_PACKET_DST_PORT, 0, TAG_ANY);
+		if (vp) port = vp->vp_integer;
+
+		coa->home_server = home_server_find(&ipaddr, port, IPPROTO_UDP);
+		if (!coa->home_server) {
+			RWDEBUG2("Unknown destination %s:%d for CoA request.",
+			       inet_ntop(ipaddr.af, &ipaddr.ipaddr,
+					 buffer, sizeof(buffer)), port);
+			goto fail;
+		}
+	}
+
+#ifdef WITH_COA_TUNNEL
+set_packet_type:
+#endif
+	vp = fr_pair_find_by_num(coa->proxy->vps, PW_PACKET_TYPE, 0, TAG_ANY);
+	if (vp) {
+		switch (vp->vp_integer) {
+		case PW_CODE_COA_REQUEST:
+		case PW_CODE_DISCONNECT_REQUEST:
+			coa->proxy->code = vp->vp_integer;
+			break;
+
+		default:
+			DEBUG("Cannot set CoA Packet-Type to code %d",
+			      vp->vp_integer);
+			goto fail;
+		}
+	}
+
+	if (!coa->proxy->code) coa->proxy->code = PW_CODE_COA_REQUEST;
+
+	/*
+	 *	The rest of the server code assumes that
+	 *	request->packet && request->reply exist.  Copy them
+	 *	from the original request.
+	 */
+	rad_assert(coa->packet != NULL);
+	rad_assert(coa->packet->vps == NULL);
+
+	coa->packet = rad_copy_packet(coa, request->packet);
+	coa->reply = rad_copy_packet(coa, request->reply);
+
+	coa->config = fr_pair_list_copy(coa, request->config);
+	coa->num_coa_requests = 0;
+	coa->number = request->number; /* it's associated with the same request */
+
+	/*
+	 *	Call the pre-proxy routines.
+	 */
+	vp = fr_pair_find_by_num(request->config, PW_PRE_PROXY_TYPE, 0, TAG_ANY);
+	if (vp) {
+		DICT_VALUE const *dval = dict_valbyattr(vp->da->attr, vp->da->vendor, vp->vp_integer);
+		/* Must be a validation issue */
+		rad_assert(dval);
+		RDEBUG2("Found Pre-Proxy-Type %s", dval->name);
+		pre_proxy_type = vp->vp_integer;
+	}
+
+	/*
+	 *	Run the request through the virtual server for the
+	 *	home server, OR through the virtual server for the
+	 *	home server pool.
+	 */
+	old_server = request->server;
+	if (coa->home_server && coa->home_server->virtual_server) {
+		coa->server = coa->home_server->virtual_server;
+
+#ifdef WITH_COA_TUNNEL
+	} else if (coa->proxy_listener && (coa->proxy_listener->type != RAD_LISTEN_PROXY)) {
+		rad_assert((coa->proxy->code == PW_CODE_COA_REQUEST) ||
+			   (coa->proxy->code == PW_CODE_DISCONNECT_REQUEST));
+		rad_assert(coa->home_server != NULL);
+		rad_assert(coa->home_server->recv_coa_server != NULL);
+		coa->server = coa->home_server->recv_coa_server;
+#endif
+
+	} else if (coa->home_pool && coa->home_pool->virtual_server) {
+		coa->server = coa->home_pool->virtual_server;
+	}
+
+	RDEBUG2("server %s {", coa->server);
+	RINDENT();
+	rcode = process_pre_proxy(pre_proxy_type, coa);
+	REXDENT();
+	RDEBUG2("}");
+	coa->server = old_server;
+
+	switch (rcode) {
+	default:
+		goto fail;
+
+	/*
+	 *	Only send the CoA packet if the pre-proxy code succeeded.
+	 */
+	case RLM_MODULE_NOOP:
+	case RLM_MODULE_OK:
+	case RLM_MODULE_UPDATED:
+		break;
+	}
+
+	/*
+	 *	Source IP / port is set when the proxy socket
+	 *	is chosen.
+	 */
+	coa->proxy->dst_ipaddr = coa->home_server->ipaddr;
+	coa->proxy->dst_port = coa->home_server->port;
+
+	if (!insert_into_proxy_hash(coa)) {
+		radlog_request(L_PROXY, 0, coa, "Failed to insert CoA request into proxy list");
+		goto fail;
+	}
+
+	/*
+	 *	We CANNOT divorce the CoA request from the parent
+	 *	request.  This function is running in a child thread,
+	 *	and we need access to the main event loop in order to
+	 *	to add the timers for the CoA packet.
+	 *
+	 *	Instead, we wait for the timer on the parent request
+	 *	to fire.
+	 */
+	gettimeofday(&coa->proxy->timestamp, NULL);
+	coa->packet->timestamp = coa->proxy->timestamp; /* for max_request_time */
+	coa->home_server->last_packet_sent = coa->proxy->timestamp.tv_sec;
+	coa->delay = 0;		/* need to calculate a new delay */
+
+	/*
+	 *	If requested, put a State attribute into the packet,
+	 *	and cache the VPS.
+	 */
+	fr_state_put_vps(coa, NULL, coa->packet);
+
+	/*
+	 *	Encode the packet before we do anything else.
+	 */
+	coa->proxy_listener->proxy_encode(coa->proxy_listener, coa);
+	debug_packet(coa, coa->proxy, false);
+
+#ifdef DEBUG_STATE_MACHINE
+	if (rad_debug_lvl) printf("(%u) ********\tSTATE %s C-%s -> C-%s\t********\n", request->number, __FUNCTION__,
+			       child_state_names[request->child_state],
+			       child_state_names[REQUEST_PROXIED]);
+#endif
+
+	/*
+	 *	Set the state function, then the state, no child, and
+	 *	send the packet.
+	 */
+	coa->process = coa_wait_for_reply;
+	coa->child_state = REQUEST_PROXIED;
+
+#ifdef HAVE_PTHREAD_H
+	coa->child_pid = NO_SUCH_CHILD_PID;
+#endif
+
+	if (we_are_master()) coa_separate(request->coa, true);
+
+	/*
+	 *	And send the packet.
+	 */
+	coa->proxy_listener->proxy_send(coa->proxy_listener, coa);
+}
+
+
+static void coa_retransmit(REQUEST *request)
+{
+	uint32_t delay, frac;
+	struct timeval now, when, mrd;
+	char buffer[128];
+
+	VERIFY_REQUEST(request);
+
+	/*
+	 *	Don't do fail-over.  This is a 3.1 feature.
+	 */
+	if (!request->home_server ||
+	    HOME_SERVER_IS_DEAD(request->home_server) ||
+	    request->proxy_reply ||
+	    !request->proxy_listener ||
+	    (request->proxy_listener->status >= RAD_LISTEN_STATUS_EOL)) {
+		request_done(request, FR_ACTION_COA_CANCELLED);
+		return;
+	}
+
+	fr_event_now(el, &now);
+
+	/*
+	 *	Home server has gone away.  The request is done.
+	 */
+	if (!request->home_server) {
+		RDEBUG("No home server for CoA packet.  Failing it.");
+		goto fail;
+	}
+
+	if (request->delay == 0) {
+		/*
+		 *	Implement re-transmit algorithm as per RFC 5080
+		 *	Section 2.2.1.
+		 *
+		 *	We want IRT + RAND*IRT
+		 *	or 0.9 IRT + rand(0,.2) IRT
+		 *
+		 *	2^20 ~ USEC, and we want 2.
+		 *	rand(0,0.2) USEC ~ (rand(0,2^21) / 10)
+		 */
+		delay = (fr_rand() & ((1 << 22) - 1)) / 10;
+		request->delay = delay * request->home_server->coa_irt;
+		delay = request->home_server->coa_irt * USEC;
+		delay -= delay / 10;
+		delay += request->delay;
+		request->delay = delay;
+
+		when = request->proxy->timestamp;
+		tv_add(&when, delay);
+
+		if (timercmp(&when, &now, >)) {
+			STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+			return;
+		}
+	}
+
+	/*
+	 *	Retransmit CoA request.
+	 */
+
+	/*
+	 *	Cap count at MRC, if it is non-zero.
+	 */
+	if (request->home_server->coa_mrc &&
+	    (request->num_coa_requests >= request->home_server->coa_mrc)) {
+		RERROR("Failing request - originate-coa ID %u, due to lack of any response from coa server %s port %d",
+		       request->proxy->id,
+			       inet_ntop(request->proxy->dst_ipaddr.af,
+					 &request->proxy->dst_ipaddr.ipaddr,
+					 buffer, sizeof(buffer)),
+			       request->proxy->dst_port);
+
+	fail:
+		if (setup_post_proxy_fail(request)) {
+			request_queue_or_run(request, coa_no_reply);
+		} else {
+			request_done(request, FR_ACTION_DONE);
+		}
+		return;
+	}
+
+	/*
+	 *	RFC 5080 Section 2.2.1
+	 *
+	 *	RT = 2*RTprev + RAND*RTprev
+	 *	   = 1.9 * RTprev + rand(0,.2) * RTprev
+	 *	   = 1.9 * RTprev + rand(0,1) * (RTprev / 5)
+	 */
+	delay = fr_rand();
+	delay ^= (delay >> 16);
+	delay &= 0xffff;
+	frac = request->delay / 5;
+	delay = ((frac >> 16) * delay) + (((frac & 0xffff) * delay) >> 16);
+
+	delay += (2 * request->delay) - (request->delay / 10);
+
+	/*
+	 *	Cap delay at MRT, if MRT is non-zero.
+	 */
+	if (request->home_server->coa_mrt &&
+	    (delay > (request->home_server->coa_mrt * USEC))) {
+		int mrt_usec = request->home_server->coa_mrt * USEC;
+
+		/*
+		 *	delay = MRT + RAND * MRT
+		 *	      = 0.9 MRT + rand(0,.2)  * MRT
+		 */
+		delay = fr_rand();
+		delay ^= (delay >> 15);
+		delay &= 0x1ffff;
+		delay = ((mrt_usec >> 16) * delay) + (((mrt_usec & 0xffff) * delay) >> 16);
+		delay += mrt_usec - (mrt_usec / 10);
+	}
+
+	request->delay = delay;
+	when = now;
+	tv_add(&when, request->delay);
+	mrd = request->proxy->timestamp;
+	mrd.tv_sec += request->home_server->coa_mrd;
+
+	/*
+	 *	Cap duration at MRD.
+	 */
+	if (timercmp(&mrd, &when, <)) {
+		when = mrd;
+	}
+	STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+
+	request->num_coa_requests++; /* is NOT reset by code 3 lines above! */
+
+	FR_STATS_TYPE_INC(request->home_server->stats.total_requests);
+
+	RDEBUG2("Sending duplicate CoA request to home server %s port %d - ID: %d",
+		inet_ntop(request->proxy->dst_ipaddr.af,
+			  &request->proxy->dst_ipaddr.ipaddr,
+			  buffer, sizeof(buffer)),
+		request->proxy->dst_port,
+		request->proxy->id);
+
+	request->proxy_listener->proxy_send(request->proxy_listener,
+					    request);
+}
+
+
+/*
+ *	Enforce maximum time for CoA packets
+ */
+static bool coa_max_time(REQUEST *request)
+{
+	struct timeval now, when;
+	rad_assert(request->magic == REQUEST_MAGIC);
+#ifdef DEBUG_STATE_MACHINE
+	int action = FR_ACTION_TIMER;
+#endif
+	int mrd;
+
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+	ASSERT_MASTER;
+
+	/*
+	 *	The child thread has acknowledged it's done.
+	 *	Transition to the DONE state.
+	 *
+	 *	If the request was marked STOP, then the "check for
+	 *	stop" macro already took care of it.
+	 */
+	if (request->child_state == REQUEST_DONE) {
+	done:
+		request->max_time = true;
+		request_done(request, FR_ACTION_MAX_TIME);
+		return true;
+	}
+
+	/*
+	 *	The request is still running.  Enforce max_request_time.
+	 *
+	 *	Note that the *proxy* timestamp is the one we use, as
+	 *	that's when the CoA packet was sent.
+	 *
+	 *	Note also that if there's an error, the home server
+	 *	may not exist.
+	 */
+	fr_event_now(el, &now);
+	when = request->proxy->timestamp;
+	if (request->home_server && (request->process != coa_running)) {
+		mrd = request->home_server->coa_mrd;
+	} else {
+		mrd = request->root->max_request_time;
+	}
+	when.tv_sec += mrd;
+
+	/*
+	 *	Taking too long: tell it to die.
+	 */
+	if (timercmp(&now, &when, >=)) {
+		char buffer[256];
+
+		if (request->process != coa_running) {
+			RERROR("Failing request - originate-coa ID %u, due to lack of any response from coa server %s port %d within %d seconds",
+			       request->proxy->id,
+			       inet_ntop(request->proxy->dst_ipaddr.af,
+					 &request->proxy->dst_ipaddr.ipaddr,
+					 buffer, sizeof(buffer)),
+			       request->proxy->dst_port,
+			       mrd);
+			request_done(request, FR_ACTION_DONE);
+			return true;
+		}
+
+#ifdef HAVE_PTHREAD_H
+		/*
+		 *	If there's a child thread processing it,
+		 *	complain.
+		 */
+		if (spawn_flag &&
+		    (pthread_equal(request->child_pid, NO_SUCH_CHILD_PID) == 0)) {
+			RERROR("Unresponsive child for originate-coa, in component %s module %s",
+			      request->component ? request->component : "<core>",
+			      request->module ? request->module : "<core>");
+			exec_trigger(request, NULL, "server.thread.unresponsive", true);
+		} else
+#endif
+		{
+			RERROR("originate-coa hit max_request_time.  Cancelling it.");
+		}
+
+		/*
+		 *	Tell the request that it's done.
+		 */
+		goto done;
+	}
+
+	/*
+	 *	Let coa_retransmit() handle the retransmission timers.
+	 */
+	if (request->process != coa_running) return false;
+
+	/*
+	 *	Sleep for some more.  We HOPE that the child will
+	 *	become responsive at some point in the future.  We do
+	 *	this by adding 50% to the current timer.
+	 */
+	when = now;
+	tv_add(&when, request->delay);
+	request->delay += request->delay >> 1;
+	STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+	return false;
+}
+
+
+/** Wait for a reply after originating a CoA a request.
+ *
+ *  Retransmit the proxied packet, or time out and go to
+ *  coa_no_reply.  Mark the home server unresponsive, etc.
+ *
+ *  If we do receive a reply, we transition to coa_running.
+ *
+ *  \dot
+ *	digraph coa_wait_for_reply {
+ *		coa_wait_for_reply;
+ *
+ *		coa_wait_for_reply -> coa_no_reply [ label = "TIMER >= response_window" ];
+ *		coa_wait_for_reply -> timer [ label = "TIMER < max_request_time" ];
+ *		coa_wait_for_reply -> coa_running [ label = "PROXY_REPLY" arrowhead = "none"];
+ *		coa_wait_for_reply -> done [ label = "TIMER >= max_request_time" ];
+ *	}
+ *  \enddot
+ */
+static void coa_wait_for_reply(REQUEST *request, int action)
+{
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+	ASSERT_MASTER;
+	CHECK_FOR_STOP;
+
+	if (request->parent) coa_separate(request, false);
+
+	switch (action) {
+	case FR_ACTION_TIMER:
+		if (coa_max_time(request)) break;
+
+		coa_retransmit(request);
+		break;
+
+	case FR_ACTION_PROXY_REPLY:
+		/*
+		 *	Reset the initial delay for checking if we
+		 *	should still run.
+		 */
+		request->delay = (int)request->root->init_delay.tv_sec * USEC +
+			(int)request->root->init_delay.tv_usec;
+
+		request_queue_or_run(request, coa_running);
+		break;
+
+	default:
+		RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]);
+		break;
+	}
+}
+
+static void coa_separate(REQUEST *request, bool retransmit)
+{
+	VERIFY_REQUEST(request);
+#ifdef DEBUG_STATE_MACHINE
+	int action = FR_ACTION_TIMER;
+#endif
+
+	TRACE_STATE_MACHINE;
+	ASSERT_MASTER;
+
+	rad_assert(request->parent != NULL);
+	rad_assert(request->parent->coa == request);
+	rad_assert(request->ev == NULL);
+	rad_assert(!request->in_request_hash);
+	rad_assert(request->coa == NULL);
+
+	(void) talloc_steal(NULL, request);
+	request->parent->coa = NULL;
+	request->parent = NULL;
+
+	if (retransmit && (request->delay == 0) && !request->proxy_reply) {
+		coa_retransmit(request);
+	}
+}
+
+
+/** Process a request after the CoA has timed out.
+ *
+ *  Run the packet through Post-Proxy-Type Fail
+ *
+ *  \dot
+ *	digraph coa_no_reply {
+ *		coa_no_reply;
+ *
+ *		coa_no_reply -> dup [ label = "DUP", arrowhead = "none" ];
+ *		coa_no_reply -> timer [ label = "TIMER < max_request_time" ];
+ *		coa_no_reply -> coa_reply_too_late [ label = "PROXY_REPLY" arrowhead = "none"];
+ *		coa_no_reply -> process_proxy_reply [ label = "RUN" ];
+ *		coa_no_reply -> done [ label = "TIMER >= timeout" ];
+ *	}
+ *  \enddot
+ */
+static void coa_no_reply(REQUEST *request, int action)
+{
+	char buffer[128];
+
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+	CHECK_FOR_STOP;
+
+	switch (action) {
+	case FR_ACTION_TIMER:
+		(void) coa_max_time(request);
+		break;
+
+	case FR_ACTION_PROXY_REPLY: /* too late! */
+		RDEBUG2("Reply from CoA server %s port %d  - ID: %d arrived too late.",
+			inet_ntop(request->proxy->src_ipaddr.af,
+				  &request->proxy->src_ipaddr.ipaddr,
+				  buffer, sizeof(buffer)),
+			request->proxy->dst_port, request->proxy->id);
+		break;
+
+	case FR_ACTION_RUN:
+		if (process_proxy_reply(request, NULL)) {
+			request->handle(request);
+		}
+		request_done(request, FR_ACTION_DONE);
+		break;
+
+	default:
+		RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]);
+		break;
+	}
+}
+
+
+/** Process the request after receiving a coa reply.
+ *
+ *  Throught the post-proxy section, and the through the handler
+ *  function.
+ *
+ *  \dot
+ *	digraph coa_running {
+ *		coa_running;
+ *
+ *		coa_running -> timer [ label = "TIMER < max_request_time" ];
+ *		coa_running -> process_proxy_reply [ label = "RUN" ];
+ *		coa_running -> done [ label = "TIMER >= timeout" ];
+ *	}
+ *  \enddot
+ */
+static void coa_running(REQUEST *request, int action)
+{
+	VERIFY_REQUEST(request);
+
+	TRACE_STATE_MACHINE;
+	CHECK_FOR_STOP;
+
+	switch (action) {
+	case FR_ACTION_TIMER:
+		(void) coa_max_time(request);
+		break;
+
+	case FR_ACTION_RUN:
+		if (process_proxy_reply(request, request->proxy_reply)) {
+			request->handle(request);
+		}
+		request_done(request, FR_ACTION_DONE);
+		break;
+
+	default:
+		RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]);
+		break;
+	}
+}
+#endif	/* WITH_COA */
+
+/***********************************************************************
+ *
+ *  End of the State machine.  Start of additional helper code.
+ *
+ ***********************************************************************/
+
+/***********************************************************************
+ *
+ *	Event handlers.
+ *
+ ***********************************************************************/
+static void event_socket_handler(fr_event_list_t *xel, UNUSED int fd, void *ctx)
+{
+	rad_listen_t *listener = talloc_get_type_abort(ctx, rad_listen_t);
+
+	rad_assert(xel == el);
+
+	if ((listener->fd < 0)
+#ifdef WITH_DETAIL
+#ifndef WITH_DETAIL_THREAD
+	    && (listener->type != RAD_LISTEN_DETAIL)
+#endif
+#endif
+		) {
+		char buffer[256];
+
+		listener->print(listener, buffer, sizeof(buffer));
+		ERROR("FATAL: Asked to read from closed socket: %s",
+		       buffer);
+
+		rad_panic("Socket was closed on us!");
+		fr_exit_now(1);
+	}
+
+	listener->recv(listener);
+}
+
+#ifdef WITH_DETAIL
+#ifdef WITH_DETAIL_THREAD
+#else
+/*
+ *	This function is called periodically to see if this detail
+ *	file is available for reading.
+ */
+static void event_poll_detail(void *ctx)
+{
+	int delay;
+	rad_listen_t *this = talloc_get_type_abort(ctx, rad_listen_t);
+	struct timeval when, now;
+	listen_detail_t *detail = this->data;
+
+	rad_assert(this->type == RAD_LISTEN_DETAIL);
+
+ redo:
+	event_socket_handler(el, this->fd, this);
+
+	fr_event_now(el, &now);
+	when = now;
+
+	/*
+	 *	Backdoor API to get the delay until the next poll
+	 *	time.
+	 */
+	delay = this->encode(this, NULL);
+	if (delay == 0) goto redo;
+
+	tv_add(&when, delay);
+
+	ASSERT_MASTER;
+	if (!fr_event_insert(el, event_poll_detail, this,
+			     &when, &detail->ev)) {
+		ERROR("Failed creating handler");
+		fr_exit(1);
+	}
+}
+#endif	/* WITH_DETAIL_THREAD */
+#endif	/* WITH_DETAIL */
+
+static void event_status(struct timeval *wake)
+{
+	if (rad_debug_lvl == 0) {
+		if (just_started) {
+			INFO("Ready to process requests");
+			just_started = false;
+		}
+		return;
+	}
+
+	if (!wake) {
+		INFO("Ready to process requests");
+
+	} else if ((wake->tv_sec != 0) ||
+		   (wake->tv_usec >= 100000)) {
+		DEBUG("Waking up in %d.%01u seconds.",
+		      (int) wake->tv_sec, (unsigned int) wake->tv_usec / 100000);
+	}
+
+
+	/*
+	 *	FIXME: Put this somewhere else, where it isn't called
+	 *	all of the time...
+	 */
+
+	if (!spawn_flag) {
+		int argval;
+
+		/*
+		 *	If there are no child threads, then there may
+		 *	be child processes.  In that case, wait for
+		 *	their exit status, and throw that exit status
+		 *	away.  This helps get rid of zxombie children.
+		 */
+		while (waitpid(-1, &argval, WNOHANG) > 0) {
+			/* do nothing */
+		}
+	}
+}
+
+static void listener_free_cb(void *ctx)
+{
+	rad_listen_t *this = talloc_get_type_abort(ctx, rad_listen_t);
+	listen_socket_t *sock = this->data;
+	char buffer[1024];
+
+	if (this->count > 0) {
+		struct timeval when;
+
+		fr_event_now(el, &when);
+		when.tv_sec += 3;
+
+		ASSERT_MASTER;
+		if (!fr_event_insert(el, listener_free_cb, this, &when,
+				     &(sock->ev))) {
+			rad_panic("Failed to insert event");
+		}
+
+		return;
+	}
+
+	/*
+	 *	It's all free, close the socket.
+	 */
+
+	this->print(this, buffer, sizeof(buffer));
+	DEBUG("... cleaning up socket %s", buffer);
+	rad_assert(this->next == NULL);
+#ifdef WITH_TCP
+	fr_event_delete(el, &sock->ev);
+#endif
+	talloc_free(this);
+}
+
+#ifdef WITH_TCP
+#ifdef WITH_PROXY
+static int proxy_eol_cb(void *ctx, void *data)
+{
+	struct timeval when;
+	REQUEST *request = fr_packet2myptr(REQUEST, proxy, data);
+
+	if (request->proxy_listener != ctx) return 0;
+
+	/*
+	 *	We don't care if it's being processed in a child thread.
+	 */
+
+#ifdef WITH_ACCOUNTING
+	/*
+	 *	Accounting packets should be deleted immediately.
+	 *	They will never be retransmitted by the client.
+	 */
+	if (request->proxy->code == PW_CODE_ACCOUNTING_REQUEST) {
+		RDEBUG("Stopping request due to failed connection to home server");
+		request->master_state = REQUEST_STOP_PROCESSING;
+	}
+#endif
+
+	/*
+	 *	Reset the timer to be now, so that the request is
+	 *	quickly updated.  But spread the requests randomly
+	 *	over the next second, so that we don't overload the
+	 *	server.
+	 */
+	fr_event_now(el, &when);
+	tv_add(&when, fr_rand() % USEC);
+	STATE_MACHINE_TIMER(FR_ACTION_TIMER);
+
+	/*
+	 *	Don't delete it from the list.
+	 */
+	return 0;
+}
+#endif	/* WITH_PROXY */
+#endif	/* WITH_TCP */
+
+static void event_new_fd(rad_listen_t *this)
+{
+	char buffer[1024];
+	listen_socket_t *sock = NULL;
+
+	ASSERT_MASTER;
+
+	if (this->status == RAD_LISTEN_STATUS_KNOWN) return;
+
+	this->print(this, buffer, sizeof(buffer));
+
+	if (this->type != RAD_LISTEN_DETAIL) {
+		sock = this->data;
+		rad_assert(sock != NULL);
+	}
+
+	if (this->status == RAD_LISTEN_STATUS_INIT) {
+		if (just_started) {
+			DEBUG("Listening on %s", buffer);
+
+#ifdef WITH_PROXY
+		} else if (this->type == RAD_LISTEN_PROXY) {
+			home_server_t *home = sock->home;
+
+			if (home && home->limit.max_connections) {
+				INFO(" ... adding new socket %s (%u of %u)", buffer,
+				     home->limit.num_connections, home->limit.max_connections);
+			} else {
+				INFO(" ... adding new socket %s", buffer);
+			}
+#endif
+		} else {
+			INFO(" ... adding new socket %s", buffer);
+		}
+
+		switch (this->type) {
+#ifdef WITH_DETAIL
+		/*
+		 *	Detail files are always known, and aren't
+		 *	put into the socket event loop.
+		 */
+		case RAD_LISTEN_DETAIL:
+			this->status = RAD_LISTEN_STATUS_KNOWN;
+
+#ifndef WITH_DETAIL_THREAD
+			/*
+			 *	Set up the first poll interval.
+			 */
+			event_poll_detail(this);
+			return;
+#else
+			break;	/* add the FD to the list */
+#endif
+#endif	/* WITH_DETAIL */
+
+#ifdef WITH_PROXY
+		/*
+		 *	Add it to the list of sockets we can use.
+		 *	Server sockets (i.e. auth/acct) are never
+		 *	added to the packet list.
+		 */
+		case RAD_LISTEN_PROXY:
+#ifdef WITH_TCP
+			rad_assert(sock != NULL);
+			rad_assert((sock->proto == IPPROTO_UDP) || (sock->home != NULL));
+
+			/*
+			 *	Add timers to outgoing child sockets, if necessary.
+			 */
+			if (sock->proto == IPPROTO_TCP && sock->opened &&
+			    (sock->home->limit.lifetime || sock->home->limit.idle_timeout)) {
+				struct timeval when;
+
+				when.tv_sec = sock->opened + 1;
+				when.tv_usec = 0;
+
+				ASSERT_MASTER;
+				if (!fr_event_insert(el, tcp_socket_timer, this, &when,
+						     &(sock->ev))) {
+					rad_panic("Failed to insert event");
+				}
+			}
+
+			/*
+			 *	Run a callback to do any specific
+			 *	signalling on "connection up".
+			 *
+			 *	For TLS sockets and WITH_COA_TUNNEL,
+			 *	this function should be similar to
+			 *	ping_home_server(), except that it
+			 *	should send a Status-Server packet,
+			 *	with Originating-Realm-Key as a VSA.
+			 */
+//			process_listener_up(this);
+
+#endif	/* WITH_TCP */
+			break;
+#endif	/* WITH_PROXY */
+
+			/*
+			 *	FIXME: put idle timers on command sockets.
+			 */
+
+		default:
+#ifdef WITH_TCP
+			/*
+			 *	Add timers to incoming child sockets, if necessary.
+			 */
+			if (sock->proto == IPPROTO_TCP && sock->opened &&
+			    (sock->limit.lifetime || sock->limit.idle_timeout)) {
+				struct timeval when;
+
+				when.tv_sec = sock->opened + 1;
+				when.tv_usec = 0;
+
+				ASSERT_MASTER;
+				if (!fr_event_insert(el, tcp_socket_timer, this, &when,
+						     &(sock->ev))) {
+					ERROR("Failed adding timer for socket: %s", fr_strerror());
+					fr_exit(1);
+				}
+			}
+
+#ifdef WITH_COA_TUNNEL
+			/*
+			 *	If we're allowed to send CoA requests
+			 *	back down this incoming socket, then
+			 *	add the socket to the proxy listener
+			 *	list.  We need to check for "parent",
+			 *	as the main incoming listener has
+			 *	"send_coa" set, but it just calls
+			 *	accept(), and doesn't actually send
+			 *	any packets.
+			 */
+			if (this->send_coa && this->parent) {
+				PTHREAD_MUTEX_LOCK(&proxy_mutex);
+				if (!fr_packet_list_socket_add(proxy_list, this->fd,
+							       sock->proto,
+#ifdef WITH_RADIUSV11
+							       sock->radiusv11,
+#endif
+							       &sock->other_ipaddr, sock->other_port,
+							       this)) {
+					ERROR("Failed adding coa proxy socket");
+					fr_exit_now(1);
+				}
+				PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+			}
+#endif	/* WITH_COA_TUNNEL */
+
+#endif	/* WITH_TCP */
+			break;
+		} /* switch over listener types */
+
+		/*
+		 *	All sockets: add the FD to the event handler.
+		 */
+	insert_fd:
+		if (fr_event_fd_insert(el, 0, this->fd,
+				       event_socket_handler, this)) {
+			this->status = RAD_LISTEN_STATUS_KNOWN;
+			return;
+		}
+
+		/*
+		 *	Print out which socket failed.
+		 *
+		 *	If we're trying to add the socket, then
+		 *	forcibly remove it immediately, without any
+		 *	additional cleanups.  There cannot, and MUST
+		 *	NOT be any packets associated with the socket.
+		 */
+		this->print(this, buffer, sizeof(buffer));
+		ERROR("Failed adding event handler for socket %s: %s", buffer, fr_strerror());
+		this->status = RAD_LISTEN_STATUS_EOL;
+		goto listener_is_eol;
+	} /* end of INIT */
+
+	if (this->status == RAD_LISTEN_STATUS_PAUSE) {
+		fr_event_fd_delete(el, 0, this->fd);
+		return;
+	}
+
+	if (this->status == RAD_LISTEN_STATUS_RESUME) goto insert_fd;
+
+#ifdef WITH_TCP
+	/*
+	 *	The socket has reached a timeout.  Try to close it.
+	 */
+	if (this->status == RAD_LISTEN_STATUS_FROZEN) {
+		/*
+		 *	Requests are still using the socket.  Wait for
+		 *	them to finish.
+		 */
+		if (this->count > 0) {
+			struct timeval when;
+
+			/*
+			 *	Try again to clean up the socket in 30
+			 *	seconds.
+			 */
+			gettimeofday(&when, NULL);
+			when.tv_sec += 30;
+
+			ASSERT_MASTER;
+			if (!fr_event_insert(el,
+					     (fr_event_callback_t) event_new_fd,
+					     this, &when, &sock->ev)) {
+				rad_panic("Failed to insert event");
+			}
+
+			return;
+		}
+
+		fr_event_fd_delete(el, 0, this->fd);
+		this->status = RAD_LISTEN_STATUS_REMOVE_NOW;
+	}
+
+	/*
+	 *	The socket has had a catastrophic error.  Close it.
+	 */
+	if (this->status == RAD_LISTEN_STATUS_EOL) {
+		/*
+		 *	Remove it from the list of live FD's.
+		 */
+		fr_event_fd_delete(el, 0, this->fd);
+
+	listener_is_eol:
+#ifdef WITH_PROXY
+		/*
+		 *	Tell all requests using this socket that the socket is dead.
+		 */
+		if (this->type == RAD_LISTEN_PROXY
+#ifdef WITH_COA_TUNNEL
+		    || (this->send_coa && this->parent)
+#endif
+			) {
+			PTHREAD_MUTEX_LOCK(&proxy_mutex);
+			if (!fr_packet_list_socket_freeze(proxy_list,
+							  this->fd)) {
+				ERROR("Fatal error freezing socket: %s", fr_strerror());
+				fr_exit(1);
+			}
+
+			if (this->count > 0) {
+				fr_packet_list_walk(proxy_list, this, proxy_eol_cb);
+			}
+			PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+		}
+#endif	/* WITH_PROXY */
+
+		/*
+		 *	Requests are still using the socket.  Wait for
+		 *	them to finish.
+		 */
+		if (this->count > 0) {
+			struct timeval when;
+
+			/*
+			 *	Try again to clean up the socket in 30
+			 *	seconds.
+			 */
+			gettimeofday(&when, NULL);
+			when.tv_sec += 30;
+
+			ASSERT_MASTER;
+			if (!fr_event_insert(el,
+					     (fr_event_callback_t) event_new_fd,
+					     this, &when, &sock->ev)) {
+				rad_panic("Failed to insert event");
+			}
+
+			return;
+		}
+
+		/*
+		 *	No one is using the socket.  We can remove it now.
+		 */
+		this->status = RAD_LISTEN_STATUS_REMOVE_NOW;
+	} /* socket is at EOL */
+#endif	  /* WITH_TCP */
+
+	if (this->dead) goto wait_some_more;
+
+	/*
+	 *	Nuke the socket.
+	 */
+	if (this->status == RAD_LISTEN_STATUS_REMOVE_NOW) {
+		int devnull;
+
+		this->dead = true;
+
+		/*
+		 *      Re-open the socket, pointing it to /dev/null.
+		 *      This means that all writes proceed without
+		 *      blocking, and all reads return "no data".
+		 *
+		 *      This leaves the socket active, so any child
+		 *      threads won't go insane.  But it means that
+		 *      they cannot send or receive any packets.
+		 *
+		 *	This is EXTRA work in the normal case, when
+		 *	sockets are closed without error.  But it lets
+		 *	us have one simple processing method for all
+		 *	sockets.
+		 */
+		devnull = open("/dev/null", O_RDWR);
+		if (devnull < 0) {
+			ERROR("FATAL failure opening /dev/null: %s",
+			       fr_syserror(errno));
+			fr_exit(1);
+		}
+		if (dup2(devnull, this->fd) < 0) {
+			ERROR("FATAL failure closing socket: %s",
+			       fr_syserror(errno));
+			fr_exit(1);
+		}
+		close(devnull);
+
+#ifdef WITH_DETAIL
+		rad_assert(this->type != RAD_LISTEN_DETAIL);
+#endif
+
+#ifdef WITH_TCP
+#ifdef WITH_PROXY
+		/*
+		 *	The socket is dead.  Force all proxied packets
+		 *	to stop using it.  And then remove it from the
+		 *	list of outgoing sockets.
+		 */
+		if (this->type == RAD_LISTEN_PROXY
+#ifdef WITH_COA_TUNNEL
+		    || (this->send_coa && this->parent)
+#endif
+			) {
+			home_server_t *home;
+			sock = this->data;
+
+			home = sock->home;
+			if (!home || !home->limit.max_connections) {
+				INFO(" ... shutting down socket %s", buffer);
+			} else {
+				INFO(" ... shutting down socket %s (%u of %u)", buffer,
+				     home->limit.num_connections, home->limit.max_connections);
+			}
+
+			PTHREAD_MUTEX_LOCK(&proxy_mutex);
+			fr_packet_list_walk(proxy_list, this, eol_proxy_listener);
+
+			if (!fr_packet_list_socket_del(proxy_list, this->fd)) {
+				ERROR("Fatal error removing socket %s: %s",
+				      buffer, fr_strerror());
+				fr_exit(1);
+			}
+
+#ifdef WITH_TLS
+			/*
+			 *	Remove this socket from the list of sockets assocated with this home server.
+			 *
+			 *	This MUST be done with the proxy mutex locked!
+			 */
+			if (home && home->tls) {
+				fr_assert(home->listeners);
+
+				(void) rbtree_deletebydata(home->listeners, this);
+			}
+#endif
+
+			PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+
+#ifdef WITH_COA_TUNNEL
+			/*
+			 *	Clean up the proxied packets AND the
+			 *	normal one.
+			 */
+			if (this->send_coa && this->parent) goto shutdown;
+#endif
+
+		} else
+#endif	/* WITH_PROXY */
+		{
+#ifdef WITH_COA_TUNNEL
+		shutdown:
+#endif
+			INFO(" ... shutting down socket %s", buffer);
+
+			/*
+			 *	EOL all requests using this socket.
+			 */
+			rbtree_walk(pl, RBTREE_DELETE_ORDER, eol_listener, this);
+		}
+
+		/*
+		 *	No child threads, clean it up now.
+		 */
+		if (!spawn_flag) {
+			ASSERT_MASTER;
+
+			if (this->type != RAD_LISTEN_DETAIL && sock && sock->ev) {
+				fr_event_delete(el, &sock->ev);
+			}
+			listen_free(&this);
+			return;
+		}
+
+		/*
+		 *	Wait until all requests using this socket are done.
+		 */
+	wait_some_more:
+		listener_free_cb(this);
+#endif	/* WITH_TCP */
+	}
+
+	return;
+}
+
+/***********************************************************************
+ *
+ *	Signal handlers.
+ *
+ ***********************************************************************/
+
+static void handle_signal_self(int flag)
+{
+	ASSERT_MASTER;
+
+	if ((flag & (RADIUS_SIGNAL_SELF_EXIT | RADIUS_SIGNAL_SELF_TERM)) != 0) {
+		if ((flag & RADIUS_SIGNAL_SELF_EXIT) != 0) {
+			INFO("Signalled to exit");
+			fr_event_loop_exit(el, 1);
+		} else {
+			INFO("Signalled to terminate");
+			fr_event_loop_exit(el, 2);
+		}
+
+		return;
+	} /* else exit/term flags weren't set */
+
+	/*
+	 *	Tell the even loop to stop processing.
+	 */
+	if ((flag & RADIUS_SIGNAL_SELF_HUP) != 0) {
+		time_t when;
+		static time_t last_hup = 0;
+
+		when = time(NULL);
+		if ((int) (when - last_hup) < 5) {
+			INFO("Ignoring HUP (less than 5s since last one)");
+			return;
+		}
+
+		INFO("Received HUP signal");
+
+		last_hup = when;
+
+		exec_trigger(NULL, NULL, "server.signal.hup", true);
+		fr_event_loop_exit(el, 0x80);
+	}
+
+#if defined(WITH_DETAIL) && !defined(WITH_DETAIL_THREAD)
+	if ((flag & RADIUS_SIGNAL_SELF_DETAIL) != 0) {
+		rad_listen_t *this;
+
+		/*
+		 *	FIXME: O(N) loops suck.
+		 */
+		for (this = main_config.listen;
+		     this != NULL;
+		     this = this->next) {
+			if (this->type != RAD_LISTEN_DETAIL) continue;
+
+			/*
+			 *	This one didn't send the signal, skip
+			 *	it.
+			 */
+			if (!this->decode(this, NULL)) continue;
+
+			/*
+			 *	Go service the interrupt.
+			 */
+			event_poll_detail(this);
+		}
+	}
+#endif
+
+#if defined(WITH_PROXY) && defined(HAVE_PTHREAD_H)
+	/*
+	 *	There are new listeners in the list.  Run
+	 *	event_new_fd() on them.
+	 */
+	if ((flag & RADIUS_SIGNAL_SELF_NEW_FD) != 0) {
+		rad_listen_t *this, *next;
+
+		FD_MUTEX_LOCK(&fd_mutex);
+
+		/*
+		 *	FIXME: unlock the mutex before calling
+		 *	event_new_fd()?
+		 */
+		for (this = new_listeners; this != NULL; this = next) {
+			next = this->next;
+			this->next = NULL;
+
+			event_new_fd(this);
+		}
+
+		new_listeners = NULL;
+		FD_MUTEX_UNLOCK(&fd_mutex);
+	}
+#endif
+}
+
+#ifndef HAVE_PTHREAD_H
+void radius_signal_self(int flag)
+{
+	if (flag == RADIUS_SIGNAL_SELF_TERM) {
+		main_config.exiting = true;
+	}
+
+	return handle_signal_self(flag);
+}
+
+#else
+static int self_pipe[2] = { -1, -1 };
+
+/*
+ *	Inform ourselves that we received a signal.
+ */
+void radius_signal_self(int flag)
+{
+	ssize_t rcode;
+	uint8_t buffer[16];
+
+	if (flag == RADIUS_SIGNAL_SELF_TERM) {
+		main_config.exiting = true;
+	}
+
+	/*
+	 *	The read MUST be non-blocking for this to work.
+	 */
+	rcode = read(self_pipe[0], buffer, sizeof(buffer));
+	if (rcode > 0) {
+		ssize_t i;
+
+		for (i = 0; i < rcode; i++) {
+			buffer[0] |= buffer[i];
+		}
+	} else {
+		buffer[0] = 0;
+	}
+
+	buffer[0] |= flag;
+
+	if (write(self_pipe[1], buffer, 1) < 0) fr_exit(0);
+}
+
+
+static void event_signal_handler(UNUSED fr_event_list_t *xel,
+				 UNUSED int fd, UNUSED void *ctx)
+{
+	ssize_t i, rcode;
+	uint8_t buffer[32];
+
+	rcode = read(self_pipe[0], buffer, sizeof(buffer));
+	if (rcode <= 0) return;
+
+	/*
+	 *	Merge pending signals.
+	 */
+	for (i = 0; i < rcode; i++) {
+		buffer[0] |= buffer[i];
+	}
+
+	handle_signal_self(buffer[0]);
+}
+#endif	/* HAVE_PTHREAD_H */
+
+/***********************************************************************
+ *
+ *	Bootstrapping code.
+ *
+ ***********************************************************************/
+
+/*
+ *	Externally-visibly functions.
+ */
+int radius_event_init(TALLOC_CTX *ctx) {
+	el = fr_event_list_create(ctx, event_status);
+	if (!el) return 0;
+
+#ifdef HAVE_SYSTEMD_WATCHDOG
+	if (sd_watchdog_interval.tv_sec || sd_watchdog_interval.tv_usec) {
+		struct timeval now;
+
+		fr_event_now(el, &now);
+
+		sdwd.when = now;
+		sdwd.el = el;
+
+		sd_watchdog_event(&sdwd);
+	}
+#endif
+
+	return 1;
+}
+
+static int packet_entry_cmp(void const *one, void const *two)
+{
+	RADIUS_PACKET const * const *a = one;
+	RADIUS_PACKET const * const *b = two;
+
+	return fr_packet_cmp(*a, *b);
+}
+
+#ifdef WITH_PROXY
+/*
+ *	They haven't defined a proxy listener.  Automatically
+ *	add one for them, with the correct address family.
+ */
+static void create_default_proxy_listener(int af)
+{
+	uint16_t	port = 0;
+	home_server_t	home;
+	listen_socket_t *sock;
+	rad_listen_t	*this;
+
+	memset(&home, 0, sizeof(home));
+
+	/*
+	 *	Open a default UDP port
+	 */
+	home.proto = IPPROTO_UDP;
+	port = 0;
+
+	/*
+	 *	Set the address family.
+	 */
+	home.src_ipaddr.af = af;
+	home.ipaddr.af = af;
+
+	/*
+	 *	Get the correct listener.
+	 */
+	this = proxy_new_listener(proxy_ctx, &home, port);
+	if (!this) {
+		fr_exit_now(1);
+	}
+
+	sock = this->data;
+	if (!fr_packet_list_socket_add(proxy_list, this->fd,
+				       sock->proto,
+#ifdef WITH_RADIUSV11
+				       sock->radiusv11,
+#endif
+				       &sock->other_ipaddr, sock->other_port,
+				       this)) {
+		ERROR("Failed adding proxy socket");
+		fr_exit_now(1);
+	}
+
+	/*
+	 *	Insert the FD into list of FDs to listen on.
+	 */
+	radius_update_listener(this);
+}
+
+/*
+ *	See if we automatically need to open a proxy socket.
+ */
+static void check_proxy(rad_listen_t *head)
+{
+	bool		defined_proxy;
+	bool		has_v4, has_v6;
+	rad_listen_t	*this;
+
+	if (check_config) return;
+	if (!main_config.proxy_requests) {
+		DEBUG3("Cannot proxy packets unless 'proxy_requests = yes'");
+		return;
+	}
+	if (!head) return;
+#ifdef WITH_TCP
+	if (!home_servers_udp) return;
+#endif
+
+	/*
+	 *	We passed "-i" on the command line.  Use that address
+	 *	family for the proxy socket.
+	 */
+	if (main_config.myip.af != AF_UNSPEC) {
+		create_default_proxy_listener(main_config.myip.af);
+		return;
+	}
+
+	defined_proxy = has_v4 = has_v6 = false;
+
+	/*
+	 *	Figure out if we need to open a proxy socket, and if
+	 *	so, which one.
+	 */
+	for (this = head; this != NULL; this = this->next) {
+		listen_socket_t *sock;
+
+		switch (this->type) {
+		case RAD_LISTEN_PROXY:
+			defined_proxy = true;
+			break;
+
+		case RAD_LISTEN_AUTH:
+#ifdef WITH_ACCT
+		case RAD_LISTEN_ACCT:
+#endif
+#ifdef WITH_COA
+		case RAD_LISTEN_COA:
+#endif
+			sock = this->data;
+			if (sock->my_ipaddr.af == AF_INET) has_v4 = true;
+			if (sock->my_ipaddr.af == AF_INET6) has_v6 = true;
+			break;
+			
+		default:
+			break;
+		}
+	}
+
+	/*
+	 *	Assume they know what they're doing.
+	 */
+	if (defined_proxy) return;
+
+	if (has_v4) create_default_proxy_listener(AF_INET);
+
+	if (has_v6) create_default_proxy_listener(AF_INET6);
+}
+#endif
+
+int radius_event_start(CONF_SECTION *cs, bool have_children)
+{
+	rad_listen_t *head = NULL;
+
+	if (fr_start_time != (time_t)-1) return 0;
+
+	time(&fr_start_time);
+
+	if (!check_config) {
+		/*
+		 *  radius_event_init() must be called first
+		 */
+		rad_assert(el);
+
+		pl = rbtree_create(NULL, packet_entry_cmp, NULL, 0);
+		if (!pl) return 0;	/* leak el */
+	}
+
+	request_num_counter = 0;
+
+#ifdef WITH_PROXY
+	if (main_config.proxy_requests && !check_config) {
+		/*
+		 *	Create the tree for managing proxied requests and
+		 *	responses.
+		 */
+		proxy_list = fr_packet_list_create(1);
+		if (!proxy_list) return 0;
+
+#ifdef HAVE_PTHREAD_H
+		if (pthread_mutex_init(&proxy_mutex, NULL) != 0) {
+			ERROR("FATAL: Failed to initialize proxy mutex: %s",
+			       fr_syserror(errno));
+			fr_exit(1);
+		}
+#endif
+
+		/*
+		 *	The "init_delay" is set to "response_window".
+		 *	Reset it to half of "response_window" in order
+		 *	to give the event loop enough time to service
+		 *	the event before hitting "response_window".
+		 */
+		main_config.init_delay.tv_usec += (main_config.init_delay.tv_sec & 0x01) * USEC;
+		main_config.init_delay.tv_usec >>= 1;
+		main_config.init_delay.tv_sec >>= 1;
+
+		proxy_ctx = talloc_init("proxy");
+	}
+#endif
+
+	/*
+	 *	Move all of the thread calls to this file?
+	 *
+	 *	It may be best for the mutexes to be in this file...
+	 */
+	spawn_flag = have_children;
+
+#ifdef HAVE_PTHREAD_H
+	NO_SUCH_CHILD_PID = pthread_self(); /* not a child thread */
+
+	/*
+	 *	Initialize the threads ONLY if we're spawning, AND
+	 *	we're running normally.
+	 */
+	if (have_children && !check_config &&
+	    (thread_pool_init(cs, &spawn_flag) < 0)) {
+		fr_exit(1);
+	}
+#endif
+
+	if (check_config) {
+		DEBUG("%s: #### Skipping IP addresses and Ports ####",
+		       main_config.name);
+		if (listen_init(cs, &head, spawn_flag) < 0) {
+			fflush(NULL);
+			fr_exit(1);
+		}
+		return 1;
+	}
+
+#ifdef HAVE_PTHREAD_H
+	/*
+	 *	Child threads need a pipe to signal us, as do the
+	 *	signal handlers.
+	 */
+	if (pipe(self_pipe) < 0) {
+		ERROR("Error opening internal pipe: %s", fr_syserror(errno));
+		fr_exit(1);
+	}
+	if ((fcntl(self_pipe[0], F_SETFL, O_NONBLOCK) < 0) ||
+	    (fcntl(self_pipe[0], F_SETFD, FD_CLOEXEC) < 0)) {
+		ERROR("Error setting internal flags: %s", fr_syserror(errno));
+		fr_exit(1);
+	}
+	if ((fcntl(self_pipe[1], F_SETFL, O_NONBLOCK) < 0) ||
+	    (fcntl(self_pipe[1], F_SETFD, FD_CLOEXEC) < 0)) {
+		ERROR("Error setting internal flags: %s", fr_syserror(errno));
+		fr_exit(1);
+	}
+	DEBUG4("Created signal pipe.  Read end FD %i, write end FD %i", self_pipe[0], self_pipe[1]);
+
+	if (!fr_event_fd_insert(el, 0, self_pipe[0], event_signal_handler, el)) {
+		ERROR("Failed creating signal pipe handler: %s", fr_strerror());
+		fr_exit(1);
+	}
+#endif
+
+	DEBUG("%s: #### Opening IP addresses and Ports ####", main_config.name);
+
+	/*
+	 *	The server temporarily switches to an unprivileged
+	 *	user very early in the bootstrapping process.
+	 *	However, some sockets MAY require privileged access
+	 *	(bind to device, or to port < 1024, or to raw
+	 *	sockets).  Those sockets need to call suid up/down
+	 *	themselves around the functions that need a privileged
+	 *	uid.
+	 */
+	if (listen_init(cs, &head, spawn_flag) < 0) {
+		fr_exit_now(1);
+	}
+
+	main_config.listen = head;
+
+#ifdef WITH_PROXY
+	check_proxy(head);
+#endif
+
+	/*
+	 *	At this point, no one has any business *ever* going
+	 *	back to root uid.
+	 */
+	rad_suid_down_permanent();
+
+	return 1;
+}
+
+
+#ifdef WITH_PROXY
+static int proxy_delete_cb(UNUSED void *ctx, void *data)
+{
+	REQUEST *request = fr_packet2myptr(REQUEST, proxy, data);
+
+	VERIFY_REQUEST(request);
+
+	request->master_state = REQUEST_STOP_PROCESSING;
+
+#ifdef HAVE_PTHREAD_H
+	if (pthread_equal(request->child_pid, NO_SUCH_CHILD_PID) == 0) return 0;
+#endif
+
+	/*
+	 *	If it's queued we can't delete it from the queue.
+	 *
+	 *	Otherwise, it's OK to delete it.  Even RUNNING, because
+	 *	that will get caught by the check above.
+	 */
+	if (request->child_state == REQUEST_QUEUED) return 0;
+
+	request->in_proxy_hash = false;
+
+	if (!request->in_request_hash) {
+		request_done(request, FR_ACTION_CANCELLED);
+	}
+
+	/*
+	 *	Delete it from the list.
+	 */
+	return 2;
+}
+#endif
+
+
+static int request_delete_cb(UNUSED void *ctx, void *data)
+{
+	REQUEST *request = fr_packet2myptr(REQUEST, packet, data);
+
+	VERIFY_REQUEST(request);
+
+	request->master_state = REQUEST_STOP_PROCESSING;
+
+	/*
+	 *	Not done, or the child thread is still processing it.
+	 */
+	if (request->child_state < REQUEST_RESPONSE_DELAY) return 0; /* continue */
+
+#ifdef HAVE_PTHREAD_H
+	if (pthread_equal(request->child_pid, NO_SUCH_CHILD_PID) == 0) return 0;
+#endif
+
+#ifdef WITH_PROXY
+	rad_assert(request->in_proxy_hash == false);
+#endif
+
+	request->in_request_hash = false;
+	ASSERT_MASTER;
+	if (request->ev) fr_event_delete(el, &request->ev);
+
+	if (main_config.memory_report) {
+		RDEBUG2("Cleaning up request packet ID %u with timestamp +%d",
+			request->packet->id,
+			(unsigned int) (request->timestamp - fr_start_time));
+	}
+
+#ifdef WITH_COA
+	if (request->coa) {
+		rad_assert(!request->coa->in_proxy_hash);
+	}
+#endif
+
+	request_free(request);
+
+	/*
+	 *	Delete it from the list, and continue;
+	 */
+	return 2;
+}
+
+
+void radius_event_free(void)
+{
+	ASSERT_MASTER;
+
+#ifdef WITH_PROXY
+	/*
+	 *	There are requests in the proxy hash that aren't
+	 *	referenced from anywhere else.  Remove them first.
+	 */
+	if (proxy_list) {
+		fr_packet_list_walk(proxy_list, NULL, proxy_delete_cb);
+	}
+#endif
+
+	rbtree_walk(pl, RBTREE_DELETE_ORDER,  request_delete_cb, NULL);
+
+	if (spawn_flag) {
+		/*
+		 *	Now that all requests have been marked "please stop",
+		 *	ensure that all of the threads have exited.
+		 */
+#ifdef HAVE_PTHREAD_H
+		thread_pool_stop();
+#endif
+
+		/*
+		 *	Walk the lists again, ensuring that all
+		 *	requests are done.
+		 */
+		if (main_config.memory_report) {
+			int num;
+
+#ifdef WITH_PROXY
+			if (proxy_list) {
+				fr_packet_list_walk(proxy_list, NULL, proxy_delete_cb);
+				num = fr_packet_list_num_elements(proxy_list);
+				if (num > 0) {
+					ERROR("Proxy list has %d requests still in it.", num);
+				}
+			}
+#endif
+
+			rbtree_walk(pl, RBTREE_DELETE_ORDER, request_delete_cb, NULL);
+			num = rbtree_num_elements(pl);
+			if (num > 0) {
+				ERROR("Request list has %d requests still in it.", num);
+			}
+		}
+	}
+
+	rbtree_free(pl);
+	pl = NULL;
+
+#ifdef WITH_PROXY
+	fr_packet_list_free(proxy_list);
+	proxy_list = NULL;
+
+	if (proxy_ctx) talloc_free(proxy_ctx);
+#endif
+
+	TALLOC_FREE(el);
+
+	if (debug_condition) talloc_free(debug_condition);
+}
+
+int radius_event_process(void)
+{
+	if (!el) return 0;
+
+	return fr_event_loop(el);
+}
diff --git a/src/main/radattr.c b/src/main/radattr.c
new file mode 100644
index 0000000..8accd0d
--- /dev/null
+++ b/src/main/radattr.c
@@ -0,0 +1,1123 @@
+/*
+ * radattr.c	RADIUS Attribute debugging tool.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2010  Alan DeKok <aland@freeradius.org>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/libradius.h>
+
+typedef struct REQUEST REQUEST;
+
+#include <freeradius-devel/parser.h>
+#include <freeradius-devel/xlat.h>
+#include <freeradius-devel/conf.h>
+#include <freeradius-devel/radpaths.h>
+#include <freeradius-devel/dhcp.h>
+
+#include <ctype.h>
+
+#ifdef HAVE_GETOPT_H
+#	include <getopt.h>
+#endif
+
+#include <assert.h>
+
+#include <freeradius-devel/log.h>
+extern log_lvl_t rad_debug_lvl;
+
+#include <sys/wait.h>
+#ifdef HAVE_PTHREAD_H
+pid_t rad_fork(void);
+pid_t rad_waitpid(pid_t pid, int *status);
+
+pid_t rad_fork(void)
+{
+	return fork();
+}
+
+pid_t rad_waitpid(pid_t pid, int *status)
+{
+	return waitpid(pid, status, 0);
+}
+#endif
+
+static TALLOC_CTX *autofree;
+
+static ssize_t xlat_test(UNUSED void *instance, UNUSED REQUEST *request,
+			 UNUSED char const *fmt, UNUSED char *out, UNUSED size_t outlen)
+{
+	return 0;
+}
+
+static RADIUS_PACKET access_request = {
+	.sockfd = -1,
+	.id = 0,
+	.code = PW_CODE_ACCESS_REQUEST,
+	.vector = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f },
+};
+
+static RADIUS_PACKET access_accept = {
+	.sockfd = -1,
+	.id = 0,
+	.code = PW_CODE_ACCESS_ACCEPT,
+	.vector = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f },
+};
+
+static RADIUS_PACKET coa_request = {
+	.sockfd = -1,
+	.id = 0,
+	.code = PW_CODE_COA_REQUEST,
+	.vector = { 0 },
+};
+
+static RADIUS_PACKET *my_original = &access_request;
+static RADIUS_PACKET *my_packet = &access_accept;
+
+static char const *my_secret = "testing123";
+
+/*
+ *	End of hacks for xlat
+ *
+ **********************************************************************/
+
+static int encode_tlv(char *buffer, uint8_t *output, size_t outlen);
+
+static char const hextab[] = "0123456789abcdef";
+
+static int encode_data_string(char *buffer,
+			      uint8_t *output, size_t outlen)
+{
+	int length = 0;
+	char *p;
+
+	p = buffer + 1;
+
+	while (*p && (outlen > 0)) {
+		if (*p == '"') {
+			return length;
+		}
+
+		if (*p != '\\') {
+			*(output++) = *(p++);
+			outlen--;
+			length++;
+			continue;
+		}
+
+		switch (p[1]) {
+		default:
+			*(output++) = p[1];
+			break;
+
+		case 'n':
+			*(output++) = '\n';
+			break;
+
+		case 'r':
+			*(output++) = '\r';
+			break;
+
+		case 't':
+			*(output++) = '\t';
+			break;
+		}
+
+		outlen--;
+		length++;
+	}
+
+	fprintf(stderr, "String is not terminated\n");
+	return 0;
+}
+
+static int encode_data_tlv(char *buffer, char **endptr,
+			   uint8_t *output, size_t outlen)
+{
+	int depth = 0;
+	int length;
+	char *p;
+
+	for (p = buffer; *p != '\0'; p++) {
+		if (*p == '{') depth++;
+		if (*p == '}') {
+			depth--;
+			if (depth == 0) break;
+		}
+	}
+
+	if (*p != '}') {
+		fprintf(stderr, "No trailing '}' in string starting "
+			"with \"%s\"\n",
+			buffer);
+		return 0;
+	}
+
+	*endptr = p + 1;
+	*p = '\0';
+
+	p = buffer + 1;
+	while (isspace((uint8_t) *p)) p++;
+
+	length = encode_tlv(p, output, outlen);
+	if (length == 0) return 0;
+
+	return length;
+}
+
+static int encode_hex(char *p, uint8_t *output, size_t outlen)
+{
+	int length = 0;
+	while (*p) {
+		char *c1, *c2;
+
+		while (isspace((uint8_t) *p)) p++;
+
+		if (!*p) break;
+
+		if(!(c1 = memchr(hextab, tolower((uint8_t) p[0]), 16)) ||
+		   !(c2 = memchr(hextab, tolower((uint8_t)  p[1]), 16))) {
+			fprintf(stderr, "Invalid data starting at "
+				"\"%s\"\n", p);
+			return 0;
+		}
+
+		*output = ((c1 - hextab) << 4) + (c2 - hextab);
+		output++;
+		length++;
+		p += 2;
+
+		outlen--;
+		if (outlen == 0) {
+			fprintf(stderr, "Too much data\n");
+			return 0;
+		}
+	}
+
+	return length;
+}
+
+
+static int encode_data(char *p, uint8_t *output, size_t outlen)
+{
+	int length;
+
+	if (!isspace((uint8_t) *p)) {
+		fprintf(stderr, "Invalid character following attribute "
+			"definition\n");
+		return 0;
+	}
+
+	while (isspace((uint8_t) *p)) p++;
+
+	if (*p == '{') {
+		int sublen;
+		char *q;
+
+		length = 0;
+
+		do {
+			while (isspace((uint8_t) *p)) p++;
+			if (!*p) {
+				if (length == 0) {
+					fprintf(stderr, "No data\n");
+					return 0;
+				}
+
+				break;
+			}
+
+			sublen = encode_data_tlv(p, &q, output, outlen);
+			if (sublen == 0) return 0;
+
+			length += sublen;
+			output += sublen;
+			outlen -= sublen;
+			p = q;
+		} while (*q);
+
+		return length;
+	}
+
+	if (*p == '"') {
+		length = encode_data_string(p, output, outlen);
+		return length;
+	}
+
+	length = encode_hex(p, output, outlen);
+
+	if (length == 0) {
+		fprintf(stderr, "Empty string\n");
+		return 0;
+	}
+
+	return length;
+}
+
+static int decode_attr(char *buffer, char **endptr)
+{
+	long attr;
+
+	attr = strtol(buffer, endptr, 10);
+	if (*endptr == buffer) {
+		fprintf(stderr, "No valid number found in string "
+			"starting with \"%s\"\n", buffer);
+		return 0;
+	}
+
+	if (!**endptr) {
+		fprintf(stderr, "Nothing follows attribute number\n");
+		return 0;
+	}
+
+	if ((attr <= 0) || (attr > 256)) {
+		fprintf(stderr, "Attribute number is out of valid "
+			"range\n");
+		return 0;
+	}
+
+	return (int) attr;
+}
+
+static int decode_vendor(char *buffer, char **endptr)
+{
+	long vendor;
+
+	if (*buffer != '.') {
+		fprintf(stderr, "Invalid separator before vendor id\n");
+		return 0;
+	}
+
+	vendor = strtol(buffer + 1, endptr, 10);
+	if (*endptr == (buffer + 1)) {
+		fprintf(stderr, "No valid vendor number found\n");
+		return 0;
+	}
+
+	if (!**endptr) {
+		fprintf(stderr, "Nothing follows vendor number\n");
+		return 0;
+	}
+
+	if ((vendor <= 0) || (vendor > (1 << 24))) {
+		fprintf(stderr, "Vendor number is out of valid range\n");
+		return 0;
+	}
+
+	if (**endptr != '.') {
+		fprintf(stderr, "Invalid data following vendor number\n");
+		return 0;
+	}
+	(*endptr)++;
+
+	return (int) vendor;
+}
+
+static int encode_tlv(char *buffer, uint8_t *output, size_t outlen)
+{
+	int attr;
+	int length;
+	char *p;
+
+	attr = decode_attr(buffer, &p);
+	if (attr == 0) return 0;
+
+	output[0] = attr;
+	output[1] = 2;
+
+	if (*p == '.') {
+		p++;
+		length = encode_tlv(p, output + 2, outlen - 2);
+
+	} else {
+		length = encode_data(p, output + 2, outlen - 2);
+	}
+
+	if (length == 0) return 0;
+	if (length > (255 - 2)) {
+		fprintf(stderr, "TLV data is too long\n");
+		return 0;
+	}
+
+	output[1] += length;
+
+	return length + 2;
+}
+
+static int encode_vsa(char *buffer, uint8_t *output, size_t outlen)
+{
+	int vendor;
+	int length;
+	char *p;
+
+	vendor = decode_vendor(buffer, &p);
+	if (vendor == 0) return 0;
+
+	output[0] = 0;
+	output[1] = (vendor >> 16) & 0xff;
+	output[2] = (vendor >> 8) & 0xff;
+	output[3] = vendor & 0xff;
+
+	length = encode_tlv(p, output + 4, outlen - 4);
+	if (length == 0) return 0;
+	if (length > (255 - 6)) {
+		fprintf(stderr, "VSA data is too long\n");
+		return 0;
+	}
+
+
+	return length + 4;
+}
+
+static int encode_evs(char *buffer, uint8_t *output, size_t outlen)
+{
+	int vendor;
+	int attr;
+	int length;
+	char *p;
+
+	vendor = decode_vendor(buffer, &p);
+	if (vendor == 0) return 0;
+
+	attr = decode_attr(p, &p);
+	if (attr == 0) return 0;
+
+	output[0] = 0;
+	output[1] = (vendor >> 16) & 0xff;
+	output[2] = (vendor >> 8) & 0xff;
+	output[3] = vendor & 0xff;
+	output[4] = attr;
+
+	length = encode_data(p, output + 5, outlen - 5);
+	if (length == 0) return 0;
+
+	return length + 5;
+}
+
+static int encode_extended(char *buffer,
+			   uint8_t *output, size_t outlen)
+{
+	int attr;
+	int length;
+	char *p;
+
+	attr = decode_attr(buffer, &p);
+	if (attr == 0) return 0;
+
+	output[0] = attr;
+
+	if (attr == 26) {
+		length = encode_evs(p, output + 1, outlen - 1);
+	} else {
+		length = encode_data(p, output + 1, outlen - 1);
+	}
+	if (length == 0) return 0;
+	if (length > (255 - 3)) {
+		fprintf(stderr, "Extended Attr data is too long\n");
+		return 0;
+	}
+
+	return length + 1;
+}
+
+static int encode_long_extended(char *buffer,
+				 uint8_t *output, size_t outlen)
+{
+	int attr;
+	int length, total;
+	char *p;
+
+	attr = decode_attr(buffer, &p);
+	if (attr == 0) return 0;
+
+	/* output[0] is the extended attribute */
+	output[1] = 4;
+	output[2] = attr;
+	output[3] = 0;
+
+	if (attr == 26) {
+		length = encode_evs(p, output + 4, outlen - 4);
+		if (length == 0) return 0;
+
+		output[1] += 5;
+		length -= 5;
+	} else {
+		length = encode_data(p, output + 4, outlen - 4);
+	}
+	if (length == 0) return 0;
+
+	total = 0;
+	while (1) {
+		int sublen = 255 - output[1];
+
+		if (length <= sublen) {
+			output[1] += length;
+			total += output[1];
+			break;
+		}
+
+		length -= sublen;
+
+		memmove(output + 255 + 4, output + 255, length);
+		memcpy(output + 255, output, 4);
+
+		output[1] = 255;
+		output[3] |= 0x80;
+
+		output += 255;
+		output[1] = 4;
+		total += 255;
+	}
+
+	return total;
+}
+
+static int encode_rfc(char *buffer, uint8_t *output, size_t outlen)
+{
+	int attr;
+	int length, sublen;
+	char *p;
+
+	attr = decode_attr(buffer, &p);
+	if (attr == 0) return 0;
+
+	length = 2;
+	output[0] = attr;
+	output[1] = 2;
+
+	if (attr == 26) {
+		sublen = encode_vsa(p, output + 2, outlen - 2);
+
+	} else if ((attr < 241) || (attr > 246)) {
+		sublen = encode_data(p, output + 2, outlen - 2);
+
+	} else {
+		if (*p != '.') {
+			fprintf(stderr, "Invalid data following "
+				"attribute number\n");
+			return 0;
+		}
+
+		if (attr < 245) {
+			sublen = encode_extended(p + 1,
+						 output + 2, outlen - 2);
+		} else {
+
+			/*
+			 *	Not like the others!
+			 */
+			return encode_long_extended(p + 1, output, outlen);
+		}
+	}
+	if (sublen == 0) return 0;
+	if (sublen > (255 -2)) {
+		fprintf(stderr, "RFC Data is too long\n");
+		return 0;
+	}
+
+	output[1] += sublen;
+	return length + sublen;
+}
+
+static void parse_condition(char const *input, char *output, size_t outlen)
+{
+	ssize_t slen;
+	char const *error = NULL;
+	fr_cond_t *cond;
+
+	slen = fr_condition_tokenize(NULL, NULL, input, &cond, &error, FR_COND_ONE_PASS);
+	if (slen <= 0) {
+		snprintf(output, outlen, "ERROR offset %d %s", (int) -slen, error);
+		return;
+	}
+
+	input += slen;
+	if (*input != '\0') {
+		talloc_free(cond);
+		snprintf(output, outlen, "ERROR offset %d 'Too much text'", (int) slen);
+		return;
+	}
+
+	fr_cond_sprint(output, outlen, cond);
+
+	talloc_free(cond);
+}
+
+static void parse_xlat(char const *input, char *output, size_t outlen)
+{
+	ssize_t slen;
+	char const *error = NULL;
+	char *fmt = talloc_typed_strdup(autofree, input);
+	xlat_exp_t *head;
+
+	slen = xlat_tokenize(autofree, fmt, &head, &error);
+	if (slen <= 0) {
+		snprintf(output, outlen, "ERROR offset %d '%s'", (int) -slen, error);
+		return;
+	}
+
+	if (input[slen] != '\0') {
+		snprintf(output, outlen, "ERROR offset %d 'Too much text'", (int) slen);
+		talloc_free(fmt);
+		return;
+	}
+
+	xlat_sprint(output, outlen, head);
+	talloc_free(fmt);
+}
+
+static void process_file(const char *root_dir, char const *filename)
+{
+	int lineno;
+	size_t i, outlen;
+	ssize_t len, data_len;
+	FILE *fp;
+	char input[8192], buffer[8192];
+	char output[8192];
+	char directory[8192];
+	uint8_t *attr, data[2048];
+
+	if (strcmp(filename, "-") == 0) {
+		fp = stdin;
+		directory[0] = '\0';
+
+	} else {
+		if (root_dir && *root_dir) {
+			snprintf(directory, sizeof(directory), "%s/%s", root_dir, filename);
+		} else {
+			strlcpy(directory, filename, sizeof(directory));
+		}
+
+		fp = fopen(directory, "r");
+		if (!fp) {
+			fprintf(stderr, "Error opening %s: %s\n",
+				directory, fr_syserror(errno));
+			exit(1);
+		}
+
+		filename = directory;
+	}
+
+	lineno = 0;
+	*output = '\0';
+	data_len = 0;
+
+	while (fgets(buffer, sizeof(buffer), fp) != NULL) {
+		char *p = strchr(buffer, '\n');
+		VALUE_PAIR *vp, *head;
+		VALUE_PAIR **tail = &head;
+
+		lineno++;
+		head = NULL;
+
+		if (!p) {
+			if (!feof(fp)) {
+				fprintf(stderr, "Line %d too long in %s\n",
+					lineno, directory);
+				exit(1);
+			}
+		} else {
+			*p = '\0';
+		}
+
+		/*
+		 *	Comments, with hacks for User-Name[#]
+		 */
+		p = strchr(buffer, '#');
+		if (p && ((p == buffer) ||
+			  ((p > buffer) && (p[-1] != '[')))) *p = '\0';
+
+		p = buffer;
+		while (isspace((uint8_t) *p)) p++;
+		if (!*p) continue;
+
+		DEBUG2("%s[%d]: %s\n", filename, lineno, buffer);
+
+		strlcpy(input, p, sizeof(input));
+
+		if (strncmp(p, "raw ", 4) == 0) {
+			outlen = encode_rfc(p + 4, data, sizeof(data));
+			if (outlen == 0) {
+				fprintf(stderr, "Parse error in line %d of %s\n",
+					lineno, directory);
+				exit(1);
+			}
+
+		print_hex:
+			if (outlen == 0) {
+				output[0] = 0;
+				continue;
+			}
+
+			if (outlen > sizeof(data)) outlen = sizeof(data);
+
+			if (outlen >= (sizeof(output) / 2)) {
+				outlen = (sizeof(output) / 2) - 1;
+			}
+
+			data_len = outlen;
+			for (i = 0; i < outlen; i++) {
+				if (sizeof(output) < (3*i)) break;
+
+				snprintf(output + 3*i, sizeof(output) - (3*i) - 1,
+					 "%02x ", data[i]);
+			}
+			outlen = strlen(output);
+			output[outlen - 1] = '\0';
+			continue;
+		}
+
+		if (strncmp(p, "data ", 5) == 0) {
+			if (strcmp(p + 5, output) != 0) {
+				fprintf(stderr, "Mismatch at line %d of %s\n\tgot      : %s\n\texpected : %s\n",
+					lineno, directory, output, p + 5);
+				exit(1);
+			}
+			continue;
+		}
+
+		if (strncmp(p, "packet ", 7) == 0) {
+			p += 7;
+			if (strncmp(p, "access_accept", 13) == 0) {
+				my_packet = &access_accept;
+			} else if (strncmp(p, "coa_request", 11) == 0) {
+				my_packet = &coa_request;
+			} else {
+				fprintf(stderr, "Unsupported packet type at line %d of %s: %s\n",
+					lineno, directory, p);
+				exit(1);
+			}
+			continue;
+		}
+		if (strncmp(p, "original ", 9) == 0) {
+			p += 9;
+			if (strncmp(p, "null", 4) == 0) {
+				my_original = NULL;
+			} else if (strncmp(p, "access_request", 14) == 0) {
+				my_original = &access_request;
+			} else {
+				fprintf(stderr, "Unsupported original type at line %d of %s: %s\n",
+					lineno, directory, p);
+				exit(1);
+			}
+			continue;
+		}
+
+		if (strncmp(p, "encode ", 7) == 0) {
+			if (strcmp(p + 7, "-") == 0) {
+				p = output;
+			} else {
+				p += 7;
+			}
+
+			if (fr_pair_list_afrom_str(autofree, p, &head) != T_EOL) {
+				strlcpy(output, fr_strerror(), sizeof(output));
+				continue;
+			}
+
+			attr = data;
+			vp = head;
+			while (vp) {
+				VALUE_PAIR **pvp = &vp;
+				VALUE_PAIR const **qvp;
+
+				memcpy(&qvp, &pvp, sizeof(pvp));
+
+				len = rad_vp2attr(my_packet, my_original, my_secret, qvp,
+						  attr, data + sizeof(data) - attr);
+				if (len < 0) {
+					fprintf(stderr, "Failed encoding %s: %s\n",
+						vp->da->name, fr_strerror());
+					fr_pair_list_free(&head);
+					exit(1);
+				}
+
+				attr += len;
+				if (len == 0) break;
+			}
+
+			fr_pair_list_free(&head);
+			outlen = attr - data;
+			goto print_hex;
+		}
+
+		if (strncmp(p, "decode ", 7) == 0) {
+			ssize_t my_len;
+
+			if (strcmp(p + 7, "-") == 0) {
+				attr = data;
+				len = data_len;
+			} else {
+				attr = data;
+				len = encode_hex(p + 7, data, sizeof(data));
+				if (len == 0) {
+					fprintf(stderr, "Failed decoding hex string at line %d of %s\n", lineno, directory);
+					exit(1);
+				}
+			}
+
+			my_len = 0;
+			while (len > 0) {
+				vp = NULL;
+				my_len = rad_attr2vp(autofree, my_packet, my_original, my_secret, attr, len, &vp);
+				if (my_len < 0) {
+					fr_pair_list_free(&head);
+					break;
+				}
+
+				if (my_len > len) {
+					fprintf(stderr, "Internal sanity check failed at %d\n", __LINE__);
+					exit(1);
+				}
+
+				*tail = vp;
+				while (vp) {
+					tail = &(vp->next);
+					vp = vp->next;
+				}
+
+				attr += my_len;
+				len -= my_len;
+			}
+
+			/*
+			 *	Output may be an error, and we ignore
+			 *	it if so.
+			 */
+			if (head) {
+				vp_cursor_t cursor;
+				p = output;
+				for (vp = fr_cursor_init(&cursor, &head);
+				     vp;
+				     vp = fr_cursor_next(&cursor)) {
+					vp_prints(p, sizeof(output) - (p - output), vp);
+					p += strlen(p);
+
+					if (vp->next) {
+						strcpy(p, ", ");
+						p += 2;
+					}
+				}
+
+				fr_pair_list_free(&head);
+			} else if (my_len < 0) {
+				strlcpy(output, fr_strerror(), sizeof(output));
+
+			} else { /* zero-length attribute */
+				*output = '\0';
+			}
+
+			continue;
+		}
+
+#ifdef WITH_DHCP
+		/*
+		 *	And some DHCP tests
+		 */
+		if (strncmp(p, "encode-dhcp ", 12) == 0) {
+			vp_cursor_t cursor;
+
+			if (strcmp(p + 12, "-") == 0) {
+				p = output;
+			} else {
+				p += 12;
+			}
+
+			if (fr_pair_list_afrom_str(NULL, p, &head) != T_EOL) {
+				strlcpy(output, fr_strerror(), sizeof(output));
+				continue;
+			}
+
+			fr_cursor_init(&cursor, &head);
+
+
+			attr = data;
+			vp = head;
+
+			while ((vp = fr_cursor_current(&cursor))) {
+				len = fr_dhcp_encode_option(NULL, attr, data + sizeof(data) - attr, &cursor);
+				if (len < 0) {
+					fprintf(stderr, "Failed encoding %s: %s\n",
+						vp->da->name, fr_strerror());
+					exit(1);
+				}
+				attr += len;
+			};
+
+			fr_pair_list_free(&head);
+			outlen = attr - data;
+			goto print_hex;
+		}
+
+		if (strncmp(p, "decode-dhcp ", 12) == 0) {
+			ssize_t my_len;
+
+			if (strcmp(p + 12, "-") == 0) {
+				attr = data;
+				len = data_len;
+			} else {
+				attr = data;
+				len = encode_hex(p + 12, data, sizeof(data));
+				if (len == 0) {
+					fprintf(stderr, "Failed decoding hex string at line %d of %s\n", lineno, directory);
+					exit(1);
+				}
+			}
+
+			my_len = fr_dhcp_decode_options(NULL, &head, attr, len);
+
+			/*
+			 *	Output may be an error, and we ignore
+			 *	it if so.
+			 */
+			if (head) {
+				vp_cursor_t cursor;
+				p = output;
+				for (vp = fr_cursor_init(&cursor, &head);
+				     vp;
+				     vp = fr_cursor_next(&cursor)) {
+					vp_prints(p, sizeof(output) - (p - output), vp);
+					p += strlen(p);
+
+					if (vp->next) {strcpy(p, ", ");
+						p += 2;
+					}
+				}
+
+				fr_pair_list_free(&head);
+			} else if (my_len < 0) {
+				strlcpy(output, fr_strerror(), sizeof(output));
+
+			} else { /* zero-length attribute */
+				*output = '\0';
+			}
+			continue;
+		}
+#endif
+
+		if (strncmp(p, "attribute ", 10) == 0) {
+			p += 10;
+
+			if (fr_pair_list_afrom_str(NULL, p, &head) != T_EOL) {
+				strlcpy(output, fr_strerror(), sizeof(output));
+				continue;
+			}
+
+			vp_prints(output, sizeof(output), head);
+
+			fr_pair_list_free(&head);
+			continue;
+		}
+
+		if (strncmp(p, "$INCLUDE ", 9) == 0) {
+			char *q;
+
+			p += 9;
+			while (isspace((uint8_t) *p)) p++;
+
+			q = strrchr(directory, '/');
+			if (q) {
+				*q = '\0';
+				process_file(directory, p);
+				*q = '/';
+			} else {
+				process_file(NULL, p);
+			}
+			continue;
+		}
+
+		if (strncmp(p, "condition ", 10) == 0) {
+			p += 10;
+			parse_condition(p, output, sizeof(output));
+			continue;
+		}
+
+		if (strncmp(p, "xlat ", 5) == 0) {
+			p += 5;
+			parse_xlat(p, output, sizeof(output));
+			continue;
+		}
+
+		fprintf(stderr, "Unknown input at line %d of %s\n",
+			lineno, directory);
+		exit(1);
+	}
+
+	if (fp != stdin) fclose(fp);
+}
+
+/** Dump all of the dictionary entries as
+ *
+ *	ALIAS name OID
+ *
+ *  To create dictionaries which allow files to be used with v4.
+ *
+ * rm -rf alias;mkdir alias;./build/make/jlibtool --mode=execute ./build/bin/radattr  -D ./share/ -A  | sort -n -k6 -k7 -k8 -k9 -k10 -k11 | gawk '{printf "%s\t%-40s\t%s\n", $1, $2, $3 >> "alias/alias." tolower($5) }'
+ *
+ *  And then post-process each file to remove the comments.
+ *
+ *  Note that we have to use GNU Awk, as OSX awk doesn't like redirection to a file which includes a variable.
+ */
+static int dump_aliases(void *ctx, void *data)
+{
+	DICT_ATTR *da = data;
+	FILE *fp = ctx;
+	int nest, attr, dv_type;
+	DICT_VENDOR *dv;
+	char buffer[1024];
+
+	if (!da->vendor || (da->vendor > FR_MAX_VENDOR)) return 0;
+
+	dv = dict_vendorbyvalue(da->vendor);
+	dv_type = dv->type;
+
+	(void) dict_print_oid(buffer, sizeof(buffer), da);
+	fprintf(fp, "ALIAS\t%s\t%s # %s %u", da->name, buffer, dv->name, da->vendor);
+
+	attr = da->attr;
+	switch (dv_type) {
+	default:
+	case 1:
+		fprintf(fp, " %u", attr & 0xff);
+
+		/*
+		 *	Only these ones are bit-packed.
+		 */
+		for (nest = 1; nest <= fr_attr_max_tlv; nest++) {
+			if (((attr >> fr_attr_shift[nest]) & fr_attr_mask[nest]) == 0) break;
+
+			fprintf(fp, " %u",
+				(attr >> fr_attr_shift[nest]) & fr_attr_mask[nest]);
+		}
+		break;
+
+	case 2:
+		fprintf(fp, " %u", attr & 0xffff);
+		break;
+
+	case 4:
+		fprintf(fp, " %u", attr);
+		break;
+	}
+
+	printf("\n");
+
+	return 0;
+}
+
+static void NEVER_RETURNS usage(void)
+{
+	fprintf(stderr, "usage: radattr [OPTS] filename\n");
+	fprintf(stderr, "  -d <raddb>             Set user dictionary directory (defaults to " RADDBDIR ").\n");
+	fprintf(stderr, "  -D <dictdir>           Set main dictionary directory (defaults to " DICTDIR ").\n");
+	fprintf(stderr, "  -x                     Debugging mode.\n");
+	fprintf(stderr, "  -M                     Show talloc memory report.\n");
+
+	exit(1);
+}
+
+int main(int argc, char *argv[])
+{
+	int c;
+	bool report = false;
+	bool dump_alias = false;
+	char const *radius_dir = RADDBDIR;
+	char const *dict_dir = DICTDIR;
+	int *inst = &c;
+
+DIAG_OFF(deprecated-declarations)
+	autofree = talloc_autofree_context();
+DIAG_ON(deprecated-declarations)
+
+	cf_new_escape = true;	/* fix the tests */
+
+#ifndef NDEBUG
+	if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) {
+		fr_perror("radattr");
+		exit(EXIT_FAILURE);
+	}
+#endif
+
+	while ((c = getopt(argc, argv, "Ad:D:xMh")) != EOF) switch (c) {
+		case 'A':
+			dump_alias = true;
+			break;
+		case 'd':
+			radius_dir = optarg;
+			break;
+		case 'D':
+			dict_dir = optarg;
+			break;
+		case 'x':
+			fr_debug_lvl++;
+			rad_debug_lvl = fr_debug_lvl;
+			break;
+		case 'M':
+			report = true;
+			break;
+		case 'h':
+		default:
+			usage();
+	}
+	argc -= (optind - 1);
+	argv += (optind - 1);
+
+	/*
+	 *	Mismatch between the binary and the libraries it depends on
+	 */
+	if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) {
+		fr_perror("radattr");
+		return 1;
+	}
+
+	if (dict_init(dict_dir, RADIUS_DICTIONARY) < 0) {
+		fr_perror("radattr");
+		return 1;
+	}
+
+	if (dict_read(radius_dir, RADIUS_DICTIONARY) == -1) {
+		fr_perror("radattr");
+		return 1;
+	}
+
+	if (xlat_register("test", xlat_test, NULL, inst) < 0) {
+		fprintf(stderr, "Failed registering xlat");
+		return 1;
+	}
+
+	if (dump_alias) {
+		(void) dict_walk(dump_aliases, stdout);
+		return 0;
+	}
+
+	if (argc < 2) {
+		process_file(NULL, "-");
+
+	} else {
+		process_file(NULL, argv[1]);
+	}
+
+	if (report) {
+		dict_free();
+		fr_log_talloc_report(NULL);
+	}
+
+	return 0;
+}
diff --git a/src/main/radattr.mk b/src/main/radattr.mk
new file mode 100644
index 0000000..1a184bd
--- /dev/null
+++ b/src/main/radattr.mk
@@ -0,0 +1,10 @@
+TARGET		:= radattr
+SOURCES		:= radattr.c
+
+TGT_PREREQS	:= libfreeradius-server.a libfreeradius-radius.a
+
+ifneq "$(WITH_DHCP)" "no"
+TGT_PREREQS	+= libfreeradius-dhcp.a
+endif
+
+TGT_LDLIBS	:= $(LIBS)
diff --git a/src/main/radclient.c b/src/main/radclient.c
new file mode 100644
index 0000000..49da461
--- /dev/null
+++ b/src/main/radclient.c
@@ -0,0 +1,1712 @@
+/*
+ * radclient.c	General radius packet debug tool.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2000,2006,2014  The FreeRADIUS server project
+ * Copyright 2000  Miquel van Smoorenburg <miquels@cistron.nl>
+ * Copyright 2000  Alan DeKok <aland@ox.org>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radclient.h>
+#include <freeradius-devel/radpaths.h>
+#include <freeradius-devel/udpfromto.h>
+#include <freeradius-devel/conf.h>
+#ifdef HAVE_OPENSSL_SSL_H
+#include <openssl/ssl.h>
+#include <freeradius-devel/openssl3.h>
+#endif
+#include <ctype.h>
+
+#ifdef HAVE_GETOPT_H
+#  include <getopt.h>
+#endif
+
+#include <assert.h>
+
+USES_APPLE_DEPRECATED_API
+
+typedef struct REQUEST REQUEST;	/* to shut up warnings about mschap.h */
+
+#include "smbdes.h"
+#include "mschap.h"
+
+static int retries = 3;
+static float timeout = 5;
+static char const *secret = NULL;
+static bool do_output = true;
+
+static rc_stats_t stats;
+
+static uint16_t server_port = 0;
+static int packet_code = PW_CODE_UNDEFINED;
+static fr_ipaddr_t server_ipaddr;
+static int resend_count = 1;
+static bool done = true;
+static bool print_filename = false;
+
+static fr_ipaddr_t client_ipaddr;
+static uint16_t client_port = 0;
+
+static int sockfd;
+
+#ifdef WITH_TCP
+static char const *proto = NULL;
+#endif
+static int ipproto = IPPROTO_UDP;
+
+static rbtree_t *filename_tree = NULL;
+static fr_packet_list_t *pl = NULL;
+
+static int sleep_time = -1;
+
+static rc_request_t *request_head = NULL;
+static rc_request_t *rc_request_tail = NULL;
+
+static char const *radclient_version = "radclient version " RADIUSD_VERSION_STRING
+#ifdef RADIUSD_VERSION_COMMIT
+" (git #" STRINGIFY(RADIUSD_VERSION_COMMIT) ")"
+#endif
+#ifndef ENABLE_REPRODUCIBLE_BUILDS
+", built on " __DATE__ " at " __TIME__
+#endif
+;
+
+static void NEVER_RETURNS usage(void)
+{
+	fprintf(stderr, "Usage: radclient [options] server[:port] <command> [<secret>]\n");
+
+	fprintf(stderr, "  <command>              One of auth, acct, status, coa, disconnect or auto.\n");
+	fprintf(stderr, "  -4                     Use IPv4 address of server\n");
+	fprintf(stderr, "  -6                     Use IPv6 address of server.\n");
+	fprintf(stderr, "  -c <count>             Send each packet 'count' times.\n");
+	fprintf(stderr, "  -d <raddb>             Set user dictionary directory (defaults to " RADDBDIR ").\n");
+	fprintf(stderr, "  -D <dictdir>           Set main dictionary directory (defaults to " DICTDIR ").\n");
+	fprintf(stderr, "  -f <file>[:<file>]     Read packets from file, not stdin.\n");
+	fprintf(stderr, "                         If a second file is provided, it will be used to verify responses\n");
+	fprintf(stderr, "  -F                     Print the file name, packet number and reply code.\n");
+	fprintf(stderr, "  -h                     Print usage help information.\n");
+	fprintf(stderr, "  -n <num>               Send N requests/s\n");
+	fprintf(stderr, "  -p <num>               Send 'num' packets from a file in parallel.\n");
+	fprintf(stderr, "  -q                     Do not print anything out.\n");
+	fprintf(stderr, "  -r <retries>           If timeout, retry sending the packet 'retries' times.\n");
+	fprintf(stderr, "  -s                     Print out summary information of auth results.\n");
+	fprintf(stderr, "  -S <file>              read secret from file, not command line.\n");
+	fprintf(stderr, "  -t <timeout>           Wait 'timeout' seconds before retrying (may be a floating point number).\n");
+	fprintf(stderr, "  -v                     Show program version information.\n");
+	fprintf(stderr, "  -x                     Debugging mode.\n");
+
+#ifdef WITH_TCP
+	fprintf(stderr, "  -P <proto>             Use proto (tcp or udp) for transport.\n");
+#endif
+
+	exit(1);
+}
+
+static const FR_NAME_NUMBER request_types[] = {
+	{ "auth",	PW_CODE_ACCESS_REQUEST },
+	{ "challenge",	PW_CODE_ACCESS_CHALLENGE },
+	{ "acct",	PW_CODE_ACCOUNTING_REQUEST },
+	{ "status",	PW_CODE_STATUS_SERVER },
+	{ "disconnect",	PW_CODE_DISCONNECT_REQUEST },
+	{ "coa",	PW_CODE_COA_REQUEST },
+	{ "auto",	PW_CODE_UNDEFINED },
+
+	{ NULL, 0}
+};
+
+/*
+ *	Free a radclient struct, which may (or may not)
+ *	already be in the list.
+ */
+static int _rc_request_free(rc_request_t *request)
+{
+	rc_request_t *prev, *next;
+
+	prev = request->prev;
+	next = request->next;
+
+	if (prev) {
+		assert(request_head != request);
+		prev->next = next;
+	} else if (request_head) {
+		assert(request_head == request);
+		request_head = next;
+	}
+
+	if (next) {
+		assert(rc_request_tail != request);
+		next->prev = prev;
+	} else if (rc_request_tail) {
+		assert(rc_request_tail == request);
+		rc_request_tail = prev;
+	}
+
+	return 0;
+}
+
+#if defined(OPENSSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L
+#  include <openssl/provider.h>
+
+static OSSL_PROVIDER *openssl_default_provider = NULL;
+static OSSL_PROVIDER *openssl_legacy_provider = NULL;
+
+static int openssl3_init(void)
+{
+	/*
+	 *	Load the default provider for most algorithms
+	 */
+	openssl_default_provider = OSSL_PROVIDER_load(NULL, "default");
+	if (!openssl_default_provider) {
+		ERROR("(TLS) Failed loading default provider");
+		return -1;
+	}
+
+	/*
+	 *	Needed for MD4
+	 *
+	 *	https://www.openssl.org/docs/man3.0/man7/migration_guide.html#Legacy-Algorithms
+	 */
+	openssl_legacy_provider = OSSL_PROVIDER_load(NULL, "legacy");
+	if (!openssl_legacy_provider) {
+		ERROR("(TLS) Failed loading legacy provider");
+		return -1;
+	}
+
+	return 0;
+}
+
+static void openssl3_free(void)
+{
+	if (openssl_default_provider && !OSSL_PROVIDER_unload(openssl_default_provider)) {
+		ERROR("Failed unloading default provider");
+	}
+	openssl_default_provider = NULL;
+
+	if (openssl_legacy_provider && !OSSL_PROVIDER_unload(openssl_legacy_provider)) {
+		ERROR("Failed unloading legacy provider");
+	}
+	openssl_legacy_provider = NULL;
+}
+#else
+#define openssl3_init()
+#define openssl3_free()
+#endif
+
+
+
+static int mschapv1_encode(RADIUS_PACKET *packet, VALUE_PAIR **request,
+			   char const *password)
+{
+	int rcode;
+	unsigned int i;
+	uint8_t *p;
+	VALUE_PAIR *challenge, *reply;
+	uint8_t nthash[16];
+
+	fr_pair_delete_by_num(&packet->vps, PW_MSCHAP_CHALLENGE, VENDORPEC_MICROSOFT, TAG_ANY);
+	fr_pair_delete_by_num(&packet->vps, PW_MSCHAP_RESPONSE, VENDORPEC_MICROSOFT, TAG_ANY);
+
+	challenge = fr_pair_afrom_num(packet, PW_MSCHAP_CHALLENGE, VENDORPEC_MICROSOFT);
+	if (!challenge) {
+		return 0;
+	}
+
+	fr_pair_add(request, challenge);
+	challenge->vp_length = 8;
+	challenge->vp_octets = p = talloc_array(challenge, uint8_t, challenge->vp_length);
+	for (i = 0; i < challenge->vp_length; i++) {
+		p[i] = fr_rand();
+	}
+
+	reply = fr_pair_afrom_num(packet, PW_MSCHAP_RESPONSE, VENDORPEC_MICROSOFT);
+	if (!reply) {
+		return 0;
+	}
+
+	fr_pair_add(request, reply);
+	reply->vp_length = 50;
+	reply->vp_octets = p = talloc_array(reply, uint8_t, reply->vp_length);
+	memset(p, 0, reply->vp_length);
+
+	p[1] = 0x01; /* NT hash */
+
+	rcode = mschap_ntpwdhash(nthash, password);
+	if (rcode < 0) return 0;
+
+	smbdes_mschap(nthash, challenge->vp_octets, p + 26);
+	return 1;
+}
+
+
+static int getport(char const *name)
+{
+	struct servent *svp;
+
+	svp = getservbyname(name, "udp");
+	if (!svp) return 0;
+
+	return ntohs(svp->s_port);
+}
+
+/*
+ *	Set a port from the request type if we don't already have one
+ */
+static void radclient_get_port(PW_CODE type, uint16_t *port)
+{
+	switch (type) {
+	default:
+	case PW_CODE_ACCESS_REQUEST:
+	case PW_CODE_ACCESS_CHALLENGE:
+	case PW_CODE_STATUS_SERVER:
+		if (*port == 0) *port = getport("radius");
+		if (*port == 0) *port = PW_AUTH_UDP_PORT;
+		return;
+
+	case PW_CODE_ACCOUNTING_REQUEST:
+		if (*port == 0) *port = getport("radacct");
+		if (*port == 0) *port = PW_ACCT_UDP_PORT;
+		return;
+
+	case PW_CODE_DISCONNECT_REQUEST:
+		if (*port == 0) *port = PW_POD_UDP_PORT;
+		return;
+
+	case PW_CODE_COA_REQUEST:
+		if (*port == 0) *port = PW_COA_UDP_PORT;
+		return;
+
+	case PW_CODE_UNDEFINED:
+		if (*port == 0) *port = 0;
+		return;
+	}
+}
+
+/*
+ *	Resolve a port to a request type
+ */
+static PW_CODE radclient_get_code(uint16_t port)
+{
+	/*
+	 *	getport returns 0 if the service doesn't exist
+	 *	so we need to return early, to avoid incorrect
+	 *	codes.
+	 */
+	if (port == 0) return PW_CODE_UNDEFINED;
+
+	if ((port == getport("radius")) || (port == PW_AUTH_UDP_PORT) || (port == PW_AUTH_UDP_PORT_ALT)) {
+		return PW_CODE_ACCESS_REQUEST;
+	}
+	if ((port == getport("radacct")) || (port == PW_ACCT_UDP_PORT) || (port == PW_ACCT_UDP_PORT_ALT)) {
+		return PW_CODE_ACCOUNTING_REQUEST;
+	}
+	if (port == PW_COA_UDP_PORT) return PW_CODE_COA_REQUEST;
+	if (port == PW_POD_UDP_PORT) return PW_CODE_DISCONNECT_REQUEST;
+
+	return PW_CODE_UNDEFINED;
+}
+
+
+static bool already_hex(VALUE_PAIR *vp)
+{
+	size_t i;
+
+	if (!vp || (vp->da->type != PW_TYPE_OCTETS)) return true;
+
+	/*
+	 *	If it's 17 octets, it *might* be already encoded.
+	 *	Or, it might just be a 17-character password (maybe UTF-8)
+	 *	Check it for non-printable characters.  The odds of ALL
+	 *	of the characters being 32..255 is (1-7/8)^17, or (1/8)^17,
+	 *	or 1/(2^51), which is pretty much zero.
+	 */
+	for (i = 0; i < vp->vp_length; i++) {
+		if (vp->vp_octets[i] < 32) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+
+/*
+ *	Initialize a radclient data structure and add it to
+ *	the global linked list.
+ */
+static int radclient_init(TALLOC_CTX *ctx, rc_file_pair_t *files)
+{
+	FILE *packets, *filters = NULL;
+
+	vp_cursor_t cursor;
+	VALUE_PAIR *vp;
+	rc_request_t *request;
+	bool packets_done = false;
+	uint64_t num = 0;
+
+	assert(files->packets != NULL);
+
+	/*
+	 *	Determine where to read the VP's from.
+	 */
+	if (strcmp(files->packets, "-") != 0) {
+		packets = fopen(files->packets, "r");
+		if (!packets) {
+			ERROR("Error opening %s: %s", files->packets, strerror(errno));
+			return 0;
+		}
+
+		/*
+		 *	Read in the pairs representing the expected response.
+		 */
+		if (files->filters) {
+			filters = fopen(files->filters, "r");
+			if (!filters) {
+				ERROR("Error opening %s: %s", files->filters, strerror(errno));
+				fclose(packets);
+				return 0;
+			}
+		}
+	} else {
+		packets = stdin;
+	}
+
+
+	/*
+	 *	Loop until the file is done.
+	 */
+	do {
+		/*
+		 *	Allocate it.
+		 */
+		request = talloc_zero(ctx, rc_request_t);
+		if (!request) {
+			ERROR("Out of memory");
+			goto error;
+		}
+
+		request->packet = rad_alloc(request, true);
+		if (!request->packet) {
+			ERROR("Out of memory");
+			goto error;
+		}
+
+		request->packet->src_ipaddr = client_ipaddr;
+		request->packet->src_port = client_port;
+		request->packet->dst_ipaddr = server_ipaddr;
+		request->packet->dst_port = server_port;
+#ifdef WITH_TCP
+		request->packet->proto = ipproto;
+#endif
+
+		request->files = files;
+		request->packet->id = -1; /* allocate when sending */
+		request->num = num++;
+
+		/*
+		 *	Read the request VP's.
+		 */
+		if (fr_pair_list_afrom_file(request->packet, &request->packet->vps, packets, &packets_done) < 0) {
+			char const *input;
+
+			if ((files->packets[0] == '-') && (files->packets[1] == '\0')) {
+				input = "stdin";
+			} else {
+				input = files->packets;
+			}
+
+			REDEBUG("Error parsing \"%s\"", input);
+			goto error;
+		}
+
+		/*
+		 *	Skip empty entries
+		 */
+		if (!request->packet->vps) {
+			talloc_free(request);
+			continue;
+		}
+
+		/*
+		 *	Read in filter VP's.
+		 */
+		if (filters) {
+			bool filters_done;
+
+			if (fr_pair_list_afrom_file(request, &request->filter, filters, &filters_done) < 0) {
+				REDEBUG("Error parsing \"%s\"", files->filters);
+				goto error;
+			}
+
+			if (filters_done && !packets_done) {
+				REDEBUG("Differing number of packets/filters in %s:%s "
+				        "(too many requests))", files->packets, files->filters);
+				goto error;
+			}
+
+			if (!filters_done && packets_done) {
+				REDEBUG("Differing number of packets/filters in %s:%s "
+				        "(too many filters))", files->packets, files->filters);
+				goto error;
+			}
+
+			/*
+			 *	xlat expansions aren't supported here
+			 */
+			for (vp = fr_cursor_init(&cursor, &request->filter);
+			     vp;
+			     vp = fr_cursor_next(&cursor)) {
+				if (vp->type == VT_XLAT) {
+					vp->type = VT_DATA;
+					vp->vp_strvalue = vp->value.xlat;
+					vp->vp_length = talloc_array_length(vp->vp_strvalue) - 1;
+				}
+
+				if (vp->da->vendor == 0 ) switch (vp->da->attr) {
+				case PW_RESPONSE_PACKET_TYPE:
+				case PW_PACKET_TYPE:
+					fr_cursor_remove(&cursor);	/* so we don't break the filter */
+					request->filter_code = vp->vp_integer;
+					talloc_free(vp);
+
+				default:
+					break;
+				}
+			}
+
+			/*
+			 *	This allows efficient list comparisons later
+			 */
+			fr_pair_list_sort(&request->filter, fr_pair_cmp_by_da_tag);
+		}
+
+		/*
+		 *	Process special attributes
+		 */
+		for (vp = fr_cursor_init(&cursor, &request->packet->vps);
+		     vp;
+		     vp = fr_cursor_next(&cursor)) {
+			/*
+			 *	Double quoted strings get marked up as xlat expansions,
+			 *	but we don't support that in request.
+			 */
+			if (vp->type == VT_XLAT) {
+				vp->type = VT_DATA;
+				vp->vp_strvalue = vp->value.xlat;
+				vp->vp_length = talloc_array_length(vp->vp_strvalue) - 1;
+			}
+
+			if (!vp->da->vendor) switch (vp->da->attr) {
+			default:
+				break;
+
+			/*
+			 *	Allow it to set the packet type in
+			 *	the attributes read from the file.
+			 */
+			case PW_PACKET_TYPE:
+				request->packet->code = vp->vp_integer;
+				break;
+
+			case PW_RESPONSE_PACKET_TYPE:
+				request->filter_code = vp->vp_integer;
+				break;
+
+			case PW_PACKET_DST_PORT:
+				request->packet->dst_port = (vp->vp_integer & 0xffff);
+				break;
+
+			case PW_PACKET_DST_IP_ADDRESS:
+				request->packet->dst_ipaddr.af = AF_INET;
+				request->packet->dst_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+				request->packet->dst_ipaddr.prefix = 32;
+				break;
+
+			case PW_PACKET_DST_IPV6_ADDRESS:
+				request->packet->dst_ipaddr.af = AF_INET6;
+				request->packet->dst_ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr;
+				request->packet->dst_ipaddr.prefix = 128;
+				break;
+
+			case PW_PACKET_SRC_PORT:
+				if ((vp->vp_integer < 1024) ||
+				    (vp->vp_integer > 65535)) {
+					ERROR("Invalid value '%u' for Packet-Src-Port", vp->vp_integer);
+					goto error;
+				}
+				request->packet->src_port = (vp->vp_integer & 0xffff);
+				break;
+
+			case PW_PACKET_SRC_IP_ADDRESS:
+				request->packet->src_ipaddr.af = AF_INET;
+				request->packet->src_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+				request->packet->src_ipaddr.prefix = 32;
+				break;
+
+			case PW_PACKET_SRC_IPV6_ADDRESS:
+				request->packet->src_ipaddr.af = AF_INET6;
+				request->packet->src_ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr;
+				request->packet->src_ipaddr.prefix = 128;
+				break;
+
+			case PW_DIGEST_REALM:
+			case PW_DIGEST_NONCE:
+			case PW_DIGEST_METHOD:
+			case PW_DIGEST_URI:
+			case PW_DIGEST_QOP:
+			case PW_DIGEST_ALGORITHM:
+			case PW_DIGEST_BODY_DIGEST:
+			case PW_DIGEST_CNONCE:
+			case PW_DIGEST_NONCE_COUNT:
+			case PW_DIGEST_USER_NAME:
+			/* overlapping! */
+			{
+				DICT_ATTR const *da;
+				uint8_t *p, *q;
+
+				p = talloc_array(vp, uint8_t, vp->vp_length + 2);
+
+				memcpy(p + 2, vp->vp_octets, vp->vp_length);
+				p[0] = vp->da->attr - PW_DIGEST_REALM + 1;
+				vp->vp_length += 2;
+				p[1] = vp->vp_length;
+
+				da = dict_attrbyvalue(PW_DIGEST_ATTRIBUTES, 0);
+				if (!da) {
+					ERROR("Out of memory");
+					goto error;
+				}
+				vp->da = da;
+
+				/*
+				 *	Re-do fr_pair_value_memsteal ourselves,
+				 *	because we play games with
+				 *	vp->da, and fr_pair_value_memsteal goes
+				 *	to GREAT lengths to sanitize
+				 *	and fix and change and
+				 *	double-check the various
+				 *	fields.
+				 */
+				memcpy(&q, &vp->vp_octets, sizeof(q));
+				talloc_free(q);
+
+				vp->vp_octets = talloc_steal(vp, p);
+				vp->type = VT_DATA;
+
+				VERIFY_VP(vp);
+			}
+				break;
+
+				/*
+				 *	Cache this for later.
+				 */
+			case PW_CLEARTEXT_PASSWORD:
+				request->password = vp;
+				break;
+
+			/*
+			 *	Keep a copy of the the password attribute.
+			 */
+			case PW_CHAP_PASSWORD:
+				/*
+				 *	If it's already hex, do nothing.
+				 */
+				if ((vp->vp_length == 17) &&
+				    (already_hex(vp))) break;
+
+				/*
+				 *	CHAP-Password is octets, so it may not be zero terminated.
+				 */
+				request->password = fr_pair_make(request->packet, &request->packet->vps, "Cleartext-Password",
+							     "", T_OP_EQ);
+				fr_pair_value_bstrncpy(request->password, vp->vp_strvalue, vp->vp_length);
+				break;
+
+			case PW_USER_PASSWORD:
+			case PW_MS_CHAP_PASSWORD:
+				request->password = fr_pair_make(request->packet, &request->packet->vps, "Cleartext-Password",
+							     vp->vp_strvalue, T_OP_EQ);
+				break;
+
+			case PW_RADCLIENT_TEST_NAME:
+				request->name = vp->vp_strvalue;
+				break;
+			}
+		} /* loop over the VP's we read in */
+
+		/*
+		 *	Use the default set on the command line
+		 */
+		if (request->packet->code == PW_CODE_UNDEFINED) request->packet->code = packet_code;
+
+		/*
+		 *	Default to the filename
+		 */
+		if (!request->name) request->name = request->files->packets;
+
+		/*
+		 *	Automatically set the response code from the request code
+		 *	(if one wasn't already set).
+		 */
+		if (request->filter_code == PW_CODE_UNDEFINED) {
+			switch (request->packet->code) {
+			case PW_CODE_ACCESS_REQUEST:
+				request->filter_code = PW_CODE_ACCESS_ACCEPT;
+				break;
+
+			case PW_CODE_ACCOUNTING_REQUEST:
+				request->filter_code = PW_CODE_ACCOUNTING_RESPONSE;
+				break;
+
+			case PW_CODE_COA_REQUEST:
+				request->filter_code = PW_CODE_COA_ACK;
+				break;
+
+			case PW_CODE_DISCONNECT_REQUEST:
+				request->filter_code = PW_CODE_DISCONNECT_ACK;
+				break;
+
+			case PW_CODE_STATUS_SERVER:
+				switch (radclient_get_code(request->packet->dst_port)) {
+				case PW_CODE_ACCESS_REQUEST:
+					request->filter_code = PW_CODE_ACCESS_ACCEPT;
+					break;
+
+				case PW_CODE_ACCOUNTING_REQUEST:
+					request->filter_code = PW_CODE_ACCOUNTING_RESPONSE;
+					break;
+
+				default:
+					request->filter_code = PW_CODE_UNDEFINED;
+					break;
+				}
+				break;
+
+			case PW_CODE_UNDEFINED:
+				REDEBUG("Both Packet-Type and Response-Packet-Type undefined, specify at least one, "
+					"or a well known RADIUS port");
+				goto error;
+
+			default:
+				REDEBUG("Can't determine expected Response-Packet-Type for Packet-Type %i",
+					request->packet->code);
+				goto error;
+			}
+		/*
+		 *	Automatically set the request code from the response code
+		 *	(if one wasn't already set).
+		 */
+		} else if (request->packet->code == PW_CODE_UNDEFINED) {
+			switch (request->filter_code) {
+			case PW_CODE_ACCESS_ACCEPT:
+			case PW_CODE_ACCESS_REJECT:
+				request->packet->code = PW_CODE_ACCESS_REQUEST;
+				break;
+
+			case PW_CODE_ACCOUNTING_RESPONSE:
+				request->packet->code = PW_CODE_ACCOUNTING_REQUEST;
+				break;
+
+			case PW_CODE_DISCONNECT_ACK:
+			case PW_CODE_DISCONNECT_NAK:
+				request->packet->code = PW_CODE_DISCONNECT_REQUEST;
+				break;
+
+			case PW_CODE_COA_ACK:
+			case PW_CODE_COA_NAK:
+				request->packet->code = PW_CODE_COA_REQUEST;
+				break;
+
+			default:
+				REDEBUG("Can't determine expected Packet-Type for Response-Packet-Type %i",
+					request->filter_code);
+				goto error;
+			}
+		}
+
+		/*
+		 *	Automatically set the dst port (if one wasn't already set).
+		 */
+		if (request->packet->dst_port == 0) {
+			radclient_get_port(request->packet->code, &request->packet->dst_port);
+			if (request->packet->dst_port == 0) {
+				REDEBUG("Can't determine destination port");
+				goto error;
+			}
+		}
+
+		/*
+		 *	Add it to the tail of the list.
+		 */
+		if (!request_head) {
+			assert(rc_request_tail == NULL);
+			request_head = request;
+			request->prev = NULL;
+		} else {
+			assert(rc_request_tail->next == NULL);
+			rc_request_tail->next = request;
+			request->prev = rc_request_tail;
+		}
+		rc_request_tail = request;
+		request->next = NULL;
+
+		/*
+		 *	Set the destructor so it removes itself from the
+		 *	request list when freed. We don't set this until
+		 *	the packet is actually in the list, else we trigger
+		 *	the asserts in the free callback.
+		 */
+		talloc_set_destructor(request, _rc_request_free);
+	} while (!packets_done); /* loop until the file is done. */
+
+	if (packets != stdin) fclose(packets);
+	if (filters) fclose(filters);
+
+	/*
+	 *	And we're done.
+	 */
+	return 1;
+
+error:
+	talloc_free(request);
+
+	if (packets != stdin) fclose(packets);
+	if (filters) fclose(filters);
+
+	return 0;
+}
+
+
+/*
+ *	Sanity check each argument.
+ */
+static int radclient_sane(rc_request_t *request)
+{
+	if (request->packet->dst_port == 0) {
+		request->packet->dst_port = server_port;
+	}
+	if (request->packet->dst_ipaddr.af == AF_UNSPEC) {
+		if (server_ipaddr.af == AF_UNSPEC) {
+			ERROR("No server was given, and request %" PRIu64 " in file %s did not contain "
+			      "Packet-Dst-IP-Address", request->num, request->files->packets);
+			return -1;
+		}
+		request->packet->dst_ipaddr = server_ipaddr;
+	}
+	if (request->packet->code == 0) {
+		if (packet_code == -1) {
+			ERROR("Request was \"auto\", and request %" PRIu64 " in file %s did not contain Packet-Type",
+			      request->num, request->files->packets);
+			return -1;
+		}
+		request->packet->code = packet_code;
+	}
+	request->packet->sockfd = -1;
+
+	return 0;
+}
+
+
+/*
+ *	For request handling.
+ */
+static int filename_cmp(void const *one, void const *two)
+{
+	int cmp;
+
+	rc_file_pair_t const *a = one;
+	rc_file_pair_t const *b = two;
+
+	cmp = strcmp(a->packets, b->packets);
+	if (cmp != 0) return cmp;
+
+	return strcmp(a->filters, b->filters);
+}
+
+static int filename_walk(UNUSED void *context, void *data)
+{
+	rc_file_pair_t *files = data;
+
+	/*
+	 *	Read request(s) from the file.
+	 */
+	if (!radclient_init(files, files)) return -1;	/* stop walking */
+
+	return 0;
+}
+
+
+/*
+ *	Deallocate packet ID, etc.
+ */
+static void deallocate_id(rc_request_t *request)
+{
+	if (!request || !request->packet ||
+	    (request->packet->id < 0)) {
+		return;
+	}
+
+	/*
+	 *	One more unused RADIUS ID.
+	 */
+	fr_packet_list_id_free(pl, request->packet, true);
+
+	/*
+	 *	If we've already sent a packet, free up the old one,
+	 *	and ensure that the next packet has a unique
+	 *	authentication vector.
+	 */
+	if (request->packet->data) TALLOC_FREE(request->packet->data);
+	if (request->reply) rad_free(&request->reply);
+}
+
+/*
+ *	Send one packet.
+ */
+static int send_one_packet(rc_request_t *request)
+{
+	assert(request->done == false);
+
+	/*
+	 *	Remember when we have to wake up, to re-send the
+	 *	request, of we didn't receive a reply.
+	 */
+	if ((sleep_time == -1) || (sleep_time > (int) timeout)) sleep_time = (int) timeout;
+
+	/*
+	 *	Haven't sent the packet yet.  Initialize it.
+	 */
+	if (request->packet->id == -1) {
+		int i;
+		bool rcode;
+
+		assert(request->reply == NULL);
+
+		/*
+		 *	Didn't find a free packet ID, we're not done,
+		 *	we don't sleep, and we stop trying to process
+		 *	this packet.
+		 */
+	retry:
+		request->packet->src_ipaddr.af = server_ipaddr.af;
+		rcode = fr_packet_list_id_alloc(pl, ipproto, &request->packet, NULL);
+		if (!rcode) {
+			int mysockfd;
+
+#ifdef WITH_TCP
+			if (proto) {
+				mysockfd = fr_socket_client_tcp(NULL,
+								&request->packet->dst_ipaddr,
+								request->packet->dst_port, false);
+				if (mysockfd < 0) {
+					ERROR("Failed opening socket");
+					exit(1);
+				}
+			} else
+#endif
+			{
+				mysockfd = fr_socket(&client_ipaddr, 0);
+				if (mysockfd < 0) {
+					ERROR("Failed opening socket");
+					exit(1);
+				}
+
+#ifdef WITH_UDPFROMTO
+				if (udpfromto_init(mysockfd) < 0) {
+					ERROR("Failed initializing socket");
+					exit(1);
+				}
+#endif
+			}
+			if (!fr_packet_list_socket_add(pl, mysockfd, ipproto,
+#ifdef WITH_RADIUSV11
+						       false,
+#endif
+						       &request->packet->dst_ipaddr,
+						       request->packet->dst_port, NULL)) {
+				ERROR("Can't add new socket");
+				exit(1);
+			}
+			goto retry;
+		}
+
+		assert(request->packet->id != -1);
+		assert(request->packet->data == NULL);
+
+		for (i = 0; i < 4; i++) {
+			((uint32_t *) request->packet->vector)[i] = fr_rand();
+		}
+
+		/*
+		 *	Update the password, so it can be encrypted with the
+		 *	new authentication vector.
+		 */
+		if (request->password) {
+			VALUE_PAIR *vp;
+
+			if ((vp = fr_pair_find_by_num(request->packet->vps, PW_USER_PASSWORD, 0, TAG_ANY)) != NULL) {
+				fr_pair_value_strcpy(vp, request->password->vp_strvalue);
+
+			} else if ((vp = fr_pair_find_by_num(request->packet->vps, PW_CHAP_PASSWORD, 0, TAG_ANY)) != NULL) {
+				uint8_t buffer[17];
+
+				rad_chap_encode(request->packet, buffer, fr_rand() & 0xff, request->password);
+				fr_pair_value_memcpy(vp, buffer, 17);
+
+			} else if (fr_pair_find_by_num(request->packet->vps, PW_MS_CHAP_PASSWORD, 0, TAG_ANY) != NULL) {
+				mschapv1_encode(request->packet, &request->packet->vps, request->password->vp_strvalue);
+
+			} else {
+				DEBUG("WARNING: No password in the request");
+			}
+		}
+
+		request->timestamp = time(NULL);
+		request->tries = 1;
+		request->resend++;
+
+	} else {		/* request->packet->id >= 0 */
+		time_t now = time(NULL);
+
+		/*
+		 *	FIXME: Accounting packets are never retried!
+		 *	The Acct-Delay-Time attribute is updated to
+		 *	reflect the delay, and the packet is re-sent
+		 *	from scratch!
+		 */
+
+		/*
+		 *	Not time for a retry, do so.
+		 */
+		if ((now - request->timestamp) < timeout) {
+			/*
+			 *	When we walk over the tree sending
+			 *	packets, we update the minimum time
+			 *	required to sleep.
+			 */
+			if ((sleep_time == -1) ||
+			    (sleep_time > (now - request->timestamp))) {
+				sleep_time = now - request->timestamp;
+			}
+			return 0;
+		}
+
+		/*
+		 *	We're not trying later, maybe the packet is done.
+		 */
+		if (request->tries == retries) {
+			assert(request->packet->id >= 0);
+
+			/*
+			 *	Delete the request from the tree of
+			 *	outstanding requests.
+			 */
+			fr_packet_list_yank(pl, request->packet);
+
+			RDEBUG("No reply from server for ID %d socket %d",
+			       request->packet->id, request->packet->sockfd);
+			deallocate_id(request);
+
+			/*
+			 *	Normally we mark it "done" when we've received
+			 *	the reply, but this is a special case.
+			 */
+			if (request->resend == resend_count) {
+				request->done = true;
+			}
+			stats.lost++;
+			return -1;
+		}
+
+		/*
+		 *	We are trying later.
+		 */
+		request->timestamp = now;
+		request->tries++;
+	}
+
+	/*
+	 *	Send the packet.
+	 */
+	if (rad_send(request->packet, NULL, secret) < 0) {
+		REDEBUG("Failed to send packet for ID %d", request->packet->id);
+		deallocate_id(request);
+		request->done = true;
+		return -1;
+	}
+
+	if (fr_log_fp) {
+		fr_packet_header_print(fr_log_fp, request->packet, false);
+		if (fr_debug_lvl > 0) vp_printlist(fr_log_fp, request->packet->vps);
+	}
+
+	return 0;
+}
+
+/*
+ *	Receive one packet, maybe.
+ */
+static int recv_one_packet(int wait_time)
+{
+	fd_set		set;
+	struct timeval  tv;
+	rc_request_t	*request;
+	RADIUS_PACKET	*reply, **packet_p;
+	volatile int max_fd;
+
+	/* And wait for reply, timing out as necessary */
+	FD_ZERO(&set);
+
+	max_fd = fr_packet_list_fd_set(pl, &set);
+	if (max_fd < 0) exit(1); /* no sockets to listen on! */
+
+	tv.tv_sec = (wait_time <= 0) ? 0 : wait_time;
+	tv.tv_usec = 0;
+
+	/*
+	 *	No packet was received.
+	 */
+	if (select(max_fd, &set, NULL, NULL, &tv) <= 0) return 0;
+
+	/*
+	 *	Look for the packet.
+	 */
+	reply = fr_packet_list_recv(pl, &set);
+	if (!reply) {
+		ERROR("Received bad packet");
+#ifdef WITH_TCP
+		/*
+		 *	If the packet is bad, we close the socket.
+		 *	I'm not sure how to do that now, so we just
+		 *	die...
+		 */
+		if (proto) exit(1);
+#endif
+		return -1;	/* bad packet */
+	}
+
+	packet_p = fr_packet_list_find_byreply(pl, reply);
+	if (!packet_p) {
+		ERROR("Received reply to request we did not send. (id=%d socket %d)",
+		      reply->id, reply->sockfd);
+		rad_free(&reply);
+		return -1;	/* got reply to packet we didn't send */
+	}
+	request = fr_packet2myptr(rc_request_t, packet, packet_p);
+
+	/*
+	 *	Fails the signature validation: not a real reply.
+	 *	FIXME: Silently drop it and listen for another packet.
+	 */
+	if (rad_verify(reply, request->packet, secret) < 0) {
+		REDEBUG("Reply verification failed");
+		stats.lost++;
+		goto packet_done; /* shared secret is incorrect */
+	}
+
+	if (print_filename) {
+		RDEBUG("%s response code %d", request->files->packets, reply->code);
+	}
+
+	deallocate_id(request);
+	request->reply = reply;
+	reply = NULL;
+
+	/*
+	 *	If this fails, we're out of memory.
+	 */
+	if (rad_decode(request->reply, request->packet, secret) != 0) {
+		REDEBUG("Reply decode failed");
+		stats.lost++;
+		goto packet_done;
+	}
+
+	if (fr_log_fp) {
+		fr_packet_header_print(fr_log_fp, request->reply, true);
+		if (fr_debug_lvl > 0) vp_printlist(fr_log_fp, request->reply->vps);
+	}
+
+	/*
+	 *	Increment counters...
+	 */
+	switch (request->reply->code) {
+	case PW_CODE_ACCESS_ACCEPT:
+	case PW_CODE_ACCOUNTING_RESPONSE:
+	case PW_CODE_COA_ACK:
+	case PW_CODE_DISCONNECT_ACK:
+		stats.accepted++;
+		break;
+
+	case PW_CODE_ACCESS_CHALLENGE:
+		break;
+
+	default:
+		stats.rejected++;
+	}
+
+	/*
+	 *	If we had an expected response code, check to see if the
+	 *	packet matched that.
+	 */
+	if ((request->filter_code != PW_CODE_UNDEFINED) && (request->reply->code != request->filter_code)) {
+		fr_strerror_printf(NULL);
+
+		if (is_radius_code(request->reply->code)) {
+			REDEBUG("%s: Expected %s got %s", request->name, fr_packet_codes[request->filter_code],
+				fr_packet_codes[request->reply->code]);
+		} else {
+			REDEBUG("%s: Expected %u got %i", request->name, request->filter_code,
+				request->reply->code);
+		}
+		stats.failed++;
+	/*
+	 *	Check if the contents of the packet matched the filter
+	 */
+	} else if (!request->filter) {
+		stats.passed++;
+	} else {
+		VALUE_PAIR const *failed[2];
+
+		fr_pair_list_sort(&request->reply->vps, fr_pair_cmp_by_da_tag);
+		if (fr_pair_validate(failed, request->filter, request->reply->vps)) {
+			RDEBUG("%s: Response passed filter", request->name);
+			stats.passed++;
+		} else {
+			fr_pair_validate_debug(request, failed);
+			REDEBUG("%s: Response for failed filter", request->name);
+			stats.failed++;
+		}
+	}
+
+	if (request->resend == resend_count) {
+		request->done = true;
+	}
+
+packet_done:
+	rad_free(&request->reply);
+	rad_free(&reply);	/* may be NULL */
+
+	return 0;
+}
+
+DIAG_OFF(deprecated-declarations)
+int main(int argc, char **argv)
+{
+	int		c;
+	char		const *radius_dir = RADDBDIR;
+	char		const *dict_dir = DICTDIR;
+	char		filesecret[256];
+	FILE		*fp;
+	int		do_summary = false;
+	int		persec = 0;
+	int		parallel = 1;
+	rc_request_t	*this;
+	int		force_af = AF_UNSPEC;
+
+	/*
+	 *	It's easier having two sets of flags to set the
+	 *	verbosity of library calls and the verbosity of
+	 *	radclient.
+	 */
+	fr_debug_lvl = 0;
+	fr_log_fp = stdout;
+
+#ifndef NDEBUG
+	if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) {
+		fr_perror("radclient");
+		exit(EXIT_FAILURE);
+	}
+#endif
+
+	talloc_set_log_stderr();
+
+	filename_tree = rbtree_create(NULL, filename_cmp, NULL, 0);
+	if (!filename_tree) {
+	oom:
+		ERROR("Out of memory");
+		exit(1);
+	}
+
+	while ((c = getopt(argc, argv, "46c:d:D:f:Fhn:p:qr:sS:t:vx"
+#ifdef WITH_TCP
+		"P:"
+#endif
+			   )) != EOF) switch (c) {
+		case '4':
+			force_af = AF_INET;
+			break;
+
+		case '6':
+			force_af = AF_INET6;
+			break;
+
+		case 'c':
+			if (!isdigit((uint8_t) *optarg)) usage();
+
+			resend_count = atoi(optarg);
+
+			if (resend_count < 1) usage();
+			break;
+
+		case 'D':
+			dict_dir = optarg;
+			break;
+
+		case 'd':
+			radius_dir = optarg;
+			break;
+
+		case 'f':
+		{
+			char const *p;
+			rc_file_pair_t *files;
+
+			files = talloc(talloc_autofree_context(), rc_file_pair_t);
+			if (!files) goto oom;
+
+			p = strchr(optarg, ':');
+			if (p) {
+				files->packets = talloc_strndup(files, optarg, p - optarg);
+				if (!files->packets) goto oom;
+				files->filters = p + 1;
+			} else {
+				files->packets = optarg;
+				files->filters = NULL;
+			}
+			rbtree_insert(filename_tree, (void *) files);
+		}
+			break;
+
+		case 'F':
+			print_filename = true;
+			break;
+
+		case 'n':
+			persec = atoi(optarg);
+			if (persec <= 0) usage();
+			break;
+
+			/*
+			 *	Note that sending MANY requests in
+			 *	parallel can over-run the kernel
+			 *	queues, and Linux will happily discard
+			 *	packets.  So even if the server responds,
+			 *	the client may not see the reply.
+			 */
+		case 'p':
+			parallel = atoi(optarg);
+			if (parallel <= 0) usage();
+			break;
+
+#ifdef WITH_TCP
+		case 'P':
+			proto = optarg;
+			if (strcmp(proto, "tcp") != 0) {
+				if (strcmp(proto, "udp") == 0) {
+					proto = NULL;
+				} else {
+					usage();
+				}
+			} else {
+				ipproto = IPPROTO_TCP;
+			}
+			break;
+
+#endif
+
+		case 'q':
+			do_output = false;
+			fr_log_fp = NULL; /* no output from you, either! */
+			break;
+
+		case 'r':
+			if (!isdigit((uint8_t) *optarg)) usage();
+			retries = atoi(optarg);
+			if ((retries == 0) || (retries > 1000)) usage();
+			break;
+
+		case 's':
+			do_summary = true;
+			break;
+
+		case 'S':
+		{
+			char *p;
+			fp = fopen(optarg, "r");
+			if (!fp) {
+			       ERROR("Error opening %s: %s", optarg, fr_syserror(errno));
+			       exit(1);
+			}
+			if (fgets(filesecret, sizeof(filesecret), fp) == NULL) {
+			       ERROR("Error reading %s: %s", optarg, fr_syserror(errno));
+			       exit(1);
+			}
+			fclose(fp);
+
+			/* truncate newline */
+			p = filesecret + strlen(filesecret) - 1;
+			while ((p >= filesecret) &&
+			      (*p < ' ')) {
+			       *p = '\0';
+			       --p;
+			}
+
+			if (strlen(filesecret) < 2) {
+			       ERROR("Secret in %s is too short", optarg);
+			       exit(1);
+			}
+			secret = filesecret;
+		}
+		       break;
+
+		case 't':
+			if (!isdigit((uint8_t) *optarg))
+				usage();
+			timeout = atof(optarg);
+			break;
+
+		case 'v':
+			fr_debug_lvl = 1;
+			DEBUG("%s", radclient_version);
+			exit(0);
+
+		case 'x':
+			fr_debug_lvl++;
+			break;
+
+		case 'h':
+		default:
+			usage();
+	}
+	argc -= (optind - 1);
+	argv += (optind - 1);
+
+	if ((argc < 3)  || ((secret == NULL) && (argc < 4))) {
+		ERROR("Insufficient arguments");
+		usage();
+	}
+	/*
+	 *	Mismatch between the binary and the libraries it depends on
+	 */
+	if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) {
+		fr_perror("radclient");
+		return 1;
+	}
+
+	if (dict_init(dict_dir, RADIUS_DICTIONARY) < 0) {
+		fr_perror("radclient");
+		return 1;
+	}
+
+	if (dict_read(radius_dir, RADIUS_DICTIONARY) == -1) {
+		fr_perror("radclient");
+		return 1;
+	}
+	fr_strerror();	/* Clear the error buffer */
+
+	/*
+	 *	Get the request type
+	 */
+	if (!isdigit((uint8_t) argv[2][0])) {
+		packet_code = fr_str2int(request_types, argv[2], -2);
+		if (packet_code == -2) {
+			ERROR("Unrecognised request type \"%s\"", argv[2]);
+			usage();
+		}
+	} else {
+		packet_code = atoi(argv[2]);
+	}
+
+	/*
+	 *	Resolve hostname.
+	 */
+	if (strcmp(argv[1], "-") != 0) {
+		if (fr_pton_port(&server_ipaddr, &server_port, argv[1], -1, force_af, true) < 0) {
+			ERROR("%s", fr_strerror());
+			exit(1);
+		}
+
+		/*
+		 *	Work backwards from the port to determine the packet type
+		 */
+		if (packet_code == PW_CODE_UNDEFINED) packet_code = radclient_get_code(server_port);
+	}
+	radclient_get_port(packet_code, &server_port);
+
+	/*
+	 *	Add the secret.
+	 */
+	if (argv[3]) secret = argv[3];
+
+	/*
+	 *	If no '-f' is specified, we're reading from stdin.
+	 */
+	if (rbtree_num_elements(filename_tree) == 0) {
+		rc_file_pair_t *files;
+
+		files = talloc_zero(talloc_autofree_context(), rc_file_pair_t);
+		files->packets = "-";
+		if (!radclient_init(files, files)) {
+			exit(1);
+		}
+	}
+
+	/*
+	 *	Walk over the list of filenames, creating the requests.
+	 */
+	if (rbtree_walk(filename_tree, RBTREE_IN_ORDER, filename_walk, NULL) != 0) {
+		ERROR("Failed parsing input files");
+		exit(1);
+	}
+
+	/*
+	 *	No packets read.  Die.
+	 */
+	if (!request_head) {
+		ERROR("Nothing to send");
+		exit(1);
+	}
+
+	openssl3_init();
+
+	/*
+	 *	Bind to the first specified IP address and port.
+	 *	This means we ignore later ones.
+	 */
+	if (request_head->packet->src_ipaddr.af == AF_UNSPEC) {
+		memset(&client_ipaddr, 0, sizeof(client_ipaddr));
+		client_ipaddr.af = server_ipaddr.af;
+	} else {
+		client_ipaddr = request_head->packet->src_ipaddr;
+	}
+
+	client_port = request_head->packet->src_port;
+
+#ifdef WITH_TCP
+	if (proto) {
+		sockfd = fr_socket_client_tcp(NULL, &server_ipaddr, server_port, false);
+		if (sockfd < 0) {
+			ERROR("Error opening socket");
+			exit(1);
+		}
+	} else
+#endif
+	{
+		sockfd = fr_socket(&client_ipaddr, client_port);
+		if (sockfd < 0) {
+			ERROR("Error opening socket");
+			exit(1);
+		}
+
+#ifdef WITH_UDPFROMTO
+		if (udpfromto_init(sockfd) < 0) {
+			ERROR("Failed initializing socket");
+			exit(1);
+		}
+#endif
+	}
+
+	pl = fr_packet_list_create(1);
+	if (!pl) {
+		ERROR("Out of memory");
+		exit(1);
+	}
+
+	if (!fr_packet_list_socket_add(pl, sockfd, ipproto,
+#ifdef WITH_RADIUSV11
+				       false,
+#endif
+				       &server_ipaddr, server_port, NULL)) {
+		ERROR("Out of memory");
+		exit(1);
+	}
+
+	/*
+	 *	Walk over the list of packets, sanity checking
+	 *	everything.
+	 */
+	for (this = request_head; this != NULL; this = this->next) {
+		this->packet->src_ipaddr = client_ipaddr;
+		this->packet->src_port = client_port;
+		if (radclient_sane(this) != 0) {
+			exit(1);
+		}
+	}
+
+	/*
+	 *	Walk over the packets to send, until
+	 *	we're all done.
+	 *
+	 *	FIXME: This currently busy-loops until it receives
+	 *	all of the packets.  It should really have some sort of
+	 *	send packet, get time to wait, select for time, etc.
+	 *	loop.
+	 */
+	do {
+		int n = parallel;
+		rc_request_t *next;
+		char const *filename = NULL;
+
+		done = true;
+		sleep_time = -1;
+
+		/*
+		 *	Walk over the packets, sending them.
+		 */
+
+		for (this = request_head; this != NULL; this = next) {
+			next = this->next;
+
+			/*
+			 *	If there's a packet to receive,
+			 *	receive it, but don't wait for a
+			 *	packet.
+			 */
+			recv_one_packet(0);
+
+			/*
+			 *	This packet is done.  Delete it.
+			 */
+			if (this->done) {
+				talloc_free(this);
+				continue;
+			}
+
+			/*
+			 *	Packets from multiple '-f' are sent
+			 *	in parallel.
+			 *
+			 *	Packets from one file are sent in
+			 *	series, unless '-p' is specified, in
+			 *	which case N packets from each file
+			 *	are sent in parallel.
+			 */
+			if (this->files->packets != filename) {
+				filename = this->files->packets;
+				n = parallel;
+			}
+
+			if (n > 0) {
+				n--;
+
+				/*
+				 *	Send the current packet.
+				 */
+				if (send_one_packet(this) < 0) {
+					talloc_free(this);
+					break;
+				}
+
+				/*
+				 *	Wait a little before sending
+				 *	the next packet, if told to.
+				 */
+				if (persec) {
+					struct timeval tv;
+
+					/*
+					 *	Don't sleep elsewhere.
+					 */
+					sleep_time = 0;
+
+					if (persec == 1) {
+						tv.tv_sec = 1;
+						tv.tv_usec = 0;
+					} else {
+						tv.tv_sec = 0;
+						tv.tv_usec = 1000000/persec;
+					}
+
+					/*
+					 *	Sleep for milliseconds,
+					 *	portably.
+					 *
+					 *	If we get an error or
+					 *	a signal, treat it like
+					 *	a normal timeout.
+					 */
+					select(0, NULL, NULL, NULL, &tv);
+				}
+
+				/*
+				 *	If we haven't sent this packet
+				 *	often enough, we're not done,
+				 *	and we shouldn't sleep.
+				 */
+				if (this->resend < resend_count) {
+					done = false;
+					sleep_time = 0;
+				}
+			} else { /* haven't sent this packet, we're not done */
+				assert(this->done == false);
+				assert(this->reply == NULL);
+				done = false;
+			}
+		}
+
+		/*
+		 *	Still have outstanding requests.
+		 */
+		if (fr_packet_list_num_elements(pl) > 0) {
+			done = false;
+		} else {
+			sleep_time = 0;
+		}
+
+		/*
+		 *	Nothing to do until we receive a request, so
+		 *	sleep until then.  Once we receive one packet,
+		 *	we go back, and walk through the whole list again,
+		 *	sending more packets (if necessary), and updating
+		 *	the sleep time.
+		 */
+		if (!done && (sleep_time > 0)) {
+			recv_one_packet(sleep_time);
+		}
+	} while (!done);
+
+	rbtree_free(filename_tree);
+	fr_packet_list_free(pl);
+	while (request_head) TALLOC_FREE(request_head);
+	dict_free();
+
+	if (do_summary) {
+		printf("Packet summary:\n"
+		       "\tAccepted      : %" PRIu64 "\n"
+		       "\tRejected      : %" PRIu64 "\n"
+		       "\tLost          : %" PRIu64 "\n"
+		       "\tPassed filter : %" PRIu64 "\n"
+		       "\tFailed filter : %" PRIu64 "\n",
+		       stats.accepted,
+		       stats.rejected,
+		       stats.lost,
+		       stats.passed,
+		       stats.failed
+		);
+	}
+
+	if ((stats.lost > 0) || (stats.failed > 0)) {
+		exit(1);
+	}
+
+	openssl3_free();
+
+	exit(0);
+}
+DIAG_ON(deprecated-declarations)
diff --git a/src/main/radclient.mk b/src/main/radclient.mk
new file mode 100644
index 0000000..0db0a8a
--- /dev/null
+++ b/src/main/radclient.mk
@@ -0,0 +1,8 @@
+TARGET		:= radclient
+SOURCES		:= radclient.c ${top_srcdir}/src/modules/rlm_mschap/smbdes.c \
+		   ${top_srcdir}/src/modules/rlm_mschap/mschap.c
+
+TGT_PREREQS	:= libfreeradius-radius.a
+
+SRC_CFLAGS	:= -I${top_srcdir}/src/modules/rlm_mschap
+TGT_LDLIBS	:= $(LIBS)
diff --git a/src/main/radiusd.c b/src/main/radiusd.c
new file mode 100644
index 0000000..f2acec7
--- /dev/null
+++ b/src/main/radiusd.c
@@ -0,0 +1,794 @@
+/*
+ * radiusd.c	Main loop of the radius server.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2000-2019  The FreeRADIUS server project
+ * Copyright 1999,2000  Miquel van Smoorenburg <miquels@cistron.nl>
+ * Copyright 2000  Alan DeKok <aland@ox.org>
+ * Copyright 2000  Alan Curry <pacman-radius@cqc.com>
+ * Copyright 2000  Jeff Carneal <jeff@apex.net>
+ * Copyright 2000  Chad Miller <cmiller@surfsouth.com>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/state.h>
+#include <freeradius-devel/rad_assert.h>
+#include <freeradius-devel/autoconf.h>
+
+#include <sys/file.h>
+
+#include <fcntl.h>
+#include <ctype.h>
+
+#ifdef HAVE_GETOPT_H
+#	include <getopt.h>
+#endif
+
+#ifdef HAVE_SYS_WAIT_H
+#	include <sys/wait.h>
+#endif
+#ifndef WEXITSTATUS
+#	define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8)
+#endif
+#ifndef WIFEXITED
+#	define WIFEXITED(stat_val) (((stat_val) & 255) == 0)
+#endif
+
+#ifdef HAVE_SYSTEMD
+#  include <systemd/sd-daemon.h>
+#endif
+
+/*
+ *  Global variables.
+ */
+char const	*radacct_dir = NULL;
+char const	*radlog_dir = NULL;
+
+bool		log_stripped_names;
+
+char const *radiusd_version = "FreeRADIUS Version " RADIUSD_VERSION_STRING
+#ifdef RADIUSD_VERSION_COMMIT
+" (git #" STRINGIFY(RADIUSD_VERSION_COMMIT) ")"
+#endif
+", for host " HOSTINFO
+#ifndef ENABLE_REPRODUCIBLE_BUILDS
+", built on " __DATE__ " at " __TIME__
+#endif
+;
+
+static pid_t radius_pid;
+
+/*
+ *  Configuration items.
+ */
+
+/*
+ *	Static functions.
+ */
+static void usage(int);
+
+static void sig_fatal (int);
+#ifdef SIGHUP
+static void sig_hup (int);
+#endif
+
+/*
+ *	The main guy.
+ */
+int main(int argc, char *argv[])
+{
+	int rcode = EXIT_SUCCESS;
+	int status;
+	int argval;
+	bool spawn_flag = true;
+	bool display_version = false;
+	int flag = 0;
+	int from_child[2] = {-1, -1};
+	char *p;
+	fr_state_t *state = NULL;
+
+	/*
+	 *  We probably don't want to free the talloc autofree context
+	 *  directly, so we'll allocate a new context beneath it, and
+	 *  free that before any leak reports.
+	 */
+	TALLOC_CTX *autofree = talloc_init("main");
+
+#ifdef OSFC2
+	set_auth_parameters(argc, argv);
+#endif
+
+#ifdef WIN32
+	{
+		WSADATA wsaData;
+		if (WSAStartup(MAKEWORD(2, 0), &wsaData)) {
+			fprintf(stderr, "%s: Unable to initialize socket library.\n",
+				main_config.name);
+			exit(EXIT_FAILURE);
+		}
+	}
+#endif
+
+	rad_debug_lvl = 0;
+	set_radius_dir(autofree, RADIUS_DIR);
+
+	/*
+	 *	Ensure that the configuration is initialized.
+	 */
+	memset(&main_config, 0, sizeof(main_config));
+	main_config.myip.af = AF_UNSPEC;
+	main_config.port = 0;
+	main_config.daemonize = true;
+
+	p = strrchr(argv[0], FR_DIR_SEP);
+	if (!p) {
+		main_config.name = argv[0];
+	} else {
+		main_config.name = p + 1;
+	}
+
+	/*
+	 *	Don't put output anywhere until we get told a little
+	 *	more.
+	 */
+	default_log.dst = L_DST_NULL;
+	default_log.fd = -1;
+	main_config.log_file = NULL;
+
+	/*  Process the options.  */
+	while ((argval = getopt(argc, argv, "Cd:D:fhi:l:mMn:p:PstvxX")) != EOF) {
+
+		switch (argval) {
+			case 'C':
+				check_config = true;
+				spawn_flag = false;
+				main_config.daemonize = false;
+				break;
+
+			case 'd':
+				set_radius_dir(autofree, optarg);
+				break;
+
+			case 'D':
+				main_config.dictionary_dir = talloc_typed_strdup(autofree, optarg);
+				break;
+
+			case 'f':
+				main_config.daemonize = false;
+				break;
+
+			case 'h':
+				usage(0);
+				break;
+
+			case 'l':
+				if (strcmp(optarg, "stdout") == 0) {
+					goto do_stdout;
+				}
+				main_config.log_file = strdup(optarg);
+				default_log.dst = L_DST_FILES;
+				default_log.fd = open(main_config.log_file,
+							    O_WRONLY | O_APPEND | O_CREAT, 0640);
+				if (default_log.fd < 0) {
+					fprintf(stderr, "%s: Failed to open log file %s: %s\n",
+						main_config.name, main_config.log_file, fr_syserror(errno));
+					exit(EXIT_FAILURE);
+				}
+				fr_log_fp = fdopen(default_log.fd, "a");
+				break;
+
+			case 'i':
+				if (ip_hton(&main_config.myip, AF_UNSPEC, optarg, false) < 0) {
+					fprintf(stderr, "%s: Invalid IP Address or hostname \"%s\"\n",
+						main_config.name, optarg);
+					exit(EXIT_FAILURE);
+				}
+				flag |= 1;
+				break;
+
+			case 'n':
+				main_config.name = optarg;
+				break;
+
+			case 'm':
+				main_config.debug_memory = true;
+				break;
+
+			case 'M':
+				main_config.memory_report = true;
+				main_config.debug_memory = true;
+				break;
+
+			case 'p':
+			{
+				unsigned long port;
+
+				port = strtoul(optarg, 0, 10);
+				if ((port == 0) || (port > UINT16_MAX)) {
+					fprintf(stderr, "%s: Invalid port number \"%s\"\n",
+						main_config.name, optarg);
+					exit(EXIT_FAILURE);
+				}
+
+				main_config.port = (uint16_t) port;
+				flag |= 2;
+			}
+				break;
+
+			case 'P':
+				/* Force the PID to be written, even in -f mode */
+				main_config.write_pid = true;
+				break;
+
+			case 's':	/* Single process mode */
+				spawn_flag = false;
+				main_config.daemonize = false;
+				break;
+
+			case 't':	/* no child threads */
+				spawn_flag = false;
+				break;
+
+			case 'v':
+				display_version = true;
+				break;
+
+			case 'X':
+				spawn_flag = false;
+				main_config.daemonize = false;
+				rad_debug_lvl += 2;
+				main_config.log_auth = true;
+				main_config.log_auth_badpass = true;
+				main_config.log_auth_goodpass = true;
+		do_stdout:
+				fr_log_fp = stdout;
+				default_log.dst = L_DST_STDOUT;
+				default_log.fd = STDOUT_FILENO;
+				break;
+
+			case 'x':
+				rad_debug_lvl++;
+				break;
+
+			default:
+				usage(1);
+				break;
+		}
+	}
+
+	/*
+	 *  Mismatch between the binary and the libraries it depends on.
+	 */
+	if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) {
+		fr_perror("%s", main_config.name);
+		exit(EXIT_FAILURE);
+	}
+
+	if (rad_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) exit(EXIT_FAILURE);
+
+	/*
+	 *  Mismatch between build time OpenSSL and linked SSL, better to die
+	 *  here than segfault later.
+	 */
+#ifdef HAVE_OPENSSL_CRYPTO_H
+	if (ssl_check_consistency() < 0) exit(EXIT_FAILURE);
+#endif
+
+	if (flag && (flag != 0x03)) {
+		fprintf(stderr, "%s: The options -i and -p cannot be used individually.\n",
+			main_config.name);
+		exit(EXIT_FAILURE);
+	}
+
+	/*
+	 *  According to the talloc peeps, no two threads may modify any part of
+	 *  a ctx tree with a common root without synchronisation.
+	 *
+	 *  So we can't run with a null context and threads.
+	 */
+	if (main_config.memory_report) {
+		if (spawn_flag) {
+			fprintf(stderr, "%s: The server cannot produce memory reports (-M) in threaded mode\n",
+				main_config.name);
+			exit(EXIT_FAILURE);
+		}
+		talloc_enable_null_tracking();
+	} else {
+		talloc_disable_null_tracking();
+	}
+
+	/*
+	 *  Better here, so it doesn't matter whether we get passed -xv or -vx.
+	 */
+	if (display_version) {
+		if (rad_debug_lvl == 0) rad_debug_lvl = 1;
+		fr_log_fp = stdout;
+		default_log.dst = L_DST_STDOUT;
+		default_log.fd = STDOUT_FILENO;
+
+		INFO("%s: %s", main_config.name, radiusd_version);
+		version_print();
+		exit(EXIT_SUCCESS);
+	}
+
+	if (rad_debug_lvl) version_print();
+
+	/*
+	 *  Under linux CAP_SYS_PTRACE is usually only available before setuid/setguid,
+	 *  so we need to check whether we can attach before calling those functions
+	 *  (in main_config_init()).
+	 */
+	fr_store_debug_state();
+
+	/*
+	 *  Initialising OpenSSL once, here, is safer than having individual modules do it.
+	 */
+#ifdef HAVE_OPENSSL_CRYPTO_H
+	if (tls_global_init(spawn_flag, check_config) < 0) exit(EXIT_FAILURE);
+#endif
+
+	/*
+	 *  Write the PID always if we're running as a daemon.
+	 */
+	if (main_config.daemonize) main_config.write_pid = true;
+
+	/*
+	 *  Read the configuration files, BEFORE doing anything else.
+	 */
+	if (main_config_init() < 0) exit(EXIT_FAILURE);
+
+	/*
+	 *  This is very useful in figuring out why the panic_action didn't fire.
+	 */
+	INFO("%s", fr_debug_state_to_msg(fr_debug_state));
+
+	/*
+	 *  Check for vulnerabilities in the version of libssl were linked against.
+	 */
+#if defined(HAVE_OPENSSL_CRYPTO_H) && defined(ENABLE_OPENSSL_VERSION_CHECK)
+	if (tls_global_version_check(main_config.allow_vulnerable_openssl) < 0) exit(EXIT_FAILURE);
+#endif
+
+	fr_talloc_fault_setup();
+
+	/*
+	 *  Set the panic action (if required)
+	 */
+	{
+		char const *panic_action = NULL;
+
+		panic_action = getenv("PANIC_ACTION");
+		if (!panic_action) panic_action = main_config.panic_action;
+
+		if (panic_action && (fr_fault_setup(panic_action, argv[0]) < 0)) {
+			fr_perror("%s", main_config.name);
+			exit(EXIT_FAILURE);
+		}
+	}
+
+	/*
+	 *  The systemd watchdog enablement must be checked before we
+	 *  daemonize, but the notifications can come from any process.
+	 */
+#ifdef HAVE_SYSTEMD_WATCHDOG
+	if (!check_config) {
+		uint64_t usec;
+
+		if ((sd_watchdog_enabled(0, &usec) > 0) && (usec > 0)) {
+			usec /= 2;
+			fr_timeval_from_usec(&sd_watchdog_interval, usec);
+
+			INFO("systemd watchdog interval is %ld.%.2ld secs", sd_watchdog_interval.tv_sec, sd_watchdog_interval.tv_usec);
+		} else {
+			INFO("systemd watchdog is disabled");
+		}
+	}
+#else
+	/*
+	 *	Some users get frustrated due to can't handle the service using "systemctl start radiusd"
+	 *	even when the SO supports systemd. The reason is because the FreeRADIUS version was built
+	 *	without the proper support.
+	 *
+	 *	Then, as can be seen in https://www.systutorials.com/docs/linux/man/3-sd_notify/
+	 *	We could assume that if find the NOTIFY_SOCKET, it's because we are under systemd.
+	 *
+	 */
+	if (getenv("NOTIFY_SOCKET"))
+		WARN("Built without support for systemd watchdog, but running under systemd.");
+#endif
+
+#ifndef __MINGW32__
+	/*
+	 *  Disconnect from session
+	 */
+	if (main_config.daemonize) {
+		pid_t pid;
+		int devnull;
+
+		/*
+		 *  Really weird things happen if we leave stdin open and call things like
+		 *  system() later.
+		 */
+		devnull = open("/dev/null", O_RDWR);
+		if (devnull < 0) {
+			ERROR("Failed opening /dev/null: %s", fr_syserror(errno));
+			exit(EXIT_FAILURE);
+		}
+		dup2(devnull, STDIN_FILENO);
+
+		close(devnull);
+
+		if (pipe(from_child) != 0) {
+			ERROR("Couldn't open pipe for child status: %s", fr_syserror(errno));
+			exit(EXIT_FAILURE);
+		}
+
+		pid = fork();
+		if (pid < 0) {
+			ERROR("Couldn't fork: %s", fr_syserror(errno));
+			exit(EXIT_FAILURE);
+		}
+
+		/*
+		 *  The parent exits, so the child can run in the background.
+		 *
+		 *  As the child can still encounter an error during initialisation
+		 *  we do a blocking read on a pipe between it and the parent.
+		 *
+		 *  Just before entering the event loop the child will send a success
+		 *  or failure message to the parent, via the pipe.
+		 */
+		if (pid > 0) {
+			uint8_t ret = 0;
+			int stat_loc;
+
+			/* So the pipe is correctly widowed if the child exits */
+			close(from_child[1]);
+
+			/*
+			 *  The child writes a 0x01 byte on success, and closes
+			 *  the pipe on error.
+			 */
+			if ((read(from_child[0], &ret, 1) < 0)) {
+				ret = 0;
+			}
+
+			/* For cleanliness... */
+			close(from_child[0]);
+
+			/* Don't turn children into zombies */
+			if (!ret) {
+				waitpid(pid, &stat_loc, WNOHANG);
+				exit(EXIT_FAILURE);
+			}
+
+#ifdef HAVE_SYSTEMD
+			/*
+			 *	Update the systemd MAINPID to be our child,
+			 *	as the parent is about to exit.
+			 */
+			sd_notifyf(0, "MAINPID=%lu", (unsigned long)pid);
+#endif
+
+			exit(EXIT_SUCCESS);
+		}
+
+		/* so the pipe is correctly widowed if the parent exits?! */
+		close(from_child[0]);
+#  ifdef HAVE_SETSID
+		setsid();
+#  endif
+	}
+#endif
+
+	/*
+	 *  Ensure that we're using the CORRECT pid after forking, NOT the one
+	 *  we started with.
+	 */
+	radius_pid = getpid();
+
+	/*
+	 *  Initialize any event loops just enough so module instantiations can
+	 *  add fd/event to them, but do not start them yet.
+	 *
+	 *  This has to be done post-fork in case we're using kqueue, where the
+	 *  queue isn't inherited by the child process.
+	 */
+	if (!radius_event_init(autofree)) exit(EXIT_FAILURE);
+
+	/*
+	 *   Load the modules
+	 */
+	if (modules_init(main_config.config) < 0) exit(EXIT_FAILURE);
+
+	/*
+	 *  Redirect stderr/stdout as appropriate.
+	 */
+	if (radlog_init(&default_log, main_config.daemonize) < 0) {
+		ERROR("%s", fr_strerror());
+		exit(EXIT_FAILURE);
+	}
+
+	event_loop_started = true;
+
+	/*
+	 *  Start the event loop(s) and threads.
+	 */
+	radius_event_start(main_config.config, spawn_flag);
+
+	/*
+	 *  Now that we've set everything up, we can install the signal
+	 *  handlers.  Before this, if we get any signal, we don't know
+	 *  what to do, so we might as well do the default, and die.
+	 */
+#ifdef SIGPIPE
+	signal(SIGPIPE, SIG_IGN);
+#endif
+
+	if ((fr_set_signal(SIGHUP, sig_hup) < 0) ||
+	    (fr_set_signal(SIGTERM, sig_fatal) < 0)) {
+	set_signal_error:
+		ERROR("%s", fr_strerror());
+		exit(EXIT_FAILURE);
+	}
+
+	/*
+	 *  If we're debugging, then a CTRL-C will cause the server to die
+	 *  immediately.  Use SIGTERM to shut down the server cleanly in
+	 *  that case.
+	 */
+	if (fr_set_signal(SIGINT, sig_fatal) < 0) goto set_signal_error;
+
+#ifdef SIGQUIT
+	if (main_config.debug_memory || (rad_debug_lvl == 0)) {
+		if (fr_set_signal(SIGQUIT, sig_fatal) < 0) goto set_signal_error;
+	}
+#endif
+
+	/*
+	 *  Everything seems to have loaded OK, exit gracefully.
+	 */
+	if (check_config) {
+		DEBUG("Configuration appears to be OK");
+
+		/* for -C -m|-M */
+		if (main_config.debug_memory) goto cleanup;
+
+		exit(EXIT_SUCCESS);
+	}
+
+#ifdef WITH_STATS
+	radius_stats_init(0);
+#endif
+
+	/*
+	 *  Write the PID after we've forked, so that we write the correct one.
+	 */
+	if (main_config.write_pid) {
+		FILE *fp;
+
+		fp = fopen(main_config.pid_file, "w");
+		if (fp != NULL) {
+			/*
+			 *  @fixme What about following symlinks,
+			 *  and having it over-write a normal file?
+			 */
+			fprintf(fp, "%d\n", (int) radius_pid);
+			fclose(fp);
+		} else {
+			ERROR("Failed creating PID file %s: %s", main_config.pid_file, fr_syserror(errno));
+			exit(EXIT_FAILURE);
+		}
+	}
+
+	exec_trigger(NULL, NULL, "server.start", false);
+
+	/*
+	 *  Inform the parent (who should still be waiting) that the rest of
+	 *  initialisation went OK, and that it should exit with a 0 status.
+	 *  If we don't get this far, then we just close the pipe on exit, and the
+	 *  parent gets a read failure.
+	 */
+	if (main_config.daemonize) {
+		if (write(from_child[1], "\001", 1) < 0) {
+			WARN("Failed informing parent of successful start: %s",
+			     fr_syserror(errno));
+		}
+		close(from_child[1]);
+	}
+
+	/*
+	 *  Clear the libfreeradius error buffer.
+	 */
+	fr_strerror();
+
+	/*
+	 *  Initialise the state rbtree (used to link multiple rounds of challenges).
+	 */
+	state = fr_state_init(NULL);
+
+#ifdef HAVE_SYSTEMD
+	{
+		int ret_notif;
+
+		ret_notif = sd_notify(0, "READY=1\nSTATUS=Processing requests");
+		if (ret_notif < 0)
+			WARN("Failed notifying systemd that process is READY: %s", fr_syserror(ret_notif));
+	}
+#endif
+
+	/*
+	 *  Process requests until HUP or exit.
+	 */
+	while ((status = radius_event_process()) == 0x80) {
+#ifdef WITH_STATS
+		radius_stats_init(1);
+#endif
+		main_config_hup();
+	}
+	if (status < 0) {
+		ERROR("Exiting due to internal error: %s", fr_strerror());
+		rcode = EXIT_FAILURE;
+	} else {
+		INFO("Exiting normally");
+	}
+
+	/*
+	 *  Ignore the TERM signal: we're about to die.
+	 */
+	signal(SIGTERM, SIG_IGN);
+
+	/*
+	 *   Fire signal and stop triggers after ignoring SIGTERM, so handlers are
+	 *   not killed with the rest of the process group, below.
+	 */
+	if (status == 2) exec_trigger(NULL, NULL, "server.signal.term", true);
+	exec_trigger(NULL, NULL, "server.stop", false);
+
+	/*
+	 *  Send a TERM signal to all associated processes
+	 *  (including us, which gets ignored.)
+	 */
+#ifndef __MINGW32__
+	if (spawn_flag) kill(-radius_pid, SIGTERM);
+#endif
+
+	/*
+	 *  We're exiting, so we can delete the PID file.
+	 *  (If it doesn't exist, we can ignore the error returned by unlink)
+	 */
+	if (main_config.daemonize) unlink(main_config.pid_file);
+
+	radius_event_free();
+
+cleanup:
+	/*
+	 *  Detach any modules.
+	 */
+	modules_free();
+
+	xlat_free();		/* modules may have xlat's */
+
+	fr_state_delete(state);
+
+	/*
+	 *  Free the configuration items.
+	 */
+	main_config_free();
+
+#ifdef WITH_COA_TUNNEL
+	/*
+	 *	This should be after freeing all of the listeners.
+	 */
+	listen_coa_free();
+#endif
+
+#ifdef WIN32
+	WSACleanup();
+#endif
+
+#ifdef HAVE_OPENSSL_CRYPTO_H
+	tls_global_cleanup();
+#endif
+
+	/*
+	 *  So we don't see autofreed memory in the talloc report
+	 */
+	talloc_free(autofree);
+
+	if (main_config.memory_report) {
+		INFO("Allocated memory at time of report:");
+		fr_log_talloc_report(NULL);
+		talloc_disable_null_tracking();
+	}
+
+	return rcode;
+}
+
+
+/*
+ *  Display the syntax for starting this program.
+ */
+static void NEVER_RETURNS usage(int status)
+{
+	FILE *output = status?stderr:stdout;
+
+	fprintf(output, "Usage: %s [options]\n", main_config.name);
+	fprintf(output, "Options:\n");
+	fprintf(output, "  -C            Check configuration and exit.\n");
+	fprintf(stderr, "  -d <raddb>    Set configuration directory (defaults to " RADDBDIR ").\n");
+	fprintf(stderr, "  -D <dictdir>  Set main dictionary directory (defaults to " DICTDIR ").\n");
+	fprintf(output, "  -f            Run as a foreground process, not a daemon.\n");
+	fprintf(output, "  -h            Print this help message.\n");
+	fprintf(output, "  -i <ipaddr>   Listen on ipaddr ONLY.\n");
+	fprintf(output, "  -l <log_file> Logging output will be written to this file.\n");
+	fprintf(output, "  -m            On SIGINT or SIGQUIT clean up all used memory instead of just exiting.\n");
+	fprintf(output, "  -n <name>     Read raddb/name.conf instead of raddb/radiusd.conf.\n");
+	fprintf(output, "  -p <port>     Listen on port ONLY.\n");
+	fprintf(output, "  -P            Always write out PID, even with -f.\n");
+	fprintf(output, "  -s            Do not spawn child processes to handle requests (same as -ft).\n");
+	fprintf(output, "  -t            Disable threads.\n");
+	fprintf(output, "  -v            Print server version information.\n");
+	fprintf(output, "  -X            Turn on full debugging (similar to -tfxxl stdout).\n");
+	fprintf(output, "  -x            Turn on additional debugging (-xx gives more debugging).\n");
+	exit(status);
+}
+
+
+/*
+ *	We got a fatal signal.
+ */
+static void sig_fatal(int sig)
+{
+	if (getpid() != radius_pid) _exit(sig);
+
+	switch (sig) {
+	case SIGTERM:
+		radius_signal_self(RADIUS_SIGNAL_SELF_TERM);
+		break;
+
+	case SIGINT:
+#ifdef SIGQUIT
+	case SIGQUIT:
+#endif
+		if (main_config.debug_memory || main_config.memory_report) {
+			radius_signal_self(RADIUS_SIGNAL_SELF_TERM);
+			break;
+		}
+		/* FALL-THROUGH */
+
+	default:
+		fr_exit(sig);
+	}
+}
+
+#ifdef SIGHUP
+/*
+ *  We got the hangup signal.
+ *  Re-read the configuration files.
+ */
+static void sig_hup(UNUSED int sig)
+{
+	reset_signal(SIGHUP, sig_hup);
+
+	radius_signal_self(RADIUS_SIGNAL_SELF_HUP);
+}
+#endif
diff --git a/src/main/radiusd.mk b/src/main/radiusd.mk
new file mode 100644
index 0000000..651413a
--- /dev/null
+++ b/src/main/radiusd.mk
@@ -0,0 +1,21 @@
+TARGET	:= radiusd
+SOURCES := acct.c auth.c client.c crypt.c files.c \
+		  listen.c  mainconfig.c modules.c modcall.c \
+		  radiusd.c state.c stats.c soh.c connection.c \
+		  session.c threads.c channel.c \
+		  process.c realms.c detail.c
+ifneq ($(OPENSSL_LIBS),)
+SOURCES	+= cb.c tls.c tls_listen.c
+endif
+
+SRC_CFLAGS	:= -DHOSTINFO=\"${HOSTINFO}\"
+
+TGT_INSTALLDIR  := ${sbindir}
+TGT_LDLIBS	:= $(LIBS) $(OPENSSL_LIBS) $(SYSTEMD_LIBS) $(LCRYPT)
+TGT_PREREQS	:= libfreeradius-server.a libfreeradius-radius.a
+
+# Libraries can't depend on libraries (oops), so make the binary
+# depend on the EAP code...
+ifneq "$(filter rlm_eap_%,${ALL_TGTS})" ""
+TGT_PREREQS	+= libfreeradius-eap.a
+endif
diff --git a/src/main/radlast.in b/src/main/radlast.in
new file mode 100755
index 0000000..69867bb
--- /dev/null
+++ b/src/main/radlast.in
@@ -0,0 +1,7 @@
+#! /bin/sh
+
+prefix=@prefix@
+localstatedir=@localstatedir@
+logdir=@logdir@
+
+exec last -f $logdir/radwtmp "$@"
diff --git a/src/main/radlast.mk b/src/main/radlast.mk
new file mode 100644
index 0000000..766ce1f
--- /dev/null
+++ b/src/main/radlast.mk
@@ -0,0 +1,5 @@
+install: $(R)$(bindir)/radlast
+
+$(R)$(bindir)/radlast: src/main/radlast | $(R)$(bindir)
+	@echo INSTALL $(notdir $<)
+	@$(INSTALL) -m 755 $< $(R)$(bindir)
diff --git a/src/main/radmin.c b/src/main/radmin.c
new file mode 100644
index 0000000..badc186
--- /dev/null
+++ b/src/main/radmin.c
@@ -0,0 +1,773 @@
+/*
+ * radmin.c	RADIUS Administration tool.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2012   The FreeRADIUS server project
+ * Copyright 2012   Alan DeKok <aland@deployingradius.com>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/md5.h>
+#include <freeradius-devel/channel.h>
+
+#include <pwd.h>
+#include <grp.h>
+
+#ifdef HAVE_GETOPT_H
+#  include <getopt.h>
+#endif
+
+#ifdef HAVE_SYS_STAT_H
+#  include <sys/stat.h>
+#endif
+
+#ifndef READLINE_MAX_HISTORY_LINES
+#	define READLINE_MAX_HISTORY_LINES 1000
+#endif
+
+#ifdef HAVE_LIBREADLINE
+
+# include <stdio.h>
+#if defined(HAVE_READLINE_READLINE_H)
+#  include <readline/readline.h>
+#  define USE_READLINE (1)
+#elif defined(HAVE_READLINE_H)
+#  include <readline.h>
+#  define USE_READLINE (1)
+#endif /* !defined(HAVE_READLINE_H) */
+
+#ifdef HAVE_READLINE_HISTORY
+#  if defined(HAVE_READLINE_HISTORY_H)
+#    include <readline/history.h>
+#    define USE_READLINE_HISTORY (1)
+#  elif defined(HAVE_HISTORY_H)
+#    include <history.h>
+#    define USE_READLINE_HISTORY (1)
+#endif /* defined(HAVE_READLINE_HISTORY_H) */
+
+#endif /* HAVE_READLINE_HISTORY */
+
+#endif /* HAVE_LIBREADLINE */
+
+/*
+ *	For configuration file stuff.
+ */
+static char const *progname = "radmin";
+static char const *radmin_version = "radmin version " RADIUSD_VERSION_STRING
+#ifdef RADIUSD_VERSION_COMMIT
+" (git #" STRINGIFY(RADIUSD_VERSION_COMMIT) ")"
+#endif
+#ifndef ENABLE_REPRODUCIBLE_BUILDS
+", built on " __DATE__ " at " __TIME__
+#endif
+;
+
+
+/*
+ *	The rest of this is because the conffile.c, etc. assume
+ *	they're running inside of the server.  And we don't (yet)
+ *	have a "libfreeradius-server", or "libfreeradius-util".
+ */
+main_config_t main_config;
+
+static bool echo = false;
+static char const *secret = "testing123";
+
+#include <sys/wait.h>
+
+#ifdef HAVE_PTHREAD_H
+pid_t rad_fork(void)
+{
+	return fork();
+}
+pid_t rad_waitpid(pid_t pid, int *status)
+{
+	return waitpid(pid, status, 0);
+}
+#endif
+
+static void NEVER_RETURNS usage(int status)
+{
+	FILE *output = status ? stderr : stdout;
+	fprintf(output, "Usage: %s [ args ]\n", progname);
+	fprintf(output, "  -d raddb_dir    Configuration files are in \"raddbdir/*\".\n");
+	fprintf(output, "  -D <dictdir>    Set main dictionary directory (defaults to " DICTDIR ").\n");
+	fprintf(output, "  -e command      Execute 'command' and then exit.\n");
+	fprintf(output, "  -E              Echo commands as they are being executed.\n");
+	fprintf(output, "  -f socket_file  Open socket_file directly, without reading radius.conf\n");
+	fprintf(output, "  -h              Print usage help information.\n");
+	fprintf(output, "  -i input_file   Read commands from 'input_file'.\n");
+	fprintf(output, "  -n name         Read raddb/name.conf instead of raddb/radiusd.conf\n");
+	fprintf(output, "  -q              Quiet mode.\n");
+	fprintf(output, "  -v              Show program version information.\n");
+
+	exit(status);
+}
+
+static int client_socket(char const *server)
+{
+	int sockfd;
+	uint16_t port;
+	fr_ipaddr_t ipaddr;
+	char *p, buffer[1024];
+
+	strlcpy(buffer, server, sizeof(buffer));
+
+	p = strchr(buffer, ':');
+	if (!p) {
+		port = PW_RADMIN_PORT;
+	} else {
+		port = atoi(p + 1);
+		*p = '\0';
+	}
+
+	if (ip_hton(&ipaddr, AF_INET, buffer, false) < 0) {
+		fprintf(stderr, "%s: Failed looking up host %s: %s\n",
+			progname, buffer, fr_syserror(errno));
+		exit(1);
+	}
+
+	sockfd = fr_socket_client_tcp(NULL, &ipaddr, port, false);
+	if (sockfd < 0) {
+		fprintf(stderr, "%s: Failed opening socket %s: %s\n",
+			progname, server, fr_syserror(errno));
+		exit(1);
+	}
+
+	return sockfd;
+}
+
+static ssize_t do_challenge(int sockfd)
+{
+	ssize_t r;
+	fr_channel_type_t channel;
+	uint8_t challenge[16];
+
+	challenge[0] = 0;
+
+	/*
+	 *	When connecting over a socket, the server challenges us.
+	 */
+	r = fr_channel_read(sockfd, &channel, challenge, sizeof(challenge));
+	if (r <= 0) return r;
+
+	if ((r != 16) || (channel != FR_CHANNEL_AUTH_CHALLENGE)) {
+		fprintf(stderr, "%s: Failed to read challenge.\n",
+			progname);
+		exit(1);
+	}
+
+	fr_hmac_md5(challenge, (uint8_t const *) secret, strlen(secret),
+		    challenge, sizeof(challenge));
+
+	r = fr_channel_write(sockfd, FR_CHANNEL_AUTH_RESPONSE, challenge, sizeof(challenge));
+	if (r <= 0) return r;
+
+	/*
+	 *	If the server doesn't like us, he just closes the
+	 *	socket.  So we don't look for an ACK.
+	 */
+
+	return r;
+}
+
+
+/*
+ *	Returns -1 on error.  0 on connection failed.  +1 on OK.
+ */
+static ssize_t run_command(int sockfd, char const *command,
+			   char *buffer, size_t bufsize)
+{
+	ssize_t r;
+	uint32_t status;
+	fr_channel_type_t channel;
+
+	if (echo) {
+		fprintf(stdout, "%s\n", command);
+	}
+
+	/*
+	 *	Write the text to the socket.
+	 */
+	r = fr_channel_write(sockfd, FR_CHANNEL_STDIN, command, strlen(command));
+	if (r <= 0) return r;
+
+	while (true) {
+		r = fr_channel_read(sockfd, &channel, buffer, bufsize - 1);
+		if (r <= 0) return r;
+
+		buffer[r] = '\0';	/* for C strings */
+
+		switch (channel) {
+		case FR_CHANNEL_STDOUT:
+			fprintf(stdout, "%s", buffer);
+			break;
+
+		case FR_CHANNEL_STDERR:
+			fprintf(stderr, "ERROR: %s", buffer);
+			break;
+
+		case FR_CHANNEL_CMD_STATUS:
+			if (r < 4) return 1;
+
+			memcpy(&status, buffer, sizeof(status));
+			status = ntohl(status);
+			return status;
+
+		default:
+			fprintf(stderr, "Unexpected response\n");
+			return -1;
+		}
+	}
+
+	/* never gets here */
+}
+
+static int do_connect(int *out, char const *file, char const *server)
+{
+	int sockfd;
+	ssize_t r;
+	fr_channel_type_t channel;
+	char buffer[65536];
+
+	uint32_t magic;
+
+	/*
+	 *	Close stale file descriptors
+	 */
+	if (*out != -1) {
+		close(*out);
+		*out = -1;
+	}
+
+	if (file) {
+		/*
+		 *	FIXME: Get destination from command line, if possible?
+		 */
+		sockfd = fr_socket_client_unix(file, false);
+		if (sockfd < 0) {
+			fr_perror("radmin");
+			if (errno == ENOENT) {
+					fprintf(stderr, "Perhaps you need to run the commands:");
+					fprintf(stderr, "\tcd /etc/raddb\n");
+					fprintf(stderr, "\tln -s sites-available/control-socket "
+						"sites-enabled/control-socket\n");
+					fprintf(stderr, "and then re-start the server?\n");
+			}
+			return -1;
+		}
+	} else {
+		sockfd = client_socket(server);
+	}
+
+	/*
+	 *	Only works for BSD, but Linux allows us
+	 *	to mask SIGPIPE, so that's fine.
+	 */
+#ifdef SO_NOSIGPIPE
+	{
+		int set = 1;
+
+		setsockopt(sockfd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));
+	}
+#endif
+
+	/*
+	 *	Set up the initial header data.
+	 */
+	magic = 0xf7eead16;
+	magic = htonl(magic);
+	memcpy(buffer, &magic, sizeof(magic));
+	memset(buffer + sizeof(magic), 0, sizeof(magic));
+
+	r = fr_channel_write(sockfd, FR_CHANNEL_INIT_ACK, buffer, 8);
+	if (r <= 0) {
+	do_close:
+		fprintf(stderr, "%s: Error in socket: %s\n",
+			progname, fr_syserror(errno));
+		close(sockfd);
+			return -1;
+	}
+
+	r = fr_channel_read(sockfd, &channel, buffer + 8, 8);
+	if (r <= 0) goto do_close;
+
+	if ((r != 8) || (channel != FR_CHANNEL_INIT_ACK) ||
+	    (memcmp(buffer, buffer + 8, 8) != 0)) {
+		fprintf(stderr, "%s: Incompatible versions\n", progname);
+		close(sockfd);
+		return -1;
+	}
+
+	if (server && secret) {
+		r = do_challenge(sockfd);
+		if (r <= 0) goto do_close;
+	}
+
+	*out = sockfd;
+
+	return 0;
+}
+
+#define MAX_COMMANDS (4)
+
+int main(int argc, char **argv)
+{
+	int		argval;
+	bool		quiet = false;
+	int		sockfd = -1;
+	char		*line = NULL;
+	ssize_t		len;
+	char const	*file = NULL;
+	char const	*name = "radiusd";
+	char		*p, buffer[65536];
+	char const	*input_file = NULL;
+	FILE		*inputfp = stdin;
+	char const	*server = NULL;
+
+	char const	*radius_dir = RADIUS_DIR;
+	char const	*dict_dir = DICTDIR;
+#ifdef USE_READLINE_HISTORY
+	char 		history_file[PATH_MAX];
+#endif
+
+	char *commands[MAX_COMMANDS];
+	int num_commands = -1;
+
+	int exit_status = EXIT_SUCCESS;
+
+#ifndef NDEBUG
+	if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) {
+		fr_perror("radmin");
+		exit(EXIT_FAILURE);
+	}
+#endif
+
+	talloc_set_log_stderr();
+
+	if ((progname = strrchr(argv[0], FR_DIR_SEP)) == NULL) {
+		progname = argv[0];
+	} else {
+		progname++;
+	}
+
+	while ((argval = getopt(argc, argv, "d:D:hi:e:Ef:n:qs:vS")) != EOF) {
+		switch (argval) {
+		case 'd':
+			if (file) {
+				fprintf(stderr, "%s: -d and -f cannot be used together.\n", progname);
+				exit(1);
+			}
+			if (server) {
+				fprintf(stderr, "%s: -d and -s cannot be used together.\n", progname);
+				exit(1);
+			}
+			radius_dir = optarg;
+			break;
+
+		case 'D':
+			dict_dir = optarg;
+			break;
+
+		case 'e':
+			num_commands++; /* starts at -1 */
+			if (num_commands >= MAX_COMMANDS) {
+				fprintf(stderr, "%s: Too many '-e'\n",
+					progname);
+				exit(1);
+			}
+
+			commands[num_commands] = optarg;
+			break;
+
+		case 'E':
+			echo = true;
+			break;
+
+		case 'f':
+			radius_dir = NULL;
+			file = optarg;
+			break;
+
+		default:
+		case 'h':
+			usage(0);	/* never returns */
+
+		case 'i':
+#ifdef __clang_analyzer__
+			if (!optarg) exit(1);
+#endif
+
+			if (strcmp(optarg, "-") != 0) {
+				input_file = optarg;
+			}
+			quiet = true;
+			break;
+
+		case 'n':
+			name = optarg;
+			break;
+
+		case 'q':
+			quiet = true;
+			break;
+
+		case 's':
+			if (file) {
+				fprintf(stderr, "%s: -s and -f cannot be used together.\n", progname);
+				usage(1);
+			}
+			radius_dir = NULL;
+			server = optarg;
+			break;
+
+		case 'S':
+			secret = NULL;
+			break;
+
+		case 'v':
+			printf("%s\n", radmin_version);
+			exit(EXIT_SUCCESS);
+		}
+	}
+
+	/*
+	 *	Mismatch between the binary and the libraries it depends on
+	 */
+	if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) {
+		fr_perror("radmin");
+		exit(1);
+	}
+
+	if (radius_dir) {
+		int rcode;
+		CONF_SECTION *cs, *subcs;
+		uid_t		uid;
+		gid_t		gid;
+		char const	*uid_name = NULL;
+		char const	*gid_name = NULL;
+		struct passwd	*pwd;
+		struct group	*grp;
+
+		file = NULL;	/* MUST read it from the conffile now */
+
+		snprintf(buffer, sizeof(buffer), "%s/%s.conf", radius_dir, name);
+
+		/*
+		 *	Need to read in the dictionaries, else we may get
+		 *	validation errors when we try and parse the config.
+		 */
+		if (dict_init(dict_dir, RADIUS_DICTIONARY) < 0) {
+			fr_perror("radmin");
+			exit(64);
+		}
+
+		if (dict_read(radius_dir, RADIUS_DICTIONARY) == -1) {
+			fr_perror("radmin");
+			exit(64);
+		}
+
+		cs = cf_section_alloc(NULL, "main", NULL);
+		if (!cs) exit(1);
+
+		if (cf_file_read(cs, buffer) < 0) {
+			fprintf(stderr, "%s: Errors reading or parsing %s\n", progname, buffer);
+			talloc_free(cs);
+			usage(1);
+		}
+
+		uid = getuid();
+		gid = getgid();
+
+		subcs = NULL;
+		while ((subcs = cf_subsection_find_next(cs, subcs, "listen")) != NULL) {
+			char const *value;
+			CONF_PAIR *cp = cf_pair_find(subcs, "type");
+
+			if (!cp) continue;
+
+			value = cf_pair_value(cp);
+			if (!value) continue;
+
+			if (strcmp(value, "control") != 0) continue;
+
+			/*
+			 *	Now find the socket name (sigh)
+			 */
+			rcode = cf_item_parse(subcs, "socket", FR_ITEM_POINTER(PW_TYPE_STRING, &file), NULL);
+			if (rcode < 0) {
+				fprintf(stderr, "%s: Failed parsing listen section 'socket'\n", progname);
+				exit(1);
+			}
+
+			if (!file) {
+				fprintf(stderr, "%s: No path given for socket\n", progname);
+				usage(1);
+			}
+
+			/*
+			 *	If we're root, just use the first one we find
+			 */
+			if (uid == 0) break;
+
+			/*
+			 *	Check UID and GID.
+			 */
+			rcode = cf_item_parse(subcs, "uid", FR_ITEM_POINTER(PW_TYPE_STRING, &uid_name), NULL);
+			if (rcode < 0) {
+				fprintf(stderr, "%s: Failed parsing listen section 'uid'\n", progname);
+				exit(1);
+			}
+
+			if (!uid_name) break;
+
+			pwd = getpwnam(uid_name);
+			if (!pwd) {
+				fprintf(stderr, "%s: Failed getting UID for user %s: %s\n", progname, uid_name, strerror(errno));
+				exit(1);
+			}
+
+			if (uid != pwd->pw_uid) continue;
+
+			rcode = cf_item_parse(subcs, "gid", FR_ITEM_POINTER(PW_TYPE_STRING, &gid_name), NULL);
+			if (rcode < 0) {
+				fprintf(stderr, "%s: Failed parsing listen section 'gid'\n", progname);
+				exit(1);
+			}
+
+			if (!gid_name) break;
+
+			grp = getgrnam(gid_name);
+			if (!grp) {
+				fprintf(stderr, "%s: Failed getting GID for group %s: %s\n", progname, gid_name, strerror(errno));
+				exit(1);
+			}
+
+			if (gid != grp->gr_gid) continue;
+
+			break;
+		}
+
+		if (!file) {
+			fprintf(stderr, "%s: Could not find control socket in %s\n", progname, buffer);
+			exit(1);
+		}
+	}
+
+	if (input_file) {
+		inputfp = fopen(input_file, "r");
+		if (!inputfp) {
+			fprintf(stderr, "%s: Failed opening %s: %s\n", progname, input_file, fr_syserror(errno));
+			exit(1);
+		}
+	}
+
+	if (!file && !server) {
+		fprintf(stderr, "%s: Must use one of '-d' or '-f' or '-s'\n",
+			progname);
+		exit(1);
+	}
+
+	/*
+	 *	Check if stdin is a TTY only if input is from stdin
+	 */
+	if (input_file && !quiet && !isatty(STDIN_FILENO)) quiet = true;
+
+#ifdef USE_READLINE
+	if (!quiet) {
+#ifdef USE_READLINE_HISTORY
+		using_history();
+		stifle_history(READLINE_MAX_HISTORY_LINES);
+
+		snprintf(history_file, sizeof(history_file), "%s/%s", getenv("HOME"), ".radmin_history");
+		read_history(history_file);
+#endif
+		rl_bind_key('\t', rl_insert);
+	}
+#endif
+
+	/*
+	 *	Prevent SIGPIPEs from terminating the process
+	 */
+	signal(SIGPIPE, SIG_IGN);
+
+	if (do_connect(&sockfd, file, server) < 0) exit(1);
+
+	/*
+	 *	Run one command.
+	 */
+	if (num_commands >= 0) {
+		int i;
+
+		for (i = 0; i <= num_commands; i++) {
+			len = run_command(sockfd, commands[i], buffer, sizeof(buffer));
+			if (len < 0) exit(1);
+
+			if (len == FR_CHANNEL_FAIL) exit_status = EXIT_FAILURE;
+		}
+		exit(exit_status);
+	}
+
+	if (!quiet) {
+		printf("%s - FreeRADIUS Server administration tool.\n", radmin_version);
+		printf("Copyright (C) 2008-2019 The FreeRADIUS server project and contributors.\n");
+		printf("There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A\n");
+		printf("PARTICULAR PURPOSE.\n");
+		printf("You may redistribute copies of FreeRADIUS under the terms of the\n");
+		printf("GNU General Public License v2.\n");
+	}
+
+	/*
+	 *	FIXME: Do login?
+	 */
+
+	while (1) {
+		int retries;
+
+#ifndef USE_READLINE
+		if (!quiet) {
+			printf("radmin> ");
+			fflush(stdout);
+		}
+#else
+		if (!quiet) {
+			line = readline("radmin> ");
+
+			if (!line) break;
+
+			if (!*line) {
+				free(line);
+				continue;
+			}
+
+#ifdef USE_READLINE_HISTORY
+			add_history(line);
+			write_history(history_file);
+#endif
+		} else		/* quiet, or no readline */
+#endif
+		{
+			line = fgets(buffer, sizeof(buffer), inputfp);
+			if (!line) break;
+
+			p = strchr(buffer, '\n');
+			if (!p) {
+				fprintf(stderr, "%s: Input line too long\n",
+					progname);
+				exit(1);
+			}
+
+			*p = '\0';
+
+			/*
+			 *	Strip off leading spaces.
+			 */
+			for (p = line; *p != '\0'; p++) {
+				if ((p[0] == ' ') ||
+				    (p[0] == '\t')) {
+					line = p + 1;
+					continue;
+				}
+
+				if (p[0] == '#') {
+					line = NULL;
+					break;
+				}
+
+				break;
+			}
+
+			/*
+			 *	Comments: keep going.
+			 */
+			if (!line) continue;
+
+			/*
+			 *	Strip off CR / LF
+			 */
+			for (p = line; *p != '\0'; p++) {
+				if ((p[0] == '\r') ||
+				    (p[0] == '\n')) {
+					p[0] = '\0';
+					break;
+				}
+			}
+		}
+
+		if (strcmp(line, "reconnect") == 0) {
+			if (do_connect(&sockfd, file, server) < 0) exit(1);
+			line = NULL;
+			continue;
+		}
+
+		if (strncmp(line, "secret ", 7) == 0) {
+			if (!secret) {
+				secret = line + 7;
+				do_challenge(sockfd);
+			}
+			line = NULL;
+			continue;
+		}
+
+		/*
+		 *	Exit, done, etc.
+		 */
+		if ((strcmp(line, "exit") == 0) ||
+		    (strcmp(line, "quit") == 0)) {
+			break;
+		}
+
+		if (server && !secret) {
+			fprintf(stderr, "ERROR: You must enter 'secret <SECRET>' before running any commands\n");
+			line = NULL;
+			continue;
+		}
+
+		retries = 0;
+	retry:
+		len = run_command(sockfd, line, buffer, sizeof(buffer));
+		if (len < 0) {
+			if (!quiet) fprintf(stderr, "... reconnecting ...\n");
+
+			if (do_connect(&sockfd, file, server) < 0) {
+				exit(1);
+			}
+
+			retries++;
+			if (retries < 2) goto retry;
+
+			fprintf(stderr, "Failed to connect to server\n");
+			exit(1);
+
+		} else if (len == FR_CHANNEL_SUCCESS) {
+			break;
+
+		} else if (len == FR_CHANNEL_FAIL) {
+			exit_status = EXIT_FAILURE;
+		}
+	}
+
+	fprintf(stdout, "\n");
+
+	if (inputfp != stdin) fclose(inputfp);
+
+	return exit_status;
+}
+
diff --git a/src/main/radmin.mk b/src/main/radmin.mk
new file mode 100644
index 0000000..9e58e6b
--- /dev/null
+++ b/src/main/radmin.mk
@@ -0,0 +1,7 @@
+TARGET 		:= radmin
+
+SOURCES		:= radmin.c channel.c
+
+TGT_INSTALLDIR  := ${sbindir}
+TGT_PREREQS	:= libfreeradius-server.a libfreeradius-radius.a
+TGT_LDLIBS	:= $(LIBS) $(LIBREADLINE)
diff --git a/src/main/radsniff.c b/src/main/radsniff.c
new file mode 100644
index 0000000..e0d2b65
--- /dev/null
+++ b/src/main/radsniff.c
@@ -0,0 +1,2683 @@
+/*
+ *   This program is is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or (at
+ *   your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ * @file radsniff.c
+ * @brief Capture, filter, and generate statistics for RADIUS traffic
+ *
+ * @copyright 2013 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
+ * @copyright 2006 The FreeRADIUS server project
+ * @copyright 2006 Nicolas Baradakis <nicolas.baradakis@cegetel.net>
+ */
+
+RCSID("$Id$")
+
+#define _LIBRADIUS 1
+#include <time.h>
+#include <math.h>
+#include <freeradius-devel/libradius.h>
+#include <freeradius-devel/event.h>
+
+#include <freeradius-devel/radpaths.h>
+#include <freeradius-devel/conf.h>
+#include <freeradius-devel/pcap.h>
+#include <freeradius-devel/radsniff.h>
+
+#ifdef HAVE_COLLECTDC_H
+#  include <collectd/client.h>
+#endif
+
+#define RS_ASSERT(_x) if (!(_x) && !fr_assert(_x)) exit(1)
+
+static rs_t *conf;
+static struct timeval start_pcap = {0, 0};
+static char timestr[50];
+
+static rbtree_t *request_tree = NULL;
+static rbtree_t *link_tree = NULL;
+static fr_event_list_t *events;
+static bool cleanup;
+
+static int self_pipe[2] = {-1, -1};		//!< Signals from sig handlers
+
+typedef int (*rbcmp)(void const *, void const *);
+
+static char const *radsniff_version = "radsniff version " RADIUSD_VERSION_STRING
+#ifdef RADIUSD_VERSION_COMMIT
+" (git #" STRINGIFY(RADIUSD_VERSION_COMMIT) ")"
+#endif
+#ifndef ENABLE_REPRODUCIBLE_BUILDS
+", built on " __DATE__ " at " __TIME__
+#endif
+;
+
+static int rs_useful_codes[] = {
+	PW_CODE_ACCESS_REQUEST,			//!< RFC2865 - Authentication request
+	PW_CODE_ACCESS_ACCEPT,			//!< RFC2865 - Access-Accept
+	PW_CODE_ACCESS_REJECT,			//!< RFC2865 - Access-Reject
+	PW_CODE_ACCOUNTING_REQUEST,		//!< RFC2866 - Accounting-Request
+	PW_CODE_ACCOUNTING_RESPONSE,		//!< RFC2866 - Accounting-Response
+	PW_CODE_ACCESS_CHALLENGE,		//!< RFC2865 - Access-Challenge
+	PW_CODE_STATUS_SERVER,			//!< RFC2865/RFC5997 - Status Server (request)
+	PW_CODE_STATUS_CLIENT,			//!< RFC2865/RFC5997 - Status Server (response)
+	PW_CODE_DISCONNECT_REQUEST,		//!< RFC3575/RFC5176 - Disconnect-Request
+	PW_CODE_DISCONNECT_ACK,			//!< RFC3575/RFC5176 - Disconnect-Ack (positive)
+	PW_CODE_DISCONNECT_NAK,			//!< RFC3575/RFC5176 - Disconnect-Nak (not willing to perform)
+	PW_CODE_COA_REQUEST,			//!< RFC3575/RFC5176 - CoA-Request
+	PW_CODE_COA_ACK,			//!< RFC3575/RFC5176 - CoA-Ack (positive)
+	PW_CODE_COA_NAK,			//!< RFC3575/RFC5176 - CoA-Nak (not willing to perform)
+};
+
+static const FR_NAME_NUMBER rs_events[] = {
+	{ "received",	RS_NORMAL	},
+	{ "norsp",	RS_LOST		},
+	{ "rtx",	RS_RTX		},
+	{ "noreq",	RS_UNLINKED	},
+	{ "reused",	RS_REUSED	},
+	{ "error",	RS_ERROR	},
+	{  NULL , -1 }
+};
+
+static void NEVER_RETURNS usage(int status);
+
+/** Fork and kill the parent process, writing out our PID
+ *
+ * @param pidfile the PID file to write our PID to
+ */
+static void rs_daemonize(char const *pidfile)
+{
+	FILE *fp;
+	pid_t pid, sid;
+
+	pid = fork();
+	if (pid < 0) {
+		exit(EXIT_FAILURE);
+	}
+
+	/*
+	 *	Kill the parent...
+	 */
+	if (pid > 0) {
+		close(self_pipe[0]);
+		close(self_pipe[1]);
+		exit(EXIT_SUCCESS);
+	}
+
+	/*
+	 *	Continue as the child.
+	 */
+
+	/* Create a new SID for the child process */
+	sid = setsid();
+	if (sid < 0) {
+		exit(EXIT_FAILURE);
+	}
+
+	/*
+	 *	Change the current working directory. This prevents the current
+	 *	directory from being locked; hence not being able to remove it.
+	 */
+	if ((chdir("/")) < 0) {
+		exit(EXIT_FAILURE);
+	}
+
+	/*
+	 *	And write it AFTER we've forked, so that we write the
+	 *	correct PID.
+	 */
+	fp = fopen(pidfile, "w");
+	if (fp != NULL) {
+		fprintf(fp, "%d\n", (int) sid);
+		fclose(fp);
+	} else {
+		ERROR("Failed creating PID file %s: %s", pidfile, fr_syserror(errno));
+		exit(EXIT_FAILURE);
+	}
+
+	/*
+	 *	Close stdout and stderr if they've not been redirected.
+	 */
+	if (isatty(fileno(stdout))) {
+		if (!freopen("/dev/null", "w", stdout)) {
+			exit(EXIT_FAILURE);
+		}
+	}
+
+	if (isatty(fileno(stderr))) {
+		if (!freopen("/dev/null", "w", stderr)) {
+			exit(EXIT_FAILURE);
+		}
+	}
+}
+
+#define USEC 1000000
+static void rs_tv_sub(struct timeval const *end, struct timeval const *start, struct timeval *elapsed)
+{
+	elapsed->tv_sec = end->tv_sec - start->tv_sec;
+	if (elapsed->tv_sec > 0) {
+		elapsed->tv_sec--;
+		elapsed->tv_usec = USEC;
+	} else {
+		elapsed->tv_usec = 0;
+	}
+	elapsed->tv_usec += end->tv_usec;
+	elapsed->tv_usec -= start->tv_usec;
+
+	if (elapsed->tv_usec >= USEC) {
+		elapsed->tv_usec -= USEC;
+		elapsed->tv_sec++;
+	}
+}
+
+static void rs_tv_add_ms(struct timeval const *start, unsigned long interval, struct timeval *result) {
+    result->tv_sec = start->tv_sec + (interval / 1000);
+    result->tv_usec = start->tv_usec + ((interval % 1000) * 1000);
+
+    if (result->tv_usec > USEC) {
+	result->tv_usec -= USEC;
+	result->tv_sec++;
+    }
+}
+
+static void rs_time_print(char *out, size_t len, struct timeval const *t)
+{
+	size_t ret;
+	struct timeval now;
+	uint32_t usec;
+
+	if (!t) {
+		gettimeofday(&now, NULL);
+		t = &now;
+	}
+
+	ret = strftime(out, len, "%Y-%m-%d %H:%M:%S", localtime(&t->tv_sec));
+	if (ret >= len) {
+		return;
+	}
+
+	usec = t->tv_usec;
+
+	if (usec) {
+		while (usec < 100000) usec *= 10;
+		snprintf(out + ret, len - ret, ".%i", usec);
+	} else {
+		snprintf(out + ret, len - ret, ".000000");
+	}
+}
+
+static size_t rs_prints_csv(char *out, size_t outlen, char const *in, size_t inlen)
+{
+	char const	*start = out;
+	uint8_t const	*str = (uint8_t const *) in;
+
+	if (!in) {
+		if (outlen) {
+			*out = '\0';
+		}
+
+		return 0;
+	}
+
+	if (inlen == 0) {
+		inlen = strlen(in);
+	}
+
+	while ((inlen > 0) && (outlen > 2)) {
+		/*
+		 *	Escape double quotes with... MORE DOUBLE QUOTES!
+		 */
+		if (*str == '"') {
+			*out++ = '"';
+			outlen--;
+		}
+
+		/*
+		 *	Safe chars which require no escaping
+		 */
+		if ((*str == '\r') || (*str == '\n') || ((*str >= '\x20') && (*str <= '\x7E'))) {
+			*out++ = *str++;
+			outlen--;
+			inlen--;
+
+			continue;
+		}
+
+		/*
+		 *	Everything else is dropped
+		 */
+		str++;
+		inlen--;
+	}
+	*out = '\0';
+
+	return out - start;
+}
+
+static void rs_packet_print_csv_header(void)
+{
+	char buffer[2048];
+	char *p = buffer;
+	int i;
+
+	ssize_t len, s = sizeof(buffer);
+
+	len = strlcpy(p, "\"Status\",\"Count\",\"Time\",\"Latency\",\"Type\",\"Interface\","
+		      "\"Src IP\",\"Src Port\",\"Dst IP\",\"Dst Port\",\"ID\",", s);
+	p += len;
+	s -= len;
+
+	if (s <= 0) return;
+
+	for (i = 0; i < conf->list_da_num; i++) {
+		char const *in;
+
+		*p++ = '"';
+		s -= 1;
+		if (s <= 0) return;
+
+		for (in = conf->list_da[i]->name; *in; in++) {
+			*p++ = *in;
+			s -= len;
+			if (s <= 0) return;
+		}
+
+		*p++ = '"';
+		s -= 1;
+		if (s <= 0) return;
+		*p++ = ',';
+		s -= 1;
+		if (s <= 0) return;
+	}
+
+	*--p = '\0';
+
+	fprintf(stdout , "%s\n", buffer);
+}
+
+static void rs_packet_print_csv(uint64_t count, rs_status_t status, fr_pcap_t *handle, RADIUS_PACKET *packet,
+				UNUSED struct timeval *elapsed, struct timeval *latency, UNUSED bool response,
+				bool body)
+{
+	char const *status_str;
+	char buffer[2048];
+	char *p = buffer;
+
+	char src[INET6_ADDRSTRLEN];
+	char dst[INET6_ADDRSTRLEN];
+
+	ssize_t len, s = sizeof(buffer);
+
+	inet_ntop(packet->src_ipaddr.af, &packet->src_ipaddr.ipaddr, src, sizeof(src));
+	inet_ntop(packet->dst_ipaddr.af, &packet->dst_ipaddr.ipaddr, dst, sizeof(dst));
+
+	status_str = fr_int2str(rs_events, status, NULL);
+	RS_ASSERT(status_str);
+
+	len = snprintf(p, s, "%s,%" PRIu64 ",%s,", status_str, count, timestr);
+	p += len;
+	s -= len;
+
+	if (s <= 0) return;
+
+	if (latency) {
+		len = snprintf(p, s, "%u.%03u,",
+			       (unsigned int) latency->tv_sec, ((unsigned int) latency->tv_usec / 1000));
+		p += len;
+		s -= len;
+	} else {
+		*p = ',';
+		p += 1;
+		s -= 1;
+	}
+
+	if (s <= 0) return;
+
+	/* Status, Type, Interface, Src, Src port, Dst, Dst port, ID */
+	if (is_radius_code(packet->code)) {
+		len = snprintf(p, s, "%s,%s,%s,%i,%s,%i,%i,", fr_packet_codes[packet->code], handle->name,
+			       src, packet->src_port, dst, packet->dst_port, packet->id);
+	} else {
+		len = snprintf(p, s, "%u,%s,%s,%i,%s,%i,%i,", packet->code, handle->name,
+			       src, packet->src_port, dst, packet->dst_port, packet->id);
+	}
+	p += len;
+	s -= len;
+
+	if (s <= 0) return;
+
+	if (body) {
+		int i;
+		VALUE_PAIR *vp;
+
+		for (i = 0; i < conf->list_da_num; i++) {
+			vp = fr_pair_find_by_da(packet->vps, conf->list_da[i], TAG_ANY);
+			if (vp && (vp->vp_length > 0)) {
+				if (conf->list_da[i]->type == PW_TYPE_STRING) {
+					*p++ = '"';
+					s--;
+					if (s <= 0) return;
+
+					len = rs_prints_csv(p, s, vp->vp_strvalue, vp->vp_length);
+					p += len;
+					s -= len;
+					if (s <= 0) return;
+
+					*p++ = '"';
+					s--;
+					if (s <= 0) return;
+				} else {
+					len = vp_prints_value(p, s, vp, 0);
+					p += len;
+					s -= len;
+					if (s <= 0) return;
+				}
+			}
+
+			*p++ = ',';
+			s -= 1;
+			if (s <= 0) return;
+		}
+	} else {
+		s -= conf->list_da_num;
+		if (s <= 0) return;
+
+		memset(p, ',', conf->list_da_num);
+		p += conf->list_da_num;
+	}
+
+	*--p = '\0';
+	fprintf(stdout , "%s\n", buffer);
+}
+
+static void rs_packet_print_fancy(uint64_t count, rs_status_t status, fr_pcap_t *handle, RADIUS_PACKET *packet,
+				  struct timeval *elapsed, struct timeval *latency, bool response, bool body)
+{
+	char buffer[2048];
+	char *p = buffer;
+
+	char src[INET6_ADDRSTRLEN];
+	char dst[INET6_ADDRSTRLEN];
+
+	ssize_t len, s = sizeof(buffer);
+
+	inet_ntop(packet->src_ipaddr.af, &packet->src_ipaddr.ipaddr, src, sizeof(src));
+	inet_ntop(packet->dst_ipaddr.af, &packet->dst_ipaddr.ipaddr, dst, sizeof(dst));
+
+	/* Only print out status str if something's not right */
+	if (status != RS_NORMAL) {
+		char const *status_str;
+
+		status_str = fr_int2str(rs_events, status, NULL);
+		RS_ASSERT(status_str);
+
+		len = snprintf(p, s, "** %s ** ", status_str);
+		p += len;
+		s -= len;
+		if (s <= 0) return;
+	}
+
+	if (is_radius_code(packet->code)) {
+		len = snprintf(p, s, "%s Id %i %s:%s:%d %s %s:%i ",
+			       fr_packet_codes[packet->code],
+			       packet->id,
+			       handle->name,
+			       response ? dst : src,
+			       response ? packet->dst_port : packet->src_port,
+			       response ? "<-" : "->",
+			       response ? src : dst ,
+			       response ? packet->src_port : packet->dst_port);
+	} else {
+		len = snprintf(p, s, "%u Id %i %s:%s:%i %s %s:%i ",
+			       packet->code,
+			       packet->id,
+			       handle->name,
+			       response ? dst : src,
+			       response ? packet->dst_port : packet->src_port,
+			       response ? "<-" : "->",
+			       response ? src : dst ,
+			       response ? packet->src_port : packet->dst_port);
+	}
+	p += len;
+	s -= len;
+	if (s <= 0) return;
+
+	if (elapsed) {
+		len = snprintf(p, s, "+%u.%03u ",
+			       (unsigned int) elapsed->tv_sec, ((unsigned int) elapsed->tv_usec / 1000));
+		p += len;
+		s -= len;
+		if (s <= 0) return;
+	}
+
+	if (latency) {
+		len = snprintf(p, s, "+%u.%03u ",
+			       (unsigned int) latency->tv_sec, ((unsigned int) latency->tv_usec / 1000));
+		p += len;
+		s -= len;
+		if (s <= 0) return;
+	}
+
+	*--p = '\0';
+
+	RIDEBUG("%s", buffer);
+
+	if (body) {
+		/*
+		 *	Print out verbose HEX output
+		 */
+		if (conf->print_packet && (fr_debug_lvl > 3)) {
+			rad_print_hex(packet);
+		}
+
+		if (conf->print_packet && (fr_debug_lvl > 1)) {
+			char vector[(AUTH_VECTOR_LEN * 2) + 1];
+
+			if (packet->vps) {
+				fr_pair_list_sort(&packet->vps, fr_pair_cmp_by_da_tag);
+				vp_printlist(fr_log_fp, packet->vps);
+			}
+
+			fr_bin2hex(vector, packet->vector, AUTH_VECTOR_LEN);
+			INFO("\tAuthenticator-Field = 0x%s", vector);
+		}
+	}
+}
+
+static inline void rs_packet_print(rs_request_t *request, uint64_t count, rs_status_t status, fr_pcap_t *handle,
+				   RADIUS_PACKET *packet, struct timeval *elapsed, struct timeval *latency,
+				   bool response, bool body)
+{
+	if (!conf->logger) return;
+
+	if (request) request->logged = true;
+	conf->logger(count, status, handle, packet, elapsed, latency, response, body);
+}
+
+static void rs_stats_print(rs_latency_t *stats, PW_CODE code)
+{
+	int i;
+	bool have_rt = false;
+
+	for (i = 0; i <= RS_RETRANSMIT_MAX; i++) {
+		if (stats->interval.rt[i]) {
+			have_rt = true;
+		}
+	}
+
+	if (!stats->interval.received && !have_rt && !stats->interval.reused) {
+		return;
+	}
+
+	if (stats->interval.received || stats->interval.linked) {
+		INFO("%s counters:", fr_packet_codes[code]);
+		if (stats->interval.received > 0) {
+			INFO("\tTotal     : %.3lf/s" , stats->interval.received);
+		}
+	}
+
+	if (stats->interval.linked > 0) {
+		INFO("\tLinked    : %.3lf/s", stats->interval.linked);
+		INFO("\tUnlinked  : %.3lf/s", stats->interval.unlinked);
+		INFO("%s latency:", fr_packet_codes[code]);
+		INFO("\tHigh      : %.3lfms", stats->interval.latency_high);
+		INFO("\tLow       : %.3lfms", stats->interval.latency_low);
+		INFO("\tAverage   : %.3lfms", stats->interval.latency_average);
+		INFO("\tMA        : %.3lfms", stats->latency_smoothed);
+	}
+
+	if (have_rt || stats->interval.lost || stats->interval.reused) {
+		INFO("%s retransmits & loss:",  fr_packet_codes[code]);
+
+		if (stats->interval.lost) {
+			INFO("\tLost      : %.3lf/s", stats->interval.lost);
+		}
+
+		if (stats->interval.reused) {
+			INFO("\tID Reused : %.3lf/s", stats->interval.reused);
+		}
+
+		for (i = 0; i <= RS_RETRANSMIT_MAX; i++) {
+			if (!stats->interval.rt[i]) {
+				continue;
+			}
+
+			if (i != RS_RETRANSMIT_MAX) {
+				INFO("\tRT (%i)    : %.3lf/s", i, stats->interval.rt[i]);
+			} else {
+				INFO("\tRT (%i+)   : %.3lf/s", i, stats->interval.rt[i]);
+			}
+		}
+	}
+}
+
+/** Query libpcap to see if it dropped any packets
+ *
+ * We need to check to see if libpcap dropped any packets and if it did, we need to stop stats output for long
+ * enough for inaccurate statistics to be cleared out.
+ *
+ * @param in pcap handle to check.
+ * @param interval time between checks (used for debug output)
+ * @return 0, no drops, -1 we couldn't check, -2 dropped because of buffer exhaustion, -3 dropped because of NIC.
+ */
+static int rs_check_pcap_drop(fr_pcap_t *in, int interval) {
+	int ret = 0;
+	struct pcap_stat pstats;
+
+	if (pcap_stats(in->handle, &pstats) != 0) {
+		ERROR("%s failed retrieving pcap stats: %s", in->name, pcap_geterr(in->handle));
+		return -1;
+	}
+
+	INFO("\t%s%*s: %.3lf/s", in->name, (int) (10 - strlen(in->name)), "",
+	     ((double) (pstats.ps_recv - in->pstats.ps_recv)) / interval);
+
+	if (pstats.ps_drop - in->pstats.ps_drop > 0) {
+		ERROR("%s dropped %i packets: Buffer exhaustion", in->name, pstats.ps_drop - in->pstats.ps_drop);
+		ret = -2;
+	}
+
+	if (pstats.ps_ifdrop - in->pstats.ps_ifdrop > 0) {
+		ERROR("%s dropped %i packets: Interface", in->name, pstats.ps_ifdrop - in->pstats.ps_ifdrop);
+		ret = -3;
+	}
+
+	in->pstats = pstats;
+
+	return ret;
+}
+
+/** Update smoothed average
+ *
+ */
+static void rs_stats_process_latency(rs_latency_t *stats)
+{
+	/*
+	 *	If we didn't link any packets during this interval, we don't have a value to return.
+	 *	returning 0 is misleading as it would be like saying the latency had dropped to 0.
+	 *	We instead set NaN which libcollectd converts to a 'U' or unknown value.
+	 *
+	 *	This will cause gaps in graphs, but is completely legitimate as we are missing data.
+	 *	This is unfortunately an effect of being just a passive observer.
+	 */
+	if (stats->interval.linked_total == 0) {
+		double unk = strtod("NAN()", (char **) NULL);
+
+		stats->interval.latency_average = unk;
+		stats->interval.latency_high = unk;
+		stats->interval.latency_low = unk;
+
+		/*
+		 *	We've not yet been able to determine latency, so latency_smoothed is also NaN
+		 */
+		if (stats->latency_smoothed_count == 0) {
+			stats->latency_smoothed = unk;
+		}
+		return;
+	}
+
+	if (stats->interval.linked_total && stats->interval.latency_total) {
+		stats->interval.latency_average = (stats->interval.latency_total / stats->interval.linked_total);
+	}
+
+	if (isnan(stats->latency_smoothed)) {
+		stats->latency_smoothed = 0;
+	}
+	if (stats->interval.latency_average > 0) {
+		stats->latency_smoothed_count++;
+		stats->latency_smoothed += ((stats->interval.latency_average - stats->latency_smoothed) /
+				       ((stats->latency_smoothed_count < 100) ? stats->latency_smoothed_count : 100));
+	}
+}
+
+static void rs_stats_process_counters(rs_latency_t *stats)
+{
+	int i;
+
+	stats->interval.received = ((long double) stats->interval.received_total) / conf->stats.interval;
+	stats->interval.linked = ((long double) stats->interval.linked_total) / conf->stats.interval;
+	stats->interval.unlinked = ((long double) stats->interval.unlinked_total) / conf->stats.interval;
+	stats->interval.reused = ((long double) stats->interval.reused_total) / conf->stats.interval;
+	stats->interval.lost = ((long double) stats->interval.lost_total) / conf->stats.interval;
+
+	for (i = 0; i < RS_RETRANSMIT_MAX; i++) {
+		stats->interval.rt[i] = ((long double) stats->interval.rt_total[i]) / conf->stats.interval;
+	}
+}
+
+/** Process stats for a single interval
+ *
+ */
+static void rs_stats_process(void *ctx)
+{
+	size_t i;
+	size_t rs_codes_len = (sizeof(rs_useful_codes) / sizeof(*rs_useful_codes));
+	fr_pcap_t		*in_p;
+	rs_update_t		*this = ctx;
+	rs_stats_t		*stats = this->stats;
+	struct timeval		now;
+
+	gettimeofday(&now, NULL);
+
+	stats->intervals++;
+
+	INFO("######### Stats Iteration %i #########", stats->intervals);
+
+	/*
+	 *	Verify that none of the pcap handles have dropped packets.
+	 */
+	INFO("Interface capture rate:");
+	for (in_p = this->in;
+	     in_p;
+	     in_p = in_p->next) {
+		if (rs_check_pcap_drop(in_p, conf->stats.interval) < 0) {
+			ERROR("Muting stats for the next %i milliseconds", conf->stats.timeout);
+
+			rs_tv_add_ms(&now, conf->stats.timeout, &stats->quiet);
+			goto clear;
+		}
+	}
+
+	if ((stats->quiet.tv_sec + (stats->quiet.tv_usec / 1000000.0)) -
+	    (now.tv_sec + (now.tv_usec / 1000000.0)) > 0) {
+		INFO("Stats muted because of warmup, or previous error");
+		goto clear;
+	}
+
+	/*
+	 *	Latency stats need a bit more work to calculate the SMA.
+	 *
+	 *	No further work is required for codes.
+	 */
+	for (i = 0; i < rs_codes_len; i++) {
+		rs_stats_process_latency(&stats->exchange[rs_useful_codes[i]]);
+		rs_stats_process_counters(&stats->exchange[rs_useful_codes[i]]);
+		if (fr_debug_lvl > 0) {
+			rs_stats_print(&stats->exchange[rs_useful_codes[i]], rs_useful_codes[i]);
+		}
+	}
+
+#ifdef HAVE_COLLECTDC_H
+	/*
+	 *	Update stats in collectd using the complex structures we
+	 *	initialised earlier.
+	 */
+	if ((conf->stats.out == RS_STATS_OUT_COLLECTD) && conf->stats.handle) {
+		rs_stats_collectd_do_stats(conf, conf->stats.tmpl, &now);
+	}
+#endif
+
+	clear:
+	/*
+	 *	Rinse and repeat...
+	 */
+	for (i = 0; i < rs_codes_len; i++) {
+		memset(&stats->exchange[rs_useful_codes[i]].interval, 0,
+		       sizeof(stats->exchange[rs_useful_codes[i]].interval));
+	}
+
+	{
+		static fr_event_t *event;
+
+		now.tv_sec += conf->stats.interval;
+		now.tv_usec = 0;
+
+		if (!fr_event_insert(this->list, rs_stats_process, ctx, &now, &event)) {
+			ERROR("Failed inserting stats interval event");
+		}
+	}
+}
+
+
+/** Update latency statistics for request/response and forwarded packets
+ *
+ */
+static void rs_stats_update_latency(rs_latency_t *stats, struct timeval *latency)
+{
+	double lint;
+
+	stats->interval.linked_total++;
+	/* More useful is this in milliseconds */
+	lint = (latency->tv_sec + (latency->tv_usec / 1000000.0)) * 1000;
+	if (lint > stats->interval.latency_high) {
+		stats->interval.latency_high = lint;
+	}
+	if (!stats->interval.latency_low || (lint < stats->interval.latency_low)) {
+		stats->interval.latency_low = lint;
+	}
+	stats->interval.latency_total += lint;
+
+}
+
+/** Copy a subset of attributes from one list into the other
+ *
+ * Should be O(n) if all the attributes exist.  List must be pre-sorted.
+ */
+static int rs_get_pairs(TALLOC_CTX *ctx, VALUE_PAIR **out, VALUE_PAIR *vps, DICT_ATTR const *da[], int num)
+{
+	vp_cursor_t list_cursor, out_cursor;
+	VALUE_PAIR *match, *last_match, *copy;
+	uint64_t count = 0;
+	int i;
+
+	last_match = vps;
+
+	fr_cursor_init(&list_cursor, &last_match);
+	fr_cursor_init(&out_cursor, out);
+	for (i = 0; i < num; i++) {
+		match = fr_cursor_next_by_da(&list_cursor, da[i], TAG_ANY);
+		if (!match) {
+			fr_cursor_init(&list_cursor, &last_match);
+			continue;
+		}
+
+		do {
+			copy = fr_pair_copy(ctx, match);
+			if (!copy) {
+				fr_pair_list_free(out);
+				return -1;
+			}
+			fr_cursor_insert(&out_cursor, copy);
+			last_match = match;
+
+			count++;
+		} while ((match = fr_cursor_next_by_da(&list_cursor, da[i], TAG_ANY)));
+	}
+
+	return count;
+}
+
+static int _request_free(rs_request_t *request)
+{
+	bool ret;
+
+	/*
+	 *	If were attempting to cleanup the request, and it's no longer in the request_tree
+	 *	something has gone very badly wrong.
+	 */
+	if (request->in_request_tree) {
+		ret = rbtree_deletebydata(request_tree, request);
+		RS_ASSERT(ret);
+	}
+
+	if (request->in_link_tree) {
+		ret = rbtree_deletebydata(link_tree, request);
+		RS_ASSERT(ret);
+	}
+
+	if (request->event) {
+		ret = fr_event_delete(events, &request->event);
+		RS_ASSERT(ret);
+	}
+
+	rad_free(&request->packet);
+	rad_free(&request->expect);
+	rad_free(&request->linked);
+
+	return 0;
+}
+
+static void rs_packet_cleanup(rs_request_t *request)
+{
+
+	RADIUS_PACKET *packet = request->packet;
+	uint64_t count = request->id;
+
+	RS_ASSERT(request->stats_req);
+	RS_ASSERT(!request->rt_rsp || request->stats_rsp);
+	RS_ASSERT(packet);
+
+	/*
+	 *	Don't pollute stats or print spurious messages as radsniff closes.
+	 */
+	if (cleanup) {
+		talloc_free(request);
+		return;
+	}
+
+	if (RIDEBUG_ENABLED()) {
+		rs_time_print(timestr, sizeof(timestr), &request->when);
+	}
+
+	/*
+	 *	Were at packet cleanup time which is when the packet was received + timeout
+	 *	and it's not been linked with a forwarded packet or a response.
+	 *
+	 *	We now count it as lost.
+	 */
+	if (!request->silent_cleanup) {
+		if (!request->linked) {
+			if (!request->stats_req) return;
+
+			request->stats_req->interval.lost_total++;
+
+			if (conf->event_flags & RS_LOST) {
+				/* @fixme We should use flags in the request to indicate whether it's been dumped
+				 * to a PCAP file or logged yet, this simplifies the body logging logic */
+				rs_packet_print(request, request->id, RS_LOST, request->in, packet, NULL, NULL, false,
+					        conf->filter_response_vps || !(conf->event_flags & RS_NORMAL));
+			}
+		}
+
+		if ((request->in->type == PCAP_INTERFACE_IN) && request->logged) {
+			RDEBUG("Cleaning up request packet ID %i", request->expect->id);
+		}
+	}
+
+	/*
+	 *	Now the request is done, we can update the retransmission stats
+	 */
+	if (request->rt_req > RS_RETRANSMIT_MAX) {
+		request->stats_req->interval.rt_total[RS_RETRANSMIT_MAX]++;
+	} else {
+		request->stats_req->interval.rt_total[request->rt_req]++;
+	}
+
+	if (request->rt_rsp) {
+		if (request->rt_rsp > RS_RETRANSMIT_MAX) {
+			request->stats_rsp->interval.rt_total[RS_RETRANSMIT_MAX]++;
+		} else {
+			request->stats_rsp->interval.rt_total[request->rt_rsp]++;
+		}
+	}
+
+	talloc_free(request);
+}
+
+static void _rs_event(void *ctx)
+{
+	rs_request_t *request = talloc_get_type_abort(ctx, rs_request_t);
+	request->event = NULL;
+	rs_packet_cleanup(request);
+}
+
+/** Wrapper around fr_packet_cmp to strip off the outer request struct
+ *
+ */
+static int rs_packet_cmp(rs_request_t const *a, rs_request_t const *b)
+{
+	return fr_packet_cmp(a->expect, b->expect);
+}
+
+static inline int rs_response_to_pcap(rs_event_t *event, rs_request_t *request, struct pcap_pkthdr const *header,
+				      uint8_t const *data)
+{
+	if (!event->out) return 0;
+
+	/*
+	 *	If we're filtering by response then the requests then the capture buffer
+	 *	associated with the request should contain buffered request packets.
+	 */
+	if (conf->filter_response && request) {
+		rs_capture_t *start;
+
+		/*
+		 *	Record the current position in the header
+		 */
+		start = request->capture_p;
+
+		/*
+		 *	Buffer hasn't looped set capture_p to the start of the buffer
+		 */
+		if (!start->header) request->capture_p = request->capture;
+
+		/*
+		 *	If where capture_p points to, has a header set, write out the
+		 *	packet to the PCAP file, looping over the buffer until we
+		 *	hit our start point.
+		 */
+		if (request->capture_p->header) do {
+			pcap_dump((void *)event->out->dumper, request->capture_p->header,
+				  request->capture_p->data);
+			TALLOC_FREE(request->capture_p->header);
+			TALLOC_FREE(request->capture_p->data);
+
+			/* Reset the pointer to the start of the circular buffer */
+			if (request->capture_p++ >=
+					(request->capture +
+					 sizeof(request->capture) / sizeof(*request->capture))) {
+				request->capture_p = request->capture;
+			}
+		} while (request->capture_p != start);
+	}
+
+	/*
+	 *	Now log the response
+	 */
+	pcap_dump((void *)event->out->dumper, header, data);
+
+	return 0;
+}
+
+static inline int rs_request_to_pcap(rs_event_t *event, rs_request_t *request, struct pcap_pkthdr const *header,
+				     uint8_t const *data)
+{
+	if (!event->out) return 0;
+
+	/*
+	 *	If we're filtering by response, then we need to wait to write out the requests
+	 */
+	if (conf->filter_response) {
+		/* Free the old capture */
+		if (request->capture_p->header) {
+			talloc_free(request->capture_p->header);
+			TALLOC_FREE(request->capture_p->data);
+		}
+
+		if (!(request->capture_p->header = talloc(request, struct pcap_pkthdr))) return -1;
+		if (!(request->capture_p->data = talloc_array(request, uint8_t, header->caplen))) {
+			TALLOC_FREE(request->capture_p->header);
+			return -1;
+		}
+		memcpy(request->capture_p->header, header, sizeof(struct pcap_pkthdr));
+		memcpy(request->capture_p->data, data, header->caplen);
+
+		/* Reset the pointer to the start of the circular buffer */
+		if (++request->capture_p >=
+				(request->capture +
+				 sizeof(request->capture) / sizeof(*request->capture))) {
+			request->capture_p = request->capture;
+		}
+		return 0;
+	}
+
+	pcap_dump((void *)event->out->dumper, header, data);
+
+	return 0;
+}
+
+/* This is the same as immediately scheduling the cleanup event */
+#define RS_CLEANUP_NOW(_x, _s)\
+	{\
+		_x->silent_cleanup = _s;\
+		_x->when = header->ts;\
+		rs_packet_cleanup(_x);\
+		_x = NULL;\
+	} while (0)
+
+static void rs_packet_process(uint64_t count, rs_event_t *event, struct pcap_pkthdr const *header, uint8_t const *data)
+{
+	rs_stats_t		*stats = event->stats;
+	struct timeval		elapsed = {0, 0};
+	struct timeval		latency;
+
+	/*
+	 *	Pointers into the packet data we just received
+	 */
+	ssize_t len;
+	uint8_t const		*p = data;
+
+	ip_header_t const	*ip = NULL;		/* The IP header */
+	ip_header6_t const	*ip6 = NULL;		/* The IPv6 header */
+	udp_header_t const	*udp;			/* The UDP header */
+	uint8_t			version;		/* IP header version */
+	bool			response;		/* Was it a response code */
+
+	decode_fail_t		reason;			/* Why we failed decoding the packet */
+	static uint64_t		captured = 0;
+
+	rs_status_t		status = RS_NORMAL;	/* Any special conditions (RTX, Unlinked, ID-Reused) */
+	RADIUS_PACKET		*current;		/* Current packet were processing */
+	rs_request_t		*original = NULL;
+
+	rs_request_t		search;
+
+	memset(&search, 0, sizeof(search));
+
+	if (!start_pcap.tv_sec) {
+		start_pcap = header->ts;
+	}
+
+	if (RIDEBUG_ENABLED()) {
+		rs_time_print(timestr, sizeof(timestr), &header->ts);
+	}
+
+	len = fr_pcap_link_layer_offset(data, header->caplen, event->in->link_layer);
+	if (len < 0) {
+		REDEBUG("Failed determining link layer header offset");
+		return;
+	}
+	p += len;
+
+	version = (p[0] & 0xf0) >> 4;
+	switch (version) {
+	case 4:
+		ip = (ip_header_t const *)p;
+		len = (0x0f & ip->ip_vhl) * 4;	/* ip_hl specifies length in 32bit words */
+		p += len;
+		break;
+
+	case 6:
+		ip6 = (ip_header6_t const *)p;
+		p += sizeof(ip_header6_t);
+
+		break;
+
+	default:
+		REDEBUG("IP version invalid %i", version);
+		return;
+	}
+
+	/*
+	 *	End of variable length bits, do basic check now to see if packet looks long enough
+	 */
+	len = (p - data) + sizeof(udp_header_t) + sizeof(radius_packet_t);	/* length value */
+	if ((size_t) len > header->caplen) {
+		REDEBUG("Packet too small, we require at least %zu bytes, captured %i bytes",
+			(size_t) len, header->caplen);
+		return;
+	}
+
+	/*
+	 *	UDP header validation.
+	 */
+	udp = (udp_header_t const *)p;
+	{
+		uint16_t udp_len;
+		ssize_t diff;
+
+		udp_len = ntohs(udp->len);
+		diff = udp_len - (header->caplen - (p - data));
+		/* Truncated data */
+		if (diff > 0) {
+			REDEBUG("Packet too small by %zi bytes, UDP header + Payload should be %hu bytes",
+				diff, udp_len);
+			return;
+		}
+
+#if 0
+		/*
+		 *	It seems many probes add trailing garbage to the end
+		 *	of each capture frame.  This has been observed with
+		 *	the F5 and Netscout.
+		 *
+		 *	Leaving the code here in case it's ever needed for
+		 *	debugging.
+		 */
+		else if (diff < 0) {
+			REDEBUG("Packet too big by %zi bytes, UDP header + Payload should be %hu bytes",
+				diff * -1, udp_len);
+			return;
+		}
+#endif
+	}
+	if ((version == 4) && conf->verify_udp_checksum) {
+		uint16_t expected;
+
+		expected = fr_udp_checksum((uint8_t const *) udp, ntohs(udp->len), udp->checksum,
+					   ip->ip_src, ip->ip_dst);
+		if (udp->checksum != expected) {
+			REDEBUG("UDP checksum invalid, packet: 0x%04hx calculated: 0x%04hx",
+				ntohs(udp->checksum), ntohs(expected));
+			/* Not a fatal error */
+		}
+	}
+	p += sizeof(udp_header_t);
+
+	/*
+	 *	With artificial talloc memory limits there's a good chance we can
+	 *	recover once some requests timeout, so make an effort to deal
+	 *	with allocation failures gracefully.
+	 */
+	current = rad_alloc(conf, false);
+	if (!current) {
+		REDEBUG("Failed allocating memory to hold decoded packet");
+		rs_tv_add_ms(&header->ts, conf->stats.timeout, &stats->quiet);
+		return;
+	}
+
+	current->timestamp = header->ts;
+	current->data_len = header->caplen - (p - data);
+	memcpy(&current->data, &p, sizeof(current->data));
+
+	/*
+	 *	Populate IP/UDP fields from PCAP data
+	 */
+	if (ip) {
+		current->src_ipaddr.af = AF_INET;
+		current->src_ipaddr.ipaddr.ip4addr.s_addr = ip->ip_src.s_addr;
+
+		current->dst_ipaddr.af = AF_INET;
+		current->dst_ipaddr.ipaddr.ip4addr.s_addr = ip->ip_dst.s_addr;
+	} else {
+		current->src_ipaddr.af = AF_INET6;
+		memcpy(current->src_ipaddr.ipaddr.ip6addr.s6_addr, ip6->ip_src.s6_addr,
+		       sizeof(current->src_ipaddr.ipaddr.ip6addr.s6_addr));
+
+		current->dst_ipaddr.af = AF_INET6;
+		memcpy(current->dst_ipaddr.ipaddr.ip6addr.s6_addr, ip6->ip_dst.s6_addr,
+		       sizeof(current->dst_ipaddr.ipaddr.ip6addr.s6_addr));
+	}
+
+	current->src_port = ntohs(udp->src);
+	current->dst_port = ntohs(udp->dst);
+
+	if (!rad_packet_ok(current, 0, &reason)) {
+		REDEBUG("%s", fr_strerror());
+		if (conf->event_flags & RS_ERROR) {
+			rs_packet_print(NULL, count, RS_ERROR, event->in, current, &elapsed, NULL, false, false);
+		}
+		rad_free(&current);
+
+		return;
+	}
+
+	switch (current->code) {
+	case PW_CODE_ACCOUNTING_RESPONSE:
+	case PW_CODE_ACCESS_REJECT:
+	case PW_CODE_ACCESS_ACCEPT:
+	case PW_CODE_ACCESS_CHALLENGE:
+	case PW_CODE_COA_NAK:
+	case PW_CODE_COA_ACK:
+	case PW_CODE_DISCONNECT_NAK:
+	case PW_CODE_DISCONNECT_ACK:
+	case PW_CODE_STATUS_CLIENT:
+	{
+		/* look for a matching request and use it for decoding */
+		search.expect = current;
+		original = rbtree_finddata(request_tree, &search);
+
+		/*
+		 *	Verify this code is allowed
+		 */
+		if (conf->filter_response_code && (conf->filter_response_code != current->code)) {
+			drop_response:
+			RDEBUG2("Response dropped by filter");
+			rad_free(&current);
+
+			/* We now need to cleanup the original request too */
+			if (original) {
+				RS_CLEANUP_NOW(original, true);
+			}
+			return;
+		}
+
+		/*
+		 *	Only decode attributes if we want to print them or filter on them
+		 *	rad_packet_ok does checks to verify the packet is actually valid.
+		 */
+		if (conf->decode_attrs) {
+			int ret;
+			FILE *log_fp = fr_log_fp;
+
+			fr_log_fp = NULL;
+			ret = rad_decode(current, original ? original->expect : NULL, conf->radius_secret);
+			fr_log_fp = log_fp;
+			if (ret != 0) {
+				rad_free(&current);
+				REDEBUG("Failed decoding");
+				return;
+			}
+		}
+
+		/*
+		 *	Check if we've managed to link it to a request
+		 */
+		if (original) {
+			/*
+			 *	Now verify the packet passes the attribute filter
+			 */
+			if (conf->filter_response_vps) {
+				fr_pair_list_sort(&current->vps, fr_pair_cmp_by_da_tag);
+				if (!fr_pair_validate_relaxed(NULL, conf->filter_response_vps, current->vps)) {
+					goto drop_response;
+				}
+			}
+
+			/*
+			 *	Is this a retransmission?
+			 */
+			if (original->linked) {
+				status = RS_RTX;
+				original->rt_rsp++;
+
+				rad_free(&original->linked);
+				fr_event_delete(event->list, &original->event);
+			/*
+			 *	...nope it's the first response to a request.
+			 */
+			} else {
+				original->stats_rsp = &stats->exchange[current->code];
+			}
+
+			/*
+			 *	Insert a callback to remove the request and response
+			 *	from the tree after the timeout period.
+			 *	The delay is so we can detect retransmissions.
+			 */
+			original->linked = talloc_steal(original, current);
+			rs_tv_add_ms(&header->ts, conf->stats.timeout, &original->when);
+			if (!fr_event_insert(event->list, _rs_event, original, &original->when,
+					     &original->event)) {
+				REDEBUG("Failed inserting new event");
+				/*
+				 *	Delete the original request/event, it's no longer valid
+				 *	for statistics.
+				 */
+				talloc_free(original);
+				return;
+			}
+		/*
+		 *	No request seen, or request was dropped by attribute filter
+		 */
+		} else {
+			/*
+			 *	If conf->filter_request_vps are set assume the original request was dropped,
+			 *	the alternative is maintaining another 'filter', but that adds
+			 *	complexity, reduces max capture rate, and is generally a PITA.
+			 */
+			if (conf->filter_request) {
+				rad_free(&current);
+				RDEBUG2("Original request dropped by filter");
+				return;
+			}
+
+			status = RS_UNLINKED;
+			stats->exchange[current->code].interval.unlinked_total++;
+		}
+
+		rs_response_to_pcap(event, original, header, data);
+		response = true;
+		break;
+	}
+
+	case PW_CODE_ACCOUNTING_REQUEST:
+	case PW_CODE_ACCESS_REQUEST:
+	case PW_CODE_COA_REQUEST:
+	case PW_CODE_DISCONNECT_REQUEST:
+	case PW_CODE_STATUS_SERVER:
+	{
+		/*
+		 *	Verify this code is allowed
+		 */
+		if (conf->filter_request_code && (conf->filter_request_code != current->code)) {
+			drop_request:
+
+			RDEBUG2("Request dropped by filter");
+			rad_free(&current);
+
+			return;
+		}
+
+		/*
+		 *	Only decode attributes if we want to print them or filter on them
+		 *	rad_packet_ok does checks to verify the packet is actually valid.
+		 */
+		if (conf->decode_attrs) {
+			int ret;
+			FILE *log_fp = fr_log_fp;
+
+			fr_log_fp = NULL;
+			ret = rad_decode(current, NULL, conf->radius_secret);
+			fr_log_fp = log_fp;
+
+			if (ret != 0) {
+				rad_free(&current);
+				REDEBUG("Failed decoding");
+				return;
+			}
+
+			fr_pair_list_sort(&current->vps, fr_pair_cmp_by_da_tag);
+		}
+
+		/*
+		 *	Save the request for later matching
+		 */
+		search.expect = rad_alloc_reply(current, current);
+		if (!search.expect) {
+			REDEBUG("Failed allocating memory to hold expected reply");
+			rs_tv_add_ms(&header->ts, conf->stats.timeout, &stats->quiet);
+			rad_free(&current);
+			return;
+		}
+		search.expect->code = current->code;
+
+		if ((conf->link_da_num > 0) && current->vps) {
+			int ret;
+			ret = rs_get_pairs(current, &search.link_vps, current->vps, conf->link_da,
+					   conf->link_da_num);
+			if (ret < 0) {
+				ERROR("Failed extracting RTX linking pairs from request");
+				rad_free(&current);
+				return;
+			}
+		}
+
+		/*
+		 *	If we have linking attributes set, attempt to find a request in the linking tree.
+		 */
+		if (search.link_vps) {
+			rs_request_t *tuple;
+
+			original = rbtree_finddata(link_tree, &search);
+			tuple = rbtree_finddata(request_tree, &search);
+
+			/*
+			 *	If the packet we matched using attributes is not the same
+			 *	as the packet in the request tree, then we need to clean up
+			 *	the packet in the request tree.
+			 */
+			if (tuple && (original != tuple)) {
+				RS_CLEANUP_NOW(tuple, true);
+			}
+		/*
+		 *	Detect duplicates using the normal 5-tuple of src/dst ips/ports id
+		 */
+		} else {
+			original = rbtree_finddata(request_tree, &search);
+			if (original && (memcmp(original->expect->vector, current->vector,
+			    			sizeof(original->expect->vector)) != 0)) {
+				/*
+				 *	ID reused before the request timed out (which may be an issue)...
+				 */
+				if (!original->linked) {
+					status = RS_REUSED;
+					stats->exchange[current->code].interval.reused_total++;
+					/* Occurs regularly downstream of proxy servers (so don't complain) */
+					RS_CLEANUP_NOW(original, true);
+				/*
+				 *	...and before we saw a response (which may be a bigger issue).
+				 */
+				} else {
+					RS_CLEANUP_NOW(original, false);
+				}
+				/* else it's a proper RTX with the same src/dst id authenticator/nonce */
+			}
+		}
+
+		/*
+		 *	Now verify the packet passes the attribute filter
+		 */
+		if (conf->filter_request_vps) {
+			if (!fr_pair_validate_relaxed(NULL, conf->filter_request_vps, current->vps)) {
+				goto drop_request;
+			}
+		}
+
+		/*
+		 *	Is this a retransmission?
+		 */
+		if (original) {
+			status = RS_RTX;
+			original->rt_req++;
+
+			rad_free(&original->packet);
+
+			/* We may of seen the response, but it may of been lost upstream */
+			rad_free(&original->linked);
+
+			original->packet = talloc_steal(original, current);
+
+			/* Request may need to be reinserted as the 5 tuple of the response may of changed */
+			if (rs_packet_cmp(original, &search) != 0) {
+				rbtree_deletebydata(request_tree, original);
+			}
+
+			rad_free(&original->expect);
+			original->expect = talloc_steal(original, search.expect);
+
+			/* Disarm the timer for the cleanup event for the original request */
+			fr_event_delete(event->list, &original->event);
+		/*
+		 *	...nope it's a new request.
+		 */
+		} else {
+			original = talloc_zero(conf, rs_request_t);
+			talloc_set_destructor(original, _request_free);
+
+			original->id = count;
+			original->in = event->in;
+			original->stats_req = &stats->exchange[current->code];
+
+			/* Set the packet pointer to the start of the buffer*/
+			original->capture_p = original->capture;
+
+			original->packet = talloc_steal(original, current);
+			original->expect = talloc_steal(original, search.expect);
+
+			if (search.link_vps) {
+				bool ret;
+				vp_cursor_t cursor;
+				VALUE_PAIR *vp;
+
+				for (vp = fr_cursor_init(&cursor, &search.link_vps);
+				     vp;
+				     vp = fr_cursor_next(&cursor)) {
+					fr_pair_steal(original, search.link_vps);
+				}
+				original->link_vps = search.link_vps;
+
+				/* We should never have conflicts */
+				ret = rbtree_insert(link_tree, original);
+				RS_ASSERT(ret);
+				original->in_link_tree = true;
+			}
+
+			/*
+			 *	Special case for when were filtering by response,
+			 *	we never count any requests as lost, because we
+			 *	don't know what the response to that request would
+			 *	of been.
+			 */
+			if (conf->filter_response_vps) {
+				original->silent_cleanup = true;
+			}
+		}
+
+		if (!original->in_request_tree) {
+			bool ret;
+
+			/* We should never have conflicts */
+			ret = rbtree_insert(request_tree, original);
+			RS_ASSERT(ret);
+			original->in_request_tree = true;
+		}
+
+		/*
+		 *	Insert a callback to remove the request from the tree
+		 */
+		original->packet->timestamp = header->ts;
+		rs_tv_add_ms(&header->ts, conf->stats.timeout, &original->when);
+		if (!fr_event_insert(event->list, _rs_event, original,
+				     &original->when, &original->event)) {
+			REDEBUG("Failed inserting new event");
+
+			talloc_free(original);
+			return;
+		}
+		rs_request_to_pcap(event, original, header, data);
+		response = false;
+		break;
+	}
+
+	default:
+		REDEBUG("Unsupported code %i", current->code);
+		rad_free(&current);
+
+		return;
+	}
+
+	rs_tv_sub(&header->ts, &start_pcap, &elapsed);
+
+	/*
+	 *	Increase received count
+	 */
+	stats->exchange[current->code].interval.received_total++;
+
+	/*
+	 *	It's a linked response
+	 */
+	if (original && original->linked) {
+		rs_tv_sub(&current->timestamp, &original->packet->timestamp, &latency);
+
+		/*
+		 *	Update stats for both the request and response types.
+		 *
+		 *	This isn't useful for things like Access-Requests, but will be useful for
+		 *	CoA and Disconnect Messages, as we get the average latency across both
+		 *	response types.
+		 *
+		 *	It also justifies allocating PW_CODE_MAX instances of rs_latency_t.
+		 */
+		rs_stats_update_latency(&stats->exchange[current->code], &latency);
+		rs_stats_update_latency(&stats->exchange[original->expect->code], &latency);
+
+		/*
+		 *	Were filtering on response, now print out the full data from the request
+		 */
+		if (conf->filter_response && RIDEBUG_ENABLED() && (conf->event_flags & RS_NORMAL)) {
+			rs_time_print(timestr, sizeof(timestr), &original->packet->timestamp);
+			rs_tv_sub(&original->packet->timestamp, &start_pcap, &elapsed);
+			rs_packet_print(original, original->id, RS_NORMAL, original->in,
+					original->packet, &elapsed, NULL, false, true);
+			rs_tv_sub(&header->ts, &start_pcap, &elapsed);
+			rs_time_print(timestr, sizeof(timestr), &header->ts);
+		}
+
+		if (conf->event_flags & status) {
+			rs_packet_print(original, count, status, event->in, current,
+					&elapsed, &latency, response, true);
+		}
+	/*
+	 *	It's the original request
+	 *
+	 *	If were filtering on responses we can only indicate we received it on response, or timeout.
+	 */
+	} else if (!conf->filter_response && (conf->event_flags & status)) {
+		rs_packet_print(original, original ? original->id : count, status, event->in,
+				current, &elapsed, NULL, response, true);
+	}
+
+	fflush(fr_log_fp);
+
+	/*
+	 *	If it's a unlinked response, we need to free it explicitly, as it will
+	 *	not be done by the event queue.
+	 */
+	if (response && !original) {
+		rad_free(&current);
+	}
+
+	captured++;
+	/*
+	 *	We've hit our capture limit, break out of the event loop
+	 */
+	if ((conf->limit > 0) && (captured >= conf->limit)) {
+		INFO("Captured %" PRIu64 " packets, exiting...", captured);
+		fr_event_loop_exit(events, 1);
+	}
+}
+
+static void rs_got_packet(fr_event_list_t *el, int fd, void *ctx)
+{
+	static uint64_t	count = 0;	/* Packets seen */
+	rs_event_t	*event = ctx;
+	pcap_t		*handle = event->in->handle;
+
+	int i;
+	int ret;
+	const uint8_t *data;
+	struct pcap_pkthdr *header;
+
+	/*
+	 *	Consume entire capture, interleaving not currently possible
+	 */
+	if ((event->in->type == PCAP_FILE_IN) || (event->in->type == PCAP_STDIO_IN)) {
+		while (!fr_event_loop_exiting(el)) {
+			struct timeval now;
+
+			ret = pcap_next_ex(handle, &header, &data);
+			if (ret == 0) {
+				/* No more packets available at this time */
+				return;
+			}
+			if (ret == -2) {
+				DEBUG("Done reading packets (%s)", event->in->name);
+				fr_event_fd_delete(events, 0, fd);
+
+				/* Signal pipe takes one slot which is why this is == 1 */
+				if (fr_event_list_num_fds(events) == 1) {
+					fr_event_loop_exit(events, 1);
+				}
+
+				return;
+			}
+			if (ret < 0) {
+				ERROR("Error requesting next packet, got (%i): %s", ret, pcap_geterr(handle));
+				return;
+			}
+
+			do {
+				now = header->ts;
+			} while (fr_event_run(el, &now) == 1);
+			count++;
+
+			rs_packet_process(count, event, header, data);
+		}
+		return;
+	}
+
+	/*
+	 *	Consume multiple packets from the capture buffer.
+	 *	We occasionally need to yield to allow events to run.
+	 */
+	for (i = 0; i < RS_FORCE_YIELD; i++) {
+		ret = pcap_next_ex(handle, &header, &data);
+		if (ret == 0) {
+			/* No more packets available at this time */
+			return;
+		}
+		if (ret < 0) {
+			ERROR("Error requesting next packet, got (%i): %s", ret, pcap_geterr(handle));
+			return;
+		}
+
+		count++;
+		rs_packet_process(count, event, header, data);
+	}
+}
+
+static void _rs_event_status(struct timeval *wake)
+{
+	if (wake && ((wake->tv_sec != 0) || (wake->tv_usec >= 100000))) {
+		DEBUG2("Waking up in %d.%01u seconds.", (int) wake->tv_sec, (unsigned int) wake->tv_usec / 100000);
+
+		if (RIDEBUG_ENABLED()) {
+			rs_time_print(timestr, sizeof(timestr), wake);
+		}
+	}
+}
+
+/** Compare requests using packet info and lists of attributes
+ *
+ */
+static int rs_rtx_cmp(rs_request_t const *a, rs_request_t const *b)
+{
+	int rcode;
+
+	RS_ASSERT(a->link_vps);
+	RS_ASSERT(b->link_vps);
+
+	rcode = (int) a->expect->code - (int) b->expect->code;
+	if (rcode != 0) return rcode;
+
+	rcode = a->expect->sockfd - b->expect->sockfd;
+	if (rcode != 0) return rcode;
+
+	rcode = fr_ipaddr_cmp(&a->expect->src_ipaddr, &b->expect->src_ipaddr);
+	if (rcode != 0) return rcode;
+
+	rcode = fr_ipaddr_cmp(&a->expect->dst_ipaddr, &b->expect->dst_ipaddr);
+	if (rcode != 0) return rcode;
+
+	return fr_pair_list_cmp(a->link_vps, b->link_vps);
+}
+
+static int rs_build_dict_list(DICT_ATTR const **out, size_t len, char *list)
+{
+	size_t i = 0;
+	char *p, *tok;
+
+	p = list;
+	while ((tok = strsep(&p, "\t ,")) != NULL) {
+		DICT_ATTR const *da;
+		if ((*tok == '\t') || (*tok == ' ') || (*tok == '\0')) {
+			continue;
+		}
+
+		if (i == len) {
+			ERROR("Too many attributes, maximum allowed is %zu", len);
+			return -1;
+		}
+
+		da = dict_attrbyname(tok);
+		if (!da) {
+			ERROR("Error parsing attribute name \"%s\"", tok);
+			return -1;
+		}
+
+		out[i] = da;
+		i++;
+	}
+
+	/*
+	 *	This allows efficient list comparisons later
+	 */
+	if (i > 1) fr_quick_sort((void const **)out, 0, i - 1, fr_pointer_cmp);
+
+	return i;
+}
+
+static int rs_build_filter(VALUE_PAIR **out, char const *filter)
+{
+	vp_cursor_t cursor;
+	VALUE_PAIR *vp;
+	FR_TOKEN code;
+
+	code = fr_pair_list_afrom_str(conf, filter, out);
+	if (code == T_INVALID) {
+		ERROR("Invalid RADIUS filter \"%s\" (%s)", filter, fr_strerror());
+		return -1;
+	}
+
+	if (!*out) {
+		ERROR("Empty RADIUS filter '%s'", filter);
+		return -1;
+	}
+
+	for (vp = fr_cursor_init(&cursor, out);
+	     vp;
+	     vp = fr_cursor_next(&cursor)) {
+		/*
+		 *	xlat expansion isn't supported here
+		 */
+		if (vp->type == VT_XLAT) {
+			vp->type = VT_DATA;
+			vp->vp_strvalue = vp->value.xlat;
+			vp->vp_length = talloc_array_length(vp->vp_strvalue) - 1;
+		}
+	}
+
+	/*
+	 *	This allows efficient list comparisons later
+	 */
+	fr_pair_list_sort(out, fr_pair_cmp_by_da_tag);
+
+	return 0;
+}
+
+static int rs_build_event_flags(int *flags, FR_NAME_NUMBER const *map, char *list)
+{
+	size_t i = 0;
+	char *p, *tok;
+
+	p = list;
+	while ((tok = strsep(&p, "\t ,")) != NULL) {
+		int flag;
+
+		if ((*tok == '\t') || (*tok == ' ') || (*tok == '\0')) {
+			continue;
+		}
+
+		*flags |= flag = fr_str2int(map, tok, -1);
+		if (flag < 0) {
+			ERROR("Invalid flag \"%s\"", tok);
+			return -1;
+		}
+
+		i++;
+	}
+
+	return i;
+}
+
+/** Callback for when the request is removed from the request tree
+ *
+ * @param request being removed.
+ */
+static void _unmark_request(void *request)
+{
+	rs_request_t *this = request;
+	this->in_request_tree = false;
+}
+
+/** Callback for when the request is removed from the link tree
+ *
+ * @param request being removed.
+ */
+static void _unmark_link(void *request)
+{
+	rs_request_t *this = request;
+	this->in_link_tree = false;
+}
+
+#ifdef HAVE_COLLECTDC_H
+/** Re-open the collectd socket
+ *
+ */
+static void rs_collectd_reopen(void *ctx)
+{
+	fr_event_list_t *list = ctx;
+	static fr_event_t *event;
+	struct timeval now, when;
+
+	if (rs_stats_collectd_open(conf) == 0) {
+		DEBUG2("Stats output socket (re)opened");
+		return;
+	}
+
+	ERROR("Will attempt to re-establish connection in %i ms", RS_SOCKET_REOPEN_DELAY);
+
+	gettimeofday(&now, NULL);
+	rs_tv_add_ms(&now, RS_SOCKET_REOPEN_DELAY, &when);
+	if (!fr_event_insert(list, rs_collectd_reopen, list, &when, &event)) {
+		ERROR("Failed inserting re-open event");
+		RS_ASSERT(0);
+	}
+}
+#endif
+
+/** Write the last signal to the signal pipe
+ *
+ * @param sig raised
+ */
+static void rs_signal_self(int sig)
+{
+	if (write(self_pipe[1], &sig, sizeof(sig)) < 0) {
+		ERROR("Failed writing signal %s to pipe: %s", strsignal(sig), fr_syserror(errno));
+		exit(EXIT_FAILURE);
+	}
+}
+
+/** Read the last signal from the signal pipe
+ *
+ */
+static void rs_signal_action(
+#ifndef HAVE_COLLECTDC_H
+UNUSED
+#endif
+fr_event_list_t *list, int fd, UNUSED void *ctx)
+{
+	int sig;
+	ssize_t ret;
+
+	ret = read(fd, &sig, sizeof(sig));
+	if (ret < 0) {
+		ERROR("Failed reading signal from pipe: %s", fr_syserror(errno));
+		exit(EXIT_FAILURE);
+	}
+
+	if (ret != sizeof(sig)) {
+		ERROR("Failed reading signal from pipe: "
+		      "Expected signal to be %zu bytes but only read %zu byes", sizeof(sig), ret);
+		exit(EXIT_FAILURE);
+	}
+
+	switch (sig) {
+#ifdef HAVE_COLLECTDC_H
+	case SIGPIPE:
+		rs_collectd_reopen(list);
+		break;
+#endif
+
+	case SIGINT:
+	case SIGTERM:
+	case SIGQUIT:
+		DEBUG2("Signalling event loop to exit");
+		fr_event_loop_exit(events, 1);
+		break;
+
+	default:
+		ERROR("Unhandled signal %s", strsignal(sig));
+		exit(EXIT_FAILURE);
+	}
+}
+
+static void NEVER_RETURNS usage(int status)
+{
+	FILE *output = status ? stderr : stdout;
+	fprintf(output, "Usage: radsniff [options][stats options] -- [pcap files]\n");
+	fprintf(output, "options:\n");
+	fprintf(output, "  -a                    List all interfaces available for capture.\n");
+	fprintf(output, "  -c <count>            Number of packets to capture.\n");
+	fprintf(output, "  -C                    Enable UDP checksum validation.\n");
+	fprintf(output, "  -d <directory>        Set dictionary directory.\n");
+	fprintf(output, "  -d <raddb>            Set configuration directory (defaults to " RADDBDIR ").\n");
+	fprintf(output, "  -D <dictdir>          Set main dictionary directory (defaults to " DICTDIR ").\n");
+	fprintf(output, "  -e <event>[,<event>]  Only log requests with these event flags.\n");
+	fprintf(output, "                        Event may be one of the following:\n");
+	fprintf(output, "                        - received - a request or response.\n");
+	fprintf(output, "                        - norsp    - seen for a request.\n");
+	fprintf(output, "                        - rtx      - of a request that we've seen before.\n");
+	fprintf(output, "                        - noreq    - could be matched with the response.\n");
+	fprintf(output, "                        - reused   - ID too soon.\n");
+	fprintf(output, "                        - error    - decoding the packet.\n");
+	fprintf(output, "  -f <filter>           PCAP filter (default is 'udp port <port> or <port + 1> or 3799')\n");
+	fprintf(output, "  -h                    This help message.\n");
+	fprintf(output, "  -i <interface>        Capture packets from interface (defaults to all if supported).\n");
+	fprintf(output, "  -I <file>             Read packets from file (overrides input of -F).\n");
+	fprintf(output, "  -l <attr>[,<attr>]    Output packet sig and a list of attributes.\n");
+	fprintf(output, "  -L <attr>[,<attr>]    Detect retransmissions using these attributes to link requests.\n");
+	fprintf(output, "  -m                    Don't put interface(s) into promiscuous mode.\n");
+	fprintf(output, "  -p <port>             Filter packets by port (default is 1812).\n");
+	fprintf(output, "  -P <pidfile>          Daemonize and write out <pidfile>.\n");
+	fprintf(output, "  -q                    Print less debugging information.\n");
+	fprintf(output, "  -r <filter>           RADIUS attribute request filter.\n");
+	fprintf(output, "  -R <filter>           RADIUS attribute response filter.\n");
+	fprintf(output, "  -s <secret>           RADIUS secret.\n");
+	fprintf(output, "  -S                    Write PCAP data to stdout.\n");
+	fprintf(output, "  -v                    Show program version information.\n");
+	fprintf(output, "  -w <file>             Write output packets to file.\n");
+	fprintf(output, "  -x                    Print more debugging information.\n");
+	fprintf(output, "stats options:\n");
+	fprintf(output, "  -W <interval>         Periodically write out statistics every <interval> seconds.\n");
+	fprintf(output, "  -T <timeout>          How many milliseconds before the request is counted as lost "
+		"(defaults to %i).\n", RS_DEFAULT_TIMEOUT);
+#ifdef HAVE_COLLECTDC_H
+	fprintf(output, "  -N <prefix>           The instance name passed to the collectd plugin.\n");
+	fprintf(output, "  -O <server>           Write statistics to this collectd server.\n");
+#endif
+	exit(status);
+}
+
+int main(int argc, char *argv[])
+{
+	fr_pcap_t *in = NULL, *in_p;
+	fr_pcap_t **in_head = &in;
+	fr_pcap_t *out = NULL;
+
+	int ret = 1;					/* Exit status */
+
+	char errbuf[PCAP_ERRBUF_SIZE];			/* Error buffer */
+	int port = 1812;
+
+	char buffer[1024];
+
+	int opt;
+	char const *radius_dir = RADDBDIR;
+	char const *dict_dir = DICTDIR;
+
+	rs_stats_t stats;
+
+	fr_debug_lvl = 1;
+	fr_log_fp = stdout;
+
+	/*
+	 *	Useful if using radsniff as a long running stats daemon
+	 */
+#ifndef NDEBUG
+	if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) {
+		fr_perror("radsniff");
+		exit(EXIT_FAILURE);
+	}
+#endif
+
+	talloc_set_log_stderr();
+
+	conf = talloc_zero(NULL, rs_t);
+	RS_ASSERT(conf);
+
+	/*
+	 *  We don't really want probes taking down machines
+	 */
+#ifdef HAVE_TALLOC_SET_MEMLIMIT
+	/*
+	 *	@fixme causes hang in talloc steal
+	 */
+	 //talloc_set_memlimit(conf, 524288000);		/* 50 MB */
+#endif
+
+	/*
+	 *	Set some defaults
+	 */
+	conf->print_packet = true;
+	conf->limit = 0;
+	conf->promiscuous = true;
+#ifdef HAVE_COLLECTDC_H
+	conf->stats.prefix = RS_DEFAULT_PREFIX;
+#endif
+	conf->radius_secret = RS_DEFAULT_SECRET;
+	conf->logger = NULL;
+
+#ifdef HAVE_COLLECTDC_H
+	conf->stats.prefix = RS_DEFAULT_PREFIX;
+#endif
+
+	/*
+	 *  Get options
+	 */
+	while ((opt = getopt(argc, argv, "ab:c:Cd:D:e:Ff:hi:I:l:L:mp:P:qr:R:s:Svw:xXW:T:P:N:O:")) != EOF) {
+		switch (opt) {
+		case 'a':
+		{
+			pcap_if_t *all_devices = NULL;
+			pcap_if_t *dev_p;
+
+			if (pcap_findalldevs(&all_devices, errbuf) < 0) {
+				ERROR("Error getting available capture devices: %s", errbuf);
+				goto finish;
+			}
+
+			int i = 1;
+			for (dev_p = all_devices;
+			     dev_p;
+			     dev_p = dev_p->next) {
+				INFO("%i.%s", i++, dev_p->name);
+			}
+			ret = 0;
+			pcap_freealldevs(all_devices);
+			goto finish;
+		}
+
+		/* super secret option */
+		case 'b':
+			conf->buffer_pkts = atoi(optarg);
+			if (conf->buffer_pkts == 0) {
+				ERROR("Invalid buffer length \"%s\"", optarg);
+				usage(1);
+			}
+			break;
+
+		case 'c':
+			conf->limit = atoi(optarg);
+			if (conf->limit == 0) {
+				ERROR("Invalid number of packets \"%s\"", optarg);
+				usage(1);
+			}
+			break;
+
+		/* udp checksum */
+		case 'C':
+			conf->verify_udp_checksum = true;
+			break;
+
+		case 'd':
+			radius_dir = optarg;
+			break;
+
+		case 'D':
+			dict_dir = optarg;
+			break;
+
+		case 'e':
+			if (rs_build_event_flags((int *) &conf->event_flags, rs_events, optarg) < 0) {
+				usage(64);
+			}
+			break;
+
+		case 'f':
+			conf->pcap_filter = optarg;
+			break;
+
+		case 'h':
+			usage(0);	/* never returns */
+
+		case 'i':
+			*in_head = fr_pcap_init(conf, optarg, PCAP_INTERFACE_IN);
+			if (!*in_head) goto finish;
+			in_head = &(*in_head)->next;
+			conf->from_dev = true;
+			break;
+
+		case 'I':
+			*in_head = fr_pcap_init(conf, optarg, PCAP_FILE_IN);
+			if (!*in_head) {
+				goto finish;
+			}
+			in_head = &(*in_head)->next;
+			conf->from_file = true;
+			break;
+
+		case 'l':
+			conf->list_attributes = optarg;
+			break;
+
+		case 'L':
+			conf->link_attributes = optarg;
+			break;
+
+		case 'm':
+			conf->promiscuous = false;
+			break;
+
+		case 'p':
+			port = atoi(optarg);
+			break;
+
+		case 'P':
+			conf->daemonize = true;
+			conf->pidfile = optarg;
+			break;
+
+		case 'q':
+			if (fr_debug_lvl > 0) {
+				fr_debug_lvl--;
+			}
+			break;
+
+		case 'r':
+			conf->filter_request = optarg;
+			break;
+
+		case 'R':
+			conf->filter_response = optarg;
+			break;
+
+		case 's':
+			conf->radius_secret = optarg;
+			break;
+
+		case 'S':
+			conf->to_stdout = true;
+			break;
+
+		case 'v':
+#ifdef HAVE_COLLECTDC_H
+			INFO("%s, %s, collectdclient version %s", radsniff_version, pcap_lib_version(),
+			     lcc_version_string());
+#else
+			INFO("%s %s", radsniff_version, pcap_lib_version());
+#endif
+			exit(EXIT_SUCCESS);
+
+		case 'w':
+			out = fr_pcap_init(conf, optarg, PCAP_FILE_OUT);
+			if (!out) {
+				ERROR("Failed creating pcap file \"%s\"", optarg);
+				exit(EXIT_FAILURE);
+			}
+			conf->to_file = true;
+			break;
+
+		case 'x':
+		case 'X':
+			fr_debug_lvl++;
+			break;
+
+		case 'W':
+			conf->stats.interval = atoi(optarg);
+			conf->print_packet = false;
+			if (conf->stats.interval <= 0) {
+				ERROR("Stats interval must be > 0");
+				usage(64);
+			}
+			break;
+
+		case 'T':
+			conf->stats.timeout = atoi(optarg);
+			if (conf->stats.timeout <= 0) {
+				ERROR("Timeout value must be > 0");
+				usage(64);
+			}
+			break;
+
+#ifdef HAVE_COLLECTDC_H
+		case 'N':
+			conf->stats.prefix = optarg;
+			break;
+
+		case 'O':
+			conf->stats.collectd = optarg;
+			conf->stats.out = RS_STATS_OUT_COLLECTD;
+			break;
+#endif
+		default:
+			usage(64);
+		}
+	}
+
+	/*
+	 *	Mismatch between the binary and the libraries it depends on
+	 */
+	if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) {
+		fr_perror("radsniff");
+		exit(EXIT_FAILURE);
+	}
+
+	/* Useful for file globbing */
+	while (optind < argc) {
+		*in_head = fr_pcap_init(conf, argv[optind], PCAP_FILE_IN);
+		if (!*in_head) {
+			goto finish;
+		}
+		in_head = &(*in_head)->next;
+		conf->from_file = true;
+		optind++;
+	}
+
+	/* Is stdin not a tty? If so it's probably a pipe */
+	if (!isatty(fileno(stdin))) {
+		conf->from_stdin = true;
+	}
+
+	/* What's the point in specifying -F ?! */
+	if (conf->from_stdin && conf->from_file && conf->to_file) {
+		usage(64);
+	}
+
+	/* Can't read from both... */
+	if (conf->from_file && conf->from_dev) {
+		usage(64);
+	}
+
+	/* Reading from file overrides stdin */
+	if (conf->from_stdin && (conf->from_file || conf->from_dev)) {
+		conf->from_stdin = false;
+	}
+
+	/* Writing to file overrides stdout */
+	if (conf->to_file && conf->to_stdout) {
+		conf->to_stdout = false;
+	}
+
+	if (conf->to_stdout) {
+		out = fr_pcap_init(conf, "stdout", PCAP_STDIO_OUT);
+		if (!out) {
+			goto finish;
+		}
+	}
+
+	if (conf->from_stdin) {
+		*in_head = fr_pcap_init(conf, "stdin", PCAP_STDIO_IN);
+		if (!*in_head) {
+			goto finish;
+		}
+		in_head = &(*in_head)->next;
+	}
+
+	if (conf->stats.interval && !conf->stats.out) {
+		conf->stats.out = RS_STATS_OUT_STDIO;
+	}
+
+	if (conf->stats.timeout == 0) {
+		conf->stats.timeout = RS_DEFAULT_TIMEOUT;
+	}
+
+	/*
+	 *	If were writing pcap data, or CSV to stdout we *really* don't want to send
+	 *	logging there as well.
+	 */
+	if (conf->to_stdout || conf->list_attributes) {
+		fr_log_fp = stderr;
+	}
+
+	if (conf->list_attributes) {
+		conf->logger = rs_packet_print_csv;
+	} else if (fr_debug_lvl > 0) {
+		conf->logger = rs_packet_print_fancy;
+	}
+
+#if !defined(HAVE_PCAP_FOPEN_OFFLINE) || !defined(HAVE_PCAP_DUMP_FOPEN)
+	if (conf->from_stdin || conf->to_stdout) {
+		ERROR("PCAP streams not supported");
+		goto finish;
+	}
+#endif
+
+	if (!conf->pcap_filter) {
+		snprintf(buffer, sizeof(buffer), "udp port %d or %d or %d",
+			 port, port + 1, 3799);
+		conf->pcap_filter = buffer;
+	}
+
+	if (dict_init(dict_dir, RADIUS_DICTIONARY) < 0) {
+		fr_perror("radsniff");
+		ret = 64;
+		goto finish;
+	}
+
+	if (dict_read(radius_dir, RADIUS_DICTIONARY) == -1) {
+		fr_perror("radsniff");
+		ret = 64;
+		goto finish;
+	}
+
+	fr_strerror();	/* Clear out any non-fatal errors */
+
+	if (conf->list_attributes) {
+		conf->list_da_num = rs_build_dict_list(conf->list_da, sizeof(conf->list_da) / sizeof(*conf->list_da),
+						       conf->list_attributes);
+		if (conf->list_da_num < 0) {
+			usage(64);
+		}
+		rs_packet_print_csv_header();
+	}
+
+	if (conf->link_attributes) {
+		conf->link_da_num = rs_build_dict_list(conf->link_da, sizeof(conf->link_da) / sizeof(*conf->link_da),
+						       conf->link_attributes);
+		if (conf->link_da_num < 0) {
+			usage(64);
+		}
+
+		link_tree = rbtree_create(conf, (rbcmp) rs_rtx_cmp, _unmark_link, 0);
+		if (!link_tree) {
+			ERROR("Failed creating RTX tree");
+			goto finish;
+		}
+	}
+
+	if (conf->filter_request) {
+		vp_cursor_t cursor;
+		VALUE_PAIR *type;
+
+		if (rs_build_filter(&conf->filter_request_vps, conf->filter_request) < 0) {
+			usage(64);
+		}
+
+		fr_cursor_init(&cursor, &conf->filter_request_vps);
+		type = fr_cursor_next_by_num(&cursor, PW_PACKET_TYPE, 0, TAG_ANY);
+		if (type) {
+			fr_cursor_remove(&cursor);
+			conf->filter_request_code = type->vp_integer;
+			talloc_free(type);
+		}
+	}
+
+	if (conf->filter_response) {
+		vp_cursor_t cursor;
+		VALUE_PAIR *type;
+
+		if (rs_build_filter(&conf->filter_response_vps, conf->filter_response) < 0) {
+			usage(64);
+		}
+
+		fr_cursor_init(&cursor, &conf->filter_response_vps);
+		type = fr_cursor_next_by_num(&cursor, PW_PACKET_TYPE, 0, TAG_ANY);
+		if (type) {
+			fr_cursor_remove(&cursor);
+			conf->filter_response_code = type->vp_integer;
+			talloc_free(type);
+		}
+	}
+
+	/*
+	 *	Default to logging and capturing all events
+	 */
+	if (conf->event_flags == 0) {
+		DEBUG("Logging all events");
+		memset(&conf->event_flags, 0xff, sizeof(conf->event_flags));
+	}
+
+	/*
+	 *	If we need to list attributes, link requests using attributes, filter attributes
+	 *	or print the packet contents, we need to decode the attributes.
+	 *
+	 *	But, if were just logging requests, or graphing packets, we don't need to decode
+	 *	attributes.
+	 */
+	if (conf->list_da_num || conf->link_da_num || conf->filter_response_vps || conf->filter_request_vps ||
+	    conf->print_packet) {
+		conf->decode_attrs = true;
+	}
+
+	/*
+	 *	Setup the request tree
+	 */
+	request_tree = rbtree_create(conf, (rbcmp) rs_packet_cmp, _unmark_request, 0);
+	if (!request_tree) {
+		ERROR("Failed creating request tree");
+		goto finish;
+	}
+
+	/*
+	 *	Get the default capture device
+	 */
+	if (!conf->from_stdin && !conf->from_file && !conf->from_dev) {
+		pcap_if_t *all_devices;			/* List of all devices libpcap can listen on */
+		pcap_if_t *dev_p;
+
+		if (pcap_findalldevs(&all_devices, errbuf) < 0) {
+			ERROR("Error getting available capture devices: %s", errbuf);
+			goto finish;
+		}
+
+		if (!all_devices) {
+			ERROR("No capture files specified and no live interfaces available");
+			ret = 64;
+			goto finish;
+		}
+
+		for (dev_p = all_devices;
+		     dev_p;
+		     dev_p = dev_p->next) {
+			int link_layer;
+
+			/* Don't use the any devices, it's horribly broken */
+			if (!strcmp(dev_p->name, "any")) continue;
+
+			link_layer = fr_pcap_if_link_layer(errbuf, dev_p);
+			if (link_layer < 0) {
+				DEBUG2("Skipping %s: %s", dev_p->name, errbuf);
+				continue;
+			}
+
+			if (!fr_pcap_link_layer_supported(link_layer)) {
+				DEBUG2("Skipping %s: datalink type %s not supported",
+				       dev_p->name, pcap_datalink_val_to_name(link_layer));
+				continue;
+			}
+
+			*in_head = fr_pcap_init(conf, dev_p->name, PCAP_INTERFACE_IN);
+			in_head = &(*in_head)->next;
+		}
+		conf->from_auto = true;
+		conf->from_dev = true;
+
+		pcap_freealldevs(all_devices);
+
+		INFO("Defaulting to capture on all interfaces");
+	}
+
+	/*
+	 *	Print captures values which will be used
+	 */
+	if (fr_debug_lvl > 2) {
+		DEBUG2("Sniffing with options:");
+		if (conf->from_dev)	{
+			char *buff = fr_pcap_device_names(conf, in, ' ');
+			DEBUG2("  Device(s)               : [%s]", buff);
+			talloc_free(buff);
+		}
+		if (out) {
+			DEBUG2("  Writing to              : [%s]", out->name);
+		}
+		if (conf->limit > 0)	{
+			DEBUG2("  Capture limit (packets) : [%" PRIu64 "]", conf->limit);
+		}
+			DEBUG2("  PCAP filter             : [%s]", conf->pcap_filter);
+			DEBUG2("  RADIUS secret           : [%s]", conf->radius_secret);
+
+		if (conf->filter_request_code) {
+			DEBUG2("  RADIUS request code     : [%s]", fr_packet_codes[conf->filter_request_code]);
+		}
+
+		if (conf->filter_request_vps){
+			DEBUG2("  RADIUS request filter   :");
+			vp_printlist(fr_log_fp, conf->filter_request_vps);
+		}
+
+		if (conf->filter_response_code) {
+			DEBUG2("  RADIUS response code    : [%s]", fr_packet_codes[conf->filter_response_code]);
+		}
+
+		if (conf->filter_response_vps){
+			DEBUG2("  RADIUS response filter  :");
+			vp_printlist(fr_log_fp, conf->filter_response_vps);
+		}
+	}
+
+	/*
+	 *	Setup collectd templates
+	 */
+#ifdef HAVE_COLLECTDC_H
+	if (conf->stats.out == RS_STATS_OUT_COLLECTD) {
+		size_t i;
+		rs_stats_tmpl_t *tmpl, **next;
+
+		if (rs_stats_collectd_open(conf) < 0) {
+			exit(EXIT_FAILURE);
+		}
+
+		next = &conf->stats.tmpl;
+
+		for (i = 0; i < (sizeof(rs_useful_codes) / sizeof(*rs_useful_codes)); i++) {
+			tmpl = rs_stats_collectd_init_latency(conf, next, conf, "exchanged",
+							      &stats.exchange[rs_useful_codes[i]],
+							      rs_useful_codes[i]);
+			if (!tmpl) {
+				ERROR("Error allocating memory for stats template");
+				goto finish;
+			}
+			next = &(tmpl->next);
+		}
+	}
+#endif
+
+	/*
+	 *	This actually opens the capture interfaces/files (we just allocated the memory earlier)
+	 */
+	{
+		fr_pcap_t *tmp;
+		fr_pcap_t **tmp_p = &tmp;
+
+		for (in_p = in;
+		     in_p;
+		     in_p = in_p->next) {
+			in_p->promiscuous = conf->promiscuous;
+			in_p->buffer_pkts = conf->buffer_pkts;
+			if (fr_pcap_open(in_p) < 0) {
+				ERROR("Failed opening pcap handle (%s): %s", in_p->name, fr_strerror());
+				if (conf->from_auto || (in_p->type == PCAP_FILE_IN)) {
+					continue;
+				}
+
+				goto finish;
+			}
+
+			if (!fr_pcap_link_layer_supported(in_p->link_layer)) {
+				ERROR("Failed opening pcap handle (%s): Datalink type %s not supported",
+				      in_p->name, pcap_datalink_val_to_name(in_p->link_layer));
+				goto finish;
+			}
+
+			if (conf->pcap_filter) {
+				if (fr_pcap_apply_filter(in_p, conf->pcap_filter) < 0) {
+					ERROR("Failed applying filter");
+					goto finish;
+				}
+			}
+
+			*tmp_p = in_p;
+			tmp_p = &(in_p->next);
+		}
+		*tmp_p = NULL;
+		in = tmp;
+
+		if (!in) {
+			ERROR("No PCAP sources available");
+			exit(EXIT_FAILURE);
+		}
+
+		/* Clear any irrelevant errors */
+		fr_strerror();
+	}
+
+	/*
+	 *	Open our output interface (if we have one);
+	 */
+	if (out) {
+		out->link_layer = -1;	/* Infer output link type from input */
+
+		for (in_p = in;
+		     in_p;
+		     in_p = in_p->next) {
+			if (out->link_layer < 0) {
+				out->link_layer = in_p->link_layer;
+				continue;
+			}
+
+			if (out->link_layer != in_p->link_layer) {
+				ERROR("Asked to write to output file, but inputs do not have the same link type");
+				ret = 64;
+				goto finish;
+			}
+		}
+
+		RS_ASSERT(out->link_layer >= 0);
+
+		if (fr_pcap_open(out) < 0) {
+			ERROR("Failed opening pcap output (%s): %s", out->name, fr_strerror());
+			goto finish;
+		}
+	}
+
+	/*
+	 *	Setup and enter the main event loop. Who needs libev when you can roll your own...
+	 */
+	 {
+		struct timeval now;
+		rs_update_t update;
+
+		char *buff;
+
+		memset(&stats, 0, sizeof(stats));
+		memset(&update, 0, sizeof(update));
+
+		events = fr_event_list_create(conf, _rs_event_status);
+		if (!events) {
+			ERROR();
+			goto finish;
+		}
+
+		/*
+		 *  Initialise the signal handler pipe
+		 */
+		if (pipe(self_pipe) < 0) {
+			ERROR("Couldn't open signal pipe: %s", fr_syserror(errno));
+			exit(EXIT_FAILURE);
+		}
+
+		if (!fr_event_fd_insert(events, 0, self_pipe[0], rs_signal_action, events)) {
+			ERROR("Failed inserting signal pipe descriptor: %s", fr_strerror());
+			goto finish;
+		}
+
+		/*
+		 *  Now add fd's for each of the pcap sessions we opened
+		 */
+		for (in_p = in;
+		     in_p;
+		     in_p = in_p->next) {
+			rs_event_t *event;
+
+			event = talloc_zero(events, rs_event_t);
+			event->list = events;
+			event->in = in_p;
+			event->out = out;
+			event->stats = &stats;
+
+			if (!fr_event_fd_insert(events, 0, in_p->fd, rs_got_packet, event)) {
+				ERROR("Failed inserting file descriptor");
+				goto finish;
+			}
+		}
+
+		buff = fr_pcap_device_names(conf, in, ' ');
+		DEBUG("Sniffing on (%s)", buff);
+		talloc_free(buff);
+
+		gettimeofday(&now, NULL);
+
+		/*
+		 *  Insert our stats processor
+		 */
+		if (conf->stats.interval) {
+			static fr_event_t *event;
+
+			update.list = events;
+			update.stats = &stats;
+			update.in = in;
+
+			now.tv_sec += conf->stats.interval;
+			now.tv_usec = 0;
+			if (!fr_event_insert(events, rs_stats_process, (void *) &update, &now, &event)) {
+				ERROR("Failed inserting stats event");
+			}
+
+			INFO("Muting stats for the next %i milliseconds (warmup)", conf->stats.timeout);
+			rs_tv_add_ms(&now, conf->stats.timeout, &stats.quiet);
+		}
+	}
+
+
+	/*
+	 *	Do this as late as possible so we can return an error code if something went wrong.
+	 */
+	if (conf->daemonize) {
+		rs_daemonize(conf->pidfile);
+	}
+
+	/*
+	 *	Setup signal handlers so we always exit gracefully, ensuring output buffers are always
+	 *	flushed.
+	 */
+	fr_set_signal(SIGPIPE, rs_signal_self);
+	fr_set_signal(SIGINT, rs_signal_self);
+	fr_set_signal(SIGTERM, rs_signal_self);
+#ifdef SIGQUIT
+	fr_set_signal(SIGQUIT, rs_signal_self);
+#endif
+
+	fr_event_loop(events);	/* Enter the main event loop */
+
+	DEBUG("Done sniffing");
+
+	finish:
+
+	cleanup = true;
+
+	/*
+	 *	Free all the things! This also closes all the sockets and file descriptors
+	 */
+	talloc_free(conf);
+
+	if (conf->daemonize) {
+		unlink(conf->pidfile);
+	}
+
+	return ret;
+}
diff --git a/src/main/radsniff.mk.in b/src/main/radsniff.mk.in
new file mode 100644
index 0000000..3de7ebc
--- /dev/null
+++ b/src/main/radsniff.mk.in
@@ -0,0 +1,13 @@
+PCAP_LIBS	:= @PCAP_LIBS@
+
+ifneq ($(PCAP_LIBS),)
+TARGET		:= radsniff
+else
+TARGET		:=
+endif
+
+SOURCES		:= radsniff.c collectd.c
+
+TGT_PREREQS	:= libfreeradius-radius.a
+TGT_LDLIBS	:= $(LIBS) $(PCAP_LIBS) $(COLLECTDC_LIBS)
+TGT_LDFLAGS     := $(LDFLAGS) $(PCAP_LDFLAGS) $(COLLECTDC_LDFLAGS)
diff --git a/src/main/radtest.in b/src/main/radtest.in
new file mode 100644
index 0000000..6b71032
--- /dev/null
+++ b/src/main/radtest.in
@@ -0,0 +1,135 @@
+#! /bin/sh
+#
+# radtest	Emulate the user interface of the old
+#		radtest that used to be part of FreeRADIUS.
+#
+# Version:	$Id$
+#
+
+prefix="@prefix@"
+exec_prefix="@exec_prefix@"
+bindir="@bindir@"
+
+usage() {
+	echo "Usage: radtest [OPTIONS] user passwd radius-server[:port] nas-port-number secret [ppphint] [nasname]" >&2
+	echo "        -d RADIUS_DIR       Set radius directory" >&2
+	echo "        -t <type>           Set authentication method" >&2
+	echo "                            type can be pap, chap, mschap, or eap-md5" >&2
+	echo "        -P protocol         Select udp (default) or tcp" >&2
+	echo "        -x                  Enable debug output" >&2
+	echo "        -4                  Use IPv4 for the NAS address (default)" >&2
+	echo "        -6                  Use IPv6 for the NAS address" >&2
+	exit 1
+}
+
+radclient=$bindir/radclient
+if [ ! -x "$radclient" ] && [ -x ./radclient ]
+then
+	radclient=./radclient
+fi
+
+# radeapclient is used for EAP-MD5.
+radeapclient=$bindir/radeapclient
+
+OPTIONS=
+PASSWORD="User-Password"
+NAS_ADDR_ATTR="NAS-IP-Address"
+
+#  We need at LEAST these many options
+if [ $# -lt 5 ]
+then
+	usage
+fi
+
+# Parse new command-line options
+while [ `echo "$1" | cut -c 1` = "-" ]
+do
+   case "$1" in
+	-4) 
+		OPTIONS="$OPTIONS -4"
+		NAS_ADDR_ATTR="NAS-IP-Address"
+		shift
+		;;
+	-6) 
+		OPTIONS="$OPTIONS -6"
+		NAS_ADDR_ATTR="NAS-IPv6-Address"
+		shift
+		;;
+	-d) 
+		OPTIONS="$OPTIONS -d $2"
+		shift;shift
+		;;
+	-P) 
+		OPTIONS="$OPTIONS -P $2"
+		shift;shift
+		;;
+	-x)
+		OPTIONS="$OPTIONS -x"
+		shift
+		;;
+
+	-t)
+		shift;
+		case "$1" in
+			pap)
+				PASSWORD="User-Password"
+				;;
+			chap)
+				PASSWORD="CHAP-Password"
+				;;
+			mschap)
+				PASSWORD="MS-CHAP-Password"
+				;;
+			eap-md5)
+				PASSWORD="Cleartext-Password"
+				if [ ! -x "$radeapclient" ]
+				then
+				    echo "radtest: No 'radeapclient' program was found.  Cannot perform EAP-MD5." >&1
+				    exit 1
+				fi
+				radclient="$radeapclient"
+				;;
+			*)
+				usage
+				;;
+		esac
+		shift
+		;;
+
+	*)
+		usage
+		;;
+  esac
+done
+
+# Check that there are enough options left over.
+if [ $# -lt 5 ] || [ $# -gt 7 ]
+then
+	usage
+fi
+
+if [ "$7" ]
+then
+	nas=$7
+else
+	nas=`(hostname || uname -n) 2>/dev/null | sed 1q`
+fi
+
+(
+	echo "User-Name = \"$1\""
+	echo "$PASSWORD = \"$2\""
+	echo "$NAS_ADDR_ATTR = $nas"
+	echo "NAS-Port = $4"
+	echo "Message-Authenticator = 0x00"
+	if [ "$radclient" = "$radeapclient" ]
+	then
+	    echo "EAP-Code = Response"
+	    echo "EAP-Type-Identity = \"$1\""
+	fi
+	if [ "$6" != "" -a "$6" != "0" ]
+	then
+		echo "Framed-Protocol = PPP"
+	fi
+) | $radclient $OPTIONS -x $3 auth "$5"
+
+exit $?
diff --git a/src/main/radtest.mk b/src/main/radtest.mk
new file mode 100644
index 0000000..3adc133
--- /dev/null
+++ b/src/main/radtest.mk
@@ -0,0 +1,5 @@
+install: $(R)$(bindir)/radtest
+
+$(R)$(bindir)/radtest: src/main/radtest | $(R)$(bindir)
+	@echo INSTALL $(notdir $<)
+	@$(INSTALL) -m 755 $< $(R)$(bindir)
diff --git a/src/main/radwho.c b/src/main/radwho.c
new file mode 100644
index 0000000..d534760
--- /dev/null
+++ b/src/main/radwho.c
@@ -0,0 +1,565 @@
+/*@-skipposixheaders@*/
+/*
+ * radwho.c	Show who is logged in on the terminal servers.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2000,2006  The FreeRADIUS server project
+ * Copyright 2000  Alan DeKok <aland@ox.org>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/sysutmp.h>
+#include <freeradius-devel/radutmp.h>
+
+#include <pwd.h>
+#include <sys/stat.h>
+#include <ctype.h>
+
+/*
+ *	Header above output and format.
+ */
+static char const *hdr1 =
+"Login      Name	      What  TTY  When      From	    Location";
+
+static char const *hdr2 =
+"Login      Port    What      When	  From	    Location";
+
+static char const *eol = "\n";
+static int showname = -1;
+static int showptype = 0;
+static int showcid = 0;
+static char const *progname = "radwho";
+char const *radlog_dir = NULL;
+
+static char const *radutmp_file = NULL;
+static char const *raddb_dir = RADDBDIR;
+static char const *dict_dir = DICTDIR;
+
+char const *radacct_dir = NULL;
+
+bool log_stripped_names;
+
+static char const *radwho_version = "radwho version " RADIUSD_VERSION_STRING
+#ifdef RADIUSD_VERSION_COMMIT
+" (git #" STRINGIFY(RADIUSD_VERSION_COMMIT) ")"
+#endif
+#ifndef ENABLE_REPRODUCIBLE_BUILDS
+", built on " __DATE__ " at " __TIME__
+#endif
+;
+
+/*
+ *	Global, for log.c to use.
+ */
+main_config_t main_config;
+
+#include <sys/wait.h>
+#ifdef HAVE_PTHREAD_H
+pid_t rad_fork(void)
+{
+	return fork();
+}
+
+pid_t rad_waitpid(pid_t pid, int *status)
+{
+	return waitpid(pid, status, 0);
+}
+#endif
+
+static struct radutmp_config_t {
+	char const *radutmp_fn;
+} radutmpconfig;
+
+static const CONF_PARSER module_config[] = {
+	{ "filename", FR_CONF_POINTER(PW_TYPE_FILE_INPUT, &radutmpconfig.radutmp_fn), RADUTMP },
+	CONF_PARSER_TERMINATOR
+};
+
+/*
+ *	Get fullname of a user.
+ */
+static char *fullname(char *username)
+{
+#ifdef HAVE_PWD_H
+	struct passwd *pwd;
+	char *s;
+
+	if ((pwd = getpwnam(username)) != NULL) {
+		if ((s = strchr(pwd->pw_gecos, ',')) != NULL) *s = 0;
+		return pwd->pw_gecos;
+	}
+#endif
+
+	return username;
+}
+
+/*
+ *	Return protocol type.
+ */
+static char const *proto(int id, int porttype)
+{
+	static char buf[8];
+
+	if (showptype) {
+		if (!strchr("ASITX", porttype))
+			porttype = ' ';
+		if (id == 'S')
+			snprintf(buf, sizeof(buf), "SLP %c", porttype);
+		else if (id == 'P')
+			snprintf(buf, sizeof(buf), "PPP %c", porttype);
+		else
+			snprintf(buf, sizeof(buf), "shl %c", porttype);
+		return buf;
+	}
+	if (id == 'S') return "SLIP";
+	if (id == 'P') return "PPP";
+	return "shell";
+}
+
+/*
+ *	Return a time in the form day hh:mm
+ */
+static char *dotime(time_t t)
+{
+	char *s = ctime(&t);
+
+	if (showname) {
+		strlcpy(s + 4, s + 11, 6);
+		s[9] = 0;
+	} else {
+		strlcpy(s + 4, s + 8, 9);
+		s[12] = 0;
+	}
+
+	return s;
+}
+
+
+/*
+ *	Print address of NAS.
+ */
+static char const *hostname(char *buf, size_t buflen, uint32_t ipaddr)
+{
+	/*
+	 *	WTF is this code for?
+	 */
+	if (ipaddr == 0 || ipaddr == (uint32_t)-1 || ipaddr == (uint32_t)-2)
+		return "";
+
+	return inet_ntop(AF_INET, &ipaddr, buf, buflen);
+
+}
+
+
+/*
+ *	Print usage message and exit.
+ */
+static void NEVER_RETURNS usage(int status)
+{
+	FILE *output = status?stderr:stdout;
+
+	fprintf(output, "Usage: radwho [-d raddb] [-cfihnprRsSZ] [-N nas] [-P nas_port] [-u user] [-U user]\n");
+	fprintf(output, "  -c                   Show caller ID, if available.\n");
+	fprintf(output, "  -d                   Set the raddb directory (default is %s).\n", RADIUS_DIR);
+	fprintf(output, "  -F <file>            Use radutmp <file>.\n");
+	fprintf(output, "  -i                   Show session ID.\n");
+	fprintf(output, "  -n                   No full name.\n");
+	fprintf(output, "  -N <nas-ip-address>  Show entries matching the given NAS IP address.\n");
+	fprintf(output, "  -p                   Show port type.\n");
+	fprintf(output, "  -P <port>            Show entries matching the given nas port.\n");
+	fprintf(output, "  -r                   Print output as raw comma-delimited data.\n");
+	fprintf(output, "  -R                   Print output as RADIUS attributes and values.\n");
+	fprintf(output, "                       includes ALL information from the radutmp record.\n");
+	fprintf(output, "  -s                   Show full name.\n");
+	fprintf(output, "  -S                   Hide shell users from radius.\n");
+	fprintf(output, "  -u <user>            Show entries matching the given user.\n");
+	fprintf(output, "  -U <user>            Like -u, but case-sensitive.\n");
+	fprintf(output, "  -v                   Show program version information.\n");
+	fprintf(output, "  -Z                   Include accounting stop information in radius output.  Requires -R.\n");
+	exit(status);
+}
+
+
+/*
+ *	Main program
+ */
+int main(int argc, char **argv)
+{
+	CONF_SECTION *maincs, *cs;
+	FILE *fp;
+	struct radutmp rt;
+	char othername[256];
+	char nasname[1024];
+	char session_id[sizeof(rt.session_id)+1];
+	int hideshell = 0;
+	int showsid = 0;
+	int rawoutput = 0;
+	int radiusoutput = 0;	/* Radius attributes */
+	char const *portind;
+	int c;
+	unsigned int portno;
+	char buffer[2048];
+	char const *user = NULL;
+	int user_cmp = 0;
+	time_t now = 0;
+	uint32_t nas_port = ~0;
+	uint32_t nas_ip_address = INADDR_NONE;
+	int zap = 0;
+
+	raddb_dir = RADIUS_DIR;
+
+#ifndef NDEBUG
+	if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) {
+		fr_perror("radwho");
+		exit(EXIT_FAILURE);
+	}
+#endif
+
+	talloc_set_log_stderr();
+
+	while((c = getopt(argc, argv, "d:D:fF:nN:sSipP:crRu:U:vZ")) != EOF) switch (c) {
+		case 'd':
+			raddb_dir = optarg;
+			break;
+		case 'D':
+			dict_dir = optarg;
+			break;
+		case 'F':
+			radutmp_file = optarg;
+			break;
+		case 'h':
+			usage(0);	/* never returns */
+
+		case 'S':
+			hideshell = 1;
+			break;
+		case 'n':
+			showname = 0;
+			break;
+		case 'N':
+			if (inet_pton(AF_INET, optarg, &nas_ip_address) < 0) {
+				usage(1);
+			}
+			break;
+		case 's':
+			showname = 1;
+			break;
+		case 'i':
+			showsid = 1;
+			break;
+		case 'p':
+			showptype = 1;
+			break;
+		case 'P':
+			nas_port = atoi(optarg);
+			break;
+		case 'c':
+			showcid = 1;
+			showname = 1;
+			break;
+		case 'r':
+			rawoutput = 1;
+			break;
+		case 'R':
+			radiusoutput = 1;
+			now = time(NULL);
+			break;
+		case 'u':
+			user = optarg;
+			user_cmp = 0;
+			break;
+		case 'U':
+			user = optarg;
+			user_cmp = 1;
+			break;
+		case 'v':
+			printf("%s\n", radwho_version);
+			exit(EXIT_SUCCESS);
+		case 'Z':
+			zap = 1;
+			break;
+
+		default:
+			usage(1);	/* never returns */
+	}
+
+	/*
+	 *	Mismatch between the binary and the libraries it depends on
+	 */
+	if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) {
+		fr_perror("radwho");
+		return 1;
+	}
+
+	if (dict_init(dict_dir, RADIUS_DICTIONARY) < 0) {
+		fr_perror("radwho");
+		return 1;
+	}
+
+	if (dict_read(raddb_dir, RADIUS_DICTIONARY) == -1) {
+		fr_perror("radwho");
+		return 1;
+	}
+	fr_strerror();	/* Clear the error buffer */
+
+	/*
+	 *	Be safe.
+	 */
+	if (zap && !radiusoutput) zap = 0;
+
+	/*
+	 *	zap EVERYONE, but only on this nas
+	 */
+	if (zap && !user && (~nas_port == 0)) {
+		/*
+		 *	We need to know which NAS to zap users in.
+		 */
+		if (nas_ip_address == INADDR_NONE) usage(1);
+
+		printf("Acct-Status-Type = Accounting-Off\n");
+		printf("NAS-IP-Address = %s\n",
+		       hostname(buffer, sizeof(buffer), nas_ip_address));
+		printf("Acct-Delay-Time = 0\n");
+		exit(0);	/* don't bother printing anything else */
+	}
+
+	if (radutmp_file) goto have_radutmp;
+
+	/*
+	 *	Initialize main_config
+	 */
+	memset(&main_config, 0, sizeof(main_config));
+
+	/* Read radiusd.conf */
+	maincs = cf_section_alloc(NULL, "main", NULL);
+	if (!maincs) exit(1);
+
+	snprintf(buffer, sizeof(buffer), "%.200s/radiusd.conf", raddb_dir);
+	if (cf_file_read(maincs, buffer) < 0) {
+		fprintf(stderr, "%s: Error reading or parsing radiusd.conf\n", argv[0]);
+		talloc_free(maincs);
+		exit(1);
+	}
+
+	cs = cf_section_sub_find(maincs, "modules");
+	if (!cs) {
+		fprintf(stderr, "%s: No modules section found in radiusd.conf\n", argv[0]);
+		exit(1);
+	}
+	/* Read the radutmp section of radiusd.conf */
+	cs = cf_section_sub_find_name2(cs, "radutmp", NULL);
+	if (!cs) {
+		fprintf(stderr, "%s: No configuration information in radutmp section of radiusd.conf\n", argv[0]);
+		exit(1);
+	}
+
+	cf_section_parse(cs, NULL, module_config);
+
+	/* Assign the correct path for the radutmp file */
+	radutmp_file = radutmpconfig.radutmp_fn;
+
+ have_radutmp:
+	if (showname < 0) showname = 1;
+
+	/*
+	 *	Show the users logged in on the terminal server(s).
+	 */
+	if ((fp = fopen(radutmp_file, "r")) == NULL) {
+		fprintf(stderr, "%s: Error reading %s: %s\n",
+			progname, radutmp_file, fr_syserror(errno));
+		return 0;
+	}
+
+	/*
+	 *	Don't print the headers if raw or RADIUS
+	 */
+	if (!rawoutput && !radiusoutput) {
+		fputs(showname ? hdr1 : hdr2, stdout);
+		fputs(eol, stdout);
+	}
+
+	/*
+	 *	Read the file, printing out active entries.
+	 */
+	while (fread(&rt, sizeof(rt), 1, fp) == 1) {
+		char name[sizeof(rt.login) + 1];
+
+		if (rt.type != P_LOGIN) continue; /* hide logout sessions */
+
+		/*
+		 *	We don't show shell users if we are
+		 *	fingerd, as we have done that above.
+		 */
+		if (hideshell && !strchr("PCS", rt.proto))
+			continue;
+
+		/*
+		 *	Print out sessions only for the given user.
+		 */
+		if (user) {	/* only for a particular user */
+			if (((user_cmp == 0) &&
+			     (strncasecmp(rt.login, user, strlen(user)) != 0)) ||
+			    ((user_cmp == 1) &&
+			     (strncmp(rt.login, user, strlen(user)) != 0))) {
+				continue;
+			}
+		}
+
+		/*
+		 *	Print out only for the given NAS port.
+		 */
+		if (~nas_port != 0) {
+			if (rt.nas_port != nas_port) continue;
+		}
+
+		/*
+		 *	Print out only for the given NAS IP address
+		 */
+		if (nas_ip_address != INADDR_NONE) {
+			if (rt.nas_address != nas_ip_address) continue;
+		}
+
+		memcpy(session_id, rt.session_id, sizeof(rt.session_id));
+		session_id[sizeof(rt.session_id)] = 0;
+
+		if (!rawoutput && rt.nas_port > (showname ? 999 : 99999)) {
+			portind = ">";
+			portno = (showname ? 999 : 99999);
+		} else {
+			portind = "S";
+			portno = rt.nas_port;
+		}
+
+		/*
+		 *	Print output as RADIUS attributes
+		 */
+		if (radiusoutput) {
+			memcpy(nasname, rt.login, sizeof(rt.login));
+			nasname[sizeof(rt.login)] = '\0';
+
+			fr_prints(buffer, sizeof(buffer), nasname, -1, '"');
+			printf("User-Name = \"%s\"\n", buffer);
+
+			fr_prints(buffer, sizeof(buffer), session_id, -1, '"');
+			printf("Acct-Session-Id = \"%s\"\n", buffer);
+
+			if (zap) printf("Acct-Status-Type = Stop\n");
+
+			printf("NAS-IP-Address = %s\n",
+			       hostname(buffer, sizeof(buffer),
+					rt.nas_address));
+			printf("NAS-Port = %u\n", rt.nas_port);
+
+			switch (rt.proto) {
+			case 'S':
+				printf("Service-Type = Framed-User\n");
+				printf("Framed-Protocol = SLIP\n");
+				break;
+
+			case 'P':
+				printf("Service-Type = Framed-User\n");
+				printf("Framed-Protocol = PPP\n");
+				break;
+
+			default:
+				printf("Service-type = Login-User\n");
+				break;
+			}
+			if (rt.framed_address != INADDR_NONE) {
+				printf("Framed-IP-Address = %s\n",
+				       hostname(buffer, sizeof(buffer),
+						rt.framed_address));
+			}
+
+			/*
+			 *	Some sanity checks on the time
+			 */
+			if ((rt.time <= now) &&
+			    (now - rt.time) <= (86400 * 365)) {
+				printf("Acct-Session-Time = %" PRId64 "\n", (int64_t) (now - rt.time));
+			}
+
+			if (rt.caller_id[0] != '\0') {
+				memcpy(nasname, rt.caller_id,
+				       sizeof(rt.caller_id));
+				nasname[sizeof(rt.caller_id)] = '\0';
+
+				fr_prints(buffer, sizeof(buffer), nasname, -1, '"');
+				printf("Calling-Station-Id = \"%s\"\n", buffer);
+			}
+
+			printf("\n"); /* separate entries with a blank line */
+			continue;
+		}
+
+		/*
+		 *	Show the fill name, or not.
+		 */
+		memcpy(name, rt.login, sizeof(rt.login));
+		name[sizeof(rt.login)] = '\0';
+
+		if (showname) {
+			if (rawoutput == 0) {
+				printf("%-10.10s %-17.17s %-5.5s %s%-3u %-9.9s %-15.15s %-.19s%s",
+				       name,
+				       showcid ? rt.caller_id :
+				       (showsid? session_id : fullname(rt.login)),
+				       proto(rt.proto, rt.porttype),
+				       portind, portno,
+				       dotime(rt.time),
+				       hostname(nasname, sizeof(nasname), rt.nas_address),
+				       hostname(othername, sizeof(othername), rt.framed_address), eol);
+			} else {
+				printf("%s,%s,%s,%s%u,%s,%s,%s%s",
+				       name,
+				       showcid ? rt.caller_id :
+				       (showsid? session_id : fullname(rt.login)),
+				       proto(rt.proto, rt.porttype),
+				       portind, portno,
+				       dotime(rt.time),
+				       hostname(nasname, sizeof(nasname), rt.nas_address),
+				       hostname(othername, sizeof(othername), rt.framed_address), eol);
+			}
+		} else {
+			if (rawoutput == 0) {
+				printf("%-10.10s %s%-5u  %-6.6s %-13.13s %-15.15s %-.28s%s",
+				       name,
+				       portind, portno,
+				       proto(rt.proto, rt.porttype),
+				       dotime(rt.time),
+				       hostname(nasname, sizeof(nasname), rt.nas_address),
+				       hostname(othername, sizeof(othername), rt.framed_address),
+				       eol);
+			} else {
+				printf("%s,%s%u,%s,%s,%s,%s%s",
+				       name,
+				       portind, portno,
+				       proto(rt.proto, rt.porttype),
+				       dotime(rt.time),
+				       hostname(nasname, sizeof(nasname), rt.nas_address),
+				       hostname(othername, sizeof(othername), rt.framed_address),
+				       eol);
+			}
+		}
+	}
+	fclose(fp);
+
+	return 0;
+}
diff --git a/src/main/radwho.mk b/src/main/radwho.mk
new file mode 100644
index 0000000..5d9a92a
--- /dev/null
+++ b/src/main/radwho.mk
@@ -0,0 +1,5 @@
+TARGET		:= radwho
+SOURCES		:= radwho.c
+
+TGT_PREREQS	:= libfreeradius-server.a libfreeradius-radius.a
+TGT_LDLIBS	:= $(LIBS)
diff --git a/src/main/radzap b/src/main/radzap
new file mode 100755
index 0000000..f05f253
--- /dev/null
+++ b/src/main/radzap
@@ -0,0 +1,54 @@
+#!/bin/sh
+#
+#	$Id$
+#
+
+usage() {
+	echo "Usage: radzap [options] server[:port] secret" >&2
+	echo "       -h Print usage help information."
+	echo "       -d raddb_directory: directory where radiusd.conf is located."
+	echo "       -D dict_directory: directory where the dictionaries are located."
+	echo "       -N nas_ip_address: IP address of the NAS to zap."
+	echo "       -P nas_port: NAS port that the user is logged into."
+	echo "       -u username: Name of user to zap (case insensitive)."
+	echo "       -U username: like -u, but case-sensitive."
+	echo "       -x Enable debugging output."
+	exit ${1:-0}
+}
+
+while test "$#" != "0"
+do
+  case $1 in
+      -h) usage;;
+
+      -d) OPTS="$OPTS -d $2";shift;shift;;
+
+      -D) OPTS="$OPTS -D $2";shift;shift;;
+
+      -N) NAS_IP_ADDR="-N $2";shift;shift;;
+
+      -P) NAS_PORT="-P $2";shift;shift;;
+
+      -u) USER_NAME="-u $2";shift;shift;;
+
+      -U) USER_NAME="-U $2";shift;shift;;
+
+      -x) DEBUG="-x";shift;;
+
+      *) break;;
+
+  esac
+done
+
+if test "$#" != "2"; then
+    usage 1 >&2
+fi
+
+
+SERVER=$1
+SECRET=$2
+
+#
+#  Radzap is now a wrapper around radwho & radclient.
+#
+radwho -ZR $OPTS $NAS_IP_ADDR $NAS_PORT $USER_NAME | radclient $DEBUG $OPTS -f - $SERVER acct $SECRET
diff --git a/src/main/radzap.mk b/src/main/radzap.mk
new file mode 100644
index 0000000..bd0eb6d
--- /dev/null
+++ b/src/main/radzap.mk
@@ -0,0 +1,5 @@
+install: $(R)$(bindir)/radzap
+
+$(R)$(bindir)/radzap: src/main/radzap | $(R)$(bindir)
+	@echo INSTALL $(notdir $<)
+	@$(INSTALL) -m 755 $< $(R)$(bindir)
diff --git a/src/main/realms.c b/src/main/realms.c
new file mode 100644
index 0000000..2959d82
--- /dev/null
+++ b/src/main/realms.c
@@ -0,0 +1,3247 @@
+/*
+ * realms.c	Realm handling code
+ *
+ * Version:     $Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2007  The FreeRADIUS server project
+ * Copyright 2007  Alan DeKok <aland@deployingradius.com>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/realms.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <fcntl.h>
+
+static rbtree_t *realms_byname = NULL;
+#ifdef WITH_TCP
+bool home_servers_udp = false;
+#endif
+
+#ifdef HAVE_DIRENT_H
+#include <dirent.h>
+#endif
+
+#ifdef HAVE_REGEX
+typedef struct realm_regex realm_regex_t;
+
+/** Regular expression associated with a realm
+ *
+ */
+struct realm_regex {
+	REALM		*realm;		//!< The realm this regex matches.
+	regex_t		*preg;		//!< The pre-compiled regular expression.
+	realm_regex_t	*next;		//!< The next realm in the list of regular expressions.
+};
+static realm_regex_t *realms_regex = NULL;
+#endif /* HAVE_REGEX */
+
+struct realm_config {
+	CONF_SECTION		*cs;
+#ifdef HAVE_DIRENT_H
+	char const		*directory;
+#endif
+	uint32_t		dead_time;
+	uint32_t		retry_count;
+	uint32_t		retry_delay;
+	bool			dynamic;
+	bool			fallback;
+	bool			wake_all_if_all_dead;
+};
+
+static const FR_NAME_NUMBER home_server_types[] = {
+	{ "auth",		HOME_TYPE_AUTH },
+	{ "acct",		HOME_TYPE_ACCT },
+	{ "auth+acct",		HOME_TYPE_AUTH_ACCT },
+	{ "coa",		HOME_TYPE_COA },
+#ifdef WITH_COA_TUNNEL
+	{ "auth+coa",		HOME_TYPE_AUTH_COA },
+	{ "auth+acct+coa",	HOME_TYPE_AUTH_ACCT_COA },
+#endif
+	{ NULL, 0 }
+};
+
+static const FR_NAME_NUMBER home_ping_check[] = {
+	{ "none",		HOME_PING_CHECK_NONE },
+	{ "status-server",	HOME_PING_CHECK_STATUS_SERVER },
+	{ "request",		HOME_PING_CHECK_REQUEST },
+	{ NULL, 0 }
+};
+
+static const FR_NAME_NUMBER home_proto[] = {
+	{ "UDP",		IPPROTO_UDP },
+	{ "TCP",		IPPROTO_TCP },
+	{ NULL, 0 }
+};
+
+#ifdef WITH_RADIUSV11
+extern int fr_radiusv11_client_init(fr_tls_server_conf_t *tls);
+#endif
+
+static realm_config_t *realm_config = NULL;
+
+#ifdef WITH_PROXY
+static rbtree_t	*home_servers_byaddr = NULL;
+static rbtree_t	*home_servers_byname = NULL;
+#ifdef WITH_STATS
+int home_server_max_number = 0;
+static rbtree_t	*home_servers_bynumber = NULL;
+#endif
+
+static rbtree_t	*home_pools_byname = NULL;
+
+/*
+ *  Map the proxy server configuration parameters to variables.
+ */
+static const CONF_PARSER proxy_config[] = {
+	{ "retry_delay", FR_CONF_OFFSET(PW_TYPE_INTEGER, realm_config_t, retry_delay), STRINGIFY(RETRY_DELAY)  },
+
+	{ "retry_count", FR_CONF_OFFSET(PW_TYPE_INTEGER, realm_config_t, retry_count), STRINGIFY(RETRY_COUNT)  },
+
+	{ "default_fallback", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, realm_config_t, fallback), "no" },
+
+	{ "dynamic", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, realm_config_t, dynamic), NULL },
+
+#ifdef HAVE_DIRENT_H
+	{ "directory", FR_CONF_OFFSET(PW_TYPE_STRING, realm_config_t, directory), NULL },
+#endif
+
+	{ "dead_time", FR_CONF_OFFSET(PW_TYPE_INTEGER, realm_config_t, dead_time), STRINGIFY(DEAD_TIME)  },
+
+	{ "wake_all_if_all_dead", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, realm_config_t, wake_all_if_all_dead), "no" },
+	CONF_PARSER_TERMINATOR
+};
+#endif
+
+static int realm_name_cmp(void const *one, void const *two)
+{
+	REALM const *a = one;
+	REALM const *b = two;
+
+	return strcasecmp(a->name, b->name);
+}
+
+
+#ifdef WITH_PROXY
+static void home_server_free(void *data)
+{
+	home_server_t *home = talloc_get_type_abort(data, home_server_t);
+
+	talloc_free(home);
+}
+
+static int home_server_name_cmp(void const *one, void const *two)
+{
+	home_server_t const *a = one;
+	home_server_t const *b = two;
+
+	if (a->type < b->type) return -1;
+	if (a->type > b->type) return +1;
+
+	return strcasecmp(a->name, b->name);
+}
+
+static int home_server_addr_cmp(void const *one, void const *two)
+{
+	int rcode;
+	home_server_t const *a = one;
+	home_server_t const *b = two;
+
+	if (a->virtual_server && !b->virtual_server) return -1;
+	if (!a->virtual_server && b->virtual_server) return +1;
+	if (a->virtual_server && b->virtual_server) {
+		rcode = a->type - b->type;
+		if (rcode != 0) return rcode;
+		return strcmp(a->virtual_server, b->virtual_server);
+	}
+
+	if (a->port < b->port) return -1;
+	if (a->port > b->port) return +1;
+
+#ifdef WITH_TCP
+	if (a->proto < b->proto) return -1;
+	if (a->proto > b->proto) return +1;
+#endif
+
+	rcode = fr_ipaddr_cmp(&a->src_ipaddr, &b->src_ipaddr);
+	if (rcode != 0) return rcode;
+
+	return fr_ipaddr_cmp(&a->ipaddr, &b->ipaddr);
+}
+
+#ifdef WITH_STATS
+static int home_server_number_cmp(void const *one, void const *two)
+{
+	home_server_t const *a = one;
+	home_server_t const *b = two;
+
+	return (a->number - b->number);
+}
+#endif
+
+static int home_pool_name_cmp(void const *one, void const *two)
+{
+	home_pool_t const *a = one;
+	home_pool_t const *b = two;
+
+	if (a->server_type < b->server_type) return -1;
+	if (a->server_type > b->server_type) return +1;
+
+	return strcasecmp(a->name, b->name);
+}
+
+
+static size_t xlat_cs(CONF_SECTION *cs, char const *fmt, char *out, size_t outlen)
+{
+	char const *value = NULL;
+
+	if (!fmt) {
+		DEBUG("No configuration item requested.  Ignoring.");
+
+		*out = '\0';
+		return 0;
+	}
+
+	/*
+	 *	Instance name
+	 */
+	if (strcmp(fmt, "instance") == 0) {
+		value = cf_section_name2(cs);
+		if (!value) {
+			*out = '\0';
+			return 0;
+		}
+	} else {
+		CONF_PAIR *cp;
+
+		cp = cf_pair_find(cs, fmt);
+		if (!cp || !(value = cf_pair_value(cp))) {
+			*out = '\0';
+			return 0;
+		}
+	}
+
+	strlcpy(out, value, outlen);
+
+	return strlen(out);
+}
+
+
+/*
+ *	Xlat for %{home_server:foo}
+ */
+static ssize_t xlat_home_server(UNUSED void *instance, REQUEST *request,
+				char const *fmt, char *out, size_t outlen)
+{
+	if (!request->home_server) {
+		RWDEBUG("No home_server associated with this request");
+
+		*out = '\0';
+		return 0;
+	}
+
+	if (!fmt) {
+		RWDEBUG("No configuration item requested.  Ignoring.");
+
+		*out = '\0';
+		return 0;
+	}
+
+	if (strcmp(fmt, "state") == 0) {
+		char const *state;
+
+		switch (request->home_server->state) {
+		case HOME_STATE_ALIVE:
+			state = "alive";
+			break;
+
+		case HOME_STATE_ZOMBIE:
+			state = "zombie";
+			break;
+
+		case HOME_STATE_IS_DEAD:
+			state = "dead";
+			break;
+
+		case HOME_STATE_CONNECTION_FAIL:
+			state = "fail";
+			break;
+
+		case HOME_STATE_ADMIN_DOWN:
+			state = "down";
+			break;
+
+		default:
+			state = "unknown";
+			break;
+		}
+
+		strlcpy(out, state, outlen);
+		return strlen(out);
+	}
+
+	return xlat_cs(request->home_server->cs, fmt, out, outlen);
+}
+
+
+/*
+ *	Xlat for %{home_server_pool:foo}
+ */
+static ssize_t xlat_server_pool(UNUSED void *instance, REQUEST *request,
+				char const *fmt, char *out, size_t outlen)
+{
+	if (!request->home_pool) {
+		RWDEBUG("No home_pool associated with this request");
+
+		*out = '\0';
+		return 0;
+	}
+
+	if (!fmt) {
+		RWDEBUG("No configuration item requested.  Ignoring.");
+
+		*out = '\0';
+		return 0;
+	}
+
+	if (strcmp(fmt, "state") == 0) {
+		char const *state;
+
+		if (request->home_pool->in_fallback) {
+			state = "fallback";
+
+		} else {
+			state = "alive";
+		}
+
+		strlcpy(out, state, outlen);
+		return strlen(out);
+	}
+
+	return xlat_cs(request->home_pool->cs, fmt, out, outlen);
+}
+
+
+/*
+ *	Xlat for %{home_server_dynamic:foo}
+ */
+static ssize_t xlat_home_server_dynamic(UNUSED void *instance, REQUEST *request,
+					char const *fmt, char *out, size_t outlen)
+{
+	int type;
+	char const *p, *name;
+	home_server_t *home;
+	char buffer[1024];
+
+	if (outlen < 2) return 0;
+
+	switch (request->packet->code) {
+	case PW_CODE_ACCESS_REQUEST:
+		type = HOME_TYPE_AUTH;
+		break;
+
+#ifdef WITH_ACCOUNTING
+	case PW_CODE_ACCOUNTING_REQUEST:
+		type = HOME_TYPE_ACCT;
+		break;
+#endif
+
+#ifdef WITH_COA
+	case PW_CODE_COA_REQUEST:
+	case PW_CODE_DISCONNECT_REQUEST:
+		type = HOME_TYPE_COA;
+		break;
+#endif
+
+	default:
+		*out = '\0';
+		return 0;
+	}
+
+	p = fmt;
+	while (isspace((uint8_t) *p)) p++;
+
+	/*
+	 *	Allow for dynamic strings as arguments.
+	 */
+	if (*p == '&') {
+		VALUE_PAIR *vp;
+
+		if ((radius_get_vp(&vp, request, p) < 0) || !vp ||
+		    (vp->da->type != PW_TYPE_STRING)) {
+			return -1;
+		}
+		name = vp->vp_strvalue;
+
+	} else if (*p == '%') {
+		if (radius_xlat(buffer, sizeof(buffer), request, p, NULL, NULL) < 0) {
+			return -1;
+		}
+		name = buffer;
+
+	} else {
+		name = p;
+	}
+
+	home = home_server_byname(name, type);
+	if (!home) {
+		*out = '\0';
+		return 0;
+	}
+
+	/*
+	 *	1 for dynamic, 0 for static
+	 */
+	out[0] = '0' + home->dynamic;
+	out[1] = '\0';
+
+	return 1;
+}
+#endif
+
+void realms_free(void)
+{
+#ifdef WITH_PROXY
+#  ifdef WITH_STATS
+	rbtree_free(home_servers_bynumber);
+	home_servers_bynumber = NULL;
+#  endif
+
+	rbtree_free(home_servers_byname);
+	home_servers_byname = NULL;
+
+	rbtree_free(home_servers_byaddr);
+	home_servers_byaddr = NULL;
+
+	rbtree_free(home_pools_byname);
+	home_pools_byname = NULL;
+#endif
+
+	rbtree_free(realms_byname);
+	realms_byname = NULL;
+
+	realm_pool_free(NULL);
+
+	talloc_free(realm_config);
+	realm_config = NULL;
+}
+
+
+#ifdef WITH_PROXY
+static CONF_PARSER limit_config[] = {
+	{ "max_connections", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, limit.max_connections), "16" },
+	{ "max_requests", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, limit.max_requests), "0" },
+	{ "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, limit.lifetime), "0" },
+	{ "idle_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, limit.idle_timeout), "0" },
+	CONF_PARSER_TERMINATOR
+};
+
+#ifdef WITH_COA
+static CONF_PARSER home_server_coa[] = {
+	{ "irt",  FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, coa_irt), STRINGIFY(2) },
+	{ "mrt",  FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, coa_mrt), STRINGIFY(16) },
+	{ "mrc",  FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, coa_mrc), STRINGIFY(5) },
+	{ "mrd",  FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, coa_mrd), STRINGIFY(30) },
+	CONF_PARSER_TERMINATOR
+};
+
+
+
+#ifdef WITH_COA_TUNNEL
+static CONF_PARSER home_server_recv_coa[] = {
+	{ "virtual_server",  FR_CONF_OFFSET(PW_TYPE_STRING, home_server_t, recv_coa_server), NULL },
+	CONF_PARSER_TERMINATOR
+};
+#endif
+
+#endif
+
+static CONF_PARSER home_server_config[] = {
+	{ "nonblock", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, home_server_t, nonblock), "no" },
+	{ "ipaddr", FR_CONF_OFFSET(PW_TYPE_COMBO_IP_ADDR, home_server_t, ipaddr), NULL },
+	{ "ipv4addr", FR_CONF_OFFSET(PW_TYPE_IPV4_ADDR, home_server_t, ipaddr), NULL },
+	{ "ipv6addr", FR_CONF_OFFSET(PW_TYPE_IPV6_ADDR, home_server_t, ipaddr), NULL },
+	{ "virtual_server", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_NOT_EMPTY, home_server_t, virtual_server), NULL },
+
+	{ "port", FR_CONF_OFFSET(PW_TYPE_SHORT, home_server_t, port), "0" },
+
+	{ "type", FR_CONF_OFFSET(PW_TYPE_STRING, home_server_t, type_str), NULL },
+
+#ifdef WITH_TCP
+	{ "proto", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_NOT_EMPTY, home_server_t, proto_str), NULL },
+#endif
+
+	{ "secret", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, home_server_t, secret), NULL },
+
+	{ "src_ipaddr", FR_CONF_OFFSET(PW_TYPE_STRING, home_server_t, src_ipaddr_str), NULL },
+
+	{ "response_window", FR_CONF_OFFSET(PW_TYPE_TIMEVAL, home_server_t, response_window), "30" },
+	{ "response_timeouts", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, max_response_timeouts), "1" },
+	{ "max_outstanding", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, max_outstanding), "65536" },
+
+	{ "zombie_period", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, zombie_period), "40" },
+
+	{ "status_check",  FR_CONF_OFFSET(PW_TYPE_STRING, home_server_t, ping_check_str), "none" },
+	{ "ping_check", FR_CONF_OFFSET(PW_TYPE_STRING, home_server_t, ping_check_str), NULL },
+
+	{ "ping_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, ping_interval), "30" },
+	{ "check_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, ping_interval), NULL },
+
+	{ "check_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, ping_timeout), "4" },
+	{ "status_check_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, ping_timeout), NULL },
+
+	{ "num_answers_to_alive", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, num_pings_to_alive), "3" },
+	{ "revive_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, revive_interval), "300" },
+
+	{ "username", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_NOT_EMPTY, home_server_t, ping_user_name), NULL },
+	{ "password", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_NOT_EMPTY, home_server_t, ping_user_password), NULL },
+
+#ifdef WITH_STATS
+	{ "historic_average_window", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, ema.window), NULL },
+#endif
+
+	{ "limit", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) limit_config },
+
+#ifdef WITH_COA
+	{ "coa", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) home_server_coa },
+#ifdef WITH_COA_TUNNEL
+	{ "recv_coa", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) home_server_recv_coa },
+#endif
+#endif
+
+	CONF_PARSER_TERMINATOR
+};
+
+
+static void null_free(UNUSED void *data)
+{
+}
+
+/*
+ *	Ensure that all of the parameters in the home server are OK.
+ */
+void realm_home_server_sanitize(home_server_t *home, CONF_SECTION *cs)
+{
+	CONF_SECTION *parent = NULL;
+
+	FR_INTEGER_BOUND_CHECK("max_outstanding", home->max_outstanding, >=, 8);
+	FR_INTEGER_BOUND_CHECK("max_outstanding", home->max_outstanding, <=, 65536*16);
+
+	FR_INTEGER_BOUND_CHECK("ping_interval", home->ping_interval, >=, 6);
+	FR_INTEGER_BOUND_CHECK("ping_interval", home->ping_interval, <=, 120);
+
+	FR_TIMEVAL_BOUND_CHECK("response_window", &home->response_window, >=, 0, 1000);
+	FR_TIMEVAL_BOUND_CHECK("response_window", &home->response_window, <=,
+			       main_config.max_request_time, 0);
+	FR_TIMEVAL_BOUND_CHECK("response_window", &home->response_window, <=, 60, 0);
+
+	FR_INTEGER_BOUND_CHECK("response_timeouts", home->max_response_timeouts, >=, 1);
+	FR_INTEGER_BOUND_CHECK("response_timeouts", home->max_response_timeouts, <=, 1000);
+
+	/*
+	 *	Track the minimum response window, so that we can
+	 *	correctly set the timers in process.c
+	 */
+	if (timercmp(&main_config.init_delay, &home->response_window, >)) {
+		main_config.init_delay = home->response_window;
+	}
+
+	FR_INTEGER_BOUND_CHECK("zombie_period", home->zombie_period, >=, 1);
+	FR_INTEGER_BOUND_CHECK("zombie_period", home->zombie_period, <=, 120);
+	FR_INTEGER_BOUND_CHECK("zombie_period", home->zombie_period, >=, (uint32_t) home->response_window.tv_sec);
+
+	FR_INTEGER_BOUND_CHECK("num_pings_to_alive", home->num_pings_to_alive, >=, 3);
+	FR_INTEGER_BOUND_CHECK("num_pings_to_alive", home->num_pings_to_alive, <=, 10);
+
+	FR_INTEGER_BOUND_CHECK("check_timeout", home->ping_timeout, >=, 1);
+	FR_INTEGER_BOUND_CHECK("check_timeout", home->ping_timeout, <=, 10);
+
+	FR_INTEGER_BOUND_CHECK("revive_interval", home->revive_interval, >=, 10);
+	FR_INTEGER_BOUND_CHECK("revive_interval", home->revive_interval, <=, 3600);
+
+#ifdef WITH_COA
+	FR_INTEGER_BOUND_CHECK("coa_irt", home->coa_irt, >=, 1);
+	FR_INTEGER_BOUND_CHECK("coa_irt", home->coa_irt, <=, 5);
+
+	FR_INTEGER_BOUND_CHECK("coa_mrc", home->coa_mrc, <=, 20);
+
+	FR_INTEGER_BOUND_CHECK("coa_mrt", home->coa_mrt, <=, 30);
+
+	FR_INTEGER_BOUND_CHECK("coa_mrd", home->coa_mrd, >=, 5);
+	FR_INTEGER_BOUND_CHECK("coa_mrd", home->coa_mrd, <=, 60);
+#endif
+
+	FR_INTEGER_BOUND_CHECK("max_connections", home->limit.max_connections, <=, 1024);
+
+#ifdef WITH_TCP
+	/*
+	 *	UDP sockets can't be connection limited.
+	 */
+	if (home->proto != IPPROTO_TCP) home->limit.max_connections = 0;
+#endif
+
+	if ((home->limit.idle_timeout > 0) && (home->limit.idle_timeout < 5))
+		home->limit.idle_timeout = 5;
+	if ((home->limit.lifetime > 0) && (home->limit.lifetime < 5))
+		home->limit.lifetime = 5;
+	if ((home->limit.lifetime > 0) && (home->limit.idle_timeout > home->limit.lifetime))
+		home->limit.idle_timeout = 0;
+
+	/*
+	 *	Make sure that this is set.
+	 */
+	if (home->src_ipaddr.af == AF_UNSPEC) {
+		home->src_ipaddr.af = home->ipaddr.af;
+	}
+
+	parent = cf_item_parent(cf_section_to_item(cs));
+	if (parent && strcmp(cf_section_name1(parent), "server") == 0) {
+		home->parent_server = cf_section_name2(parent);
+	}
+}
+
+/** Insert a new home server into the various internal lookup trees
+ *
+ * @param home server to add.
+ * @param cs That defined the home server.
+ * @return true on success else false.
+ */
+static bool home_server_insert(home_server_t *home, CONF_SECTION *cs)
+{
+	if (home->name && !rbtree_insert(home_servers_byname, home)) {
+		cf_log_err_cs(cs, "Internal error %d adding home server %s", __LINE__, home->log_name);
+		return false;
+	}
+
+	if (!home->virtual_server && !rbtree_insert(home_servers_byaddr, home)) {
+		rbtree_deletebydata(home_servers_byname, home);
+		cf_log_err_cs(cs, "Internal error %d adding home server %s", __LINE__, home->log_name);
+		return false;
+	}
+
+#ifdef WITH_STATS
+	home->number = home_server_max_number++;
+	if (!rbtree_insert(home_servers_bynumber, home)) {
+		rbtree_deletebydata(home_servers_byname, home);
+		if (home->ipaddr.af != AF_UNSPEC) {
+			rbtree_deletebydata(home_servers_byname, home);
+		}
+		cf_log_err_cs(cs, "Internal error %d adding home server %s", __LINE__, home->log_name);
+		return false;
+	}
+#endif
+
+	return true;
+}
+
+/** Add an already allocate home_server_t to the various trees
+ *
+ * @param home server to add.
+ * @return true on success, else false on error.
+ */
+bool realm_home_server_add(home_server_t *home)
+{
+	/*
+	 *	The structs aren't mutex protected.  Refuse to destroy
+	 *	the server.
+	 */
+	if (event_loop_started && !realm_config->dynamic) {
+		ERROR("Failed to add dynamic home server, \"dynamic = yes\" must be set in proxy.conf");
+		return false;
+	}
+
+	if (home->name && (rbtree_finddata(home_servers_byname, home) != NULL)) {
+		cf_log_err_cs(home->cs, "Duplicate home server name %s", home->name);
+		return false;
+	}
+
+	if (!home->virtual_server && (rbtree_finddata(home_servers_byaddr, home) != NULL)) {
+		char buffer[INET6_ADDRSTRLEN + 3];
+
+		inet_ntop(home->ipaddr.af, &home->ipaddr.ipaddr, buffer, sizeof(buffer));
+
+		cf_log_err_cs(home->cs, "Duplicate home server address%s%s%s: %s:%s%s/%i",
+			      home->name ? " (already in use by " : "",
+			      home->name ? home->name : "",
+			      home->name ? ")" : "",
+			      buffer,
+			      fr_int2str(home_proto, home->proto, "<INVALID>"),
+#ifdef WITH_TLS
+			      home->tls ? "+tls" : "",
+#else
+			      "",
+#endif
+			      home->port);
+
+		return false;
+	}
+
+	if (!home_server_insert(home, home->cs)) return false;
+
+	/*
+	 *	Dual home servers cause us to auto-create an
+	 *	accounting server for UDP sockets, and leave
+	 *	everything alone for TLS sockets.
+	 */
+	if (home->dual
+#ifdef WITH_TLS
+	    && !home->tls
+#endif
+) {
+		home_server_t *home2 = talloc(talloc_parent(home), home_server_t);
+
+		memcpy(home2, home, sizeof(*home2));
+
+		home2->type = HOME_TYPE_ACCT;
+		home2->dual = true;
+		home2->port++;
+
+		home2->ping_user_password = NULL;
+		home2->cs = home->cs;
+		home2->parent_server = home->parent_server;
+
+		if (!home_server_insert(home2, home->cs)) {
+			talloc_free(home2);
+			return false;
+		}
+	}
+
+#ifdef WITH_COA_TUNNEL
+	if (home->recv_coa) {
+		if (!home->tls) {
+			ERROR("TLS is required in order to accept CoA requests from a home server");
+			return false;
+		}
+
+		if (!home->recv_coa_server) {
+			ERROR("A 'virtual_server' configuration is required in order to accept CoA requests from a home server");
+			return false;
+		}
+	}
+#endif
+
+	/*
+	 *	Mark it as already processed
+	 */
+	cf_data_add(home->cs, "home_server", (void *)null_free, null_free);
+
+	return true;
+}
+
+#ifdef WITH_TLS
+/*
+ *	The listeners are always different.  And we always look them up by *known* listener.  And not "find me some random thing".
+ */
+static int listener_cmp(void const *one, void const *two)
+{
+	if (one < two) return -1;
+	if (one > two) return +1;
+
+	return 0;
+}
+#endif
+
+/** Alloc a new home server defined by a CONF_SECTION
+ *
+ * @param ctx to allocate home_server_t in.
+ * @param rc Realm config, may be NULL in which case the global realm_config will be used.
+ * @param cs Configuration section containing home server parameters.
+ * @return a new home_server_t alloced in the context of the realm_config, or NULL on error.
+ */
+home_server_t *home_server_afrom_cs(TALLOC_CTX *ctx, realm_config_t *rc, CONF_SECTION *cs)
+{
+	home_server_t	*home;
+	CONF_SECTION	*tls;
+
+	if (!rc) rc = realm_config; /* Use the global config */
+
+	home = talloc_zero(ctx, home_server_t);
+	home->name = cf_section_name2(cs);
+	home->log_name = talloc_typed_strdup(home, home->name);
+	home->cs = cs;
+	home->state = HOME_STATE_UNKNOWN;
+	home->proto = IPPROTO_UDP;
+
+	/*
+	 *	Parse the configuration into the home server
+	 *	struct.
+	 */
+	if (cf_section_parse(cs, home, home_server_config) < 0) goto error;
+
+	/*
+	 *	It has an IP address, it must be a remote server.
+	 */
+	if (cf_pair_find(cs, "ipaddr") || cf_pair_find(cs, "ipv4addr") || cf_pair_find(cs, "ipv6addr")) {
+		if (fr_inaddr_any(&home->ipaddr) == 1) {
+			cf_log_err_cs(cs, "Wildcard '*' addresses are not permitted for home servers");
+			goto error;
+		}
+
+		if (!home->log_name) {
+			char buffer[INET6_ADDRSTRLEN + 3];
+
+			fr_ntop(buffer, sizeof(buffer), &home->ipaddr);
+
+			home->log_name = talloc_asprintf(home, "%s:%i", buffer, home->port);
+		}
+	/*
+	 *	If it has a 'virtual_Server' config item, it's
+	 *	a loopback into a virtual server.
+	 */
+	} else if (cf_pair_find(cs, "virtual_server") != NULL) {
+		home->ipaddr.af = AF_UNSPEC;	/* mark ipaddr as unused */
+
+		if (!home->virtual_server) {
+			cf_log_err_cs(cs, "Invalid value for virtual_server");
+			goto error;
+		}
+
+		/*
+		 *	Try and find a "server" section off the root of
+		 *	the config with a name that matches the
+		 *	virtual_server.
+		 */
+		if (!rc) goto error;
+
+		if (!cf_section_sub_find_name2(rc->cs, "server", home->virtual_server)) {
+			cf_log_err_cs(cs, "No such server %s", home->virtual_server);
+			goto error;
+		}
+
+		home->secret = "";
+		home->log_name = talloc_typed_strdup(home, home->virtual_server);
+	/*
+	 *	Otherwise it's an invalid config section and we
+	 *	raise an error.
+	 */
+	} else {
+		cf_log_err_cs(cs, "No ipaddr, ipv4addr, ipv6addr, or virtual_server defined "
+			      "for home server");
+	error:
+		talloc_free(home);
+		return false;
+	}
+
+ 	{
+ 		home_type_t type = HOME_TYPE_AUTH_ACCT;
+
+ 		if (home->type_str) type = fr_str2int(home_server_types, home->type_str, HOME_TYPE_INVALID);
+
+		home->type = type;
+
+ 		switch (type) {
+ 		case HOME_TYPE_AUTH_ACCT:
+			home->dual = true;
+			break;
+
+		case HOME_TYPE_AUTH:
+		case HOME_TYPE_ACCT:
+			break;
+
+#ifdef WITH_COA
+ 		case HOME_TYPE_COA:
+			if (home->virtual_server != NULL) {
+				cf_log_err_cs(cs, "Home servers of type \"coa\" cannot point to a virtual server");
+				goto error;
+			}
+			break;
+
+#ifdef WITH_COA_TUNNEL
+		case HOME_TYPE_AUTH_ACCT_COA:
+			home->dual = true;
+			home->recv_coa = true;
+			break;
+
+		case HOME_TYPE_AUTH_COA:
+			home->recv_coa = true;
+			break;
+#endif
+#endif
+
+  		case HOME_TYPE_INVALID:
+ 			cf_log_err_cs(cs, "Invalid type \"%s\" for home server %s", home->type_str, home->log_name);
+ 			goto error;
+ 		}
+ 	}
+
+ 	{
+ 		home_ping_check_t type = HOME_PING_CHECK_NONE;
+
+ 		if (home->ping_check_str) type = fr_str2int(home_ping_check, home->ping_check_str,
+ 							    HOME_PING_CHECK_INVALID);
+
+ 		switch (type) {
+ 		case HOME_PING_CHECK_STATUS_SERVER:
+ 		case HOME_PING_CHECK_NONE:
+ 			break;
+
+ 		case HOME_PING_CHECK_REQUEST:
+			if (!home->ping_user_name) {
+				cf_log_err_cs(cs, "You must supply a 'username' to enable status_check=request");
+				goto error;
+			}
+
+			if (((home->type == HOME_TYPE_AUTH) ||
+#ifdef WITH_COA_TUNNEL
+			     (home->type == HOME_TYPE_AUTH_COA) ||
+			     (home->type == HOME_TYPE_AUTH_ACCT_COA) ||
+#endif
+			     (home->type == HOME_TYPE_AUTH_ACCT)) && !home->ping_user_password) {
+				cf_log_err_cs(cs, "You must supply a 'password' to enable status_check=request");
+				goto error;
+			}
+
+ 			break;
+
+ 		case HOME_PING_CHECK_INVALID:
+ 			cf_log_err_cs(cs, "Invalid status_check \"%s\" for home server %s",
+ 				      home->ping_check_str, home->log_name);
+ 			goto error;
+ 		}
+
+		home->ping_check = type;
+ 	}
+
+	{
+		int proto = IPPROTO_UDP;
+
+		if (home->proto_str) proto = fr_str2int(home_proto, home->proto_str, -1);
+
+		switch (proto) {
+		case IPPROTO_UDP:
+#ifdef WITH_TCP
+			home_servers_udp = true;
+#endif
+			break;
+
+		case IPPROTO_TCP:
+#ifndef WITH_TCP
+			cf_log_err_cs(cs, "Server not built with support for RADIUS over TCP");
+			goto error;
+#endif
+			if ((home->ping_check != HOME_PING_CHECK_NONE) &&
+			    (home->ping_check != HOME_PING_CHECK_STATUS_SERVER)) {
+				cf_log_err_cs(cs, "Only 'status_check = status-server' is allowed for home "
+					      "servers with 'proto = tcp'");
+				goto error;
+			}
+			break;
+
+		default:
+			cf_log_err_cs(cs, "Unknown proto \"%s\"", home->proto_str);
+			goto error;
+		}
+
+		home->proto = proto;
+	}
+
+	if (!home->virtual_server && rbtree_finddata(home_servers_byaddr, home)) {
+		cf_log_err_cs(cs, "Duplicate home server");
+		goto error;
+	}
+
+	/*
+	 *	Check the TLS configuration.
+	 */
+	tls = cf_section_sub_find(cs, "tls");
+#ifndef WITH_TLS
+	if (tls) {
+		cf_log_err_cs(cs, "TLS transport is not available in this executable");
+		goto error;
+	}
+#endif
+
+	/*
+	 *	Check the reverse CoA configuration.
+	 */
+#ifdef WITH_COA_TUNNEL
+	if (home->recv_coa) {
+		if (!tls) {
+			ERROR("TLS is required in order to accept CoA requests from a home server");
+			goto error;
+		}
+
+		if (!home->recv_coa_server) {
+			ERROR("A 'virtual_server' configuration is required in order to accept CoA requests from a home server");
+			goto error;
+		}
+
+		/*
+		 *	Try and find a 'server' section off the root of
+		 *	the config with a name that matches the coa
+		 *	virtual_server.
+		 */
+		if (!rc) {
+			ERROR("Dynamic home servers cannot accept CoA requests");
+			goto error;
+		}
+
+		if (!cf_section_sub_find_name2(rc->cs, "server", home->recv_coa_server)) {
+			cf_log_err_cs(cs, "No such coa server %s", home->recv_coa_server);
+			goto error;
+		}
+	}
+#endif
+
+	/*
+	 *	If were doing RADSEC (tls+tcp) the secret should default
+	 *	to radsec, else a secret must be set.
+	 */
+	if (!home->secret) {
+#ifdef WITH_TLS
+		if (tls && (home->proto == IPPROTO_TCP)) {
+			home->secret = "radsec";
+		} else
+#endif
+		{
+			cf_log_err_cs(cs, "No shared secret defined for home server %s", home->log_name);
+			goto error;
+		}
+	}
+
+	/*
+	 *	Virtual servers have some TLS restrictions.
+	 */
+	if (home->virtual_server) {
+		if (tls) {
+			cf_log_err_cs(cs, "Virtual home_servers cannot have a \"tls\" subsection");
+			goto error;
+		}
+	} else {
+		/*
+		 *	If the home is not a virtual server, guess the port
+		 *	and look up the source ip address.
+		 */
+		rad_assert(home->ipaddr.af != AF_UNSPEC);
+
+#ifdef WITH_TLS
+		if (tls && (home->proto != IPPROTO_TCP)) {
+			cf_log_err_cs(cs, "TLS transport is not available for UDP sockets");
+			goto error;
+		}
+#endif
+
+		/*
+		 *	Set the default port if necessary.
+		 */
+		if (home->port == 0) {
+			char buffer[INET6_ADDRSTRLEN + 3];
+
+			/*
+			 *	For RADSEC we use the special RADIUS over TCP/TLS port
+			 *	for both accounting and authentication, but for some
+			 *	bizarre reason for RADIUS over plain TCP we use separate
+			 *	ports 1812 and 1813.
+			 */
+#ifdef WITH_TLS
+			if (tls) {
+				home->port = PW_RADIUS_TLS_PORT;
+			} else
+#endif
+			switch (home->type) {
+			default:
+				rad_assert(0);
+				/* FALL-THROUGH */
+
+			/*
+			 *	One is added to get the accounting port
+			 *	for home->dual.
+			 */
+			case HOME_TYPE_AUTH_ACCT:
+			case HOME_TYPE_AUTH:
+				home->port = PW_AUTH_UDP_PORT;
+				break;
+
+			case HOME_TYPE_ACCT:
+				home->port = PW_ACCT_UDP_PORT;
+				break;
+
+			case HOME_TYPE_COA:
+				home->port = PW_COA_UDP_PORT;
+				break;
+			}
+
+			/*
+			 *	Now that we have a real port, use that.
+			 */
+			rad_const_free(home->log_name);
+
+			fr_ntop(buffer, sizeof(buffer), &home->ipaddr);
+
+			home->log_name = talloc_asprintf(home, "%s:%i", buffer, home->port);
+		}
+
+		/*
+		 *	If we have a src_ipaddr_str resolve it to
+		 *	the same address family as the destination
+		 *	IP.
+		 */
+		if (home->src_ipaddr_str) {
+			if (ip_hton(&home->src_ipaddr, home->ipaddr.af, home->src_ipaddr_str, false) < 0) {
+				cf_log_err_cs(cs, "Failed parsing src_ipaddr");
+				goto error;
+			}
+		/*
+		 *	Source isn't specified, set it to the
+		 *	correct address family, but leave it as
+		 *	zeroes.
+		 */
+		} else {
+			home->src_ipaddr.af = home->ipaddr.af;
+		}
+
+#ifdef WITH_TLS
+		/*
+		 *	Parse the SSL client configuration.
+		 */
+		if (tls) {
+			int rcode;
+
+			home->tls = tls_client_conf_parse(tls);
+			if (!home->tls) {
+				goto error;
+			}
+
+			/*
+			 *	Connection timeouts for outgoing TLS connections.
+			 */
+
+			rcode = cf_item_parse(tls, "connect_timeout", FR_ITEM_POINTER(PW_TYPE_INTEGER, &home->connect_timeout), NULL);
+			if (rcode < 0) goto error;
+
+			if (!home->connect_timeout || (home->connect_timeout > 30)) home->connect_timeout = 30;
+
+			home->listeners = rbtree_create(home, listener_cmp, NULL, RBTREE_FLAG_LOCK);
+			if (!home->listeners) goto error;
+
+#ifdef WITH_RADIUSV11
+			if (home->tls->radiusv11_name) {
+				rcode = fr_str2int(radiusv11_types, home->tls->radiusv11_name, -1);
+				if (rcode < 0) {
+					cf_log_err_cs(cs, "Invalid value for 'radiusv11'");
+					goto error;
+				}
+
+				home->tls->radiusv11 = rcode;
+
+				if (fr_radiusv11_client_init(home->tls) < 0) {
+					cf_log_err_cs(cs, "Failed setting OpenSSL callbacks for radiusv11");
+					goto error;
+				}
+			}
+#endif
+
+		}
+#endif
+	} /* end of parse home server */
+
+	realm_home_server_sanitize(home, cs);
+
+	return home;
+}
+
+/** Fixup a client configuration section to specify a home server
+ *
+ * This is used to create the equivalent CoA home server entry for a client,
+ * so that the server can originate CoA messages.
+ *
+ * The server section automatically inherits the following fields from the client:
+ *  - ipaddr/ipv4addr/ipv6addr
+ *  - secret
+ *  - src_ipaddr
+ *
+ * @note new CONF_SECTION will be allocated in the context of the client, but the client
+ *	CONF_SECTION will not be modified.
+ *
+ * @param client CONF_SECTION to inherit values from.
+ * @return a new server CONF_SCTION, or a pointer to the existing CONF_SECTION in the client.
+ */
+CONF_SECTION *home_server_cs_afrom_client(CONF_SECTION *client)
+{
+	CONF_SECTION *server, *cs;
+	CONF_PAIR *cp;
+
+	/*
+	 *	Alloc a plain home server for both cases
+	 *
+	 *	There's no way these can be referenced by a pool,
+	 *	and they may conflict with home servers in proxy.conf
+	 *	so it's easier to not set a name.
+	 */
+
+	/*
+	 *
+	 *	Duplicate the server section, so we don't mangle
+	 *	the client CONF_SECTION we were passed.
+	 */
+	cs = cf_section_sub_find(client, "coa_server");
+	if (cs) {
+		server = cf_section_dup(client, cs, "home_server", NULL, true);
+	} else {
+		server = cf_section_alloc(client, "home_server", cf_section_name2(client));
+	}
+
+	if (!cs || (!cf_pair_find(cs, "ipaddr") && !cf_pair_find(cs, "ipv4addr") && !cf_pair_find(cs, "ipv6addr"))) {
+		cp = cf_pair_find(client, "ipaddr");
+		if (!cp) cp = cf_pair_find(client, "ipv4addr");
+		if (!cp) cp = cf_pair_find(client, "ipv6addr");
+
+		cf_pair_add(server, cf_pair_dup(server, cp));
+	}
+
+	if (!cs || !cf_pair_find(cs, "secret")) {
+		cp = cf_pair_find(client, "secret");
+		if (cp) cf_pair_add(server, cp);
+	}
+
+	if (!cs || !cf_pair_find(cs, "src_ipaddr")) {
+		cp = cf_pair_find(client, "src_ipaddr");
+		if (cp) cf_pair_add(server, cf_pair_dup(server, cp));
+	}
+
+	if (!cs || !(cp = cf_pair_find(cs, "type"))) {
+		cp = cf_pair_alloc(server, "type", "coa", T_OP_EQ, T_BARE_WORD, T_SINGLE_QUOTED_STRING);
+		if (cp) cf_pair_add(server, cf_pair_dup(server, cp));
+	} else if (strcmp(cf_pair_value(cp), "coa") != 0) {
+		talloc_free(server);
+		cf_log_err_cs(server, "server.type must be \"coa\"");
+		return NULL;
+	}
+
+	return server;
+}
+
+static home_pool_t *server_pool_alloc(char const *name, home_pool_type_t type,
+				      home_type_t server_type, int num_home_servers)
+{
+	home_pool_t *pool;
+
+	pool = rad_malloc(sizeof(*pool) + (sizeof(pool->servers[0]) * num_home_servers));
+	if (!pool) return NULL;	/* just for pairanoia */
+
+	memset(pool, 0, sizeof(*pool) + (sizeof(pool->servers[0]) * num_home_servers));
+
+	pool->name = name;
+	pool->type = type;
+	pool->server_type = server_type;
+	pool->num_home_servers = num_home_servers;
+
+	return pool;
+}
+
+/*
+ * Ensure any home_server clauses in a home_server_pool section reference
+ * defined home servers, which should already have been created, regardless
+ * of where they appear in the configuration.
+ */
+static int pool_check_home_server(UNUSED realm_config_t *rc, CONF_PAIR *cp,
+				  char const *name, home_type_t server_type,
+				  home_server_t **phome)
+{
+	home_server_t myhome, *home;
+
+	if (!name) {
+		cf_log_err_cp(cp,
+			   "No value given for home_server");
+		return 0;
+	}
+
+	myhome.name = name;
+	myhome.type = server_type;
+	home = rbtree_finddata(home_servers_byname, &myhome);
+	if (home) {
+		*phome = home;
+		return 1;
+	}
+
+	switch (server_type) {
+	case HOME_TYPE_AUTH:
+	case HOME_TYPE_ACCT:
+		myhome.type = HOME_TYPE_AUTH_ACCT;
+		home = rbtree_finddata(home_servers_byname, &myhome);
+#ifdef WITH_COA_TUNNEL
+		if (!home) {
+			myhome.type = HOME_TYPE_AUTH_COA;
+			home = rbtree_finddata(home_servers_byname, &myhome);
+			if(!home) {
+				myhome.type = HOME_TYPE_AUTH_ACCT_COA;
+				home = rbtree_finddata(home_servers_byname, &myhome);
+			}
+		}
+#endif
+		if (home) {
+			*phome = home;
+			return 1;
+		}
+		break;
+
+	default:
+		break;
+	}
+
+	cf_log_err_cp(cp, "Unknown home_server \"%s\".", name);
+	return 0;
+}
+
+
+#ifndef HAVE_PTHREAD_H
+void realm_pool_free(home_pool_t *pool)
+{
+	if (!event_loop_started) return;
+	if (!realm_config->dynamic) return;
+
+	talloc_free(pool);
+}
+#else  /* HAVE_PTHREAD_H */
+typedef struct pool_list_t pool_list_t;
+
+struct pool_list_t {
+	pool_list_t	*next;
+	home_pool_t	*pool;
+	time_t		when;
+};
+
+static bool		pool_free_init = false;
+static pthread_mutex_t	pool_free_mutex;
+static pool_list_t	*pool_list = NULL;
+
+void realm_pool_free(home_pool_t *pool)
+{
+	int i;
+	time_t now;
+	pool_list_t *this, **last;
+
+	if (!event_loop_started) return;
+	if (!realm_config->dynamic) return;
+
+	if (pool) {
+		/*
+		 *	Double-check that the realm wasn't loaded from the
+		 *	configuration files.
+		 */
+		for (i = 0; i < pool->num_home_servers; i++) {
+			if (pool->servers[i]->cs) {
+				rad_assert(0 == 1);
+				return;
+			}
+		}
+	}
+
+	if (!pool_free_init) {
+		pthread_mutex_init(&pool_free_mutex, NULL);
+		pool_free_init = true;
+	}
+
+	/*
+	 *	Ensure only one caller at a time is freeing a pool.
+	 */
+	pthread_mutex_lock(&pool_free_mutex);
+
+	/*
+	 *	Free all of the pools.
+	 */
+	if (!pool) {
+		while ((this = pool_list) != NULL) {
+			pool_list = this->next;
+			talloc_free(this->pool);
+			talloc_free(this);
+		}
+		pthread_mutex_unlock(&pool_free_mutex);
+		return;
+	}
+
+	now = time(NULL);
+
+	/*
+	 *	Free the oldest pool(s)
+	 */
+	while ((this = pool_list) != NULL) {
+		if (this->when > now) break;
+
+		pool_list = this->next;
+		talloc_free(this->pool);
+		talloc_free(this);
+	}
+
+	/*
+	 *	Add this pool to the end of the list.
+	 */
+	for (last = &pool_list;
+	     *last != NULL;
+	     last = &((*last))->next) {
+		/* do nothing */
+	}
+
+	*last = this = talloc(NULL, pool_list_t);
+	if (!this) {
+		talloc_free(pool); /* hope for the best */
+		pthread_mutex_unlock(&pool_free_mutex);
+		return;
+	}
+
+	this->next = NULL;
+	this->when = now + 300;
+	this->pool = pool;
+	pthread_mutex_unlock(&pool_free_mutex);
+}
+#endif	/* HAVE_PTHREAD_H */
+
+int realm_pool_add(home_pool_t *pool, CONF_SECTION *cs)
+{
+	home_pool_t *old;
+
+	old = rbtree_finddata(home_pools_byname, pool);
+	if (old) {
+		cf_log_err_cs(cs, "Cannot add duplicate home server %s, original is at %s[%d]", pool->name,
+			      cf_section_filename(old->cs), cf_section_lineno(old->cs));
+		return 0;
+	}
+
+	/*
+	 *	The structs aren't mutex protected.  Refuse to destroy
+	 *	the server.
+	 */
+	if (event_loop_started && !realm_config->dynamic) {
+		DEBUG("Must set \"dynamic = true\" in proxy.conf");
+		return 0;
+	}
+
+	if (!rbtree_insert(home_pools_byname, pool)) {
+		rad_assert("Internal sanity check failed" == NULL);
+		return 0;
+	}
+
+	return 1;
+}
+
+static int server_pool_add(realm_config_t *rc,
+			   CONF_SECTION *cs, home_type_t server_type, bool do_print)
+{
+	char const *name2;
+	home_pool_t *pool = NULL;
+	char const *value;
+	CONF_PAIR *cp;
+	int num_home_servers;
+	home_server_t *home;
+
+	name2 = cf_section_name1(cs);
+	if (!name2 || ((strcasecmp(name2, "server_pool") != 0) &&
+		       (strcasecmp(name2, "home_server_pool") != 0))) {
+		cf_log_err_cs(cs,
+			   "Section is not a home_server_pool");
+		return 0;
+	}
+
+	name2 = cf_section_name2(cs);
+	if (!name2) {
+		cf_log_err_cs(cs,
+			   "Server pool section is missing a name");
+		return 0;
+	}
+
+	/*
+	 *	Count the home servers and initalize them.
+	 */
+	num_home_servers = 0;
+	for (cp = cf_pair_find(cs, "home_server");
+	     cp != NULL;
+	     cp = cf_pair_find_next(cs, cp, "home_server")) {
+		num_home_servers++;
+
+		if (!pool_check_home_server(rc, cp, cf_pair_value(cp),
+					    server_type, &home)) {
+			return 0;
+		}
+	}
+
+	if (num_home_servers == 0) {
+		cf_log_err_cs(cs,
+			   "No home servers defined in pool %s",
+			   name2);
+		goto error;
+	}
+
+	pool = server_pool_alloc(name2, HOME_POOL_FAIL_OVER, server_type,
+				 num_home_servers);
+	if (!pool) {
+		cf_log_err_cs(cs, "Failed allocating memory for pool");
+		goto error;
+	}
+	pool->cs = cs;
+
+
+	/*
+	 *	Fallback servers must be defined, and must be
+	 *	virtual servers.
+	 */
+	cp = cf_pair_find(cs, "fallback");
+	if (cp) {
+#ifdef WITH_COA
+		if (server_type == HOME_TYPE_COA) {
+			cf_log_err_cs(cs, "Home server pools of type \"coa\" cannot have a fallback virtual server");
+			goto error;
+		}
+#endif
+
+		if (!pool_check_home_server(rc, cp, cf_pair_value(cp), server_type, &pool->fallback)) {
+			goto error;
+		}
+
+		if (!pool->fallback->virtual_server) {
+			cf_log_err_cs(cs, "Fallback home_server %s does NOT contain a virtual_server directive",
+				      pool->fallback->log_name);
+			goto error;
+		}
+	}
+
+	if (do_print) cf_log_info(cs, " home_server_pool %s {", name2);
+
+	cp = cf_pair_find(cs, "type");
+	if (cp) {
+		static FR_NAME_NUMBER pool_types[] = {
+			{ "load-balance", HOME_POOL_LOAD_BALANCE },
+
+			{ "fail-over", HOME_POOL_FAIL_OVER },
+			{ "fail_over", HOME_POOL_FAIL_OVER },
+
+			{ "round-robin", HOME_POOL_LOAD_BALANCE },
+			{ "round_robin", HOME_POOL_LOAD_BALANCE },
+
+			{ "client-balance", HOME_POOL_CLIENT_BALANCE },
+			{ "client-port-balance", HOME_POOL_CLIENT_PORT_BALANCE },
+			{ "keyed-balance", HOME_POOL_KEYED_BALANCE },
+			{ NULL, 0 }
+		};
+
+		value = cf_pair_value(cp);
+		if (!value) {
+			cf_log_err_cp(cp,
+				   "No value given for type");
+			goto error;
+		}
+
+		pool->type = fr_str2int(pool_types, value, 0);
+		if (!pool->type) {
+			cf_log_err_cp(cp,
+				   "Unknown type \"%s\".",
+				   value);
+			goto error;
+		}
+
+		if (do_print) cf_log_info(cs, "\ttype = %s", value);
+	}
+
+	cp = cf_pair_find(cs, "virtual_server");
+	if (cp) {
+		pool->virtual_server = cf_pair_value(cp);
+		if (!pool->virtual_server) {
+			cf_log_err_cp(cp, "No value given for virtual_server");
+			goto error;
+		}
+
+		if (do_print) {
+			cf_log_info(cs, "\tvirtual_server = %s", pool->virtual_server);
+		}
+
+		if (!cf_section_sub_find_name2(rc->cs, "server", pool->virtual_server)) {
+			cf_log_err_cp(cp, "No such server %s", pool->virtual_server);
+			goto error;
+		}
+
+	}
+
+	num_home_servers = 0;
+	for (cp = cf_pair_find(cs, "home_server");
+	     cp != NULL;
+	     cp = cf_pair_find_next(cs, cp, "home_server")) {
+		home_server_t myhome;
+
+		value = cf_pair_value(cp);
+
+		memset(&myhome, 0, sizeof(myhome));
+		myhome.name = value;
+		myhome.type = server_type;
+
+		home = rbtree_finddata(home_servers_byname, &myhome);
+		if (!home) {
+			switch (server_type) {
+			case HOME_TYPE_AUTH:
+			case HOME_TYPE_ACCT:
+				myhome.type = HOME_TYPE_AUTH_ACCT;
+				home = rbtree_finddata(home_servers_byname, &myhome);
+#ifdef WITH_COA_TUNNEL
+				if (!home) {
+					myhome.type = HOME_TYPE_AUTH_COA;
+					home = rbtree_finddata(home_servers_byname, &myhome);
+					if (!home) {
+						myhome.type = HOME_TYPE_AUTH_ACCT_COA;
+						home = rbtree_finddata(home_servers_byname, &myhome);
+					}
+				}
+#endif
+				break;
+
+			default:
+				break;
+			}
+		}
+
+		if (!home) {
+			ERROR("Failed to find home server %s", value);
+			goto error;
+		}
+
+		if (do_print) cf_log_info(cs, "\thome_server = %s", home->name);
+		pool->servers[num_home_servers++] = home;
+	} /* loop over home_server's */
+
+	if (pool->fallback && do_print) {
+		cf_log_info(cs, "\tfallback = %s", pool->fallback->name);
+	}
+
+	if (!realm_pool_add(pool, cs)) goto error;
+
+	if (do_print) cf_log_info(cs, " }");
+
+	cf_data_add(cs, "home_server_pool", pool, free);
+
+	rad_assert(pool->server_type != 0);
+
+	return 1;
+
+ error:
+	if (do_print) cf_log_info(cs, " }");
+	free(pool);
+	return 0;
+}
+#endif
+
+static int old_server_add(realm_config_t *rc, CONF_SECTION *cs,
+			  char const *realm,
+			  char const *name, char const *secret,
+			  home_pool_type_t ldflag, home_pool_t **pool_p,
+			  home_type_t type, char const *server)
+{
+#ifdef WITH_PROXY
+	int i, insert_point, num_home_servers;
+	home_server_t myhome, *home;
+	home_pool_t mypool, *pool;
+	CONF_SECTION *subcs;
+#else
+	(void) rc;		/* -Wunused */
+	(void) realm;
+	(void) secret;
+	(void) ldflag;
+	(void) type;
+	(void) server;
+#endif
+
+	/*
+	 *	LOCAL realms get sanity checked, and nothing else happens.
+	 */
+	if (strcmp(name, "LOCAL") == 0) {
+		if (*pool_p) {
+			cf_log_err_cs(cs, "Realm \"%s\" cannot be both LOCAL and remote", name);
+			return 0;
+		}
+		return 1;
+	}
+
+#ifndef WITH_PROXY
+	return 0;		/* Not proxying.  Can't do non-LOCAL realms */
+
+#else
+	mypool.name = realm;
+	mypool.server_type = type;
+	pool = rbtree_finddata(home_pools_byname, &mypool);
+	if (pool) {
+		if (pool->type != ldflag) {
+			cf_log_err_cs(cs, "Inconsistent ldflag for server pool \"%s\"", name);
+			return 0;
+		}
+
+		if (pool->server_type != type) {
+			cf_log_err_cs(cs, "Inconsistent home server type for server pool \"%s\"", name);
+			return 0;
+		}
+	}
+
+	myhome.name = name;
+	myhome.type = type;
+	home = rbtree_finddata(home_servers_byname, &myhome);
+	if (home) {
+		WARN("Please use pools instead of authhost and accthost");
+
+		if (secret && (strcmp(home->secret, secret) != 0)) {
+			cf_log_err_cs(cs, "Inconsistent shared secret for home server \"%s\"", name);
+			return 0;
+		}
+
+		if (home->type != type) {
+			cf_log_err_cs(cs, "Inconsistent type for home server \"%s\"", name);
+			return 0;
+		}
+
+		/*
+		 *	Don't check for duplicate home servers.  If
+		 *	the user specifies that, well, they can do it.
+		 *
+		 *	Allowing duplicates means that all of the
+		 *	realm->server[] entries are filled, which is
+		 *	what the rest of the code assumes.
+		 */
+	}
+
+	/*
+	 *	If we do have a pool, check that there is room to
+	 *	insert the home server we've found, or the one that we
+	 *	create here.
+	 *
+	 *	Note that we insert it into the LAST available
+	 *	position, in order to maintain the same order as in
+	 *	the configuration files.
+	 */
+	insert_point = -1;
+	if (pool) {
+		for (i = pool->num_home_servers - 1; i >= 0; i--) {
+			if (pool->servers[i]) break;
+
+			if (!pool->servers[i]) {
+				insert_point = i;
+			}
+		}
+
+		if (insert_point < 0) {
+			cf_log_err_cs(cs, "No room in pool to add home server \"%s\".  Please update the realm configuration to use the new-style home servers and server pools.", name);
+			return 0;
+		}
+	}
+
+	/*
+	 *	No home server, allocate one.
+	 */
+	if (!home) {
+		char const *p;
+		char *q;
+
+		home = talloc_zero(rc, home_server_t);
+		home->name = name;
+		home->type = type;
+		home->secret = secret;
+		home->cs = cs;
+		home->proto = IPPROTO_UDP;
+
+		p = strchr(name, ':');
+		if (!p) {
+			if (type == HOME_TYPE_AUTH) {
+				home->port = PW_AUTH_UDP_PORT;
+			} else {
+				home->port = PW_ACCT_UDP_PORT;
+			}
+
+			p = name;
+			q = NULL;
+
+		} else if (p == name) {
+			cf_log_err_cs(cs, "Invalid hostname %s", name);
+			talloc_free(home);
+			return 0;
+		} else {
+			unsigned long port = strtoul(p + 1, NULL, 0);
+			if ((port == 0) || (port > 65535)) {
+				cf_log_err_cs(cs, "Invalid port %s", p + 1);
+				talloc_free(home);
+				return 0;
+			}
+
+			home->port = (uint16_t)port;
+			q = talloc_array(home, char, (p - name) + 1);
+			memcpy(q, name, (p - name));
+			q[p - name] = '\0';
+			p = q;
+		}
+
+		if (!server) {
+			if (ip_hton(&home->ipaddr, AF_UNSPEC, p, false) < 0) {
+				cf_log_err_cs(cs,
+					   "Failed looking up hostname %s.",
+					   p);
+				talloc_free(home);
+				talloc_free(q);
+				return 0;
+			}
+			home->src_ipaddr.af = home->ipaddr.af;
+		} else {
+			home->ipaddr.af = AF_UNSPEC;
+			home->virtual_server = server;
+		}
+		talloc_free(q);
+
+		/*
+		 *	Use the old-style configuration.
+		 */
+		home->max_outstanding = 65535*16;
+		home->zombie_period = rc->retry_delay * rc->retry_count;
+		if (home->zombie_period < 2) home->zombie_period = 30;
+		home->response_window.tv_sec = home->zombie_period - 1;
+		home->response_window.tv_usec = 0;
+
+		home->ping_check = HOME_PING_CHECK_NONE;
+
+		home->revive_interval = rc->dead_time;
+
+		if (rbtree_finddata(home_servers_byaddr, home)) {
+			cf_log_err_cs(cs, "Home server %s has the same IP address and/or port as another home server.", name);
+			talloc_free(home);
+			return 0;
+		}
+
+		if (!rbtree_insert(home_servers_byname, home)) {
+			cf_log_err_cs(cs, "Internal error %d adding home server %s.", __LINE__, name);
+			talloc_free(home);
+			return 0;
+		}
+
+		if (!rbtree_insert(home_servers_byaddr, home)) {
+			rbtree_deletebydata(home_servers_byname, home);
+			cf_log_err_cs(cs, "Internal error %d adding home server %s.", __LINE__, name);
+			talloc_free(home);
+			return 0;
+		}
+
+#ifdef WITH_STATS
+		home->number = home_server_max_number++;
+		if (!rbtree_insert(home_servers_bynumber, home)) {
+			rbtree_deletebydata(home_servers_byname, home);
+			if (home->ipaddr.af != AF_UNSPEC) {
+				rbtree_deletebydata(home_servers_byname, home);
+			}
+			cf_log_err_cs(cs,
+				   "Internal error %d adding home server %s.",
+				   __LINE__, name);
+			talloc_free(home);
+			return 0;
+		}
+#endif
+	}
+
+	/*
+	 *	We now have a home server, see if we can insert it
+	 *	into pre-existing pool.
+	 */
+	if (insert_point >= 0) {
+		rad_assert(pool != NULL);
+		pool->servers[insert_point] = home;
+		return 1;
+	}
+
+	rad_assert(pool == NULL);
+	rad_assert(home != NULL);
+
+	/*
+	 *	Count the old-style realms of this name.
+	 */
+	num_home_servers = 0;
+	for (subcs = cf_section_find_next(cs, NULL, "realm");
+	     subcs != NULL;
+	     subcs = cf_section_find_next(cs, subcs, "realm")) {
+		char const *this = cf_section_name2(subcs);
+
+		if (!this || (strcmp(this, realm) != 0)) continue;
+		num_home_servers++;
+	}
+
+	if (num_home_servers == 0) {
+		cf_log_err_cs(cs, "Internal error counting pools for home server %s.", name);
+		talloc_free(home);
+		return 0;
+	}
+
+	pool = server_pool_alloc(realm, ldflag, type, num_home_servers);
+	if (!pool) {
+		cf_log_err_cs(cs, "Out of memory");
+		return 0;
+	}
+
+	pool->cs = cs;
+
+	pool->servers[0] = home;
+
+	if (!rbtree_insert(home_pools_byname, pool)) {
+		rad_assert("Internal sanity check failed" == NULL);
+		return 0;
+	}
+
+	*pool_p = pool;
+
+	return 1;
+#endif
+}
+
+static int old_realm_config(realm_config_t *rc, CONF_SECTION *cs, REALM *r)
+{
+	char const *host;
+	char const *secret = NULL;
+	home_pool_type_t ldflag;
+	CONF_PAIR *cp;
+
+	cp = cf_pair_find(cs, "ldflag");
+	ldflag = HOME_POOL_FAIL_OVER;
+	if (cp) {
+		host = cf_pair_value(cp);
+		if (!host) {
+			cf_log_err_cp(cp, "No value specified for ldflag");
+			return 0;
+		}
+
+		if (strcasecmp(host, "fail_over") == 0) {
+			cf_log_info(cs, "\tldflag = fail_over");
+
+		} else if (strcasecmp(host, "round_robin") == 0) {
+			ldflag = HOME_POOL_LOAD_BALANCE;
+			cf_log_info(cs, "\tldflag = round_robin");
+
+		} else {
+			cf_log_err_cs(cs, "Unknown value \"%s\" for ldflag", host);
+			return 0;
+		}
+	} /* else don't print it. */
+
+	/*
+	 *	Allow old-style if it doesn't exist, or if it exists and
+	 *	it's LOCAL.
+	 */
+	cp = cf_pair_find(cs, "authhost");
+	if (cp) {
+		host = cf_pair_value(cp);
+		if (!host) {
+			cf_log_err_cp(cp, "No value specified for authhost");
+			return 0;
+		}
+
+		if (strcmp(host, "LOCAL") != 0) {
+			cp = cf_pair_find(cs, "secret");
+			if (!cp) {
+				cf_log_err_cs(cs, "No shared secret supplied for realm: %s", r->name);
+				return 0;
+			}
+
+			secret = cf_pair_value(cp);
+			if (!secret) {
+				cf_log_err_cp(cp, "No value specified for secret");
+				return 0;
+			}
+		}
+
+		cf_log_info(cs, "\tauthhost = %s",  host);
+
+		if (!old_server_add(rc, cs, r->name, host, secret, ldflag,
+				    &r->auth_pool, HOME_TYPE_AUTH, NULL)) {
+			return 0;
+		}
+	}
+
+	cp = cf_pair_find(cs, "accthost");
+	if (cp) {
+		host = cf_pair_value(cp);
+		if (!host) {
+			cf_log_err_cp(cp, "No value specified for accthost");
+			return 0;
+		}
+
+		/*
+		 *	Don't look for a secret again if it was found
+		 *	above.
+		 */
+		if ((strcmp(host, "LOCAL") != 0) && !secret) {
+			cp = cf_pair_find(cs, "secret");
+			if (!cp) {
+				cf_log_err_cs(cs, "No shared secret supplied for realm: %s", r->name);
+				return 0;
+			}
+
+			secret = cf_pair_value(cp);
+			if (!secret) {
+				cf_log_err_cp(cp, "No value specified for secret");
+				return 0;
+			}
+		}
+
+		cf_log_info(cs, "\taccthost = %s", host);
+
+		if (!old_server_add(rc, cs, r->name, host, secret, ldflag,
+				    &r->acct_pool, HOME_TYPE_ACCT, NULL)) {
+			return 0;
+		}
+	}
+
+	cp = cf_pair_find(cs, "virtual_server");
+	if (cp) {
+		host = cf_pair_value(cp);
+		if (!host) {
+			cf_log_err_cp(cp, "No value specified for virtual_server");
+			return 0;
+		}
+
+		cf_log_info(cs, "\tvirtual_server = %s", host);
+
+		if (!old_server_add(rc, cs, r->name, host, "", ldflag,
+				    &r->auth_pool, HOME_TYPE_AUTH, host)) {
+			return 0;
+		}
+		if (!old_server_add(rc, cs, r->name, host, "", ldflag,
+				    &r->acct_pool, HOME_TYPE_ACCT, host)) {
+			return 0;
+		}
+	}
+
+	if (secret) {
+		if (rad_debug_lvl <= 2) {
+			cf_log_info(cs, "\tsecret = <<< secret >>>");
+		} else {
+			cf_log_info(cs, "\tsecret = %s", secret);
+		}
+	}
+
+	return 1;
+
+}
+
+
+#ifdef WITH_PROXY
+static int add_pool_to_realm(realm_config_t *rc, CONF_SECTION *cs,
+			     char const *name, home_pool_t **dest,
+			     home_type_t server_type, bool do_print)
+{
+	home_pool_t mypool, *pool;
+
+	mypool.name = name;
+	mypool.server_type = server_type;
+
+	pool = rbtree_finddata(home_pools_byname, &mypool);
+	if (!pool) {
+		CONF_SECTION *pool_cs;
+
+		pool_cs = cf_section_sub_find_name2(rc->cs,
+						    "home_server_pool",
+						    name);
+		if (!pool_cs) {
+			pool_cs = cf_section_sub_find_name2(rc->cs,
+							    "server_pool",
+							    name);
+		}
+		if (!pool_cs) {
+			cf_log_err_cs(cs, "Failed to find home_server_pool \"%s\"", name);
+			return 0;
+		}
+
+		if (!server_pool_add(rc, pool_cs, server_type, do_print)) {
+			return 0;
+		}
+
+		pool = rbtree_finddata(home_pools_byname, &mypool);
+		if (!pool) {
+			ERROR("Internal sanity check failed in add_pool_to_realm");
+			return 0;
+		}
+	}
+
+	if (pool->server_type != server_type) {
+		cf_log_err_cs(cs, "Incompatible home_server_pool \"%s\" (mixed auth_pool / acct_pool)", name);
+		return 0;
+	}
+
+	*dest = pool;
+
+	return 1;
+}
+#endif
+
+
+static int realm_add(realm_config_t *rc, CONF_SECTION *cs)
+{
+	char const *name2;
+	REALM *r = NULL;
+	CONF_PAIR *cp;
+#ifdef WITH_PROXY
+	home_pool_t *auth_pool, *acct_pool;
+	char const *auth_pool_name, *acct_pool_name;
+#ifdef WITH_COA
+	char const *coa_pool_name;
+	home_pool_t *coa_pool;
+#endif
+#endif
+
+	name2 = cf_section_name1(cs);
+	if (!name2 || (strcasecmp(name2, "realm") != 0)) {
+		cf_log_err_cs(cs, "Section is not a realm");
+		return 0;
+	}
+
+	name2 = cf_section_name2(cs);
+	if (!name2) {
+		cf_log_err_cs(cs, "Realm section is missing the realm name");
+		return 0;
+	}
+
+#ifdef WITH_PROXY
+	auth_pool = acct_pool = NULL;
+	auth_pool_name = acct_pool_name = NULL;
+#ifdef WITH_COA
+	coa_pool = NULL;
+	coa_pool_name = NULL;
+#endif
+
+	/*
+	 *	Prefer new configuration to old one.
+	 */
+	cp = cf_pair_find(cs, "pool");
+	if (!cp) cp = cf_pair_find(cs, "home_server_pool");
+	if (cp) auth_pool_name = cf_pair_value(cp);
+	if (cp && auth_pool_name) {
+		acct_pool_name = auth_pool_name;
+		if (!add_pool_to_realm(rc, cs,
+				       auth_pool_name, &auth_pool,
+				       HOME_TYPE_AUTH, 1)) {
+			return 0;
+		}
+		if (!add_pool_to_realm(rc, cs,
+				       auth_pool_name, &acct_pool,
+				       HOME_TYPE_ACCT, 0)) {
+			return 0;
+		}
+	}
+
+	cp = cf_pair_find(cs, "auth_pool");
+	if (cp) auth_pool_name = cf_pair_value(cp);
+	if (cp && auth_pool_name) {
+		if (auth_pool) {
+			cf_log_err_cs(cs, "Cannot use \"pool\" and \"auth_pool\" at the same time");
+			return 0;
+		}
+		if (!add_pool_to_realm(rc, cs,
+				       auth_pool_name, &auth_pool,
+				       HOME_TYPE_AUTH, 1)) {
+			return 0;
+		}
+	}
+
+	cp = cf_pair_find(cs, "acct_pool");
+	if (cp) acct_pool_name = cf_pair_value(cp);
+	if (cp && acct_pool_name) {
+		bool do_print = true;
+
+		if (acct_pool) {
+			cf_log_err_cs(cs, "Cannot use \"pool\" and \"acct_pool\" at the same time");
+			return 0;
+		}
+
+		if (!auth_pool ||
+		    (auth_pool_name &&
+		     (strcmp(auth_pool_name, acct_pool_name) != 0))) {
+			do_print = true;
+		}
+
+		if (!add_pool_to_realm(rc, cs,
+				       acct_pool_name, &acct_pool,
+				       HOME_TYPE_ACCT, do_print)) {
+			return 0;
+		}
+	}
+
+#ifdef WITH_COA
+	cp = cf_pair_find(cs, "coa_pool");
+	if (cp) coa_pool_name = cf_pair_value(cp);
+	if (cp && coa_pool_name) {
+		bool do_print = true;
+
+		if (!add_pool_to_realm(rc, cs,
+				       coa_pool_name, &coa_pool,
+				       HOME_TYPE_COA, do_print)) {
+			return 0;
+		}
+	}
+#endif
+#endif
+
+	cf_log_info(cs, " realm %s {", name2);
+
+#ifdef WITH_PROXY
+	/*
+	 *	The realm MAY already exist if it's an old-style realm.
+	 *	In that case, merge the old-style realm with this one.
+	 */
+	r = realm_find2(name2);
+	if (r && (strcmp(r->name, name2) == 0)) {
+		if (cf_pair_find(cs, "auth_pool") ||
+		    cf_pair_find(cs, "acct_pool")) {
+			cf_log_err_cs(cs, "Duplicate realm \"%s\"", name2);
+			goto error;
+		}
+
+		if (!old_realm_config(rc, cs, r)) {
+			goto error;
+		}
+
+		cf_log_info(cs, " } # realm %s", name2);
+		return 1;
+	}
+#endif
+
+	r = talloc_zero(rc, REALM);
+	r->name = name2;
+	r->strip_realm = true;
+#ifdef WITH_PROXY
+	r->auth_pool = auth_pool;
+	r->acct_pool = acct_pool;
+#ifdef WITH_COA
+	r->coa_pool = coa_pool;
+#endif
+
+	if (auth_pool_name &&
+	    (auth_pool_name == acct_pool_name)) { /* yes, ptr comparison */
+		cf_log_info(cs, "\tpool = %s", auth_pool_name);
+	} else {
+		if (auth_pool_name) cf_log_info(cs, "\tauth_pool = %s", auth_pool_name);
+		if (acct_pool_name) cf_log_info(cs, "\tacct_pool = %s", acct_pool_name);
+#ifdef WITH_COA
+		if (coa_pool_name) cf_log_info(cs, "\tcoa_pool = %s", coa_pool_name);
+#endif
+	}
+#endif
+
+	cp = cf_pair_find(cs, "nostrip");
+	if (cp && (cf_pair_value(cp) == NULL)) {
+		r->strip_realm = false;
+		cf_log_info(cs, "\tnostrip");
+	}
+
+	/*
+	 *	We're a new-style realm.  Complain if we see the old
+	 *	directives.
+	 */
+	if (r->auth_pool || r->acct_pool) {
+		if (((cp = cf_pair_find(cs, "authhost")) != NULL) ||
+		    ((cp = cf_pair_find(cs, "accthost")) != NULL) ||
+		    ((cp = cf_pair_find(cs, "secret")) != NULL) ||
+		    ((cp = cf_pair_find(cs, "ldflag")) != NULL)) {
+			WARN("Ignoring old-style configuration entry \"%s\" in realm \"%s\"", cf_pair_attr(cp), r->name);
+		}
+
+
+		/*
+		 *	The realm MAY be an old-style realm, as there
+		 *	was no auth_pool or acct_pool.  Double-check
+		 *	it, just to be safe.
+		 */
+	} else if (!old_realm_config(rc, cs, r)) {
+		goto error;
+	}
+
+	if (!realm_realm_add(r, cs)) {
+		goto error;
+	}
+
+	cf_log_info(cs, " }");
+
+	return 1;
+
+ error:
+	cf_log_info(cs, " } # realm %s", name2);
+	return 0;
+}
+
+#ifdef HAVE_REGEX
+int realm_realm_add(REALM *r, CONF_SECTION *cs)
+#else
+int realm_realm_add(REALM *r, UNUSED CONF_SECTION *cs)
+#endif
+{
+	/*
+	 *	The structs aren't mutex protected.  Refuse to destroy
+	 *	the server.
+	 */
+	if (event_loop_started && !realm_config->dynamic) {
+		DEBUG("Must set \"dynamic = true\" in proxy.conf");
+		return 0;
+	}
+
+#ifdef HAVE_REGEX
+	/*
+	 *	It's a regex.  Sanity check it, and add it to a
+	 *	separate list.
+	 */
+	if (r->name[0] == '~') {
+		ssize_t slen;
+		realm_regex_t *rr, **last;
+
+		rr = talloc(r, realm_regex_t);
+
+		/*
+		 *	Include substring matches.
+		 */
+		slen = regex_compile(rr, &rr->preg, r->name + 1, strlen(r->name) - 1, true, false, false, false);
+		if (slen <= 0) {
+			char *spaces, *text;
+
+			fr_canonicalize_error(r, &spaces, &text, slen, r->name + 1);
+
+			cf_log_err_cs(cs, "Invalid regular expression:");
+			cf_log_err_cs(cs, "%s", text);
+			cf_log_err_cs(cs, "%s^ %s", spaces, fr_strerror());
+
+			talloc_free(spaces);
+			talloc_free(text);
+			talloc_free(rr);
+
+			return 0;
+		}
+
+		last = &realms_regex;
+		while (*last) last = &((*last)->next);  /* O(N^2)... sue me. */
+
+		rr->realm = r;
+		rr->next = NULL;
+
+		*last = rr;
+		return 1;
+	}
+#endif
+
+	if (!rbtree_insert(realms_byname, r)) {
+		rad_assert("Internal sanity check failed" == NULL);
+		return 0;
+	}
+
+	return 1;
+}
+
+#ifdef WITH_COA
+
+static int pool_peek_type(CONF_SECTION *config, CONF_SECTION *cs)
+{
+	int home;
+	char const *name, *type;
+	CONF_PAIR *cp;
+	CONF_SECTION *server_cs;
+
+	cp = cf_pair_find(cs, "home_server");
+	if (!cp) {
+		cf_log_err_cs(cs, "Pool does not contain a \"home_server\" entry");
+		return HOME_TYPE_INVALID;
+	}
+
+	name = cf_pair_value(cp);
+	if (!name) {
+		cf_log_err_cp(cp, "home_server entry does not reference a home server");
+		return HOME_TYPE_INVALID;
+	}
+
+	server_cs = cf_section_sub_find_name2(config, "home_server", name);
+	if (!server_cs) {
+		cf_log_err_cp(cp, "home_server \"%s\" does not exist", name);
+		return HOME_TYPE_INVALID;
+	}
+
+	cp = cf_pair_find(server_cs, "type");
+	if (!cp) {
+		cf_log_err_cs(server_cs, "home_server %s does not contain a \"type\" entry", name);
+		return HOME_TYPE_INVALID;
+	}
+
+	type = cf_pair_value(cp);
+	if (!type) {
+		cf_log_err_cs(server_cs, "home_server %s contains an empty \"type\" entry", name);
+		return HOME_TYPE_INVALID;
+	}
+
+	home = fr_str2int(home_server_types, type, HOME_TYPE_INVALID);
+	if (home == HOME_TYPE_INVALID) {
+		cf_log_err_cs(server_cs, "home_server %s contains an invalid \"type\" entry of value \"%s\"", name, type);
+		return HOME_TYPE_INVALID;
+	}
+
+	return home;		/* 'cause we miss it so much */
+}
+#endif
+
+int realms_init(CONF_SECTION *config)
+{
+	CONF_SECTION *cs;
+	int flags = 0;
+#ifdef WITH_PROXY
+	CONF_SECTION *server_cs;
+#endif
+	realm_config_t *rc;
+
+	if (event_loop_started) return 1;
+
+	rc = talloc_zero(NULL, realm_config_t);
+	rc->cs = config;
+
+#ifdef WITH_PROXY
+	cs = cf_subsection_find_next(config, NULL, "proxy");
+	if (cs) {
+		if (cf_section_parse(cs, rc, proxy_config) < 0) {
+			ERROR("Failed parsing proxy section");
+			goto error;
+		}
+	} else {
+		rc->dead_time = DEAD_TIME;
+		rc->retry_count = RETRY_COUNT;
+		rc->retry_delay = RETRY_DELAY;
+		rc->fallback = false;
+		rc->dynamic = false;
+		rc->wake_all_if_all_dead= 0;
+	}
+
+	if (rc->dynamic) {
+		flags = RBTREE_FLAG_LOCK;
+	}
+
+	home_servers_byaddr = rbtree_create(NULL, home_server_addr_cmp, home_server_free, flags);
+	if (!home_servers_byaddr) goto error;
+
+	home_servers_byname = rbtree_create(NULL, home_server_name_cmp, NULL, flags);
+	if (!home_servers_byname) goto error;
+
+#ifdef WITH_STATS
+	home_servers_bynumber = rbtree_create(NULL, home_server_number_cmp, NULL, flags);
+	if (!home_servers_bynumber) goto error;
+#endif
+
+	home_pools_byname = rbtree_create(NULL, home_pool_name_cmp, NULL, flags);
+	if (!home_pools_byname) goto error;
+
+	for (cs = cf_subsection_find_next(config, NULL, "home_server");
+	     cs != NULL;
+	     cs = cf_subsection_find_next(config, cs, "home_server")) {
+	     	home_server_t *home;
+
+	     	home = home_server_afrom_cs(rc, rc, cs);
+	     	if (!home) goto error;
+		if (!realm_home_server_add(home)) goto error;
+	}
+
+	/*
+	 *	Loop over virtual servers to find home servers which
+	 *	are defined in them.
+	 */
+	for (server_cs = cf_subsection_find_next(config, NULL, "server");
+	     server_cs != NULL;
+	     server_cs = cf_subsection_find_next(config, server_cs, "server")) {
+		for (cs = cf_subsection_find_next(server_cs, NULL, "home_server");
+		     cs != NULL;
+		     cs = cf_subsection_find_next(server_cs, cs, "home_server")) {
+			home_server_t *home;
+
+			home = home_server_afrom_cs(rc, rc, cs);
+			if (!home) goto error;
+			if (!realm_home_server_add(home)) goto error;
+		}
+	}
+#endif
+
+	/*
+	 *	Now create the realms, which point to the home servers
+	 *	and home server pools.
+	 */
+	realms_byname = rbtree_create(NULL, realm_name_cmp, NULL, flags);
+	if (!realms_byname) goto error;
+
+	for (cs = cf_subsection_find_next(config, NULL, "realm");
+	     cs != NULL;
+	     cs = cf_subsection_find_next(config, cs, "realm")) {
+		if (!realm_add(rc, cs)) {
+		error:
+			realms_free();
+			/*
+			 *	Must be called after realms_free as home_servers
+			 *	parented by rc are in trees freed by realms_free()
+			 */
+			talloc_free(rc);
+			return 0;
+		}
+	}
+
+#ifdef WITH_COA
+	/*
+	 *	CoA pools aren't necessarily tied to realms.
+	 */
+	for (cs = cf_subsection_find_next(config, NULL, "home_server_pool");
+	     cs != NULL;
+	     cs = cf_subsection_find_next(config, cs, "home_server_pool")) {
+		int type;
+
+		/*
+		 *	Pool was already loaded.
+		 */
+		if (cf_data_find(cs, "home_server_pool")) continue;
+
+		type = pool_peek_type(config, cs);
+		if (type == HOME_TYPE_INVALID) goto error;
+		if (!server_pool_add(rc, cs, type, true)) goto error;
+	}
+#endif
+
+#ifdef WITH_PROXY
+	xlat_register("home_server", xlat_home_server, NULL, NULL);
+	xlat_register("home_server_pool", xlat_server_pool, NULL, NULL);
+	xlat_register("home_server_dynamic", xlat_home_server_dynamic, NULL, NULL);
+#endif
+
+	realm_config = rc;
+
+#ifdef HAVE_DIRENT_H
+	if (!rc->dynamic) {
+		if (rc->directory) {
+			WARN("Ignoring 'directory' as dynamic home servers were not configured.");
+		}
+	} else {
+		DIR		*dir;
+		struct dirent	*dp;
+
+		if (!rc->directory) {
+			WARN("Ignoring \"dynamic = true\" due to not set \"directory\" in proxy.conf");
+			return 1;
+		}
+
+		DEBUG2("including files in directory %s", rc->directory);
+
+		dir = opendir(rc->directory);
+		if (!dir) {
+			cf_log_err_cs(config, "Error reading directory %s: %s",
+				      rc->directory, fr_syserror(errno));				      
+			goto error;
+		}
+
+		/*
+		 *	Read the directory, ignoring "." files.
+		 */
+		while ((dp = readdir(dir)) != NULL) {
+			char const *p;
+			char conf_file[PATH_MAX];
+
+			if (dp->d_name[0] == '.') continue;
+
+			/*
+			 *	Skip the TLS configuration.
+			 */
+			if (strcmp(dp->d_name, "tls.conf") == 0) continue;
+		
+			/*
+			 *	Check for valid characters
+			 */
+			for (p = dp->d_name; *p != '\0'; p++) {
+				if (isalpha((uint8_t)*p) ||
+				    isdigit((uint8_t)*p) ||
+				    (*p == '-') ||
+				    (*p == '_') ||
+				    (*p == '.')) continue;
+				break;
+			}
+			if (*p != '\0') continue;
+
+			snprintf(conf_file, sizeof(conf_file), "%s/%s", rc->directory, dp->d_name);
+			if (home_server_afrom_file(conf_file) < 0) {
+				ERROR("Failed reading home_server from %s - %s",
+				      conf_file, fr_strerror());
+				closedir(dir);
+				goto error;
+			}
+		}
+		closedir(dir);
+	}
+#endif
+
+	return 1;
+}
+
+/*
+ *	Find a realm where "name" might be the regex.
+ */
+REALM *realm_find2(char const *name)
+{
+	REALM myrealm;
+	REALM *realm;
+
+	if (!name) name = "NULL";
+
+	myrealm.name = name;
+	realm = rbtree_finddata(realms_byname, &myrealm);
+	if (realm) return realm;
+
+#ifdef HAVE_REGEX
+	if (realms_regex) {
+		realm_regex_t *this;
+
+		for (this = realms_regex; this != NULL; this = this->next) {
+			if (strcmp(this->realm->name, name) == 0) {
+				return this->realm;
+			}
+		}
+	}
+#endif
+
+	/*
+	 *	Couldn't find a realm.  Look for DEFAULT.
+	 */
+	myrealm.name = "DEFAULT";
+	return rbtree_finddata(realms_byname, &myrealm);
+}
+
+
+/*
+ *	Find a realm in the REALM list.
+ */
+REALM *realm_find(char const *name)
+{
+	REALM myrealm;
+	REALM *realm;
+
+	if (!name) name = "NULL";
+
+	myrealm.name = name;
+	realm = rbtree_finddata(realms_byname, &myrealm);
+	if (realm) return realm;
+
+#ifdef HAVE_REGEX
+	if (realms_regex) {
+		realm_regex_t *this;
+
+		for (this = realms_regex;
+		     this != NULL;
+		     this = this->next) {
+			int compare;
+
+			compare = regex_exec(this->preg, name, strlen(name), NULL, NULL);
+			if (compare < 0) {
+				ERROR("Failed performing realm comparison: %s", fr_strerror());
+				return NULL;
+			}
+			if (compare == 1) return this->realm;
+		}
+	}
+#endif
+
+	/*
+	 *	Couldn't find a realm.  Look for DEFAULT.
+	 */
+	myrealm.name = "DEFAULT";
+	return rbtree_finddata(realms_byname, &myrealm);
+}
+
+
+#ifdef WITH_PROXY
+
+/*
+ *	Allocate the proxy list if it doesn't already exist, and copy request
+ *	VPs into it. Setup src/dst IP addresses based on home server, and
+ *	calculate and add the message-authenticator.
+ *
+ *	This is a distinct function from home_server_ldb, as not all home_server_t
+ *	lookups result in the *CURRENT* request being proxied,
+ *	as in rlm_replicate, and this may trigger asserts elsewhere in the
+ *	server.
+ */
+void home_server_update_request(home_server_t *home, REQUEST *request)
+{
+
+	/*
+	 *	Allocate the proxy packet, only if it wasn't
+	 *	already allocated by a module.  This check is
+	 *	mainly to support the proxying of EAP-TTLS and
+	 *	EAP-PEAP tunneled requests.
+	 *
+	 *	In those cases, the EAP module creates a
+	 *	"fake" request, and recursively passes it
+	 *	through the authentication stage of the
+	 *	server.  The module then checks if the request
+	 *	was supposed to be proxied, and if so, creates
+	 *	a proxy packet from the TUNNELED request, and
+	 *	not from the EAP request outside of the
+	 *	tunnel.
+	 *
+	 *	The proxy then works like normal, except that
+	 *	the response packet is "eaten" by the EAP
+	 *	module, and encapsulated into an EAP packet.
+	 */
+	if (!request->proxy) {
+		request->proxy = rad_alloc(request, true);
+		if (!request->proxy) {
+			ERROR("no memory");
+			fr_exit(1);
+		}
+
+		/*
+		 *	Copy the request, then look up name
+		 *	and plain-text password in the copy.
+		 *
+		 *	Note that the User-Name attribute is
+		 *	the *original* as sent over by the
+		 *	client.  The Stripped-User-Name
+		 *	attribute is the one hacked through
+		 *	the 'hints' file.
+		 */
+		request->proxy->vps = fr_pair_list_copy(request->proxy,
+							request->packet->vps);
+	}
+
+	/*
+	 *	Update the various fields as appropriate.
+	 */
+	request->proxy->src_ipaddr = home->src_ipaddr;
+	request->proxy->src_port = 0;
+	request->proxy->dst_ipaddr = home->ipaddr;
+	request->proxy->dst_port = home->port;
+#ifdef WITH_TCP
+	request->proxy->proto = home->proto;
+#endif
+	request->home_server = home;
+
+	/*
+	 *	Access-Requests have a Message-Authenticator added,
+	 *	unless one already exists.
+	 */
+	if ((request->packet->code == PW_CODE_ACCESS_REQUEST) &&
+#ifdef WITH_RADIUSV11
+	    !request->proxy->radiusv11 &&
+#endif
+	    !fr_pair_find_by_num(request->proxy->vps, PW_MESSAGE_AUTHENTICATOR, 0, TAG_ANY)) {
+		fr_pair_make(request->proxy, &request->proxy->vps,
+			 "Message-Authenticator", "0x00",
+			 T_OP_SET);
+	}
+}
+
+home_server_t *home_server_ldb(char const *realmname,
+			     home_pool_t *pool, REQUEST *request)
+{
+	int		start;
+	int		count;
+	home_server_t	*found = NULL;
+	home_server_t	*zombie = NULL;
+	VALUE_PAIR	*vp;
+	uint32_t	hash;
+
+	/*
+	 *	Determine how to pick choose the home server.
+	 */
+	switch (pool->type) {
+
+
+		/*
+		 *	For load-balancing by client IP address, we
+		 *	pick a home server by hashing the client IP.
+		 *
+		 *	This isn't as even a load distribution as
+		 *	tracking the State attribute, but it's better
+		 *	than nothing.
+		 */
+	case HOME_POOL_CLIENT_BALANCE:
+		switch (request->packet->src_ipaddr.af) {
+		case AF_INET:
+			hash = fr_hash(&request->packet->src_ipaddr.ipaddr.ip4addr,
+					 sizeof(request->packet->src_ipaddr.ipaddr.ip4addr));
+			break;
+
+		case AF_INET6:
+			hash = fr_hash(&request->packet->src_ipaddr.ipaddr.ip6addr,
+					 sizeof(request->packet->src_ipaddr.ipaddr.ip6addr));
+			break;
+
+		default:
+			hash = 0;
+			break;
+		}
+		start = hash % pool->num_home_servers;
+		break;
+
+	case HOME_POOL_CLIENT_PORT_BALANCE:
+		switch (request->packet->src_ipaddr.af) {
+		case AF_INET:
+			hash = fr_hash(&request->packet->src_ipaddr.ipaddr.ip4addr,
+					 sizeof(request->packet->src_ipaddr.ipaddr.ip4addr));
+			break;
+
+		case AF_INET6:
+			hash = fr_hash(&request->packet->src_ipaddr.ipaddr.ip6addr,
+					 sizeof(request->packet->src_ipaddr.ipaddr.ip6addr));
+			break;
+
+		default:
+			hash = 0;
+			break;
+		}
+		hash = fr_hash_update(&request->packet->src_port,
+				      sizeof(request->packet->src_port), hash);
+		start = hash % pool->num_home_servers;
+		break;
+
+	case HOME_POOL_KEYED_BALANCE:
+		if ((vp = fr_pair_find_by_num(request->config, PW_LOAD_BALANCE_KEY, 0, TAG_ANY)) != NULL) {
+			hash = fr_hash(vp->vp_strvalue, vp->vp_length);
+			start = hash % pool->num_home_servers;
+			break;
+		}
+		/* FALL-THROUGH */
+
+	case HOME_POOL_LOAD_BALANCE:
+	case HOME_POOL_FAIL_OVER:
+		start = 0;
+		break;
+
+	default:		/* this shouldn't happen... */
+		start = 0;
+		break;
+
+	}
+
+	/*
+	 *	Starting with the home server we chose, loop through
+	 *	all home servers.  If the current one is dead, skip
+	 *	it.  If it is too busy, skip it.
+	 *
+	 *	Otherwise, use it.
+	 */
+	for (count = 0; count < pool->num_home_servers; count++) {
+		home_server_t *home = pool->servers[(start + count) % pool->num_home_servers];
+
+		if (!home) continue;
+
+		/*
+		 *	Skip dead home servers.
+		 *
+		 *	Home servers that are unknown, alive, or zombie
+		 *	are used for proxying.
+		 */
+		if (HOME_SERVER_IS_DEAD(home)) {
+			continue;
+		}
+
+		/*
+		 *	This home server is too busy.  Choose another one.
+		 */
+		if (home->currently_outstanding >= home->max_outstanding) {
+			continue;
+		}
+
+#ifdef WITH_DETAIL
+		/*
+		 *	We read the packet from a detail file, AND it
+		 *	came from this server.  Don't re-proxy it
+		 *	there.
+		 */
+		if (request->listener &&
+		    (request->listener->type == RAD_LISTEN_DETAIL) &&
+		    (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) &&
+		    (fr_ipaddr_cmp(&home->ipaddr, &request->packet->src_ipaddr) == 0)) {
+			continue;
+		}
+#endif
+
+		/*
+		 *	Default virtual: ignore homes tied to a
+		 *	virtual.
+		 */
+		if (!request->server && home->parent_server) {
+			continue;
+		}
+
+		/*
+		 *	A virtual AND home is tied to virtual,
+		 *	ignore ones which don't match.
+		 */
+		if (request->server && home->parent_server &&
+		    strcmp(request->server, home->parent_server) != 0) {
+			continue;
+		}
+
+		/*
+		 *	Allow request->server && !home->parent_server
+		 *
+		 *	i.e. virtuals can proxy to globally defined
+		 *	homes.
+		 */
+
+		/*
+		 *	It's zombie, so we remember the first zombie
+		 *	we find, but we don't mark it as a "live"
+		 *	server.
+		 */
+		if (home->state == HOME_STATE_ZOMBIE) {
+			if (!zombie) zombie = home;
+			continue;
+		}
+
+		/*
+		 *	We've found the first "live" one.  Use that.
+		 */
+		if (pool->type != HOME_POOL_LOAD_BALANCE) {
+			found = home;
+			break;
+		}
+
+		/*
+		 *	Otherwise we're doing some kind of load balancing.
+		 *	If we haven't found one yet, pick this one.
+		 */
+		if (!found) {
+			found = home;
+			continue;
+		}
+
+		RDEBUG3("PROXY %s %d\t%s %d",
+		       found->log_name, found->currently_outstanding,
+		       home->log_name, home->currently_outstanding);
+
+		/*
+		 *	Prefer this server if it's less busy than the
+		 *	one we had previously found.
+		 */
+		if (home->currently_outstanding < found->currently_outstanding) {
+			RDEBUG3("PROXY Choosing %s: It's less busy than %s",
+			       home->log_name, found->log_name);
+			found = home;
+			continue;
+		}
+
+		/*
+		 *	Ignore servers which are busier than the one
+		 *	we found.
+		 */
+		if (home->currently_outstanding > found->currently_outstanding) {
+			RDEBUG3("PROXY Skipping %s: It's busier than %s",
+			       home->log_name, found->log_name);
+			continue;
+		}
+
+		/*
+		 *	From the list of servers which have the same
+		 *	load, choose one at random.
+		 */
+		if (((count + 1) * (fr_rand() & 0xffff)) < (uint32_t) 0x10000) {
+			found = home;
+		}
+	} /* loop over the home servers */
+
+	/*
+	 *	We have no live servers, BUT we have a zombie.  Use
+	 *	the zombie as a last resort.
+	 */
+	if (!found && zombie) {
+		found = zombie;
+		zombie = NULL;
+	}
+
+	/*
+	 *	There's a fallback if they're all dead.
+	 */
+	if (!found && pool->fallback) {
+		found = pool->fallback;
+
+		WARN("Home server pool %s failing over to fallback %s",
+		      pool->name, found->virtual_server);
+		if (pool->in_fallback) goto update_and_return;
+
+		pool->in_fallback = true;
+
+		/*
+		 *      Run the trigger once an hour saying that
+		 *      they're all dead.
+		 */
+		if ((pool->time_all_dead + 3600) < request->timestamp) {
+			pool->time_all_dead = request->timestamp;
+			exec_trigger(request, pool->cs, "home_server_pool.fallback", false);
+		}
+	}
+
+	if (found) {
+	update_and_return:
+		if ((found != pool->fallback) && pool->in_fallback) {
+			pool->in_fallback = false;
+			exec_trigger(request, pool->cs, "home_server_pool.normal", false);
+		}
+
+		return found;
+	}
+
+	/*
+	 *	No live match found, and no fallback to the "DEFAULT"
+	 *	realm.  We fix this by blindly marking all servers as
+	 *	"live".  But only do it for ones that don't support
+	 *	"pings", as they will be marked live when they
+	 *	actually are live.
+	 */
+	if (!realm_config->fallback &&
+	    realm_config->wake_all_if_all_dead) {
+		for (count = 0; count < pool->num_home_servers; count++) {
+			home_server_t *home = pool->servers[count];
+
+			if (!home) continue;
+
+			if (HOME_SERVER_IS_DEAD(home) &&
+			    (home->ping_check == HOME_PING_CHECK_NONE)) {
+				home->state = HOME_STATE_ALIVE;
+				home->response_timeouts = 0;
+				if (!found) found = home;
+			}
+		}
+
+		if (found) goto update_and_return;
+	}
+
+	/*
+	 *	Still nothing.  Look up the DEFAULT realm, but only
+	 *	if we weren't looking up the NULL or DEFAULT realms.
+	 */
+	if (realm_config->fallback &&
+	    realmname &&
+	    (strcmp(realmname, "NULL") != 0) &&
+	    (strcmp(realmname, "DEFAULT") != 0)) {
+		REALM *rd = realm_find("DEFAULT");
+
+		if (!rd) return NULL;
+
+		pool = NULL;
+		if (request->packet->code == PW_CODE_ACCESS_REQUEST) {
+			pool = rd->auth_pool;
+
+		} else if (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) {
+			pool = rd->acct_pool;
+		}
+		if (!pool) return NULL;
+
+		RDEBUG2("PROXY - realm %s has no live home servers.  Falling back to the DEFAULT realm.", realmname);
+		return home_server_ldb(rd->name, pool, request);
+	}
+
+	/*
+	 *	Still haven't found anything.  Oh well.
+	 */
+	return NULL;
+}
+
+
+home_server_t *home_server_find(fr_ipaddr_t *ipaddr, uint16_t port,
+#ifndef WITH_TCP
+				UNUSED
+#endif
+				int proto)
+{
+	home_server_t myhome;
+
+	memset(&myhome, 0, sizeof(myhome));
+	myhome.ipaddr = *ipaddr;
+	myhome.src_ipaddr.af = ipaddr->af;
+	myhome.port = port;
+#ifdef WITH_TCP
+	myhome.proto = proto;
+#else
+	myhome.proto = IPPROTO_UDP;
+#endif
+	myhome.virtual_server = NULL;	/* we're not called for internal proxying */
+
+	return rbtree_finddata(home_servers_byaddr, &myhome);
+}
+
+home_server_t *home_server_find_bysrc(fr_ipaddr_t *ipaddr, uint16_t port,
+				int proto,
+				fr_ipaddr_t *src_ipaddr)
+{
+	home_server_t myhome;
+
+	if (!src_ipaddr) return home_server_find(ipaddr, port, proto);
+
+	if (src_ipaddr->af != ipaddr->af) return NULL;
+
+	memset(&myhome, 0, sizeof(myhome));
+	myhome.ipaddr = *ipaddr;
+	myhome.src_ipaddr = *src_ipaddr;
+	myhome.port = port;
+#ifdef WITH_TCP
+	myhome.proto = proto;
+#else
+	myhome.proto = IPPROTO_UDP;
+#endif
+	myhome.virtual_server = NULL;	/* we're not called for internal proxying */
+
+	return rbtree_finddata(home_servers_byaddr, &myhome);
+}
+
+#ifdef WITH_COA
+home_server_t *home_server_byname(char const *name, int type)
+{
+	home_server_t myhome;
+
+	memset(&myhome, 0, sizeof(myhome));
+	myhome.type = type;
+	myhome.name = name;
+
+	return rbtree_finddata(home_servers_byname, &myhome);
+}
+#endif
+
+#ifdef WITH_STATS
+home_server_t *home_server_bynumber(int number)
+{
+	home_server_t myhome;
+
+	memset(&myhome, 0, sizeof(myhome));
+	myhome.number = number;
+	myhome.virtual_server = NULL;	/* we're not called for internal proxying */
+
+	return rbtree_finddata(home_servers_bynumber, &myhome);
+}
+#endif
+
+home_pool_t *home_pool_byname(char const *name, int type)
+{
+	home_pool_t mypool;
+
+	memset(&mypool, 0, sizeof(mypool));
+	mypool.name = name;
+	mypool.server_type = type;
+	return rbtree_finddata(home_pools_byname, &mypool);
+}
+
+int home_server_afrom_file(char const *filename)
+{
+	CONF_SECTION *cs, *subcs;
+	char const *p;
+	home_server_t *home;
+
+	if (!realm_config->dynamic) {
+		fr_strerror_printf("Must set \"dynamic = true\" in proxy.conf for dynamic home servers to work");
+		return -1;
+	}
+
+	cs = cf_section_alloc(NULL, "home", filename);
+	if (!cs) {
+		fr_strerror_printf("Failed allocating memory");
+		return -1;
+	}
+
+	if (cf_file_read(cs, filename) < 0) {
+		fr_strerror_printf("Failed reading file %s", filename);
+	error:
+		talloc_free(cs);
+		return -1;
+	}
+
+	p = strrchr(filename, '/');
+	if (p) {
+		p++;
+	} else {
+		p = filename;
+	}
+
+	subcs = cf_section_sub_find_name2(cs, "home_server", p);
+	if (!subcs) {
+		fr_strerror_printf("No 'home_server %s' definition in the file.", p);
+		goto error;
+	}
+
+	home = home_server_afrom_cs(realm_config, realm_config, subcs);
+	if (!home) {
+		fr_strerror_printf("Failed parsing configuration to a home_server structure");
+		goto error;
+	}
+
+	home->dynamic = true;
+
+	if (home->virtual_server) {
+		fr_strerror_printf("Dynamic home_server '%s' cannot have 'server = %s' configuration item", p, home->virtual_server);
+		talloc_free(home);
+		goto error;
+	}
+
+	if (home->dual) {
+		fr_strerror_printf("Dynamic home_server '%s' is missing 'type', or it is set to 'auth+acct'.  Please specify 'type = auth' or 'type = acct', etc.", p);
+		talloc_free(home);
+		goto error;
+	}
+
+#ifdef COA_TUNNEL
+	if (home->recv_coa) {
+		fr_strerror_printf("Dynamic home_server '%s' cannot receive CoA requests'", p);
+		talloc_free(home);
+		goto error;
+	}
+#endif
+
+	if (!realm_home_server_add(home)) {
+		fr_strerror_printf("Failed adding home_server to the internal data structures");
+		talloc_free(home);
+		goto error;
+	}
+
+	return 0;
+}
+
+int home_server_delete(char const *name, char const *type_name)
+{
+	home_server_t *home;
+	int type;
+	char const *p;
+
+	if (!realm_config->dynamic) {
+		fr_strerror_printf("Must set 'dynamic' in proxy.conf for dynamic home servers to work");
+		return -1;
+	}
+
+	type = fr_str2int(home_server_types, type_name, HOME_TYPE_INVALID);
+	if (type == HOME_TYPE_INVALID) {
+		fr_strerror_printf("Unknown home_server type '%s'", type_name);
+		return -1;
+	}
+
+	p = strrchr(name, '/');
+	if (p) {
+		p++;
+	} else {
+		p = name;
+	}
+
+	home = home_server_byname(p, type);
+	if (!home) {
+		fr_strerror_printf("Failed to find home_server %s", p);
+		return -1;
+	}
+
+	if (!home->dynamic) {
+		fr_strerror_printf("Cannot delete static home_server %s", p);
+		return -1;
+	}
+
+	(void) rbtree_deletebydata(home_servers_byname, home);
+	(void) rbtree_deletebydata(home_servers_byaddr, home);
+#ifdef WITH_STATS
+	(void) rbtree_deletebydata(home_servers_bynumber, home);
+#endif
+
+	/*
+	 *	Leak home, and home->cs.  Oh well.
+	 */
+	return 0;
+}
+#endif
diff --git a/src/main/regex.c b/src/main/regex.c
new file mode 100644
index 0000000..f66414c
--- /dev/null
+++ b/src/main/regex.c
@@ -0,0 +1,279 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/*
+ * $Id$
+ *
+ * @file main/regex.c
+ * @brief Regular expression functions used by the server library.
+ *
+ * @copyright 2014  The FreeRADIUS server project
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/rad_assert.h>
+
+#ifdef HAVE_REGEX
+
+#define REQUEST_DATA_REGEX (0xadbeef00)
+
+typedef struct regcapture {
+#ifdef HAVE_PCRE
+	regex_t		*preg;		//!< Compiled pattern.
+#endif
+	char const	*value;		//!< Original string.
+	regmatch_t	*rxmatch;	//!< Match vectors.
+	size_t		nmatch;		//!< Number of match vectors.
+} regcapture_t;
+
+/** Adds subcapture values to request data
+ *
+ * Allows use of %{n} expansions.
+ *
+ * @note After calling regex_sub_to_request *preg may no longer be valid and
+ *	should be passed to talloc_free.
+ *
+ * @param request Current request.
+ * @param preg Compiled pattern. May be set to NULL if reparented to the regcapture struct.
+ * @param value The original value.
+ * @param rxmatch Pointers into value.
+ * @param nmatch Sizeof rxmatch.
+ */
+void regex_sub_to_request(REQUEST *request, regex_t **preg, char const *value, size_t len,
+			  regmatch_t rxmatch[], size_t nmatch)
+{
+	regcapture_t *old_sc, *new_sc;	/* lldb doesn't like new *sigh* */
+	char *p;
+
+	/*
+	 *	Clear out old_sc matches
+	 */
+	old_sc = request_data_get(request, request, REQUEST_DATA_REGEX);
+	if (old_sc) {
+		DEBUG4("Clearing %zu old matches", old_sc->nmatch);
+		talloc_free(old_sc);
+	} else {
+		DEBUG4("No old matches");
+	}
+
+	if (nmatch == 0) return;
+
+	rad_assert(preg && *preg);
+	rad_assert(rxmatch);
+
+	DEBUG4("Adding %zu matches", nmatch);
+
+	/*
+	 *	Add new_sc matches
+	 */
+	MEM(new_sc = talloc(request, regcapture_t));
+
+	MEM(new_sc->rxmatch = talloc_memdup(new_sc, rxmatch, sizeof(rxmatch[0]) * nmatch));
+	talloc_set_type(new_sc->rxmatch, regmatch_t[]);
+
+	MEM(p = talloc_array(new_sc, char, len + 1));
+	memcpy(p, value, len);
+	p[len] = '\0';
+	new_sc->value = p;
+	new_sc->nmatch = nmatch;
+
+#ifdef HAVE_PCRE
+	if (!(*preg)->precompiled) {
+		new_sc->preg = talloc_steal(new_sc, *preg);
+		*preg = NULL;
+	} else {
+		new_sc->preg = *preg;
+	}
+#endif
+
+	request_data_add(request, request, REQUEST_DATA_REGEX, new_sc, true);
+}
+
+#  ifdef HAVE_PCRE
+/** Extract a subcapture value from the request
+ *
+ * @note This is the PCRE variant of the function.
+ *
+ * @param ctx To allocate subcapture buffer in.
+ * @param out Where to write the subcapture string.
+ * @param request to extract.
+ * @param num Subcapture index (0 for entire match).
+ * @return 0 on success, -1 on notfound.
+ */
+int regex_request_to_sub(TALLOC_CTX *ctx, char **out, REQUEST *request, uint32_t num)
+{
+	regcapture_t *cap;
+	char const *p;
+	int ret;
+
+	cap = request_data_reference(request, request, REQUEST_DATA_REGEX);
+	if (!cap) {
+		RDEBUG4("No subcapture data found");
+		*out = NULL;
+		return -1;
+	}
+
+	ret = pcre_get_substring(cap->value, (int *)cap->rxmatch, (int)cap->nmatch, num, &p);
+	switch (ret) {
+	case PCRE_ERROR_NOMEMORY:
+		MEM(NULL);
+		/* FALL-THROUGH */
+
+	/*
+	 *	Not finding a substring is fine
+	 */
+	case PCRE_ERROR_NOSUBSTRING:
+		RDEBUG4("%i/%zu Not found", num, cap->nmatch);
+		*out = NULL;
+		return -1;
+
+	default:
+		if (ret < 0) {
+			*out = NULL;
+			return -1;
+		}
+
+		/*
+		 *	Check libpcre really is using our overloaded
+		 *	malloc/free talloc wrappers.
+		 */
+		p = (char *)talloc_get_type_abort(p, uint8_t);
+		talloc_set_type(p, char *);
+		talloc_steal(ctx, p);
+		memcpy(out, &p, sizeof(*out));
+
+		RDEBUG4("%i/%zu Found: %s (%zu)", num, cap->nmatch, p, talloc_array_length(p));
+
+		return 0;
+	}
+}
+
+/** Extract a named subcapture value from the request
+ *
+ * @note This is the PCRE variant of the function.
+ *
+ * @param ctx To allocate subcapture buffer in.
+ * @param out Where to write the subcapture string.
+ * @param request to extract.
+ * @param name of subcapture.
+ * @return 0 on success, -1 on notfound.
+ */
+int regex_request_to_sub_named(TALLOC_CTX *ctx, char **out, REQUEST *request, char const *name)
+{
+	regcapture_t *cap;
+	char const *p;
+	int ret;
+
+	cap = request_data_reference(request, request, REQUEST_DATA_REGEX);
+	if (!cap) {
+		RDEBUG4("No subcapture data found");
+		*out = NULL;
+		return -1;
+	}
+
+	ret = pcre_get_named_substring(cap->preg->compiled, cap->value,
+				       (int *)cap->rxmatch, (int)cap->nmatch, name, &p);
+	switch (ret) {
+	case PCRE_ERROR_NOMEMORY:
+		MEM(NULL);
+		/* FALL-THROUGH */
+
+	/*
+	 *	Not finding a substring is fine
+	 */
+	case PCRE_ERROR_NOSUBSTRING:
+		RDEBUG4("No named capture group \"%s\"", name);
+		*out = NULL;
+		return -1;
+
+	default:
+		if (ret < 0) {
+			*out = NULL;
+			return -1;
+		}
+
+		/*
+		 *	Check libpcre really is using our overloaded
+		 *	malloc/free talloc wrappers.
+		 */
+		p = (char *)talloc_get_type_abort(p, uint8_t);
+		talloc_set_type(p, char *);
+		talloc_steal(ctx, p);
+		memcpy(out, &p, sizeof(*out));
+
+		RDEBUG4("Found \"%s\": %s (%zu)", name, p, talloc_array_length(p));
+
+		return 0;
+	}
+}
+#  else
+/** Extract a subcapture value from the request
+ *
+ * @note This is the POSIX variant of the function.
+ *
+ * @param ctx To allocate subcapture buffer in.
+ * @param out Where to write the subcapture string.
+ * @param request to extract.
+ * @param num Subcapture index (0 for entire match).
+ * @return 0 on success, -1 on notfound.
+ */
+int regex_request_to_sub(TALLOC_CTX *ctx, char **out, REQUEST *request, uint32_t num)
+{
+	regcapture_t	*cap;
+	char 		*p;
+	char const	*start;
+	size_t		len;
+
+	cap = request_data_reference(request, request, REQUEST_DATA_REGEX);
+	if (!cap) {
+		RDEBUG4("No subcapture data found");
+		*out = NULL;
+		return -1;
+	}
+
+	/*
+	 *	Greater than our capture array
+	 *
+	 *	-1 means no value in this capture group.
+	 */
+	if ((num >= cap->nmatch) || (cap->rxmatch[num].rm_eo == -1) || (cap->rxmatch[num].rm_so == -1)) {
+		RDEBUG4("%i/%zu Not found", num, cap->nmatch);
+		*out = NULL;
+		return -1;
+	}
+
+	/*
+	 *	Sanity checks on the offsets
+	 */
+	rad_assert(cap->rxmatch[num].rm_eo <= (regoff_t)talloc_array_length(cap->value));
+	rad_assert(cap->rxmatch[num].rm_so <= (regoff_t)talloc_array_length(cap->value));
+
+	start = cap->value + cap->rxmatch[num].rm_so;
+	len = cap->rxmatch[num].rm_eo - cap->rxmatch[num].rm_so;
+
+	RDEBUG4("%i/%zu Found: %.*s (%zu)", num, cap->nmatch, (int)len, start, len);
+	MEM(p = talloc_array(ctx, char, len + 1));
+	memcpy(p, start, len);
+	p[len] = '\0';
+
+	*out = p;
+
+	return 0;
+}
+#  endif
+#endif
diff --git a/src/main/session.c b/src/main/session.c
new file mode 100644
index 0000000..ddec8ff
--- /dev/null
+++ b/src/main/session.c
@@ -0,0 +1,262 @@
+/*
+ * session.c	session management
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2000,2006  The FreeRADIUS server project
+ */
+
+RCSID("$Id$")
+
+#include	<freeradius-devel/radiusd.h>
+#include	<freeradius-devel/modules.h>
+#include	<freeradius-devel/rad_assert.h>
+
+#ifdef HAVE_SYS_WAIT_H
+#include	<sys/wait.h>
+#endif
+
+#ifdef WITH_SESSION_MGMT
+/*
+ *	End a session by faking a Stop packet to all accounting modules.
+ */
+int session_zap(REQUEST *request, fr_ipaddr_t const *nasaddr, uint32_t nas_port,
+		char const *nas_port_id, char const *user,
+		char const *sessionid, uint32_t cliaddr, char proto,
+		int session_time)
+{
+	REQUEST *stopreq;
+	VALUE_PAIR *vp;
+	int ret;
+
+	stopreq = request_alloc_fake(request);
+	rad_assert(stopreq != NULL);
+	rad_assert(stopreq->packet != NULL);
+	stopreq->packet->code = PW_CODE_ACCOUNTING_REQUEST; /* just to be safe */
+	stopreq->listener = request->listener;
+
+	/* Hold your breath */
+#define PAIR(n,v,e) do { \
+		if(!(vp = fr_pair_afrom_num(stopreq->packet,n, 0))) {	\
+			talloc_free(stopreq); \
+			ERROR("no memory"); \
+			return 0; \
+		} \
+		vp->e = v; \
+		fr_pair_add(&(stopreq->packet->vps), vp); \
+	} while(0)
+
+#define INTPAIR(n,v) PAIR(n,v,vp_integer)
+
+#define IPPAIR(n,v) PAIR(n,v,vp_ipaddr)
+
+#define IPV6PAIR(n,v) PAIR(n,v,vp_ipv6addr)
+
+#define STRINGPAIR(n,v) do { \
+	  if(!(vp = fr_pair_afrom_num(stopreq->packet,n, 0))) {	\
+		talloc_free(stopreq); \
+		ERROR("no memory"); \
+		return 0; \
+	} \
+	fr_pair_value_strcpy(vp, v);	\
+	fr_pair_add(&(stopreq->packet->vps), vp); \
+	} while(0)
+
+	INTPAIR(PW_ACCT_STATUS_TYPE, PW_STATUS_STOP);
+
+	if (nasaddr->af == AF_INET) {
+		IPPAIR(PW_NAS_IP_ADDRESS, nasaddr->ipaddr.ip4addr.s_addr);
+	} else {
+		IPV6PAIR(PW_NAS_IPV6_ADDRESS, nasaddr->ipaddr.ip6addr);
+	}
+
+	INTPAIR(PW_EVENT_TIMESTAMP, 0);
+	vp->vp_date = time(NULL);
+	INTPAIR(PW_ACCT_DELAY_TIME, 0);
+
+	STRINGPAIR(PW_USER_NAME, user);
+	stopreq->username = vp;
+
+	if (!nas_port_id) {
+		INTPAIR(PW_NAS_PORT, nas_port);
+	} else {
+		STRINGPAIR(PW_NAS_PORT_ID, nas_port_id);
+	}
+	STRINGPAIR(PW_ACCT_SESSION_ID, sessionid);
+	if(proto == 'P') {
+		INTPAIR(PW_SERVICE_TYPE, PW_FRAMED_USER);
+		INTPAIR(PW_FRAMED_PROTOCOL, PW_PPP);
+	} else if(proto == 'S') {
+		INTPAIR(PW_SERVICE_TYPE, PW_FRAMED_USER);
+		INTPAIR(PW_FRAMED_PROTOCOL, PW_SLIP);
+	} else {
+		INTPAIR(PW_SERVICE_TYPE, PW_LOGIN_USER); /* A guess, really */
+	}
+	if(cliaddr != 0)
+		IPPAIR(PW_FRAMED_IP_ADDRESS, cliaddr);
+	INTPAIR(PW_ACCT_SESSION_TIME, session_time);
+	INTPAIR(PW_ACCT_INPUT_OCTETS, 0);
+	INTPAIR(PW_ACCT_OUTPUT_OCTETS, 0);
+	INTPAIR(PW_ACCT_INPUT_PACKETS, 0);
+	INTPAIR(PW_ACCT_OUTPUT_PACKETS, 0);
+
+	stopreq->password = NULL;
+
+	RDEBUG("Running Accounting section for automatically created accounting 'stop'");
+	rdebug_pair_list(L_DBG_LVL_1, request, request->packet->vps, NULL);
+	ret = rad_accounting(stopreq);
+
+	/*
+	 *  We've got to clean it up by hand, because no one else will.
+	 */
+	talloc_free(stopreq);
+
+	return ret;
+}
+
+#ifndef __MINGW32__
+
+/*
+ *	Check one terminal server to see if a user is logged in.
+ *
+ *	Return values:
+ *		0 The user is off-line.
+ *		1 The user is logged in.
+ *		2 Some error occured.
+ */
+int rad_check_ts(fr_ipaddr_t const *nasaddr, uint32_t nas_port, char const *nas_port_id, char const *user,
+		 char const *session_id)
+{
+	pid_t	pid, child_pid;
+	int	status;
+	char	address[64];
+	char	port[11];
+	RADCLIENT *cl;
+
+	/*
+	 *	Find NAS type.
+	 */
+	cl = client_find_old(nasaddr);
+	if (!cl) {
+		/*
+		 *  Unknown NAS, so trusting radutmp.
+		 */
+		DEBUG2("checkrad: Unknown NAS %s, not checking",
+		       inet_ntop(nasaddr->af, &(nasaddr->ipaddr), address, sizeof(address)));
+		return 1;
+	}
+
+	/*
+	 *  No nas_type, or nas type 'other', trust radutmp.
+	 */
+	if (!cl->nas_type || (cl->nas_type[0] == '\0') ||
+	    (strcmp(cl->nas_type, "other") == 0)) {
+		DEBUG2("checkrad: No NAS type, or type \"other\" not checking");
+		return 1;
+	}
+
+	/*
+	 *	Fork.
+	 */
+	if ((pid = rad_fork()) < 0) { /* do wait for the fork'd result */
+		ERROR("Accounting: Failed in fork(): Cannot run checkrad\n");
+		return 2;
+	}
+
+	if (pid > 0) {
+		child_pid = rad_waitpid(pid, &status);
+
+		/*
+		 *	It's taking too long.  Stop waiting for it.
+		 *
+		 *	Don't bother to kill it, as we don't care what
+		 *	happens to it now.
+		 */
+		if (child_pid == 0) {
+			ERROR("Check-TS: timeout waiting for checkrad");
+			return 2;
+		}
+
+		if (child_pid < 0) {
+			ERROR("Check-TS: unknown error in waitpid()");
+			return 2;
+		}
+
+		return WEXITSTATUS(status);
+	}
+
+	/*
+	 *  We don't close fd's 0, 1, and 2.  If we're in debugging mode,
+	 *  then they should go to stdout (etc), along with the other
+	 *  server log messages.
+	 *
+	 *  If we're not in debugging mode, then the code in radiusd.c
+	 *  takes care of connecting fd's 0, 1, and 2 to /dev/null.
+	 */
+	closefrom(3);
+
+	inet_ntop(nasaddr->af, &(nasaddr->ipaddr), address, sizeof(address));
+
+	if (!nas_port_id) {
+		snprintf(port, sizeof(port), "%u", nas_port);
+		nas_port_id = port;
+	}
+
+#ifdef __EMX__
+	/* OS/2 can't directly execute scripts then we call the command
+	   processor to execute checkrad
+	*/
+	execl(getenv("COMSPEC"), "", "/C","checkrad", cl->nas_type, address, nas_port_id,
+		user, session_id, NULL);
+#else
+	execl(main_config.checkrad, "checkrad", cl->nas_type, address, nas_port_id,
+		user, session_id, NULL);
+#endif
+	ERROR("Check-TS: exec %s: %s", main_config.checkrad, fr_syserror(errno));
+
+	/*
+	 *	Exit - 2 means "some error occured".
+	 */
+	exit(2);
+}
+#else
+int rad_check_ts(fr_ipaddr_t const *nasaddr, UNUSED unsigned int nas_port,
+		 UNUSED char const *user, UNUSED char const *session_id)
+{
+	ERROR("Simultaneous-Use is not supported");
+	return 2;
+}
+#endif
+
+#else
+/* WITH_SESSION_MGMT */
+
+int session_zap(UNUSED REQUEST *request, fr_ipaddr_t const *nasaddr, UNUSED uint32_t nas_port,
+		UNUSED char const *nas_port_id, UNUSED char const *user,
+		UNUSED char const *sessionid, UNUSED uint32_t cliaddr, UNUSED char proto,
+		UNUSED int session_time)
+{
+	return RLM_MODULE_FAIL;
+}
+
+int rad_check_ts(fr_ipaddr_t const *nasaddr, UNUSED unsigned int nas_port,
+		 UNUSED char const *nas_port_id, UNUSED char const *user, UNUSED char const *session_id)
+{
+	ERROR("Simultaneous-Use is not supported");
+	return 2;
+}
+#endif
diff --git a/src/main/soh.c b/src/main/soh.c
new file mode 100644
index 0000000..754ad84
--- /dev/null
+++ b/src/main/soh.c
@@ -0,0 +1,675 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @file soh.c
+ * @brief Implements the MS-SOH parsing code. This is called from rlm_eap_peap
+ *
+ * @copyright 2010 Phil Mayers <p.mayers@imperial.ac.uk>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/soh.h>
+#include <freeradius-devel/rad_assert.h>
+
+/*
+ * This code implements parsing of MS-SOH data into FreeRadius AVPs
+ * allowing for FreeRadius MS-NAP policies
+ */
+
+/**
+ * EAP-SOH packet
+ */
+typedef struct {
+	uint16_t tlv_type;	/**< ==7 for EAP-SOH */
+	uint16_t tlv_len;
+	uint32_t tlv_vendor;
+
+	/**
+	 * @name soh-payload
+	 * @brief either an soh request or response */
+	uint16_t soh_type;	/**< ==2 for request, 1 for response */
+	uint16_t soh_len;
+
+	/* an soh-response may now follow... */
+} eap_soh;
+
+/**
+ * SOH response payload
+ * Send by client to server
+ */
+typedef struct {
+	uint16_t outer_type;
+	uint16_t outer_len;
+	uint32_t vendor;
+	uint16_t inner_type;
+	uint16_t inner_len;
+} soh_response;
+
+/**
+ * SOH mode subheader
+ * Typical microsoft binary blob nonsense
+ */
+typedef struct {
+	uint16_t outer_type;
+	uint16_t outer_len;
+	uint32_t vendor;
+	uint8_t corrid[24];
+	uint8_t intent;
+	uint8_t content_type;
+} soh_mode_subheader;
+
+/**
+ * SOH type-length-value header
+ */
+typedef struct {
+	uint16_t tlv_type;
+	uint16_t tlv_len;
+} soh_tlv;
+
+/** Read big-endian 2-byte unsigned from p
+ *
+ * caller must ensure enough data exists at "p"
+ */
+uint16_t soh_pull_be_16(uint8_t const *p) {
+	uint16_t r;
+
+	r = *p++ << 8;
+	r += *p++;
+
+	return r;
+}
+
+/** Read big-endian 3-byte unsigned from p
+ *
+ * caller must ensure enough data exists at "p"
+ */
+uint32_t soh_pull_be_24(uint8_t const *p) {
+	uint32_t r;
+
+	r = *p++ << 16;
+	r += *p++ << 8;
+	r += *p++;
+
+	return r;
+}
+
+/** Read big-endian 4-byte unsigned from p
+ *
+ * caller must ensure enough data exists at "p"
+ */
+uint32_t soh_pull_be_32(uint8_t const *p) {
+	uint32_t r;
+
+	r = *p++ << 24;
+	r += *p++ << 16;
+	r += *p++ << 8;
+	r += *p++;
+
+	return r;
+}
+
+static int eapsoh_mstlv(REQUEST *request, uint8_t const *p, unsigned int data_len) CC_HINT(nonnull);
+
+/** Parses the MS-SOH type/value (note: NOT type/length/value) data and update the sohvp list
+ *
+ * See section 2.2.4 of MS-SOH. Because there's no "length" field we CANNOT just skip
+ * unknown types; we need to know their length ahead of time. Therefore, we abort
+ * if we find an unknown type. Note that sohvp may still have been modified in the
+ * failure case.
+ *
+ * @param request Current request
+ * @param p binary blob
+ * @param data_len length of blob
+ * @return 1 on success, 0 on failure
+ */
+static int eapsoh_mstlv(REQUEST *request, uint8_t const *p, unsigned int data_len)
+{
+	VALUE_PAIR *vp;
+	uint8_t c;
+	int t;
+
+	while (data_len > 0) {
+		c = *p++;
+		data_len--;
+
+		switch (c) {
+		case 1:
+			/* MS-Machine-Inventory-Packet
+			 * MS-SOH section 2.2.4.1
+			 */
+			if (data_len < 18) {
+				RDEBUG("insufficient data for MS-Machine-Inventory-Packet");
+				return 0;
+			}
+			data_len -= 18;
+
+			vp = pair_make_request("SoH-MS-Machine-OS-vendor", "Microsoft", T_OP_EQ);
+			if (!vp) return 0;
+
+			vp = pair_make_request("SoH-MS-Machine-OS-version", NULL, T_OP_EQ);
+			if (!vp) return 0;
+
+			vp->vp_integer = soh_pull_be_32(p); p+=4;
+
+			vp = pair_make_request("SoH-MS-Machine-OS-release", NULL, T_OP_EQ);
+			if (!vp) return 0;
+
+			vp->vp_integer = soh_pull_be_32(p); p+=4;
+
+			vp = pair_make_request("SoH-MS-Machine-OS-build", NULL, T_OP_EQ);
+			if (!vp) return 0;
+
+			vp->vp_integer = soh_pull_be_32(p); p+=4;
+
+			vp = pair_make_request("SoH-MS-Machine-SP-version", NULL, T_OP_EQ);
+			if (!vp) return 0;
+
+			vp->vp_integer = soh_pull_be_16(p); p+=2;
+
+			vp = pair_make_request("SoH-MS-Machine-SP-release", NULL, T_OP_EQ);
+			if (!vp) return 0;
+
+			vp->vp_integer = soh_pull_be_16(p); p+=2;
+
+			vp = pair_make_request("SoH-MS-Machine-Processor", NULL, T_OP_EQ);
+			if (!vp) return 0;
+
+			vp->vp_integer = soh_pull_be_16(p); p+=2;
+			break;
+
+		case 2:
+			/* MS-Quarantine-State - FIXME: currently unhandled
+			 * MS-SOH 2.2.4.1
+			 *
+			 * 1 byte reserved
+			 * 1 byte flags
+			 * 8 bytes NT Time field (100-nanosec since 1 Jan 1601)
+			 * 2 byte urilen
+			 * N bytes uri
+			 */
+			p += 10;
+			t = soh_pull_be_16(p);	/* t == uri len */
+			p += 2;
+			p += t;
+			data_len -= 12 + t;
+			break;
+
+		case 3:
+			/* MS-Packet-Info
+			 * MS-SOH 2.2.4.3
+			 */
+			RDEBUG3("SoH MS-Packet-Info %s vers=%i", *p & 0x10 ? "request" : "response", *p & 0xf);
+			p++;
+			data_len--;
+			break;
+
+		case 4:
+			/* MS-SystemGenerated-Ids - FIXME: currently unhandled
+			 * MS-SOH 2.2.4.4
+			 *
+			 * 2 byte length
+			 * N bytes (3 bytes IANA enterprise# + 1 byte component id#)
+			 */
+			t = soh_pull_be_16(p);
+			p += 2;
+			p += t;
+			data_len -= 2 + t;
+			break;
+
+		case 5:
+			/* MS-MachineName
+			 * MS-SOH 2.2.4.5
+			 *
+			 * 1 byte namelen
+			 * N bytes name
+			 */
+			t = soh_pull_be_16(p);
+			p += 2;
+
+			vp = pair_make_request("SoH-MS-Machine-Name", NULL, T_OP_EQ);
+			if (!vp) return 0;
+
+			fr_pair_value_bstrncpy(vp, p, t);
+
+			p += t;
+			data_len -= 2 + t;
+			break;
+
+		case 6:
+			/* MS-CorrelationId
+			 * MS-SOH 2.2.4.6
+			 *
+			 * 24 bytes opaque binary which we might, in future, have
+			 * to echo back to the client in a final SoHR
+			 */
+			vp = pair_make_request("SoH-MS-Correlation-Id", NULL, T_OP_EQ);
+			if (!vp) return 0;
+
+			fr_pair_value_memcpy(vp, p, 24);
+			p += 24;
+			data_len -= 24;
+			break;
+
+		case 7:
+			/* MS-Installed-Shvs - FIXME: currently unhandled
+			 * MS-SOH 2.2.4.7
+			 *
+			 * 2 bytes length
+			 * N bytes (3 bytes IANA enterprise# + 1 byte component id#)
+			 */
+			t = soh_pull_be_16(p);
+			p += 2;
+			p += t;
+			data_len -= 2 + t;
+			break;
+
+		case 8:
+			/* MS-Machine-Inventory-Ex
+			 * MS-SOH 2.2.4.8
+			 *
+			 * 4 bytes reserved
+			 * 1 byte product type (client=1 domain_controller=2 server=3)
+			 */
+			p += 4;
+			vp = pair_make_request("SoH-MS-Machine-Role", NULL, T_OP_EQ);
+			if (!vp) return 0;
+
+			vp->vp_integer = *p;
+			p++;
+			data_len -= 5;
+			break;
+
+		default:
+			RDEBUG("SoH Unknown MS TV %i stopping", c);
+			return 0;
+		}
+	}
+	return 1;
+}
+/** Convert windows Health Class status into human-readable string
+ *
+ * Tedious, really, really tedious...
+ */
+static char const* clientstatus2str(uint32_t hcstatus) {
+	switch (hcstatus) {
+	/* this lot should all just be for windows updates */
+	case 0xff0005:
+		return "wua-ok";
+
+	case 0xff0006:
+		return "wua-missing";
+
+	case 0xff0008:
+		return "wua-not-started";
+
+	case 0xc0ff000c:
+		return "wua-no-wsus-server";
+
+	case 0xc0ff000d:
+		return "wua-no-wsus-clientid";
+
+	case 0xc0ff000e:
+		return "wua-disabled";
+
+	case 0xc0ff000f:
+		return "wua-comm-failure";
+
+	/* these next 3 are for all health-classes */
+	case 0xc0ff0002:
+		return "not-installed";
+
+	case 0xc0ff0003:
+		return "down";
+
+	case 0xc0ff0018:
+		return "not-started";
+	}
+	return NULL;
+}
+
+/** Convert a Health Class into a string
+ *
+ */
+static char const* healthclass2str(uint8_t hc) {
+	switch (hc) {
+	case 0:
+		return "firewall";
+
+	case 1:
+		return "antivirus";
+
+	case 2:
+		return "antispyware";
+
+	case 3:
+		return "updates";
+
+	case 4:
+		return "security-updates";
+	}
+	return NULL;
+}
+
+/** Parse the MS-SOH response in data and update sohvp
+ *
+ * Note that sohvp might still have been updated in event of a failure.
+ *
+ * @param request Current request
+ * @param data MS-SOH blob
+ * @param data_len length of MS-SOH blob
+ *
+ * @return 0 on success, -1 on failure
+ *
+ */
+int soh_verify(REQUEST *request, uint8_t const *data, unsigned int data_len) {
+
+	VALUE_PAIR *vp;
+	eap_soh hdr;
+	soh_response resp;
+	soh_mode_subheader mode;
+	soh_tlv tlv;
+	int curr_shid=-1, curr_shid_c=-1, curr_hc=-1;
+
+	rad_assert(request->packet != NULL);
+
+	hdr.tlv_type = soh_pull_be_16(data); data += 2;
+	hdr.tlv_len = soh_pull_be_16(data); data += 2;
+	hdr.tlv_vendor = soh_pull_be_32(data); data += 4;
+
+	if (hdr.tlv_type != 7 || hdr.tlv_vendor != 0x137) {
+		RDEBUG("SoH payload is %i %08x not a ms-vendor packet", hdr.tlv_type, hdr.tlv_vendor);
+		return -1;
+	}
+
+	hdr.soh_type = soh_pull_be_16(data); data += 2;
+	hdr.soh_len = soh_pull_be_16(data); data += 2;
+	if (hdr.soh_type != 1) {
+		RDEBUG("SoH tlv %04x is not a response", hdr.soh_type);
+		return -1;
+	}
+
+	/* FIXME: check for sufficient data */
+	resp.outer_type = soh_pull_be_16(data); data += 2;
+	resp.outer_len = soh_pull_be_16(data); data += 2;
+	resp.vendor = soh_pull_be_32(data); data += 4;
+	resp.inner_type = soh_pull_be_16(data); data += 2;
+	resp.inner_len = soh_pull_be_16(data); data += 2;
+
+
+	if (resp.outer_type!=7 || resp.vendor != 0x137) {
+		RDEBUG("SoH response outer type %i/vendor %08x not recognised", resp.outer_type, resp.vendor);
+		return -1;
+	}
+	switch (resp.inner_type) {
+	case 1:
+		/* no mode sub-header */
+		RDEBUG("SoH without mode subheader");
+		break;
+
+	case 2:
+		mode.outer_type = soh_pull_be_16(data); data += 2;
+		mode.outer_len = soh_pull_be_16(data); data += 2;
+		mode.vendor = soh_pull_be_32(data); data += 4;
+		memcpy(mode.corrid, data, 24); data += 24;
+		mode.intent = data[0];
+		mode.content_type = data[1];
+		data += 2;
+
+		if (mode.outer_type != 7 || mode.vendor != 0x137 || mode.content_type != 0) {
+			RDEBUG3("SoH mode subheader outer type %i/vendor %08x/content type %i invalid", mode.outer_type, mode.vendor, mode.content_type);
+			return -1;
+		}
+		RDEBUG3("SoH with mode subheader");
+		break;
+
+	default:
+		RDEBUG("SoH invalid inner type %i", resp.inner_type);
+		return -1;
+	}
+
+	/* subtract off the relevant amount of data */
+	if (resp.inner_type==2) {
+		data_len = resp.inner_len - 34;
+	} else {
+		data_len = resp.inner_len;
+	}
+
+	/* TLV
+	 * MS-SOH 2.2.1
+	 * See also 2.2.3
+	 *
+	 * 1 bit mandatory
+	 * 1 bit reserved
+	 * 14 bits tlv type
+	 * 2 bytes tlv length
+	 * N bytes payload
+	 *
+	 */
+	while (data_len >= 4) {
+		tlv.tlv_type = soh_pull_be_16(data); data += 2;
+		tlv.tlv_len = soh_pull_be_16(data); data += 2;
+
+		data_len -= 4;
+
+		switch (tlv.tlv_type) {
+		case 2:
+			/* System-Health-Id TLV
+			 * MS-SOH 2.2.3.1
+			 *
+			 * 3 bytes IANA/SMI vendor code
+			 * 1 byte component (i.e. within vendor, which SoH component
+			 */
+			curr_shid = soh_pull_be_24(data);
+			curr_shid_c = data[3];
+			RDEBUG2("SoH System-Health-ID vendor %08x component=%i", curr_shid, curr_shid_c);
+			break;
+
+		case 7:
+			/* Vendor-Specific packet
+			 * MS-SOH 2.2.3.3
+			 *
+			 * 4 bytes vendor, supposedly ignored by NAP
+			 * N bytes payload; for Microsoft component#0 this is the MS TV stuff
+			 */
+			if (curr_shid==0x137 && curr_shid_c==0) {
+				RDEBUG2("SoH MS type-value payload");
+				eapsoh_mstlv(request, data + 4, tlv.tlv_len - 4);
+			} else {
+				RDEBUG2("SoH unhandled vendor-specific TLV %08x/component=%i %i bytes payload",
+					curr_shid, curr_shid_c, tlv.tlv_len);
+			}
+			break;
+
+		case 8:
+			/* Health-Class
+			 * MS-SOH 2.2.3.5.6
+			 *
+			 * 1 byte integer
+			 */
+			RDEBUG2("SoH Health-Class %i", data[0]);
+			curr_hc = data[0];
+			break;
+
+		case 9:
+			/* Software-Version
+			 * MS-SOH 2.2.3.5.7
+			 *
+			 * 1 byte integer
+			 */
+			RDEBUG2("SoH Software-Version %i", data[0]);
+			break;
+
+		case 11:
+			/* Health-Class status
+			 * MS-SOH 2.2.3.5.9
+			 *
+			 * variable data; for the MS System Health vendor, these are 4-byte
+			 * integers which are a really, really dumb format:
+			 *
+			 *  28 bits ignore
+			 *  1 bit - 1==product snoozed
+			 *  1 bit - 1==microsoft product
+			 *  1 bit - 1==product up-to-date
+			 *  1 bit - 1==product enabled
+			 */
+			RDEBUG2("SoH Health-Class-Status - current shid=%08x component=%i", curr_shid, curr_shid_c);
+
+			if (curr_shid == 0x137 && curr_shid_c == 128) {
+				char const *s, *t;
+				uint32_t hcstatus = soh_pull_be_32(data);
+
+				RDEBUG2("SoH Health-Class-Status microsoft DWORD=%08x", hcstatus);
+
+				vp = pair_make_request("SoH-MS-Windows-Health-Status", NULL, T_OP_EQ);
+				if (!vp) return 0;
+
+				switch (curr_hc) {
+				case 4:
+					/* security updates */
+					s = "security-updates";
+					switch (hcstatus) {
+					case 0xff0005:
+						fr_pair_value_sprintf(vp, "%s ok all-installed", s);
+						break;
+
+					case 0xff0006:
+						fr_pair_value_sprintf(vp, "%s warn some-missing", s);
+						break;
+
+					case 0xff0008:
+						fr_pair_value_sprintf(vp, "%s warn never-started", s);
+						break;
+
+					case 0xc0ff000c:
+						fr_pair_value_sprintf(vp, "%s error no-wsus-srv", s);
+						break;
+
+					case 0xc0ff000d:
+						fr_pair_value_sprintf(vp, "%s error no-wsus-clid", s);
+						break;
+
+					case 0xc0ff000e:
+						fr_pair_value_sprintf(vp, "%s warn wsus-disabled", s);
+						break;
+
+					case 0xc0ff000f:
+						fr_pair_value_sprintf(vp, "%s error comm-failure", s);
+						break;
+
+					case 0xc0ff0010:
+						fr_pair_value_sprintf(vp, "%s warn needs-reboot", s);
+						break;
+
+					default:
+						fr_pair_value_sprintf(vp, "%s error %08x", s, hcstatus);
+						break;
+					}
+					break;
+
+				case 3:
+					/* auto updates */
+					s = "auto-updates";
+					switch (hcstatus) {
+					case 1:
+						fr_pair_value_sprintf(vp, "%s warn disabled", s);
+						break;
+
+					case 2:
+						fr_pair_value_sprintf(vp, "%s ok action=check-only", s);
+						break;
+
+					case 3:
+						fr_pair_value_sprintf(vp, "%s ok action=download", s);
+						break;
+
+					case 4:
+						fr_pair_value_sprintf(vp, "%s ok action=install", s);
+						break;
+
+					case 5:
+						fr_pair_value_sprintf(vp, "%s warn unconfigured", s);
+						break;
+
+					case 0xc0ff0003:
+						fr_pair_value_sprintf(vp, "%s warn service-down", s);
+						break;
+
+					case 0xc0ff0018:
+						fr_pair_value_sprintf(vp, "%s warn never-started", s);
+						break;
+
+					default:
+						fr_pair_value_sprintf(vp, "%s error %08x", s, hcstatus);
+						break;
+					}
+					break;
+
+				default:
+					/* other - firewall, antivirus, antispyware */
+					s = healthclass2str(curr_hc);
+					if (s) {
+						/* bah. this is vile. stupid microsoft
+						 */
+						if (hcstatus & 0xff000000) {
+							/* top octet non-zero means an error
+							 * FIXME: is this always correct? MS-WSH 2.2.8 is unclear
+							 */
+							t = clientstatus2str(hcstatus);
+							if (t) {
+								fr_pair_value_sprintf(vp, "%s error %s", s, t);
+							} else {
+								fr_pair_value_sprintf(vp, "%s error %08x", s, hcstatus);
+							}
+						} else {
+							fr_pair_value_sprintf(vp,
+									"%s ok snoozed=%i microsoft=%i up2date=%i enabled=%i",
+									s,
+									hcstatus & 0x8 ? 1 : 0,
+									hcstatus & 0x4 ? 1 : 0,
+									hcstatus & 0x2 ? 1 : 0,
+									hcstatus & 0x1 ? 1 : 0
+									);
+						}
+					} else {
+						fr_pair_value_sprintf(vp, "%i unknown %08x", curr_hc, hcstatus);
+					}
+					break;
+				}
+			} else {
+				vp = pair_make_request("SoH-MS-Health-Other", NULL, T_OP_EQ);
+				if (!vp) return 0;
+
+				/* FIXME: what to do with the payload? */
+				fr_pair_value_sprintf(vp, "%08x/%i ?", curr_shid, curr_shid_c);
+			}
+			break;
+
+		default:
+			RDEBUG("SoH Unknown TLV %i len=%i", tlv.tlv_type, tlv.tlv_len);
+			break;
+		}
+
+		data += tlv.tlv_len;
+		data_len -= tlv.tlv_len;
+	}
+
+	return 0;
+}
diff --git a/src/main/state.c b/src/main/state.c
new file mode 100644
index 0000000..3700062
--- /dev/null
+++ b/src/main/state.c
@@ -0,0 +1,713 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @brief Multi-packet state handling
+ * @file main/state.c
+ *
+ * @ingroup AVP
+ *
+ * @copyright 2014 The FreeRADIUS server project
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/state.h>
+#include <freeradius-devel/md5.h>
+#include <freeradius-devel/rad_assert.h>
+#include <freeradius-devel/process.h>
+
+typedef struct state_entry_t {
+	uint8_t		state[AUTH_VECTOR_LEN];
+
+	time_t		cleanup;
+	struct state_entry_t *prev;
+	struct state_entry_t *next;
+
+	int		tries;
+
+	TALLOC_CTX		*ctx;
+	VALUE_PAIR		*vps;
+
+	char		*server;
+	unsigned int	request_number;
+	RADCLIENT	*request_client;
+	main_config_t	*request_root;
+
+	void 		*opaque;
+	void 		(*free_opaque)(void *opaque);
+} state_entry_t;
+
+struct fr_state_t {
+	rbtree_t *tree;
+
+	state_entry_t *head, *tail;
+
+#ifdef HAVE_PTHREAD_H
+	pthread_mutex_t mutex;
+#endif
+};
+
+static fr_state_t global_state;
+
+#define STATE_FREE(_x) if (_x != &global_state) talloc_free(_x)
+
+#ifdef HAVE_PTHREAD_H
+
+#define PTHREAD_MUTEX_LOCK pthread_mutex_lock
+#define PTHREAD_MUTEX_UNLOCK pthread_mutex_unlock
+
+#else
+/*
+ *	This is easier than ifdef's throughout the code.
+ */
+#define PTHREAD_MUTEX_LOCK(_x)
+#define PTHREAD_MUTEX_UNLOCK(_x)
+
+#endif
+
+/*
+ *	rbtree callback.
+ */
+static int state_entry_cmp(void const *one, void const *two)
+{
+	state_entry_t const *a = one;
+	state_entry_t const *b = two;
+
+	return memcmp(a->state, b->state, sizeof(a->state));
+}
+
+static bool state_entry_link(fr_state_t *state, state_entry_t *entry)
+{
+	if (!rbtree_insert(state->tree, entry)) {
+		return false;
+	}
+
+	/*
+	 *	Link it to the end of the list, which is implicitely
+	 *	ordered by cleanup time.
+	 */
+	if (!state->head) {
+		entry->prev = entry->next = NULL;
+		state->head = state->tail = entry;
+	} else {
+		rad_assert(state->tail != NULL);
+
+		entry->prev = state->tail;
+		state->tail->next = entry;
+
+		entry->next = NULL;
+		state->tail = entry;
+	}
+
+	return true;
+}
+
+static void state_entry_unlink(fr_state_t *state, state_entry_t *entry)
+{
+	state_entry_t *prev, *next;
+
+	prev = entry->prev;
+	next = entry->next;
+
+	if (prev) {
+		rad_assert(state->head != entry);
+		prev->next = next;
+	} else if (state->head) {
+		rad_assert(state->head == entry);
+		state->head = next;
+	}
+
+	if (next) {
+		rad_assert(state->tail != entry);
+		next->prev = prev;
+	} else if (state->tail) {
+		rad_assert(state->tail == entry);
+		state->tail = prev;
+	}
+
+	rbtree_deletebydata(state->tree, entry);
+}
+
+/*
+ *	When an entry is free'd, it's removed from the linked list of
+ *	cleanup timers.
+ */
+static void state_entry_free(fr_state_t *state, state_entry_t *entry)
+{
+	/*
+	 *	If we're deleting the whole tree, don't bother doing
+	 *	all of the fixups.
+	 */
+	if (!state || !state->tree) return;
+
+	state_entry_unlink(state, entry);
+
+	if (entry->opaque) {
+		entry->free_opaque(entry->opaque);
+	}
+
+#ifdef WITH_VERIFY_PTR
+	(void) talloc_get_type_abort(entry, state_entry_t);
+#endif
+	if (entry->ctx) talloc_free(entry->ctx);
+
+	talloc_free(entry);
+}
+
+fr_state_t *fr_state_init(TALLOC_CTX *ctx)
+{
+	fr_state_t *state;
+
+	if (!ctx) {
+		state = &global_state;
+		if (state->tree) return state;
+	} else {
+		state = talloc_zero(ctx, fr_state_t);
+		if (!state) return 0;
+	}
+
+#ifdef HAVE_PTHREAD_H
+	if (pthread_mutex_init(&state->mutex, NULL) != 0) {
+		STATE_FREE(state);
+		return NULL;
+	}
+#endif
+
+	state->tree = rbtree_create(NULL, state_entry_cmp, NULL, 0);
+	if (!state->tree) {
+		STATE_FREE(state);
+		return NULL;
+	}
+
+	return state;
+}
+
+void fr_state_delete(fr_state_t *state)
+{
+	rbtree_t *my_tree;
+
+	if (!state) return;
+
+	PTHREAD_MUTEX_LOCK(&state->mutex);
+
+	/*
+	 *	Tell the talloc callback to NOT delete the entry from
+	 *	the tree.  We're deleting the entire tree.
+	 */
+	my_tree = state->tree;
+	state->tree = NULL;
+
+	rbtree_free(my_tree);
+	PTHREAD_MUTEX_UNLOCK(&state->mutex);
+
+	STATE_FREE(state);
+}
+
+/*
+ *	Create a fake request, based on what we know about the
+ *	session that has expired, and inject it into the server to
+ *	allow final logging or cleaning up.
+ */
+static REQUEST *fr_state_cleanup_request(state_entry_t *entry)
+{
+	REQUEST		*request;
+	RADIUS_PACKET	*packet = NULL;
+	RADIUS_PACKET	*reply_packet = NULL;
+	VALUE_PAIR	*vp;
+
+	/*
+	 *	Allocate a new fake request with enough to keep
+	 *	the rest of the server happy.
+	 */
+	request = request_alloc(NULL);
+	if (unlikely(!request)) return NULL;
+
+	packet = rad_alloc(request, false);
+	if (unlikely(!packet)) {
+	error:
+		TALLOC_FREE(reply_packet);
+		TALLOC_FREE(packet);
+		TALLOC_FREE(request);
+		return NULL;
+	}
+
+	reply_packet = rad_alloc(request, false);
+	if (unlikely(!reply_packet)) goto error;
+
+	/*
+	 *	Move the server from the state entry over to the
+	 *	request. Clearing it in the state means this
+	 *	function will never be called again.
+	 */
+	request->server = talloc_steal(request, entry->server);
+	entry->server = NULL;
+
+	/*
+	 *	Build the fake request with the limited
+	 *	information we have from the state.
+	 */
+	request->packet = packet;
+	request->reply = reply_packet;
+	request->number = entry->request_number;
+	request->client = entry->request_client;
+	request->root = entry->request_root;
+	request->handle = rad_postauth;
+
+	/*
+	 *	Move session-state VPS over, after first freeing the
+	 *	separately-parented state_ctx that was allocated along with the
+	 *	fake request.
+	 */
+	talloc_free(request->state_ctx);
+	request->state_ctx = entry->ctx;
+	request->state = entry->vps;
+
+	entry->ctx = NULL;
+	entry->vps = NULL;
+
+	/*
+	 *	Set correct Post-Auth-Type section
+	 */
+	fr_pair_delete_by_num(&request->config, PW_POST_AUTH_TYPE, 0, TAG_ANY);
+	vp = pair_make_config("Post-Auth-Type", "Client-Lost", T_OP_SET);
+	if (unlikely(!vp)) goto error;
+
+	VERIFY_REQUEST(request);
+	return request;
+}
+
+/*
+ *	Check state for old entries that need to be cleaned up. If
+ *	they are old enough then move them from the global state
+ *	list to a list of entries to clean up (outside the mutex).
+ *	Called with the mutex held.
+ */
+static state_entry_t *fr_state_cleanup_find(fr_state_t *state)
+{
+	time_t now = time(NULL);
+	state_entry_t *entry, *next;
+	state_entry_t *head = NULL, **tail = &head;
+
+	for (entry = state->head; entry != NULL; entry = next) {
+		next = entry->next;
+
+		/*
+		 *	Unused.  We can delete it, even if now isn't
+		 *	the time to clean it up.
+		 */
+		if (!entry->ctx && !entry->opaque) {
+			state_entry_free(state, entry);
+			continue;
+		}
+
+		/*
+		 *	Not yet time to clean it up.
+		 */
+		if (entry->cleanup > now) {
+			continue;
+		}
+
+		/*
+		 *	We're not running the "client lost" section.
+		 *	Just nuke the entry now.
+		 */
+		if (!main_config.postauth_client_lost) {
+			state_entry_free(state, entry);
+			continue;
+		}
+
+		/*
+		 *	Old enough that the request has been removed.
+		 *	We can add it to the cleanup list.
+		 */
+		state_entry_unlink(state, entry);
+		entry->prev = entry->next = NULL;
+		(*tail) = entry;
+		tail = &entry->next;
+	}
+
+	return head;
+}
+
+/*
+ *	Inject all requests in cleanup list for cleanup post-auth
+ */
+static void fr_state_cleanup(state_entry_t *head)
+{
+	state_entry_t *entry, *next;
+
+	if (!head) return;
+
+	for (entry = head; entry != NULL; entry = next) {
+		REQUEST *request;
+
+		next = entry->next;
+
+		request = fr_state_cleanup_request(entry);
+		if (request) {
+			RDEBUG2("No response from client, cleaning up expired state");
+			RDEBUG2("Restoring &session-state");
+
+			/*
+			 *	@todo - print out message
+			 *	saying where the handler was
+			 *	in the process?  i.e. "sent
+			 *	server cert", etc.  This will
+			 *	require updating the EAP code
+			 *	to put a new attribute into
+			 *	the session state list.
+			 */
+
+			rdebug_pair_list(L_DBG_LVL_2, request, request->state, "&session-state:");
+
+			request_inject(request);
+		}
+
+		talloc_free(entry);
+	}
+}
+
+
+/*
+ *	Create a new entry.  Called with the mutex held.
+ */
+static state_entry_t *fr_state_entry_create(fr_state_t *state, REQUEST *request, RADIUS_PACKET *packet, state_entry_t *old)
+{
+	size_t i;
+	uint32_t x;
+	time_t now = time(NULL);
+	VALUE_PAIR *vp;
+	state_entry_t *entry;
+
+	/*
+	 *	Limit the size of the cache based on how many requests
+	 *	we can handle at the same time.
+	 */
+	if (rbtree_num_elements(state->tree) >= main_config.max_requests * 2) {
+		return NULL;
+	}
+
+	/*
+	 *	Allocate a new one.
+	 */
+	entry = talloc_zero(state->tree, state_entry_t);
+	if (!entry) return NULL;
+
+	/*
+	 *	Limit the lifetime of this entry based on how long the
+	 *	server takes to process a request.  Doing it this way
+	 *	isn't perfect, but it's reasonable, and it's one less
+	 *	thing for an administrator to configure.
+	 */
+	entry->cleanup = now + main_config.max_request_time * 2;
+
+	/*
+	 *	Hacks for EAP, until we convert EAP to using the state API.
+	 *
+	 *	The EAP module creates it's own State attribute, so we
+	 *	want to use that one in preference to one we create.
+	 */
+	vp = fr_pair_find_by_num(packet->vps, PW_STATE, 0, TAG_ANY);
+
+	/*
+	 *	If possible, base the new one off of the old one.
+	 */
+	if (old) {
+		entry->tries = old->tries + 1;
+
+		/*
+		 *	Track State
+		 */
+		if (!vp) {
+			memcpy(entry->state, old->state, sizeof(entry->state));
+
+			entry->state[1] = entry->state[0] ^ entry->tries;
+			entry->state[8] = entry->state[2] ^ ((((uint32_t) HEXIFY(RADIUSD_VERSION)) >> 16) & 0xff);
+			entry->state[10] = entry->state[2] ^ ((((uint32_t) HEXIFY(RADIUSD_VERSION)) >> 8) & 0xff);
+			entry->state[12] = entry->state[2] ^ (((uint32_t) HEXIFY(RADIUSD_VERSION)) & 0xff);
+		}
+
+		/*
+		 *	The old one isn't used any more, so we can free it.
+		 */
+		if (!old->opaque) state_entry_free(state, old);
+
+	} else if (!vp) {
+		/*
+		 *	16 octets of randomness should be enough to
+		 *	have a globally unique state.
+		 */
+		for (i = 0; i < sizeof(entry->state) / sizeof(x); i++) {
+			x = fr_rand();
+			memcpy(entry->state + (i * 4), &x, sizeof(x));
+		}
+	}
+
+	/*
+	 *	If EAP created a State, use that.  Otherwise, use the
+	 *	one we created above.
+	 */
+	if (vp) {
+		/*
+		 *	Assume our own State first.
+		 */
+		if (vp->vp_length == sizeof(entry->state)) {
+			memcpy(entry->state, vp->vp_octets, sizeof(entry->state));
+
+			/*
+			 *	Too big?  Get the MD5 hash, in order
+			 *	to depend on the entire contents of State.
+			 */
+		} else if (vp->vp_length > sizeof(entry->state)) {
+			fr_md5_calc(entry->state, vp->vp_octets, vp->vp_length);
+
+			/*
+			 *	Too small?  Use the whole thing, and
+			 *	set the rest of entry->state to zero.
+			 */
+		} else {
+			memcpy(entry->state, vp->vp_octets, vp->vp_length);
+			memset(&entry->state[vp->vp_length], 0, sizeof(entry->state) - vp->vp_length);
+		}
+	} else {
+		vp = fr_pair_afrom_num(packet, PW_STATE, 0);
+		fr_pair_value_memcpy(vp, entry->state, sizeof(entry->state));
+		fr_pair_add(&packet->vps, vp);
+	}
+
+	/*	Make unique for different virtual servers handling same request
+	 */
+	if (request->server) {
+		/*
+		 *	Make unique for different virtual servers handling same request
+		 */
+		*((uint32_t *)(&entry->state[4])) ^= fr_hash_string(request->server);
+
+		/*
+		 *	Copy server to state in case it's needed for cleanup
+		 */
+		entry->server = talloc_strdup(entry, request->server);
+		entry->request_number = request->number;
+		entry->request_client = request->client;
+		entry->request_root = request->root;
+	}
+
+	if (!state_entry_link(state, entry)) {
+		talloc_free(entry);
+		return NULL;
+	}
+
+	return entry;
+}
+
+
+/*
+ *	Find the entry, based on the State attribute.
+ */
+static state_entry_t *fr_state_find(fr_state_t *state, const char *server, RADIUS_PACKET *packet)
+{
+	VALUE_PAIR *vp;
+	state_entry_t *entry, my_entry;
+
+	vp = fr_pair_find_by_num(packet->vps, PW_STATE, 0, TAG_ANY);
+	if (!vp) return NULL;
+
+	/*
+	 *	Assume our own State first.
+	 */
+	if (vp->vp_length == sizeof(my_entry.state)) {
+		memcpy(my_entry.state, vp->vp_octets, sizeof(my_entry.state));
+
+		/*
+		 *	Too big?  Get the MD5 hash, in order
+		 *	to depend on the entire contents of State.
+		 */
+	} else if (vp->vp_length > sizeof(my_entry.state)) {
+		fr_md5_calc(my_entry.state, vp->vp_octets, vp->vp_length);
+
+		/*
+		 *	Too small?  Use the whole thing, and
+		 *	set the rest of my_entry.state to zero.
+		 */
+	} else {
+		memcpy(my_entry.state, vp->vp_octets, vp->vp_length);
+		memset(&my_entry.state[vp->vp_length], 0, sizeof(my_entry.state) - vp->vp_length);
+	}
+
+	/*	Make unique for different virtual servers handling same request
+	 */
+	if (server) *((uint32_t *)(&my_entry.state[4])) ^= fr_hash_string(server);
+
+	entry = rbtree_finddata(state->tree, &my_entry);
+
+#ifdef WITH_VERIFY_PTR
+	if (entry)  (void) talloc_get_type_abort(entry, state_entry_t);
+#endif
+
+	return entry;
+}
+
+/*
+ *	Called when sending Access-Accept or Access-Reject, so
+ *	that all State is discarded.
+ */
+void fr_state_discard(REQUEST *request, RADIUS_PACKET *original)
+{
+	state_entry_t *entry;
+	fr_state_t *state = &global_state;
+
+	fr_pair_list_free(&request->state);
+	request->state = NULL;
+
+	PTHREAD_MUTEX_LOCK(&state->mutex);
+	entry = fr_state_find(state, request->server, original);
+	if (entry) state_entry_free(state, entry);
+	PTHREAD_MUTEX_UNLOCK(&state->mutex);
+}
+
+/*
+ *	Get the VPS from the state.
+ */
+void fr_state_get_vps(REQUEST *request, RADIUS_PACKET *packet)
+{
+	state_entry_t *entry;
+	fr_state_t *state = &global_state;
+	TALLOC_CTX *old_ctx = NULL;
+
+	/*
+	 *	No State, don't do anything.
+	 */
+	if (!fr_pair_find_by_num(request->packet->vps, PW_STATE, 0, TAG_ANY)) {
+		RDEBUG3("session-state: No State attribute");
+		return;
+	}
+
+	rad_assert(request->state == NULL);
+
+	PTHREAD_MUTEX_LOCK(&state->mutex);
+	entry = fr_state_find(state, request->server, packet);
+
+	/*
+	 *	This has to be done in a mutex lock, because talloc
+	 *	isn't thread-safe.
+	 */
+	if (entry) {
+		RDEBUG2("Restoring &session-state");
+
+		if (request->state_ctx) old_ctx = request->state_ctx;
+
+		request->state_ctx = entry->ctx;
+		request->state = entry->vps;
+
+		entry->ctx = NULL;
+		entry->vps = NULL;
+
+		rdebug_pair_list(L_DBG_LVL_2, request, request->state, "&session-state:");
+
+	} else {
+		RDEBUG2("session-state: No cached attributes");
+	}
+
+	PTHREAD_MUTEX_UNLOCK(&state->mutex);
+
+	/*
+	 *	Free this outside of the mutex for less contention.
+	 */
+	if (old_ctx) talloc_free(old_ctx);
+
+	VERIFY_REQUEST(request);
+	return;
+}
+
+
+/*
+ *	Put request->state into the State attribute.  Put the State
+ *	attribute into the vps list.  Delete the original entry, if it
+ *	exists.
+ */
+bool fr_state_put_vps(REQUEST *request, RADIUS_PACKET *original, RADIUS_PACKET *packet)
+{
+	state_entry_t *entry, *old;
+	fr_state_t *state = &global_state;
+	state_entry_t *cleanup_list;
+
+	if (!request->state) {
+		size_t i;
+		uint32_t x;
+		VALUE_PAIR *vp;
+		uint8_t buffer[16];
+
+		RDEBUG3("session-state: Nothing to cache");
+
+		if (packet->code != PW_CODE_ACCESS_CHALLENGE) return true;
+
+		vp = fr_pair_find_by_num(packet->vps, PW_STATE, 0, TAG_ANY);
+		if (vp) return true;
+
+		/*
+		 *
+		 */
+		for (i = 0; i < sizeof(buffer) / sizeof(x); i++) {
+			x = fr_rand();
+			memcpy(buffer + (i * 4), &x, sizeof(x));
+		}
+
+		vp = fr_pair_afrom_num(packet, PW_STATE, 0);
+		fr_pair_value_memcpy(vp, buffer, sizeof(buffer));
+		fr_pair_add(&packet->vps, vp);
+
+		return true;
+	}
+
+	RDEBUG2("session-state: Saving cached attributes");
+	rdebug_pair_list(L_DBG_LVL_1, request, request->state, NULL);
+
+	PTHREAD_MUTEX_LOCK(&state->mutex);
+
+	cleanup_list = fr_state_cleanup_find(state);
+
+	if (original) {
+		old = fr_state_find(state, request->server, original);
+	} else {
+		old = NULL;
+	}
+
+	/*
+	 *	Create a new entry and add it to the list.
+	 */
+	entry = fr_state_entry_create(state, request, packet, old);
+	if (!entry) {
+		PTHREAD_MUTEX_UNLOCK(&state->mutex);
+		fr_state_cleanup(cleanup_list);
+		return false;
+	}
+
+	rad_assert(entry->ctx == NULL);
+	entry->ctx = request->state_ctx;
+	entry->vps = request->state;
+
+	request->state_ctx = NULL;
+	request->state = NULL;
+
+	PTHREAD_MUTEX_UNLOCK(&state->mutex);
+	fr_state_cleanup(cleanup_list);
+
+	VERIFY_REQUEST(request);
+	return true;
+}
diff --git a/src/main/stats.c b/src/main/stats.c
new file mode 100644
index 0000000..a5c672e
--- /dev/null
+++ b/src/main/stats.c
@@ -0,0 +1,1028 @@
+/*
+ * stats.c	Internal statistics handling.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2008  The FreeRADIUS server project
+ * Copyright 2008  Alan DeKok <aland@deployingradius.com>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/rad_assert.h>
+
+#ifdef WITH_STATS
+
+#define USEC (1000000)
+#define EMA_SCALE (100)
+#define F_EMA_SCALE (1000000)
+
+static struct timeval	start_time;
+static struct timeval	hup_time;
+
+#define FR_STATS_INIT { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,	\
+				 { 0, 0, 0, 0, 0, 0, 0, 0 }}
+
+fr_stats_t radius_auth_stats = FR_STATS_INIT;
+#ifdef WITH_ACCOUNTING
+fr_stats_t radius_acct_stats = FR_STATS_INIT;
+#endif
+#ifdef WITH_COA
+fr_stats_t radius_coa_stats = FR_STATS_INIT;
+fr_stats_t radius_dsc_stats = FR_STATS_INIT;
+#endif
+
+#ifdef WITH_PROXY
+fr_stats_t proxy_auth_stats = FR_STATS_INIT;
+#ifdef WITH_ACCOUNTING
+fr_stats_t proxy_acct_stats = FR_STATS_INIT;
+#endif
+#ifdef WITH_COA
+fr_stats_t proxy_coa_stats = FR_STATS_INIT;
+fr_stats_t proxy_dsc_stats = FR_STATS_INIT;
+#endif
+#endif
+
+static void stats_time(fr_stats_t *stats, struct timeval *start,
+		       struct timeval *end)
+{
+	struct timeval diff;
+	uint32_t delay;
+
+	if ((start->tv_sec == 0) || (end->tv_sec == 0) ||
+	    (end->tv_sec < start->tv_sec)) return;
+
+	rad_tv_sub(end, start, &diff);
+
+	if (diff.tv_sec >= 10) {
+		stats->elapsed[7]++;
+	} else {
+		int i;
+		uint32_t cmp;
+
+		delay = (diff.tv_sec * USEC) + diff.tv_usec;
+
+		cmp = 10;
+		for (i = 0; i < 7; i++) {
+			if (delay < cmp) {
+				stats->elapsed[i]++;
+				break;
+			}
+			cmp *= 10;
+		}
+	}
+}
+
+void request_stats_final(REQUEST *request)
+{
+	rad_listen_t *listener;
+
+	if (request->master_state == REQUEST_COUNTED) return;
+
+	if (!request->listener) return;
+	if (!request->client) return;
+
+	if ((request->listener->type != RAD_LISTEN_NONE) &&
+#ifdef WITH_ACCOUNTING
+	    (request->listener->type != RAD_LISTEN_ACCT) &&
+#endif
+#ifdef WITH_COA
+	    (request->listener->type != RAD_LISTEN_COA) &&
+#endif
+	    (request->listener->type != RAD_LISTEN_AUTH)) return;
+
+	/* don't count statistic requests */
+	if (request->packet->code == PW_CODE_STATUS_SERVER)
+		return;
+
+	/*
+	 *	Deal with TCP / TLS issues.  The statistics are kept in the parent socket.
+	 */
+	listener = request->listener;
+	if (listener->parent) listener = listener->parent;
+
+#undef INC_AUTH
+#define INC_AUTH(_x) radius_auth_stats._x++;listener->stats._x++;request->client->auth._x++;
+
+#undef INC_ACCT
+#ifdef WITH_ACCOUNTING
+#define INC_ACCT(_x) radius_acct_stats._x++;listener->stats._x++;request->client->acct._x++
+#else
+#define INC_ACCT(_x)
+#endif
+
+#undef INC_COA
+#ifdef WITH_COA
+#define INC_COA(_x) radius_coa_stats._x++;listener->stats._x++;request->client->coa._x++
+#else
+#define INC_COA(_x)
+#endif
+
+#undef INC_DSC
+#ifdef WITH_DSC
+#define INC_DSC(_x) radius_dsc_stats._x++;listener->stats._x++;request->client->dsc._x++
+#else
+#define INC_DSC(_x)
+#endif
+
+	/*
+	 *	Update the statistics.
+	 *
+	 *	Note that we do NOT do this in a child thread.
+	 *	Instead, we update the stats when a request is
+	 *	deleted, because only the main server thread calls
+	 *	this function, which makes it thread-safe.
+	 */
+	if (request->reply && (request->packet->code != PW_CODE_STATUS_SERVER)) switch (request->reply->code) {
+	case PW_CODE_ACCESS_ACCEPT:
+		INC_AUTH(total_access_accepts);
+
+		auth_stats:
+		INC_AUTH(total_responses);
+
+		/*
+		 *	FIXME: Do the time calculations once...
+		 */
+		stats_time(&radius_auth_stats,
+			   &request->packet->timestamp,
+			   &request->reply->timestamp);
+		stats_time(&request->client->auth,
+			   &request->packet->timestamp,
+			   &request->reply->timestamp);
+		stats_time(&listener->stats,
+			   &request->packet->timestamp,
+			   &request->reply->timestamp);
+		break;
+
+	case PW_CODE_ACCESS_REJECT:
+		INC_AUTH(total_access_rejects);
+		goto auth_stats;
+
+	case PW_CODE_ACCESS_CHALLENGE:
+		INC_AUTH(total_access_challenges);
+		goto auth_stats;
+
+#ifdef WITH_ACCOUNTING
+	case PW_CODE_ACCOUNTING_RESPONSE:
+		INC_ACCT(total_responses);
+		stats_time(&radius_acct_stats,
+			   &request->packet->timestamp,
+			   &request->reply->timestamp);
+		stats_time(&request->client->acct,
+			   &request->packet->timestamp,
+			   &request->reply->timestamp);
+		break;
+#endif
+
+#ifdef WITH_COA
+	case PW_CODE_COA_ACK:
+		INC_COA(total_access_accepts);
+	  coa_stats:
+		INC_COA(total_responses);
+		stats_time(&request->client->coa,
+			   &request->packet->timestamp,
+			   &request->reply->timestamp);
+		break;
+
+	case PW_CODE_COA_NAK:
+		INC_COA(total_access_rejects);
+		goto coa_stats;
+
+	case PW_CODE_DISCONNECT_ACK:
+		INC_DSC(total_access_accepts);
+	  dsc_stats:
+		INC_DSC(total_responses);
+		stats_time(&request->client->dsc,
+			   &request->packet->timestamp,
+			   &request->reply->timestamp);
+		break;
+
+	case PW_CODE_DISCONNECT_NAK:
+		INC_DSC(total_access_rejects);
+		goto dsc_stats;
+#endif
+
+		/*
+		 *	No response, it must have been a bad
+		 *	authenticator.
+		 */
+	case 0:
+		if (request->packet->code == PW_CODE_ACCESS_REQUEST) {
+			if (request->reply->offset == -2) {
+				INC_AUTH(total_bad_authenticators);
+			} else {
+				INC_AUTH(total_packets_dropped);
+			}
+		} else if (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) {
+			if (request->reply->offset == -2) {
+				INC_ACCT(total_bad_authenticators);
+			} else {
+				INC_ACCT(total_packets_dropped);
+			}
+		}
+		break;
+
+	default:
+		break;
+	}
+
+#ifdef WITH_PROXY
+	if (!request->proxy || !request->home_server) goto done;	/* simplifies formatting */
+
+	switch (request->proxy->code) {
+	case PW_CODE_ACCESS_REQUEST:
+		proxy_auth_stats.total_requests += request->num_proxied_requests;
+		request->home_server->stats.total_requests += request->num_proxied_requests;
+		break;
+
+#ifdef WITH_ACCOUNTING
+	case PW_CODE_ACCOUNTING_REQUEST:
+		proxy_acct_stats.total_requests += request->num_proxied_requests;
+		request->home_server->stats.total_requests += request->num_proxied_requests;
+		break;
+#endif
+
+#ifdef WITH_COA
+	case PW_CODE_COA_REQUEST:
+		proxy_coa_stats.total_requests += request->num_proxied_requests;
+		request->home_server->stats.total_requests += request->num_proxied_requests;
+		break;
+
+	case PW_CODE_DISCONNECT_REQUEST:
+		proxy_dsc_stats.total_requests += request->num_proxied_requests;
+		request->home_server->stats.total_requests += request->num_proxied_requests;
+		break;
+#endif
+
+	default:
+		break;
+	}
+
+	if (!request->proxy_reply) goto done;	/* simplifies formatting */
+
+#undef INC
+#define INC(_x) proxy_auth_stats._x += request->num_proxied_responses; request->home_server->stats._x += request->num_proxied_responses;
+
+	switch (request->proxy_reply->code) {
+	case PW_CODE_ACCESS_ACCEPT:
+		INC(total_access_accepts);
+	proxy_stats:
+		INC(total_responses);
+		stats_time(&proxy_auth_stats,
+			   &request->proxy->timestamp,
+			   &request->proxy_reply->timestamp);
+		stats_time(&request->home_server->stats,
+			   &request->proxy->timestamp,
+			   &request->proxy_reply->timestamp);
+		break;
+
+	case PW_CODE_ACCESS_REJECT:
+		INC(total_access_rejects);
+		goto proxy_stats;
+
+	case PW_CODE_ACCESS_CHALLENGE:
+		INC(total_access_challenges);
+		goto proxy_stats;
+
+#ifdef WITH_ACCOUNTING
+	case PW_CODE_ACCOUNTING_RESPONSE:
+		proxy_acct_stats.total_responses++;
+		request->home_server->stats.total_responses++;
+		stats_time(&proxy_acct_stats,
+			   &request->proxy->timestamp,
+			   &request->proxy_reply->timestamp);
+		stats_time(&request->home_server->stats,
+			   &request->proxy->timestamp,
+			   &request->proxy_reply->timestamp);
+		break;
+#endif
+
+#ifdef WITH_COA
+	case PW_CODE_COA_ACK:
+	case PW_CODE_COA_NAK:
+		proxy_coa_stats.total_responses++;
+		request->home_server->stats.total_responses++;
+		stats_time(&proxy_coa_stats,
+			   &request->proxy->timestamp,
+			   &request->proxy_reply->timestamp);
+		stats_time(&request->home_server->stats,
+			   &request->proxy->timestamp,
+			   &request->proxy_reply->timestamp);
+		break;
+
+	case PW_CODE_DISCONNECT_ACK:
+	case PW_CODE_DISCONNECT_NAK:
+		proxy_dsc_stats.total_responses++;
+		request->home_server->stats.total_responses++;
+		stats_time(&proxy_dsc_stats,
+			   &request->proxy->timestamp,
+			   &request->proxy_reply->timestamp);
+		stats_time(&request->home_server->stats,
+			   &request->proxy->timestamp,
+			   &request->proxy_reply->timestamp);
+		break;
+#endif
+
+	default:
+		proxy_auth_stats.total_unknown_types++;
+		request->home_server->stats.total_unknown_types++;
+		break;
+	}
+
+ done:
+#endif /* WITH_PROXY */
+
+
+	if (request->max_time) {
+		RADCLIENT *client = request->client;
+
+		switch (request->packet->code) {
+		case PW_CODE_ACCESS_REQUEST:
+			FR_STATS_INC(auth, unresponsive_child);
+			break;
+
+#ifdef WITH_ACCOUNTING
+		case PW_CODE_ACCOUNTING_REQUEST:
+			FR_STATS_INC(acct, unresponsive_child);
+			break;
+#endif
+#ifdef WITH_COA
+		case PW_CODE_COA_REQUEST:
+			FR_STATS_INC(coa, unresponsive_child);
+			break;
+
+		case PW_CODE_DISCONNECT_REQUEST:
+			FR_STATS_INC(dsc, unresponsive_child);
+			break;
+#endif
+
+		default:
+			break;
+		}
+	}
+
+	request->master_state = REQUEST_COUNTED;
+}
+
+typedef struct fr_stats2vp {
+	int	attribute;
+	size_t	offset;
+} fr_stats2vp;
+
+/*
+ *	Authentication
+ */
+static fr_stats2vp authvp[] = {
+	{ PW_FREERADIUS_TOTAL_ACCESS_REQUESTS, offsetof(fr_stats_t, total_requests) },
+	{ PW_FREERADIUS_TOTAL_ACCESS_ACCEPTS, offsetof(fr_stats_t, total_access_accepts) },
+	{ PW_FREERADIUS_TOTAL_ACCESS_REJECTS, offsetof(fr_stats_t, total_access_rejects) },
+	{ PW_FREERADIUS_TOTAL_ACCESS_CHALLENGES, offsetof(fr_stats_t, total_access_challenges) },
+	{ PW_FREERADIUS_TOTAL_AUTH_RESPONSES, offsetof(fr_stats_t, total_responses) },
+	{ PW_FREERADIUS_TOTAL_AUTH_DUPLICATE_REQUESTS, offsetof(fr_stats_t, total_dup_requests) },
+	{ PW_FREERADIUS_TOTAL_AUTH_MALFORMED_REQUESTS, offsetof(fr_stats_t, total_malformed_requests) },
+	{ PW_FREERADIUS_TOTAL_AUTH_INVALID_REQUESTS, offsetof(fr_stats_t, total_bad_authenticators) },
+	{ PW_FREERADIUS_TOTAL_AUTH_DROPPED_REQUESTS, offsetof(fr_stats_t, total_packets_dropped) },
+	{ PW_FREERADIUS_TOTAL_AUTH_UNKNOWN_TYPES, offsetof(fr_stats_t, total_unknown_types) },
+	{ PW_FREERADIUS_TOTAL_AUTH_CONFLICTS, offsetof(fr_stats_t, total_conflicts) },
+	{ 0, 0 }
+};
+
+
+#ifdef WITH_PROXY
+/*
+ *	Proxied authentication requests.
+ */
+static fr_stats2vp proxy_authvp[] = {
+	{ PW_FREERADIUS_TOTAL_PROXY_ACCESS_REQUESTS, offsetof(fr_stats_t, total_requests) },
+	{ PW_FREERADIUS_TOTAL_PROXY_ACCESS_ACCEPTS, offsetof(fr_stats_t, total_access_accepts) },
+	{ PW_FREERADIUS_TOTAL_PROXY_ACCESS_REJECTS, offsetof(fr_stats_t, total_access_rejects) },
+	{ PW_FREERADIUS_TOTAL_PROXY_ACCESS_CHALLENGES, offsetof(fr_stats_t, total_access_challenges) },
+	{ PW_FREERADIUS_TOTAL_PROXY_AUTH_RESPONSES, offsetof(fr_stats_t, total_responses) },
+	{ PW_FREERADIUS_TOTAL_PROXY_AUTH_DUPLICATE_REQUESTS, offsetof(fr_stats_t, total_dup_requests) },
+	{ PW_FREERADIUS_TOTAL_PROXY_AUTH_MALFORMED_REQUESTS, offsetof(fr_stats_t, total_malformed_requests) },
+	{ PW_FREERADIUS_TOTAL_PROXY_AUTH_INVALID_REQUESTS, offsetof(fr_stats_t, total_bad_authenticators) },
+	{ PW_FREERADIUS_TOTAL_PROXY_AUTH_DROPPED_REQUESTS, offsetof(fr_stats_t, total_packets_dropped) },
+	{ PW_FREERADIUS_TOTAL_PROXY_AUTH_UNKNOWN_TYPES, offsetof(fr_stats_t, total_unknown_types) },
+	{ 0, 0 }
+};
+#endif
+
+
+#ifdef WITH_ACCOUNTING
+/*
+ *	Accounting
+ */
+static fr_stats2vp acctvp[] = {
+	{ PW_FREERADIUS_TOTAL_ACCOUNTING_REQUESTS, offsetof(fr_stats_t, total_requests) },
+	{ PW_FREERADIUS_TOTAL_ACCOUNTING_RESPONSES, offsetof(fr_stats_t, total_responses) },
+	{ PW_FREERADIUS_TOTAL_ACCT_DUPLICATE_REQUESTS, offsetof(fr_stats_t, total_dup_requests) },
+	{ PW_FREERADIUS_TOTAL_ACCT_MALFORMED_REQUESTS, offsetof(fr_stats_t, total_malformed_requests) },
+	{ PW_FREERADIUS_TOTAL_ACCT_INVALID_REQUESTS, offsetof(fr_stats_t, total_bad_authenticators) },
+	{ PW_FREERADIUS_TOTAL_ACCT_DROPPED_REQUESTS, offsetof(fr_stats_t, total_packets_dropped) },
+	{ PW_FREERADIUS_TOTAL_ACCT_UNKNOWN_TYPES, offsetof(fr_stats_t, total_unknown_types) },
+	{ PW_FREERADIUS_TOTAL_ACCT_CONFLICTS, offsetof(fr_stats_t, total_conflicts) },
+	{ 0, 0 }
+};
+
+#ifdef WITH_PROXY
+static fr_stats2vp proxy_acctvp[] = {
+	{ PW_FREERADIUS_TOTAL_PROXY_ACCOUNTING_REQUESTS, offsetof(fr_stats_t, total_requests) },
+	{ PW_FREERADIUS_TOTAL_PROXY_ACCOUNTING_RESPONSES, offsetof(fr_stats_t, total_responses) },
+	{ PW_FREERADIUS_TOTAL_PROXY_ACCT_DUPLICATE_REQUESTS, offsetof(fr_stats_t, total_dup_requests) },
+	{ PW_FREERADIUS_TOTAL_PROXY_ACCT_MALFORMED_REQUESTS, offsetof(fr_stats_t, total_malformed_requests) },
+	{ PW_FREERADIUS_TOTAL_PROXY_ACCT_INVALID_REQUESTS, offsetof(fr_stats_t, total_bad_authenticators) },
+	{ PW_FREERADIUS_TOTAL_PROXY_ACCT_DROPPED_REQUESTS, offsetof(fr_stats_t, total_packets_dropped) },
+	{ PW_FREERADIUS_TOTAL_PROXY_ACCT_UNKNOWN_TYPES, offsetof(fr_stats_t, total_unknown_types) },
+	{ 0, 0 }
+};
+#endif
+#endif
+
+static fr_stats2vp client_authvp[] = {
+	{ PW_FREERADIUS_TOTAL_ACCESS_REQUESTS, offsetof(fr_stats_t, total_requests) },
+	{ PW_FREERADIUS_TOTAL_ACCESS_ACCEPTS, offsetof(fr_stats_t, total_access_accepts) },
+	{ PW_FREERADIUS_TOTAL_ACCESS_REJECTS, offsetof(fr_stats_t, total_access_rejects) },
+	{ PW_FREERADIUS_TOTAL_ACCESS_CHALLENGES, offsetof(fr_stats_t, total_access_challenges) },
+	{ PW_FREERADIUS_TOTAL_AUTH_RESPONSES, offsetof(fr_stats_t, total_responses) },
+	{ PW_FREERADIUS_TOTAL_AUTH_DUPLICATE_REQUESTS, offsetof(fr_stats_t, total_dup_requests) },
+	{ PW_FREERADIUS_TOTAL_AUTH_MALFORMED_REQUESTS, offsetof(fr_stats_t, total_malformed_requests) },
+	{ PW_FREERADIUS_TOTAL_AUTH_INVALID_REQUESTS, offsetof(fr_stats_t, total_bad_authenticators) },
+	{ PW_FREERADIUS_TOTAL_AUTH_DROPPED_REQUESTS, offsetof(fr_stats_t, total_packets_dropped) },
+	{ PW_FREERADIUS_TOTAL_AUTH_UNKNOWN_TYPES, offsetof(fr_stats_t, total_unknown_types) },
+	{ 0, 0 }
+};
+
+#ifdef WITH_ACCOUNTING
+static fr_stats2vp client_acctvp[] = {
+	{ PW_FREERADIUS_TOTAL_ACCOUNTING_REQUESTS, offsetof(fr_stats_t, total_requests) },
+	{ PW_FREERADIUS_TOTAL_ACCOUNTING_RESPONSES, offsetof(fr_stats_t, total_responses) },
+	{ PW_FREERADIUS_TOTAL_ACCT_DUPLICATE_REQUESTS, offsetof(fr_stats_t, total_dup_requests) },
+	{ PW_FREERADIUS_TOTAL_ACCT_MALFORMED_REQUESTS, offsetof(fr_stats_t, total_malformed_requests) },
+	{ PW_FREERADIUS_TOTAL_ACCT_INVALID_REQUESTS, offsetof(fr_stats_t, total_bad_authenticators) },
+	{ PW_FREERADIUS_TOTAL_ACCT_DROPPED_REQUESTS, offsetof(fr_stats_t, total_packets_dropped) },
+	{ PW_FREERADIUS_TOTAL_ACCT_UNKNOWN_TYPES, offsetof(fr_stats_t, total_unknown_types) },
+	{ 0, 0 }
+};
+#endif
+
+static void request_stats_addvp(REQUEST *request,
+				fr_stats2vp *table, fr_stats_t *stats)
+{
+	int i;
+	uint64_t counter;
+	VALUE_PAIR *vp;
+
+	for (i = 0; table[i].attribute != 0; i++) {
+		vp = radius_pair_create(request->reply, &request->reply->vps,
+				       table[i].attribute, VENDORPEC_FREERADIUS);
+		if (!vp) continue;
+
+		counter = *(uint64_t *) (((uint8_t *) stats) + table[i].offset);
+		vp->vp_integer = counter;
+	}
+}
+
+static void stats_error(REQUEST *request, char const *msg)
+{
+	VALUE_PAIR *vp;
+
+	vp = radius_pair_create(request->reply, &request->reply->vps,
+				PW_FREERADIUS_STATS_ERROR, VENDORPEC_FREERADIUS);
+	if (!vp) return;
+
+	fr_pair_value_strcpy(vp, msg);
+}
+
+
+void request_stats_reply(REQUEST *request)
+{
+	VALUE_PAIR *flag, *vp;
+
+	/*
+	 *	Statistics are available ONLY on a "status" port.
+	 */
+	rad_assert(request->packet->code == PW_CODE_STATUS_SERVER);
+	rad_assert(request->listener->type == RAD_LISTEN_NONE);
+
+	flag = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATISTICS_TYPE, VENDORPEC_FREERADIUS, TAG_ANY);
+	if (!flag || (flag->vp_integer == 0)) return;
+
+	/*
+	 *	Authentication.
+	 */
+	if (((flag->vp_integer & 0x01) != 0) &&
+	    ((flag->vp_integer & 0xc0) == 0)) {
+		request_stats_addvp(request, authvp, &radius_auth_stats);
+	}
+
+#ifdef WITH_ACCOUNTING
+	/*
+	 *	Accounting
+	 */
+	if (((flag->vp_integer & 0x02) != 0) &&
+	    ((flag->vp_integer & 0xc0) == 0)) {
+		request_stats_addvp(request, acctvp, &radius_acct_stats);
+	}
+#endif
+
+#ifdef WITH_PROXY
+	/*
+	 *	Proxied authentication requests.
+	 */
+	if (((flag->vp_integer & 0x04) != 0) &&
+	    ((flag->vp_integer & 0x20) == 0)) {
+		request_stats_addvp(request, proxy_authvp, &proxy_auth_stats);
+	}
+
+#ifdef WITH_ACCOUNTING
+	/*
+	 *	Proxied accounting requests.
+	 */
+	if (((flag->vp_integer & 0x08) != 0) &&
+	    ((flag->vp_integer & 0x20) == 0)) {
+		request_stats_addvp(request, proxy_acctvp, &proxy_acct_stats);
+	}
+#endif
+#endif
+
+	/*
+	 *	Internal server statistics
+	 */
+	if ((flag->vp_integer & 0x10) != 0) {
+		vp = radius_pair_create(request->reply, &request->reply->vps,
+				       PW_FREERADIUS_STATS_START_TIME, VENDORPEC_FREERADIUS);
+		if (vp) vp->vp_date = start_time.tv_sec;
+		vp = radius_pair_create(request->reply, &request->reply->vps,
+				       PW_FREERADIUS_STATS_HUP_TIME, VENDORPEC_FREERADIUS);
+		if (vp) vp->vp_date = hup_time.tv_sec;
+
+#ifdef HAVE_PTHREAD_H
+		int i, array[RAD_LISTEN_MAX], stats[3];
+
+		thread_pool_queue_stats(array, stats);
+
+		for (i = 0; i <= 4; i++) {
+			vp = radius_pair_create(request->reply, &request->reply->vps,
+					       PW_FREERADIUS_QUEUE_LEN_INTERNAL + i, VENDORPEC_FREERADIUS);
+
+			if (!vp) continue;
+			vp->vp_integer = array[i];
+		}
+
+		for (i = 0; i < 2; i++) {
+			vp = radius_pair_create(request->reply, &request->reply->vps,
+					       PW_FREERADIUS_QUEUE_PPS_IN + i, VENDORPEC_FREERADIUS);
+
+			if (!vp) continue;
+			vp->vp_integer = stats[i];
+		}
+
+		thread_pool_thread_stats(stats);
+
+		for (i = 0; i < 3; i++) {
+			vp = radius_pair_create(request->reply, &request->reply->vps,
+					       PW_FREERADIUS_STATS_THREADS_ACTIVE + i, VENDORPEC_FREERADIUS);
+
+			if (!vp) continue;
+			vp->vp_integer = stats[i];
+		}
+#endif
+	}
+
+	/*
+	 *	For a particular client.
+	 */
+	if ((flag->vp_integer & 0x20) != 0) {
+		fr_ipaddr_t ipaddr;
+		VALUE_PAIR *server_ip, *server_port = NULL;
+		RADCLIENT *client = NULL;
+		RADCLIENT_LIST *cl = NULL;
+
+		/*
+		 *	See if we need to look up the client by server
+		 *	socket.
+		 */
+		server_ip = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_IP_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY);
+		if (server_ip) {
+			server_port = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_PORT, VENDORPEC_FREERADIUS, TAG_ANY);
+
+			if (server_port) {
+				ipaddr.af = AF_INET;
+				ipaddr.ipaddr.ip4addr.s_addr = server_ip->vp_ipaddr;
+				cl = listener_find_client_list(&ipaddr, server_port->vp_integer, IPPROTO_UDP);
+
+				/*
+				 *	Not found: don't do anything
+				 */
+				if (!cl) return;
+			}
+#ifdef AF_INET6
+		} else {
+			server_ip = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_IPV6_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY);
+			if (server_ip) {
+				server_port = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_PORT, VENDORPEC_FREERADIUS, TAG_ANY);
+				if (server_port) {
+					ipaddr.af = AF_INET6;
+					ipaddr.ipaddr.ip6addr = server_ip->vp_ipv6addr;
+					cl = listener_find_client_list(&ipaddr, server_port->vp_integer, IPPROTO_UDP);
+
+					/*
+					 *	Not found: don't do anything
+					 */
+					if (!cl) return;
+				}
+			}
+#endif	/* AF_INET6 */
+		}
+
+
+		vp = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_CLIENT_IP_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY);
+		if (vp) {
+			memset(&ipaddr, 0, sizeof(ipaddr));
+			ipaddr.af = AF_INET;
+			ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+			client = client_find(cl, &ipaddr, IPPROTO_UDP);
+#ifdef WITH_TCP
+			if (!client) {
+				client = client_find(cl, &ipaddr, IPPROTO_TCP);
+			}
+#endif
+
+#ifdef AF_INET6
+		} else if ((vp = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_CLIENT_IPV6_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY)) != NULL) {
+			memset(&ipaddr, 0, sizeof(ipaddr));
+			ipaddr.af = AF_INET6;
+			ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr;
+			client = client_find(cl, &ipaddr, IPPROTO_UDP);
+#ifdef WITH_TCP
+			if (!client) {
+				client = client_find(cl, &ipaddr, IPPROTO_TCP);
+			}
+#endif
+#endif	/* AF_INET6 */
+
+			/*
+			 *	Else look it up by number.
+			 */
+		} else if ((vp = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_CLIENT_NUMBER, VENDORPEC_FREERADIUS, TAG_ANY)) != NULL) {
+			client = client_findbynumber(cl, vp->vp_integer);
+		}
+
+		if (client) {
+			/*
+			 *	If found, echo it back, along with
+			 *	the requested statistics.
+			 */
+			fr_pair_add(&request->reply->vps, fr_pair_copy(request->reply, vp));
+
+			/*
+			 *	When retrieving client by number, also
+			 *	echo back it's IP address.
+			 */
+			if (vp->da->type == PW_TYPE_INTEGER) {
+				if (client->ipaddr.af == AF_INET) {
+					vp = radius_pair_create(request->reply,
+								&request->reply->vps,
+								PW_FREERADIUS_STATS_CLIENT_IP_ADDRESS, VENDORPEC_FREERADIUS);
+					if (vp) {
+						vp->vp_ipaddr = client->ipaddr.ipaddr.ip4addr.s_addr;
+					}
+
+					if (client->ipaddr.prefix != 32) {
+						vp = radius_pair_create(request->reply,
+									&request->reply->vps,
+									PW_FREERADIUS_STATS_CLIENT_NETMASK, VENDORPEC_FREERADIUS);
+						if (vp) {
+							vp->vp_integer = client->ipaddr.prefix;
+						}
+					}
+				}
+
+#ifdef AF_INET6
+				if (client->ipaddr.af == AF_INET6) {
+					vp = radius_pair_create(request->reply,
+								&request->reply->vps,
+								PW_FREERADIUS_STATS_CLIENT_IPV6_ADDRESS, VENDORPEC_FREERADIUS);
+					if (vp) {
+						vp->vp_ipv6addr = client->ipaddr.ipaddr.ip6addr;
+					}
+
+					if (client->ipaddr.prefix != 128) {
+						vp = radius_pair_create(request->reply,
+									&request->reply->vps,
+									PW_FREERADIUS_STATS_CLIENT_NETMASK, VENDORPEC_FREERADIUS);
+						if (vp) {
+							vp->vp_integer = client->ipaddr.prefix;
+						}
+					}
+				}
+#endif	/* AF_INET6 */
+			}
+
+			if (server_ip) {
+				fr_pair_add(&request->reply->vps,
+					fr_pair_copy(request->reply, server_ip));
+			}
+			if (server_port) {
+				fr_pair_add(&request->reply->vps,
+					fr_pair_copy(request->reply, server_port));
+			}
+
+			if ((flag->vp_integer & 0x01) != 0) {
+				request_stats_addvp(request, client_authvp,
+						    &client->auth);
+			}
+#ifdef WITH_ACCOUNTING
+			if ((flag->vp_integer & 0x02) != 0) {
+				request_stats_addvp(request, client_acctvp,
+						    &client->acct);
+			}
+#endif
+		} else {
+			/*
+			 *	No such client.
+			 */
+			stats_error(request, "No such client");
+		}
+	}
+
+	/*
+	 *	For a particular "listen" socket.
+	 */
+	if (((flag->vp_integer & 0x40) != 0) &&
+	    ((flag->vp_integer & 0x03) != 0)) {
+		rad_listen_t *this;
+		VALUE_PAIR *server_ip, *server_port;
+		fr_ipaddr_t ipaddr;
+
+		/*
+		 *	See if we need to look up the server by socket
+		 *	socket.
+		 */
+		server_port = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_PORT, VENDORPEC_FREERADIUS, TAG_ANY);
+		if (!server_port) return;
+
+		server_ip = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_IP_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY);
+		if (server_ip) {
+			ipaddr.af = AF_INET;
+			ipaddr.ipaddr.ip4addr.s_addr = server_ip->vp_ipaddr;
+#ifdef AF_INET6
+		} else if ((server_ip = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_IPV6_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY)) != NULL) {
+			ipaddr.af = AF_INET6;
+			ipaddr.ipaddr.ip6addr = server_ip->vp_ipv6addr;
+#endif	/* AF_INET6 */
+		} else {
+			stats_error(request, "No listener IP address supplied");
+		}
+
+		/*
+		 *	Not found: don't do anything
+		 */
+		this = listener_find_byipaddr(&ipaddr, server_port->vp_integer, IPPROTO_UDP);
+#ifdef WITH_TCP
+		if (!this) this = listener_find_byipaddr(&ipaddr, server_port->vp_integer, IPPROTO_TCP);
+#endif
+		if (!this) {
+			stats_error(request, "No such listener");
+			return;
+		}
+
+		fr_pair_add(&request->reply->vps,
+			fr_pair_copy(request->reply, server_ip));
+		fr_pair_add(&request->reply->vps,
+			fr_pair_copy(request->reply, server_port));
+
+		if ((flag->vp_integer & 0x01) != 0) {
+			if ((request->listener->type == RAD_LISTEN_AUTH) ||
+			    (request->listener->type == RAD_LISTEN_NONE)) {
+				request_stats_addvp(request, authvp, &this->stats);
+			} else {
+				stats_error(request, "Listener is not auth");
+			}
+		}
+
+#ifdef WITH_ACCOUNTING
+		if ((flag->vp_integer & 0x02) != 0) {
+			if ((request->listener->type == RAD_LISTEN_ACCT) ||
+			    (request->listener->type == RAD_LISTEN_NONE)) {
+				request_stats_addvp(request, acctvp, &this->stats);
+			} else {
+				stats_error(request, "Listener is not acct");
+			}
+		}
+#endif
+	}
+
+#ifdef WITH_PROXY
+	/*
+	 *	Home servers.
+	 */
+	if (((flag->vp_integer & 0x80) != 0) &&
+	    ((flag->vp_integer & 0x03) != 0)) {
+		home_server_t *home;
+		VALUE_PAIR *server_ip, *server_port;
+		fr_ipaddr_t ipaddr;
+
+		server_port = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_PORT, VENDORPEC_FREERADIUS, TAG_ANY);
+		if (!server_port) {
+			stats_error(request, "No home server port supplied");
+			return;
+		}
+
+#ifndef NDEBUG
+		memset(&ipaddr, 0, sizeof(ipaddr));
+#endif
+
+		/*
+		 *	See if we need to look up the server by socket
+		 *	socket.
+		 */
+		server_ip = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_IP_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY);
+		if (server_ip) {
+			ipaddr.af = AF_INET;
+			ipaddr.prefix = 32;
+			ipaddr.ipaddr.ip4addr.s_addr = server_ip->vp_ipaddr;
+#ifdef AF_INET6
+		} else if ((server_ip = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_IPV6_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY)) != NULL) {
+			ipaddr.af = AF_INET6;
+			ipaddr.ipaddr.ip6addr = server_ip->vp_ipv6addr;
+#endif	/* AF_INET6 */
+		} else {
+			stats_error(request, "No home server IP supplied");
+			return;
+		}
+
+		/*
+		 *	Not found: don't do anything
+		 */
+		home = home_server_find(&ipaddr, server_port->vp_integer, IPPROTO_UDP);
+#ifdef WITH_TCP
+		if (!home) home = home_server_find(&ipaddr, server_port->vp_integer, IPPROTO_TCP);
+#endif
+		if (!home) {
+			stats_error(request, "Failed to find home server IP");
+			return;
+		}
+
+		fr_pair_add(&request->reply->vps,
+			fr_pair_copy(request->reply, server_ip));
+		fr_pair_add(&request->reply->vps,
+			fr_pair_copy(request->reply, server_port));
+
+		vp = radius_pair_create(request->reply, &request->reply->vps,
+				       PW_FREERADIUS_STATS_SERVER_OUTSTANDING_REQUESTS, VENDORPEC_FREERADIUS);
+		if (vp) vp->vp_integer = home->currently_outstanding;
+
+		vp = radius_pair_create(request->reply, &request->reply->vps,
+				       PW_FREERADIUS_STATS_SERVER_STATE, VENDORPEC_FREERADIUS);
+		if (vp) vp->vp_integer = home->state;
+
+		if ((home->state == HOME_STATE_ALIVE) &&
+		    (home->revive_time.tv_sec != 0)) {
+			vp = radius_pair_create(request->reply, &request->reply->vps,
+					       PW_FREERADIUS_STATS_SERVER_TIME_OF_LIFE, VENDORPEC_FREERADIUS);
+			if (vp) vp->vp_date = home->revive_time.tv_sec;
+		}
+
+		if ((home->state == HOME_STATE_ALIVE) &&
+		    (home->ema.window > 0)) {
+				vp = radius_pair_create(request->reply,
+						       &request->reply->vps,
+						       PW_FREERADIUS_SERVER_EMA_WINDOW, VENDORPEC_FREERADIUS);
+				if (vp) vp->vp_integer = home->ema.window;
+				vp = radius_pair_create(request->reply,
+						       &request->reply->vps,
+						       PW_FREERADIUS_SERVER_EMA_USEC_WINDOW_1, VENDORPEC_FREERADIUS);
+				if (vp) vp->vp_integer = home->ema.ema1 / EMA_SCALE;
+				vp = radius_pair_create(request->reply,
+						       &request->reply->vps,
+						       PW_FREERADIUS_SERVER_EMA_USEC_WINDOW_10, VENDORPEC_FREERADIUS);
+				if (vp) vp->vp_integer = home->ema.ema10 / EMA_SCALE;
+
+		}
+
+		if (home->state == HOME_STATE_IS_DEAD) {
+			vp = radius_pair_create(request->reply, &request->reply->vps,
+					       PW_FREERADIUS_STATS_SERVER_TIME_OF_DEATH, VENDORPEC_FREERADIUS);
+			if (vp) vp->vp_date = home->zombie_period_start.tv_sec + home->zombie_period;
+		}
+
+		/*
+		 *	Show more information...
+		 *
+		 *	FIXME: do this for clients, too!
+		 */
+		vp = radius_pair_create(request->reply, &request->reply->vps,
+				       PW_FREERADIUS_STATS_LAST_PACKET_RECV, VENDORPEC_FREERADIUS);
+		if (vp) vp->vp_date = home->last_packet_recv;
+
+		vp = radius_pair_create(request->reply, &request->reply->vps,
+				       PW_FREERADIUS_STATS_LAST_PACKET_SENT, VENDORPEC_FREERADIUS);
+		if (vp) vp->vp_date = home->last_packet_sent;
+
+		if ((flag->vp_integer & 0x01) != 0) {
+			if (home->type == HOME_TYPE_AUTH) {
+				request_stats_addvp(request, proxy_authvp,
+						    &home->stats);
+			} else {
+				stats_error(request, "Home server is not auth");
+			}
+		}
+
+#ifdef WITH_ACCOUNTING
+		if ((flag->vp_integer & 0x02) != 0) {
+			if (home->type == HOME_TYPE_ACCT) {
+				request_stats_addvp(request, proxy_acctvp,
+						    &home->stats);
+			} else {
+				stats_error(request, "Home server is not acct");
+			}
+		}
+#endif
+	}
+#endif	/* WITH_PROXY */
+}
+
+void radius_stats_init(int flag)
+{
+	if (!flag) {
+		gettimeofday(&start_time, NULL);
+		hup_time = start_time; /* it's just nicer this way */
+	} else {
+		gettimeofday(&hup_time, NULL);
+	}
+}
+
+void radius_stats_ema(fr_stats_ema_t *ema,
+		      struct timeval *start, struct timeval *end)
+{
+	int micro;
+	time_t tdiff;
+#ifdef WITH_STATS_DEBUG
+	static int n = 0;
+#endif
+	if (ema->window == 0) return;
+
+	rad_assert(start->tv_sec <= end->tv_sec);
+
+	/*
+	 *	Initialize it.
+	 */
+	if (ema->f1 == 0) {
+		if (ema->window > 10000) ema->window = 10000;
+
+		ema->f1 =  (2 * F_EMA_SCALE) / (ema->window + 1);
+		ema->f10 = (2 * F_EMA_SCALE) / ((10 * ema->window) + 1);
+	}
+
+
+	tdiff = start->tv_sec;
+	tdiff -= end->tv_sec;
+
+	micro = (int) tdiff;
+	if (micro > 40) micro = 40; /* don't overflow 32-bit ints */
+	micro *= USEC;
+	micro += start->tv_usec;
+	micro -= end->tv_usec;
+
+	micro *= EMA_SCALE;
+
+	if (ema->ema1 == 0) {
+		ema->ema1 = micro;
+		ema->ema10 = micro;
+	} else {
+		int diff;
+
+		diff = ema->f1 * (micro - ema->ema1);
+		ema->ema1 += (diff / 1000000);
+
+		diff = ema->f10 * (micro - ema->ema10);
+		ema->ema10 += (diff / 1000000);
+	}
+
+
+#ifdef WITH_STATS_DEBUG
+	DEBUG("time %d %d.%06d\t%d.%06d\t%d.%06d\n",
+	      n, micro / PREC, (micro / EMA_SCALE) % USEC,
+	      ema->ema1 / PREC, (ema->ema1 / EMA_SCALE) % USEC,
+	      ema->ema10 / PREC, (ema->ema10 / EMA_SCALE) % USEC);
+	n++;
+#endif
+}
+
+#endif /* WITH_STATS */
diff --git a/src/main/threads.c b/src/main/threads.c
new file mode 100644
index 0000000..a187106
--- /dev/null
+++ b/src/main/threads.c
@@ -0,0 +1,1697 @@
+/*
+ * threads.c	request threading support
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2000,2006  The FreeRADIUS server project
+ * Copyright 2000  Alan DeKok <aland@ox.org>
+ */
+
+RCSID("$Id$")
+USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/process.h>
+
+#ifdef HAVE_STDATOMIC_H
+#include <freeradius-devel/atomic_queue.h>
+#endif
+
+#include <freeradius-devel/rad_assert.h>
+
+/*
+ *	Other OS's have sem_init, OS X doesn't.
+ */
+#ifdef HAVE_SEMAPHORE_H
+#include <semaphore.h>
+#endif
+
+#ifdef __APPLE__
+#ifdef WITH_GCD
+#include <dispatch/dispatch.h>
+#endif
+#include <mach/task.h>
+#include <mach/mach_init.h>
+#include <mach/semaphore.h>
+
+#ifndef WITH_GCD
+#undef sem_t
+#define sem_t semaphore_t
+#undef sem_init
+#define sem_init(s,p,c) semaphore_create(mach_task_self(),s,SYNC_POLICY_FIFO,c)
+#undef sem_wait
+#define sem_wait(s) semaphore_wait(*s)
+#undef sem_post
+#define sem_post(s) semaphore_signal(*s)
+#endif	/* WITH_GCD */
+#endif	/* __APPLE__ */
+
+#ifdef HAVE_SYS_WAIT_H
+#include <sys/wait.h>
+#endif
+
+#ifdef HAVE_PTHREAD_H
+
+#ifdef HAVE_OPENSSL_CRYPTO_H
+#include <openssl/crypto.h>
+#endif
+#ifdef HAVE_OPENSSL_ERR_H
+#include <openssl/err.h>
+#endif
+#ifdef HAVE_OPENSSL_EVP_H
+#include <openssl/evp.h>
+#endif
+
+#ifndef WITH_GCD
+#define SEMAPHORE_LOCKED	(0)
+
+#define THREAD_RUNNING		(1)
+#define THREAD_CANCELLED	(2)
+#define THREAD_EXITED		(3)
+
+#define NUM_FIFOS	       RAD_LISTEN_MAX
+
+#ifndef HAVE_STDALIGN_H
+#undef HAVE_STDATOMIC_H
+#endif
+
+#ifdef HAVE_STDATOMIC_H
+#define CAS_INCR(_x) do { uint32_t num; \
+			  num = load(_x); \
+			  if (cas_incr(_x, num)) break; \
+                     } while (true)
+
+#define CAS_DECR(_x) do { uint32_t num; \
+			  num = load(_x); \
+			  if (cas_decr(_x, num)) break; \
+                     } while (true)
+#endif
+
+/*
+ *  A data structure which contains the information about
+ *  the current thread.
+ */
+typedef struct THREAD_HANDLE {
+	struct THREAD_HANDLE	*prev;		//!< Previous thread handle (in the linked list).
+	struct THREAD_HANDLE	*next;		//!< Next thread handle (int the linked list).
+	pthread_t		pthread_id;	//!< pthread_id.
+	int			thread_num;	//!< Server thread number, 1...number of threads.
+	int			status;		//!< Is the thread running or exited?
+	unsigned int		request_count;	//!< The number of requests that this thread has handled.
+	time_t			timestamp;	//!< When the thread started executing.
+	REQUEST			*request;
+} THREAD_HANDLE;
+
+#endif	/* WITH_GCD */
+
+#ifdef WNOHANG
+typedef struct thread_fork_t {
+	pid_t		pid;
+	int		status;
+	int		exited;
+} thread_fork_t;
+#endif
+
+
+#ifdef WITH_STATS
+typedef struct fr_pps_t {
+	uint32_t	pps_old;
+	uint32_t	pps_now;
+	uint32_t	pps;
+	time_t		time_old;
+} fr_pps_t;
+#endif
+
+
+/*
+ *	A data structure to manage the thread pool.  There's no real
+ *	need for a data structure, but it makes things conceptually
+ *	easier.
+ */
+typedef struct THREAD_POOL {
+#ifndef WITH_GCD
+	THREAD_HANDLE	*head;
+	THREAD_HANDLE	*tail;
+
+	uint32_t	total_threads;
+
+	uint32_t	max_thread_num;
+	uint32_t	start_threads;
+	uint32_t	max_threads;
+	uint32_t	min_spare_threads;
+	uint32_t	max_spare_threads;
+	uint32_t	max_requests_per_thread;
+	uint32_t	request_count;
+	time_t		time_last_spawned;
+	uint32_t	cleanup_delay;
+	bool		stop_flag;
+#endif	/* WITH_GCD */
+	bool		spawn_flag;
+
+#ifdef WNOHANG
+	pthread_mutex_t	wait_mutex;
+	fr_hash_table_t *waiters;
+#endif
+
+#ifdef WITH_GCD
+	dispatch_queue_t	queue;
+#else
+
+#ifdef WITH_STATS
+	fr_pps_t	pps_in, pps_out;
+#ifdef WITH_ACCOUNTING
+	bool		auto_limit_acct;
+#endif
+#endif
+
+	/*
+	 *	All threads wait on this semaphore, for requests
+	 *	to enter the queue.
+	 */
+	sem_t		semaphore;
+
+	uint32_t	max_queue_size;
+
+#ifndef HAVE_STDATOMIC_H
+	/*
+	 *	To ensure only one thread at a time touches the queue.
+	 */
+	pthread_mutex_t	queue_mutex;
+
+	uint32_t	active_threads;	/* protected by queue_mutex */
+	uint32_t	exited_threads;
+	uint32_t	num_queued;
+	fr_fifo_t	*fifo[NUM_FIFOS];
+#else
+	atomic_uint32_t	  active_threads;
+	atomic_uint32_t	  exited_threads;
+	fr_atomic_queue_t *queue[NUM_FIFOS];
+#endif	/* STDATOMIC */
+#endif	/* WITH_GCD */
+} THREAD_POOL;
+
+static THREAD_POOL thread_pool;
+static bool pool_initialized = false;
+
+#ifndef WITH_GCD
+static time_t last_cleaned = 0;
+
+static void thread_pool_manage(time_t now);
+#endif
+
+#ifndef WITH_GCD
+/*
+ *	A mapping of configuration file names to internal integers
+ */
+static const CONF_PARSER thread_config[] = {
+	{ "start_servers", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.start_threads), "5" },
+	{ "max_servers", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.max_threads), "32" },
+	{ "min_spare_servers", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.min_spare_threads), "3" },
+	{ "max_spare_servers", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.max_spare_threads), "10" },
+	{ "max_requests_per_server", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.max_requests_per_thread), "0" },
+	{ "cleanup_delay", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.cleanup_delay), "5" },
+	{ "max_queue_size", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.max_queue_size), "65536" },
+#ifdef WITH_STATS
+#ifdef WITH_ACCOUNTING
+	{ "auto_limit_acct", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &thread_pool.auto_limit_acct), NULL },
+#endif
+#endif
+	CONF_PARSER_TERMINATOR
+};
+#endif
+
+#if defined(HAVE_OPENSSL_CRYPTO_H) && defined(HAVE_CRYPTO_SET_LOCKING_CALLBACK)
+
+/*
+ *	If we're linking against OpenSSL, then it is the
+ *	duty of the application, if it is multithreaded,
+ *	to provide OpenSSL with appropriate thread id
+ *	and mutex locking functions
+ *
+ *	Note: this only implements static callbacks.
+ *	OpenSSL does not use dynamic locking callbacks
+ *	right now, but may in the future, so we will have
+ *	to add them at some point.
+ */
+static pthread_mutex_t *ssl_mutexes = NULL;
+
+static void ssl_locking_function(int mode, int n, UNUSED char const *file, UNUSED int line)
+{
+	rad_assert(&ssl_mutexes[n] != NULL);
+
+	if (mode & CRYPTO_LOCK) {
+		pthread_mutex_lock(&ssl_mutexes[n]);
+	} else {
+		pthread_mutex_unlock(&ssl_mutexes[n]);
+	}
+}
+
+/*
+ *	Create the TLS mutexes.
+ */
+int tls_mutexes_init(void)
+{
+	int i, num;
+
+	rad_assert(ssl_mutexes == NULL);
+
+	num = CRYPTO_num_locks();
+
+	ssl_mutexes = rad_malloc(num * sizeof(pthread_mutex_t));
+	if (!ssl_mutexes) {
+		ERROR("Error allocating memory for SSL mutexes!");
+		return -1;
+	}
+
+	for (i = 0; i < num; i++) {
+		pthread_mutex_init(&ssl_mutexes[i], NULL);
+	}
+
+	CRYPTO_set_locking_callback(ssl_locking_function);
+
+	return 0;
+}
+
+static void tls_mutexes_destroy(void)
+{
+#ifdef HAVE_CRYPTO_SET_LOCKING_CALLBACK
+	int i, num;
+
+	rad_assert(ssl_mutex != NULL);
+
+	num = CRYPTO_num_locks();
+
+	for (i = 0; i < num; i++) {
+		pthread_mutex_destroy(&ssl_mutexes[i]);
+	}
+	free(ssl_mutexes);
+
+	CRYPTO_set_locking_callback(NULL);
+#endif
+}
+#else
+#define tls_mutexes_destroy()
+#endif
+
+#ifdef WNOHANG
+/*
+ *	We don't want to catch SIGCHLD for a host of reasons.
+ *
+ *	- exec_wait means that someone, somewhere, somewhen, will
+ *	call waitpid(), and catch the child.
+ *
+ *	- SIGCHLD is delivered to a random thread, not the one that
+ *	forked.
+ *
+ *	- if another thread catches the child, we have to coordinate
+ *	with the thread doing the waiting.
+ *
+ *	- if we don't waitpid() for non-wait children, they'll be zombies,
+ *	and will hang around forever.
+ *
+ */
+static void reap_children(void)
+{
+	pid_t pid;
+	int status;
+	thread_fork_t mytf, *tf;
+
+
+	pthread_mutex_lock(&thread_pool.wait_mutex);
+
+	do {
+	retry:
+		pid = waitpid(0, &status, WNOHANG);
+		if (pid <= 0) break;
+
+		mytf.pid = pid;
+		tf = fr_hash_table_finddata(thread_pool.waiters, &mytf);
+		if (!tf) goto retry;
+
+		tf->status = status;
+		tf->exited = 1;
+	} while (fr_hash_table_num_elements(thread_pool.waiters) > 0);
+
+	pthread_mutex_unlock(&thread_pool.wait_mutex);
+}
+#else
+#define reap_children()
+#endif /* WNOHANG */
+
+#ifndef WITH_GCD
+/*
+ *	Add a request to the list of waiting requests.
+ *	This function gets called ONLY from the main handler thread...
+ *
+ *	This function should never fail.
+ */
+int request_enqueue(REQUEST *request)
+{
+	bool managed = false;
+
+	rad_assert(pool_initialized == true);
+
+	/*
+	 *	If we haven't checked the number of child threads
+	 *	in a while, OR if the thread pool appears to be full,
+	 *	go manage it.
+	 */
+	if (last_cleaned < request->timestamp) {
+		thread_pool_manage(request->timestamp);
+		managed = true;
+	}
+
+#ifdef HAVE_STDATOMIC_H
+	if (!managed) {
+		uint32_t num;
+
+		num = load(thread_pool.active_threads);
+		if (num == thread_pool.total_threads) {
+			thread_pool_manage(request->timestamp);
+			managed = true;
+		}
+
+		if (!managed) {
+			num = load(thread_pool.exited_threads);
+			if (num > 0) {
+				thread_pool_manage(request->timestamp);
+			}
+		}
+	}
+
+	/*
+	 *	Use atomic queues where possible.  They're substantially faster than mutexes.
+	 */
+	request->component = "<core>";
+	request->module = "<queue>";
+	request->child_state = REQUEST_QUEUED;
+
+	/*
+	 *	Push the request onto the appropriate fifo for that
+	 */
+	if (!fr_atomic_queue_push(thread_pool.queue[request->priority], request)) {
+		ERROR("!!! ERROR !!! Failed inserting request %d into the queue", request->number);
+		return 0;
+	}
+
+#else  /* no atomic queues */
+
+	if (!managed && 
+	    ((thread_pool.active_threads == thread_pool.total_threads) ||
+	     (thread_pool.exited_threads > 0))) {
+		thread_pool_manage(request->timestamp);
+	}
+
+	pthread_mutex_lock(&thread_pool.queue_mutex);
+
+#ifdef WITH_STATS
+#ifdef WITH_ACCOUNTING
+	if (thread_pool.auto_limit_acct) {
+		struct timeval now;
+
+		/*
+		 *	Throw away accounting requests if we're too
+		 *	busy.  The NAS should retransmit these, and no
+		 *	one should notice.
+		 *
+		 *	In contrast, we always try to process
+		 *	authentication requests.  Those are more time
+		 *	critical, and it's harder to determine which
+		 *	we can throw away, and which we can keep.
+		 *
+		 *	We allow the queue to get half full before we
+		 *	start worrying.  Even then, we still require
+		 *	that the rate of input packets is higher than
+		 *	the rate of outgoing packets.  i.e. the queue
+		 *	is growing.
+		 *
+		 *	Once that happens, we roll a dice to see where
+		 *	the barrier is for "keep" versus "toss".  If
+		 *	the queue is smaller than the barrier, we
+		 *	allow it.  If the queue is larger than the
+		 *	barrier, we throw the packet away.  Otherwise,
+		 *	we keep it.
+		 *
+		 *	i.e. the probability of throwing the packet
+		 *	away increases from 0 (queue is half full), to
+		 *	100 percent (queue is completely full).
+		 *
+		 *	A probabilistic approach allows us to process
+		 *	SOME of the new accounting packets.
+		 */
+		if ((request->packet->code == PW_CODE_ACCOUNTING_REQUEST) &&
+		    (thread_pool.num_queued > (thread_pool.max_queue_size / 2)) &&
+		    (thread_pool.pps_in.pps_now > thread_pool.pps_out.pps_now)) {
+			uint32_t prob;
+			uint32_t keep;
+
+			/*
+			 *	Take a random value of how full we
+			 *	want the queue to be.  It's OK to be
+			 *	half full, but we get excited over
+			 *	anything more than that.
+			 */
+			keep = (thread_pool.max_queue_size / 2);
+			prob = fr_rand() & ((1 << 10) - 1);
+			keep *= prob;
+			keep >>= 10;
+			keep += (thread_pool.max_queue_size / 2);
+
+			/*
+			 *	If the queue is larger than our dice
+			 *	roll, we throw the packet away.
+			 */
+			if (thread_pool.num_queued > keep) {
+				pthread_mutex_unlock(&thread_pool.queue_mutex);
+				return 0;
+			}
+		}
+
+		gettimeofday(&now, NULL);
+
+		/*
+		 *	Calculate the instantaneous arrival rate into
+		 *	the queue.
+		 */
+		thread_pool.pps_in.pps = rad_pps(&thread_pool.pps_in.pps_old,
+						 &thread_pool.pps_in.pps_now,
+						 &thread_pool.pps_in.time_old,
+						 &now);
+
+		thread_pool.pps_in.pps_now++;
+	}
+#endif	/* WITH_ACCOUNTING */
+#endif
+
+	thread_pool.request_count++;
+
+	if (thread_pool.num_queued >= thread_pool.max_queue_size) {
+		pthread_mutex_unlock(&thread_pool.queue_mutex);
+
+		/*
+		 *	Mark the request as done.
+		 */
+		RATE_LIMIT(ERROR("Something is blocking the server.  There are %d packets in the queue, "
+				 "waiting to be processed.  Ignoring the new request.", thread_pool.num_queued));
+		return 0;
+	}
+
+	request->component = "<core>";
+	request->module = "<queue>";
+	request->child_state = REQUEST_QUEUED;
+
+	/*
+	 *	Push the request onto the appropriate fifo for that
+	 */
+	if (!fr_fifo_push(thread_pool.fifo[request->priority], request)) {
+		pthread_mutex_unlock(&thread_pool.queue_mutex);
+		ERROR("!!! ERROR !!! Failed inserting request %d into the queue", request->number);
+		return 0;
+	}
+
+	thread_pool.num_queued++;
+
+	pthread_mutex_unlock(&thread_pool.queue_mutex);
+#endif
+
+	/*
+	 *	There's one more request in the queue.
+	 *
+	 *	Note that we're not touching the queue any more, so
+	 *	the semaphore post is outside of the mutex.  This also
+	 *	means that when the thread wakes up and tries to lock
+	 *	the mutex, it will be unlocked, and there won't be
+	 *	contention.
+	 */
+	sem_post(&thread_pool.semaphore);
+
+	return 1;
+}
+
+/*
+ *	Remove a request from the queue.
+ */
+static int request_dequeue(REQUEST **prequest)
+{
+	time_t blocked;
+	static time_t last_complained = 0;
+	static time_t total_blocked = 0;
+	int num_blocked = 0;
+#ifndef HAVE_STDATOMIC_H
+	RAD_LISTEN_TYPE start;
+#endif
+	RAD_LISTEN_TYPE i;
+	REQUEST *request = NULL;
+	reap_children();
+
+	rad_assert(pool_initialized == true);
+
+#ifdef HAVE_STDATOMIC_H
+retry:
+	for (i = 0; i < NUM_FIFOS; i++) {
+		if (!fr_atomic_queue_pop(thread_pool.queue[i], (void **) &request)) continue;
+
+		rad_assert(request != NULL);
+
+		VERIFY_REQUEST(request);
+
+		if (request->master_state != REQUEST_STOP_PROCESSING) {
+			break;
+		}
+
+		/*
+		 *	This entry was marked to be stopped.  Acknowledge it.
+		 */
+		request->child_state = REQUEST_DONE;
+	}
+
+	/*
+	 *	Popping might fail.  If so, return.
+	 */
+	if (!request) return 0;
+
+#else
+	pthread_mutex_lock(&thread_pool.queue_mutex);
+
+#ifdef WITH_STATS
+#ifdef WITH_ACCOUNTING
+	if (thread_pool.auto_limit_acct) {
+		struct timeval now;
+
+		gettimeofday(&now, NULL);
+
+		/*
+		 *	Calculate the instantaneous departure rate
+		 *	from the queue.
+		 */
+		thread_pool.pps_out.pps  = rad_pps(&thread_pool.pps_out.pps_old,
+						   &thread_pool.pps_out.pps_now,
+						   &thread_pool.pps_out.time_old,
+						   &now);
+		thread_pool.pps_out.pps_now++;
+	}
+#endif
+#endif
+
+	/*
+	 *	Clear old requests from all queues.
+	 *
+	 *	We only do one pass over the queue, in order to
+	 *	amortize the work across the child threads.  Since we
+	 *	do N checks for one request de-queued, the old
+	 *	requests will be quickly cleared.
+	 */
+	for (i = 0; i < NUM_FIFOS; i++) {
+		request = fr_fifo_peek(thread_pool.fifo[i]);
+		if (!request) continue;
+
+		VERIFY_REQUEST(request);
+
+		if (request->master_state != REQUEST_STOP_PROCESSING) {
+			continue;
+		}
+
+		/*
+		 *	This entry was marked to be stopped.  Acknowledge it.
+		 */
+		request = fr_fifo_pop(thread_pool.fifo[i]);
+		rad_assert(request != NULL);
+		VERIFY_REQUEST(request);
+		request->child_state = REQUEST_DONE;
+		thread_pool.num_queued--;
+	}
+
+	start = 0;
+ retry:
+	/*
+	 *	Pop results from the top of the queue
+	 */
+	for (i = start; i < NUM_FIFOS; i++) {
+		request = fr_fifo_pop(thread_pool.fifo[i]);
+		if (request) {
+			VERIFY_REQUEST(request);
+			start = i;
+			break;
+		}
+	}
+
+	if (!request) {
+		pthread_mutex_unlock(&thread_pool.queue_mutex);
+		*prequest = NULL;
+		return 0;
+	}
+
+	rad_assert(thread_pool.num_queued > 0);
+	thread_pool.num_queued--;
+#endif	/* HAVE_STD_ATOMIC_H */
+
+	*prequest = request;
+
+	rad_assert(*prequest != NULL);
+	rad_assert(request->magic == REQUEST_MAGIC);
+
+	request->component = "<core>";
+	request->module = "";
+	request->child_state = REQUEST_RUNNING;
+
+	/*
+	 *	If the request has sat in the queue for too long,
+	 *	kill it.
+	 *
+	 *	The main clean-up code can't delete the request from
+	 *	the queue, and therefore won't clean it up until we
+	 *	have acknowledged it as "done".
+	 */
+	if (request->master_state == REQUEST_STOP_PROCESSING) {
+		request->module = "<done>";
+		request->child_state = REQUEST_DONE;
+		goto retry;
+	}
+
+	/*
+	 *	The thread is currently processing a request.
+	 */
+#ifdef HAVE_STDATOMIC_H
+	CAS_INCR(thread_pool.active_threads);
+#else
+	thread_pool.active_threads++;
+#endif
+
+	blocked = time(NULL);
+	if (!request->proxy && (blocked - request->timestamp) > 5) {
+		total_blocked++;
+		if (last_complained < blocked) {
+			last_complained = blocked;
+			blocked -= request->timestamp;
+			num_blocked = total_blocked;
+		} else {
+			blocked = 0;
+		}
+	} else {
+		total_blocked = 0;
+		blocked = 0;
+	}
+
+#ifndef HAVE_STDATOMIC_H
+	pthread_mutex_unlock(&thread_pool.queue_mutex);
+#endif
+
+	if (blocked) {
+		ERROR("%d requests have been waiting in the processing queue for %d seconds.  Check that all databases are running properly!",
+		      num_blocked, (int) blocked);
+	}
+
+	return 1;
+}
+
+
+/*
+ *	The main thread handler for requests.
+ *
+ *	Wait on the semaphore until we have it, and process the request.
+ */
+static void *request_handler_thread(void *arg)
+{
+	THREAD_HANDLE *self = (THREAD_HANDLE *) arg;
+
+	/*
+	 *	Loop forever, until told to exit.
+	 */
+	do {
+		/*
+		 *	Wait to be signalled.
+		 */
+		DEBUG2("Thread %d waiting to be assigned a request",
+		       self->thread_num);
+	re_wait:
+		if (sem_wait(&thread_pool.semaphore) != 0) {
+			/*
+			 *	Interrupted system call.  Go back to
+			 *	waiting, but DON'T print out any more
+			 *	text.
+			 */
+			if ((errno == EINTR) || (errno == EAGAIN)) {
+				DEBUG2("Re-wait %d", self->thread_num);
+				goto re_wait;
+			}
+			ERROR("Thread %d failed waiting for semaphore: %s: Exiting\n",
+			       self->thread_num, fr_syserror(errno));
+			break;
+		}
+
+		DEBUG2("Thread %d got semaphore", self->thread_num);
+
+#ifdef HAVE_OPENSSL_ERR_H
+		/*
+		 *	Clear the error queue for the current thread.
+		 */
+		ERR_clear_error();
+#endif
+
+		/*
+		 *	The server is exiting.  Don't dequeue any
+		 *	requests.
+		 */
+		if (thread_pool.stop_flag) break;
+
+		/*
+		 *	Try to grab a request from the queue.
+		 *
+		 *	It may be empty, in which case we fail
+		 *	gracefully.
+		 */
+		if (!request_dequeue(&self->request)) continue;
+
+		self->request->child_pid = self->pthread_id;
+		self->request_count++;
+
+		DEBUG2("Thread %d handling request %d, (%d handled so far)",
+		       self->thread_num, self->request->number,
+		       self->request_count);
+
+#ifndef HAVE_STDATOMIC_H
+#ifdef WITH_ACCOUNTING
+		if ((self->request->packet->code == PW_CODE_ACCOUNTING_REQUEST) &&
+		    thread_pool.auto_limit_acct) {
+			VALUE_PAIR *vp;
+			REQUEST *request = self->request;
+
+			vp = radius_pair_create(request, &request->config,
+					       181, VENDORPEC_FREERADIUS);
+			if (vp) vp->vp_integer = thread_pool.pps_in.pps;
+
+			vp = radius_pair_create(request, &request->config,
+					       182, VENDORPEC_FREERADIUS);
+			if (vp) vp->vp_integer = thread_pool.pps_in.pps;
+
+			vp = radius_pair_create(request, &request->config,
+					       183, VENDORPEC_FREERADIUS);
+			if (vp) {
+				vp->vp_integer = thread_pool.max_queue_size - thread_pool.num_queued;
+				vp->vp_integer *= 100;
+				vp->vp_integer /= thread_pool.max_queue_size;
+			}
+		}
+#endif
+#endif
+
+		self->request->process(self->request, FR_ACTION_RUN);
+		self->request = NULL;
+
+#ifdef HAVE_STDATOMIC_H
+		CAS_DECR(thread_pool.active_threads);
+#else
+		/*
+		 *	Update the active threads.
+		 */
+		pthread_mutex_lock(&thread_pool.queue_mutex);
+		rad_assert(thread_pool.active_threads > 0);
+		thread_pool.active_threads--;
+		pthread_mutex_unlock(&thread_pool.queue_mutex);
+#endif
+
+		/*
+		 *	If the thread has handled too many requests, then make it
+		 *	exit.
+		 */
+		if ((thread_pool.max_requests_per_thread > 0) &&
+		    (self->request_count >= thread_pool.max_requests_per_thread)) {
+			DEBUG2("Thread %d handled too many requests",
+			       self->thread_num);
+			break;
+		}
+	} while (self->status != THREAD_CANCELLED);
+
+	DEBUG2("Thread %d exiting...", self->thread_num);
+
+#ifdef HAVE_OPENSSL_ERR_H
+	/*
+	 *	If we linked with OpenSSL, the application
+	 *	must remove the thread's error queue before
+	 *	exiting to prevent memory leaks.
+	 */
+#if OPENSSL_VERSION_NUMBER < 0x10000000L
+	ERR_remove_state(0);
+#elif OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
+	ERR_remove_thread_state(NULL);
+#endif
+#endif
+
+#ifdef HAVE_STDATOMIC_H
+	CAS_INCR(thread_pool.exited_threads);
+#else
+	pthread_mutex_lock(&thread_pool.queue_mutex);
+	thread_pool.exited_threads++;
+	pthread_mutex_unlock(&thread_pool.queue_mutex);
+#endif
+
+	/*
+	 *  Do this as the LAST thing before exiting.
+	 */
+	self->request = NULL;
+	self->status = THREAD_EXITED;
+	exec_trigger(NULL, NULL, "server.thread.stop", true);
+
+	return NULL;
+}
+
+/*
+ *	Take a THREAD_HANDLE, delete it from the thread pool and
+ *	free its resources.
+ *
+ *	This function is called ONLY from the main server thread,
+ *	ONLY after the thread has exited.
+ */
+static void delete_thread(THREAD_HANDLE *handle)
+{
+	THREAD_HANDLE *prev;
+	THREAD_HANDLE *next;
+
+	rad_assert(handle->request == NULL);
+
+	DEBUG2("Deleting thread %d", handle->thread_num);
+
+	prev = handle->prev;
+	next = handle->next;
+	rad_assert(thread_pool.total_threads > 0);
+	thread_pool.total_threads--;
+
+	/*
+	 *	Remove the handle from the list.
+	 */
+	if (prev == NULL) {
+		rad_assert(thread_pool.head == handle);
+		thread_pool.head = next;
+	} else {
+		prev->next = next;
+	}
+
+	if (next == NULL) {
+		rad_assert(thread_pool.tail == handle);
+		thread_pool.tail = prev;
+	} else {
+		next->prev = prev;
+	}
+
+	/*
+	 *	Free the handle, now that it's no longer referencable.
+	 */
+	free(handle);
+}
+
+
+/*
+ *	Spawn a new thread, and place it in the thread pool.
+ *
+ *	The thread is started initially in the blocked state, waiting
+ *	for the semaphore.
+ */
+static THREAD_HANDLE *spawn_thread(time_t now, int do_trigger)
+{
+	int rcode;
+	THREAD_HANDLE *handle;
+
+	/*
+	 *	Ensure that we don't spawn too many threads.
+	 */
+	if (thread_pool.total_threads >= thread_pool.max_threads) {
+		DEBUG2("Thread spawn failed.  Maximum number of threads (%d) already running.", thread_pool.max_threads);
+		return NULL;
+	}
+
+	/*
+	 *	Allocate a new thread handle.
+	 */
+	handle = (THREAD_HANDLE *) rad_malloc(sizeof(THREAD_HANDLE));
+	memset(handle, 0, sizeof(THREAD_HANDLE));
+	handle->prev = NULL;
+	handle->next = NULL;
+	handle->thread_num = thread_pool.max_thread_num++;
+	handle->request_count = 0;
+	handle->status = THREAD_RUNNING;
+	handle->timestamp = time(NULL);
+
+	/*
+	 *	Create the thread joinable, so that it can be cleaned up
+	 *	using pthread_join().
+	 *
+	 *	Note that the function returns non-zero on error, NOT
+	 *	-1.  The return code is the error, and errno isn't set.
+	 */
+	rcode = pthread_create(&handle->pthread_id, 0, request_handler_thread, handle);
+	if (rcode != 0) {
+		free(handle);
+		ERROR("Thread create failed: %s",
+		       fr_syserror(rcode));
+		return NULL;
+	}
+
+	/*
+	 *	One more thread to go into the list.
+	 */
+	thread_pool.total_threads++;
+	DEBUG2("Thread spawned new child %d. Total threads in pool: %d",
+			handle->thread_num, thread_pool.total_threads);
+	if (do_trigger) exec_trigger(NULL, NULL, "server.thread.start", true);
+
+	/*
+	 *	Add the thread handle to the tail of the thread pool list.
+	 */
+	if (thread_pool.tail) {
+		thread_pool.tail->next = handle;
+		handle->prev = thread_pool.tail;
+		thread_pool.tail = handle;
+	} else {
+		rad_assert(thread_pool.head == NULL);
+		thread_pool.head = thread_pool.tail = handle;
+	}
+
+	/*
+	 *	Update the time we last spawned a thread.
+	 */
+	thread_pool.time_last_spawned = now;
+
+	/*
+	 * Fire trigger if maximum number of threads reached
+	 */
+	if (thread_pool.total_threads >= thread_pool.max_threads)
+		exec_trigger(NULL, NULL, "server.thread.max_threads", true);
+
+	/*
+	 *	And return the new handle to the caller.
+	 */
+	return handle;
+}
+#endif	/* WITH_GCD */
+
+
+#ifdef WNOHANG
+static uint32_t pid_hash(void const *data)
+{
+	thread_fork_t const *tf = data;
+
+	return fr_hash(&tf->pid, sizeof(tf->pid));
+}
+
+static int pid_cmp(void const *one, void const *two)
+{
+	thread_fork_t const *a = one;
+	thread_fork_t const *b = two;
+
+	return (a->pid - b->pid);
+}
+#endif
+
+/*
+ *	Allocate the thread pool, and seed it with an initial number
+ *	of threads.
+ *
+ *	FIXME: What to do on a SIGHUP???
+ */
+DIAG_OFF(deprecated-declarations)
+int thread_pool_init(CONF_SECTION *cs, bool *spawn_flag)
+{
+#ifndef WITH_GCD
+	uint32_t	i;
+	int		rcode;
+#endif
+	CONF_SECTION	*pool_cf;
+	time_t		now;
+#ifdef HAVE_STDATOMIC_H
+	int num;
+	TALLOC_CTX *autofree;
+
+	autofree = talloc_autofree_context();
+#endif
+
+	now = time(NULL);
+
+	rad_assert(spawn_flag != NULL);
+	rad_assert(*spawn_flag == true);
+	rad_assert(pool_initialized == false); /* not called on HUP */
+
+	pool_cf = cf_subsection_find_next(cs, NULL, "thread");
+#ifdef WITH_GCD
+	if (pool_cf) WARN("Built with Grand Central Dispatch.  Ignoring 'thread' subsection");
+#else
+	if (!pool_cf) *spawn_flag = false;
+#endif
+
+	/*
+	 *	Initialize the thread pool to some reasonable values.
+	 */
+	memset(&thread_pool, 0, sizeof(THREAD_POOL));
+#ifndef WITH_GCD
+	thread_pool.head = NULL;
+	thread_pool.tail = NULL;
+	thread_pool.total_threads = 0;
+	thread_pool.max_thread_num = 1;
+	thread_pool.cleanup_delay = 5;
+	thread_pool.stop_flag = false;
+#endif
+	thread_pool.spawn_flag = *spawn_flag;
+
+	/*
+	 *	Don't bother initializing the mutexes or
+	 *	creating the hash tables.  They won't be used.
+	 */
+	if (!*spawn_flag) return 0;
+
+#ifdef WNOHANG
+	if ((pthread_mutex_init(&thread_pool.wait_mutex,NULL) != 0)) {
+		ERROR("FATAL: Failed to initialize wait mutex: %s",
+		       fr_syserror(errno));
+		return -1;
+	}
+
+	/*
+	 *	Create the hash table of child PID's
+	 */
+	thread_pool.waiters = fr_hash_table_create(pid_hash,
+						   pid_cmp,
+						   free);
+	if (!thread_pool.waiters) {
+		ERROR("FATAL: Failed to set up wait hash");
+		return -1;
+	}
+#endif
+
+#ifndef WITH_GCD
+	if (cf_section_parse(pool_cf, NULL, thread_config) < 0) {
+		return -1;
+	}
+
+	/*
+	 *	Catch corner cases.
+	 */
+	if (thread_pool.min_spare_threads < 1)
+		thread_pool.min_spare_threads = 1;
+	if (thread_pool.max_spare_threads < 1)
+		thread_pool.max_spare_threads = 1;
+	if (thread_pool.max_spare_threads < thread_pool.min_spare_threads)
+		thread_pool.max_spare_threads = thread_pool.min_spare_threads;
+	if (thread_pool.max_threads == 0)
+		thread_pool.max_threads = 256;
+	if ((thread_pool.max_queue_size < 2) || (thread_pool.max_queue_size > 1024*1024)) {
+		ERROR("FATAL: max_queue_size value must be in range 2-1048576");
+		return -1;
+	}
+
+	if (thread_pool.start_threads > thread_pool.max_threads) {
+		ERROR("FATAL: start_servers (%i) must be <= max_servers (%i)",
+		      thread_pool.start_threads, thread_pool.max_threads);
+		return -1;
+	}
+#endif	/* WITH_GCD */
+
+	/*
+	 *	The pool has already been initialized.  Don't spawn
+	 *	new threads, and don't forget about forked children.
+	 */
+	if (pool_initialized) {
+		return 0;
+	}
+
+#ifndef WITH_GCD
+	/*
+	 *	Initialize the queue of requests.
+	 */
+	memset(&thread_pool.semaphore, 0, sizeof(thread_pool.semaphore));
+	rcode = sem_init(&thread_pool.semaphore, 0, SEMAPHORE_LOCKED);
+	if (rcode != 0) {
+		ERROR("FATAL: Failed to initialize semaphore: %s",
+		       fr_syserror(errno));
+		return -1;
+	}
+
+#ifndef HAVE_STDATOMIC_H
+	rcode = pthread_mutex_init(&thread_pool.queue_mutex,NULL);
+	if (rcode != 0) {
+		ERROR("FATAL: Failed to initialize queue mutex: %s",
+		       fr_syserror(errno));
+		return -1;
+	}
+#else
+	num = 0;
+	store(thread_pool.active_threads, num);
+	store(thread_pool.exited_threads, num);
+#endif
+
+	/*
+	 *	Allocate multiple fifos.
+	 */
+	for (i = 0; i < NUM_FIFOS; i++) {
+#ifdef HAVE_STDATOMIC_H
+		thread_pool.queue[i] = fr_atomic_queue_alloc(autofree, thread_pool.max_queue_size);
+		if (!thread_pool.queue[i]) {
+			ERROR("FATAL: Failed to set up request fifo");
+			return -1;
+		}
+#else
+		thread_pool.fifo[i] = fr_fifo_create(NULL, thread_pool.max_queue_size, NULL);
+		if (!thread_pool.fifo[i]) {
+			ERROR("FATAL: Failed to set up request fifo");
+			return -1;
+		}
+#endif
+	}
+#endif
+
+#ifndef WITH_GCD
+	/*
+	 *	Create a number of waiting threads.
+	 *
+	 *	If we fail while creating them, do something intelligent.
+	 */
+	for (i = 0; i < thread_pool.start_threads; i++) {
+		if (spawn_thread(now, 0) == NULL) {
+			return -1;
+		}
+	}
+#else
+	thread_pool.queue = dispatch_queue_create("org.freeradius.threads", NULL);
+	if (!thread_pool.queue) {
+		ERROR("Failed creating dispatch queue: %s", fr_syserror(errno));
+		fr_exit(1);
+	}
+#endif
+
+	DEBUG2("Thread pool initialized");
+	pool_initialized = true;
+	return 0;
+}
+DIAG_ON(deprecated-declarations)
+
+/*
+ *	Stop all threads in the pool.
+ */
+void thread_pool_stop(void)
+{
+#ifndef WITH_GCD
+	int i;
+	int total_threads;
+	THREAD_HANDLE *handle;
+	THREAD_HANDLE *next;
+
+	if (!pool_initialized) return;
+
+	/*
+	 *	Set pool stop flag.
+	 */
+	thread_pool.stop_flag = true;
+
+	/*
+	 *	Wakeup all threads to make them see stop flag.
+	 */
+	total_threads = thread_pool.total_threads;
+	for (i = 0; i != total_threads; i++) {
+		sem_post(&thread_pool.semaphore);
+	}
+
+	/*
+	 *	Join and free all threads.
+	 */
+	for (handle = thread_pool.head; handle; handle = next) {
+		next = handle->next;
+		pthread_join(handle->pthread_id, NULL);
+		delete_thread(handle);
+	}
+
+	for (i = 0; i < NUM_FIFOS; i++) {
+#ifdef HAVE_STDATOMIC_H
+		fr_atomic_queue_free(&thread_pool.queue[i]);
+#else
+		fr_fifo_free(thread_pool.fifo[i]);
+#endif
+	}
+
+#ifdef WNOHANG
+	fr_hash_table_free(thread_pool.waiters);
+#endif
+
+	/*
+	 *	We're no longer threaded.  Remove the mutexes and free
+	 *	the memory.
+	 */
+	tls_mutexes_destroy();
+#endif
+}
+
+
+#ifdef WITH_GCD
+int request_enqueue(REQUEST *request)
+{
+	dispatch_block_t block;
+
+	block = ^{
+		request->process(request, FR_ACTION_RUN);
+	};
+
+	dispatch_async(thread_pool.queue, block);
+
+	return 1;
+}
+#endif
+
+#ifndef WITH_GCD
+/*
+ *	Check the min_spare_threads and max_spare_threads.
+ *
+ *	If there are too many or too few threads waiting, then we
+ *	either create some more, or delete some.
+ */
+static void thread_pool_manage(time_t now)
+{
+	uint32_t spare;
+	int i, total;
+	THREAD_HANDLE *handle, *next;
+	uint32_t active_threads;
+
+	/*
+	 *	Loop over the thread pool, deleting exited threads.
+	 */
+	for (handle = thread_pool.head; handle; handle = next) {
+		next = handle->next;
+
+		/*
+		 *	Maybe we've asked the thread to exit, and it
+		 *	has agreed.
+		 */
+		if (handle->status == THREAD_EXITED) {
+			pthread_join(handle->pthread_id, NULL);
+			delete_thread(handle);
+
+#ifdef HAVE_STDATOMIC_H
+			CAS_DECR(thread_pool.exited_threads);
+#else
+			pthread_mutex_lock(&thread_pool.queue_mutex);
+			thread_pool.exited_threads--;
+			pthread_mutex_unlock(&thread_pool.queue_mutex);
+#endif
+		}
+	}
+
+	/*
+	 *	We don't need a mutex lock here, as we're reading
+	 *	active_threads, and not modifying it.  We want a close
+	 *	approximation of the number of active threads, and this
+	 *	is good enough.
+	 */
+#ifdef HAVE_STDATOMIC_H
+	active_threads = load(thread_pool.active_threads);
+#else
+	active_threads = thread_pool.active_threads;
+#endif
+	spare = thread_pool.total_threads - active_threads;
+	if (rad_debug_lvl) {
+		static uint32_t old_total = 0;
+		static uint32_t old_active = 0;
+
+		if ((old_total != thread_pool.total_threads) || (old_active != active_threads)) {
+			DEBUG2("Threads: total/active/spare threads = %d/%d/%d",
+			       thread_pool.total_threads, active_threads, spare);
+			old_total = thread_pool.total_threads;
+			old_active = active_threads;
+		}
+	}
+
+	/*
+	 *	If there are too few spare threads.  Go create some more.
+	 */
+	if ((thread_pool.total_threads < thread_pool.max_threads) &&
+	    (spare < thread_pool.min_spare_threads)) {
+		total = thread_pool.min_spare_threads - spare;
+
+		if ((total + thread_pool.total_threads) > thread_pool.max_threads) {
+			total = thread_pool.max_threads - thread_pool.total_threads;
+		}
+
+		DEBUG2("Threads: Spawning %d spares", total);
+
+		/*
+		 *	Create a number of spare threads.
+		 */
+		for (i = 0; i < total; i++) {
+			handle = spawn_thread(now, 1);
+			if (handle == NULL) {
+				return;
+			}
+		}
+
+		return;		/* there aren't too many spare threads */
+	}
+
+	/*
+	 *	Only delete spare threads if we haven't already done
+	 *	so this second.
+	 */
+	if (now == last_cleaned) {
+		return;
+	}
+	last_cleaned = now;
+
+	/*
+	 *	Only delete the spare threads if sufficient time has
+	 *	passed since we last created one.  This helps to minimize
+	 *	the amount of create/delete cycles.
+	 */
+	if ((now - thread_pool.time_last_spawned) < (int)thread_pool.cleanup_delay) {
+		return;
+	}
+
+	/*
+	 *	If there are too many spare threads, delete one.
+	 *
+	 *	Note that we only delete ONE at a time, instead of
+	 *	wiping out many.  This allows the excess servers to
+	 *	be slowly reaped, just in case the load spike comes again.
+	 */
+	if (spare > thread_pool.max_spare_threads) {
+
+		spare -= thread_pool.max_spare_threads;
+
+		DEBUG2("Threads: deleting 1 spare out of %d spares", spare);
+
+		/*
+		 *	Walk through the thread pool, deleting the
+		 *	first idle thread we come across.
+		 */
+		for (handle = thread_pool.head; (handle != NULL) && (spare > 0) ; handle = next) {
+			next = handle->next;
+
+			/*
+			 *	If the thread is not handling a
+			 *	request, but still live, then tell it
+			 *	to exit.
+			 *
+			 *	It will eventually wake up, and realize
+			 *	it's been told to commit suicide.
+			 */
+			if ((handle->request == NULL) &&
+			    (handle->status == THREAD_RUNNING)) {
+				handle->status = THREAD_CANCELLED;
+				/*
+				 *	Post an extra semaphore, as a
+				 *	signal to wake up, and exit.
+				 */
+				sem_post(&thread_pool.semaphore);
+				spare--;
+				break;
+			}
+		}
+	}
+
+	/*
+	 *	Otherwise everything's kosher.  There are not too few,
+	 *	or too many spare threads.  Exit happily.
+	 */
+	return;
+}
+#endif	/* WITH_GCD */
+
+#ifdef WNOHANG
+/*
+ *	Thread wrapper for fork().
+ */
+pid_t rad_fork(void)
+{
+	pid_t child_pid;
+
+	if (!pool_initialized) return fork();
+
+	reap_children();	/* be nice to non-wait thingies */
+
+	if (fr_hash_table_num_elements(thread_pool.waiters) >= 1024) {
+		return -1;
+	}
+
+	/*
+	 *	Fork & save the PID for later reaping.
+	 */
+	child_pid = fork();
+	if (child_pid > 0) {
+		int rcode;
+		thread_fork_t *tf;
+
+		tf = rad_malloc(sizeof(*tf));
+		memset(tf, 0, sizeof(*tf));
+
+		tf->pid = child_pid;
+
+		pthread_mutex_lock(&thread_pool.wait_mutex);
+		rcode = fr_hash_table_insert(thread_pool.waiters, tf);
+		pthread_mutex_unlock(&thread_pool.wait_mutex);
+
+		if (!rcode) {
+			ERROR("Failed to store PID, creating what will be a zombie process %d",
+			       (int) child_pid);
+			free(tf);
+		}
+	}
+
+	/*
+	 *	Return whatever we were told.
+	 */
+	return child_pid;
+}
+
+
+/*
+ *	Wait 10 seconds at most for a child to exit, then give up.
+ */
+pid_t rad_waitpid(pid_t pid, int *status)
+{
+	int i;
+	thread_fork_t mytf, *tf;
+
+	if (!pool_initialized) return waitpid(pid, status, 0);
+
+	if (pid <= 0) return -1;
+
+	mytf.pid = pid;
+
+	pthread_mutex_lock(&thread_pool.wait_mutex);
+	tf = fr_hash_table_finddata(thread_pool.waiters, &mytf);
+	pthread_mutex_unlock(&thread_pool.wait_mutex);
+
+	if (!tf) return -1;
+
+	for (i = 0; i < 100; i++) {
+		reap_children();
+
+		if (tf->exited) {
+			*status = tf->status;
+
+			pthread_mutex_lock(&thread_pool.wait_mutex);
+			fr_hash_table_delete(thread_pool.waiters, &mytf);
+			pthread_mutex_unlock(&thread_pool.wait_mutex);
+			return pid;
+		}
+		usleep(100000);	/* sleep for 1/10 of a second */
+	}
+
+	/*
+	 *	10 seconds have passed, give up on the child.
+	 */
+	pthread_mutex_lock(&thread_pool.wait_mutex);
+	fr_hash_table_delete(thread_pool.waiters, &mytf);
+	pthread_mutex_unlock(&thread_pool.wait_mutex);
+
+	return 0;
+}
+#else
+/*
+ *	No rad_fork or rad_waitpid
+ */
+#endif
+
+void thread_pool_queue_stats(int array[RAD_LISTEN_MAX], int pps[2])
+{
+	int i;
+
+#ifndef WITH_GCD
+	if (pool_initialized) {
+		struct timeval now;
+
+		for (i = 0; i < RAD_LISTEN_MAX; i++) {
+#ifndef HAVE_STDATOMIC_H
+			array[i] = fr_fifo_num_elements(thread_pool.fifo[i]);
+#else
+			array[i] = 0;
+#endif
+		}
+
+		gettimeofday(&now, NULL);
+
+		pps[0] = rad_pps(&thread_pool.pps_in.pps_old,
+				 &thread_pool.pps_in.pps_now,
+				 &thread_pool.pps_in.time_old,
+				 &now);
+		pps[1] = rad_pps(&thread_pool.pps_out.pps_old,
+				 &thread_pool.pps_out.pps_now,
+				 &thread_pool.pps_out.time_old,
+				 &now);
+
+	} else
+#endif	/* WITH_GCD */
+	{
+		for (i = 0; i < RAD_LISTEN_MAX; i++) {
+			array[i] = 0;
+		}
+
+		pps[0] = pps[1] = 0;
+	}
+}
+
+void thread_pool_thread_stats(int stats[3])
+{
+#ifndef WITH_GCD
+	if (pool_initialized) {
+		/*
+		 *	We don't need a mutex lock here as we only want to
+		 *	read a close approximation of the number of active
+		 *	threads, and not modify it.
+		 */
+#ifdef HAVE_STDATOMIC_H
+		stats[0] = load(thread_pool.active_threads);
+#else
+		stats[0] = thread_pool.active_threads;
+#endif
+		stats[1] = thread_pool.total_threads;
+		stats[2] = thread_pool.max_threads;
+	} else
+#endif	/* WITH_GCD */
+	{
+		stats[0] = stats[1] = stats[2] = 0;
+	}
+}
+#endif /* HAVE_PTHREAD_H */
+
+static void time_free(void *data)
+{
+	free(data);
+}
+
+void exec_trigger(REQUEST *request, CONF_SECTION *cs, char const *name, int quench)
+{
+	CONF_SECTION *subcs;
+	CONF_ITEM *ci;
+	CONF_PAIR *cp;
+	char const *attr;
+	char const *value;
+	VALUE_PAIR *vp;
+	bool alloc = false;
+
+	/*
+	 *	Use global "trigger" section if no local config is given.
+	 */
+	if (!cs) {
+		cs = main_config.config;
+		attr = name;
+	} else {
+		/*
+		 *	Try to use pair name, rather than reference.
+		 */
+		attr = strrchr(name, '.');
+		if (attr) {
+			attr++;
+		} else {
+			attr = name;
+		}
+	}
+
+	/*
+	 *	Find local "trigger" subsection.  If it isn't found,
+	 *	try using the global "trigger" section, and reset the
+	 *	reference to the full path, rather than the sub-path.
+	 */
+	subcs = cf_section_sub_find(cs, "trigger");
+	if (!subcs && (cs != main_config.config)) {
+		subcs = cf_section_sub_find(main_config.config, "trigger");
+		attr = name;
+	}
+
+	if (!subcs) return;
+
+	ci = cf_reference_item(subcs, main_config.config, attr);
+	if (!ci) {
+		ERROR("No such item in trigger section: %s", attr);
+		return;
+	}
+
+	if (!cf_item_is_pair(ci)) {
+		ERROR("Trigger is not a configuration variable: %s", attr);
+		return;
+	}
+
+	cp = cf_item_to_pair(ci);
+	if (!cp) return;
+
+	value = cf_pair_value(cp);
+	if (!value) {
+		ERROR("Trigger has no value: %s", name);
+		return;
+	}
+
+	/*
+	 *	May be called for Status-Server packets.
+	 */
+	vp = NULL;
+	if (request && request->packet) vp = request->packet->vps;
+
+	/*
+	 *	Perform periodic quenching.
+	 */
+	if (quench) {
+		time_t *last_time;
+
+		last_time = cf_data_find(cs, value);
+		if (!last_time) {
+			last_time = rad_malloc(sizeof(*last_time));
+			*last_time = 0;
+
+			if (cf_data_add(cs, value, last_time, time_free) < 0) {
+				free(last_time);
+				last_time = NULL;
+			}
+		}
+
+		/*
+		 *	Send the quenched traps at most once per second.
+		 */
+		if (last_time) {
+			time_t now = time(NULL);
+			if (*last_time == now) return;
+
+			*last_time = now;
+		}
+	}
+
+	/*
+	 *	radius_exec_program always needs a request.
+	 */
+	if (!request) {
+		request = request_alloc(NULL);
+		alloc = true;
+	}
+
+	DEBUG("Trigger %s -> %s", name, value);
+
+	radius_exec_program(request, NULL, 0, NULL, request, value, vp, false, true, 0);
+
+	if (alloc) talloc_free(request);
+}
diff --git a/src/main/tls.c b/src/main/tls.c
new file mode 100644
index 0000000..c8cae3b
--- /dev/null
+++ b/src/main/tls.c
@@ -0,0 +1,5420 @@
+/*
+ * tls.c
+ *
+ * Version:     $Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2001  hereUare Communications, Inc. <raghud@hereuare.com>
+ * Copyright 2003  Alan DeKok <aland@freeradius.org>
+ * Copyright 2006  The FreeRADIUS server project
+ */
+
+RCSID("$Id$")
+USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/process.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/rad_assert.h>
+
+#ifdef HAVE_SYS_STAT_H
+#include <sys/stat.h>
+#endif
+
+#ifdef HAVE_FCNTL_H
+#include <fcntl.h>
+#endif
+
+#ifdef HAVE_DIRENT_H
+#include <dirent.h>
+#endif
+
+#ifdef HAVE_UTIME_H
+#include <utime.h>
+#endif
+#include <ctype.h>
+
+#ifdef WITH_TLS
+#  ifdef HAVE_OPENSSL_RAND_H
+#    include <openssl/rand.h>
+#  endif
+
+#  ifdef HAVE_OPENSSL_OCSP_H
+#    include <openssl/ocsp.h>
+#  endif
+
+#  ifdef HAVE_OPENSSL_EVP_H
+#    include <openssl/evp.h>
+#  endif
+#  include <openssl/ssl.h>
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+#  include <openssl/provider.h>
+
+static OSSL_PROVIDER *openssl_default_provider = NULL;
+static OSSL_PROVIDER *openssl_legacy_provider = NULL;
+#endif
+
+#define LOG_PREFIX "tls"
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+#define ERR_get_error_line(_file, _line) ERR_get_error_all(_file, _line, NULL, NULL, NULL)
+
+#define FIPS_mode(_x) EVP_default_properties_is_fips_enabled(NULL)
+#define PEM_read_bio_DHparams(_bio, _x, _y, _z) PEM_read_bio_Parameters(_bio, &dh)
+#define SSL_CTX_set0_tmp_dh_pkey(_ctx, _dh) SSL_CTX_set_tmp_dh(_ctx, _dh)
+#define DH EVP_PKEY
+#define DH_free(_dh)
+#endif
+
+#ifdef ENABLE_OPENSSL_VERSION_CHECK
+typedef struct libssl_defect {
+	uint64_t	high;
+	uint64_t	low;
+
+	char const	*id;
+	char const	*name;
+	char const	*comment;
+} libssl_defect_t;
+
+/* Record critical defects in libssl here, new versions of OpenSSL to older versions of OpenSSL.  */
+static libssl_defect_t libssl_defects[] =
+{
+	{
+		.low		= 0x01010001f,		/* 1.1.0a */
+		.high		= 0x01010001f,		/* 1.1.0a */
+		.id		= "CVE-2016-6309",
+		.name		= "OCSP status request extension",
+		.comment	= "For more information see https://www.openssl.org/news/secadv/20160926.txt"
+	},
+	{
+		.low		= 0x01010000f,		/* 1.1.0  */
+		.high		= 0x01010000f,		/* 1.1.0  */
+		.id		= "CVE-2016-6304",
+		.name		= "OCSP status request extension",
+		.comment	= "For more information see https://www.openssl.org/news/secadv/20160922.txt"
+	},
+	{
+		.low		= 0x01000209f,		/* 1.0.2i */
+		.high		= 0x01000209f,		/* 1.0.2i */
+		.id		= "CVE-2016-7052",
+		.name		= "OCSP status request extension",
+		.comment	= "For more information see https://www.openssl.org/news/secadv/20160926.txt"
+	},
+	{
+		.low		= 0x01000200f,		/* 1.0.2  */
+		.high		= 0x01000208f,		/* 1.0.2h */
+		.id		= "CVE-2016-6304",
+		.name		= "OCSP status request extension",
+		.comment	= "For more information see https://www.openssl.org/news/secadv/20160922.txt"
+	},
+	{
+		.low		= 0x01000100f,		/* 1.0.1  */
+		.high		= 0x01000114f,		/* 1.0.1t */
+		.id		= "CVE-2016-6304",
+		.name		= "OCSP status request extension",
+		.comment	= "For more information see https://www.openssl.org/news/secadv/20160922.txt"
+	},
+	{
+		.low		= 0x010001000,		/* 1.0.1  */
+		.high		= 0x01000106f,		/* 1.0.1f */
+		.id		= "CVE-2014-0160",
+		.name		= "Heartbleed",
+		.comment	= "For more information see http://heartbleed.com"
+	},
+};
+#endif /* ENABLE_OPENSSL_VERSION_CHECK */
+
+FR_NAME_NUMBER const fr_tls_status_table[] = {
+	{ "invalid",			FR_TLS_INVALID },
+	{ "request",			FR_TLS_REQUEST },
+	{ "response",			FR_TLS_RESPONSE },
+	{ "success",			FR_TLS_SUCCESS },
+	{ "fail",			FR_TLS_FAIL },
+	{ "noop",			FR_TLS_NOOP },
+
+	{ "start",			FR_TLS_START },
+	{ "ok",				FR_TLS_OK },
+	{ "ack",			FR_TLS_ACK },
+	{ "first fragment",		FR_TLS_FIRST_FRAGMENT },
+	{ "more fragments",		FR_TLS_MORE_FRAGMENTS },
+	{ "length included",		FR_TLS_LENGTH_INCLUDED },
+	{ "more fragments with length",	FR_TLS_MORE_FRAGMENTS_WITH_LENGTH },
+	{ "handled",			FR_TLS_HANDLED },
+	{  NULL , 			-1},
+};
+
+/* index we use to store cached session VPs
+ * needs to be dynamic so we can supply a "free" function
+ */
+int fr_tls_ex_index_vps = -1;
+int fr_tls_ex_index_certs = -1;
+
+/* Session */
+static void 		session_close(tls_session_t *ssn);
+static void 		session_init(tls_session_t *ssn);
+
+/* record */
+static void 		record_init(record_t *buf);
+static void 		record_close(record_t *buf);
+static unsigned int 	record_plus(record_t *buf, void const *ptr,
+				    unsigned int size);
+static unsigned int 	record_minus(record_t *buf, void *ptr,
+				     unsigned int size);
+
+typedef struct {
+	char const	*name;
+	SSL_CTX		*ctx;
+} fr_realm_ctx_t;
+
+DIAG_OFF(format-nonliteral)
+/** Print errors in the TLS thread local error stack
+ *
+ * Drains the thread local OpenSSL error queue, and prints out errors.
+ *
+ * @param[in] request	The current request (may be NULL).
+ * @param[in] msg	Error message describing the operation being attempted.
+ * @param[in] ap	Arguments for msg.
+ * @return the number of errors drained from the stack.
+ */
+static int tls_verror_log(REQUEST *request, char const *msg, va_list ap)
+{
+	unsigned long	error;
+	char		*p;
+	int		in_stack = 0;
+	char		buffer[256];
+
+	int		line;
+	char const	*file;
+
+	/*
+	 *	Pop the first error, so ERR_peek_error()
+	 *	can be used to determine if there are
+	 *	multiple errors.
+	 */
+	error = ERR_get_error_line(&file, &line);
+
+	if (msg) {
+		p = talloc_vasprintf(request, msg, ap);
+
+		/*
+		 *	Single line mode (there's only one error)
+		 */
+		if (error && !ERR_peek_error()) {
+			ERR_error_string_n(error, buffer, sizeof(buffer));
+
+			/* Extra verbose */
+			if ((request && RDEBUG_ENABLED3) || DEBUG_ENABLED3) {
+				ROPTIONAL(REDEBUG, ERROR, "(TLS) %s: %s[%i]:%s", p, file, line, buffer);
+			} else {
+				ROPTIONAL(REDEBUG, ERROR, "(TLS) %s: %s", p, buffer);
+			}
+
+			talloc_free(p);
+
+			return 1;
+		}
+
+		/*
+		 *	Print the error we were given, irrespective
+		 *	of whether there were any OpenSSL errors.
+		 */
+		ROPTIONAL(RERROR, ERROR, "(TLS) %s", p);
+		talloc_free(p);
+	}
+
+	/*
+	 *	Stack mode (there are multiple errors)
+	 */
+	if (!error) return 0;
+	do {
+		ERR_error_string_n(error, buffer, sizeof(buffer));
+		/* Extra verbose */
+		if ((request && RDEBUG_ENABLED3) || DEBUG_ENABLED3) {
+			ROPTIONAL(REDEBUG, ERROR, "(TLS) %s[%i]:%s", file, line, buffer);
+		} else {
+			ROPTIONAL(REDEBUG, ERROR, "(TLS) %s", buffer);
+		}
+		in_stack++;
+	} while ((error = ERR_get_error_line(&file, &line)));
+
+	return in_stack;
+}
+DIAG_ON(format-nonliteral)
+
+/** Print errors in the TLS thread local error stack
+ *
+ * Drains the thread local OpenSSL error queue, and prints out errors.
+ *
+ * @param[in] request	The current request (may be NULL).
+ * @param[in] msg	Error message describing the operation being attempted.
+ * @param[in] ...	Arguments for msg.
+ * @return the number of errors drained from the stack.
+ */
+int tls_error_log(REQUEST *request, char const *msg, ...)
+{
+	va_list ap;
+	int ret;
+
+	va_start(ap, msg);
+	ret = tls_verror_log(request, msg, ap);
+	va_end(ap);
+
+	return ret;
+}
+
+/** Print errors raised by OpenSSL I/O functions
+ *
+ * Drains the thread local OpenSSL error queue, and prints out errors
+ * based on the SSL handle and the return code of the I/O  function.
+ *
+ * OpenSSL lists I/O functions to be:
+ *   - SSL_connect
+ *   - SSL_accept
+ *   - SSL_do_handshake
+ *   - SSL_read
+ *   - SSL_peek
+ *   - SSL_write
+ *
+ * @param request	The current request (may be NULL).
+ * @param session	The current tls_session.
+ * @param ret		from the I/O operation.
+ * @param msg		Error message describing the operation being attempted.
+ * @param ...		Arguments for msg.
+ * @return
+ *	- 0 TLS session cannot continue.
+ *	- 1 TLS session may still be viable.
+ */
+int tls_error_io_log(REQUEST *request, tls_session_t *session, int ret, char const *msg, ...)
+{
+	int	error;
+	va_list	ap;
+
+	if (ERR_peek_error()) {
+		va_start(ap, msg);
+		tls_verror_log(request, msg, ap);
+		va_end(ap);
+	}
+
+	error = SSL_get_error(session->ssl, ret);
+	switch (error) {
+	/*
+	 *	These seem to be harmless and already "dealt
+	 *	with" by our non-blocking environment. NB:
+	 *	"ZERO_RETURN" is the clean "error"
+	 *	indicating a successfully closed SSL
+	 *	tunnel. We let this happen because our IO
+	 *	loop should not appear to have broken on
+	 *	this condition - and outside the IO loop, the
+	 *	"shutdown" state is checked.
+	 *
+	 *	Don't print anything if we ignore the error.
+	 */
+	case SSL_ERROR_NONE:
+	case SSL_ERROR_WANT_READ:
+	case SSL_ERROR_WANT_WRITE:
+	case SSL_ERROR_WANT_X509_LOOKUP:
+	case SSL_ERROR_ZERO_RETURN:
+		break;
+
+	/*
+	 *	These seem to be indications of a genuine
+	 *	error that should result in the SSL tunnel
+	 *	being regarded as "dead".
+	 */
+	case SSL_ERROR_SYSCALL:
+		ROPTIONAL(REDEBUG, ERROR, "(TLS) System call (I/O) error (%i)", ret);
+		return 0;
+
+	case SSL_ERROR_SSL:
+		ROPTIONAL(REDEBUG, ERROR, "(TLS) Protocol error (%i)", ret);
+		return 0;
+
+	/*
+	 *	For any other errors that (a) exist, and (b)
+	 *	crop up - we need to interpret what to do with
+	 *	them - so "politely inform" the caller that
+	 *	the code needs updating here.
+	 */
+	default:
+		ROPTIONAL(REDEBUG, ERROR, "(TLS) Session error %i (%i)", error, ret);
+		return 0;
+	}
+
+	return 1;
+}
+
+#ifdef PSK_MAX_IDENTITY_LEN
+static bool identity_is_safe(const char *identity)
+{
+	char c;
+
+	if (!identity) return true;
+
+	while ((c = *(identity++)) != '\0') {
+		if (isalpha((uint8_t) c) || isdigit((uint8_t) c) || isspace((uint8_t) c) ||
+		    (c == '@') || (c == '-') || (c == '_') || (c == '.')) {
+			continue;
+		}
+
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ *	When a client uses TLS-PSK to talk to a server, this callback
+ *	is used by the server to determine the PSK to use.
+ */
+static unsigned int psk_server_callback(SSL *ssl, const char *identity,
+					unsigned char *psk,
+					unsigned int max_psk_len)
+{
+	unsigned int psk_len = 0;
+	fr_tls_server_conf_t *conf;
+	REQUEST *request;
+
+	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl,
+						       FR_TLS_EX_INDEX_CONF);
+	if (!conf) return 0;
+
+	request = (REQUEST *)SSL_get_ex_data(ssl,
+					     FR_TLS_EX_INDEX_REQUEST);
+	if (request && conf->psk_query) {
+		size_t hex_len;
+		VALUE_PAIR *vp, **certs;
+		TALLOC_CTX *talloc_ctx;
+		char buffer[2 * PSK_MAX_PSK_LEN + 4]; /* allow for too-long keys */
+
+		/*
+		 *	The passed identity is weird.  Deny it.
+		 */
+		if (!identity_is_safe(identity)) {
+			RWDEBUG("(TLS) Invalid characters in PSK identity %s", identity);
+			return 0;
+		}
+
+		vp = pair_make_request("TLS-PSK-Identity", identity, T_OP_SET);
+		if (!vp) return 0;
+
+		certs = (VALUE_PAIR **)SSL_get_ex_data(ssl, fr_tls_ex_index_certs);
+		talloc_ctx = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_TALLOC);
+		fr_assert(certs != NULL); /* pointer to sock->certs */
+		fr_assert(talloc_ctx != NULL); /* sock */
+
+		fr_pair_add(certs, fr_pair_copy(talloc_ctx, vp));
+
+		hex_len = radius_xlat(buffer, sizeof(buffer), request, conf->psk_query,
+				      NULL, NULL);
+		if (!hex_len) {
+			RWDEBUG("(TLS) PSK expansion returned an empty string.");
+			return 0;
+		}
+
+		/*
+		 *	The returned key is truncated at MORE than
+		 *	OpenSSL can handle.  That way we can detect
+		 *	the truncation, and complain about it.
+		 */
+		if (hex_len > (2 * max_psk_len)) {
+			RWDEBUG("(TLS) Returned PSK is too long (%u > %u)",
+				(unsigned int) hex_len, 2 * max_psk_len);
+			return 0;
+		}
+
+		/*
+		 *	Leave the TLS-PSK-Identity in the request, and
+		 *	convert the expansion from printable string
+		 *	back to hex.
+		 */
+		return fr_hex2bin(psk, max_psk_len, buffer, hex_len);
+	}
+
+	if (!conf->psk_identity) {
+		DEBUG("No static PSK identity set.  Rejecting the user");
+		return 0;
+	}
+
+	/*
+	 *	No REQUEST, or no dynamic query.  Just look for a
+	 *	static identity.
+	 */
+	if (strcmp(identity, conf->psk_identity) != 0) {
+		ERROR("(TKS) Supplied PSK identity %s does not match configuration.  Rejecting.",
+		      identity);
+		return 0;
+	}
+
+	psk_len = strlen(conf->psk_password);
+	if (psk_len > (2 * max_psk_len)) return 0;
+
+	return fr_hex2bin(psk, max_psk_len, conf->psk_password, psk_len);
+}
+
+static unsigned int psk_client_callback(SSL *ssl, UNUSED char const *hint,
+					char *identity, unsigned int max_identity_len,
+					unsigned char *psk, unsigned int max_psk_len)
+{
+	unsigned int psk_len;
+	fr_tls_server_conf_t *conf;
+
+	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl,
+						       FR_TLS_EX_INDEX_CONF);
+	if (!conf) return 0;
+
+	psk_len = strlen(conf->psk_password);
+	if (psk_len > (2 * max_psk_len)) return 0;
+
+	strlcpy(identity, conf->psk_identity, max_identity_len);
+
+	return fr_hex2bin(psk, max_psk_len, conf->psk_password, psk_len);
+}
+
+#endif
+
+#define MAX_SESSION_SIZE (256)
+
+
+void tls_session_id(SSL_SESSION *ssn, char *buffer, size_t bufsize)
+{
+#if OPENSSL_VERSION_NUMBER < 0x10001000L
+	size_t size;
+
+	size = ssn->session_id_length;
+	if (size > bufsize) size = bufsize;
+
+	fr_bin2hex(buffer, ssn->session_id, size);
+#else
+	unsigned int size;
+	uint8_t const *p;
+
+	p = SSL_SESSION_get_id(ssn, &size);
+	if (size > bufsize) size = bufsize;
+
+	fr_bin2hex(buffer, p, size);
+
+#endif
+}
+
+static int _tls_session_free(tls_session_t *ssn)
+{
+	/*
+	 *	Free any opaque TTLS or PEAP data.
+	 */
+	if ((ssn->opaque) && (ssn->free_opaque)) {
+		ssn->free_opaque(ssn->opaque);
+		ssn->opaque = NULL;
+	}
+
+	session_close(ssn);
+
+	return 0;
+}
+
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER)
+/*
+ *  By setting the environment variable SSLKEYLOGFILE to a filename keying
+ *  material will be exported that you may use with Wireshark to decode any
+ *  TLS flows. Please see the following for more details:
+ *
+ *	https://gitlab.com/wireshark/wireshark/-/wikis/TLS#tls-decryption
+ *
+ *  An example logging session is (you should delete the file on each run):
+ *
+ *	rm -f /tmp/sslkey.log; env SSLKEYLOGFILE=/tmp/sslkey.log freeradius -X | tee /tmp/debug
+ */
+static void tls_keylog_cb(UNUSED const SSL *ssl, const char *line)
+{
+	int fd;
+	size_t len;
+	const char *filename;
+	// less than _POSIX_PIPE_BUF (512) guarantees writes are atomic for O_APPEND
+	char buffer[64 + 2*SSL3_RANDOM_SIZE + 2*SSL_MAX_MASTER_KEY_LENGTH];
+
+	filename = getenv("SSLKEYLOGFILE");
+	if (!filename) return;
+
+	len = strlen(line);
+	if ((len + 1) > sizeof(buffer)) {
+		DEBUG("SSLKEYLOGFILE buffer not large enough, max %lu, required %lu", sizeof(buffer), len + 1);
+		return;
+	}
+
+	memcpy(buffer, line, len);
+	buffer[len] = '\n';
+
+	fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
+	if (fd < 0) {
+		fr_strerror_printf("Failed to open file %s: %s", filename, strerror(errno));
+		return;
+	}
+
+	if (write(fd, buffer, len + 1) == -1) {
+		DEBUG("Failed to write to file %s: %s", filename, strerror(errno));
+	}
+
+	close(fd);
+}
+#endif
+
+tls_session_t *tls_new_client_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, int fd, VALUE_PAIR **certs)
+{
+	int ret;
+	int verify_mode;
+	tls_session_t *ssn = NULL;
+	REQUEST *request;
+
+	ssn = talloc_zero(ctx, tls_session_t);
+	if (!ssn) return NULL;
+
+	talloc_set_destructor(ssn, _tls_session_free);
+
+	ssn->ctx = conf->ctx;
+	ssn->mtu = conf->fragment_size;
+	ssn->conf = conf;
+
+	SSL_CTX_set_mode(ssn->ctx, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_AUTO_RETRY);
+
+	ssn->ssl = SSL_new(ssn->ctx);
+	if (!ssn->ssl) {
+		talloc_free(ssn);
+		return NULL;
+	}
+
+	request = request_alloc(ssn);
+	request->packet = rad_alloc(request, false);
+	request->reply = rad_alloc(request, false);
+
+	SSL_set_ex_data(ssn->ssl, FR_TLS_EX_INDEX_REQUEST, (void *)request);
+
+	if (conf->fix_cert_order) {
+		SSL_set_ex_data(ssn->ssl, FR_TLS_EX_INDEX_FIX_CERT_ORDER, (void *) &conf->fix_cert_order);
+	}
+
+	/*
+	 *	Add the message callback to identify what type of
+	 *	message/handshake is passed
+	 */
+	SSL_set_msg_callback(ssn->ssl, cbtls_msg);
+	SSL_set_msg_callback_arg(ssn->ssl, ssn);
+	SSL_set_info_callback(ssn->ssl, cbtls_info);
+
+	/*
+	 *	Always verify the peer certificate.
+	 */
+	DEBUG2("Requiring Server certificate");
+	verify_mode = SSL_VERIFY_PEER;
+	verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
+	SSL_set_verify(ssn->ssl, verify_mode, cbtls_verify);
+
+	SSL_set_ex_data(ssn->ssl, FR_TLS_EX_INDEX_CONF, (void *)conf);
+	SSL_set_ex_data(ssn->ssl, FR_TLS_EX_INDEX_SSN, (void *)ssn);
+	if (certs) SSL_set_ex_data(ssn->ssl, fr_tls_ex_index_certs, (void *)certs);
+
+	SSL_set_fd(ssn->ssl, fd);
+
+	ret = SSL_connect(ssn->ssl);
+	if (ret < 0) {
+		switch (SSL_get_error(ssn->ssl, ret)) {
+		default:
+			break;
+
+		case SSL_ERROR_WANT_READ:
+			ssn->connected = false;
+			return ssn;
+
+		case SSL_ERROR_WANT_WRITE:
+			ssn->connected = false;
+			return ssn;
+		}
+	}
+
+	if (ret <= 0) {
+		tls_error_io_log(NULL, ssn, ret, "Failed in connecting TLS session.");
+		talloc_free(ssn);
+
+		return NULL;
+	}
+
+	ssn->connected = true;
+	return ssn;
+}
+
+
+/** Create a new TLS session
+ *
+ * Configures a new TLS session, configuring options, setting callbacks etc...
+ *
+ * @param ctx to alloc session data in. Should usually be NULL unless the lifetime of the
+ *	session is tied to another talloc'd object.
+ * @param conf to use to configure the tls session.
+ * @param request The current #REQUEST.
+ * @param client_cert Whether to require a client_cert.
+ * @param allow_tls13 Whether to allow or forbid TLS 1.3.
+ * @return a new session on success, or NULL on error.
+ */
+tls_session_t *tls_new_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, REQUEST *request, bool client_cert,
+#ifndef TLS1_3_VERSION
+			       UNUSED
+#endif
+			       bool allow_tls13)
+{
+	tls_session_t	*state = NULL;
+	SSL		*new_tls = NULL;
+	int		verify_mode = 0;
+	VALUE_PAIR	*vp;
+	X509_STORE	*new_cert_store;
+
+	rad_assert(request != NULL);
+
+	RDEBUG2("(TLS) Initiating new session");
+
+	/*
+	 *	Replace X509 store if it is time to update CRLs/certs in ca_path
+	 */
+	if (conf->ca_path_reload_interval > 0 && conf->ca_path_last_reload + conf->ca_path_reload_interval <= request->timestamp) {
+		pthread_mutex_lock(&conf->mutex);
+		/* recheck conf->ca_path_last_reload because it may be inaccurate without mutex */
+		if (conf->ca_path_last_reload + conf->ca_path_reload_interval <= request->timestamp) {
+			RDEBUG2("Flushing X509 store to re-read data from ca_path dir");
+
+			if ((new_cert_store = fr_init_x509_store(conf)) == NULL) {
+				RERROR("(TLS) Error replacing X509 store, out of memory (?)");
+			} else {
+				if (conf->old_x509_store) X509_STORE_free(conf->old_x509_store);
+				/*
+				 * Swap empty store with the old one.
+				 */
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER)
+				conf->old_x509_store = SSL_CTX_get_cert_store(conf->ctx);
+				/* Bump refcnt so the store is kept allocated till next store replacement */
+				X509_STORE_up_ref(conf->old_x509_store);
+				SSL_CTX_set_cert_store(conf->ctx, new_cert_store);
+#else
+				/*
+				 * We do not use SSL_CTX_set_cert_store() call here because
+				 * we are not sure that old X509 store is not in the use by some
+				 * thread (i.e. cert check in progress).
+				 * Keep it allocated till next store replacement.
+				 */
+				conf->old_x509_store = conf->ctx->cert_store;
+				conf->ctx->cert_store = new_cert_store;
+#endif
+				conf->ca_path_last_reload = request->timestamp;
+			}
+		}
+		pthread_mutex_unlock(&conf->mutex);
+	}
+
+	new_tls = SSL_new(conf->ctx);
+	if (new_tls == NULL) {
+		tls_error_log(request, "Error creating new TLS session");
+		return NULL;
+	}
+
+#ifdef TLS1_3_VERSION
+	/*
+	 *	Disallow TLS 1.3 for FAST.
+	 *
+	 *	We need another magic configuration option to allow
+	 *	it.
+	 */
+	if (!allow_tls13 && (conf->max_version == TLS1_3_VERSION)) {
+		WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+		WARN("!!                    FORCING MAXIMUM TLS VERSION TO TLS 1.2                  !!");
+		WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+		WARN("!! There is no standard for using this EAP method with TLS 1.3");
+		WARN("!! Please set tls_max_version = \"1.2\"");
+		WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+
+		if (SSL_set_max_proto_version(new_tls, TLS1_2_VERSION) == 0) {
+			tls_error_log(request, "Failed limiting maximum version to TLS 1.2");
+			return NULL;
+		}
+	}
+#endif
+
+	/* We use the SSL's "app_data" to indicate a call-back */
+	SSL_set_app_data(new_tls, NULL);
+
+	if ((state = talloc_zero(ctx, tls_session_t)) == NULL) {
+		RERROR("(TLS) Error allocating memory for SSL state");
+		return NULL;
+	}
+	session_init(state);
+	talloc_set_destructor(state, _tls_session_free);
+
+	state->ctx = conf->ctx;
+	state->ssl = new_tls;
+	state->conf = conf;
+
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER)
+	/*
+	 *	Set the keylog file if the admin requested it.
+	 */
+	if (getenv("SSLKEYLOGFILE") != NULL) SSL_CTX_set_keylog_callback(state->ctx, tls_keylog_cb);
+#endif
+
+	/*
+	 *	Initialize callbacks
+	 */
+	state->record_init = record_init;
+	state->record_close = record_close;
+	state->record_plus = record_plus;
+	state->record_minus = record_minus;
+
+	/*
+	 *	Create & hook the BIOs to handle the dirty side of the
+	 *	SSL.  This is *very important* as we want to handle
+	 *	the transmission part.  Now the only IO interface
+	 *	that SSL is aware of, is our defined BIO buffers.
+	 *
+	 *	This means that all SSL IO is done to/from memory,
+	 *	and we can update those BIOs from the packets we've
+	 *	received.
+	 */
+	state->into_ssl = BIO_new(BIO_s_mem());
+	state->from_ssl = BIO_new(BIO_s_mem());
+	SSL_set_bio(state->ssl, state->into_ssl, state->from_ssl);
+
+	/*
+	 *	Add the message callback to identify what type of
+	 *	message/handshake is passed
+	 */
+	SSL_set_msg_callback(new_tls, cbtls_msg);
+	SSL_set_msg_callback_arg(new_tls, state);
+	SSL_set_info_callback(new_tls, cbtls_info);
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+	/*
+	 *	Allow policies to load context-specific certificate chains.
+	 */
+	vp = fr_pair_find_by_num(request->config, PW_TLS_SESSION_CERT_FILE, 0, TAG_ANY);
+	if (vp) {
+		VALUE_PAIR *key = fr_pair_find_by_num(request->config, PW_TLS_SESSION_CERT_PRIVATE_KEY_FILE, 0, TAG_ANY);
+		if (!key) key = vp;
+
+		RDEBUG2("(TLS) Loading session certificate file \"%s\"", vp->vp_strvalue);
+
+		if (conf->realms) {
+			fr_realm_ctx_t my_r, *r;
+
+			/*
+			 *	Use a pre-existing SSL CTX, if
+			 *	available.  Note that due to OpenSSL
+			 *	issues, this really changes only the
+			 *	certificate files, and leaves all
+			 *	other fields alone.  e.g. you can't
+			 *	select a different TLS version.
+			 *
+			 *	This is fine for our purposes in v3.
+			 *	Due to how we build them, the various
+			 *	additional SSL_CTXs are identical to
+			 *	the main one, except for certs.
+			 */
+			my_r.name = vp->vp_strvalue;
+			r = fr_hash_table_finddata(conf->realms, &my_r);
+			if (r) {
+				(void) SSL_set_SSL_CTX(state->ssl, r->ctx);
+				goto after_chain;
+			}
+
+			/*
+			 *	Else fall through to trying to dynamically load the certs.
+			 */
+		}
+
+		if (conf->file_type) {
+			if (SSL_use_certificate_chain_file(state->ssl, vp->vp_strvalue) != 1) {
+				tls_error_log(request, "Failed loading TLS session certificate \"%s\"",
+					      vp->vp_strvalue);
+			error:
+				talloc_free(state);
+				return NULL;
+			}
+		} else {
+			if (SSL_use_certificate_file(state->ssl, vp->vp_strvalue, SSL_FILETYPE_ASN1) != 1) {
+				tls_error_log(request, "Failed loading TLS session certificate \"%s\"",
+					      vp->vp_strvalue);
+				goto error;
+			}
+		}
+
+		/*
+		 *	Note that there is either no password, or it
+		 *	has to be the same as what's in the
+		 *	configuration.
+		 *
+		 *	There is just no additional security to
+		 *	putting a password into the same file system
+		 *	as the private key.
+		 */
+		if (SSL_use_PrivateKey_file(state->ssl, key->vp_strvalue, SSL_FILETYPE_PEM) != 1) {
+			tls_error_log(request, "Failed loading TLS session certificate \"%s\"",
+				      key->vp_strvalue);
+			goto error;
+		}
+
+		if (SSL_check_private_key(state->ssl) != 1) {
+			tls_error_log(request, "Failed validating TLS session certificate \"%s\"",
+				      vp->vp_strvalue);
+			goto error;
+		}
+	}
+after_chain:
+#endif
+
+	/*
+	 *	In Server mode we only accept.
+	 */
+	SSL_set_accept_state(state->ssl);
+
+	/*
+	 *	Verify the peer certificate, if asked.
+	 */
+	if (client_cert) {
+		RDEBUG2("(TLS) Setting verify mode to require certificate from client");
+		verify_mode = SSL_VERIFY_PEER;
+		verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
+		verify_mode |= SSL_VERIFY_CLIENT_ONCE;
+	}
+	SSL_set_verify(state->ssl, verify_mode, cbtls_verify);
+
+	SSL_set_ex_data(state->ssl, FR_TLS_EX_INDEX_CONF, (void *)conf);
+	SSL_set_ex_data(state->ssl, FR_TLS_EX_INDEX_SSN, (void *)state);
+	state->length_flag = conf->include_length;
+
+	/*
+	 *	We use default fragment size, unless the Framed-MTU
+	 *	tells us it's too big.  Note that we do NOT account
+	 *	for the EAP-TLS headers if conf->fragment_size is
+	 *	large, because that config item looks to be confusing.
+	 *
+	 *	i.e. it should REALLY be called MTU, and the code here
+	 *	should figure out what that means for TLS fragment size.
+	 *	asking the administrator to know the internal details
+	 *	of EAP-TLS in order to calculate fragment sizes is
+	 *	just too much.
+	 */
+	state->mtu = conf->fragment_size;
+#define EAP_TLS_MAGIC_OVERHEAD (63)
+
+	/*
+	 *	If the packet contains an MTU, then use that.  We
+	 *	trust the admin!
+	 */
+	vp = fr_pair_find_by_num(request->packet->vps, PW_FRAMED_MTU, 0, TAG_ANY);
+	if (vp) {
+		if ((vp->vp_integer > 100) && (vp->vp_integer < state->mtu)) {
+			state->mtu = vp->vp_integer;
+		}
+
+	} else if (request->parent) {
+		/*
+		 *	If there's a parent request, we look for what
+		 *	MTU was set there.  Then, we use an MTU which
+		 *	accounts for the extra overhead of nesting EAP
+		 *	+ TLS inside of EAP + TLS.
+		 */
+		vp = fr_pair_find_by_num(request->parent->state, PW_FRAMED_MTU, 0, TAG_ANY);
+		if (vp && (vp->vp_integer > (100 + EAP_TLS_MAGIC_OVERHEAD)) && (vp->vp_integer <= state->mtu)) {
+			state->mtu = vp->vp_integer - EAP_TLS_MAGIC_OVERHEAD;
+		}
+	}
+
+	/*
+	 *	Cache / update the Framed-MTU in the session-state
+	 *	list.
+	 */
+	vp = fr_pair_find_by_num(request->state, PW_FRAMED_MTU, 0, TAG_ANY);
+	if (!vp) {
+		vp = fr_pair_afrom_num(request->state_ctx, PW_FRAMED_MTU, 0);
+		fr_pair_add(&request->state, vp);
+	}
+	if (vp) vp->vp_integer = state->mtu;
+
+	if (conf->session_cache_enable) state->allow_session_resumption = true; /* otherwise it's false */
+
+	return state;
+}
+
+/*
+ * We are the server, we always get the dirty data
+ * (Handshake data is also considered as dirty data)
+ * During handshake, since SSL API handles itself,
+ * After clean-up, dirty_out will be filled with
+ * the data required for handshaking. So we check
+ * if dirty_out is empty then we simply send it back.
+ * As of now, if handshake is successful, then we keep going,
+ * otherwise we fail.
+ *
+ * Fill the Bio with the dirty data to clean it
+ * Get the cleaned data from SSL, if it is not Handshake data
+ */
+int tls_handshake_recv(REQUEST *request, tls_session_t *ssn)
+{
+	int err;
+
+	if (ssn->invalid_hb_used) {
+		REDEBUG("(TLS) OpenSSL Heartbeat attack detected.  Closing connection");
+		return 0;
+	}
+
+	if (ssn->dirty_in.used > 0) {
+		err = BIO_write(ssn->into_ssl, ssn->dirty_in.data, ssn->dirty_in.used);
+		if (err != (int) ssn->dirty_in.used) {
+			REDEBUG("(TLS) Failed writing %zd bytes to SSL BIO: %d", ssn->dirty_in.used, err);
+			record_init(&ssn->dirty_in);
+			return 0;
+		}
+		record_init(&ssn->dirty_in);
+	}
+
+	err = SSL_read(ssn->ssl, ssn->clean_out.data + ssn->clean_out.used,
+		       sizeof(ssn->clean_out.data) - ssn->clean_out.used);
+	if (err > 0) {
+		ssn->clean_out.used += err;
+		return 1;
+	}
+
+	if (!tls_error_io_log(request, ssn, err, "Failed reading from OpenSSL")) return 0;
+
+	/* Some Extra STATE information for easy debugging */
+	if (!ssn->is_init_finished && SSL_is_init_finished(ssn->ssl)) {
+		VALUE_PAIR *vp;
+		char const *str_version;
+
+		RDEBUG2("(TLS) Connection Established");
+		ssn->is_init_finished = true;
+
+		vp = fr_pair_afrom_num(request->state_ctx, PW_TLS_SESSION_CIPHER_SUITE, 0);
+		if (vp) {
+			fr_pair_value_strcpy(vp, SSL_CIPHER_get_name(SSL_get_current_cipher(ssn->ssl)));
+			fr_pair_add(&request->state, vp);
+			RINDENT();
+			rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+			REXDENT();
+		}
+
+		switch (SSL_version(ssn->ssl)) {
+		case SSL2_VERSION:
+			str_version = "SSL 2.0";
+			break;
+		case SSL3_VERSION:
+			str_version = "SSL 3.0";
+			break;
+		case TLS1_VERSION:
+			str_version = "TLS 1.0";
+			break;
+#ifdef TLS1_1_VERSION
+		case TLS1_1_VERSION:
+			str_version = "TLS 1.1";
+			break;
+#endif
+#ifdef TLS1_2_VERSION
+		case TLS1_2_VERSION:
+			str_version = "TLS 1.2";
+			break;
+#endif
+#ifdef TLS1_3_VERSION
+		case TLS1_3_VERSION:
+			str_version = "TLS 1.3";
+			break;
+#endif
+		default:
+			str_version = "UNKNOWN";
+			break;
+		}
+
+		vp = fr_pair_afrom_num(request->state_ctx, PW_TLS_SESSION_VERSION, 0);
+		if (vp) {
+			fr_pair_value_strcpy(vp, str_version);
+			fr_pair_add(&request->state, vp);
+			RINDENT();
+			rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+			REXDENT();
+		}
+	}
+	else if (SSL_in_init(ssn->ssl)) { RDEBUG2("(TLS) In Handshake Phase"); }
+	else if (SSL_in_before(ssn->ssl)) { RDEBUG2("(TLS) Before Handshake Phase"); }
+	else if (SSL_in_accept_init(ssn->ssl)) { RDEBUG2("(TLS) In Accept mode"); }
+	else if (SSL_in_connect_init(ssn->ssl)) { RDEBUG2("(TLS) In Connect mode"); }
+
+#if OPENSSL_VERSION_NUMBER >= 0x10001000L
+	/*
+	 *	Cache the SSL_SESSION pointer.
+	 */
+	if (!ssn->ssl_session) {
+		ssn->ssl_session = SSL_get_session(ssn->ssl);
+
+		/*
+		 *	Some versions of OpenSSL don't allow you to
+		 *	get the session before the init is finished.
+		 *	In that case, this error is a soft fail.
+		 *
+		 *	If the session init is finished, then failure
+		 *	to get the session is a hard fail.
+		 */
+		if (!ssn->ssl_session && ssn->is_init_finished) {
+			RDEBUG("(TLS) Failed getting session");
+			return 0;
+		}
+	}
+
+#else
+#error You must use a newer version of OpenSSL
+#endif
+
+	err = BIO_ctrl_pending(ssn->from_ssl);
+	if (err > 0) {
+		err = BIO_read(ssn->from_ssl, ssn->dirty_out.data,
+			       sizeof(ssn->dirty_out.data));
+		if (err > 0) {
+			RDEBUG3("(TLS) got %d bytes of data", err);
+			ssn->dirty_out.used = err;
+
+		} else if (BIO_should_retry(ssn->from_ssl)) {
+			record_init(&ssn->dirty_in);
+			RDEBUG2("(TLS) Asking for more data in tunnel.");
+			return 1;
+
+		} else {
+			tls_error_log(NULL, "Error reading from OpenSSL");
+			record_init(&ssn->dirty_in);
+			return 0;
+		}
+	} else {
+		RDEBUG2("(TLS) Application data.");
+		/* Its clean application data, leave whatever is in the buffer */
+#if 0
+		record_init(&ssn->clean_out);
+#endif
+	}
+
+	/* We are done with dirty_in, reinitialize it */
+	record_init(&ssn->dirty_in);
+	return 1;
+}
+
+/*
+ *	Take cleartext user data, and encrypt it into the output buffer,
+ *	to send to the client at the other end of the SSL connection.
+ */
+int tls_handshake_send(REQUEST *request, tls_session_t *ssn)
+{
+	int err;
+
+	/*
+	 *	If there's un-encrypted data in 'clean_in', then write
+	 *	that data to the SSL session, and then call the BIO function
+	 *	to get that encrypted data from the SSL session, into
+	 *	a buffer which we can then package into an EAP packet.
+	 *
+	 *	Based on Server's logic this clean_in is expected to
+	 *	contain the data to send to the client.
+	 */
+	if (ssn->clean_in.used > 0) {
+		int written;
+
+		written = SSL_write(ssn->ssl, ssn->clean_in.data, ssn->clean_in.used);
+		record_minus(&ssn->clean_in, NULL, written);
+
+		/* Get the dirty data from Bio to send it */
+		err = BIO_read(ssn->from_ssl, ssn->dirty_out.data + ssn->dirty_out.used,
+			       sizeof(ssn->dirty_out.data) - ssn->dirty_out.used);
+		if (err > 0) {
+			ssn->dirty_out.used += err;
+		} else {
+			if (!tls_error_io_log(request, ssn, err, "Failed writing to OpenSSL")) {
+				return 0;
+			}
+		}
+	}
+
+	return 1;
+}
+
+static void session_init(tls_session_t *ssn)
+{
+	ssn->ssl = NULL;
+	ssn->into_ssl = ssn->from_ssl = NULL;
+	record_init(&ssn->clean_in);
+	record_init(&ssn->clean_out);
+	record_init(&ssn->dirty_in);
+	record_init(&ssn->dirty_out);
+
+	memset(&ssn->info, 0, sizeof(ssn->info));
+
+	ssn->mtu = 0;
+	ssn->fragment = false;
+	ssn->tls_msg_len = 0;
+	ssn->length_flag = false;
+	ssn->opaque = NULL;
+	ssn->free_opaque = NULL;
+}
+
+static void session_close(tls_session_t *ssn)
+{
+	if (ssn->ssl) {
+		SSL_set_quiet_shutdown(ssn->ssl, 1);
+		SSL_shutdown(ssn->ssl);
+
+		SSL_free(ssn->ssl);
+		ssn->ssl = NULL;
+	}
+
+	record_close(&ssn->clean_in);
+	record_close(&ssn->clean_out);
+	record_close(&ssn->dirty_in);
+	record_close(&ssn->dirty_out);
+	session_init(ssn);
+}
+
+static void record_init(record_t *rec)
+{
+	rec->used = 0;
+}
+
+static void record_close(record_t *rec)
+{
+	rec->used = 0;
+}
+
+
+/*
+ *	Copy data to the intermediate buffer, before we send
+ *	it somewhere.
+ */
+static unsigned int record_plus(record_t *rec, void const *ptr,
+				unsigned int size)
+{
+	unsigned int added = MAX_RECORD_SIZE - rec->used;
+
+	if(added > size)
+		added = size;
+	if(added == 0)
+		return 0;
+	memcpy(rec->data + rec->used, ptr, added);
+	rec->used += added;
+	return added;
+}
+
+/*
+ *	Take data from the buffer, and give it to the caller.
+ */
+static unsigned int record_minus(record_t *rec, void *ptr,
+				 unsigned int size)
+{
+	unsigned int taken = rec->used;
+
+	if(taken > size)
+		taken = size;
+	if(taken == 0)
+		return 0;
+	if(ptr)
+		memcpy(ptr, rec->data, taken);
+	rec->used -= taken;
+
+	/*
+	 *	This is pretty bad...
+	 */
+	if (rec->used > 0) memmove(rec->data, rec->data + taken, rec->used);
+
+	return taken;
+}
+
+void tls_session_information(tls_session_t *tls_session)
+{
+	char const *str_write_p, *str_version, *str_content_type = "";
+	char const *str_details1 = "", *str_details2= "";
+	char const *details = NULL;
+	REQUEST *request;
+	VALUE_PAIR *vp;
+	char content_type[16], alert_buf[16];
+	char buffer[32];
+
+	/*
+	 *	Don't print this out in the normal course of
+	 *	operations.
+	 */
+	if (rad_debug_lvl == 0) return;
+
+	/*
+	 *	OpenSSL calls this function with 'pseudo' content
+	 *	types.  The user doesn't care about them, so suppress them.
+	 */
+	if (tls_session->info.content_type > UINT8_MAX) return;
+
+	request = SSL_get_ex_data(tls_session->ssl, FR_TLS_EX_INDEX_REQUEST);
+	if (!request) return;
+
+	str_write_p = tls_session->info.origin ? "(TLS) send" : "(TLS) recv";
+
+#define FROM_CLIENT (tls_session->info.origin == 0)
+
+	switch (SSL_version(tls_session->ssl)) {
+	case SSL2_VERSION:
+		str_version = "SSL 2.0 ";
+		break;
+	case SSL3_VERSION:
+		str_version = "SSL 3.0 ";
+		break;
+	case TLS1_VERSION:
+		str_version = "TLS 1.0 ";
+		break;
+#ifdef TLS1_1_VERSION
+	case TLS1_1_VERSION:
+		str_version = "TLS 1.1 ";
+		break;
+#endif
+#ifdef TLS1_2_VERSION
+	case TLS1_2_VERSION:
+		str_version = "TLS 1.2 ";
+		break;
+#endif
+#ifdef TLS1_3_VERSION
+	case TLS1_3_VERSION:
+		str_version = "TLS 1.3 ";
+		break;
+#endif
+
+	default:
+		sprintf(buffer, "UNKNOWN TLS VERSION '%04X'", SSL_version(tls_session->ssl));
+		str_version = buffer;
+		break;
+	}
+
+	if (1) {
+		switch (tls_session->info.content_type) {
+		case SSL3_RT_CHANGE_CIPHER_SPEC:
+			str_content_type = "ChangeCipherSpec";
+			break;
+
+		case SSL3_RT_ALERT:
+			str_content_type = "Alert";
+			break;
+
+		case SSL3_RT_HANDSHAKE:
+			str_content_type = "Handshake";
+			break;
+
+		case SSL3_RT_APPLICATION_DATA:
+			str_content_type = "ApplicationData";
+			break;
+
+		default:
+			snprintf(content_type, sizeof(content_type), "content=%d", tls_session->info.content_type);
+			str_content_type = content_type;
+			break;
+		}
+
+		if (tls_session->info.content_type == SSL3_RT_ALERT) {
+			str_details1 = ", ???";
+
+			if (tls_session->info.record_len == 2) {
+
+				switch (tls_session->info.alert_level) {
+				case SSL3_AL_WARNING:
+					str_details1 = ", warning";
+					break;
+				case SSL3_AL_FATAL:
+					str_details1 = ", fatal";
+					break;
+				}
+
+				str_details2 = " ???";
+				details = "there is a failure inside the TLS protocol exchange";
+
+				switch (tls_session->info.alert_description) {
+				case SSL3_AD_CLOSE_NOTIFY:
+					str_details2 = " close_notify";
+					details = "the connection has been closed, and no further TLS exchanges will take place";
+					break;
+
+				case SSL3_AD_UNEXPECTED_MESSAGE:
+					str_details2 = " unexpected_message";
+					break;
+
+				case SSL3_AD_BAD_RECORD_MAC:
+					str_details2 = " bad_record_mac";
+					break;
+
+				case TLS1_AD_DECRYPTION_FAILED:
+					str_details2 = " decryption_failed";
+					break;
+
+				case TLS1_AD_RECORD_OVERFLOW:
+					str_details2 = " record_overflow";
+					break;
+
+				case SSL3_AD_DECOMPRESSION_FAILURE:
+					str_details2 = " decompression_failure";
+					break;
+
+				case SSL3_AD_HANDSHAKE_FAILURE:
+					str_details2 = " handshake_failure";
+					break;
+
+				case SSL3_AD_NO_CERTIFICATE:
+					str_details2 = " no_certificate";
+					details = "the server did not present a certificate to the client";
+					break;
+
+				case SSL3_AD_BAD_CERTIFICATE:
+					str_details2 = " bad_certificate";
+					details = "it believes the server certificate is invalid or malformed";
+					break;
+
+				case SSL3_AD_UNSUPPORTED_CERTIFICATE:
+					str_details2 = " unsupported_certificate";
+					details = "it does not understand the certificate presented by the server";
+					break;
+
+				case SSL3_AD_CERTIFICATE_REVOKED:
+					str_details2 = " certificate_revoked";
+					details = "it believes that the server certificate has been revoked";
+					break;
+
+				case SSL3_AD_CERTIFICATE_EXPIRED:
+					str_details2 = " certificate_expired";
+					details = "it believes that the server certificate has expired.  Either renew the server certificate, or check the time on the client";
+					break;
+
+				case SSL3_AD_CERTIFICATE_UNKNOWN:
+					str_details2 = " certificate_unknown";
+					details = "it does not recognize the server certificate";
+					break;
+
+				case SSL3_AD_ILLEGAL_PARAMETER:
+					str_details2 = " illegal_parameter";
+					break;
+
+				case TLS1_AD_UNKNOWN_CA:
+					str_details2 = " unknown_ca";
+					details = "it does not recognize the CA used to issue the server certificate.  Please update the client so that it knows about the CA";
+					break;
+
+				case TLS1_AD_ACCESS_DENIED:
+					str_details2 = " access_denied";
+					break;
+
+				case TLS1_AD_DECODE_ERROR:
+					str_details2 = " decode_error";
+					break;
+
+				case TLS1_AD_DECRYPT_ERROR:
+					str_details2 = " decrypt_error";
+					break;
+
+				case TLS1_AD_EXPORT_RESTRICTION:
+					str_details2 = " export_restriction";
+					break;
+
+				case TLS1_AD_PROTOCOL_VERSION:
+					str_details2 = " protocol_version";
+					details = "the client does not accept the version of TLS negotiated by the server";
+
+#ifdef TLS1_3_VERSION
+					/*
+					 *	Complain about OpenSSL bugs.
+					 */
+					if ((SSL_version(tls_session->ssl) > tls_session->conf->max_version) &&
+					    (rad_debug_lvl > 0)) {
+						WARN("TLS 1.3 has been negotiated even though it was disabled.  This is an OpenSSL Bug.");
+						WARN("Please set: cipher_list = \"DEFAULT@SECLEVEL=1\" in the tls {...} section.");
+					}
+#endif
+					break;
+
+				case TLS1_AD_INSUFFICIENT_SECURITY:
+					str_details2 = " insufficient_security";
+					break;
+
+				case TLS1_AD_INTERNAL_ERROR:
+					str_details2 = " internal_error";
+					break;
+
+				case TLS1_AD_USER_CANCELLED:
+					str_details2 = " user_canceled";
+					break;
+
+				case TLS1_AD_NO_RENEGOTIATION:
+					str_details2 = " no_renegotiation";
+					break;
+
+#ifdef TLS13_AD_MISSING_EXTENSIONS
+				case TLS13_AD_MISSING_EXTENSIONS:
+					str_details2 = " missing_extensions";
+					details = "the server did not present a TLS extension which the client expected to be present.  Please check the TLS libraries on the client and server for compatibility";
+					break;
+#endif
+
+#ifdef TLS13_AD_CERTIFICATE_REQUIRED
+				case TLS13_AD_CERTIFICATE_REQUIRED:
+					str_details2 = " certificate_required";
+					details = "the server did not present a certificate";
+					break;
+#endif
+
+#ifdef TLS1_AD_UNSUPPORTED_EXTENSION
+				case TLS1_AD_UNSUPPORTED_EXTENSION:
+					str_details2 = " unsupported_extension";
+					details = "the server has sent a TLS message which the client does not recognize.  Please check the TLS libraries on the client and server for compatibility";
+					break;
+#endif
+
+#ifdef TLS1_AD_CERTIFICATE_UNOBTAINABLE
+				case TLS1_AD_CERTIFICATE_UNOBTAINABLE:
+					str_details2 = " certificate_unobtainable";
+					break;
+#endif
+
+#ifdef TLS1_AD_UNRECOGNIZED_NAME
+				case TLS1_AD_UNRECOGNIZED_NAME:
+					str_details2 = " unrecognized_name";
+					break;
+#endif
+
+#ifdef TLS1_AD_BAD_CERTIFICATE_STATUS_RESPONSE
+				case TLS1_AD_BAD_CERTIFICATE_STATUS_RESPONSE:
+					str_details2 = " bad_certificate_status_response";
+					break;
+#endif
+
+#ifdef TLS1_AD_BAD_CERTIFICATE_HASH_VALUE
+				case TLS1_AD_BAD_CERTIFICATE_HASH_VALUE:
+					str_details2 = " bad_certificate_hash_value";
+					break;
+#endif
+
+#ifdef TLS1_AD_UNKNOWN_PSK_IDENTITY
+				case TLS1_AD_UNKNOWN_PSK_IDENTITY:
+					str_details2 = " unknown_psk_identity";
+					break;
+#endif
+
+#ifdef TLS1_AD_NO_APPLICATION_PROTOCOL
+				case TLS1_AD_NO_APPLICATION_PROTOCOL:
+					str_details2 = " no_application_protocol";
+					break;
+#endif
+				}
+			}
+		}
+
+		if (tls_session->info.content_type == SSL3_RT_HANDSHAKE) {
+			str_details1 = "";
+
+			if (tls_session->info.record_len > 0) switch (tls_session->info.handshake_type) {
+			case SSL3_MT_HELLO_REQUEST:
+				str_details1 = ", HelloRequest";
+				break;
+
+			case SSL3_MT_CLIENT_HELLO:
+				str_details1 = ", ClientHello";
+				break;
+
+			case SSL3_MT_SERVER_HELLO:
+				str_details1 = ", ServerHello";
+				break;
+
+#ifdef SSL3_MT_NEWSESSION_TICKET
+			case SSL3_MT_NEWSESSION_TICKET:
+				str_details1 = ", NewSessionTicket";
+				break;
+#endif
+
+#ifdef SSL3_MT_ENCRYPTED_EXTENSIONS
+			case SSL3_MT_ENCRYPTED_EXTENSIONS:
+				str_details1 = ", EncryptedExtensions";
+				break;
+#endif
+
+			case SSL3_MT_CERTIFICATE:
+				str_details1 = ", Certificate";
+				break;
+
+			case SSL3_MT_SERVER_KEY_EXCHANGE:
+				str_details1 = ", ServerKeyExchange";
+				break;
+
+			case SSL3_MT_CERTIFICATE_REQUEST:
+				str_details1 = ", CertificateRequest";
+				break;
+
+			case SSL3_MT_SERVER_DONE:
+				str_details1 = ", ServerHelloDone";
+				break;
+
+			case SSL3_MT_CERTIFICATE_VERIFY:
+				str_details1 = ", CertificateVerify";
+				break;
+
+			case SSL3_MT_CLIENT_KEY_EXCHANGE:
+				str_details1 = ", ClientKeyExchange";
+				break;
+
+			case SSL3_MT_FINISHED:
+				str_details1 = ", Finished";
+				break;
+
+#ifdef SSL3_MT_KEY_UPDATE
+			case SSL3_MT_KEY_UPDATE:
+				str_content_type = "KeyUpdate";
+				break;
+#endif
+
+			default:
+				snprintf(alert_buf, sizeof(alert_buf), ", type=%d", tls_session->info.handshake_type);
+				str_details1 = alert_buf;
+				break;
+			}
+		}
+	}
+
+	snprintf(tls_session->info.info_description,
+		 sizeof(tls_session->info.info_description),
+		 "%s %s%s%s%s",
+		 str_write_p, str_version, str_content_type,
+		 str_details1, str_details2);
+
+	/*
+	 *	Cache the TLS session information in the session-state
+	 *	list, so it can be accessed by Post-Auth-Type
+	 *	Client-Lost { ... }
+	 */
+	vp = fr_pair_afrom_num(request->state_ctx, PW_TLS_SESSION_INFORMATION, 0);
+	if (vp) {
+		fr_pair_value_strcpy(vp, tls_session->info.info_description);
+		fr_pair_add(&request->state, vp);
+	}
+
+	RDEBUG2("%s", tls_session->info.info_description);
+
+	if (FROM_CLIENT && details) RDEBUG2("(TLS) The client is informing us that %s.", details);
+}
+
+static CONF_PARSER cache_config[] = {
+	{ "enable", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, session_cache_enable), "no" },
+
+	{ "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, session_lifetime), "24" },
+	{ "name", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, session_id_name), NULL },
+
+	{ "max_entries", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, session_cache_size), "255" },
+	{ "persist_dir", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, session_cache_path), NULL },
+	{ "virtual_server", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, session_cache_server), NULL },
+	CONF_PARSER_TERMINATOR
+};
+
+static CONF_PARSER verify_config[] = {
+	{ "skip_if_ocsp_ok", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, verify_skip_if_ocsp_ok), "no" },
+	{ "tmpdir", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, verify_tmp_dir), NULL },
+	{ "client", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, verify_client_cert_cmd), NULL },
+	CONF_PARSER_TERMINATOR
+};
+
+#ifdef HAVE_OPENSSL_OCSP_H
+static CONF_PARSER ocsp_config[] = {
+	{ "enable", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, ocsp_enable), "no" },
+	{ "override_cert_url", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, ocsp_override_url), "no" },
+	{ "url", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, ocsp_url), NULL },
+	{ "use_nonce", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, ocsp_use_nonce), "yes" },
+	{ "timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, ocsp_timeout), "yes" },
+	{ "softfail", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, ocsp_softfail), "no" },
+	CONF_PARSER_TERMINATOR
+};
+#endif
+
+static CONF_PARSER tls_server_config[] = {
+	{ "verify_depth", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, verify_depth), "0" },
+	{ "CA_path", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT | PW_TYPE_DEPRECATED, fr_tls_server_conf_t, ca_path), NULL },
+	{ "ca_path", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, ca_path), NULL },
+	{ "pem_file_type", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, file_type), "yes" },
+	{ "private_key_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, private_key_file), NULL },
+	{ "certificate_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, certificate_file), NULL },
+	{ "CA_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT | PW_TYPE_DEPRECATED, fr_tls_server_conf_t, ca_file), NULL },
+	{ "ca_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, ca_file), NULL },
+	{ "private_key_password", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, fr_tls_server_conf_t, private_key_password), NULL },
+#ifdef PSK_MAX_IDENTITY_LEN
+	{ "psk_identity", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, psk_identity), NULL },
+	{ "psk_hexphrase", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, fr_tls_server_conf_t, psk_password), NULL },
+	{ "psk_query", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, psk_query), NULL },
+#endif
+	{ "dh_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, dh_file), NULL },
+	{ "random_file", FR_CONF_OFFSET(PW_TYPE_FILE_EXISTS, fr_tls_server_conf_t, random_file), NULL },
+	{ "fragment_size", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, fragment_size), "1024" },
+	{ "include_length", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, include_length), "yes" },
+	{ "auto_chain", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, auto_chain), "yes" },
+	{ "disable_single_dh_use", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_single_dh_use), NULL },
+	{ "check_crl", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, check_crl), "no" },
+#ifdef X509_V_FLAG_CRL_CHECK_ALL
+	{ "check_all_crl", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, check_all_crl), "no" },
+#endif
+	{ "ca_path_reload_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, ca_path_reload_interval), "0" },
+	{ "allow_expired_crl", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, allow_expired_crl), NULL },
+	{ "check_cert_cn", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, check_cert_cn), NULL },
+	{ "cipher_list", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, cipher_list), NULL },
+	{ "cipher_server_preference", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, cipher_server_preference), NULL },
+	{ "check_cert_issuer", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, check_cert_issuer), NULL },
+	{ "require_client_cert", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, require_client_cert), NULL },
+
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+	{ "sigalgs_list", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, sigalgs_list), NULL },
+#endif
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+	{ "reject_unknown_intermediate_ca", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disallow_untrusted), .dflt = "no", },
+#endif
+
+#if OPENSSL_VERSION_NUMBER >= 0x0090800fL
+#ifndef OPENSSL_NO_ECDH
+	{ "ecdh_curve", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, ecdh_curve), "prime256v1" },
+#endif
+#endif
+
+#ifdef SSL_OP_NO_TLSv1
+	{ "disable_tlsv1", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1), NULL },
+#endif
+
+#ifdef SSL_OP_NO_TLSv1_1
+	{ "disable_tlsv1_1", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1_1), NULL },
+#endif
+
+#ifdef SSL_OP_NO_TLSv1_2
+	{ "disable_tlsv1_2", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1_2), NULL },
+#endif
+
+	{ "tls_max_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_max_version), NULL },
+
+	{ "tls_min_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_min_version),
+#if defined(TLS1_2_VERSION)
+	  "1.2"
+#elif defined(TLS1_1_VERSION)
+	  "1.1"
+#else
+	  "1.0"
+#endif
+	},
+
+#ifdef WITH_RADIUSV11
+	{ "radiusv1_1", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, radiusv11_name), NULL },
+#endif
+
+	{ "realm_dir", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, realm_dir), NULL },
+
+	{ "cache", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) cache_config },
+
+	{ "verify", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) verify_config },
+
+#ifdef HAVE_OPENSSL_OCSP_H
+	{ "ocsp", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) ocsp_config },
+#endif
+	CONF_PARSER_TERMINATOR
+};
+
+
+static CONF_PARSER tls_client_config[] = {
+	{ "verify_depth", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, verify_depth), "0" },
+	{ "ca_path", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, ca_path), NULL },
+	{ "pem_file_type", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, file_type), "yes" },
+	{ "private_key_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, private_key_file), NULL },
+	{ "certificate_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, certificate_file), NULL },
+	{ "ca_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, ca_file), NULL },
+	{ "private_key_password", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, fr_tls_server_conf_t, private_key_password), NULL },
+	{ "dh_file", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, dh_file), NULL },
+	{ "random_file", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, random_file), NULL },
+	{ "fragment_size", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, fragment_size), "1024" },
+	{ "include_length", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, include_length), "yes" },
+	{ "check_crl", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, check_crl), "no" },
+	{ "check_cert_cn", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, check_cert_cn), NULL },
+	{ "cipher_list", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, cipher_list), NULL },
+	{ "check_cert_issuer", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, check_cert_issuer), NULL },
+	{ "ca_path_reload_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, ca_path_reload_interval), "0" },
+
+	{ "fix_cert_order", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, fix_cert_order), NULL },
+
+#if OPENSSL_VERSION_NUMBER >= 0x0090800fL
+#ifndef OPENSSL_NO_ECDH
+	{ "ecdh_curve", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, ecdh_curve), "prime256v1" },
+#endif
+#endif
+
+#ifdef SSL_OP_NO_TLSv1
+	{ "disable_tlsv1", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1), NULL },
+#endif
+
+#ifdef SSL_OP_NO_TLSv1_1
+	{ "disable_tlsv1_1", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1_1), NULL },
+#endif
+
+#ifdef SSL_OP_NO_TLSv1_2
+	{ "disable_tlsv1_2", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1_2), NULL },
+#endif
+
+	{ "tls_max_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_max_version), NULL },
+
+	{ "tls_min_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_min_version),
+#if defined(TLS1_2_VERSION)
+	  "1.2"
+#elif defined(TLS1_1_VERSION)
+	  "1.1"
+#else
+	  "1.0"
+#endif
+	},
+
+#ifdef WITH_RADIUSV11
+	{ "radiusv1_1", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, radiusv11_name), NULL },
+#endif
+
+	{ "hostname", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, client_hostname), NULL },
+
+	CONF_PARSER_TERMINATOR
+};
+
+
+/*
+ *	TODO: Check for the type of key exchange * like conf->dh_key
+ */
+static int load_dh_params(SSL_CTX *ctx, char *file)
+{
+	DH *dh = NULL;
+	BIO *bio;
+
+	/*
+	 * Prior to trying to load the file, check what OpenSSL will do with it.
+	 *
+	 * Certain downstreams (such as RHEL) will ignore user-provided dhparams
+	 * in FIPS mode, unless the specified parameters are FIPS-approved.
+	 * However, since OpenSSL >= 1.1.1 will automatically select parameters
+	 * anyways, there's no point in attempting to load them.
+	 *
+	 * Change suggested by @t8m
+	 */
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+	if (FIPS_mode() > 0) {
+		WARN(LOG_PREFIX ": Ignoring user-selected DH parameters in FIPS mode. Using defaults.");
+		file = NULL;
+	}
+
+	/*
+	 *	No dh file, set auto context.
+	 */
+	if (!file) {
+		if (!SSL_CTX_set_dh_auto(ctx, 1)) {
+			ERROR(LOG_PREFIX ": Unable to set DH parameters");
+			return -1;
+		}
+
+		return 0;
+	}
+
+	WARN(LOG_PREFIX ": Setting DH parameters from %s - this is no longer necessary.", file);
+	WARN(LOG_PREFIX ": You should comment out the 'dh_file' configuration item.");
+
+#else
+	if (!file) {
+		WARN(LOG_PREFIX ": Cannot set DH parameters.  DH cipher suites may not work.");
+		return 0;
+	}
+#endif
+
+
+	if ((bio = BIO_new_file(file, "r")) == NULL) {
+		ERROR(LOG_PREFIX ": Unable to open DH file - %s", file);
+		return -1;
+	}
+
+	dh = PEM_read_bio_DHparams(bio, NULL, NULL, NULL);
+	BIO_free(bio);
+	if (!dh) {
+		WARN(LOG_PREFIX ": Unable to set DH parameters.  DH cipher suites may not work!");
+		WARN(LOG_PREFIX ": Fix this by running the OpenSSL command listed in eap.conf");
+		return 0;
+	}
+
+	if (SSL_CTX_set_tmp_dh(ctx, dh) < 0) {
+		ERROR(LOG_PREFIX ": Unable to set DH parameters");
+		DH_free(dh);
+		return -1;
+	}
+
+	DH_free(dh);
+	return 0;
+}
+
+
+/*
+ *	Print debugging messages, and free data.
+ */
+static void cbtls_remove_session(SSL_CTX *ctx, SSL_SESSION *sess)
+{
+	char			buffer[2 * MAX_SESSION_SIZE + 1];
+	fr_tls_server_conf_t	*conf;
+
+	tls_session_id(sess, buffer, MAX_SESSION_SIZE);
+
+	conf = (fr_tls_server_conf_t *)SSL_CTX_get_app_data(ctx);
+	if (!conf) {
+		DEBUG(LOG_PREFIX ": Failed to find TLS configuration in session");
+		return;
+	}
+
+	{
+		int rv;
+		char filename[3 * MAX_SESSION_SIZE + 1];
+
+		DEBUG2(LOG_PREFIX ": Removing session %s from the cache", buffer);
+
+		/* remove session and any cached VPs */
+		snprintf(filename, sizeof(filename), "%s%c%s.asn1",
+			 conf->session_cache_path, FR_DIR_SEP, buffer);
+		rv = unlink(filename);
+		if (rv != 0) {
+			DEBUG2(LOG_PREFIX ": Could not remove persisted session file %s: %s",
+			       filename, fr_syserror(errno));
+		}
+		/* VPs might be absent; might not have been written to disk yet */
+		snprintf(filename, sizeof(filename), "%s%c%s.vps",
+			 conf->session_cache_path, FR_DIR_SEP, buffer);
+		unlink(filename);
+	}
+
+	return;
+}
+
+static int cbtls_new_session(SSL *ssl, SSL_SESSION *sess)
+{
+	char			buffer[2 * MAX_SESSION_SIZE + 1];
+	fr_tls_server_conf_t	*conf;
+	unsigned char		*sess_blob = NULL;
+
+	REQUEST			*request = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST);
+
+	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF);
+	if (!conf) {
+		RWDEBUG("(TLS) Failed to find TLS configuration in session");
+		return 0;
+	}
+
+	tls_session_id(sess, buffer, MAX_SESSION_SIZE);
+
+	{
+		int fd, rv, todo, blob_len;
+		char filename[3 * MAX_SESSION_SIZE + 1];
+		unsigned char *p;
+
+		RDEBUG2("Serialising session %s, and storing in cache", buffer);
+
+		/* find out what length data we need */
+		blob_len = i2d_SSL_SESSION(sess, NULL);
+		if (blob_len < 1) {
+			/* something went wrong */
+			if (request) RWDEBUG("(TLS) Session serialisation failed, could not determine required buffer length");
+			return 0;
+		}
+
+		/* Do not convert to TALLOC - Thread safety */
+		/* alloc and convert to ASN.1 */
+		sess_blob = malloc(blob_len);
+		if (!sess_blob) {
+			RWDEBUG("(TLS) Session serialisation failed, couldn't allocate buffer (%d bytes)", blob_len);
+			return 0;
+		}
+		/* openssl mutates &p */
+		p = sess_blob;
+		rv = i2d_SSL_SESSION(sess, &p);
+		if (rv != blob_len) {
+			if (request) RWDEBUG("(TLS) Session serialisation failed");
+			goto error;
+		}
+
+		/* open output file */
+		snprintf(filename, sizeof(filename), "%s%c%s.asn1",
+			 conf->session_cache_path, FR_DIR_SEP, buffer);
+		fd = open(filename, O_RDWR|O_CREAT|O_EXCL, S_IWUSR);
+		if (fd < 0) {
+			if (request) RERROR("(TLS) Session serialisation failed, failed opening session file %s: %s",
+					    filename, fr_syserror(errno));
+			goto error;
+		}
+
+		/*
+		 *	Set the filename to be temporarily write-only.
+		 */
+		if (request) {
+			VALUE_PAIR *vp;
+
+			vp = fr_pair_afrom_num(request->state_ctx, PW_TLS_CACHE_FILENAME, 0);
+			if (vp) {
+				fr_pair_value_strcpy(vp, filename);
+				fr_pair_add(&request->state, vp);
+			}
+		}
+
+		todo = blob_len;
+		p = sess_blob;
+		while (todo > 0) {
+			rv = write(fd, p, todo);
+			if (rv < 1) {
+				if (request) RWDEBUG("(TLS) Failed writing session: %s", fr_syserror(errno));
+				close(fd);
+				goto error;
+			}
+			p += rv;
+			todo -= rv;
+		}
+		close(fd);
+		if (request) RWDEBUG("(TLS) Wrote session %s to %s (%d bytes)", buffer, filename, blob_len);
+	}
+
+error:
+	free(sess_blob);
+
+	return 0;
+}
+
+/** Convert OpenSSL's ASN1_TIME to an epoch time
+ *
+ * @param[out] out	Where to write the time_t.
+ * @param[in] asn1	The ASN1_TIME to convert.
+ * @return
+ *	- 0 success.
+ *	- -1 on failure.
+ */
+static int ocsp_asn1time_to_epoch(time_t *out, char const *asn1)
+{
+	struct		tm t;
+	char const	*p = asn1, *end = p + strlen(p);
+
+	memset(&t, 0, sizeof(t));
+
+	if ((end - p) <= 13) {
+		if ((end - p) < 2) {
+			fr_strerror_printf("ASN1 date string too short, expected 2 additional bytes, got %zu bytes",
+					   end - p);
+			return -1;
+		}
+
+		t.tm_year = (*(p++) - '0') * 10;
+		t.tm_year += (*(p++) - '0');
+		if (t.tm_year < 70) t.tm_year += 100;
+	} else {
+		t.tm_year = (*(p++) - '0') * 1000;
+		t.tm_year += (*(p++) - '0') * 100;
+		t.tm_year += (*(p++) - '0') * 10;
+		t.tm_year += (*(p++) - '0');
+		t.tm_year -= 1900;
+	}
+
+	if ((end - p) < 4) {
+		fr_strerror_printf("ASN1 string too short, expected 10 additional bytes, got %zu bytes",
+				   end - p);
+		return -1;
+	}
+
+	t.tm_mon = (*(p++) - '0') * 10;
+	t.tm_mon += (*(p++) - '0') - 1; // -1 since January is 0 not 1.
+	t.tm_mday = (*(p++) - '0') * 10;
+	t.tm_mday += (*(p++) - '0');
+
+	if ((end - p) < 2) goto done;
+	t.tm_hour = (*(p++) - '0') * 10;
+	t.tm_hour += (*(p++) - '0');
+
+	if ((end - p) < 2) goto done;
+	t.tm_min = (*(p++) - '0') * 10;
+	t.tm_min += (*(p++) - '0');
+
+	if ((end - p) < 2) goto done;
+	t.tm_sec = (*(p++) - '0') * 10;
+	t.tm_sec += (*(p++) - '0');
+
+	/* Apparently OpenSSL converts all timestamps to UTC? Maybe? */
+done:
+	*out = timegm(&t);
+	return 0;
+}
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER)
+static SSL_SESSION *cbtls_get_session(SSL *ssl, unsigned char *data, int len, int *copy)
+#else
+static SSL_SESSION *cbtls_get_session(SSL *ssl, const unsigned char *data, int len, int *copy)
+#endif
+{
+	size_t			size;
+	char			buffer[2 * MAX_SESSION_SIZE + 1];
+	fr_tls_server_conf_t	*conf;
+	TALLOC_CTX		*talloc_ctx;
+
+	SSL_SESSION		*sess = NULL;
+	unsigned char		*sess_data = NULL;
+	PAIR_LIST		*pairlist = NULL;
+
+	REQUEST			*request = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST);
+
+	rad_assert(request != NULL);
+
+	size = len;
+	if (size > MAX_SESSION_SIZE) size = MAX_SESSION_SIZE;
+
+	fr_bin2hex(buffer, data, size);
+
+	RDEBUG2("Peer requested cached session: %s", buffer);
+
+	*copy = 0;
+
+	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF);
+	if (!conf) {
+		RWDEBUG("(TLS) Failed to find TLS configuration in session");
+		return NULL;
+	}
+
+	talloc_ctx = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_TALLOC);
+
+	{
+		int		rv, fd, todo;
+		char		filename[3 * MAX_SESSION_SIZE + 1];
+
+		unsigned char const	**o;
+		unsigned char		**p;
+		uint8_t			*q;
+
+		struct stat	st;
+		VALUE_PAIR	*vps = NULL;
+		VALUE_PAIR	*vp;
+
+		/* load the actual SSL session */
+		snprintf(filename, sizeof(filename), "%s%c%s.asn1", conf->session_cache_path, FR_DIR_SEP, buffer);
+		fd = open(filename, O_RDONLY);
+		if (fd < 0) {
+			RWDEBUG("(TLS) No persisted session file %s: %s", filename, fr_syserror(errno));
+			goto error;
+		}
+
+		rv = fstat(fd, &st);
+		if (rv < 0) {
+			RWDEBUG("(TLS) Failed stating persisted session file %s: %s", filename, fr_syserror(errno));
+			close(fd);
+			goto error;
+		}
+
+		sess_data = talloc_array(NULL, unsigned char, st.st_size);
+		if (!sess_data) {
+			RWDEBUG("(TLS) Failed allocating buffer for persisted session (%d bytes)", (int) st.st_size);
+			close(fd);
+			goto error;
+		}
+
+		q = sess_data;
+		todo = st.st_size;
+		while (todo > 0) {
+			rv = read(fd, q, todo);
+			if (rv < 1) {
+				RWDEBUG("(TLS) Failed reading persisted session: %s", fr_syserror(errno));
+				close(fd);
+				goto error;
+			}
+			todo -= rv;
+			q += rv;
+		}
+		close(fd);
+
+		/*
+		 *	OpenSSL mutates what's passed in, so we assign sess_data to q,
+		 *	so the value of q gets mutated, and not the value of sess_data.
+		 *
+		 *	We then need a pointer to hold &q, but it can't be const, because
+		 *	clang complains about lack of consting in nested pointer types.
+		 *
+		 *	So we memcpy the value of that pointer, to one that
+		 *	does have a const, which we then pass into d2i_SSL_SESSION *sigh*.
+		 */
+		q = sess_data;
+		p = &q;
+		memcpy(&o, &p, sizeof(o));
+		sess = d2i_SSL_SESSION(NULL, o, st.st_size);
+		if (!sess) {
+			RWDEBUG("(TLS) Failed loading persisted session: %s", ERR_error_string(ERR_get_error(), NULL));
+			goto error;
+		}
+
+		/* read in the cached VPs from the .vps file */
+		snprintf(filename, sizeof(filename), "%s%c%s.vps",
+			 conf->session_cache_path, FR_DIR_SEP, buffer);
+		rv = pairlist_read(talloc_ctx, filename, &pairlist, 1);
+		if (rv < 0) {
+			/* not safe to un-persist a session w/o VPs */
+			RWDEBUG("(TLS) Failed loading persisted VPs for session %s", buffer);
+			SSL_SESSION_free(sess);
+			sess = NULL;
+			goto error;
+		}
+
+		/*
+		 *	Enforce client certificate expiration.
+		 */
+		vp = fr_pair_find_by_num(pairlist->reply, PW_TLS_CLIENT_CERT_EXPIRATION, 0, TAG_ANY);
+		if (vp) {
+			time_t expires;
+
+			if (ocsp_asn1time_to_epoch(&expires, vp->vp_strvalue) < 0) {
+				RDEBUG2("Failed getting certificate expiration, removing cache entry for session %s - %s", buffer, fr_strerror());
+				SSL_SESSION_free(sess);
+				sess = NULL;
+				goto error;
+			}
+
+			if (expires <= request->timestamp) {
+				RDEBUG2("Certificate has expired, removing cache entry for session %s", buffer);
+				SSL_SESSION_free(sess);
+				sess = NULL;
+				goto error;
+			}
+
+			/*
+			 *	Account for Session-Timeout, if it's available.
+			 */
+			vp = fr_pair_find_by_num(request->reply->vps, PW_SESSION_TIMEOUT, 0, TAG_ANY);
+			if (vp) {
+				if ((request->timestamp + vp->vp_integer) > expires) {
+					vp->vp_integer = expires - request->timestamp;
+					RWDEBUG2("(TLS) Updating Session-Timeout to %u, due to impending certificate expiration",
+						 vp->vp_integer);
+				}
+			}
+		}
+
+		/*
+		 *	Resumption MUST use the same EAP type as from
+		 *	the original packet.
+		 */
+		vp = fr_pair_find_by_num(pairlist->reply, PW_EAP_TYPE, 0, TAG_ANY);
+		if (vp) {
+			VALUE_PAIR *type = fr_pair_find_by_num(request->packet->vps, PW_EAP_TYPE, 0, TAG_ANY);
+
+			if (type && (type->vp_integer != vp->vp_integer)) {
+				REDEBUG("Resumption has changed EAP types for session %s", buffer);
+				REDEBUG("Rejecting session due to protocol violations");
+				goto error;
+			}
+		}
+
+		/* move the cached VPs into the session */
+		fr_pair_list_mcopy_by_num(talloc_ctx, &vps, &pairlist->reply, 0, 0, TAG_ANY);
+
+		SSL_SESSION_set_ex_data(sess, fr_tls_ex_index_vps, vps);
+		RDEBUG("Successfully restored session %s", buffer);
+		rdebug_pair_list(L_DBG_LVL_2, request, vps, "reply:");
+
+		/*
+		 *	The "restore VPs from OpenSSL cache" code is
+		 *	now in eaptls_process()
+		 */
+	}
+error:
+	if (sess_data) talloc_free(sess_data);
+	if (pairlist) pairlist_free(&pairlist);
+
+	return sess;
+}
+
+static size_t tls_session_id_binary(SSL_SESSION *ssn, uint8_t *buffer, size_t bufsize)
+{
+#if OPENSSL_VERSION_NUMBER < 0x10001000L
+	size_t size;
+
+	size = ssn->session_id_length;
+	if (size > bufsize) size = bufsize;
+
+	memcpy(buffer, ssn->session_id, size);
+	return size;
+#else
+	unsigned int size;
+	uint8_t const *p;
+
+	p = SSL_SESSION_get_id(ssn, &size);
+	if (size > bufsize) size = bufsize;
+
+	memcpy(buffer, p, size);
+	return size;
+#endif
+}
+
+/*
+ *	From TLS-Cache-Method
+ *
+ *	All of the save / clear / load callbacks are done with any
+ *	OpenSSL locks *unlocked*.  So says the OpenSSL code.
+ */
+#define CACHE_SAVE (1)
+#define CACHE_LOAD (2)
+#define CACHE_CLEAR (3)
+#define CACHE_REFRESH (4)
+
+static REQUEST *cache_init_fake_request(fr_tls_server_conf_t const *conf, SSL_SESSION *sess, SSL *ssl,
+					uint8_t const *data, size_t size)
+{
+	VALUE_PAIR		*vp;
+	REQUEST			*fake, *request = NULL;
+	uint8_t			buffer[MAX_SESSION_SIZE];
+
+	if (sess) {
+		size = tls_session_id_binary(sess, buffer, sizeof(buffer));
+		data = buffer;
+	}
+
+	/*
+	 *	We get called essentially at random by OpenSSL, with
+	 *	no information other than the session ID.  As a
+	 *	result, we have to manually set up our own request.
+	 */
+	if (ssl) request = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST);
+
+	if (request) {
+		fake = request_alloc_fake(request);
+	} else {
+		fake = request_alloc(NULL);
+		fake->packet = rad_alloc(fake, false);
+		fake->reply = rad_alloc(fake, false);
+	}
+
+	vp = fr_pair_afrom_num(fake->packet, PW_TLS_SESSION_ID, 0);
+	if (!vp) {
+		talloc_free(fake);
+		return NULL;
+	}
+
+	fr_pair_value_memcpy(vp, data, size);
+	fr_pair_add(&fake->packet->vps, vp);
+
+	fake->server = conf->session_cache_server;
+
+	return fake;
+}
+
+/*
+ *	Clear cached data
+ */
+static void cbtls_cache_clear(SSL_CTX *ctx, SSL_SESSION *sess)
+{
+	fr_tls_server_conf_t	*conf;
+	REQUEST			*fake;
+
+	conf = (fr_tls_server_conf_t *)SSL_CTX_get_app_data(ctx);
+	if (!conf) {
+		DEBUG(LOG_PREFIX ": Failed to find TLS configuration in session");
+		return;
+	}
+
+	/*
+	 *	Find the SSL ID from the session, and delete it.
+	 *
+	 *	Don't bother with any parent request.  We're in a
+	 *	timer callback, and there is no request available.
+	 */
+	fake = cache_init_fake_request(conf, sess, NULL, NULL, 0);
+	if (!fake) return;
+
+	/*
+	 *	Use &request:TLS-Session-Id to clear the cache entry.
+	 */
+	(void) process_post_auth(CACHE_CLEAR, fake);
+	talloc_free(fake);
+	return;
+}
+
+/*
+ *	OpenSSL calls this function in order to save the session
+ *	BEFORE it has sent the final TLS success.  So our process here
+ *	is to say "yes, we saved it", and then do the *actual* saving
+ *	after the TLS success has been sent.
+ */
+static int cbtls_cache_save(UNUSED SSL *ssl, UNUSED SSL_SESSION *sess)
+{
+	return 0;
+}
+
+static int cbtls_cache_save_vps(SSL *ssl, SSL_SESSION *sess, VALUE_PAIR *vps)
+{
+	fr_tls_server_conf_t	*conf;
+	VALUE_PAIR		*vp;
+	REQUEST			*fake = NULL;
+	size_t			size, rv;
+	uint8_t			*p, *sess_blob = NULL;
+
+	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF);
+	if (!conf) return 0;
+
+	/*
+	 *	Find the SSL ID from the session, and save it.
+	 *
+	 *	Save anything from the parent request.
+	 */
+	fake = cache_init_fake_request(conf, sess, ssl, NULL, 0);
+	if (!fake) return 0;
+
+	/* find out what length data we need */
+	size = i2d_SSL_SESSION(sess, NULL);
+	if (size < 1) return 0;
+
+	/* Do not convert to TALLOC - it's passed to OpenSSL */
+	/* alloc and convert to ASN.1 */
+	MEM(sess_blob = malloc(size));
+
+	/* openssl mutates &p */
+	p = sess_blob;
+	rv = i2d_SSL_SESSION(sess, &p);
+	if (rv != size) goto error;
+
+	vp = fr_pair_afrom_num(fake->state_ctx, PW_TLS_SESSION_DATA, 0);
+	if (!vp) goto error;
+
+	fr_pair_value_memcpy(vp, sess_blob, size);
+	fr_pair_add(&fake->state, vp);
+
+	if (vps) fr_pair_add(&fake->reply->vps, fr_pair_list_copy(fake->reply, vps));
+
+	/*
+	 *	Use &request:TLS-Session-Id to save the
+	 *	&session-state:TLS-Session-Data values.
+	 *
+	 *	The current &reply: list is the list of VPs which
+	 *	should be cached.
+	 *
+	 *	Any other attributes which need to be saved can be
+	 *	read from the &outer.reply: list.
+	 */
+	(void) process_post_auth(CACHE_SAVE, fake);
+
+error:
+	if (fake) talloc_free(fake);
+	free(sess_blob);
+
+	return 0;
+}
+
+static int cbtls_cache_refresh(SSL *ssl, SSL_SESSION *sess)
+{
+	fr_tls_server_conf_t	*conf;
+	REQUEST			*fake = NULL;
+
+	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF);
+	if (!conf) return 0;
+
+	/*
+	 *	Find the SSL ID from the session, and save it.
+	 *
+	 *	Save anything from the parent request.
+	 */
+	fake = cache_init_fake_request(conf, sess, ssl, NULL, 0);
+	if (!fake) return 0;
+	/*
+	 *	Use &request:TLS-Session-Id to update the cache
+	 *	entry so that it doesn't not expire.
+	 */
+	(void) process_post_auth(CACHE_REFRESH, fake);
+
+	talloc_free(fake);
+
+	return 0;
+}
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER)
+static SSL_SESSION *cbtls_cache_load(SSL *ssl, unsigned char *data, int len, int *copy)
+#else
+static SSL_SESSION *cbtls_cache_load(SSL *ssl, const unsigned char *data, int len, int *copy)
+#endif
+{
+	fr_tls_server_conf_t	*conf;
+	size_t			size;
+	uint8_t const  		*p;
+	VALUE_PAIR		*vp, *vps;
+	TALLOC_CTX		*talloc_ctx;
+	SSL_SESSION		*sess = NULL;
+	REQUEST			*fake = NULL;
+	REQUEST			*request = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST);
+	char			buffer[2 * MAX_SESSION_SIZE + 1];
+
+	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF);
+	if (!conf) return NULL;
+
+	rad_assert(request);
+
+	size = len;
+	if (size > MAX_SESSION_SIZE) size = MAX_SESSION_SIZE;
+
+	if (fr_debug_lvl > 1) {
+		fr_bin2hex(buffer, data, size);
+		RDEBUG2("Peer requested cached session: %s", buffer);
+	}
+
+	*copy = 0;
+
+	/*
+	 *	Take the given SSL ID, and create a fake request.
+	 *
+	 *	Don't bother parenting it from another request.  We do
+	 *	this for a number of reasons.
+	 *
+	 *	One is that rest of the code expects that the VPs will
+	 *	be added to fr_tls_ex_index_vps.  So we don't want to
+	 *	be poking the request directly, as that will result in
+	 *	a change of behavior.
+	 *
+	 *	The larger reason is that we do _not_ want to actually
+	 *	update the reply, until such time as we know that the
+	 *	user has been authenticated.
+	 */
+	fake = cache_init_fake_request(conf, NULL, NULL, data, size);
+	if (!fake) return 0;
+
+	/*
+	 *	Use &request:TLS-Session-Id to load the cached
+	 *	session.
+	 *
+	 *	The "cache load { ...}" section should put the reply
+	 *	attributes into the &reply: list, and the
+	 *	&session-state:TLS-Session-Data attribute.
+	 *
+	 *	Why?  Because v4 does it that way, and there aren't
+	 *	really good reasons for doing it differently.
+	 */
+	(void) process_post_auth(CACHE_LOAD, fake);
+
+	/*
+	 *	Enforce client certificate expiration.
+	 */
+	vp = fr_pair_find_by_num(fake->reply->vps, PW_TLS_CLIENT_CERT_EXPIRATION, 0, TAG_ANY);
+	if (vp) {
+		time_t expires;
+
+		if (ocsp_asn1time_to_epoch(&expires, vp->vp_strvalue) < 0) {
+			RDEBUG2("Failed getting certificate expiration, removing cache entry for session %s - %s", buffer, fr_strerror());
+			SSL_SESSION_free(sess);
+			sess = NULL;
+			goto error;
+		}
+
+		if (expires <= request->timestamp) {
+			RDEBUG2("Certificate has expired, removing cache entry for session %s", buffer);
+			SSL_SESSION_free(sess);
+			sess = NULL;
+			goto error;
+		}
+
+		/*
+		 *	Account for Session-Timeout, if it's available.
+		 */
+		vp = fr_pair_find_by_num(request->reply->vps, PW_SESSION_TIMEOUT, 0, TAG_ANY);
+		if (vp) {
+			if ((request->timestamp + vp->vp_integer) > expires) {
+				vp->vp_integer = expires - request->timestamp;
+				RWDEBUG2("(TLS) Updating Session-Timeout to %u, due to impending certificate expiration",
+					 vp->vp_integer);
+			}
+		}
+	}
+
+	/*
+	 *	Try to de-serialize the session data.
+	 */
+	vp = fr_pair_find_by_num(fake->state, PW_TLS_SESSION_DATA, 0, TAG_ANY);
+	if (!vp) {
+		RWDEBUG("(TLS) Failed to find TLS-Session-Data in 'session-state' list for session %s", buffer);
+		goto error;
+	}
+
+	/*
+	 *	OpenSSL mutates what's passed in, so we assign sess_data to q,
+	 *	so the value of q gets mutated, and not the value of sess_data.
+	 *
+	 *	We then need a pointer to hold &q, but it can't be const, because
+	 *	clang complains about lack of consting in nested pointer types.
+	 *
+	 *	So we memcpy the value of that pointer, to one that
+	 *	does have a const, which we then pass into d2i_SSL_SESSION *sigh*.
+	 */
+	p = vp->vp_octets;
+	sess = d2i_SSL_SESSION(NULL, &p, vp->vp_length);
+	if (!sess) {
+		RWDEBUG("(TLS) Failed loading persisted session: %s", ERR_error_string(ERR_get_error(), NULL));
+		goto error;
+	}
+
+	talloc_ctx = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_TALLOC);
+	vps = NULL;
+
+	/* move the cached VPs into the session */
+	fr_pair_list_mcopy_by_num(talloc_ctx, &vps, &fake->reply->vps, 0, 0, TAG_ANY);
+
+	SSL_SESSION_set_ex_data(sess, fr_tls_ex_index_vps, vps);
+	RDEBUG("Successfully restored session %s", buffer);
+	rdebug_pair_list(L_DBG_LVL_2, request, vps, "reply:");
+
+	/*
+	 *	The "restore VPs from OpenSSL cache" code is
+	 *	now in eaptls_process()
+	 */
+
+error:
+	if (fake) talloc_free(fake);
+
+	return sess;
+}
+
+#ifdef HAVE_OPENSSL_OCSP_H
+
+/** Extract components of OCSP responser URL from a certificate
+ *
+ * @param[in] cert to extract URL from.
+ * @param[out] host_out Portion of the URL (must be freed with free()).
+ * @param[out] port_out Port portion of the URL (must be freed with free()).
+ * @param[out] path_out Path portion of the URL (must be freed with free()).
+ * @param[out] is_https Whether the responder should be contacted using https.
+ * @return
+ *	- 0 if no valid URL is contained in the certificate.
+ *	- 1 if a URL was found and parsed.
+ *	- -1 if at least one URL was found, but none could be parsed.
+ */
+static int ocsp_parse_cert_url(X509 *cert, char **host_out, char **port_out,
+			       char **path_out, int *is_https)
+{
+	int			i;
+	bool			found_uri = false;
+
+	AUTHORITY_INFO_ACCESS	*aia;
+	ACCESS_DESCRIPTION	*ad;
+
+	aia = X509_get_ext_d2i(cert, NID_info_access, NULL, NULL);
+
+	for (i = 0; i < sk_ACCESS_DESCRIPTION_num(aia); i++) {
+		ad = sk_ACCESS_DESCRIPTION_value(aia, i);
+		if (OBJ_obj2nid(ad->method) != NID_ad_OCSP) continue;
+		if (ad->location->type != GEN_URI) continue;
+		found_uri = true;
+
+		if (OCSP_parse_url((char *) ad->location->d.ia5->data, host_out,
+				   port_out, path_out, is_https)) return 1;
+	}
+	return found_uri ? -1 : 0;
+}
+
+/*
+ * This function sends a OCSP request to a defined OCSP responder
+ * and checks the OCSP response for correctness.
+ */
+
+/* Maximum leeway in validity period: default 5 minutes */
+#define MAX_VALIDITY_PERIOD     (5 * 60)
+
+typedef enum {
+	OCSP_STATUS_FAILED	= 0,
+	OCSP_STATUS_OK		= 1,
+	OCSP_STATUS_SKIPPED	= 2,
+} ocsp_status_t;
+
+static ocsp_status_t ocsp_check(REQUEST *request, X509_STORE *store, X509 *issuer_cert, X509 *client_cert,
+				fr_tls_server_conf_t *conf)
+{
+	OCSP_CERTID	*certid;
+	OCSP_REQUEST	*req;
+	OCSP_RESPONSE	*resp = NULL;
+	OCSP_BASICRESP	*bresp = NULL;
+	char		*host = NULL;
+	char		*port = NULL;
+	char		*path = NULL;
+	char		hostheader[1024];
+	int		use_ssl = -1;
+	long		nsec = MAX_VALIDITY_PERIOD, maxage = -1;
+	BIO		*cbio, *bio_out;
+	ocsp_status_t	ocsp_status = OCSP_STATUS_FAILED;
+	int		status;
+	ASN1_GENERALIZEDTIME *rev = NULL, *thisupd, *nextupd;
+	int		reason;
+#if OPENSSL_VERSION_NUMBER >= 0x1000003f
+	OCSP_REQ_CTX	*ctx;
+	int		rc;
+	struct timeval	now;
+	struct timeval	when;
+#endif
+	VALUE_PAIR	*vp;
+
+	if (issuer_cert == NULL) {
+		RWDEBUG("(TLS) Could not get issuer certificate");
+		goto skipped;
+	}
+
+	/*
+	 * Create OCSP Request
+	 */
+	certid = OCSP_cert_to_id(NULL, client_cert, issuer_cert);
+	req = OCSP_REQUEST_new();
+	OCSP_request_add0_id(req, certid);
+	if (conf->ocsp_use_nonce) OCSP_request_add1_nonce(req, NULL, 8);
+
+	/*
+	 * Send OCSP Request and get OCSP Response
+	 */
+
+	/* Get OCSP responder URL */
+	if (conf->ocsp_override_url) {
+		char *url;
+
+	use_ocsp_url:
+		memcpy(&url, &conf->ocsp_url, sizeof(url));
+		/* Reading the libssl src, they do a strdup on the URL, so it could of been const *sigh* */
+		OCSP_parse_url(url, &host, &port, &path, &use_ssl);
+		if (!host || !port || !path) {
+			RWDEBUG("(TLS) ocsp: Host or port or path missing from configured URL \"%s\".  Not doing OCSP", url);
+			goto skipped;
+		}
+	} else {
+		int ret;
+
+		ret = ocsp_parse_cert_url(client_cert, &host, &port, &path, &use_ssl);
+		switch (ret) {
+		case -1:
+			RWDEBUG("(TLS) ocsp: Invalid URL in certificate.  Not doing OCSP");
+			break;
+
+		case 0:
+			if (conf->ocsp_url) {
+				RWDEBUG("(TLS) ocsp: No OCSP URL in certificate, falling back to configured URL");
+				goto use_ocsp_url;
+			}
+			RWDEBUG("(TLS) ocsp: No OCSP URL in certificate.  Not doing OCSP");
+			goto skipped;
+
+		case 1:
+			break;
+		}
+	}
+
+	RDEBUG2("ocsp: Using responder URL \"http://%s:%s%s\"", host, port, path);
+
+	/* Check host and port length are sane, then create Host: HTTP header */
+	if ((strlen(host) + strlen(port) + 2) > sizeof(hostheader)) {
+		RWDEBUG("(TLS) ocsp: Host and port too long");
+		goto skipped;
+	}
+	snprintf(hostheader, sizeof(hostheader), "%s:%s", host, port);
+
+	/* Setup BIO socket to OCSP responder */
+	cbio = BIO_new_connect(host);
+
+	bio_out = NULL;
+	if (rad_debug_lvl) {
+		if (default_log.dst == L_DST_STDOUT) {
+			bio_out = BIO_new_fp(stdout, BIO_NOCLOSE);
+		} else if (default_log.dst == L_DST_STDERR) {
+			bio_out = BIO_new_fp(stderr, BIO_NOCLOSE);
+		}
+	}
+
+	BIO_set_conn_port(cbio, port);
+#if OPENSSL_VERSION_NUMBER < 0x1000003f
+	BIO_do_connect(cbio);
+
+	/* Send OCSP request and wait for response */
+	resp = OCSP_sendreq_bio(cbio, path, req);
+	if (!resp) {
+		REDEBUG("ocsp: Couldn't get OCSP response");
+		ocsp_status = OCSP_STATUS_SKIPPED;
+		goto ocsp_end;
+	}
+#else
+	if (conf->ocsp_timeout)
+		BIO_set_nbio(cbio, 1);
+
+	rc = BIO_do_connect(cbio);
+	if ((rc <= 0) && ((!conf->ocsp_timeout) || !BIO_should_retry(cbio))) {
+		REDEBUG("ocsp: Couldn't connect to OCSP responder");
+		ocsp_status = OCSP_STATUS_SKIPPED;
+		goto ocsp_end;
+	}
+
+	ctx = OCSP_sendreq_new(cbio, path, NULL, -1);
+	if (!ctx) {
+		REDEBUG("ocsp: Couldn't create OCSP request");
+		ocsp_status = OCSP_STATUS_SKIPPED;
+		goto ocsp_end;
+	}
+
+	if (!OCSP_REQ_CTX_add1_header(ctx, "Host", hostheader)) {
+		REDEBUG("ocsp: Couldn't set Host header");
+		ocsp_status = OCSP_STATUS_SKIPPED;
+		goto ocsp_end;
+	}
+
+	if (!OCSP_REQ_CTX_set1_req(ctx, req)) {
+		REDEBUG("ocsp: Couldn't add data to OCSP request");
+		ocsp_status = OCSP_STATUS_SKIPPED;
+		goto ocsp_end;
+	}
+
+	gettimeofday(&when, NULL);
+	when.tv_sec += conf->ocsp_timeout;
+
+	do {
+		rc = OCSP_sendreq_nbio(&resp, ctx);
+		if (conf->ocsp_timeout) {
+			gettimeofday(&now, NULL);
+			if (!timercmp(&now, &when, <))
+				break;
+		}
+	} while ((rc == -1) && BIO_should_retry(cbio));
+
+	if (conf->ocsp_timeout && (rc == -1) && BIO_should_retry(cbio)) {
+		REDEBUG("ocsp: Response timed out");
+		ocsp_status = OCSP_STATUS_SKIPPED;
+		goto ocsp_end;
+	}
+
+	OCSP_REQ_CTX_free(ctx);
+
+	if (rc == 0) {
+		REDEBUG("ocsp: Couldn't get OCSP response");
+		ocsp_status = OCSP_STATUS_SKIPPED;
+		goto ocsp_end;
+	}
+#endif
+
+	/* Verify OCSP response status */
+	status = OCSP_response_status(resp);
+	if (status != OCSP_RESPONSE_STATUS_SUCCESSFUL) {
+		REDEBUG("ocsp: Response status: %s", OCSP_response_status_str(status));
+		goto ocsp_end;
+	}
+	bresp = OCSP_response_get1_basic(resp);
+	if (!bresp) {
+		RDEBUG("ocsp: Failed parsing response");
+		goto ocsp_end;
+	}
+
+	if (conf->ocsp_use_nonce && OCSP_check_nonce(req, bresp)!=1) {
+		REDEBUG("ocsp: Response has wrong nonce value");
+		goto ocsp_end;
+	}
+	if (OCSP_basic_verify(bresp, NULL, store, 0)!=1){
+		REDEBUG("ocsp: Couldn't verify OCSP basic response");
+		goto ocsp_end;
+	}
+
+	/*	Verify OCSP cert status */
+	if (!OCSP_resp_find_status(bresp, certid, &status, &reason, &rev, &thisupd, &nextupd)) {
+		REDEBUG("ocsp: No Status found");
+		goto ocsp_end;
+	}
+
+	if (!OCSP_check_validity(thisupd, nextupd, nsec, maxage)) {
+		if (bio_out) {
+			BIO_puts(bio_out, "WARNING: Status times invalid.\n");
+			ERR_print_errors(bio_out);
+		}
+		goto ocsp_end;
+	}
+
+	if (bio_out) {
+		BIO_puts(bio_out, "\tThis Update: ");
+		ASN1_GENERALIZEDTIME_print(bio_out, thisupd);
+		BIO_puts(bio_out, "\n");
+		if (nextupd) {
+			BIO_puts(bio_out, "\tNext Update: ");
+			ASN1_GENERALIZEDTIME_print(bio_out, nextupd);
+			BIO_puts(bio_out, "\n");
+		}
+	}
+
+	switch (status) {
+	case V_OCSP_CERTSTATUS_GOOD:
+		RDEBUG2("ocsp: Cert status: good");
+		vp = pair_make_request("TLS-OCSP-Cert-Valid", NULL, T_OP_SET);
+		vp->vp_integer = 1;	/* yes */
+		ocsp_status = OCSP_STATUS_OK;
+		break;
+
+	default:
+		/* REVOKED / UNKNOWN */
+		REDEBUG("ocsp: Cert status: %s", OCSP_cert_status_str(status));
+		if (reason != -1) REDEBUG("ocsp: Reason: %s", OCSP_crl_reason_str(reason));
+
+		if (bio_out && rev) {
+			BIO_puts(bio_out, "\tRevocation Time: ");
+			ASN1_GENERALIZEDTIME_print(bio_out, rev);
+			BIO_puts(bio_out, "\n");
+		}
+		break;
+	}
+
+ocsp_end:
+	/* Free OCSP Stuff */
+	OCSP_REQUEST_free(req);
+	OCSP_RESPONSE_free(resp);
+	free(host);
+	free(port);
+	free(path);
+	BIO_free_all(cbio);
+	if (bio_out) BIO_free(bio_out);
+	OCSP_BASICRESP_free(bresp);
+
+	switch (ocsp_status) {
+	case OCSP_STATUS_OK:
+		RDEBUG2("ocsp: Certificate is valid");
+		break;
+
+	case OCSP_STATUS_SKIPPED:
+	skipped:
+		vp = pair_make_request("TLS-OCSP-Cert-Valid", NULL, T_OP_SET);
+		vp->vp_integer = 2;	/* skipped */
+		if (conf->ocsp_softfail) {
+			RWDEBUG("(TLS) ocsp: Unable to check certificate, assuming it's valid");
+			RWDEBUG("(TLS) ocsp: This may be insecure");
+
+			/* Remove OpenSSL errors from queue or handshake will fail */
+			while (ERR_get_error());
+
+			ocsp_status = OCSP_STATUS_SKIPPED;
+		} else {
+			REDEBUG("(TLS) ocsp: Unable to check certificate, failing");
+			ocsp_status = OCSP_STATUS_FAILED;
+		}
+		break;
+
+	default:
+		vp = pair_make_request("TLS-OCSP-Cert-Valid", NULL, T_OP_SET);
+		vp->vp_integer = 0;	/* no */
+		REDEBUG("(TLS) ocsp: Certificate has been expired/revoked");
+		break;
+	}
+
+	return ocsp_status;
+}
+#endif	/* HAVE_OPENSSL_OCSP_H */
+
+/*
+ *	For creating certificate attributes.
+ */
+static char const *cert_attr_names[9][2] = {
+	{ "TLS-Client-Cert-Serial",			"TLS-Cert-Serial" },
+	{ "TLS-Client-Cert-Expiration",			"TLS-Cert-Expiration" },
+	{ "TLS-Client-Cert-Subject",			"TLS-Cert-Subject" },
+	{ "TLS-Client-Cert-Issuer",			"TLS-Cert-Issuer" },
+	{ "TLS-Client-Cert-Common-Name",		"TLS-Cert-Common-Name" },
+	{ "TLS-Client-Cert-Subject-Alt-Name-Email",	"TLS-Cert-Subject-Alt-Name-Email" },
+	{ "TLS-Client-Cert-Subject-Alt-Name-Dns",	"TLS-Cert-Subject-Alt-Name-Dns" },
+	{ "TLS-Client-Cert-Subject-Alt-Name-Upn",	"TLS-Cert-Subject-Alt-Name-Upn" },
+	{ "TLS-Client-Cert-Valid-Since",		"TLS-Cert-Valid-Since" }
+};
+
+#define FR_TLS_SERIAL		(0)
+#define FR_TLS_EXPIRATION	(1)
+#define FR_TLS_SUBJECT		(2)
+#define FR_TLS_ISSUER		(3)
+#define FR_TLS_CN		(4)
+#define FR_TLS_SAN_EMAIL       	(5)
+#define FR_TLS_SAN_DNS          (6)
+#define FR_TLS_SAN_UPN          (7)
+#define FR_TLS_VALID_SINCE	(8)
+
+static const char *cert_names[2] = {
+	"client", "server",
+};
+
+/*
+ *	Before trusting a certificate, you must make sure that the
+ *	certificate is 'valid'. There are several steps that your
+ *	application can take in determining if a certificate is
+ *	valid. Commonly used steps are:
+ *
+ *	1.Verifying the certificate's signature, and verifying that
+ *	the certificate has been issued by a trusted Certificate
+ *	Authority.
+ *
+ *	2.Verifying that the certificate is valid for the present date
+ *	(i.e. it is being presented within its validity dates).
+ *
+ *	3.Verifying that the certificate has not been revoked by its
+ *	issuing Certificate Authority, by checking with respect to a
+ *	Certificate Revocation List (CRL).
+ *
+ *	4.Verifying that the credentials presented by the certificate
+ *	fulfill additional requirements specific to the application,
+ *	such as with respect to access control lists or with respect
+ *	to OCSP (Online Certificate Status Processing).
+ *
+ *	NOTE: This callback will be called multiple times based on the
+ *	depth of the root certificate chain
+ */
+int cbtls_verify(int ok, X509_STORE_CTX *ctx)
+{
+	char		subject[1024]; /* Used for the subject name */
+	char		issuer[1024]; /* Used for the issuer name */
+	char		attribute[1024];
+	char		value[1024];
+	char		common_name[1024];
+	char		cn_str[1024];
+	char		buf[64];
+	X509		*client_cert;
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER)
+	const STACK_OF(X509_EXTENSION) *ext_list;
+#else
+	STACK_OF(X509_EXTENSION) *ext_list;
+#endif
+	SSL		*ssl;
+	int		err, depth, lookup, loc;
+	fr_tls_server_conf_t *conf;
+	int		my_ok = ok;
+
+	ASN1_INTEGER	*sn = NULL;
+	ASN1_TIME	*asn_time = NULL;
+	VALUE_PAIR	**certs;
+	char **identity;
+#ifdef HAVE_OPENSSL_OCSP_H
+	X509_STORE	*ocsp_store = NULL;
+	X509		*issuer_cert;
+	bool		do_verify = false;
+#endif
+	VALUE_PAIR	*vp;
+	TALLOC_CTX	*talloc_ctx;
+
+	REQUEST		*request;
+
+	client_cert = X509_STORE_CTX_get_current_cert(ctx);
+	err = X509_STORE_CTX_get_error(ctx);
+	depth = X509_STORE_CTX_get_error_depth(ctx);
+
+	lookup = depth;
+
+	/*
+	 * Retrieve the pointer to the SSL of the connection currently treated
+	 * and the application specific data stored into the SSL object.
+	 */
+	ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
+	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF);
+	if (!conf) return 1;
+
+	request = (REQUEST *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST);
+	rad_assert(request != NULL);
+	certs = (VALUE_PAIR **)SSL_get_ex_data(ssl, fr_tls_ex_index_certs);
+
+	identity = (char **)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_IDENTITY);
+#ifdef HAVE_OPENSSL_OCSP_H
+	ocsp_store = conf->ocsp_store;
+#endif
+
+	talloc_ctx = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_TALLOC);
+
+	/*
+	 *	Log client/issuing cert.  If there's an error, log
+	 *	issuing cert.
+	 *
+	 *	Inbound:   0 = client, 1 = server (intermediate CA), 2 = issuing CA
+	 *	Outbound:  0 = server, 2 = issuing CA.
+	 *
+	 *	Our array of certificates uses 0 for client, and 1 for server.  We
+	 *	also ignore subsequent certs.
+	 */
+	if (lookup > 1) {
+		if (!my_ok) lookup = 1;
+
+	} else if (lookup == 0) {
+		/*
+		 *	This flag is only set for outbound
+		 *	connections.  And then allows us to remap SSL
+		 *	offset 0 (server) to our offset 1 (also
+		 *	server).
+		 */
+		lookup = (SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_FIX_CERT_ORDER) != NULL);
+	}
+
+	/*
+	 *	Get the Serial Number
+	 */
+	buf[0] = '\0';
+	sn = X509_get_serialNumber(client_cert);
+
+	RDEBUG2("(TLS) Creating attributes from %s certificate", cert_names[lookup ]);
+ 	RINDENT();
+
+	/*
+	 *	For this next bit, we create the attributes *only* if
+	 *	we're at the client or issuing certificate.
+	 */
+	if (certs &&
+	    (lookup <= 1) && sn && ((size_t) sn->length < (sizeof(buf) / 2))) {
+		char *p = buf;
+		int i;
+
+		for (i = 0; i < sn->length; i++) {
+			sprintf(p, "%02x", (unsigned int)sn->data[i]);
+			p += 2;
+		}
+		vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_SERIAL][lookup], buf, T_OP_SET);
+		rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+	}
+
+	/*
+	 *	Get the Expiration Date
+	 */
+	buf[0] = '\0';
+	asn_time = X509_get_notAfter(client_cert);
+	if (certs && (lookup <= 1) && asn_time &&
+	    (asn_time->length < (int) sizeof(buf))) {
+		memcpy(buf, (char*) asn_time->data, asn_time->length);
+		buf[asn_time->length] = '\0';
+		vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_EXPIRATION][lookup], buf, T_OP_SET);
+		rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+	}
+
+	/*
+	 *	Get the Valid Since Date
+	 */
+	buf[0] = '\0';
+	asn_time = X509_get_notBefore(client_cert);
+	if (certs && (lookup <= 1) && asn_time &&
+	    (asn_time->length < (int) sizeof(buf))) {
+		memcpy(buf, (char*) asn_time->data, asn_time->length);
+		buf[asn_time->length] = '\0';
+		vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_VALID_SINCE][lookup], buf, T_OP_SET);
+		rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+	}
+
+	/*
+	 *	Get the Subject & Issuer
+	 */
+	subject[0] = issuer[0] = '\0';
+	X509_NAME_oneline(X509_get_subject_name(client_cert), subject,
+			  sizeof(subject));
+	subject[sizeof(subject) - 1] = '\0';
+	if (certs && (lookup <= 1) && subject[0]) {
+		vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_SUBJECT][lookup], subject, T_OP_SET);
+		rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+	}
+
+	X509_NAME_oneline(X509_get_issuer_name(client_cert), issuer,
+			  sizeof(issuer));
+	issuer[sizeof(issuer) - 1] = '\0';
+	if (certs && (lookup <= 1) && issuer[0]) {
+		vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_ISSUER][lookup], issuer, T_OP_SET);
+		rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+	}
+
+	/*
+	 *	Get the Common Name, if there is a subject.
+	 */
+	X509_NAME_get_text_by_NID(X509_get_subject_name(client_cert),
+				  NID_commonName, common_name, sizeof(common_name));
+	common_name[sizeof(common_name) - 1] = '\0';
+	if (certs && (lookup <= 1) && common_name[0] && subject[0]) {
+		vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_CN][lookup], common_name, T_OP_SET);
+		rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+	}
+
+	/*
+	 *	Get the RFC822 Subject Alternative Name
+	 */
+	loc = X509_get_ext_by_NID(client_cert, NID_subject_alt_name, -1);
+	if (certs && (lookup <= 1) && (loc >= 0)) {
+		X509_EXTENSION *ext = NULL;
+		GENERAL_NAMES *names = NULL;
+		int i;
+
+		if ((ext = X509_get_ext(client_cert, loc)) &&
+		    (names = X509V3_EXT_d2i(ext))) {
+			for (i = 0; i < sk_GENERAL_NAME_num(names); i++) {
+				GENERAL_NAME *name = sk_GENERAL_NAME_value(names, i);
+
+				switch (name->type) {
+#ifdef GEN_EMAIL
+				case GEN_EMAIL:
+					vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_SAN_EMAIL][lookup],
+						      (char const *) ASN1_STRING_get0_data(name->d.rfc822Name), T_OP_SET);
+					rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+					break;
+#endif	/* GEN_EMAIL */
+#ifdef GEN_DNS
+				case GEN_DNS:
+					vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_SAN_DNS][lookup],
+						      (char const *) ASN1_STRING_get0_data(name->d.dNSName), T_OP_SET);
+					rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+					break;
+#endif	/* GEN_DNS */
+#ifdef GEN_OTHERNAME
+				case GEN_OTHERNAME:
+					/* look for a MS UPN */
+					if (NID_ms_upn == OBJ_obj2nid(name->d.otherName->type_id)) {
+					    /* we've got a UPN - Must be ASN1-encoded UTF8 string */
+					    if (name->d.otherName->value->type == V_ASN1_UTF8STRING) {
+						    vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_SAN_UPN][lookup],
+								  (char const *) ASN1_STRING_get0_data(name->d.otherName->value->value.utf8string), T_OP_SET);
+						    rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+						break;
+					    } else {
+						RWARN("Invalid UPN in Subject Alt Name (should be UTF-8)");
+						break;
+					    }
+					}
+					break;
+#endif	/* GEN_OTHERNAME */
+				default:
+					/* XXX TODO handle other SAN types */
+					break;
+				}
+			}
+		}
+		if (names != NULL)
+			GENERAL_NAMES_free(names);
+	}
+
+	/*
+	 *	If the CRL has expired, that might still be OK.
+	 */
+	if (!my_ok &&
+	    (conf->allow_expired_crl) &&
+	    (err == X509_V_ERR_CRL_HAS_EXPIRED)) {
+		my_ok = 1;
+		X509_STORE_CTX_set_error( ctx, 0 );
+	}
+
+	if (!my_ok) {
+		char const *p = X509_verify_cert_error_string(err);
+		RERROR("(TLS) OpenSSL says error %d : %s", err, p);
+		REXDENT();
+
+		/*
+		 *	Copy certs even on failure so that they can be logged.
+		 */
+		if (certs && request) fr_pair_add(&request->packet->vps, fr_pair_list_copy(request->packet, *certs));
+
+		return my_ok;
+	}
+
+	if (lookup == 0) {
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER)
+		ext_list = X509_get0_extensions(client_cert);
+#else
+		X509_CINF	*client_inf;
+		client_inf = client_cert->cert_info;
+		ext_list = client_inf->extensions;
+#endif
+	} else {
+		ext_list = NULL;
+	}
+
+	/*
+	 *	Grab the X509 extensions, and create attributes out of them.
+	 *	For laziness, we re-use the OpenSSL names
+	 */
+	if (certs && (sk_X509_EXTENSION_num(ext_list) > 0)) {
+		int i, len;
+		EXTENDED_KEY_USAGE *eku;
+		char *p;
+		BIO *out;
+
+		out = BIO_new(BIO_s_mem());
+		strlcpy(attribute, "TLS-Client-Cert-", sizeof(attribute));
+
+		for (i = 0; i < sk_X509_EXTENSION_num(ext_list); i++) {
+			ASN1_OBJECT *obj;
+			X509_EXTENSION *ext;
+
+			ext = sk_X509_EXTENSION_value(ext_list, i);
+
+			obj = X509_EXTENSION_get_object(ext);
+			i2a_ASN1_OBJECT(out, obj);
+			len = BIO_read(out, attribute + 16 , sizeof(attribute) - 16 - 1);
+			if (len <= 0) continue;
+
+			attribute[16 + len] = '\0';
+
+			for (p = attribute + 16; *p != '\0'; p++) {
+				if (*p == ' ') *p = '-';
+			}
+
+			if (X509V3_EXT_get(ext)) { /* Known extension, converting value into plain string */
+				X509V3_EXT_print(out, ext, 0, 0);
+				len = BIO_read(out, value, sizeof(value) - 1);
+				if (len <= 0) continue;
+				value[len] = '\0';
+			} else {
+				/*
+				 * An extension not known to OpenSSL, dump it's value as a value of an unknown attribute.
+				 */
+				value[0] = '0';
+				value[1] = 'x';
+				const unsigned char *srcp;
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER)
+				const ASN1_STRING *srcasn1p;
+				srcasn1p = X509_EXTENSION_get_data(ext);
+				srcp = ASN1_STRING_get0_data(srcasn1p);
+#else
+				ASN1_STRING *srcasn1p;
+				srcasn1p = X509_EXTENSION_get_data(ext);
+				srcp = ASN1_STRING_data(srcasn1p);
+#endif
+				int asn1len = ASN1_STRING_length(srcasn1p);
+				/* 3 comes from '0x' + \0 */
+				if ((size_t)(asn1len << 1) >= sizeof(value) - 3) {
+					RDEBUG("Value of '%s' attribute is too long to be stored, it will be truncated", attribute);
+					asn1len = (sizeof(value) - 3) >> 1;
+				}
+				fr_bin2hex(value + 2, srcp, asn1len);
+			}
+
+			vp = fr_pair_make(talloc_ctx, certs, attribute, value, T_OP_ADD);
+			if (!vp) {
+				RDEBUG3("Skipping %s += '%s'.  Please check that both the "
+					"attribute and value are defined in the dictionaries",
+					attribute, value);
+			} else {
+				/*
+				 *	rdebug_pair_list indents (so pre REXDENT())
+				 */
+				REXDENT();
+				rdebug_pair_list(L_DBG_LVL_2, request, vp, NULL);
+				RINDENT();
+			}
+		}
+
+		BIO_free_all(out);
+
+		/* Export raw EKU OIDs to allow matching a single OID regardless of its name */
+		eku = X509_get_ext_d2i(client_cert, NID_ext_key_usage, NULL, NULL);
+		if (eku != NULL) {
+			for (i = 0; i < sk_ASN1_OBJECT_num(eku); i++) {
+				len = OBJ_obj2txt(value, sizeof(value), sk_ASN1_OBJECT_value(eku, i), 1);
+				if ((len > 0) && ((unsigned) len < sizeof(value))) {
+					vp = fr_pair_make(talloc_ctx, certs,
+							  "TLS-Client-Cert-X509v3-Extended-Key-Usage-OID",
+							  value, T_OP_ADD);
+					rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+				}
+				else {
+					RDEBUG("Failed to get EKU OID at index %d", i);
+				}
+			}
+			EXTENDED_KEY_USAGE_free(eku);
+		}
+	}
+
+	REXDENT();
+
+	switch (X509_STORE_CTX_get_error(ctx)) {
+	case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
+		RERROR("(TLS) unable to get issuer certificate for issuer=%s", issuer);
+		break;
+
+	case X509_V_ERR_CERT_NOT_YET_VALID:
+		RERROR("(TLS) Failed with certificate not yet valid.");
+		break;
+
+	case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD:
+		RERROR("(TLS) Failed with error in certificate 'not before' field.");
+#if 0
+		ASN1_TIME_print(bio_err, X509_get_notBefore(ctx->current_cert));
+#endif
+		break;
+
+	case X509_V_ERR_CERT_HAS_EXPIRED:
+		RERROR("(TLS) Failed with certificate has expired.");
+		break;
+
+	case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD:
+		RERROR("(TLS) Failed with err in certificate 'no after' field..");
+		break;
+
+#if 0
+		ASN1_TIME_print(bio_err, X509_get_notAfter(ctx->current_cert));
+#endif
+		break;
+	}
+
+	/*
+	 *	If we're at the actual client cert, apply additional
+	 *	checks.
+	 */
+	if (depth == 0) {
+		tls_session_t *ssn = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_SSN);
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+		STACK_OF(X509)* untrusted = NULL;
+#endif
+
+		rad_assert(ssn != NULL);
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+		/*
+		 *	See if there are any untrusted certificates.
+		 *	If so, complain about them.
+		 */
+		untrusted = X509_STORE_CTX_get0_untrusted(ctx);
+		if (untrusted) {
+			if (conf->disallow_untrusted || RDEBUG_ENABLED2) {
+				int  i;
+
+				WARN("Certificate chain - %i cert(s) untrusted",
+				     X509_STORE_CTX_get_num_untrusted(ctx));
+				for (i = sk_X509_num(untrusted); i > 0 ; i--) {
+					X509 *this_cert = sk_X509_value(untrusted, i - 1);
+
+					X509_NAME_oneline(X509_get_subject_name(this_cert), subject, sizeof(subject));
+					subject[sizeof(subject) - 1] = '\0';
+
+					WARN("(TLS) untrusted certificate with depth [%i] subject name %s",
+					     i - 1, subject);
+				}
+			}
+
+			if (conf->disallow_untrusted) {
+				AUTH(LOG_PREFIX ": There are untrusted certificates in the certificate chain.  Rejecting.");
+				my_ok = 0;
+			}
+		}
+#endif
+
+		/*
+		 *	If the conf tells us to, check cert issuer
+		 *	against the specified value and fail
+		 *	verification if they don't match.
+		 */
+		if (my_ok && conf->check_cert_issuer &&
+		    (strcmp(issuer, conf->check_cert_issuer) != 0)) {
+			AUTH(LOG_PREFIX ": Certificate issuer (%s) does not match specified value (%s)!",
+			     issuer, conf->check_cert_issuer);
+			my_ok = 0;
+		}
+
+		/*
+		 *	If the conf tells us to, check the CN in the
+		 *	cert against xlat'ed value, but only if the
+		 *	previous checks passed.
+		 */
+		if (my_ok && conf->check_cert_cn) {
+			if (radius_xlat(cn_str, sizeof(cn_str), request, conf->check_cert_cn, NULL, NULL) < 0) {
+				/* if this fails, fail the verification */
+				my_ok = 0;
+			} else {
+				RDEBUG2("checking certificate CN (%s) with xlat'ed value (%s)", common_name, cn_str);
+				if (strcmp(cn_str, common_name) != 0) {
+					AUTH(LOG_PREFIX ": Certificate CN (%s) does not match specified value (%s)!",
+					     common_name, cn_str);
+					my_ok = 0;
+				}
+			}
+		} /* check_cert_cn */
+
+#ifdef HAVE_OPENSSL_OCSP_H
+		if (my_ok) {
+			/*
+			 *	No OCSP, allow external verification.
+			 */
+			if (!conf->ocsp_enable) {
+				do_verify = true;
+
+			} else {
+				RDEBUG2("Starting OCSP Request");
+
+				/*
+				 *	If we don't have an issuer, then we can't send
+				 *	and OCSP request, but pass the NULL issuer in
+				 *	so ocsp_check can decide on the correct
+				 *	return code.
+				 */
+				issuer_cert = X509_STORE_CTX_get0_current_issuer(ctx);
+
+				/*
+				 *	Do the full OCSP checks.
+				 *
+				 *	If they fail, don't run the external verify.  We don't want
+				 *	to allow admins to force authentication success for bad
+				 *	certificates.
+				 *
+				 *	If the OCSP checks succeed, check whether we still want to
+				 *	run the external verification routine.  If it's marked as
+				 *	"skip verify on OK", then we don't do verify.
+				 */
+				my_ok = ocsp_check(request, ocsp_store, issuer_cert, client_cert, conf);
+				if (my_ok != OCSP_STATUS_FAILED) {
+					do_verify = !conf->verify_skip_if_ocsp_ok;
+				}
+			}
+		}
+#endif
+
+		if ((my_ok != OCSP_STATUS_FAILED)
+#ifdef HAVE_OPENSSL_OCSP_H
+		    && do_verify
+#endif
+			) while (conf->verify_client_cert_cmd) {
+			char filename[3 * MAX_SESSION_SIZE + 1];
+			int fd;
+			FILE *fp;
+
+			snprintf(filename, sizeof(filename), "%s/%s.client.XXXXXXXX",
+				 conf->verify_tmp_dir, main_config.name);
+			fd = mkstemp(filename);
+			if (fd < 0) {
+				RDEBUG("Failed creating file in %s: %s",
+				       conf->verify_tmp_dir, fr_syserror(errno));
+				break;
+			}
+
+			fp = fdopen(fd, "w");
+			if (!fp) {
+				close(fd);
+				RDEBUG("Failed opening file %s: %s",
+				       filename, fr_syserror(errno));
+				break;
+			}
+
+			if (!PEM_write_X509(fp, client_cert)) {
+				fclose(fp);
+				RDEBUG("Failed writing certificate to file");
+				goto do_unlink;
+			}
+			fclose(fp);
+
+			if (!pair_make_request("TLS-Client-Cert-Filename",
+					     filename, T_OP_SET)) {
+				RDEBUG("Failed creating TLS-Client-Cert-Filename");
+
+				goto do_unlink;
+			}
+
+			RDEBUG("Verifying client certificate: %s", conf->verify_client_cert_cmd);
+			if (radius_exec_program(request, NULL, 0, NULL, request, conf->verify_client_cert_cmd,
+						request->packet->vps,
+						true, true, EXEC_TIMEOUT) != 0) {
+				AUTH(LOG_PREFIX ": Certificate CN (%s) fails external verification!", common_name);
+				my_ok = 0;
+
+			} else  if (request) {
+				RDEBUG("Client certificate CN %s passed external validation", common_name);
+			}
+
+		do_unlink:
+			unlink(filename);
+			break;
+		}
+
+		/*
+		 *	Track that we've verified the client certificate.
+		 */
+		ssn->client_cert_ok = (my_ok == 1);
+	} /* depth == 0 */
+
+	/*
+	 *	Copy certs to request even on failure, so that the
+	 *	user can log them.
+	 */
+	if (certs && request && !my_ok) {
+		fr_pair_add(&request->packet->vps, fr_pair_list_copy(request->packet, *certs));
+	}
+
+	if (RDEBUG_ENABLED3) {
+		RDEBUG3("(TLS) chain-depth   : %d", depth);
+		RDEBUG3("(TLS) error         : %d", err);
+
+		if (identity) RDEBUG3("identity      : %s", *identity);
+		RDEBUG3("(TLS) common name   : %s", common_name);
+		RDEBUG3("(TLS) subject       : %s", subject);
+		RDEBUG3("(TLS) issuer        : %s", issuer);
+		RDEBUG3("(TLS) verify return : %d", my_ok);
+	}
+
+	return (my_ok != 0);
+}
+
+
+/*
+ * 	Configure a X509 CA store to verify OCSP or client repsonses
+ *
+ * 	- Load the trusted CAs
+ * 	- Load the trusted issuer certificates
+ *	- Configure CRLs check if needed
+ */
+X509_STORE *fr_init_x509_store(fr_tls_server_conf_t *conf)
+{
+	X509_STORE *store = X509_STORE_new();
+
+	if (store == NULL) return NULL;
+
+	/* Load the CAs we trust */
+	if (conf->ca_file || conf->ca_path)
+		if (!X509_STORE_load_locations(store, conf->ca_file, conf->ca_path)) {
+			tls_error_log(NULL, "Error reading Trusted root CA list \"%s\"", conf->ca_file);
+			X509_STORE_free(store);
+			return NULL;
+		}
+
+#ifdef X509_V_FLAG_CRL_CHECK
+	if (conf->check_crl)
+		X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);
+#endif
+#ifdef X509_V_FLAG_CRL_CHECK_ALL
+	if (conf->check_all_crl)
+		X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK_ALL);
+#endif
+
+#if defined(X509_V_FLAG_PARTIAL_CHAIN)
+	X509_STORE_set_flags(store, X509_V_FLAG_PARTIAL_CHAIN);
+#endif
+
+	return store;
+}
+
+#if OPENSSL_VERSION_NUMBER >= 0x0090800fL
+#ifndef OPENSSL_NO_ECDH
+static int set_ecdh_curve(SSL_CTX *ctx, char const *ecdh_curve, bool disable_single_dh_use)
+{
+	if (!disable_single_dh_use) {
+		SSL_CTX_set_options(ctx, SSL_OP_SINGLE_ECDH_USE);
+	}
+
+	if (!ecdh_curve) return 0;
+
+#if OPENSSL_VERSION_NUMBER >= 0x1000200fL
+	/*
+	 *	A colon-separated list of curves.
+	 */
+	if (*ecdh_curve) {
+		char *list;
+
+		memcpy(&list, &ecdh_curve, sizeof(list)); /* const issues */
+
+		if (SSL_CTX_set1_curves_list(ctx, list) == 0) {
+			ERROR(LOG_PREFIX ": Unknown ecdh_curve \"%s\"", ecdh_curve);
+			return -1;
+		}
+	}
+
+	(void) SSL_CTX_set_ecdh_auto(ctx, 1);
+#else
+	/*
+	 *	Use APIs for older versions of OpenSSL.
+	 */
+	{
+		int      nid;
+		EC_KEY  *ecdh;
+
+		nid = OBJ_sn2nid(ecdh_curve);
+		if (!nid) {
+			ERROR(LOG_PREFIX ": Unknown ecdh_curve \"%s\"", ecdh_curve);
+			return -1;
+		}
+
+		ecdh = EC_KEY_new_by_curve_name(nid);
+		if (!ecdh) {
+			ERROR(LOG_PREFIX ": Unable to create new curve \"%s\"", ecdh_curve);
+			return -1;
+		}
+
+		SSL_CTX_set_tmp_ecdh(ctx, ecdh);
+
+		EC_KEY_free(ecdh);
+	}
+#endif
+
+	return 0;
+}
+#endif
+#endif
+
+#if defined(HAVE_OPENSSL_CRYPTO_H) && defined(HAVE_CRYPTO_SET_LOCKING_CALLBACK)
+#define TLS_UNUSED
+#else
+#define TLS_UNUSED UNUSED
+#endif
+
+/** Add all the default ciphers and message digests reate our context.
+ *
+ * This should be called exactly once from main, before reading the main config
+ * or initialising any modules.
+ */
+int tls_global_init(TLS_UNUSED bool spawn_flag, TLS_UNUSED bool check)
+{
+	SSL_load_error_strings();	/* readable error messages (examples show call before library_init) */
+	SSL_library_init();		/* initialize library */
+	OpenSSL_add_all_algorithms();	/* required for SHA2 in OpenSSL < 0.9.8o and 1.0.0.a */
+	CONF_modules_load_file(NULL, NULL, 0);
+
+	/*
+	 *	Initialize the index for the certificates.
+	 */
+	fr_tls_ex_index_certs = SSL_SESSION_get_ex_new_index(0, NULL, NULL, NULL, NULL);
+
+#if defined(HAVE_OPENSSL_CRYPTO_H) && defined(HAVE_CRYPTO_SET_LOCKING_CALLBACK)
+	/*
+	 *	If we're linking with OpenSSL too, then we need
+	 *	to set up the mutexes and enable the thread callbacks.
+	 *
+	 *	'check' and not 'check_config' because it's a global,
+	 *	and we don't want to have tls.c depend on globals.
+	 */
+	if (spawn_flag && !check && (tls_mutexes_init() < 0)) {
+		ERROR("(TLS) FATAL: Failed to set up SSL mutexes");
+		return -1;
+	}
+#endif
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+	/*
+	 *	Load the default provider for most algorithms
+	 */
+	openssl_default_provider = OSSL_PROVIDER_load(NULL, "default");
+	if (!openssl_default_provider) {
+		ERROR("(TLS) Failed loading default provider");
+		return -1;
+	}
+
+	/*
+	 *	Needed for MD4
+	 *
+	 *	https://www.openssl.org/docs/man3.0/man7/migration_guide.html#Legacy-Algorithms
+	 */
+	openssl_legacy_provider = OSSL_PROVIDER_load(NULL, "legacy");
+	if (!openssl_legacy_provider) {
+		ERROR("(TLS) Failed loading legacy provider");
+		return -1;
+	}
+#endif
+
+	return 0;
+}
+
+#ifdef ENABLE_OPENSSL_VERSION_CHECK
+/** Check for vulnerable versions of libssl
+ *
+ * @param acknowledged The highest CVE number a user has confirmed is not present in the system's libssl.
+ * @return 0 if the CVE specified by the user matches the most recent CVE we have, else -1.
+ */
+int tls_global_version_check(char const *acknowledged)
+{
+	uint64_t v;
+	bool bad = false;
+	size_t i;
+
+	if (strcmp(acknowledged, "yes") == 0) return 0;
+
+	/* Check for bad versions */
+	v = (uint64_t) SSLeay();
+
+	for (i = 0; i < (sizeof(libssl_defects) / sizeof(*libssl_defects)); i++) {
+		libssl_defect_t *defect = &libssl_defects[i];
+
+		if ((v >= defect->low) && (v <= defect->high)) {
+			/*
+			 *	If the CVE is acknowledged, allow it.
+			 */
+			if (!bad && (strcmp(acknowledged, defect->id) == 0)) return 0;
+
+			ERROR("Refusing to start with libssl version %s (in range %s)",
+			      ssl_version(), ssl_version_range(defect->low, defect->high));
+			ERROR("Security advisory %s (%s)", defect->id, defect->name);
+			ERROR("%s", defect->comment);
+
+			/*
+			 *	Only warn about the first one...
+			 */
+			if (!bad) {
+				INFO("Once you have verified libssl has been correctly patched, "
+				     "set security.allow_vulnerable_openssl = '%s'", defect->id);
+
+				bad = true;
+			}
+		}
+	}
+
+	if (bad) return -1;
+
+	return 0;
+}
+#endif
+
+/** Free any memory alloced by libssl
+ *
+ */
+void tls_global_cleanup(void)
+{
+#if OPENSSL_VERSION_NUMBER < 0x10000000L
+	ERR_remove_state(0);
+#elif OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
+	ERR_remove_thread_state(NULL);
+#endif
+#ifndef OPENSSL_NO_ENGINE
+	ENGINE_cleanup();
+#endif
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+	if (openssl_default_provider && !OSSL_PROVIDER_unload(openssl_default_provider)) {
+		ERROR("Failed unloading default provider");
+	}
+	openssl_default_provider = NULL;
+
+	if (openssl_legacy_provider && !OSSL_PROVIDER_unload(openssl_legacy_provider)) {
+		ERROR("Failed unloading legacy provider");
+	}
+	openssl_legacy_provider = NULL;
+#endif
+
+	CONF_modules_unload(1);
+	ERR_free_strings();
+	EVP_cleanup();
+	CRYPTO_cleanup_all_ex_data();
+}
+
+
+/*
+ *	Map version strings to OpenSSL macros.
+ */
+static const FR_NAME_NUMBER version2int[] = {
+	{ "1.0",    TLS1_VERSION },
+#ifdef TLS1_1_VERSION
+	{ "1.1",    TLS1_1_VERSION },
+#endif
+#ifdef TLS1_2_VERSION
+	{ "1.2",    TLS1_2_VERSION },
+#endif
+#ifdef TLS1_3_VERSION
+	{ "1.3",    TLS1_3_VERSION },
+#endif
+	{ NULL, 0 }
+};
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#ifdef TLS1_3_VERSION
+#define CHECK_FOR_PSK_CERTS (1)
+#endif
+#endif
+
+/** Create SSL context
+ *
+ * - Load the trusted CAs
+ * - Load the Private key & the certificate
+ * - Set the Context options & Verify options
+ */
+SSL_CTX *tls_init_ctx(fr_tls_server_conf_t *conf, int client, char const *chain_file, char const *private_key_file)
+{
+	SSL_CTX		*ctx;
+	X509_STORE	*certstore;
+	int		verify_mode = SSL_VERIFY_NONE;
+	int		ctx_options = 0, ctx_available = 0;
+	int		type;
+#ifdef CHECK_FOR_PSK_CERTS
+	bool		psk_and_certs = false;
+#endif
+	int		min_version;
+	int		max_version;
+
+	/*
+	 *	SHA256 is in all versions of OpenSSL, but isn't
+	 *	initialized by default.  It's needed for WiMAX
+	 *	certificates.
+	 */
+#ifdef HAVE_OPENSSL_EVP_SHA256
+	EVP_add_digest(EVP_sha256());
+#endif
+
+	ctx = SSL_CTX_new(SSLv23_method()); /* which is really "all known SSL / TLS methods".  Idiots. */
+	if (!ctx) {
+		tls_error_log(NULL, "Failed creating OpenSSL context");
+		return NULL;
+	}
+
+	/*
+	 * Save the config on the context so that callbacks which
+	 * only get SSL_CTX* e.g. session persistence, can get it
+	 */
+	SSL_CTX_set_app_data(ctx, conf);
+
+	/*
+	 * Identify the type of certificates that needs to be loaded
+	 */
+	if (conf->file_type) {
+		type = SSL_FILETYPE_PEM;
+	} else {
+		type = SSL_FILETYPE_ASN1;
+	}
+
+	/*
+	 * Set the password to load private key
+	 */
+	if (conf->private_key_password) {
+#ifdef __APPLE__
+		/*
+		 * We don't want to put the private key password in eap.conf, so  check
+		 * for our special string which indicates we should get the password
+		 * programmatically.
+		 */
+		char const* special_string = "Apple:UseCertAdmin";
+		if (strncmp(conf->private_key_password, special_string, strlen(special_string)) == 0) {
+			char cmd[256];
+			char *password;
+			long const max_password_len = 128;
+			snprintf(cmd, sizeof(cmd) - 1, "/usr/sbin/certadmin --get-private-key-passphrase \"%s\"",
+				 conf->private_key_file);
+
+			DEBUG2(LOG_PREFIX ":  Getting private key passphrase using command \"%s\"", cmd);
+
+			FILE* cmd_pipe = popen(cmd, "r");
+			if (!cmd_pipe) {
+				ERROR(LOG_PREFIX ": %s command failed: Unable to get private_key_password", cmd);
+				ERROR(LOG_PREFIX ": Error reading private_key_file %s", conf->private_key_file);
+				return NULL;
+			}
+
+			rad_const_free(conf->private_key_password);
+			password = talloc_array(conf, char, max_password_len);
+			if (!password) {
+				ERROR(LOG_PREFIX ": Can't allocate space for private_key_password");
+				ERROR(LOG_PREFIX ": Error reading private_key_file %s", conf->private_key_file);
+				pclose(cmd_pipe);
+				return NULL;
+			}
+
+			fgets(password, max_password_len, cmd_pipe);
+			pclose(cmd_pipe);
+
+			/* Get rid of newline at end of password. */
+			password[strlen(password) - 1] = '\0';
+
+			DEBUG3(LOG_PREFIX ": Password from command = \"%s\"", password);
+			conf->private_key_password = password;
+		}
+#endif
+
+		{
+			char *password;
+
+			memcpy(&password, &conf->private_key_password, sizeof(password));
+			SSL_CTX_set_default_passwd_cb_userdata(ctx, password);
+			SSL_CTX_set_default_passwd_cb(ctx, cbtls_password);
+		}
+	}
+
+#ifdef PSK_MAX_IDENTITY_LEN
+	/*
+	 *	A dynamic query exists.  There MUST NOT be a
+	 *	statically configured identity and password.
+	 */
+	if (conf->psk_query) {
+		if (!*conf->psk_query) {
+			ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_query cannot be empty");
+			return NULL;
+		}
+
+		if (conf->psk_identity && *conf->psk_identity) {
+			ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_identity and psk_query cannot be used at the same time.");
+			return NULL;
+		}
+
+		if (conf->psk_password && *conf->psk_password) {
+			ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_password and psk_query cannot be used at the same time.");
+			return NULL;
+		}
+
+		if (client) {
+			ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_query cannot be used for outgoing connections");
+			return NULL;
+		}
+
+		/*
+		 *	Now check that if PSK is being used, that the config is valid.
+		 */
+	} else if (conf->psk_identity) {
+		if (!*conf->psk_identity) {
+			ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_identity is empty");
+			return NULL;
+		}
+
+
+		if (!conf->psk_password || !*conf->psk_password) {
+			ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_identity is set, but there is no psk_password");
+			return NULL;
+		}
+
+	} else if (conf->psk_password) {
+		ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_password is set, but there is no psk_identity");
+		return NULL;
+	}
+
+	/*
+	 *	Set the server PSK callback if necessary.
+	 */
+	if (!client && (conf->psk_identity || conf->psk_query)) {
+		SSL_CTX_set_psk_server_callback(ctx, psk_server_callback);
+	}
+
+	/*
+	 *	Do more sanity checking if we have a PSK identity.  We
+	 *	check the password, and convert it to it's final form.
+	 */
+	if (conf->psk_identity) {
+		size_t psk_len, hex_len;
+		uint8_t buffer[PSK_MAX_PSK_LEN];
+
+		if (client) {
+			SSL_CTX_set_psk_client_callback(ctx,
+							psk_client_callback);
+		}
+
+		if (!conf->psk_password || !*conf->psk_password) {
+			ERROR(LOG_PREFIX ": psk_hexphrase cannot be empty");
+			return NULL;
+		}
+
+		psk_len = strlen(conf->psk_password);
+		if (strlen(conf->psk_password) > (2 * PSK_MAX_PSK_LEN)) {
+			ERROR(LOG_PREFIX ": psk_hexphrase is too long (max %d)", PSK_MAX_PSK_LEN);
+			return NULL;
+		}
+
+		/*
+		 *	Check the password now, so that we don't have
+		 *	errors at run-time.
+		 */
+		hex_len = fr_hex2bin(buffer, sizeof(buffer), conf->psk_password, psk_len);
+		if (psk_len != (2 * hex_len)) {
+			ERROR(LOG_PREFIX ": psk_hexphrase is not all hex");
+			return NULL;
+		}
+
+#ifdef CHECK_FOR_PSK_CERTS
+		/*
+		 *	RFC 8446 says:
+		 *
+		 *	When authenticating via a certificate, the server will send the
+     		 *	Certificate (Section 4.4.2) and CertificateVerify (Section 4.4.3)
+		 *	messages.  In TLS 1.3 as defined by this document, either a PSK or
+		 *	a certificate is always used, but not both.  Future documents may
+		 *	define how to use them together.
+		 */
+		if (((conf->psk_identity || conf->psk_password || conf->psk_query)) &&
+		    (conf->certificate_file || conf->private_key_password || conf->private_key_file)) {
+			psk_and_certs = true;
+		}
+#endif
+
+		goto post_ca;
+	}
+#else
+	(void) client;	/* -Wunused */
+#endif
+
+	/*
+	 *	Load our keys and certificates
+	 *
+	 *	If certificates are of type PEM then we can make use
+	 *	of cert chain authentication using openssl api call
+	 *	SSL_CTX_use_certificate_chain_file.  Please see how
+	 *	the cert chain needs to be given in PEM from
+	 *	openSSL.org
+	 */
+	if (!chain_file) chain_file = conf->certificate_file;
+	if (!chain_file) goto load_ca;
+
+	if (type == SSL_FILETYPE_PEM) {
+		if (!(SSL_CTX_use_certificate_chain_file(ctx, chain_file))) {
+			tls_error_log(NULL, "Failed reading certificate file \"%s\"",
+				      chain_file);
+			return NULL;
+		}
+
+	} else if (!(SSL_CTX_use_certificate_file(ctx, chain_file, type))) {
+		tls_error_log(NULL, "Failed reading certificate file \"%s\"",
+			      chain_file);
+		return NULL;
+	}
+
+load_ca:
+	/*
+	 *	Load the CAs we trust and configure CRL checks if needed
+	 */
+	if (conf->ca_file || conf->ca_path) {
+		if ((certstore = fr_init_x509_store(conf)) == NULL ) return NULL;
+		SSL_CTX_set_cert_store(ctx, certstore);
+	} else {
+#if defined(X509_V_FLAG_PARTIAL_CHAIN)
+		X509_STORE_set_flags(SSL_CTX_get_cert_store(ctx), X509_V_FLAG_PARTIAL_CHAIN);
+#endif
+	}
+
+	if (conf->ca_file && *conf->ca_file) SSL_CTX_set_client_CA_list(ctx, SSL_load_client_CA_file(conf->ca_file));
+
+	conf->ca_path_last_reload = time(NULL);
+	conf->old_x509_store = NULL;
+
+	/*
+	 * Disable reloading of cert store if we're not using CA path
+	 */
+	if (!conf->ca_path) conf->ca_path_reload_interval = 0;
+
+	if (conf->ca_path_reload_interval > 0 && conf->ca_path_reload_interval < 300) {
+		DEBUG2("ca_path_reload_interval is set too low, reset it to 300");
+		conf->ca_path_reload_interval = 300;
+	}
+
+	/* Load private key */
+	if (!private_key_file) private_key_file = conf->private_key_file;
+	if (private_key_file) {
+		if (!(SSL_CTX_use_PrivateKey_file(ctx, private_key_file, type))) {
+			tls_error_log(NULL, "Failed reading private key file \"%s\"",
+				      private_key_file);
+			return NULL;
+		}
+
+		/*
+		 * Check if the loaded private key is the right one
+		 */
+		if (!SSL_CTX_check_private_key(ctx)) {
+			ERROR(LOG_PREFIX ": Private key does not match the certificate public key");
+			return NULL;
+		}
+	}
+
+#ifdef PSK_MAX_IDENTITY_LEN
+post_ca:
+#endif
+
+	/*
+	 *	We never want SSLv2 or SSLv3.
+	 */
+	ctx_options |= SSL_OP_NO_SSLv2;
+	ctx_options |= SSL_OP_NO_SSLv3;
+
+	/*
+	 *	If set then dummy Change Cipher Spec (CCS) messages are sent in
+	 *	TLSv1.3. This has the effect of making TLSv1.3 look more like TLSv1.2
+	 *	so that middleboxes that do not understand TLSv1.3 will not drop
+	 *	the connection. This isn't needed for EAP-TLS, so we disable it.
+	 *
+	 *	EAP (hopefully) does not have middlebox deployments
+	 */
+#ifdef SSL_OP_ENABLE_MIDDLEBOX_COMPAT
+	ctx_options &= ~SSL_OP_ENABLE_MIDDLEBOX_COMPAT;
+#endif
+
+	/*
+	 *	SSL_CTX_set_(min|max)_proto_version was included in OpenSSL 1.1.0
+	 *
+	 *	This version already defines macros for TLS1_2_VERSION and
+	 *	below, so we don't need to check for them explicitly.
+	 *
+	 *	TLS1_3_VERSION is available in OpenSSL 1.1.1.
+	 */
+
+	/*
+	 *	Get the max version from the configuration files.
+	 */
+	if (conf->tls_max_version && *conf->tls_max_version) {
+		max_version = fr_str2int(version2int, conf->tls_max_version, 0);
+		if (!max_version) {
+			ERROR("Invalid value for tls_max_version '%s'", conf->tls_max_version);
+			return NULL;
+		}
+	} else {
+		/*
+		 *	Pick the maximum version available at compile
+		 *	time.
+		 */
+#if defined(TLS1_3_VERSION)
+#ifdef WITH_RADIUSV11
+		/*
+		 *	RADIUS 1.1 requires TLS 1.3 or later.
+		 */
+		if (conf->radiusv11) {
+			max_version = TLS1_3_VERSION;
+		} else
+#endif
+
+
+		max_version = TLS1_2_VERSION; /* yes, we only use TLS 1.3 if it's EXPLICITELY ENABLED */
+#elif defined(TLS1_2_VERSION)
+		max_version = TLS1_2_VERSION;
+#elif defined(TLS1_1_VERSION)
+		max_version = TLS1_1_VERSION;
+#else
+		max_version = TLS1_VERSION;
+#endif
+	}
+
+	/*
+	 *	Get the min version from the configuration files.
+	 */
+	if (conf->tls_min_version && *conf->tls_min_version) {
+		min_version = fr_str2int(version2int, conf->tls_min_version, 0);
+		if (!min_version) {
+			ERROR("Unknown or unsupported value for tls_min_version '%s'", conf->tls_min_version);
+			return NULL;
+		}
+	} else {
+#ifdef WITH_RADIUSV11
+		/*
+		 *	RADIUS 1.1 requires TLS 1.3 or later.
+		 */
+		if (conf->radiusv11) {
+			min_version = TLS1_3_VERSION;
+		} else
+#endif
+		/*
+		 *	Allow TLS 1.0.  It is horribly insecure, but
+		 *	some systems still use it.
+		 */
+		min_version = TLS1_VERSION;
+	}
+
+	/*
+	 *	Compare the two.
+	 */
+	if ((min_version > max_version) || (max_version < min_version)) {
+		ERROR("tls_min_version '%s' must be <= tls_max_version '%s'",
+		      conf->tls_min_version, conf->tls_max_version);
+		return NULL;
+	}
+
+#ifdef CHECK_FOR_PSK_CERTS
+	/*
+	 *	Disable TLS 1.3 when using PSKs and certs.
+	 *	This doesn't work.
+	 *
+	 *	It's best to disable the offending
+	 *	configuration and warn about it.  The
+	 *	alternative is to have the admin wonder why it
+	 *	doesn't work.
+	 *
+	 *	Note that the admin can over-ride this by
+	 *	setting "min_version = max_version = 1.3"
+	 */
+	if (psk_and_certs &&
+	    (min_version < TLS1_3_VERSION) && (max_version >= TLS1_3_VERSION)) {
+		max_version = TLS1_2_VERSION;
+		radlog(L_DBG | L_WARN, "Disabling TLS 1.3 due to PSK and certificates being configured simultaneously.  This is not supported by the standards.");
+	}
+#endif
+
+	/*
+	 *	No one should be using TLS 1.0 or TLS 1.1 any more
+	 *
+	 *	If TLS1.2 isn't defined by OpenSSL, then we _know_
+	 *	it's an insecure version of OpenSSL.
+	 */
+#ifdef TLS1_2_VERSION
+	if (max_version < TLS1_2_VERSION)
+#endif
+	{
+		if (rad_debug_lvl) {
+			WARN(LOG_PREFIX ": The configuration allows TLS 1.0 and/or TLS 1.1.  We STRONGLY recommned using only TLS 1.2 for security");
+			WARN(LOG_PREFIX ": Please set: tls_min_version = '1.2'");
+		}
+	}
+
+#ifdef SSL_OP_NO_TLSv1
+	/*
+	 *	Check min / max against the old-style "disable" flag.
+	 */
+	if (conf->disable_tlsv1) {
+		if (min_version == TLS1_VERSION) {
+			ERROR(LOG_PREFIX ": 'disable_tlsv1' is set, but 'min_version = 1.0'.  These cannot both be true.");
+			return NULL;
+		}
+		if (max_version == TLS1_VERSION) {
+			ERROR(LOG_PREFIX ": 'disable_tlsv1' is set, but 'max_version = 1.0'.  These cannot both be true.");
+			return NULL;
+		}
+		ctx_options |= SSL_OP_NO_TLSv1;
+	}
+
+	if (min_version > TLS1_VERSION) ctx_options |= SSL_OP_NO_TLSv1;
+
+	ctx_available |= SSL_OP_NO_TLSv1;
+#endif
+
+#ifdef SSL_OP_NO_TLSv1_1
+	/*
+	 *	Check min / max against the old-style "disable" flag.
+	 */
+	if (conf->disable_tlsv1_1) {
+		if (min_version <= TLS1_1_VERSION) {
+			ERROR(LOG_PREFIX ": 'disable_tlsv1_1' is set, but 'min_version <= 1.1'.  These cannot both be true.");
+			return NULL;
+		}
+		if (max_version == TLS1_1_VERSION) {
+			ERROR(LOG_PREFIX ": 'disable_tlsv1_1' is set, but 'max_version = 1.1'.  These cannot both be true.");
+			return NULL;
+		}
+		ctx_options |= SSL_OP_NO_TLSv1_1;
+	}
+
+	if (min_version > TLS1_1_VERSION) ctx_options |= SSL_OP_NO_TLSv1_1;
+	if (max_version < TLS1_1_VERSION) ctx_options |= SSL_OP_NO_TLSv1_1;
+
+	ctx_available |= SSL_OP_NO_TLSv1_1;
+#endif
+
+#ifdef SSL_OP_NO_TLSv1_2
+	/*
+	 *	Check min / max against the old-style "disable" flag.
+	 */
+	if (conf->disable_tlsv1_2) {
+		if (min_version <= TLS1_2_VERSION) {
+			ERROR(LOG_PREFIX ": 'disable_tlsv1_2' is set, but 'min_version <= 1.2'.  These cannot both be true.");
+			return NULL;
+		}
+		if (max_version == TLS1_2_VERSION) {
+			ERROR(LOG_PREFIX ": 'disable_tlsv1_1' is set, but 'max_version = 1.2'.  These cannot both be true.");
+			return NULL;
+		}
+		ctx_options |= SSL_OP_NO_TLSv1_2;
+	}
+	ctx_available |= SSL_OP_NO_TLSv1_2;
+
+	if (min_version > TLS1_2_VERSION) ctx_options |= SSL_OP_NO_TLSv1_2;
+	if (max_version < TLS1_2_VERSION) ctx_options |= SSL_OP_NO_TLSv1_2;
+#endif
+
+#ifdef SSL_OP_NO_TLSv1_3
+	ctx_available |= SSL_OP_NO_TLSv1_3;
+	if (min_version > TLS1_3_VERSION) ctx_options |= SSL_OP_NO_TLSv1_3;
+	if (max_version < TLS1_3_VERSION) ctx_options |= SSL_OP_NO_TLSv1_3;
+#endif
+
+
+#ifdef WITH_RADIUSV11
+	/*
+	 *	RADIUS 1.1 requires TLS 1.3 or later.
+	 */
+	if (conf->radiusv11 && (min_version < TLS1_3_VERSION)) {
+		ERROR(LOG_PREFIX ": Please set 'tls_min_version = 1.2' or greater to use 'radiusv1_1 = true'");
+		return NULL;
+	}
+#endif
+
+	/*
+	 *	Set the cipher list if we were told to do so.  We do
+	 *	this before setting min/max TLS version.  In a sane
+	 *	world, OpenSSL would error out if we set the max TLS
+	 *	version to something which was unsupported by the
+	 *	current security level.  However, this is OpenSSL.  If
+	 *	you set conflicting options, it doesn't give an error.
+	 *	Instead, it just picks something to do.
+	 */
+	if (conf->cipher_list) {
+		if (!SSL_CTX_set_cipher_list(ctx, conf->cipher_list)) {
+			tls_error_log(NULL, "Failed setting cipher list");
+			return NULL;
+		}
+	}
+
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+	if (conf->sigalgs_list) {
+		char *list;
+
+		memcpy(&list, &(conf->sigalgs_list), sizeof(list)); /* const issues */
+
+		if (SSL_CTX_set1_sigalgs_list(ctx, list) == 0) {
+			tls_error_log(NULL, "Failed setting signature list '%s'", conf->sigalgs_list);
+			return NULL;
+		}
+	}
+#endif
+
+	/*
+	 *	Tell OpenSSL PRETTY PLEASE MAY WE USE TLS 1.1.
+	 *
+	 *	Because saying "use TLS 1.1" isn't enough.  We have to
+	 *	send it flowers and cake.
+	 */
+	if (min_version <= TLS1_1_VERSION) {
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+		int seclevel = SSL_CTX_get_security_level(ctx);
+		int required;;
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+		required = 0;
+#else
+		required = 1;
+#endif
+
+		if (seclevel != required) {
+			WARN(LOG_PREFIX ": In order to use TLS 1.0 and/or TLS 1.1, you likely need to set: cipher_list = \"DEFAULT@SECLEVEL=%d\"", required);
+		}
+
+#else
+		/*
+		 *	No API to get the security level.  Just guess based on the string in the cipher_list.
+		 */
+		if (conf->cipher_list &&
+		    !strstr(conf->cipher_list, "DEFAULT@SECLEVEL=1")) {
+			WARN(LOG_PREFIX ": In order to use TLS 1.0 and/or TLS 1.1, you likely need to set: cipher_list = \"DEFAULT@SECLEVEL=1\"");
+		}
+#endif
+	}
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+	if (conf->disable_tlsv1) {
+		WARN(LOG_PREFIX ": Please use 'tls_min_version' and 'tls_max_version' instead of 'disable_tlsv1'");
+	}
+	if (conf->disable_tlsv1_1) {
+		WARN(LOG_PREFIX ": Please use 'tls_min_version' and 'tls_max_version' instead of 'disable_tlsv1_1'");
+	}
+	if (conf->disable_tlsv1_2) {
+		WARN(LOG_PREFIX ": Please use 'tls_min_version' and 'tls_max_version' instead of 'disable_tlsv1_2'");
+	}
+
+	ctx_options &= ~(ctx_available); /* clear these flags, as they're not needed. */
+
+	if (!SSL_CTX_set_max_proto_version(ctx, max_version)) {
+		ERROR("Failed setting TLS maximum version");
+		return NULL;
+	}
+	if (!SSL_CTX_set_min_proto_version(ctx, min_version)) {
+		ERROR("Failed setting TLS minimum version");
+		return NULL;
+	}
+#endif	/* OpenSSL version < 1.1.0 */
+
+	if ((ctx_options & ctx_available) == ctx_available) {
+		ERROR(LOG_PREFIX ": You have disabled all available TLS versions.  EAP will not work");
+		return NULL;
+	}
+
+	/*
+	 *	Cache min / max TLS version so that we can
+	 *	programatically disable TLS 1.3 for TTLS, PEAP, and
+	 *	FAST.
+	 */
+	conf->min_version = min_version;
+	conf->max_version = max_version;
+
+#ifdef SSL_OP_NO_TICKET
+	ctx_options |= SSL_OP_NO_TICKET;
+#endif
+
+	if (!conf->disable_single_dh_use) {
+		/*
+		 *	SSL_OP_SINGLE_DH_USE must be used in order to prevent
+		 *	small subgroup attacks and forward secrecy. Always
+		 *	using SSL_OP_SINGLE_DH_USE has an impact on the
+		 *	computer time needed during negotiation, but it is not
+		 *	very large.
+		 */
+		ctx_options |= SSL_OP_SINGLE_DH_USE;
+	}
+
+	/*
+	 *	SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS to work around issues
+	 *	in Windows Vista client.
+	 *	http://www.openssl.org/~bodo/tls-cbc.txt
+	 *	http://www.nabble.com/(RADIATOR)-Radiator-Version-3.16-released-t2600070.html
+	 */
+	ctx_options |= SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS;
+
+	if (conf->cipher_server_preference) {
+		/*
+		 *      SSL_OP_CIPHER_SERVER_PREFERENCE to follow best practice
+		 *      of nowday's TLS: do not allow poorly-selected ciphers from
+		 *      client to take preference
+		 */
+		ctx_options |= SSL_OP_CIPHER_SERVER_PREFERENCE;
+	}
+
+	SSL_CTX_set_options(ctx, ctx_options);
+
+	/*
+	 *	TLS 1.3 introduces the concept of early data (also known as zero
+	 *	round trip data or 0-RTT data). Early data allows a client to send
+	 *	data to a server in the first round trip of a connection, without
+	 *	waiting for the TLS handshake to complete if the client has spoken
+	 *	to the same server recently. This doesn't work for EAP, so we
+	 *	disable early data.
+	 *
+	 */
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+	SSL_CTX_set_max_early_data(ctx, 0);
+#endif
+
+	/*
+	 *	TODO: Set the RSA & DH
+	 *	SSL_CTX_set_tmp_rsa_callback(ctx, cbtls_rsa);
+	 *	SSL_CTX_set_tmp_dh_callback(ctx, cbtls_dh);
+	 */
+
+	/*
+	 *	set the message callback to identify the type of
+	 *	message.  For every new session, there can be a
+	 *	different callback argument.
+	 *
+	 *	SSL_CTX_set_msg_callback(ctx, cbtls_msg);
+	 */
+
+	/*
+	 *	Set eliptical curve crypto configuration.
+	 */
+#if OPENSSL_VERSION_NUMBER >= 0x0090800fL
+#ifndef OPENSSL_NO_ECDH
+	if (set_ecdh_curve(ctx, conf->ecdh_curve, conf->disable_single_dh_use) < 0) {
+		return NULL;
+	}
+#endif
+#endif
+
+	/*
+	 *	OpenSSL will automatically create certificate chains,
+	 *	unless we tell it to not do that.  The problem is that
+	 *	it sometimes gets the chains right from a certificate
+	 *	signature view, but wrong from the clients view.
+	 */
+	if (!conf->auto_chain) {
+		SSL_CTX_set_mode(ctx, SSL_MODE_NO_AUTO_CHAIN);
+	}
+
+	/* Set Info callback */
+	SSL_CTX_set_info_callback(ctx, cbtls_info);
+
+	/*
+	 *	Callbacks, etc. for session resumption.
+	 */
+	if (conf->session_cache_enable) {
+		/*
+		 *	Cache sessions on disk if requested.
+		 */
+		if (conf->session_cache_path && *conf->session_cache_path) {
+			SSL_CTX_sess_set_new_cb(ctx, cbtls_new_session);
+			SSL_CTX_sess_set_get_cb(ctx, cbtls_get_session);
+			SSL_CTX_sess_set_remove_cb(ctx, cbtls_remove_session);
+		}
+
+		/*
+		 *	Or run the cache through a virtual server.
+		 */
+		if (conf->session_cache_server && *conf->session_cache_server) {
+			SSL_CTX_sess_set_new_cb(ctx, cbtls_cache_save);
+			SSL_CTX_sess_set_get_cb(ctx, cbtls_cache_load);
+			SSL_CTX_sess_set_remove_cb(ctx, cbtls_cache_clear);
+		}
+
+		SSL_CTX_set_quiet_shutdown(ctx, 1);
+		if (fr_tls_ex_index_vps < 0)
+			fr_tls_ex_index_vps = SSL_SESSION_get_ex_new_index(0, NULL, NULL, NULL, NULL);
+	}
+
+	/*
+	 *	Check the certificates for revocation.
+	 */
+#ifdef X509_V_FLAG_CRL_CHECK
+	if (conf->check_crl) {
+		certstore = SSL_CTX_get_cert_store(ctx);
+		if (certstore == NULL) {
+			tls_error_log(NULL, "Error reading Certificate Store");
+	    		return NULL;
+		}
+		X509_STORE_set_flags(certstore, X509_V_FLAG_CRL_CHECK);
+
+#ifdef X509_V_FLAG_USE_DELTAS
+		/*
+		 *	If set, delta CRLs (if present) are used to
+		 *	determine certificate status. If not set
+		 *	deltas are ignored.
+		 *
+		 *	So it's safe to always set this flag.
+		 */
+		X509_STORE_set_flags(certstore, X509_V_FLAG_USE_DELTAS);
+#endif
+
+#ifdef X509_V_FLAG_CRL_CHECK_ALL
+		if (conf->check_all_crl)
+			X509_STORE_set_flags(certstore, X509_V_FLAG_CRL_CHECK_ALL);
+#endif
+	}
+#endif
+
+	/*
+	 *	Set verify modes
+	 *	Always verify the peer certificate
+	 */
+	verify_mode |= SSL_VERIFY_PEER;
+	verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
+	verify_mode |= SSL_VERIFY_CLIENT_ONCE;
+	SSL_CTX_set_verify(ctx, verify_mode, cbtls_verify);
+
+	if (conf->verify_depth) {
+		SSL_CTX_set_verify_depth(ctx, conf->verify_depth);
+	}
+
+#ifndef LIBRESSL_VERSION_NUMBER
+	/* Load randomness */
+	if (conf->random_file) {
+		if (!(RAND_load_file(conf->random_file, 1024*10))) {
+			tls_error_log(NULL, "Failed loading randomness");
+			return NULL;
+		}
+	}
+#endif
+
+	/*
+	 *	Setup session caching
+	 */
+	if (conf->session_cache_enable) {
+		/*
+		 *	Create a unique context Id per EAP-TLS configuration.
+		 */
+		if (conf->session_id_name) {
+			snprintf(conf->session_context_id, sizeof(conf->session_context_id),
+				 "FR eap %s", conf->session_id_name);
+		} else {
+			snprintf(conf->session_context_id, sizeof(conf->session_context_id),
+				 "FR eap %p", conf);
+		}
+
+		/*
+		 *	Cache it, DON'T auto-clear it, and disable the internal OpenSSL session cache.
+		 */
+		SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_AUTO_CLEAR | SSL_SESS_CACHE_NO_INTERNAL);
+
+		SSL_CTX_set_session_id_context(ctx,
+					       (unsigned char *) conf->session_context_id,
+					       (unsigned int) strlen(conf->session_context_id));
+
+		/*
+		 *	Our lifetime is in hours, this is in seconds.
+		 */
+		SSL_CTX_set_timeout(ctx, conf->session_lifetime * 3600);
+
+		/*
+		 *	Set the maximum number of entries in the
+		 *	session cache.
+		 */
+		SSL_CTX_sess_set_cache_size(ctx, conf->session_cache_size);
+
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER)
+		SSL_CTX_set_num_tickets(ctx, 1);
+#endif
+
+	} else {
+		SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF);
+
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER)
+		/*
+		 *	This controls the number of stateful or stateless tickets
+		 *	generated with TLS 1.3.  In OpenSSL 1.1.1 it's also
+		 *	required to disable sending session tickets,
+		 *	SSL_SESS_CACHE_OFF is not good enough.
+		 */
+		SSL_CTX_set_num_tickets(ctx, 0);
+#endif
+	}
+
+	return ctx;
+}
+
+
+/*
+ *	Free TLS client/server config
+ *	Should not be called outside this code, as a callback is
+ *	added to automatically free the data when the CONF_SECTION
+ *	is freed.
+ */
+static int _tls_server_conf_free(fr_tls_server_conf_t *conf)
+{
+	if (conf->ctx) SSL_CTX_free(conf->ctx);
+
+	if (conf->cache_ht) fr_hash_table_free(conf->cache_ht);
+
+	pthread_mutex_destroy(&conf->mutex);
+
+#ifdef HAVE_OPENSSL_OCSP_H
+	if (conf->ocsp_store) X509_STORE_free(conf->ocsp_store);
+	conf->ocsp_store = NULL;
+#endif
+
+	if (conf->realms) fr_hash_table_free(conf->realms);
+
+#ifndef NDEBUG
+	memset(conf, 0, sizeof(*conf));
+#endif
+	return 0;
+}
+
+fr_tls_server_conf_t *tls_server_conf_alloc(TALLOC_CTX *ctx)
+{
+	fr_tls_server_conf_t *conf;
+
+	conf = talloc_zero(ctx, fr_tls_server_conf_t);
+	if (!conf) {
+		ERROR(LOG_PREFIX ": Out of memory");
+		return NULL;
+	}
+
+	talloc_set_destructor(conf, _tls_server_conf_free);
+
+	return conf;
+}
+
+static uint32_t store_hash(void const *data)
+{
+	DICT_ATTR const *da = data;
+	return fr_hash(&da, sizeof(da));
+}
+
+static int store_cmp(void const *a, void const *b)
+{
+	DICT_ATTR const *one = a;
+	DICT_ATTR const *two = b;
+
+	return (one < two) - (one > two);
+}
+
+static uint32_t realm_hash(void const *data)
+{
+	fr_realm_ctx_t const *r = data;
+
+	return fr_hash_string(r->name);
+}
+
+static int realm_cmp(void const *a, void const *b)
+{
+	fr_realm_ctx_t const *one = a;
+	fr_realm_ctx_t const *two = b;
+
+	return strcmp(one->name, two->name);
+}
+
+static void realm_free(void *data)
+{
+	fr_realm_ctx_t *r = data;
+
+	SSL_CTX_free(r->ctx);
+}
+
+static int tls_realms_load(fr_tls_server_conf_t *conf)
+{
+	fr_hash_table_t *ht;
+	DIR		*dir;
+	struct dirent	*dp;
+	char		buffer[PATH_MAX];
+	char		buffer2[PATH_MAX];
+
+	ht = fr_hash_table_create(realm_hash, realm_cmp, realm_free);
+	if (!ht) return -1;
+
+	dir = opendir(conf->realm_dir);
+	if (!dir) {
+		ERROR("Error reading directory %s: %s", conf->realm_dir, fr_syserror(errno));
+	error:
+		if (dir) closedir(dir);
+		fr_hash_table_free(ht);
+		return -1;
+	}
+
+	/*
+	 *	Read only the PEM files
+	 */
+	while ((dp = readdir(dir)) != NULL) {
+		char *p;
+		struct stat stat_buf;
+		SSL_CTX *ctx;
+		fr_realm_ctx_t *r;
+		char const *private_key_file = buffer;
+
+		if (dp->d_name[0] == '.') continue;
+
+		p = strrchr(dp->d_name, '.');
+		if (!p) continue;
+
+		if (memcmp(p, ".pem", 5) != 0) continue; /* must END in .pem */
+
+		snprintf(buffer, sizeof(buffer), "%s/%s", conf->realm_dir, dp->d_name); /* ignore directories */
+		if ((stat(buffer, &stat_buf) != 0) ||
+		    S_ISDIR(stat_buf.st_mode)) continue;
+
+		strcpy(buffer2, buffer);
+		p = strchr(buffer2, '.'); /* which must be there... */
+		if (!p) continue;
+
+		/*
+		 *	If there's a key file, then use that.
+		 *	Otherwise assume that the private key is in
+		 *	the chain file.
+		 */
+		strcpy(p, ".key");
+		if (stat(buffer2, &stat_buf) != 0) private_key_file = buffer2;
+
+		ctx = tls_init_ctx(conf, 1, buffer, private_key_file);
+		if (!ctx) goto error;
+
+		r = talloc_zero(conf, fr_realm_ctx_t);
+		if (!r) {
+			SSL_CTX_free(ctx);
+			goto error;
+		}
+
+		r->name = talloc_strdup(r, buffer);
+		r->ctx = ctx;
+
+		if (fr_hash_table_insert(ht, r) < 0) {
+			ERROR("Failed inserting certificate file %s into hash table", buffer);
+			goto error;
+		}
+	}
+
+	conf->realms = ht;
+	closedir(dir);
+
+	return 0;
+}
+
+
+fr_tls_server_conf_t *tls_server_conf_parse(CONF_SECTION *cs)
+{
+	fr_tls_server_conf_t *conf;
+
+	/*
+	 *	If cs has already been parsed there should be a cached copy
+	 *	of conf already stored, so just return that.
+	 */
+	conf = cf_data_find(cs, "tls-conf");
+	if (conf) {
+		DEBUG(LOG_PREFIX ": Using cached TLS configuration from previous invocation");
+		return conf;
+	}
+
+	conf = tls_server_conf_alloc(cs);
+
+	if (cf_section_parse(cs, conf, tls_server_config) < 0) {
+	error:
+		talloc_free(conf);
+		return NULL;
+	}
+
+	/*
+	 *	Save people from their own stupidity.
+	 */
+	if (conf->fragment_size < 100) conf->fragment_size = 100;
+
+	/*
+	 *	Disallow sessions of more than 7 days, as per RFC
+	 *	8446.
+	 *
+	 *	Note that we also enforce this on TLS 1.2, etc.
+	 *	Because there's just no reason to have month-long TLS
+	 *	sessions.
+	 */
+	if (conf->session_lifetime > (7 * 24)) conf->session_lifetime = 7 * 24;
+
+	/*
+	 *	Only check for certificate things if we don't have a
+	 *	PSK query.
+	 */
+#ifdef PSK_MAX_IDENTITY_LEN
+	if (conf->psk_identity) {
+		if (conf->private_key_file) {
+			WARN(LOG_PREFIX ": Ignoring private key file due to psk_identity being used");
+		}
+
+		if (conf->certificate_file) {
+			WARN(LOG_PREFIX ": Ignoring certificate file due to psk_identity being used");
+		}
+
+	} else
+#endif
+	{
+		if (!conf->private_key_file) {
+			ERROR(LOG_PREFIX ": TLS Server requires a private key file");
+			goto error;
+		}
+
+		if (!conf->certificate_file) {
+			ERROR(LOG_PREFIX ": TLS Server requires a certificate file");
+			goto error;
+		}
+	}
+
+	/*
+	 *	Initialize configuration mutex
+	 */
+	pthread_mutex_init(&conf->mutex, NULL);
+
+	/*
+	 *	Initialize TLS
+	 */
+	conf->ctx = tls_init_ctx(conf, 0, NULL, NULL);
+	if (conf->ctx == NULL) {
+		goto error;
+	}
+
+	if (conf->session_cache_enable) {
+		CONF_SECTION	*subcs;
+		CONF_ITEM	*ci;
+
+		subcs = cf_section_sub_find(cs, "cache");
+		if (!subcs) goto skip_list;
+		subcs = cf_section_sub_find(subcs, "store");
+		if (!subcs) goto skip_list;
+
+		/*
+		 *	Largely taken from rlm_detail for laziness.
+		 */
+		conf->cache_ht = fr_hash_table_create(store_hash, store_cmp, NULL);
+
+		for (ci = cf_item_find_next(subcs, NULL);
+		     ci != NULL;
+		     ci = cf_item_find_next(subcs, ci)) {
+			char const	*attr;
+			DICT_ATTR const	*da;
+
+			if (!cf_item_is_pair(ci)) continue;
+
+			attr = cf_pair_attr(cf_item_to_pair(ci));
+			if (!attr) continue; /* pair-anoia */
+
+			da = dict_attrbyname(attr);
+			if (!da) {
+				ERROR(LOG_PREFIX ": TLS Server requires a certificate file");
+				goto error;
+			}
+
+			/*
+			 *	Be kind to minor mistakes.
+			 */
+			if (fr_hash_table_finddata(conf->cache_ht, da)) {
+				WARN(LOG_PREFIX ": Ignoring duplicate entry '%s'", attr);
+				continue;
+			}
+
+
+			if (!fr_hash_table_insert(conf->cache_ht, da)) {
+				ERROR(LOG_PREFIX ": Failed inserting '%s' into cache list", attr);
+				goto error;
+			}
+		}
+
+		/*
+		 *	If we didn't suppress anything, delete the hash table.
+		 */
+		if (fr_hash_table_num_elements(conf->cache_ht) == 0) {
+			fr_hash_table_free(conf->cache_ht);
+			conf->cache_ht = NULL;
+		}
+	}
+
+skip_list:
+
+#ifdef HAVE_OPENSSL_OCSP_H
+	/*
+	 * 	Initialize OCSP Revocation Store
+	 */
+	if (conf->ocsp_enable) {
+		conf->ocsp_store = fr_init_x509_store(conf);
+		if (conf->ocsp_store == NULL) goto error;
+	}
+#endif /*HAVE_OPENSSL_OCSP_H*/
+
+	{
+		char *dh_file;
+
+		memcpy(&dh_file, &conf->dh_file, sizeof(dh_file));
+		if (load_dh_params(conf->ctx, dh_file) < 0) {
+			goto error;
+		}
+	}
+
+	if (conf->verify_tmp_dir) {
+		if (chmod(conf->verify_tmp_dir, S_IRWXU) < 0) {
+			ERROR(LOG_PREFIX ": Failed changing permissions on %s: %s",
+			      conf->verify_tmp_dir, fr_syserror(errno));
+			goto error;
+		}
+	}
+
+	if (conf->verify_client_cert_cmd && !conf->verify_tmp_dir) {
+		ERROR(LOG_PREFIX ": You MUST set the 'tmpdir' directory in order to use '%s' cmd", conf->verify_client_cert_cmd);
+		goto error;
+	}
+
+#ifdef SSL_OP_NO_TLSv1_2
+	/*
+	 *	OpenSSL 1.0.1f and 1.0.1g get the MS-MPPE keys wrong.
+	 */
+#if (OPENSSL_VERSION_NUMBER >= 0x1010106L) && (OPENSSL_VERSION_NUMBER <= 0x1010107L)
+	conf->disable_tlsv1_2 = true;
+	WARN(LOG_PREFIX ": Disabling TLSv1.2 due to OpenSSL bugs");
+#endif
+#endif
+
+	/*
+	 *	Load certificates and private keys from the realm directory.
+	 */
+	if (conf->realm_dir && (tls_realms_load(conf) < 0)) goto error;
+
+	/*
+	 *	Cache conf in cs in case we're asked to parse this again.
+	 */
+	cf_data_add(cs, "tls-conf", conf, NULL);
+
+	return conf;
+}
+
+fr_tls_server_conf_t *tls_client_conf_parse(CONF_SECTION *cs)
+{
+	fr_tls_server_conf_t *conf;
+
+	conf = cf_data_find(cs, "tls-conf");
+	if (conf) {
+		DEBUG2(LOG_PREFIX ": Using cached TLS configuration from previous invocation");
+		return conf;
+	}
+
+	conf = tls_server_conf_alloc(cs);
+
+	if (cf_section_parse(cs, conf, tls_client_config) < 0) {
+	error:
+		talloc_free(conf);
+		return NULL;
+	}
+
+	/*
+	 *	Save people from their own stupidity.
+	 */
+	if (conf->fragment_size < 100) conf->fragment_size = 100;
+
+	/*
+	 *	Initialize TLS
+	 */
+	conf->ctx = tls_init_ctx(conf, 1, NULL, NULL);
+	if (conf->ctx == NULL) {
+		goto error;
+	}
+
+	{
+		char *dh_file;
+
+		memcpy(&dh_file, &conf->dh_file, sizeof(dh_file));
+		if (load_dh_params(conf->ctx, dh_file) < 0) {
+			goto error;
+		}
+	}
+
+	cf_data_add(cs, "tls-conf", conf, NULL);
+
+	return conf;
+}
+
+
+int tls_success(tls_session_t *ssn, REQUEST *request)
+{
+	VALUE_PAIR *vp, *vps = NULL;
+	fr_tls_server_conf_t *conf;
+	TALLOC_CTX *talloc_ctx;
+
+	conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssn->ssl, FR_TLS_EX_INDEX_CONF);
+	rad_assert(conf != NULL);
+
+	talloc_ctx = SSL_get_ex_data(ssn->ssl, FR_TLS_EX_INDEX_TALLOC);
+
+	/*
+	 *	If there's no session resumption, delete the entry
+	 *	from the cache.  This means either it's disabled
+	 *	globally for this SSL context, OR we were told to
+	 *	disable it for this user.
+	 *
+	 *	This also means you can't turn it on just for one
+	 *	user.
+	 */
+	if ((!ssn->allow_session_resumption) ||
+	    (((vp = fr_pair_find_by_num(request->config, PW_ALLOW_SESSION_RESUMPTION, 0, TAG_ANY)) != NULL) &&
+	     (vp->vp_integer == 0))) {
+		SSL_CTX_remove_session(ssn->ctx,
+				       ssn->ssl_session);
+		ssn->allow_session_resumption = false;
+
+		/*
+		 *	If we're in a resumed session and it's
+		 *	not allowed,
+		 */
+		if (SSL_session_reused(ssn->ssl)) {
+			RDEBUG("(TLS) cache - Forcibly stopping session resumption as it is administratively disabled.");
+			return -1;
+		}
+
+	/*
+	 *	Else resumption IS allowed, so we store the
+	 *	user data in the cache.
+	 */
+	} else if ((!SSL_session_reused(ssn->ssl)) || ssn->session_not_resumed) {
+		VALUE_PAIR **certs;
+		char buffer[2 * MAX_SESSION_SIZE + 1];
+
+		tls_session_id(ssn->ssl_session, buffer, MAX_SESSION_SIZE);
+
+		RDEBUG("(TLS) cache - Setting up attributes for session resumption");
+
+		vp = fr_pair_list_copy_by_num(talloc_ctx, request->reply->vps, PW_USER_NAME, 0, TAG_ANY);
+		if (vp) fr_pair_add(&vps, vp);
+
+		vp = fr_pair_list_copy_by_num(talloc_ctx, request->packet->vps, PW_STRIPPED_USER_NAME, 0, TAG_ANY);
+		if (vp) fr_pair_add(&vps, vp);
+
+		vp = fr_pair_list_copy_by_num(talloc_ctx, request->packet->vps, PW_STRIPPED_USER_DOMAIN, 0, TAG_ANY);
+		if (vp) fr_pair_add(&vps, vp);
+
+		vp = fr_pair_list_copy_by_num(talloc_ctx, request->packet->vps, PW_EAP_TYPE, 0, TAG_ANY);
+		if (vp) fr_pair_add(&vps, vp);
+
+		vp = fr_pair_list_copy_by_num(talloc_ctx, request->reply->vps, PW_CHARGEABLE_USER_IDENTITY, 0, TAG_ANY);
+		if (vp) fr_pair_add(&vps, vp);
+
+		vp = fr_pair_list_copy_by_num(talloc_ctx, request->reply->vps, PW_CACHED_SESSION_POLICY, 0, TAG_ANY);
+		if (vp) fr_pair_add(&vps, vp);
+
+		if (conf->cache_ht) {
+			vp_cursor_t cursor;
+
+			/* Write each attribute/value to the log file */
+			for (vp = fr_cursor_init(&cursor, &request->reply->vps);
+			     vp;
+			     vp = fr_cursor_next(&cursor)) {
+				VALUE_PAIR *copy;
+
+				if (!fr_hash_table_finddata(conf->cache_ht, vp->da)) {
+					continue;
+				}
+
+				copy = fr_pair_copy(talloc_ctx, vp);
+				if (copy) fr_pair_add(&vps, copy);
+			}
+		}
+
+		/*
+		 *	Hmm... the certs should probably be session data.
+		 */
+		certs = (VALUE_PAIR **)SSL_get_ex_data(ssn->ssl, fr_tls_ex_index_certs);
+		if (certs) {
+			/*
+			 *	@todo: some go into reply, others into
+			 *	request
+			 */
+			fr_pair_add(&vps, fr_pair_list_copy(talloc_ctx, *certs));
+
+			vp = fr_pair_find_by_num(vps, PW_TLS_CLIENT_CERT_EXPIRATION, 0, TAG_ANY);
+			if (vp) {
+				time_t expires;
+
+				if (ocsp_asn1time_to_epoch(&expires, vp->vp_strvalue) < 0) {
+					RDEBUG2("Failed getting certificate expiration, removing cache entry for session %s", buffer);
+					SSL_CTX_remove_session(ssn->ctx, ssn->ssl_session);
+					return -1;
+				}
+
+				if (expires <= request->timestamp) {
+					RDEBUG2("Certificate has expired, removing cache entry for session %s", buffer);
+					SSL_CTX_remove_session(ssn->ctx, ssn->ssl_session);
+					return -1;
+				}
+
+				/*
+				 *	Account for Session-Timeout, if it's available.
+				 */
+				vp = fr_pair_find_by_num(request->reply->vps, PW_SESSION_TIMEOUT, 0, TAG_ANY);
+				if (vp) {
+					if ((request->timestamp + vp->vp_integer) > expires) {
+						vp->vp_integer = expires - request->timestamp;
+						RWDEBUG2("(TLS) Updating Session-Timeout to %u, due to impending certificate expiration",
+							 vp->vp_integer);
+					}
+				}
+			}
+		}
+
+		if (vps) {
+			SSL_SESSION_set_ex_data(ssn->ssl_session, fr_tls_ex_index_vps, vps);
+			rdebug_pair_list(L_DBG_LVL_2, request, vps, "  caching ");
+
+			if (conf->session_cache_path) {
+				/* write the VPs to the cache file */
+				char filename[3 * MAX_SESSION_SIZE + 1], buf[1024];
+				FILE *vp_file;
+
+				RDEBUG2("Saving session %s in the disk cache", buffer);
+
+				snprintf(filename, sizeof(filename), "%s%c%s.vps", conf->session_cache_path,
+					 FR_DIR_SEP, buffer);
+				vp_file = fopen(filename, "w");
+				if (vp_file == NULL) {
+					RWDEBUG("(TLS) Could not write session VPs to persistent cache: %s",
+						fr_syserror(errno));
+				} else {
+					VALUE_PAIR *prev = NULL;
+					vp_cursor_t cursor;
+					/* generate a dummy user-style entry which is easy to read back */
+					fprintf(vp_file, "# SSL cached session\n");
+					fprintf(vp_file, "%s\n\t", buffer);
+
+					for (vp = fr_cursor_init(&cursor, &vps);
+					     vp;
+					     vp = fr_cursor_next(&cursor)) {
+						/*
+						 *	Terminate the previous line.
+						 */
+						if (prev) fprintf(vp_file, ",\n\t");
+
+						/*
+						 *	Write this one.
+						 */
+						vp_prints(buf, sizeof(buf), vp);
+						fputs(buf, vp_file);
+						prev = vp;
+					}
+
+					/*
+					 *	Terminate the final line.
+					 */
+					fprintf(vp_file, "\n");
+					fclose(vp_file);
+				}
+
+			} else if (conf->session_cache_server) {
+				cbtls_cache_save_vps(ssn->ssl, ssn->ssl_session, vps);
+
+			} else {
+				RDEBUG("Failed to find 'persist_dir' in TLS configuration.  Session will not be cached on disk.");
+			}
+		} else {
+			RDEBUG2("No information to cache: session caching will be disabled for session %s", buffer);
+			SSL_CTX_remove_session(ssn->ctx, ssn->ssl_session);
+		}
+
+	/*
+	 *	Else the session WAS allowed.  Copy the cached reply.
+	 */
+	} else {
+		RDEBUG("(TLS) cache - Refreshing entry for session resumption");
+
+		/*
+		 *	The "restore VPs from OpenSSL cache" code is
+		 *	now in eaptls_process()
+		 */
+		if (conf->session_cache_path) {
+			char buffer[2 * MAX_SESSION_SIZE + 1];
+
+#if OPENSSL_VERSION_NUMBER >= 0x10001000L
+#ifdef TLS1_3_VERSION
+			/*
+			 *	OpenSSL frees the underlying session out from
+			 *	under us in TLS 1.3.
+			 */
+			if (SSL_version(ssn->ssl) == TLS1_3_VERSION) ssn->ssl_session = SSL_get_session(ssn->ssl);
+#endif
+#endif
+
+			tls_session_id(ssn->ssl_session, buffer, MAX_SESSION_SIZE);
+
+			/* "touch" the cached session/vp file */
+			char filename[3 * MAX_SESSION_SIZE + 1];
+
+			snprintf(filename, sizeof(filename), "%s%c%s.asn1",
+				 conf->session_cache_path, FR_DIR_SEP, buffer);
+			utime(filename, NULL);
+			snprintf(filename, sizeof(filename), "%s%c%s.vps",
+				 conf->session_cache_path, FR_DIR_SEP, buffer);
+			utime(filename, NULL);
+		}
+
+		if (conf->session_cache_server) {
+			cbtls_cache_refresh(ssn->ssl, ssn->ssl_session);
+		}
+
+		/*
+		 *	Mark the request as resumed.
+		 */
+		pair_make_request("EAP-Session-Resumed", "1", T_OP_SET);
+		RDEBUG("  &request:EAP-Session-Resumed := 1");
+	}
+
+	return 0;
+}
+
+
+void tls_fail(tls_session_t *ssn)
+{
+	/*
+	 *	Force the session to NOT be cached.
+	 */
+	SSL_CTX_remove_session(ssn->ctx, ssn->ssl_session);
+}
+
+fr_tls_status_t tls_application_data(tls_session_t *ssn, REQUEST *request)
+
+{
+	int err;
+	VALUE_PAIR **certs;
+
+	/*
+	 *	Decrypt the complete record.
+	 */
+	if (ssn->dirty_in.used > 0) {
+		err = BIO_write(ssn->into_ssl, ssn->dirty_in.data,
+				ssn->dirty_in.used);
+		if (err != (int) ssn->dirty_in.used) {
+			REDEBUG("(TLS) Failed writing %zd bytes to SSL BIO: %d", ssn->dirty_in.used, err);
+			record_init(&ssn->dirty_in);
+			return FR_TLS_FAIL;
+		}
+
+		record_init(&ssn->dirty_in);
+	}
+
+	/*
+	 *	tls_handshake_recv() may read application data.  So
+	 *	don't touch clean_out.  But only if the BIO_write()
+	 *	above didn't do anything.
+	 */
+	else if (ssn->clean_out.used > 0) {
+		RDEBUG("(TLS) We already have %zd bytes of application data, processing it.",
+		       (ssn->clean_out.used));
+		goto add_certs;
+	}
+
+	/*
+	 *      Read (and decrypt) the tunneled data from the
+	 *      SSL session, and put it into the decrypted
+	 *      data buffer.
+	 */
+	err = SSL_read(ssn->ssl, ssn->clean_out.data + ssn->clean_out.used,
+		       sizeof(ssn->clean_out.data) - ssn->clean_out.used);
+	if (err <= 0) {
+		int code;
+
+		RDEBUG3("(TLS) SSL_read Error");
+
+		code = SSL_get_error(ssn->ssl, err);
+		switch (code) {
+		case SSL_ERROR_WANT_READ:
+			if (ssn->clean_out.used > 0) { /* just process what application data we have */
+				err = 0;
+				break;
+			}
+
+			RDEBUG("(TLS) OpenSSL says that it needs to read more data.");
+			return FR_TLS_MORE_FRAGMENTS;
+
+		case SSL_ERROR_WANT_WRITE:
+			if (ssn->clean_out.used > 0) { /* just process what application data we have */
+				err = 0;
+				break;
+			}
+
+			REDEBUG("(TLS) Error in fragmentation logic: SSL_WANT_WRITE");
+			return FR_TLS_FAIL;
+
+		case SSL_ERROR_NONE:
+			RDEBUG2("(TLS) No application data received.  Assuming handshake is continuing...");
+			err = 0;
+			break;
+
+		case SSL_ERROR_ZERO_RETURN:
+			RDEBUG2("(TLS) Other end closed the TLS tunnel.");
+			return FR_TLS_FAIL;
+
+		default:
+			REDEBUG("(TLS) Error in fragmentation logic - code %d", code);
+			tls_error_io_log(request, ssn, err, "Failed reading application data from OpenSSL");
+			return FR_TLS_FAIL;
+		}
+	}
+
+	/*
+	 *	Passed all checks, successfully decrypted data
+	 */
+	ssn->clean_out.used += err;
+
+add_certs:
+	/*
+	 *	Add the certificates to intermediate packets, so that
+	 *	the inner tunnel policies can use them.
+	 */
+	certs = (VALUE_PAIR **)SSL_get_ex_data(ssn->ssl, fr_tls_ex_index_certs);
+
+	if (certs) fr_pair_add(&request->packet->vps, fr_pair_list_copy(request->packet, *certs));
+
+	return FR_TLS_OK;
+}
+
+
+/*
+ * Acknowledge received is for one of the following messages sent earlier
+ * 1. Handshake completed Message, so now send, EAP-Success
+ * 2. Alert Message, now send, EAP-Failure
+ * 3. Fragment Message, now send, next Fragment
+ */
+fr_tls_status_t tls_ack_handler(tls_session_t *ssn, REQUEST *request)
+{
+	if (ssn == NULL){
+		REDEBUG("(TLS) Unexpected ACK received:  No ongoing SSL session");
+		return FR_TLS_INVALID;
+	}
+	if (!ssn->info.initialized) {
+		RDEBUG("(TLS) No SSL info available.  Waiting for more SSL data");
+		return FR_TLS_REQUEST;
+	}
+
+	if ((ssn->info.content_type == handshake) && (ssn->info.origin == 0)) {
+		REDEBUG("(TLS) Unexpected ACK received:  We sent no previous messages");
+		return FR_TLS_INVALID;
+	}
+
+	switch (ssn->info.content_type) {
+	case alert:
+		RDEBUG2("(TLS) Peer ACKed our alert");
+		return FR_TLS_FAIL;
+
+	case handshake:
+		if (ssn->dirty_out.used > 0) {
+			RDEBUG2("(TLS) Peer ACKed our handshake fragment");
+			/* Fragmentation handler, send next fragment */
+			return FR_TLS_REQUEST;
+		}
+
+		if (ssn->is_init_finished || SSL_is_init_finished(ssn->ssl)) {
+			RDEBUG2("(TLS) Peer ACKed our handshake fragment.  handshake is finished");
+
+			/*
+			 *	From now on all the content is
+			 *	application data set it here as nobody else
+			 *	sets it.
+			 */
+			ssn->info.content_type = application_data;
+			return FR_TLS_SUCCESS;
+		} /* else more data to send */
+
+		REDEBUG("(TLS) Cannot continue, as the peer is misbehaving.");
+		return FR_TLS_FAIL;
+
+	case application_data:
+		RDEBUG2("(TLS) Peer ACKed our application data fragment");
+		return FR_TLS_REQUEST;
+
+		/*
+		 *	For the rest of the conditions, switch over
+		 *	to the default section below.
+		 */
+	default:
+		REDEBUG("(TLS) Invalid ACK received: %d", ssn->info.content_type);
+		return FR_TLS_INVALID;
+	}
+}
+#endif	/* WITH_TLS */
+
diff --git a/src/main/tls_listen.c b/src/main/tls_listen.c
new file mode 100644
index 0000000..fa8c382
--- /dev/null
+++ b/src/main/tls_listen.c
@@ -0,0 +1,1568 @@
+/*
+ * tls.c
+ *
+ * Version:     $Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2001  hereUare Communications, Inc. <raghud@hereuare.com>
+ * Copyright 2003  Alan DeKok <aland@freeradius.org>
+ * Copyright 2006  The FreeRADIUS server project
+ */
+
+RCSID("$Id$")
+USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/process.h>
+#include <freeradius-devel/rad_assert.h>
+
+#ifdef HAVE_SYS_STAT_H
+#include <sys/stat.h>
+#endif
+
+#ifdef WITH_TCP
+#ifdef WITH_TLS
+#ifdef HAVE_OPENSSL_RAND_H
+#include <openssl/rand.h>
+#endif
+
+#ifdef HAVE_OPENSSL_OCSP_H
+#include <openssl/ocsp.h>
+#endif
+
+#ifdef HAVE_PTHREAD_H
+#define PTHREAD_MUTEX_LOCK pthread_mutex_lock
+#define PTHREAD_MUTEX_UNLOCK pthread_mutex_unlock
+#else
+#define PTHREAD_MUTEX_LOCK(_x)
+#define PTHREAD_MUTEX_UNLOCK(_x)
+#endif
+
+static void dump_hex(char const *msg, uint8_t const *data, size_t data_len)
+{
+	size_t i;
+
+	if (rad_debug_lvl < 3) return;
+
+	printf("%s %d\n", msg, (int) data_len);
+	if (data_len > 256) data_len = 256;
+
+	for (i = 0; i < data_len; i++) {
+		if ((i & 0x0f) == 0x00) printf ("%02x: ", (unsigned int) i);
+		printf("%02x ", data[i]);
+		if ((i & 0x0f) == 0x0f) printf ("\n");
+	}
+	printf("\n");
+	fflush(stdout);
+}
+
+static void tls_socket_close(rad_listen_t *listener)
+{
+	listen_socket_t *sock = listener->data;
+
+	SSL_shutdown(sock->ssn->ssl);
+
+	listener->status = RAD_LISTEN_STATUS_EOL;
+	listener->tls = NULL; /* parent owns this! */
+
+	/*
+	 *	Tell the event handler that an FD has disappeared.
+	 */
+	DEBUG("(TLS) Closing connection");
+	radius_update_listener(listener);
+
+	/*
+	 *	Do NOT free the listener here.  It may be in use by
+	 *	a request, and will need to hang around until
+	 *	all of the requests are done.
+	 *
+	 *	It is instead free'd when all of the requests using it
+	 *	are done.
+	 */
+}
+
+static void tls_write_available(fr_event_list_t *el, int sock, void *ctx);
+
+static int CC_HINT(nonnull) tls_socket_write(rad_listen_t *listener)
+{
+	ssize_t rcode;
+	listen_socket_t *sock = listener->data;
+
+	/*
+	 *	It's not writable, so we don't bother writing to it.
+	 */
+	if (listener->blocked) return 0;
+
+	/*
+	 *	Write as much as possible.
+	 */
+	rcode = write(listener->fd, sock->ssn->dirty_out.data, sock->ssn->dirty_out.used);
+	if (rcode <= 0) {
+#ifdef EWOULDBLOCK
+		/*
+		 *	Writing to the socket would cause it to block.
+		 *	As a result, we just mark it as "don't use"
+		 *	until such time as it becomes writable.
+		 */
+		if (errno == EWOULDBLOCK) {
+			proxy_listener_freeze(listener, tls_write_available);
+			return 0;
+		}
+#endif
+
+
+		ERROR("(TLS) Error writing to socket: %s", fr_syserror(errno));
+
+		tls_socket_close(listener);
+		return -1;
+	}
+
+	/*
+	 *	All of the data was written.  It's fine.
+	 */
+	if ((size_t) rcode == sock->ssn->dirty_out.used) {
+		sock->ssn->dirty_out.used = 0;
+		return 0;
+	}
+
+	/*
+	 *	Move the data to the start of the buffer.
+	 *
+	 *	Yes, this is horrible.  But doing this means that we
+	 *	don't have to modify the rest of the code which mangles dirty_out, and assumes that the write offset is always &data[used].
+	 */
+	memmove(&sock->ssn->dirty_out.data[0], &sock->ssn->dirty_out.data[rcode], sock->ssn->dirty_out.used - rcode);
+	sock->ssn->dirty_out.used -= rcode;
+
+	return 0;
+}
+
+static void tls_write_available(UNUSED fr_event_list_t *el, UNUSED int fd, void *ctx)
+{
+	rad_listen_t *listener = ctx;
+	listen_socket_t *sock = listener->data;
+
+	proxy_listener_thaw(listener);
+
+	PTHREAD_MUTEX_LOCK(&sock->mutex);
+	(void) tls_socket_write(listener);
+	PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+}
+
+
+/*
+ *	Check for PROXY protocol.  Once that's done, clear
+ *	listener->proxy_protocol.
+ */
+static int proxy_protocol_check(rad_listen_t *listener, REQUEST *request)
+{
+	listen_socket_t *sock = listener->data;
+	uint8_t const *p, *end, *eol;
+	int af, argc, src_port, dst_port;
+	unsigned long num;
+	fr_ipaddr_t src, dst;
+	char *argv[5], *eos;
+	ssize_t rcode;
+	RADCLIENT *client;
+
+	/*
+	 *	Begin by trying to fill the buffer.
+	 */
+	rcode = read(request->packet->sockfd,
+		     sock->ssn->dirty_in.data + sock->ssn->dirty_in.used,
+		     sizeof(sock->ssn->dirty_in.data) - sock->ssn->dirty_in.used);
+	if (rcode < 0) {
+		if (errno == EINTR) return 0;
+		RDEBUG("(TLS) Closing PROXY socket from client port %u due to read error - %s", sock->other_port, fr_syserror(errno));
+		return -1;
+	}
+
+	if (rcode == 0) {
+		DEBUG("(TLS) Closing PROXY socket from client port %u - other end closed connection", sock->other_port);
+		return -1;
+	}
+
+	/*
+	 *	We've read data, scan the buffer for a CRLF.
+	 */
+	sock->ssn->dirty_in.used += rcode;
+
+	dump_hex("READ FROM PROXY PROTOCOL SOCKET", sock->ssn->dirty_in.data, sock->ssn->dirty_in.used);
+
+	p = sock->ssn->dirty_in.data;
+
+	/*
+	 *	CRLF MUST be within the first 107 bytes.
+	 */
+	if (sock->ssn->dirty_in.used < 107) {
+		end = p + sock->ssn->dirty_in.used;
+	} else {
+		end = p + 107;
+	}
+	eol = NULL;
+
+	/*
+	 *	Scan for CRLF.
+	 */
+	while ((p + 1) < end) {
+		if ((p[0] == 0x0d) && (p[1] == 0x0a)) {
+			eol = p;
+			break;
+		}
+
+		/*
+		 *	Other control characters, or non-ASCII data.
+		 *	That's a problem.
+		 */
+		if ((*p < ' ') || (*p >= 0x80)) {
+		invalid_data:
+			DEBUG("(TLS) Closing PROXY socket from client port %u - received invalid data", sock->other_port);
+			return -1;
+		}
+
+		p++;
+	}
+
+	/*
+	 *	No CRLF, keep reading until we have it.
+	 */
+	if (!eol) return 0;
+
+	p = sock->ssn->dirty_in.data;
+
+	/*
+	 *	Let's see if the PROXY line is well-formed.
+	 */
+	if ((eol - p) < 14) goto invalid_data;
+	
+	/*
+	 *	We only support TCP4 and TCP6.
+	 */
+	if (memcmp(p, "PROXY TCP", 9) != 0) goto invalid_data;
+
+	p += 9;
+
+	if (*p == '4') {
+		af = AF_INET;
+
+	} else if (*p == '6') {
+		af = AF_INET6;
+
+	} else goto invalid_data;
+
+	p++;
+	if (*p != ' ') goto invalid_data;
+	p++;
+
+	sock->ssn->dirty_in.data[eol - sock->ssn->dirty_in.data] = '\0'; /* overwite the CRLF */
+
+	/*
+	 *	Parse the fields (being a little forgiving), while
+	 *	checking for too many / too few fields.
+	 */
+	argc = str2argv((char *) &sock->ssn->dirty_in.data[p - sock->ssn->dirty_in.data], (char **) &argv, 5);
+	if (argc != 4) goto invalid_data;
+
+	memset(&src, 0, sizeof(src));
+	memset(&dst, 0, sizeof(dst));
+
+	if (fr_pton(&src, argv[0], -1, af, false) < 0) goto invalid_data;
+	if (fr_pton(&dst, argv[1], -1, af, false) < 0) goto invalid_data;
+
+	num = strtoul(argv[2], &eos, 10);
+	if (num > 65535) goto invalid_data;
+	if (*eos) goto invalid_data;
+	src_port = num;
+
+	num = strtoul(argv[3], &eos, 10);
+	if (num > 65535) goto invalid_data;
+	if (*eos) goto invalid_data;
+	dst_port = num;
+
+	/*
+	 *	And copy the various fields around.
+	 */
+	sock->haproxy_src_ipaddr = sock->other_ipaddr;
+	sock->haproxy_src_port = sock->other_port;
+
+	sock->haproxy_dst_ipaddr = sock->my_ipaddr;
+	sock->haproxy_dst_port = sock->my_port;
+
+	sock->my_ipaddr = dst;
+	sock->my_port = dst_port;
+
+	sock->other_ipaddr = src;
+	sock->other_port = src_port;
+
+	/*
+	 *	Print out what we've changed.  Note that the TCP
+	 *	socket address family and the PROXY address family may
+	 *	be different!
+	 */
+	if (RDEBUG_ENABLED) {
+		char src_buf[128], dst_buf[128];
+
+		RDEBUG("(TLS) Received PROXY protocol connection from client %s:%s -> %s:%s, via proxy %s:%u -> %s:%u",
+		       argv[0], argv[2], argv[1], argv[3],
+		       inet_ntop(af, &sock->haproxy_src_ipaddr.ipaddr, src_buf, sizeof(src_buf)),
+		       sock->haproxy_src_port,
+		       inet_ntop(af, &sock->haproxy_dst_ipaddr.ipaddr, dst_buf, sizeof(dst_buf)),
+		       sock->haproxy_dst_port);
+	}
+
+        /*
+         *      Ensure that the source IP indicated by the PROXY
+         *      protocol is a known TLS client.
+         */
+        if ((client = client_listener_find(listener, &src, src_port)) == NULL ||
+             client->proto != IPPROTO_TCP) {
+		RDEBUG("(TLS) Unknown client %s - dropping PROXY protocol connection", argv[0]);
+		return -1;
+        }
+
+        /*
+         *      Use the client indicated by the proxy.
+         */
+        sock->client = client;
+
+	/*
+         *      Fix up the current request so that the first packet's
+         *      src/dst is valid.  Subsequent packets will get the
+         *      clients IP from the listener and listen_sock
+         *      structures.
+         */
+        request->packet->dst_ipaddr = dst;
+        request->packet->dst_port = dst_port;
+        request->packet->src_ipaddr = src;
+        request->packet->src_port = src_port;
+
+	/*
+	 *	Move any remaining TLS data to the start of the buffer.
+	 */
+	eol += 2;
+	end = sock->ssn->dirty_in.data + sock->ssn->dirty_in.used;
+	if (eol < end) {
+		memmove(sock->ssn->dirty_in.data, eol, end - eol);
+		sock->ssn->dirty_in.used = end - eol;
+	} else {
+		sock->ssn->dirty_in.used = 0;
+	}
+		
+	/*
+	 *	It's no longer a PROXY protocol, but just straight TLS.
+	 */
+	listener->proxy_protocol = false;
+
+	return 1;
+}
+
+static int tls_socket_recv(rad_listen_t *listener)
+{
+	bool doing_init = false, already_read = false;
+	ssize_t rcode;
+	RADIUS_PACKET *packet;
+	REQUEST *request;
+	listen_socket_t *sock = listener->data;
+	fr_tls_status_t status;
+	RADCLIENT *client = sock->client;
+
+	if (!sock->packet) {
+		sock->packet = rad_alloc(sock, false);
+		if (!sock->packet) return 0;
+
+		sock->packet->sockfd = listener->fd;
+		sock->packet->src_ipaddr = sock->other_ipaddr;
+		sock->packet->src_port = sock->other_port;
+		sock->packet->dst_ipaddr = sock->my_ipaddr;
+		sock->packet->dst_port = sock->my_port;
+
+		if (sock->request) sock->request->packet = talloc_steal(sock->request, sock->packet);
+	}
+
+	/*
+	 *	Allocate a REQUEST for debugging, and initialize the TLS session.
+	 */
+	if (!sock->request) {
+		sock->request = request = request_alloc(sock);
+		if (!sock->request) {
+			ERROR("Out of memory");
+			return 0;
+		}
+
+		rad_assert(request->packet == NULL);
+		rad_assert(sock->packet != NULL);
+		request->packet = talloc_steal(request, sock->packet);
+
+		request->component = "<tls-connect>";
+
+		request->reply = rad_alloc(request, false);
+		if (!request->reply) return 0;
+
+		rad_assert(sock->ssn == NULL);
+
+		sock->ssn = tls_new_session(sock, listener->tls, sock->request,
+					    listener->tls->require_client_cert, true);
+		if (!sock->ssn) {
+			TALLOC_FREE(sock->request);
+			sock->packet = NULL;
+			return 0;
+		}
+
+		SSL_set_ex_data(sock->ssn->ssl, FR_TLS_EX_INDEX_REQUEST, (void *)request);
+		SSL_set_ex_data(sock->ssn->ssl, fr_tls_ex_index_certs, (void *) &sock->certs);
+		SSL_set_ex_data(sock->ssn->ssl, FR_TLS_EX_INDEX_TALLOC, sock);
+
+		sock->ssn->quick_session_tickets = true; /* we don't have inner-tunnel authentication */
+
+		doing_init = true;
+	}
+
+	rad_assert(sock->request != NULL);
+	rad_assert(sock->request->packet != NULL);
+	rad_assert(sock->packet != NULL);
+	rad_assert(sock->ssn != NULL);
+
+	request = sock->request;
+
+	/*
+	 *	Bypass ALL of the TLS stuff until we've read the PROXY
+	 *	header.
+	 *
+	 *	If the PROXY header checks pass, then the flag is
+	 *	cleared, as we don't need it any more.
+	 */
+	if (listener->proxy_protocol) {
+		rcode = proxy_protocol_check(listener, request);
+		if (rcode < 0) {
+			RDEBUG("(TLS) Closing PROXY TLS socket from client port %u", sock->other_port);
+			tls_socket_close(listener);
+			return 0;
+		}
+		if (rcode == 0) return 1;
+
+		/*
+		 *	The buffer might already have data.  In that
+		 *	case, we don't want to do a blocking read
+		 *	later.
+		 */
+		already_read = (sock->ssn->dirty_in.used > 0);
+	}
+
+	if (sock->state == LISTEN_TLS_SETUP) {
+		RDEBUG3("(TLS) Setting connection state to RUNNING");
+		sock->state = LISTEN_TLS_RUNNING;
+
+		if (sock->ssn->clean_out.used < 20) {
+			goto get_application_data;
+		}
+
+		goto read_application_data;
+	}
+
+	RDEBUG3("(TLS) Reading from socket %d", request->packet->sockfd);
+	PTHREAD_MUTEX_LOCK(&sock->mutex);
+
+	/*
+	 *	If there is pending application data, as set up by
+	 *	SSL_peek(), read that before reading more data from
+	 *	the socket.
+	 */
+	if (SSL_pending(sock->ssn->ssl)) {
+		RDEBUG3("(TLS) Reading pending buffered data");
+		sock->ssn->dirty_in.used = 0;
+		goto check_for_setup;
+	}
+
+	if (!already_read) {
+		rcode = read(request->packet->sockfd,
+			     sock->ssn->dirty_in.data,
+			     sizeof(sock->ssn->dirty_in.data));
+		if ((rcode < 0) && (errno == ECONNRESET)) {
+		do_close:
+			DEBUG("(TLS) Closing socket from client port %u", sock->other_port);
+			tls_socket_close(listener);
+			PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+			return 0;
+		}
+
+		if (rcode < 0) {
+			RDEBUG("(TLS) Error reading socket: %s", fr_syserror(errno));
+			goto do_close;
+		}
+
+		/*
+		 *	Normal socket close.
+		 */
+		if (rcode == 0) {
+			RDEBUG("(TLS) Client has closed the TCP connection");
+			goto do_close;
+		}
+
+		sock->ssn->dirty_in.used = rcode;
+	}
+
+	dump_hex("READ FROM SSL", sock->ssn->dirty_in.data, sock->ssn->dirty_in.used);
+
+	/*
+	 *	Catch attempts to use non-SSL.
+	 */
+	if (doing_init && (sock->ssn->dirty_in.data[0] != handshake)) {
+		RDEBUG("(TLS) Non-TLS data sent to TLS socket: closing");
+		goto do_close;
+	}
+
+	/*
+	 *	If we need to do more initialization, do that here.
+	 */
+check_for_setup:
+	if (!sock->ssn->is_init_finished) {
+		if (!tls_handshake_recv(request, sock->ssn)) {
+			RDEBUG("(TLS) Failed in TLS handshake receive");
+			goto do_close;
+		}
+
+		/*
+		 *	More ACK data to send.  Do so.
+		 */
+		if (sock->ssn->dirty_out.used > 0) {
+			RDEBUG3("(TLS) Writing to socket %d", listener->fd);
+			tls_socket_write(listener);
+			PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+			return 0;
+		}
+
+		/*
+		 *      If SSL handshake still isn't finished, then there
+		 *      is more data to read.  Release the mutex and
+		 *      return so this function will be called again
+		 */
+		if (!SSL_is_init_finished(sock->ssn->ssl)) {
+			PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+			return 0;
+		}
+	}
+
+	/*
+	 *	Run the request through a virtual server in
+	 *	order to see if we like the certificate
+	 *	presented by the client.
+	 */
+	if (sock->state == LISTEN_TLS_INIT) {
+		if (!SSL_is_init_finished(sock->ssn->ssl)) {
+			RDEBUG("(TLS) OpenSSL says that the TLS session is still negotiating, but there's no more data to send!");
+			goto do_close;
+		}
+
+		sock->ssn->is_init_finished = true;
+		if (!listener->check_client_connections) {
+			sock->state = LISTEN_TLS_RUNNING;
+			goto get_application_data;
+		}
+
+		request->packet->vps = fr_pair_list_copy(request->packet, sock->certs);
+
+		/*
+		 *	Fake out a Status-Server packet, which
+		 *	does NOT have a Message-Authenticator,
+		 *	or any other contents.
+		 */
+		request->packet->code = PW_CODE_STATUS_SERVER;
+		request->packet->data = talloc_zero_array(request->packet, uint8_t, 20);
+		request->packet->data[0] = PW_CODE_STATUS_SERVER;
+		request->packet->data[3] = 20;
+		request->listener = listener;
+		sock->state = LISTEN_TLS_CHECKING;
+		PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+
+		/*
+		 *	Don't read from the socket until the request
+		 *	returns.
+		 */
+		listener->status = RAD_LISTEN_STATUS_PAUSE;
+		radius_update_listener(listener);
+
+		return 1;
+	}
+
+	/*
+	 *	Try to get application data.
+	 */
+get_application_data:
+	/*
+	 *	More data to send.  Do so.
+	 */
+	if (sock->ssn->dirty_out.used > 0) {
+		RDEBUG3("(TLS) Writing to socket %d", listener->fd);
+		rcode = tls_socket_write(listener);
+		if (rcode < 0) {
+			PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+			return rcode;
+		}
+	}
+
+	status = tls_application_data(sock->ssn, request);
+	RDEBUG3("(TLS) Application data status %d", status);
+
+	/*
+	 *	Some kind of failure.  Close the socket.
+	 */
+	if (status == FR_TLS_FAIL) {
+		DEBUG("(TLS) Unable to recover from TLS error, closing socket from client port %u", sock->other_port);
+		tls_socket_close(listener);
+		PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+		return 0;
+	}
+
+	if (status == FR_TLS_MORE_FRAGMENTS) {
+		PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+		return 0;
+	}
+
+	if (sock->ssn->clean_out.used == 0) {
+		PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+		return 0;
+	}
+
+	/*
+	 *	Hold application data if we're not yet in the RUNNING
+	 *	state.
+	 */
+	if (sock->state != LISTEN_TLS_RUNNING) {
+		RDEBUG3("(TLS) Holding application data until setup is complete");
+		return 0;
+	}
+
+read_application_data:
+	/*
+	 *	We now have a bunch of application data.
+	 */
+	dump_hex("TUNNELED DATA > ", sock->ssn->clean_out.data, sock->ssn->clean_out.used);
+
+	/*
+	 *	If the packet is a complete RADIUS packet, return it to
+	 *	the caller.  Otherwise...
+	 */
+	if ((sock->ssn->clean_out.used < 20) ||
+	    (((sock->ssn->clean_out.data[2] << 8) | sock->ssn->clean_out.data[3]) != (int) sock->ssn->clean_out.used)) {
+		RDEBUG("(TLS) Received bad packet: Length %zd contents %d",
+		       sock->ssn->clean_out.used,
+		       (sock->ssn->clean_out.data[2] << 8) | sock->ssn->clean_out.data[3]);
+		goto do_close;
+	}
+
+	packet = sock->packet;
+	packet->data = talloc_array(packet, uint8_t, sock->ssn->clean_out.used);
+	packet->data_len = sock->ssn->clean_out.used;
+	sock->ssn->record_minus(&sock->ssn->clean_out, packet->data, packet->data_len);
+	packet->vps = NULL;
+	PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+
+#ifdef WITH_RADIUSV11
+	packet->radiusv11 = sock->radiusv11;
+#endif
+
+	if (!rad_packet_ok(packet, 0, NULL)) {
+		if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror());
+		DEBUG("(TLS) Closing TLS socket from client");
+		PTHREAD_MUTEX_LOCK(&sock->mutex);
+		tls_socket_close(listener);
+		PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+		return 0;	/* do_close unlocks the mutex */
+	}
+
+	/*
+	 *	Copied from src/lib/radius.c, rad_recv();
+	 */
+	if (fr_debug_lvl) {
+		char host_ipaddr[128];
+
+		if (is_radius_code(packet->code)) {
+			RDEBUG("(TLS): %s packet from host %s port %d, id=%d, length=%d",
+			       fr_packet_codes[packet->code],
+			       inet_ntop(packet->src_ipaddr.af,
+					 &packet->src_ipaddr.ipaddr,
+					 host_ipaddr, sizeof(host_ipaddr)),
+			       packet->src_port,
+			       packet->id, (int) packet->data_len);
+		} else {
+			RDEBUG("(TLS): Packet from host %s port %d code=%d, id=%d, length=%d",
+			       inet_ntop(packet->src_ipaddr.af,
+					 &packet->src_ipaddr.ipaddr,
+					 host_ipaddr, sizeof(host_ipaddr)),
+			       packet->src_port,
+			       packet->code,
+			       packet->id, (int) packet->data_len);
+		}
+	}
+
+	FR_STATS_INC(auth, total_requests);
+
+	return 1;
+}
+
+
+int dual_tls_recv(rad_listen_t *listener)
+{
+	RADIUS_PACKET *packet;
+	RAD_REQUEST_FUNP fun = NULL;
+	listen_socket_t *sock = listener->data;
+	RADCLIENT	*client = sock->client;
+	BIO		*rbio;
+#ifdef WITH_COA_TUNNEL
+	bool		is_reply = false;
+#endif
+
+	if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0;
+
+redo:
+	if (!tls_socket_recv(listener)) {
+		return 0;
+	}
+
+	rad_assert(sock->packet != NULL);
+	rad_assert(sock->ssn != NULL);
+	rad_assert(client != NULL);
+
+	packet = talloc_steal(NULL, sock->packet);
+	sock->request->packet = NULL;
+	sock->packet = NULL;
+
+	/*
+	 *	Some sanity checks, based on the packet code.
+	 *
+	 *	"auth+acct" are marked as "auth", with the "dual" flag
+	 *	set.
+	 */
+	switch (packet->code) {
+	case PW_CODE_ACCESS_REQUEST:
+		if (listener->type != RAD_LISTEN_AUTH) goto bad_packet;
+		FR_STATS_INC(auth, total_requests);
+		fun = rad_authenticate;
+		break;
+
+#ifdef WITH_ACCOUNTING
+	case PW_CODE_ACCOUNTING_REQUEST:
+		if (listener->type != RAD_LISTEN_ACCT) {
+			/*
+			 *	Allow auth + dual.  Disallow
+			 *	everything else.
+			 */
+			if (!((listener->type == RAD_LISTEN_AUTH) &&
+			      (listener->dual))) {
+				    goto bad_packet;
+			}
+		}
+		FR_STATS_INC(acct, total_requests);
+		fun = rad_accounting;
+		break;
+#endif
+
+#ifdef WITH_COA
+	case PW_CODE_COA_REQUEST:
+		if (listener->type != RAD_LISTEN_COA) goto bad_packet;
+		FR_STATS_INC(coa, total_requests);
+		fun = rad_coa_recv;
+		break;
+
+	case PW_CODE_DISCONNECT_REQUEST:
+		if (listener->type != RAD_LISTEN_COA) goto bad_packet;
+		FR_STATS_INC(dsc, total_requests);
+		fun = rad_coa_recv;
+		break;
+
+#ifdef WITH_COA_TUNNEL
+	case PW_CODE_COA_ACK:
+	case PW_CODE_COA_NAK:
+		if (!listener->send_coa) goto bad_packet;
+		is_reply = true;
+                break;
+#endif
+#endif
+
+	case PW_CODE_STATUS_SERVER:
+		if (!main_config.status_server
+#ifdef WITH_TLS
+		    && !listener->check_client_connections
+#endif
+			) {
+			FR_STATS_INC(auth, total_unknown_types);
+			WARN("Ignoring Status-Server request due to security configuration");
+			rad_free(&packet);
+			return 0;
+		}
+		fun = rad_status_server;
+		break;
+
+	default:
+	bad_packet:
+		FR_STATS_INC(auth, total_unknown_types);
+
+		DEBUG("(TLS) Invalid packet code %d sent from client %s port %d : IGNORED",
+		      packet->code, client->shortname, packet->src_port);
+		rad_free(&packet);
+		return 0;
+	} /* switch over packet types */
+
+#ifdef WITH_COA_TUNNEL
+	if (is_reply) {
+		if (!request_proxy_reply(packet)) {
+			rad_free(&packet);
+			return 0;
+		}
+	} else
+#endif
+
+	if (!request_receive(NULL, listener, packet, client, fun)) {
+		FR_STATS_INC(auth, total_packets_dropped);
+		rad_free(&packet);
+		return 0;
+	}
+
+	/*
+	 *	Check for more application data.
+	 *
+	 *	If there is pending SSL data, "peek" at the
+	 *	application data.  If we get at least one byte of
+	 *	application data, go back to tls_socket_recv().
+	 *	SSL_peek() will set SSL_pending(), and
+	 *	tls_socket_recv() will read another packet.
+	 */
+	rbio = SSL_get_rbio(sock->ssn->ssl);
+	if (BIO_ctrl_pending(rbio)) {
+		char buf[1];
+		int peek = SSL_peek(sock->ssn->ssl, buf, 1);
+
+		if (peek > 0) {
+			DEBUG("(TLS) more TLS records after dual_tls_recv");
+			goto redo;
+		}
+	}
+
+	return 1;
+}
+
+
+/*
+ *	Send a response packet
+ */
+int dual_tls_send(rad_listen_t *listener, REQUEST *request)
+{
+	listen_socket_t *sock = listener->data;
+
+	VERIFY_REQUEST(request);
+
+	rad_assert(request->listener == listener);
+	rad_assert(listener->send == dual_tls_send);
+
+	if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0;
+
+	/*
+	 *	See if the policies allowed this connection.
+	 */
+	if (sock->state == LISTEN_TLS_CHECKING) {
+		if (request->reply->code != PW_CODE_ACCESS_ACCEPT) {
+			listener->status = RAD_LISTEN_STATUS_EOL;
+			listener->tls = NULL; /* parent owns this! */
+
+			/*
+			 *	Tell the event handler that an FD has disappeared.
+			 */
+			radius_update_listener(listener);
+			return 0;
+		}
+
+		/*
+		 *	Resume reading from the listener.
+		 */
+		listener->status = RAD_LISTEN_STATUS_RESUME;
+		radius_update_listener(listener);
+
+		rad_assert(sock->request->packet != request->packet);
+
+		sock->state = LISTEN_TLS_SETUP;
+		(void) dual_tls_recv(listener);
+		return 0;
+	}
+
+	/*
+	 *	Accounting reject's are silently dropped.
+	 *
+	 *	We do it here to avoid polluting the rest of the
+	 *	code with this knowledge
+	 */
+	if (request->reply->code == 0) return 0;
+
+#ifdef WITH_COA_TUNNEL
+	/*
+	 *	Save the key, if we haven't already done that.
+	 */
+	if (listener->send_coa && !listener->key) {
+		VALUE_PAIR *vp = NULL;
+
+		vp = fr_pair_find_by_num(request->config, PW_ORIGINATING_REALM_KEY, 0, TAG_ANY);
+		if (vp) {
+			RDEBUG("Adding send CoA listener with key %s", vp->vp_strvalue);
+			listen_coa_add(request->listener, vp->vp_strvalue);
+		}
+	}
+#endif
+
+	/*
+	 *	Pack the VPs
+	 */
+	if (rad_encode(request->reply, request->packet,
+		       request->client->secret) < 0) {
+		RERROR("Failed encoding packet: %s", fr_strerror());
+		return 0;
+	}
+
+	if (request->reply->data_len > (MAX_PACKET_LEN - 100)) {
+		RWARN("Packet is large, and possibly truncated - %zd vs max %d",
+		      request->reply->data_len, MAX_PACKET_LEN);
+	}
+
+	/*
+	 *	Sign the packet.
+	 */
+	if (rad_sign(request->reply, request->packet,
+		       request->client->secret) < 0) {
+		RERROR("Failed signing packet: %s", fr_strerror());
+		return 0;
+	}
+
+	PTHREAD_MUTEX_LOCK(&sock->mutex);
+
+	/*
+	 *	Write the packet to the SSL buffers.
+	 */
+	sock->ssn->record_plus(&sock->ssn->clean_in,
+			       request->reply->data, request->reply->data_len);
+
+	dump_hex("TUNNELED DATA < ", sock->ssn->clean_in.data, sock->ssn->clean_in.used);
+
+	/*
+	 *	Do SSL magic to get encrypted data.
+	 */
+	tls_handshake_send(request, sock->ssn);
+
+	/*
+	 *	And finally write the data to the socket.
+	 */
+	if (sock->ssn->dirty_out.used > 0) {
+		dump_hex("WRITE TO SSL", sock->ssn->dirty_out.data, sock->ssn->dirty_out.used);
+
+		RDEBUG3("(TLS) Writing to socket %d", listener->fd);
+		tls_socket_write(listener);
+	}
+	PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+
+	return 0;
+}
+
+#ifdef WITH_COA_TUNNEL
+/*
+ *	Send a CoA request to a NAS, as a proxied packet.
+ *
+ *	The proxied packet MUST already have been encoded.
+ */
+int dual_tls_send_coa_request(rad_listen_t *listener, REQUEST *request)
+{
+	listen_socket_t *sock = listener->data;
+
+	VERIFY_REQUEST(request);
+
+	rad_assert(listener->proxy_send == dual_tls_send_coa_request);
+
+	if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0;
+
+	rad_assert(request->proxy->data);
+
+	if (request->proxy->data_len > (MAX_PACKET_LEN - 100)) {
+		RWARN("Packet is large, and possibly truncated - %zd vs max %d",
+		      request->proxy->data_len, MAX_PACKET_LEN);
+	}
+
+	PTHREAD_MUTEX_LOCK(&sock->mutex);
+
+	/*
+	 *	Write the packet to the SSL buffers.
+	 */
+	sock->ssn->record_plus(&sock->ssn->clean_in,
+			       request->proxy->data, request->proxy->data_len);
+
+	dump_hex("TUNNELED DATA < ", sock->ssn->clean_in.data, sock->ssn->clean_in.used);
+
+	/*
+	 *	Do SSL magic to get encrypted data.
+	 */
+	tls_handshake_send(request, sock->ssn);
+
+	/*
+	 *	And finally write the data to the socket.
+	 */
+	if (sock->ssn->dirty_out.used > 0) {
+		dump_hex("WRITE TO SSL", sock->ssn->dirty_out.data, sock->ssn->dirty_out.used);
+
+		RDEBUG3("(TLS) Writing to socket %d", listener->fd);
+		tls_socket_write(listener);
+	}
+	PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+
+	return 0;
+}
+#endif
+
+static int try_connect(listen_socket_t *sock)
+{
+	int ret;
+	time_t now;
+
+	now = time(NULL);
+	if ((sock->opened + sock->connect_timeout) < now) {
+		tls_error_io_log(NULL, sock->ssn, 0, "Timeout in SSL_connect");
+		return -1;
+	}
+
+	ret = SSL_connect(sock->ssn->ssl);
+	if (ret <= 0) {
+		switch (SSL_get_error(sock->ssn->ssl, ret)) {
+		default:
+			tls_error_io_log(NULL, sock->ssn, ret, "Failed in " STRINGIFY(__FUNCTION__) " (SSL_connect)");
+			return -1;
+
+		case SSL_ERROR_WANT_READ:
+			DEBUG3("(TLS) SSL_connect() returned WANT_READ");
+			return 2;
+
+		case SSL_ERROR_WANT_WRITE:
+			DEBUG3("(TLS) SSL_connect() returned WANT_WRITE");
+			return 2;
+		}
+	}
+
+	sock->ssn->connected = true;
+	return 1;
+}
+
+
+#ifdef WITH_PROXY
+#ifdef WITH_RADIUSV11
+extern int fr_radiusv11_client_get_alpn(rad_listen_t *listener);
+#endif
+
+/*
+ *	Read from the SSL socket.  Safe with either blocking or
+ *	non-blocking IO.  This level of complexity is probably not
+ *	necessary, as each packet gets put into one SSL application
+ *	record.  When SSL has a full record, we should be able to read
+ *	the entire packet via one SSL_read().
+ *
+ *	When SSL has a partial record, SSL_read() will return
+ *	WANT_READ or WANT_WRITE, and zero application data.
+ *
+ *	Called with the mutex held.
+ */
+static ssize_t proxy_tls_read(rad_listen_t *listener)
+{
+	int rcode;
+	size_t length;
+	uint8_t *data;
+	listen_socket_t *sock = listener->data;
+
+	if (!sock->ssn->connected) {
+		rcode = try_connect(sock);
+		if (rcode <= 0) return rcode;
+
+		if (rcode == 2) return 0; /* more negotiation needed */
+
+#ifdef WITH_RADIUSV11
+		if (!sock->alpn_checked && (fr_radiusv11_client_get_alpn(listener) < 0)) {
+			tls_socket_close(listener);
+			return -1;
+		}
+#endif
+	}
+
+	if (sock->ssn->clean_out.used) {
+		DEBUG3("(TLS) proxy writing %zu to socket", sock->ssn->clean_out.used);
+		/*
+		 *	Write to SSL.
+		 */
+		rcode = SSL_write(sock->ssn->ssl, sock->ssn->clean_out.data, sock->ssn->clean_out.used);
+		if (rcode > 0) {
+			if ((size_t) rcode < sock->ssn->clean_out.used) {
+				memmove(sock->ssn->clean_out.data,  sock->ssn->clean_out.data + rcode,
+					sock->ssn->clean_out.used - rcode);
+				sock->ssn->clean_out.used -= rcode;
+			} else {
+				sock->ssn->clean_out.used = 0;
+			}
+		}
+	}
+
+	/*
+	 *	Get the maximum size of data to receive.
+	 */
+	if (!sock->data) sock->data = talloc_array(sock, uint8_t,
+						   sock->ssn->mtu);
+
+	data = sock->data;
+
+	if (sock->partial < 4) {
+		rcode = SSL_read(sock->ssn->ssl, data + sock->partial,
+				 4 - sock->partial);
+		if (rcode <= 0) {
+			int err = SSL_get_error(sock->ssn->ssl, rcode);
+			switch (err) {
+
+			case SSL_ERROR_WANT_READ:
+				DEBUG3("(TLS) OpenSSL returned WANT_READ");
+				return 0;
+
+			case SSL_ERROR_WANT_WRITE:
+				DEBUG3("(TLS) OpenSSL returned WANT_WRITE");
+				return 0;
+
+			case SSL_ERROR_ZERO_RETURN:
+				/* remote end sent close_notify, send one back */
+				SSL_shutdown(sock->ssn->ssl);
+				/* FALL-THROUGH */
+
+			case SSL_ERROR_SYSCALL:
+			do_close:
+				return -1;
+
+			case SSL_ERROR_SSL:
+				DEBUG("(TLS) Home server has closed the connection");
+				goto do_close;
+
+			default:
+				tls_error_log(NULL, "Failed in proxy receive with OpenSSL error %d", err);
+				goto do_close;
+			}
+		}
+
+		sock->partial = rcode;
+	} /* try reading the packet header */
+
+	if (sock->partial < 4) return 0; /* read more data */
+
+	length = (data[2] << 8) | data[3];
+
+	/*
+	 *	Do these checks only once, when we read the header.
+	 */
+	if (sock->partial == 4) {
+		DEBUG3("Proxy received header saying we have a packet of %u bytes",
+		       (unsigned int) length);
+
+		/*
+		 *	FIXME: allocate a RADIUS_PACKET, and set
+		 *	"data" to be as large as necessary.
+		 */
+		if (length > sock->ssn->mtu) {
+			INFO("Received packet will be too large! Set \"fragment_size = %u\"",
+			     (data[2] << 8) | data[3]);
+			goto do_close;
+		}
+	}
+
+	/*
+	 *	Try to read some more.
+	 */
+	if (sock->partial < length) {
+		rcode = SSL_read(sock->ssn->ssl, data + sock->partial,
+				 length - sock->partial);
+		if (rcode <= 0) {
+			int err = SSL_get_error(sock->ssn->ssl, rcode);
+			switch (err) {
+
+			case SSL_ERROR_WANT_READ:
+				DEBUG3("(TLS) OpenSSL returned WANT_READ");
+				return 0;
+
+			case SSL_ERROR_WANT_WRITE:
+				DEBUG3("(TLS) OpenSSL returned WANT_WRITE");
+				return 0;
+
+			case SSL_ERROR_ZERO_RETURN:
+				/* remote end sent close_notify, send one back */
+				SSL_shutdown(sock->ssn->ssl);
+				goto do_close;
+
+			case SSL_ERROR_SSL:
+				DEBUG("(TLS) Home server has closed the connection");
+				goto do_close;
+
+			default:
+				DEBUG("(TLS) Unexpected OpenSSL error %d", err);
+				goto do_close;
+			}
+		}
+
+		sock->partial += rcode;
+	}
+
+	/*
+	 *	If we're not done, say so.
+	 *
+	 *	Otherwise, reset the partially read data flag, and say
+	 *	we have a packet.
+	 */
+	if (sock->partial < length) {
+		return 0;
+	}
+
+	sock->partial = 0;	/* we've now read the packet */
+	return length;
+}
+
+
+int proxy_tls_recv(rad_listen_t *listener)
+{
+	listen_socket_t *sock = listener->data;
+	char buffer[256];
+	RADIUS_PACKET *packet;
+	uint8_t *data;
+	ssize_t data_len;
+#ifdef WITH_COA_TUNNEL
+	bool is_request = false;
+	RADCLIENT *client = sock->client;
+#endif
+
+	if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0;
+
+	rad_assert(sock->ssn != NULL);
+
+	DEBUG3("Proxy SSL socket has data to read");
+	PTHREAD_MUTEX_LOCK(&sock->mutex);
+	data_len = proxy_tls_read(listener);
+	if (data_len < 0) {
+		tls_socket_close(listener);
+		PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+		DEBUG("Closing TLS socket to home server");
+		return 0;
+	}
+	PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+
+	if (data_len == 0) return 0; /* not done yet */
+
+	data = sock->data;
+
+	packet = rad_alloc(sock, false);
+	packet->sockfd = listener->fd;
+	packet->src_ipaddr = sock->other_ipaddr;
+	packet->src_port = sock->other_port;
+	packet->dst_ipaddr = sock->my_ipaddr;
+	packet->dst_port = sock->my_port;
+	packet->code = data[0];
+	packet->id = data[1];
+	packet->data_len = data_len;
+	packet->data = talloc_array(packet, uint8_t, packet->data_len);
+	memcpy(packet->data, data, packet->data_len);
+	memcpy(packet->vector, packet->data + 4, 16);
+
+#ifdef WITH_RADIUSV11
+	packet->radiusv11 = sock->radiusv11;
+
+	if (sock->radiusv11) {
+		uint32_t id;
+
+		memcpy(&id, data + 4, sizeof(id));
+		packet->id = ntohl(id);
+	}
+
+#endif
+
+	/*
+	 *	FIXME: Client MIB updates?
+	 */
+	switch (packet->code) {
+	case PW_CODE_ACCESS_ACCEPT:
+	case PW_CODE_ACCESS_CHALLENGE:
+	case PW_CODE_ACCESS_REJECT:
+		break;
+
+#ifdef WITH_ACCOUNTING
+	case PW_CODE_ACCOUNTING_RESPONSE:
+		break;
+#endif
+
+#ifdef WITH_COA
+	case PW_CODE_COA_ACK:
+	case PW_CODE_COA_NAK:
+	case PW_CODE_DISCONNECT_ACK:
+	case PW_CODE_DISCONNECT_NAK:
+		break;
+
+#ifdef WITH_COA_TUNNEL
+	case PW_CODE_COA_REQUEST:
+		if (!listener->send_coa) goto bad_packet;
+		FR_STATS_INC(coa, total_requests);
+		is_request = true;
+		break;
+
+	case PW_CODE_DISCONNECT_REQUEST:
+		if (!listener->send_coa) goto bad_packet;
+		FR_STATS_INC(dsc, total_requests);
+		is_request = true;
+		break;
+#endif
+#endif
+
+	default:
+#ifdef WITH_COA_TUNNEL
+	bad_packet:
+#endif
+		/*
+		 *	FIXME: Update MIB for packet types?
+		 */
+		ERROR("Invalid packet code %d sent to a proxy port "
+		       "from home server %s port %d - ID %d : IGNORED",
+		       packet->code,
+		       ip_ntoh(&packet->src_ipaddr, buffer, sizeof(buffer)),
+		       packet->src_port, packet->id);
+		rad_free(&packet);
+		return 0;
+	}
+
+#ifdef WITH_COA_TUNNEL
+	if (is_request) {
+		if (!request_receive(NULL, listener, packet, client, rad_coa_recv)) {
+			FR_STATS_INC(auth, total_packets_dropped);
+			rad_free(&packet);
+			return 0;
+		}
+	} else
+#endif
+	if (!request_proxy_reply(packet)) {
+		rad_free(&packet);
+		return 0;
+	}
+
+	return 1;
+}
+
+
+int proxy_tls_send(rad_listen_t *listener, REQUEST *request)
+{
+	int rcode;
+	listen_socket_t *sock = listener->data;
+
+	VERIFY_REQUEST(request);
+
+	if ((listener->status != RAD_LISTEN_STATUS_INIT) &&
+	    (listener->status != RAD_LISTEN_STATUS_KNOWN)) return 0;
+
+	/*
+	 *	Normal proxying calls us with the data already
+	 *	encoded.  The "ping home server" code does not.  So,
+	 *	if there's no packet, encode it here.
+	 */
+	if (!request->proxy->data) {
+		request->proxy_listener->proxy_encode(request->proxy_listener,
+						      request);
+	}
+
+	rad_assert(sock->ssn != NULL);
+
+	if (!sock->ssn->connected) {
+		PTHREAD_MUTEX_LOCK(&sock->mutex);
+		rcode = try_connect(sock);
+		if (rcode <= 0) {
+			tls_socket_close(listener);
+			PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+			return rcode;
+		}
+		PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+
+		/*
+		 *	More negotiation is needed, but remember to
+		 *	save this packet to an intermediate buffer.
+		 *	Once the SSL connection is established, the
+		 *	later code writes the packet to the
+		 *	connection.
+		 */
+		if (rcode == 2) {
+			PTHREAD_MUTEX_LOCK(&sock->mutex);
+			if ((sock->ssn->clean_out.used + request->proxy->data_len) > MAX_RECORD_SIZE) {
+				PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+				RERROR("(TLS) Too much data buffered during SSL_connect()");
+				listener->status = RAD_LISTEN_STATUS_EOL;
+				radius_update_listener(listener);
+				return -1;
+			}
+
+			memcpy(sock->ssn->clean_out.data + sock->ssn->clean_out.used, request->proxy->data, request->proxy->data_len);
+			sock->ssn->clean_out.used += request->proxy->data_len;
+			RDEBUG3("(TLS) Writing %zu bytes for later (total %zu)", request->proxy->data_len, sock->ssn->clean_out.used);
+
+			PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+			return 0;
+		}
+
+#ifdef WITH_RADIUSV11
+		if (!sock->alpn_checked && (fr_radiusv11_client_get_alpn(listener) < 0)) {
+			listener->status = RAD_LISTEN_STATUS_EOL;
+			radius_update_listener(listener);
+			return -1;
+		}
+#endif
+	}
+
+	DEBUG3("Proxy is writing %u bytes to SSL",
+	       (unsigned int) request->proxy->data_len);
+	PTHREAD_MUTEX_LOCK(&sock->mutex);
+
+	/*
+	 *	We may have previously cached data on SSL_connect(), which now needs to be written to the home server.
+	 */
+	if (sock->ssn->clean_out.used > 0) {
+		if ((sock->ssn->clean_out.used + request->proxy->data_len) > MAX_RECORD_SIZE) {
+			PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+			RERROR("(TLS) Too much data buffered after SSL_connect()");
+			listener->status = RAD_LISTEN_STATUS_EOL;
+			radius_update_listener(listener);
+			return -1;
+		}
+
+		/*
+		 *	Add in our packet.
+		 */
+		memcpy(sock->ssn->clean_out.data + sock->ssn->clean_out.used, request->proxy->data, request->proxy->data_len);
+		sock->ssn->clean_out.used += request->proxy->data_len;
+
+		/*
+		 *	Write to SSL.
+		 */
+		DEBUG3("(TLS) proxy writing %zu to socket", sock->ssn->clean_out.used);
+
+		rcode = SSL_write(sock->ssn->ssl, sock->ssn->clean_out.data, sock->ssn->clean_out.used);
+		if (rcode > 0) {
+			if ((size_t) rcode < sock->ssn->clean_out.used) {
+				memmove(sock->ssn->clean_out.data,  sock->ssn->clean_out.data + rcode,
+					sock->ssn->clean_out.used - rcode);
+				sock->ssn->clean_out.used -= rcode;
+			} else {
+				sock->ssn->clean_out.used = 0;
+			}
+			PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+			return 1;
+		}
+	} else {
+		rcode = SSL_write(sock->ssn->ssl, request->proxy->data,
+				  request->proxy->data_len);
+	}
+	if (rcode < 0) {
+		int err;
+
+		err = ERR_get_error();
+		switch (err) {
+		case SSL_ERROR_NONE:
+			break;
+
+		case SSL_ERROR_WANT_READ:
+			DEBUG3("(TLS) OpenSSL returned WANT_READ");
+			break;
+
+		case SSL_ERROR_WANT_WRITE:
+			DEBUG3("(TLS) OpenSSL returned WANT_WRITE");
+			break;
+
+		default:
+			tls_error_log(NULL, "Failed in proxy send with OpenSSL error %d", err);
+			DEBUG("(TLS) Closing socket to home server");
+			tls_socket_close(listener);
+			PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+			return 0;
+		}
+	}
+	PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+
+	return 1;
+}
+
+#ifdef WITH_COA_TUNNEL
+int proxy_tls_send_reply(rad_listen_t *listener, REQUEST *request)
+{
+	int rcode;
+	listen_socket_t *sock = listener->data;
+
+	VERIFY_REQUEST(request);
+
+	rad_assert(sock->ssn->connected);
+
+	if ((listener->status != RAD_LISTEN_STATUS_INIT &&
+	    (listener->status != RAD_LISTEN_STATUS_KNOWN))) return 0;
+
+	/*
+	 *	Pack the VPs
+	 */
+	if (rad_encode(request->reply, request->packet,
+		       request->client->secret) < 0) {
+		RERROR("Failed encoding packet: %s", fr_strerror());
+		return 0;
+	}
+
+	if (request->reply->data_len > (MAX_PACKET_LEN - 100)) {
+		RWARN("Packet is large, and possibly truncated - %zd vs max %d",
+		      request->reply->data_len, MAX_PACKET_LEN);
+	}
+
+	/*
+	 *	Sign the packet.
+	 */
+	if (rad_sign(request->reply, request->packet,
+		       request->client->secret) < 0) {
+		RERROR("Failed signing packet: %s", fr_strerror());
+		return 0;
+	}
+
+	rad_assert(sock->ssn != NULL);
+
+	DEBUG3("Proxy is writing %u bytes to SSL",
+	       (unsigned int) request->reply->data_len);
+	PTHREAD_MUTEX_LOCK(&sock->mutex);
+	rcode = SSL_write(sock->ssn->ssl, request->reply->data,
+			  request->reply->data_len);
+	if (rcode < 0) {
+		int err;
+
+		err = ERR_get_error();
+		switch (err) {
+		case SSL_ERROR_NONE:
+		case SSL_ERROR_WANT_READ:
+		case SSL_ERROR_WANT_WRITE:
+			DEBUG3("(TLS) SSL_write() returned %s", ERR_reason_error_string(err));
+			break;	/* let someone else retry */
+
+		default:
+			tls_error_log(NULL, "Failed in proxy send with OpenSSL error %d", err);
+			DEBUG("Closing TLS socket to home server");
+			tls_socket_close(listener);
+			PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+			return 0;
+		}
+	}
+	PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+
+	return 1;
+}
+#endif	/* WITH_COA_TUNNEL */
+#endif	/* WITH_PROXY */
+
+#endif	/* WITH_TLS */
+#endif	/* WITH_TCP */
diff --git a/src/main/tmpl.c b/src/main/tmpl.c
new file mode 100644
index 0000000..6ec2598
--- /dev/null
+++ b/src/main/tmpl.c
@@ -0,0 +1,2399 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @brief #VALUE_PAIR template functions
+ * @file main/tmpl.c
+ *
+ * @ingroup AVP
+ *
+ * @copyright 2014-2015 The FreeRADIUS server project
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <ctype.h>
+
+/** Map #tmpl_type_t values to descriptive strings
+ */
+FR_NAME_NUMBER const tmpl_names[] = {
+	{ "literal",		TMPL_TYPE_LITERAL 	},
+	{ "xlat",		TMPL_TYPE_XLAT		},
+	{ "attr",		TMPL_TYPE_ATTR		},
+	{ "unknown attr",	TMPL_TYPE_ATTR_UNDEFINED	},
+	{ "list",		TMPL_TYPE_LIST		},
+	{ "regex",		TMPL_TYPE_REGEX		},
+	{ "exec",		TMPL_TYPE_EXEC		},
+	{ "data",		TMPL_TYPE_DATA		},
+	{ "parsed xlat",	TMPL_TYPE_XLAT_STRUCT	},
+	{ "parsed regex",	TMPL_TYPE_REGEX_STRUCT	},
+	{ "null",		TMPL_TYPE_NULL		},
+	{ NULL, 0 }
+};
+
+/** Map keywords to #pair_lists_t values
+ */
+const FR_NAME_NUMBER pair_lists[] = {
+	{ "request",		PAIR_LIST_REQUEST },
+	{ "reply",		PAIR_LIST_REPLY },
+	{ "control",		PAIR_LIST_CONTROL },		/* New name should have priority */
+	{ "config",		PAIR_LIST_CONTROL },
+	{ "session-state",	PAIR_LIST_STATE },
+#ifdef WITH_PROXY
+	{ "proxy-request",	PAIR_LIST_PROXY_REQUEST },
+	{ "proxy-reply",	PAIR_LIST_PROXY_REPLY },
+#endif
+#ifdef WITH_COA
+	{ "coa",		PAIR_LIST_COA },
+	{ "coa-reply",		PAIR_LIST_COA_REPLY },
+	{ "disconnect",		PAIR_LIST_DM },
+	{ "disconnect-reply",	PAIR_LIST_DM_REPLY },
+#endif
+	{  NULL , -1 }
+};
+
+/** Map keywords to #request_refs_t values
+ */
+const FR_NAME_NUMBER request_refs[] = {
+	{ "outer",		REQUEST_OUTER },
+	{ "current",		REQUEST_CURRENT },
+	{ "parent",		REQUEST_PARENT },
+	{  NULL , -1 }
+};
+
+/** @name Parse list and request qualifiers to #pair_lists_t and #request_refs_t values
+ *
+ * These functions also resolve #pair_lists_t and #request_refs_t values to #REQUEST
+ * structs and the head of #VALUE_PAIR lists in those structs.
+ *
+ * For adding new #VALUE_PAIR to the lists, the #radius_list_ctx function can be used
+ * to obtain the appropriate TALLOC_CTX pointer.
+ *
+ * @note These don't really have much to do with #vp_tmpl_t. They're in the same
+ *	file as they're used almost exclusively by the tmpl_* functions.
+ * @{
+ */
+
+/** Resolve attribute name to a #pair_lists_t value.
+ *
+ * Check the name string for #pair_lists qualifiers and write a #pair_lists_t value
+ * for that list to out. This value may be passed to #radius_list, along with the current
+ * #REQUEST, to get a pointer to the actual list in the #REQUEST.
+ *
+ * If we're sure we've definitely found a list qualifier token delimiter (``:``) but the
+ * string doesn't match a #radius_list qualifier, return 0 and write #PAIR_LIST_UNKNOWN
+ * to out.
+ *
+ * If we can't find a string that looks like a request qualifier, set out to def, and
+ * return 0.
+ *
+ * @note #radius_list_name should be called before passing a name string that may
+ *	contain qualifiers to #dict_attrbyname.
+ *
+ * @param[out] out Where to write the list qualifier.
+ * @param[in] name String containing list qualifiers to parse.
+ * @param[in] def the list to return if no qualifiers were found.
+ * @return 0 if no valid list qualifier could be found, else the number of bytes consumed.
+ *	The caller may then advanced the name pointer by the value returned, to get the
+ *	start of the attribute name (if any).
+ *
+ * @see pair_list
+ * @see radius_list
+ */
+size_t radius_list_name(pair_lists_t *out, char const *name, pair_lists_t def)
+{
+	char const *p = name;
+	char const *q;
+
+	/* This should never be a NULL pointer */
+	rad_assert(name);
+
+	/*
+	 *	Try and determine the end of the token
+	 */
+	for (q = p; dict_attr_allowed_chars[(uint8_t) *q]; q++);
+
+	switch (*q) {
+	/*
+	 *	It's a bareword made up entirely of dictionary chars
+	 *	check and see if it's a list qualifier, and if it's
+	 *	not, return the def and say we couldn't parse
+	 *	anything.
+	 */
+	case '\0':
+		*out = fr_substr2int(pair_lists, p, PAIR_LIST_UNKNOWN, (q - p));
+		if (*out != PAIR_LIST_UNKNOWN) return q - p;
+		*out = def;
+		return 0;
+
+	/*
+	 *	It may be a list qualifier delimiter. Because of tags
+	 *	We need to check that it doesn't look like a tag suffix.
+	 *	We do this by looking at the chars between ':' and the
+	 *	next token delimiter, and seeing if they're all digits.
+	 */
+	case ':':
+	{
+		char const *d = q + 1;
+
+		if (isdigit((uint8_t) *d)) {
+			while (isdigit((uint8_t) *d)) d++;
+
+			/*
+			 *	Char after the number string
+			 *	was a token delimiter, so this is a
+			 *	tag, not a list qualifier.
+			 */
+			if (!dict_attr_allowed_chars[(uint8_t) *d]) {
+				*out = def;
+				return 0;
+			}
+		}
+
+		*out = fr_substr2int(pair_lists, p, PAIR_LIST_UNKNOWN, (q - p));
+		if (*out == PAIR_LIST_UNKNOWN) return 0;
+
+		return (q + 1) - name; /* Consume the list and delimiter */
+	}
+
+	default:
+		*out = def;
+		return 0;
+	}
+}
+
+/** Resolve attribute #pair_lists_t value to an attribute list.
+ *
+ * The value returned is a pointer to the pointer of the HEAD of a #VALUE_PAIR list in the
+ * #REQUEST. If the head of the list changes, the pointer will still be valid.
+ *
+ * @param[in] request containing the target lists.
+ * @param[in] list #pair_lists_t value to resolve to #VALUE_PAIR list. Will be NULL if list
+ *	name couldn't be resolved.
+ * @return a pointer to the HEAD of a list in the #REQUEST.
+ *
+ * @see tmpl_cursor_init
+ * @see fr_cursor_init
+ */
+VALUE_PAIR **radius_list(REQUEST *request, pair_lists_t list)
+{
+	if (!request) return NULL;
+
+	switch (list) {
+	/* Don't add default */
+	case PAIR_LIST_UNKNOWN:
+		break;
+
+	case PAIR_LIST_REQUEST:
+		if (!request->packet) return NULL;
+		return &request->packet->vps;
+
+	case PAIR_LIST_REPLY:
+		if (!request->reply) return NULL;
+		return &request->reply->vps;
+
+	case PAIR_LIST_CONTROL:
+		return &request->config;
+
+	case PAIR_LIST_STATE:
+		return &request->state;
+
+#ifdef WITH_PROXY
+	case PAIR_LIST_PROXY_REQUEST:
+		if (!request->proxy) break;
+		return &request->proxy->vps;
+
+	case PAIR_LIST_PROXY_REPLY:
+		if (!request->proxy_reply) break;
+		return &request->proxy_reply->vps;
+#endif
+#ifdef WITH_COA
+	case PAIR_LIST_COA:
+		if (request->coa &&
+		    (request->coa->proxy->code == PW_CODE_COA_REQUEST)) {
+			return &request->coa->proxy->vps;
+		}
+		break;
+
+	case PAIR_LIST_COA_REPLY:
+		if (request->coa && /* match reply with request */
+		    (request->coa->proxy->code == PW_CODE_COA_REQUEST) &&
+		    request->coa->proxy_reply) {
+			return &request->coa->proxy_reply->vps;
+		}
+		break;
+
+	case PAIR_LIST_DM:
+		if (request->coa &&
+		    (request->coa->proxy->code == PW_CODE_DISCONNECT_REQUEST)) {
+			return &request->coa->proxy->vps;
+		}
+		break;
+
+	case PAIR_LIST_DM_REPLY:
+		if (request->coa && /* match reply with request */
+		    (request->coa->proxy->code == PW_CODE_DISCONNECT_REQUEST) &&
+		    request->coa->proxy_reply) {
+			return &request->coa->proxy_reply->vps;
+		}
+		break;
+#endif
+	}
+
+	RWDEBUG2("List \"%s\" is not available",
+		fr_int2str(pair_lists, list, "<INVALID>"));
+
+	return NULL;
+}
+
+/** Resolve a list to the #RADIUS_PACKET holding the HEAD pointer for a #VALUE_PAIR list
+ *
+ * Returns a pointer to the #RADIUS_PACKET that holds the HEAD pointer of a given list,
+ * for the current #REQUEST.
+ *
+ * @param[in] request To resolve list in.
+ * @param[in] list #pair_lists_t value to resolve to #RADIUS_PACKET.
+ * @return a #RADIUS_PACKET on success, else NULL.
+ *
+ * @see radius_list
+ */
+RADIUS_PACKET *radius_packet(REQUEST *request, pair_lists_t list)
+{
+	switch (list) {
+	/* Don't add default */
+	case PAIR_LIST_STATE:
+	case PAIR_LIST_CONTROL:
+	case PAIR_LIST_UNKNOWN:
+		return NULL;
+
+	case PAIR_LIST_REQUEST:
+		return request->packet;
+
+	case PAIR_LIST_REPLY:
+		return request->reply;
+
+#ifdef WITH_PROXY
+	case PAIR_LIST_PROXY_REQUEST:
+		return request->proxy;
+
+	case PAIR_LIST_PROXY_REPLY:
+		return request->proxy_reply;
+#endif
+
+#ifdef WITH_COA
+	case PAIR_LIST_COA:
+	case PAIR_LIST_DM:
+		return request->coa->proxy;
+
+	case PAIR_LIST_COA_REPLY:
+	case PAIR_LIST_DM_REPLY:
+		return request->coa->proxy_reply;
+#endif
+	}
+
+	return NULL;
+}
+
+/** Return the correct TALLOC_CTX to alloc #VALUE_PAIR in, for a list
+ *
+ * Allocating new #VALUE_PAIR in the context of a #REQUEST is usually wrong.
+ * #VALUE_PAIR should be allocated in the context of a #RADIUS_PACKET, so that if the
+ * #RADIUS_PACKET is freed before the #REQUEST, the associated #VALUE_PAIR lists are
+ * freed too.
+ *
+ * @param[in] request containing the target lists.
+ * @param[in] list #pair_lists_t value to resolve to TALLOC_CTX.
+ * @return a TALLOC_CTX on success, else NULL.
+ *
+ * @see radius_list
+ */
+TALLOC_CTX *radius_list_ctx(REQUEST *request, pair_lists_t list)
+{
+	if (!request) return NULL;
+
+	switch (list) {
+	case PAIR_LIST_REQUEST:
+		return request->packet;
+
+	case PAIR_LIST_REPLY:
+		return request->reply;
+
+	case PAIR_LIST_CONTROL:
+		return request;
+
+	case PAIR_LIST_STATE:
+		return request->state_ctx;
+
+#ifdef WITH_PROXY
+	case PAIR_LIST_PROXY_REQUEST:
+		return request->proxy;
+
+	case PAIR_LIST_PROXY_REPLY:
+		return request->proxy_reply;
+#endif
+
+#ifdef WITH_COA
+	case PAIR_LIST_COA:
+		if (!request->coa) return NULL;
+		rad_assert(request->coa->proxy != NULL);
+		if (request->coa->proxy->code != PW_CODE_COA_REQUEST) return NULL;
+		return request->coa->proxy;
+
+	case PAIR_LIST_COA_REPLY:
+		if (!request->coa) return NULL;
+		rad_assert(request->coa->proxy != NULL);
+		if (request->coa->proxy->code != PW_CODE_COA_REQUEST) return NULL;
+		return request->coa->proxy_reply;
+
+	case PAIR_LIST_DM:
+		if (!request->coa) return NULL;
+		rad_assert(request->coa->proxy != NULL);
+		if (request->coa->proxy->code != PW_CODE_DISCONNECT_REQUEST) return NULL;
+		return request->coa->proxy;
+
+	case PAIR_LIST_DM_REPLY:
+		if (!request->coa) return NULL;
+		rad_assert(request->coa->proxy != NULL);
+		if (request->coa->proxy->code != PW_CODE_DISCONNECT_REQUEST) return NULL;
+		return request->coa->proxy_reply;
+#endif
+	/* Don't add default */
+	case PAIR_LIST_UNKNOWN:
+		break;
+	}
+
+	return NULL;
+}
+
+/** Resolve attribute name to a #request_refs_t value.
+ *
+ * Check the name string for qualifiers that reference a parent #REQUEST.
+ *
+ * If we find a string that matches a #request_refs qualifier, return the number of chars
+ * we consumed.
+ *
+ * If we're sure we've definitely found a list qualifier token delimiter (``*``) but the
+ * qualifier doesn't match one of the #request_refs qualifiers, return 0 and set out to
+ * #REQUEST_UNKNOWN.
+ *
+ * If we can't find a string that looks like a request qualifier, set out to def, and
+ * return 0.
+ *
+ * @param[out] out The #request_refs_t value the name resolved to (or #REQUEST_UNKNOWN).
+ * @param[in] name of attribute.
+ * @param[in] def default request ref to return if no request qualifier is present.
+ * @return 0 if no valid request qualifier could be found, else the number of bytes consumed.
+ *	The caller may then advanced the name pointer by the value returned, to get the
+ *	start of the attribute list or attribute name(if any).
+ *
+ * @see radius_list_name
+ * @see request_refs
+ */
+size_t radius_request_name(request_refs_t *out, char const *name, request_refs_t def)
+{
+	char const *p, *q;
+
+	p = name;
+	/*
+	 *	Try and determine the end of the token
+	 */
+	for (q = p; dict_attr_allowed_chars[(uint8_t) *q] && (*q != '.') && (*q != '-'); q++);
+
+	/*
+	 *	First token delimiter wasn't a '.'
+	 */
+	if (*q != '.') {
+		*out = def;
+		return 0;
+	}
+
+	*out = fr_substr2int(request_refs, name, REQUEST_UNKNOWN, q - p);
+	if (*out == REQUEST_UNKNOWN) return 0;
+
+	return (q + 1) - p;
+}
+
+/** Resolve a #request_refs_t to a #REQUEST.
+ *
+ * Sometimes #REQUEST structs may be chained to each other, as is the case
+ * when internally proxying EAP. This function resolves a #request_refs_t
+ * to a #REQUEST higher in the chain than the current #REQUEST.
+ *
+ * @see radius_list
+ * @param[in,out] context #REQUEST to start resolving from, and where to write
+ *	a pointer to the resolved #REQUEST back to.
+ * @param[in] name (request) to resolve.
+ * @return 0 if request is valid in this context, else -1.
+ */
+int radius_request(REQUEST **context, request_refs_t name)
+{
+	REQUEST *request = *context;
+
+	switch (name) {
+	case REQUEST_CURRENT:
+		return 0;
+
+	case REQUEST_PARENT:	/* for future use in request chaining */
+	case REQUEST_OUTER:
+		if (!request->parent) {
+			return -1;
+		}
+		*context = request->parent;
+		break;
+
+	case REQUEST_UNKNOWN:
+	default:
+		rad_assert(0);
+		return -1;
+	}
+
+	return 0;
+}
+/** @} */
+
+/** @name Alloc or initialise #vp_tmpl_t
+ *
+ * @note Should not usually be called outside of tmpl_* functions, use one of
+ *	the tmpl_*from_* functions instead.
+ * @{
+ */
+
+/** Initialise stack allocated #vp_tmpl_t
+ *
+ * @note Name is not strdupe'd or memcpy'd so must be available, and must not change
+ *	for the lifetime of the #vp_tmpl_t.
+ *
+ * @param[out] vpt to initialise.
+ * @param[in] type to set in the #vp_tmpl_t.
+ * @param[in] name of the #vp_tmpl_t.
+ * @param[in] len The length of the buffer (or a substring of the buffer) pointed to by name.
+ *	If < 0 strlen will be used to determine the length.
+ * @return a pointer to the initialised #vp_tmpl_t. The same value as
+ *	vpt.
+ */
+vp_tmpl_t *tmpl_init(vp_tmpl_t *vpt, tmpl_type_t type, char const *name, ssize_t len)
+{
+	rad_assert(vpt);
+	rad_assert(type != TMPL_TYPE_UNKNOWN);
+	rad_assert(type <= TMPL_TYPE_NULL);
+
+	memset(vpt, 0, sizeof(vp_tmpl_t));
+	vpt->type = type;
+
+	if (name) {
+		vpt->name = name;
+		vpt->len = len < 0 ? strlen(name) :
+				     (size_t) len;
+	}
+	return vpt;
+}
+
+/** Create a new heap allocated #vp_tmpl_t
+ *
+ * @param[in,out] ctx to allocate in.
+ * @param[in] type to set in the #vp_tmpl_t.
+ * @param[in] name of the #vp_tmpl_t (will be copied to a new talloc buffer parented
+ *	by the #vp_tmpl_t).
+ * @param[in] len The length of the buffer (or a substring of the buffer) pointed to by name.
+ *	If < 0 strlen will be used to determine the length.
+ * @return the newly allocated #vp_tmpl_t.
+ */
+vp_tmpl_t *tmpl_alloc(TALLOC_CTX *ctx, tmpl_type_t type, char const *name, ssize_t len)
+{
+	vp_tmpl_t *vpt;
+
+	rad_assert(type != TMPL_TYPE_UNKNOWN);
+	rad_assert(type <= TMPL_TYPE_NULL);
+
+	vpt = talloc_zero(ctx, vp_tmpl_t);
+	if (!vpt) return NULL;
+	vpt->type = type;
+	if (name) {
+		vpt->name = talloc_bstrndup(vpt, name, len < 0 ? strlen(name) : (size_t)len);
+		vpt->len = talloc_array_length(vpt->name) - 1;
+	}
+
+	return vpt;
+}
+/* @} **/
+
+/** @name Create new #vp_tmpl_t from a string
+ *
+ * @{
+ */
+/** Parse a string into a TMPL_TYPE_ATTR_* or #TMPL_TYPE_LIST type #vp_tmpl_t
+ *
+ * @note The name field is just a copy of the input pointer, if you know that string might be
+ *	freed before you're done with the #vp_tmpl_t use #tmpl_afrom_attr_str
+ *	instead.
+ *
+ * @param[out] vpt to modify.
+ * @param[in] name of attribute including #request_refs and #pair_lists qualifiers.
+ *	If only #request_refs and #pair_lists qualifiers are found, a #TMPL_TYPE_LIST
+ *	#vp_tmpl_t will be produced.
+ * @param[in] request_def The default #REQUEST to set if no #request_refs qualifiers are
+ *	found in name.
+ * @param[in] list_def The default list to set if no #pair_lists qualifiers are found in
+ *	name.
+ * @param[in] allow_unknown If true attributes in the format accepted by
+ *	#dict_unknown_from_substr will be allowed, even if they're not in the main
+ *	dictionaries.
+ *	If an unknown attribute is found a #TMPL_TYPE_ATTR #vp_tmpl_t will be
+ *	produced with the unknown #DICT_ATTR stored in the ``unknown.da`` buffer.
+ *	This #DICT_ATTR will have its ``flags.is_unknown`` field set to true.
+ *	If #tmpl_from_attr_substr is being called on startup, the #vp_tmpl_t may be
+ *	passed to #tmpl_define_unknown_attr to add the unknown attribute to the main
+ *	dictionary.
+ *	If the unknown attribute is not added to the main dictionary the #vp_tmpl_t
+ *	cannot be used to search for a #VALUE_PAIR in a #REQUEST.
+ * @param[in] allow_undefined If true, we don't generate a parse error on unknown attributes.
+ *	If an unknown attribute is found a #TMPL_TYPE_ATTR_UNDEFINED #vp_tmpl_t
+ *	will be produced.
+ * @return <= 0 on error (offset as negative integer), > 0 on success
+ *	(number of bytes parsed).
+ *
+ * @see REMARKER to produce pretty error markers from the return value.
+ */
+ssize_t tmpl_from_attr_substr(vp_tmpl_t *vpt, char const *name,
+			      request_refs_t request_def, pair_lists_t list_def,
+			      bool allow_unknown, bool allow_undefined)
+{
+	char const *p;
+	long num;
+	char *q;
+	tmpl_type_t type = TMPL_TYPE_ATTR;
+
+	value_pair_tmpl_attr_t attr;	/* So we don't fill the tmpl with junk and then error out */
+
+	memset(vpt, 0, sizeof(*vpt));
+	memset(&attr, 0, sizeof(attr));
+
+	p = name;
+
+	if (*p == '&') p++;
+
+	p += radius_request_name(&attr.request, p, request_def);
+	if (attr.request == REQUEST_UNKNOWN) {
+		fr_strerror_printf("Invalid request qualifier");
+		return -(p - name);
+	}
+
+	/*
+	 *	Finding a list qualifier is optional
+	 */
+	p += radius_list_name(&attr.list, p, list_def);
+	if (attr.list == PAIR_LIST_UNKNOWN) {
+		fr_strerror_printf("Invalid list qualifier");
+		return -(p - name);
+	}
+
+	attr.tag = TAG_ANY;
+	attr.num = NUM_ANY;
+
+	/*
+	 *	This may be just a bare list, but it can still
+	 *	have instance selectors and tag selectors.
+	 */
+	switch (*p) {
+	case '\0':
+		type = TMPL_TYPE_LIST;
+		attr.num = NUM_ALL;	/* Hack - Should be removed once tests are updated */
+		goto finish;
+
+	case '[':
+		type = TMPL_TYPE_LIST;
+		attr.num = NUM_ALL;	/* Hack - Should be removed once tests are updated */
+		goto do_num;
+
+	default:
+		break;
+	}
+
+	attr.da = dict_attrbyname_substr(&p);
+	if (!attr.da) {
+		char const *a;
+
+		/*
+		 *	Record start of attribute in case we need to error out.
+		 */
+		a = p;
+
+		fr_strerror();	/* Clear out any existing errors */
+
+		/*
+		 *	Attr-1.2.3.4 is OK.
+		 */
+		if (dict_unknown_from_substr((DICT_ATTR *)&attr.unknown.da, &p) == 0) {
+			/*
+			 *	Check what we just parsed really hasn't been defined
+			 *	in the main dictionaries.
+			 *
+			 *	If it has, parsing is the same as if the attribute
+			 *	name had been used instead of its OID.
+			 */
+			attr.da = dict_attrbyvalue(((DICT_ATTR *)&attr.unknown.da)->attr,
+						   ((DICT_ATTR *)&attr.unknown.da)->vendor);
+			if (attr.da) {
+				vpt->auto_converted = true;
+				goto do_num;
+			}
+
+			if (!allow_unknown) {
+				fr_strerror_printf("Unknown attribute");
+				return -(a - name);
+			}
+
+			/*
+			 *	Unknown attributes can't be encoded, as we don't
+			 *	know how to encode them!
+			 */
+			attr.da = (DICT_ATTR *)&attr.unknown.da;
+
+			goto do_num; /* unknown attributes can't have tags */
+		}
+
+		/*
+		 *	Can't parse it as an attribute, might be a literal string
+		 *	let the caller decide.
+		 *
+		 *	Don't alter the fr_strerror buffer, should contain the parse
+		 *	error from dict_unknown_from_substr.
+		 */
+		if (!allow_undefined) return -(a - name);
+
+		/*
+		 *	Copy the name to a field for later resolution
+		 */
+		type = TMPL_TYPE_ATTR_UNDEFINED;
+		for (q = attr.unknown.name; dict_attr_allowed_chars[(int) *p]; *q++ = *p++) {
+			if (q >= (attr.unknown.name + sizeof(attr.unknown.name) - 1)) {
+				fr_strerror_printf("Attribute name is too long");
+				return -(p - name);
+			}
+		}
+		*q = '\0';
+
+		goto do_num;
+	}
+
+	/*
+	 *	The string MIGHT have a tag.
+	 */
+	if (*p == ':') {
+		if (attr.da && !attr.da->flags.has_tag) { /* Lists don't have a da */
+			fr_strerror_printf("Attribute '%s' cannot have a tag", attr.da->name);
+			return -(p - name);
+		}
+
+		num = strtol(p + 1, &q, 10);
+		if ((num > 0x1f) || (num < 0)) {
+			fr_strerror_printf("Invalid tag value '%li' (should be between 0-31)", num);
+			return -((p + 1)- name);
+		}
+
+		attr.tag = num;
+		p = q;
+	}
+
+do_num:
+	if (*p == '\0') goto finish;
+
+	if (*p == '[') {
+		p++;
+
+		switch (*p) {
+		case '#':
+			attr.num = NUM_COUNT;
+			p++;
+			break;
+
+		case '*':
+			attr.num = NUM_ALL;
+			p++;
+			break;
+
+		case 'n':
+			attr.num = NUM_LAST;
+			p++;
+			break;
+
+		default:
+			num = strtol(p, &q, 10);
+			if (p == q) {
+				fr_strerror_printf("Array index is not an integer");
+				return -(p - name);
+			}
+
+			if ((num > 1000) || (num < 0)) {
+				fr_strerror_printf("Invalid array reference '%li' (should be between 0-1000)", num);
+				return -(p - name);
+			}
+			attr.num = num;
+			p = q;
+			break;
+		}
+
+		if (*p != ']') {
+			fr_strerror_printf("No closing ']' for array index");
+			return -(p - name);
+		}
+		p++;
+	}
+
+finish:
+	vpt->type = type;
+	vpt->name = name;
+	vpt->len = p - name;
+
+	/*
+	 *	Copy over the attribute definition, now we're
+	 *	sure what we were passed is valid.
+	 */
+	memcpy(&vpt->data.attribute, &attr, sizeof(vpt->data.attribute));
+	if ((vpt->type == TMPL_TYPE_ATTR) && attr.da->flags.is_unknown) {
+		vpt->tmpl_da = (DICT_ATTR *)&vpt->data.attribute.unknown.da;
+	}
+
+	VERIFY_TMPL(vpt);
+
+	return vpt->len;
+}
+
+/** Parse a string into a TMPL_TYPE_ATTR_* or #TMPL_TYPE_LIST type #vp_tmpl_t
+ *
+ * @note Unlike #tmpl_from_attr_substr this function will error out if the entire
+ *	name string isn't parsed.
+ *
+ * @copydetails tmpl_from_attr_substr
+ */
+ssize_t tmpl_from_attr_str(vp_tmpl_t *vpt, char const *name,
+			   request_refs_t request_def, pair_lists_t list_def,
+			   bool allow_unknown, bool allow_undefined)
+{
+	ssize_t slen;
+
+	slen = tmpl_from_attr_substr(vpt, name, request_def, list_def, allow_unknown, allow_undefined);
+	if (slen <= 0) return slen;
+	if (name[slen] != '\0') {
+		/* This looks wrong, but it produces meaningful errors for unknown attrs with tags */
+		fr_strerror_printf("Unexpected text after %s", fr_int2str(tmpl_names, vpt->type, "<INVALID>"));
+		return -slen;
+	}
+
+	VERIFY_TMPL(vpt);
+
+	return slen;
+}
+
+/** Parse a string into a TMPL_TYPE_ATTR_* or #TMPL_TYPE_LIST type #vp_tmpl_t
+ *
+ * @param[in,out] ctx to allocate #vp_tmpl_t in.
+ * @param[out] out Where to write pointer to new #vp_tmpl_t.
+ * @param[in] name of attribute including #request_refs and #pair_lists qualifiers.
+ *	If only #request_refs #pair_lists qualifiers are found, a #TMPL_TYPE_LIST
+ *	#vp_tmpl_t will be produced.
+ * @param[in] request_def The default #REQUEST to set if no #request_refs qualifiers are
+ *	found in name.
+ * @param[in] list_def The default list to set if no #pair_lists qualifiers are found in
+ *	name.
+ * @param[in] allow_unknown If true attributes in the format accepted by
+ *	#dict_unknown_from_substr will be allowed, even if they're not in the main
+ *	dictionaries.
+ *	If an unknown attribute is found a #TMPL_TYPE_ATTR #vp_tmpl_t will be
+ *	produced with the unknown #DICT_ATTR stored in the ``unknown.da`` buffer.
+ *	This #DICT_ATTR will have its ``flags.is_unknown`` field set to true.
+ *	If #tmpl_from_attr_substr is being called on startup, the #vp_tmpl_t may be
+ *	passed to #tmpl_define_unknown_attr to add the unknown attribute to the main
+ *	dictionary.
+ *	If the unknown attribute is not added to the main dictionary the #vp_tmpl_t
+ *	cannot be used to search for a #VALUE_PAIR in a #REQUEST.
+ * @param[in] allow_undefined If true, we don't generate a parse error on unknown attributes.
+ *	If an unknown attribute is found a #TMPL_TYPE_ATTR_UNDEFINED #vp_tmpl_t
+ *	will be produced.
+ * @return <= 0 on error (offset as negative integer), > 0 on success
+ *	(number of bytes parsed).
+ *
+ * @see REMARKER to produce pretty error markers from the return value.
+ */
+ssize_t tmpl_afrom_attr_substr(TALLOC_CTX *ctx, vp_tmpl_t **out, char const *name,
+			       request_refs_t request_def, pair_lists_t list_def,
+			       bool allow_unknown, bool allow_undefined)
+{
+	ssize_t slen;
+	vp_tmpl_t *vpt;
+
+	MEM(vpt = talloc(ctx, vp_tmpl_t)); /* tmpl_from_attr_substr zeros it */
+
+	slen = tmpl_from_attr_substr(vpt, name, request_def, list_def, allow_unknown, allow_undefined);
+	if (slen <= 0) {
+		TALLOC_FREE(vpt);
+		return slen;
+	}
+	vpt->name = talloc_strndup(vpt, vpt->name, slen);
+
+	VERIFY_TMPL(vpt);
+
+	*out = vpt;
+
+	return slen;
+}
+
+/** Parse a string into a TMPL_TYPE_ATTR_* or #TMPL_TYPE_LIST type #vp_tmpl_t
+ *
+ * @note Unlike #tmpl_afrom_attr_substr this function will error out if the entire
+ *	name string isn't parsed.
+ *
+ * @copydetails tmpl_afrom_attr_substr
+ */
+ssize_t tmpl_afrom_attr_str(TALLOC_CTX *ctx, vp_tmpl_t **out, char const *name,
+			    request_refs_t request_def, pair_lists_t list_def,
+			    bool allow_unknown, bool allow_undefined)
+{
+	ssize_t slen;
+	vp_tmpl_t *vpt;
+
+	MEM(vpt = talloc(ctx, vp_tmpl_t)); /* tmpl_from_attr_substr zeros it */
+
+	slen = tmpl_from_attr_substr(vpt, name, request_def, list_def, allow_unknown, allow_undefined);
+	if (slen <= 0) {
+		TALLOC_FREE(vpt);
+		return slen;
+	}
+	if (name[slen] != '\0') {
+		/* This looks wrong, but it produces meaningful errors for unknown attrs with tags */
+		fr_strerror_printf("Unexpected text after %s", fr_int2str(tmpl_names, vpt->type, "<INVALID>"));
+		TALLOC_FREE(vpt);
+		return -slen;
+	}
+	vpt->name = talloc_strndup(vpt, vpt->name, vpt->len);
+
+	VERIFY_TMPL(vpt);
+
+	*out = vpt;
+
+	return slen;
+}
+
+/** Convert an arbitrary string into a #vp_tmpl_t
+ *
+ * @note Unlike #tmpl_afrom_attr_str return code 0 doesn't necessarily indicate failure,
+ *	may just mean a 0 length string was parsed.
+ *
+ * @note xlats and regexes are left uncompiled.  This is to support the two pass parsing
+ *	done by the modcall code.  Compilation on pass1 of that code could fail, as
+ *	attributes or xlat functions registered by modules may not be available (yet).
+ *
+ * @note For details of attribute parsing see #tmpl_from_attr_substr.
+ *
+ * @param[in,out] ctx To allocate #vp_tmpl_t in.
+ * @param[out] out Where to write the pointer to the new #vp_tmpl_t.
+ * @param[in] in String to convert to a #vp_tmpl_t.
+ * @param[in] inlen length of string to convert.
+ * @param[in] type of quoting around value. May be one of:
+ *	- #T_BARE_WORD - If string begins with ``&`` produces #TMPL_TYPE_ATTR,
+ *	  #TMPL_TYPE_ATTR_UNDEFINED, #TMPL_TYPE_LIST or error.
+ *	  If string does not begin with ``&`` produces #TMPL_TYPE_LITERAL,
+ *	  #TMPL_TYPE_ATTR or #TMPL_TYPE_LIST.
+ *	- #T_SINGLE_QUOTED_STRING - Produces #TMPL_TYPE_LITERAL
+ *	- #T_DOUBLE_QUOTED_STRING - Produces #TMPL_TYPE_XLAT or #TMPL_TYPE_LITERAL (if
+ *	  string doesn't contain ``%``).
+ *	- #T_BACK_QUOTED_STRING - Produces #TMPL_TYPE_EXEC
+ *	- #T_OP_REG_EQ - Produces #TMPL_TYPE_REGEX
+ * @param[in] request_def The default #REQUEST to set if no #request_refs qualifiers are
+ *	found in name.
+ * @param[in] list_def The default list to set if no #pair_lists qualifiers are found in
+ *	name.
+ * @param[in] do_unescape whether or not we should do unescaping. Should be false if the
+ *	caller already did it.
+ * @return <= 0 on error (offset as negative integer), > 0 on success
+ *	(number of bytes parsed).
+ *	@see REMARKER to produce pretty error markers from the return value.
+ *
+ * @see tmpl_from_attr_substr
+ */
+ssize_t tmpl_afrom_str(TALLOC_CTX *ctx, vp_tmpl_t **out, char const *in, size_t inlen, FR_TOKEN type,
+		       request_refs_t request_def, pair_lists_t list_def, bool do_unescape)
+{
+	bool do_xlat;
+	char quote;
+	char const *p;
+	ssize_t slen;
+	PW_TYPE data_type = PW_TYPE_STRING;
+	vp_tmpl_t *vpt = NULL;
+	value_data_t data;
+
+	switch (type) {
+	case T_BARE_WORD:
+		/*
+		 *	If we can parse it as an attribute, it's an attribute.
+		 *	Otherwise, treat it as a literal.
+		 */
+		quote = '\0';
+
+		slen = tmpl_afrom_attr_str(ctx, &vpt, in, request_def, list_def, true, (in[0] == '&'));
+		if ((in[0] == '&') && (slen <= 0)) return slen;
+		if (slen > 0) break;
+		goto parse;
+
+	case T_SINGLE_QUOTED_STRING:
+		quote = '\'';
+
+	parse:
+		if (cf_new_escape && do_unescape) {
+			slen = value_data_from_str(ctx, &data, &data_type, NULL, in, inlen, quote);
+			if (slen < 0) return 0;
+
+			vpt = tmpl_alloc(ctx, TMPL_TYPE_LITERAL, data.strvalue, talloc_array_length(data.strvalue) - 1);
+			talloc_free(data.ptr);
+		} else {
+			vpt = tmpl_alloc(ctx, TMPL_TYPE_LITERAL, in, inlen);
+		}
+		vpt->quote = quote;
+		slen = vpt->len;
+		break;
+
+	case T_DOUBLE_QUOTED_STRING:
+		do_xlat = false;
+
+		p = in;
+		while (*p) {
+			if (do_unescape) { /* otherwise \ is just another character */
+				if (*p == '\\') {
+					if (!p[1]) break;
+					p += 2;
+					continue;
+				}
+			}
+
+			if (*p == '%') {
+				do_xlat = true;
+				break;
+			}
+
+			p++;
+		}
+
+		/*
+		 *	If the double quoted string needs to be
+		 *	expanded at run time, make it an xlat
+		 *	expansion.  Otherwise, convert it to be a
+		 *	literal.
+		 */
+		if (cf_new_escape && do_unescape) {
+			slen = value_data_from_str(ctx, &data, &data_type, NULL, in, inlen, '"');
+			if (slen < 0) return slen;
+
+			if (do_xlat) {
+				vpt = tmpl_alloc(ctx, TMPL_TYPE_XLAT, data.strvalue,
+						 talloc_array_length(data.strvalue) - 1);
+			} else {
+				vpt = tmpl_alloc(ctx, TMPL_TYPE_LITERAL, data.strvalue,
+						 talloc_array_length(data.strvalue) - 1);
+				vpt->quote = '"';
+			}
+			talloc_free(data.ptr);
+		} else {
+			if (do_xlat) {
+				vpt = tmpl_alloc(ctx, TMPL_TYPE_XLAT, in, inlen);
+			} else {
+				vpt = tmpl_alloc(ctx, TMPL_TYPE_LITERAL, in, inlen);
+				vpt->quote = '"';
+			}
+		}
+		slen = vpt->len;
+		break;
+
+	case T_BACK_QUOTED_STRING:
+		if (cf_new_escape && do_unescape) {
+			slen = value_data_from_str(ctx, &data, &data_type, NULL, in, inlen, '`');
+			if (slen < 0) return slen;
+
+			vpt = tmpl_alloc(ctx, TMPL_TYPE_EXEC, data.strvalue, talloc_array_length(data.strvalue) - 1);
+			talloc_free(data.ptr);
+		} else {
+			vpt = tmpl_alloc(ctx, TMPL_TYPE_EXEC, in, inlen);
+		}
+		slen = vpt->len;
+		break;
+
+	case T_OP_REG_EQ: /* hack */
+		vpt = tmpl_alloc(ctx, TMPL_TYPE_REGEX, in, inlen);
+		slen = vpt->len;
+		break;
+
+	default:
+		rad_assert(0);
+		return 0;	/* 0 is an error here too */
+	}
+
+	rad_assert((slen >= 0) && (vpt != NULL));
+
+	VERIFY_TMPL(vpt);
+
+	*out = vpt;
+
+	return slen;
+}
+/* @} **/
+
+/** @name Cast or convert #vp_tmpl_t
+ *
+ * #tmpl_cast_in_place can be used to convert #TMPL_TYPE_LITERAL to a #TMPL_TYPE_DATA of a
+ *  specified #PW_TYPE.
+ *
+ * #tmpl_cast_in_place_str does the same as #tmpl_cast_in_place, but will always convert to
+ * #PW_TYPE #PW_TYPE_STRING.
+ *
+ * #tmpl_cast_to_vp does the same as #tmpl_cast_in_place, but outputs a #VALUE_PAIR.
+ *
+ * #tmpl_define_unknown_attr converts a #TMPL_TYPE_ATTR with an unknown #DICT_ATTR to a
+ * #TMPL_TYPE_ATTR with a known #DICT_ATTR, by adding the unknown #DICT_ATTR to the main
+ * dictionary, and updating the ``tmpl_da`` pointer.
+ * @{
+ */
+
+/** Convert #vp_tmpl_t of type #TMPL_TYPE_LITERAL or #TMPL_TYPE_DATA to #TMPL_TYPE_DATA of type specified
+ *
+ * @note Conversion is done in place.
+ * @note Irrespective of whether the #vp_tmpl_t was #TMPL_TYPE_LITERAL or #TMPL_TYPE_DATA,
+ *	on successful cast it will be #TMPL_TYPE_DATA.
+ *
+ * @param[in,out] vpt The template to modify. Must be of type #TMPL_TYPE_LITERAL
+ *	or #TMPL_TYPE_DATA.
+ * @param[in] type to cast to.
+ * @param[in] enumv Enumerated dictionary values associated with a #DICT_ATTR.
+ * @return 0 on success, -1 on failure.
+ */
+int tmpl_cast_in_place(vp_tmpl_t *vpt, PW_TYPE type, DICT_ATTR const *enumv)
+{
+	ssize_t ret;
+
+	VERIFY_TMPL(vpt);
+
+	rad_assert(vpt != NULL);
+	rad_assert((vpt->type == TMPL_TYPE_LITERAL) || (vpt->type == TMPL_TYPE_DATA));
+
+	switch (vpt->type) {
+	case TMPL_TYPE_LITERAL:
+		/*
+		 *	Why do we pass a pointer to the tmpl type? Goddamn WiMAX.
+		 */
+		ret = value_data_from_str(vpt, &vpt->tmpl_data_value, &type,
+					  enumv, vpt->name, vpt->len, '\0');
+		if (ret < 0) {
+			VERIFY_TMPL(vpt);
+			return -1;
+		}
+
+		vpt->tmpl_data_type = type;
+		vpt->type = TMPL_TYPE_DATA;
+		vpt->tmpl_data_length = (size_t) ret;
+		break;
+
+	case TMPL_TYPE_DATA:
+	{
+		value_data_t new;
+
+		if (type == vpt->tmpl_data_type) return 0;	/* noop */
+
+		ret = value_data_cast(vpt, &new, type, enumv, vpt->tmpl_data_type,
+				      NULL, &vpt->tmpl_data_value, vpt->tmpl_data_length);
+		if (ret < 0) return -1;
+
+		/*
+		 *	Free old value buffers
+		 */
+		switch (vpt->tmpl_data_type) {
+		case PW_TYPE_STRING:
+		case PW_TYPE_OCTETS:
+			talloc_free(vpt->tmpl_data_value.ptr);
+			break;
+
+		default:
+			break;
+		}
+
+		memcpy(&vpt->tmpl_data_value, &new, sizeof(vpt->tmpl_data_value));
+		vpt->tmpl_data_type = type;
+		vpt->tmpl_data_length = (size_t) ret;
+	}
+		break;
+
+	default:
+		rad_assert(0);
+	}
+
+	VERIFY_TMPL(vpt);
+
+	return 0;
+}
+
+/** Convert #vp_tmpl_t of type #TMPL_TYPE_LITERAL to #TMPL_TYPE_DATA of type #PW_TYPE_STRING
+ *
+ * @note Conversion is done in place.
+ *
+ * @param[in,out] vpt The template to modify. Must be of type #TMPL_TYPE_LITERAL.
+ */
+void tmpl_cast_in_place_str(vp_tmpl_t *vpt)
+{
+	rad_assert(vpt != NULL);
+	rad_assert(vpt->type == TMPL_TYPE_LITERAL);
+
+	vpt->tmpl_data.vp_strvalue = talloc_typed_strdup(vpt, vpt->name);
+	rad_assert(vpt->tmpl_data.vp_strvalue != NULL);
+
+	vpt->type = TMPL_TYPE_DATA;
+	vpt->tmpl_data_type = PW_TYPE_STRING;
+	vpt->tmpl_data_length = talloc_array_length(vpt->tmpl_data.vp_strvalue) - 1;
+}
+
+/** Expand a #vp_tmpl_t to a string, parse it as an attribute of type cast, create a #VALUE_PAIR from the result
+ *
+ * @note Like #tmpl_expand, but produces a #VALUE_PAIR.
+ *
+ * @param out Where to write pointer to the new #VALUE_PAIR.
+ * @param request The current #REQUEST.
+ * @param vpt to cast. Must be one of the following types:
+ *	- #TMPL_TYPE_LITERAL
+ *	- #TMPL_TYPE_EXEC
+ *	- #TMPL_TYPE_XLAT
+ *	- #TMPL_TYPE_XLAT_STRUCT
+ *	- #TMPL_TYPE_ATTR
+ *	- #TMPL_TYPE_DATA
+ * @param cast type of #VALUE_PAIR to create.
+ * @return 0 on success, -1 on failure.
+ */
+int tmpl_cast_to_vp(VALUE_PAIR **out, REQUEST *request,
+		    vp_tmpl_t const *vpt, DICT_ATTR const *cast)
+{
+	int rcode;
+	VALUE_PAIR *vp;
+	value_data_t data;
+	char *p;
+
+	VERIFY_TMPL(vpt);
+
+	*out = NULL;
+
+	vp = fr_pair_afrom_da(request, cast);
+	if (!vp) return -1;
+
+	if (vpt->type == TMPL_TYPE_DATA) {
+		VERIFY_VP(vp);
+		rad_assert(vp->da->type == vpt->tmpl_data_type);
+
+		value_data_copy(vp, &vp->data, vpt->tmpl_data_type, &vpt->tmpl_data_value, vpt->tmpl_data_length);
+		*out = vp;
+		return 0;
+	}
+
+	rcode = tmpl_aexpand(vp, &p, request, vpt, NULL, NULL);
+	if (rcode < 0) {
+		fr_pair_list_free(&vp);
+		return rcode;
+	}
+	data.strvalue = p;
+
+	/*
+	 *	New escapes: strings are in binary form.
+	 */
+	if (cf_new_escape && (vp->da->type == PW_TYPE_STRING)) {
+		vp->data.ptr = talloc_steal(vp, data.ptr);
+		vp->vp_length = rcode;
+
+	} else if (fr_pair_value_from_str(vp, data.strvalue, rcode) < 0) {
+		talloc_free(data.ptr);
+		fr_pair_list_free(&vp);
+		return -1;
+	}
+
+	/*
+	 *	Copy over any additional fields needed...
+	 */
+	if ((vpt->type == TMPL_TYPE_ATTR) && vp->da->flags.has_tag) {
+		vp->tag = vpt->tmpl_tag;
+	}
+
+	*out = vp;
+	return 0;
+}
+
+/** Add an unknown #DICT_ATTR specified by a #vp_tmpl_t to the main dictionary
+ *
+ * @param vpt to add. ``tmpl_da`` pointer will be updated to point to the
+ *	#DICT_ATTR inserted into the dictionary.
+ * @return 0 on success, -1 on failure.
+ */
+int tmpl_define_unknown_attr(vp_tmpl_t *vpt)
+{
+	DICT_ATTR const *da;
+
+	if (!vpt) return -1;
+
+	VERIFY_TMPL(vpt);
+
+	if (vpt->type != TMPL_TYPE_ATTR) return 0;
+
+	if (!vpt->tmpl_da->flags.is_unknown) return 0;
+
+	da = dict_unknown_add(vpt->tmpl_da);
+	if (!da) return -1;
+	vpt->tmpl_da = da;
+	return 0;
+}
+/* @} **/
+
+/** @name Resolve a #vp_tmpl_t outputting the result in various formats
+ *
+ * @{
+ */
+
+/** Expand a #vp_tmpl_t to a string writing the result to a buffer
+ *
+ * The intended use of #tmpl_expand and #tmpl_aexpand is for modules to easily convert a #vp_tmpl_t
+ * provided by the conf parser, into a usable value.
+ * The value returned should be raw and undoctored for #PW_TYPE_STRING and #PW_TYPE_OCTETS types,
+ * and the printable (string) version of the data for all others.
+ *
+ * Depending what arguments are passed, either copies the value to buff, or writes a pointer
+ * to a string buffer to out. This allows the most efficient access to the value resolved by
+ * the #vp_tmpl_t, avoiding unecessary string copies.
+ *
+ * @note This function is used where raw string values are needed, which may mean the string
+ *	returned may be binary data or contain unprintable chars. #fr_prints or #fr_aprints should
+ *	be used before using these values in debug statements. #is_printable can be used to check
+ *	if the string only contains printable chars.
+ *
+ * @param out Where to write a pointer to the string buffer. On return may point to buff if
+ *	buff was used to store the value. Otherwise will point to a #value_data_t buffer,
+ *	or the name of the template. To force copying to buff, out should be NULL.
+ * @param buff Expansion buffer, may be NULL if out is not NULL, and processing #TMPL_TYPE_LITERAL
+ *	or string types.
+ * @param bufflen Length of expansion buffer.
+ * @param request Current request.
+ * @param vpt to expand. Must be one of the following types:
+ *	- #TMPL_TYPE_LITERAL
+ *	- #TMPL_TYPE_EXEC
+ *	- #TMPL_TYPE_XLAT
+ *	- #TMPL_TYPE_XLAT_STRUCT
+ *	- #TMPL_TYPE_ATTR
+ *	- #TMPL_TYPE_DATA
+ * @param escape xlat escape function (only used for xlat types).
+ * @param escape_ctx xlat escape function data.
+ * @return -1 on error, else the length of data written to buff, or pointed to by out.
+ */
+ssize_t tmpl_expand(char const **out, char *buff, size_t bufflen, REQUEST *request,
+		    vp_tmpl_t const *vpt, xlat_escape_t escape, void *escape_ctx)
+{
+	VALUE_PAIR *vp;
+	ssize_t slen = -1;	/* quiet compiler */
+
+	VERIFY_TMPL(vpt);
+
+	rad_assert(vpt->type != TMPL_TYPE_LIST);
+
+	if (out) *out = NULL;
+
+	switch (vpt->type) {
+	case TMPL_TYPE_LITERAL:
+		RDEBUG4("EXPAND TMPL LITERAL");
+
+		if (!out) {
+			rad_assert(buff);
+			memcpy(buff, vpt->name, vpt->len >= bufflen ? bufflen : vpt->len + 1);
+		} else {
+			*out = vpt->name;
+		}
+		return vpt->len;
+
+	case TMPL_TYPE_EXEC:
+	{
+		RDEBUG4("EXPAND TMPL EXEC");
+		rad_assert(buff);
+		if (radius_exec_program(request, buff, bufflen, NULL, request, vpt->name, NULL,
+					true, false, EXEC_TIMEOUT) != 0) {
+			return -1;
+		}
+		slen = strlen(buff);
+		if (out) *out = buff;
+	}
+		break;
+
+	case TMPL_TYPE_XLAT:
+		RDEBUG4("EXPAND TMPL XLAT");
+		rad_assert(buff);
+		/* Error in expansion, this is distinct from zero length expansion */
+		slen = radius_xlat(buff, bufflen, request, vpt->name, escape, escape_ctx);
+		if (slen < 0) return slen;
+		if (out) *out = buff;
+		break;
+
+	case TMPL_TYPE_XLAT_STRUCT:
+		RDEBUG4("EXPAND TMPL XLAT STRUCT");
+		rad_assert(buff);
+		/* Error in expansion, this is distinct from zero length expansion */
+		slen = radius_xlat_struct(buff, bufflen, request, vpt->tmpl_xlat, escape, escape_ctx);
+		if (slen < 0) {
+			return slen;
+		}
+		slen = strlen(buff);
+		if (out) *out = buff;
+		break;
+
+	case TMPL_TYPE_ATTR:
+	{
+		int ret;
+
+		RDEBUG4("EXPAND TMPL ATTR");
+		rad_assert(buff);
+		ret = tmpl_find_vp(&vp, request, vpt);
+		if (ret < 0) return -2;
+
+		if (out && ((vp->da->type == PW_TYPE_STRING) || (vp->da->type == PW_TYPE_OCTETS))) {
+			*out = vp->data.ptr;
+			slen = vp->vp_length;
+		} else {
+			if (out) *out = buff;
+			slen = vp_prints_value(buff, bufflen, vp, '\0');
+		}
+	}
+		break;
+
+	case TMPL_TYPE_DATA:
+	{
+		RDEBUG4("EXPAND TMPL DATA");
+
+		if (out && ((vpt->tmpl_data_type == PW_TYPE_STRING) || (vpt->tmpl_data_type == PW_TYPE_OCTETS))) {
+			*out = vpt->tmpl_data_value.ptr;
+			slen = vpt->tmpl_data_length;
+		} else {
+			if (out) *out = buff;
+			/**
+			 *  @todo tmpl_expand should accept an enumv da from the lhs of the map.
+			 */
+			slen = value_data_prints(buff, bufflen, vpt->tmpl_data_type, NULL, &vpt->tmpl_data_value, vpt->tmpl_data_length, '\0');
+		}
+	}
+		break;
+
+	/*
+	 *	We should never be expanding these.
+	 */
+	case TMPL_TYPE_UNKNOWN:
+	case TMPL_TYPE_NULL:
+	case TMPL_TYPE_LIST:
+	case TMPL_TYPE_REGEX:
+	case TMPL_TYPE_ATTR_UNDEFINED:
+	case TMPL_TYPE_REGEX_STRUCT:
+		rad_assert(0 == 1);
+		slen = -1;
+		break;
+	}
+
+	if (slen < 0) return slen;
+
+
+#if 0
+	/*
+	 *	If we're doing correct escapes, we may have to re-parse the string.
+	 *	If the string is from another expansion, it needs re-parsing.
+	 *	Or, if it's from a "string" attribute, it needs re-parsing.
+	 *	Integers, IP addresses, etc. don't need re-parsing.
+	 */
+	if (cf_new_escape && (vpt->type != TMPL_TYPE_ATTR)) {
+	     	value_data_t	vd;
+	     	int		ret;
+
+		PW_TYPE type = PW_TYPE_STRING;
+
+		slen = value_data_from_str(ctx, &vd, &type, NULL, *out, slen, '"');
+		talloc_free(*out);	/* free the old value */
+		*out = vd.ptr;
+	}
+#endif
+
+	if (vpt->type == TMPL_TYPE_XLAT_STRUCT) {
+		RDEBUG2("EXPAND %s", vpt->name); /* xlat_struct doesn't do this */
+		RDEBUG2("   --> %s", buff);
+	}
+
+	return slen;
+}
+
+/** Expand a template to a string, allocing a new buffer to hold the string
+ *
+ * The intended use of #tmpl_expand and #tmpl_aexpand is for modules to easily convert a #vp_tmpl_t
+ * provided by the conf parser, into a usable value.
+ * The value returned should be raw and undoctored for #PW_TYPE_STRING and #PW_TYPE_OCTETS types,
+ * and the printable (string) version of the data for all others.
+ *
+ * This function will always duplicate values, whereas #tmpl_expand may return a pointer to an
+ * existing buffer.
+ *
+ * @note This function is used where raw string values are needed, which may mean the string
+ *	returned may be binary data or contain unprintable chars. #fr_prints or #fr_aprints should
+ *	be used before using these values in debug statements. #is_printable can be used to check
+ *	if the string only contains printable chars.
+ *
+ * @note The type (char or uint8_t) can be obtained with talloc_get_type, and may be used as a
+ *	hint as to how to process or print the data.
+ *
+ * @param ctx to allocate new buffer in.
+ * @param out Where to write pointer to the new buffer.
+ * @param request Current request.
+ * @param vpt to expand. Must be one of the following types:
+ *	- #TMPL_TYPE_LITERAL
+ *	- #TMPL_TYPE_EXEC
+ *	- #TMPL_TYPE_XLAT
+ *	- #TMPL_TYPE_XLAT_STRUCT
+ *	- #TMPL_TYPE_ATTR
+ *	- #TMPL_TYPE_DATA
+ * @param escape xlat escape function (only used for xlat types).
+ * @param escape_ctx xlat escape function data (only used for xlat types).
+ * @return
+ *	- -1 on failure.
+ *	- The length of data written to buff, or pointed to by out.
+ */
+ssize_t tmpl_aexpand(TALLOC_CTX *ctx, char **out, REQUEST *request, vp_tmpl_t const *vpt,
+		     xlat_escape_t escape, void *escape_ctx)
+{
+	VALUE_PAIR *vp;
+	ssize_t slen = -1;	/* quiet compiler */
+
+	rad_assert(vpt->type != TMPL_TYPE_LIST);
+
+	VERIFY_TMPL(vpt);
+
+	*out = NULL;
+
+	switch (vpt->type) {
+	case TMPL_TYPE_LITERAL:
+		RDEBUG4("EXPAND TMPL LITERAL");
+		*out = talloc_bstrndup(ctx, vpt->name, vpt->len);
+		return vpt->len;
+
+	case TMPL_TYPE_EXEC:
+	{
+		char *buff = NULL;
+
+		RDEBUG4("EXPAND TMPL EXEC");
+		buff = talloc_array(ctx, char, 1024);
+		if (radius_exec_program(request, buff, 1024, NULL, request, vpt->name, NULL,
+					true, false, EXEC_TIMEOUT) != 0) {
+			TALLOC_FREE(buff);
+			return -1;
+		}
+		slen = strlen(buff);
+		*out = buff;
+	}
+		break;
+
+	case TMPL_TYPE_XLAT:
+		RDEBUG4("EXPAND TMPL XLAT");
+		/* Error in expansion, this is distinct from zero length expansion */
+		slen = radius_axlat(out, request, vpt->name, escape, escape_ctx);
+		if (slen < 0) {
+			rad_assert(!*out);
+			return slen;
+		}
+		rad_assert(*out);
+		slen = strlen(*out);
+		break;
+
+	case TMPL_TYPE_XLAT_STRUCT:
+		RDEBUG4("EXPAND TMPL XLAT STRUCT");
+		/* Error in expansion, this is distinct from zero length expansion */
+		slen = radius_axlat_struct(out, request, vpt->tmpl_xlat, escape, escape_ctx);
+		if (slen < 0) {
+			rad_assert(!*out);
+			return slen;
+		}
+		slen = strlen(*out);
+		break;
+
+	case TMPL_TYPE_ATTR:
+	{
+		int ret;
+
+		RDEBUG4("EXPAND TMPL ATTR");
+		ret = tmpl_find_vp(&vp, request, vpt);
+		if (ret < 0) return -2;
+
+		switch (vpt->tmpl_da->type) {
+		case PW_TYPE_STRING:
+			*out = talloc_bstrndup(ctx, vp->vp_strvalue, vp->vp_length);
+			if (!*out) return -1;
+			slen = vp->vp_length;
+			break;
+
+		case PW_TYPE_OCTETS:
+			*out = talloc_memdup(ctx, vp->vp_octets, vp->vp_length);
+			if (!*out) return -1;
+			slen = vp->vp_length;
+			break;
+
+		default:
+			*out = vp_aprints_value(ctx, vp, '\0');
+			if (!*out) return -1;
+			slen = talloc_array_length(*out) - 1;
+			break;
+		}
+	}
+		break;
+
+	case TMPL_TYPE_DATA:
+	{
+		RDEBUG4("EXPAND TMPL DATA");
+
+		switch (vpt->tmpl_data_type) {
+		case PW_TYPE_STRING:
+			*out = talloc_bstrndup(ctx, vpt->tmpl_data_value.strvalue, vpt->tmpl_data_length);
+			if (!*out) return -1;
+			slen = vpt->tmpl_data_length;
+			break;
+
+		case PW_TYPE_OCTETS:
+			*out = talloc_memdup(ctx, vpt->tmpl_data_value.octets, vpt->tmpl_data_length);
+			if (!*out) return -1;
+			slen = vpt->tmpl_data_length;
+			break;
+
+		default:
+			*out = value_data_aprints(ctx, vpt->tmpl_data_type, NULL, &vpt->tmpl_data_value, vpt->tmpl_data_length, '\0');
+			if (!*out) return -1;
+			slen = talloc_array_length(*out) - 1;
+			break;
+		}
+	}
+		break;
+
+	/*
+	 *	We should never be expanding these.
+	 */
+	case TMPL_TYPE_UNKNOWN:
+	case TMPL_TYPE_NULL:
+	case TMPL_TYPE_LIST:
+	case TMPL_TYPE_REGEX:
+	case TMPL_TYPE_ATTR_UNDEFINED:
+	case TMPL_TYPE_REGEX_STRUCT:
+		rad_assert(0 == 1);
+		slen = -1;
+		break;
+	}
+
+	if (slen < 0) return slen;
+
+	/*
+	 *	If we're doing correct escapes, we may have to re-parse the string.
+	 *	If the string is from another expansion, it needs re-parsing.
+	 *	Or, if it's from a "string" attribute, it needs re-parsing.
+	 *	Integers, IP addresses, etc. don't need re-parsing.
+	 */
+	if (cf_new_escape && (vpt->type != TMPL_TYPE_ATTR)) {
+	     	value_data_t	vd;
+
+		PW_TYPE type = PW_TYPE_STRING;
+
+		slen = value_data_from_str(ctx, &vd, &type, NULL, *out, slen, '"');
+		talloc_free(*out);	/* free the old value */
+		*out = vd.ptr;
+	}
+
+	if (vpt->type == TMPL_TYPE_XLAT_STRUCT) {
+		RDEBUG2("EXPAND %s", vpt->name); /* xlat_struct doesn't do this */
+		RDEBUG2("   --> %s", *out);
+	}
+
+	return slen;
+}
+
+/** Print a #vp_tmpl_t to a string
+ *
+ * @param[out] out Where to write the presentation format #vp_tmpl_t string.
+ * @param[in] outlen Size of output buffer.
+ * @param[in] vpt to print
+ * @param[in] values Used for integer attributes only. #DICT_ATTR to use when mapping integer
+ *	values to strings.
+ * @return the size of the string written to the output buffer.
+ */
+size_t tmpl_prints(char *out, size_t outlen, vp_tmpl_t const *vpt, DICT_ATTR const *values)
+{
+	size_t len;
+	char c;
+	char const *p;
+	char *q = out;
+
+	if (!vpt) {
+		*out = '\0';
+		return 0;
+	}
+
+	VERIFY_TMPL(vpt);
+
+	switch (vpt->type) {
+	default:
+		return 0;
+
+	case TMPL_TYPE_REGEX:
+	case TMPL_TYPE_REGEX_STRUCT:
+		c = '/';
+		break;
+
+	case TMPL_TYPE_XLAT:
+	case TMPL_TYPE_XLAT_STRUCT:
+		c = '"';
+		break;
+	case TMPL_TYPE_LITERAL:	/* single-quoted or bare word */
+		/*
+		 *	Hack
+		 */
+		for (p = vpt->name; *p != '\0'; p++) {
+			if (*p == ' ') break;
+			if (*p == '\'') break;
+			if (!dict_attr_allowed_chars[(int) *p]) break;
+		}
+
+		if (!*p) {
+			strlcpy(out, vpt->name, outlen);
+			return strlen(out);
+		}
+
+		c = vpt->quote;
+		break;
+
+	case TMPL_TYPE_EXEC:
+		c = '`';
+		break;
+
+	case TMPL_TYPE_LIST:
+		out[0] = '&';
+		if (vpt->tmpl_request == REQUEST_CURRENT) {
+			snprintf(out + 1, outlen - 1, "%s:",
+				 fr_int2str(pair_lists, vpt->tmpl_list, ""));
+		} else {
+			snprintf(out + 1, outlen - 1, "%s.%s:",
+				 fr_int2str(request_refs, vpt->tmpl_request, ""),
+				 fr_int2str(pair_lists, vpt->tmpl_list, ""));
+		}
+		len = strlen(out);
+		goto attr_inst_tag;
+
+	case TMPL_TYPE_ATTR:
+		out[0] = '&';
+		if (vpt->tmpl_request == REQUEST_CURRENT) {
+			if (vpt->tmpl_list == PAIR_LIST_REQUEST) {
+				strlcpy(out + 1, vpt->tmpl_da->name, outlen - 1);
+			} else {
+				snprintf(out + 1, outlen - 1, "%s:%s",
+					 fr_int2str(pair_lists, vpt->tmpl_list, ""),
+					 vpt->tmpl_da->name);
+			}
+
+		} else {
+			snprintf(out + 1, outlen - 1, "%s.%s:%s",
+				 fr_int2str(request_refs, vpt->tmpl_request, ""),
+				 fr_int2str(pair_lists, vpt->tmpl_list, ""),
+				 vpt->tmpl_da->name);
+		}
+
+		len = strlen(out);
+
+	attr_inst_tag:
+		if ((vpt->tmpl_tag == TAG_ANY) && (vpt->tmpl_num == NUM_ANY)) return len;
+
+		q = out + len;
+		outlen -= len;
+
+		if (vpt->tmpl_tag != TAG_ANY) {
+			snprintf(q, outlen, ":%d", vpt->tmpl_tag);
+			len = strlen(q);
+			q += len;
+			outlen -= len;
+		}
+
+		switch (vpt->tmpl_num) {
+		case NUM_ANY:
+			break;
+
+		case NUM_ALL:
+			snprintf(q, outlen, "[*]");
+			len = strlen(q);
+			q += len;
+			break;
+
+		case NUM_COUNT:
+			snprintf(q, outlen, "[#]");
+			len = strlen(q);
+			q += len;
+			break;
+
+		case NUM_LAST:
+			snprintf(q, outlen, "[n]");
+			len = strlen(q);
+			q += len;
+			break;
+
+		default:
+			snprintf(q, outlen, "[%i]", vpt->tmpl_num);
+			len = strlen(q);
+			q += len;
+			break;
+		}
+
+		return (q - out);
+
+	case TMPL_TYPE_ATTR_UNDEFINED:
+		out[0] = '&';
+		if (vpt->tmpl_request == REQUEST_CURRENT) {
+			if (vpt->tmpl_list == PAIR_LIST_REQUEST) {
+				strlcpy(out + 1, vpt->tmpl_unknown_name, outlen - 1);
+			} else {
+				snprintf(out + 1, outlen - 1, "%s:%s",
+					 fr_int2str(pair_lists, vpt->tmpl_list, ""),
+					 vpt->tmpl_unknown_name);
+			}
+
+		} else {
+			snprintf(out + 1, outlen - 1, "%s.%s:%s",
+				 fr_int2str(request_refs, vpt->tmpl_request, ""),
+				 fr_int2str(pair_lists, vpt->tmpl_list, ""),
+				 vpt->tmpl_unknown_name);
+		}
+
+		len = strlen(out);
+
+		if (vpt->tmpl_num == NUM_ANY) {
+			return len;
+		}
+
+		q = out + len;
+		outlen -= len;
+
+		if (vpt->tmpl_num != NUM_ANY) {
+			snprintf(q, outlen, "[%i]", vpt->tmpl_num);
+			len = strlen(q);
+			q += len;
+		}
+
+		return (q - out);
+
+	case TMPL_TYPE_DATA:
+		return value_data_prints(out, outlen, vpt->tmpl_data_type, values, &vpt->tmpl_data_value,
+					 vpt->tmpl_data_length, vpt->quote);
+	}
+
+	if (outlen <= 3) {
+		*out = '\0';
+		return 0;
+	}
+
+	*(q++) = c;
+
+	/*
+	 *	Print it with appropriate escaping
+	 */
+	if (cf_new_escape && (c == '/')) {
+		len = fr_prints(q, outlen - 3, vpt->name, vpt->len, '\0');
+	} else {
+		len = fr_prints(q, outlen - 3, vpt->name, vpt->len, c);
+	}
+
+	q += len;
+	*(q++) = c;
+	*q = '\0';
+
+	return q - out;
+}
+
+/** Initialise a #vp_cursor_t to the #VALUE_PAIR specified by a #vp_tmpl_t
+ *
+ * This makes iterating over the one or more #VALUE_PAIR specified by a #vp_tmpl_t
+ * significantly easier.
+ *
+ * @param err May be NULL if no error code is required. Will be set to:
+ *	- 0 on success.
+ *	- -1 if no matching #VALUE_PAIR could be found.
+ *	- -2 if list could not be found (doesn't exist in current #REQUEST).
+ *	- -3 if context could not be found (no parent #REQUEST available).
+ * @param cursor to store iterator state.
+ * @param request The current #REQUEST.
+ * @param vpt specifying the #VALUE_PAIR type/tag or list to iterate over.
+ * @return the first #VALUE_PAIR specified by the #vp_tmpl_t, or NULL if no matching
+ *	#VALUE_PAIR found, and NULL on error.
+ *
+ * @see tmpl_cursor_next
+ */
+VALUE_PAIR *tmpl_cursor_init(int *err, vp_cursor_t *cursor, REQUEST *request, vp_tmpl_t const *vpt)
+{
+	VALUE_PAIR **vps, *vp = NULL;
+	int num;
+
+	VERIFY_TMPL(vpt);
+
+	rad_assert((vpt->type == TMPL_TYPE_ATTR) || (vpt->type == TMPL_TYPE_LIST));
+
+	if (err) *err = 0;
+
+	if (radius_request(&request, vpt->tmpl_request) < 0) {
+		if (err) *err = -3;
+		return NULL;
+	}
+	vps = radius_list(request, vpt->tmpl_list);
+	if (!vps) {
+		if (err) *err = -2;
+		return NULL;
+	}
+	(void) fr_cursor_init(cursor, vps);
+
+	switch (vpt->type) {
+	/*
+	 *	May not may not be found, but it *is* a known name.
+	 */
+	case TMPL_TYPE_ATTR:
+		switch (vpt->tmpl_num) {
+		case NUM_ANY:
+			vp = fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag);
+			if (!vp) {
+				if (err) *err = -1;
+				return NULL;
+			}
+			VERIFY_VP(vp);
+			return vp;
+
+		/*
+		 *	Get the last instance of a VALUE_PAIR.
+		 */
+		case NUM_LAST:
+		{
+			VALUE_PAIR *last = NULL;
+
+			while ((vp = fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag))) {
+				VERIFY_VP(vp);
+				last = vp;
+			}
+			VERIFY_VP(last);
+			if (!last) break;
+			return last;
+		}
+
+		/*
+		 *	Callers expect NUM_COUNT to setup the cursor to point
+		 *	to the first attribute in the list we're meant to be
+		 *	counting.
+		 *
+		 *	It does not produce a virtual attribute containing the
+		 *	total number of attributes.
+		 */
+		case NUM_COUNT:
+			return fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag);
+
+		default:
+			num = vpt->tmpl_num;
+			while ((vp = fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag))) {
+				VERIFY_VP(vp);
+				if (num-- <= 0) return vp;
+			}
+			break;
+		}
+
+		if (err) *err = -1;
+		return NULL;
+
+	case TMPL_TYPE_LIST:
+		switch (vpt->tmpl_num) {
+		case NUM_COUNT:
+		case NUM_ANY:
+		case NUM_ALL:
+			vp = fr_cursor_init(cursor, vps);
+			if (!vp) {
+				if (err) *err = -1;
+				return NULL;
+			}
+			VERIFY_VP(vp);
+			return vp;
+
+		/*
+		 *	Get the last instance of a VALUE_PAIR.
+		 */
+		case NUM_LAST:
+		{
+			VALUE_PAIR *last = NULL;
+
+			for (vp = fr_cursor_init(cursor, vps);
+			     vp;
+			     vp = fr_cursor_next(cursor)) {
+				VERIFY_VP(vp);
+				last = vp;
+			}
+			if (!last) break;
+			VERIFY_VP(last);
+			return last;
+		}
+
+		default:
+			num = vpt->tmpl_num;
+			for (vp = fr_cursor_init(cursor, vps);
+			     vp;
+			     vp = fr_cursor_next(cursor)) {
+				VERIFY_VP(vp);
+				if (num-- <= 0) return vp;
+			}
+			break;
+		}
+
+		break;
+
+	default:
+		rad_assert(0);
+	}
+
+	return vp;
+}
+
+/** Returns the next #VALUE_PAIR specified by vpt
+ *
+ * @param cursor initialised with #tmpl_cursor_init.
+ * @param vpt specifying the #VALUE_PAIR type/tag to iterate over.
+ *	Must be one of the following types:
+ *	- #TMPL_TYPE_LIST
+ *	- #TMPL_TYPE_ATTR
+ * @return NULL if no more matching #VALUE_PAIR of the specified type/tag are found.
+ */
+VALUE_PAIR *tmpl_cursor_next(vp_cursor_t *cursor, vp_tmpl_t const *vpt)
+{
+	rad_assert((vpt->type == TMPL_TYPE_ATTR) || (vpt->type == TMPL_TYPE_LIST));
+
+	VERIFY_TMPL(vpt);
+
+	switch (vpt->type) {
+	/*
+	 *	May not may not be found, but it *is* a known name.
+	 */
+	case TMPL_TYPE_ATTR:
+		switch (vpt->tmpl_num) {
+		default:
+			return NULL;
+
+		case NUM_ALL:
+		case NUM_COUNT:	/* This cursor is being used to count matching attrs */
+			break;
+		}
+		return fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag);
+
+	case TMPL_TYPE_LIST:
+		switch (vpt->tmpl_num) {
+		default:
+			return NULL;
+
+		case NUM_ALL:
+		case NUM_COUNT:	/* This cursor is being used to count matching attrs */
+			break;
+		}
+		return fr_cursor_next(cursor);
+
+	default:
+		rad_assert(0);
+		return NULL;	/* Older versions of GCC flag the lack of return as an error */
+	}
+}
+
+/** Copy pairs matching a #vp_tmpl_t in the current #REQUEST
+ *
+ * @param ctx to allocate new #VALUE_PAIR in.
+ * @param out Where to write the copied #VALUE_PAIR (s).
+ * @param request The current #REQUEST.
+ * @param vpt specifying the #VALUE_PAIR type/tag or list to copy.
+ *	Must be one of the following types:
+ *	- #TMPL_TYPE_LIST
+ *	- #TMPL_TYPE_ATTR
+ * @return
+ *	- -1 if no matching #VALUE_PAIR could be found.
+ *	- -2 if list could not be found (doesn't exist in current #REQUEST).
+ *	- -3 if context could not be found (no parent #REQUEST available).
+ *	- -4 on memory allocation error.
+ */
+int tmpl_copy_vps(TALLOC_CTX *ctx, VALUE_PAIR **out, REQUEST *request, vp_tmpl_t const *vpt)
+{
+	VALUE_PAIR *vp;
+	vp_cursor_t from, to;
+
+	VERIFY_TMPL(vpt);
+
+	int err;
+
+	rad_assert((vpt->type == TMPL_TYPE_ATTR) || (vpt->type == TMPL_TYPE_LIST));
+
+	*out = NULL;
+
+	fr_cursor_init(&to, out);
+
+	for (vp = tmpl_cursor_init(&err, &from, request, vpt);
+	     vp;
+	     vp = tmpl_cursor_next(&from, vpt)) {
+		vp = fr_pair_copy(ctx, vp);
+		if (!vp) {
+			fr_pair_list_free(out);
+			return -4;
+		}
+		fr_cursor_insert(&to, vp);
+	}
+
+	return err;
+}
+
+/** Returns the first VP matching a #vp_tmpl_t
+ *
+ * @param out where to write the retrieved vp.
+ * @param request The current #REQUEST.
+ * @param vpt specifying the #VALUE_PAIR type/tag to find.
+ *	Must be one of the following types:
+ *	- #TMPL_TYPE_LIST
+ *	- #TMPL_TYPE_ATTR
+ * @return
+ *	- -1 if no matching #VALUE_PAIR could be found.
+ *	- -2 if list could not be found (doesn't exist in current #REQUEST).
+ *	- -3 if context could not be found (no parent #REQUEST available).
+ */
+int tmpl_find_vp(VALUE_PAIR **out, REQUEST *request, vp_tmpl_t const *vpt)
+{
+	vp_cursor_t cursor;
+	VALUE_PAIR *vp;
+
+	VERIFY_TMPL(vpt);
+
+	int err;
+
+	vp = tmpl_cursor_init(&err, &cursor, request, vpt);
+	if (out) *out = vp;
+
+	return err;
+}
+/* @} **/
+
+#ifdef WITH_VERIFY_PTR
+/** Used to check whether areas of a vp_tmpl_t are zeroed out
+ *
+ * @param ptr Offset to begin checking at.
+ * @param len How many bytes to check.
+ * @return pointer to the first non-zero byte, or NULL if all bytes were zero.
+ */
+static uint8_t const *not_zeroed(uint8_t const *ptr, size_t len)
+{
+	size_t i;
+
+	for (i = 0; i < len; i++) {
+		if (ptr[i] != 0x00) return ptr + i;
+	}
+
+	return NULL;
+}
+#define CHECK_ZEROED(_x) not_zeroed((uint8_t const *)&_x + sizeof(_x), sizeof(vpt->data) - sizeof(_x))
+
+/** Verify fields of a vp_tmpl_t make sense
+ *
+ * @note If the #vp_tmpl_t is invalid, causes the server to exit.
+ *
+ * @param file obtained with __FILE__.
+ * @param line obtained with __LINE__.
+ * @param vpt to check.
+ */
+void tmpl_verify(char const *file, int line, vp_tmpl_t const *vpt)
+{
+	rad_assert(vpt);
+
+	if (vpt->type == TMPL_TYPE_UNKNOWN) {
+		FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: vp_tmpl_t type was "
+			     "TMPL_TYPE_UNKNOWN (uninitialised)", file, line);
+		fr_assert(0);
+		fr_exit_now(1);
+	}
+
+	if (vpt->type > TMPL_TYPE_NULL) {
+		FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: vp_tmpl_t type was %i "
+			     "(outside range of tmpl_names)", file, line, vpt->type);
+		fr_assert(0);
+		fr_exit_now(1);
+	}
+
+	/*
+	 *  Do a memcmp of the bytes after where the space allocated for
+	 *  the union member should have ended and the end of the union.
+	 *  These should always be zero if the union has been initialised
+	 *  properly.
+	 *
+	 *  If they're still all zero, do TMPL_TYPE specific checks.
+	 */
+	switch (vpt->type) {
+	case TMPL_TYPE_NULL:
+		if (not_zeroed((uint8_t const *)&vpt->data, sizeof(vpt->data))) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_NULL "
+				     "has non-zero bytes in its data union", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+		break;
+
+	case TMPL_TYPE_LITERAL:
+		if (not_zeroed((uint8_t const *)&vpt->data, sizeof(vpt->data))) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_LITERAL "
+				     "has non-zero bytes in its data union", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+		break;
+
+	case TMPL_TYPE_XLAT:
+	case TMPL_TYPE_XLAT_STRUCT:
+		break;
+
+/* @todo When regexes get converted to xlat the flags field of the regex union is used
+	case TMPL_TYPE_XLAT:
+		if (not_zeroed((uint8_t const *)&vpt->data, sizeof(vpt->data))) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_XLAT "
+				     "has non-zero bytes in its data union", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+		break;
+
+	case TMPL_TYPE_XLAT_STRUCT:
+		if (CHECK_ZEROED(vpt->data.xlat)) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_XLAT_STRUCT "
+				     "has non-zero bytes after the data.xlat pointer in the union", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+		break;
+*/
+
+	case TMPL_TYPE_EXEC:
+		if (not_zeroed((uint8_t const *)&vpt->data, sizeof(vpt->data))) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_EXEC "
+				     "has non-zero bytes in its data union", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+		break;
+
+	case TMPL_TYPE_ATTR_UNDEFINED:
+		rad_assert(vpt->tmpl_da == NULL);
+		break;
+
+	case TMPL_TYPE_ATTR:
+		if (CHECK_ZEROED(vpt->data.attribute)) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_ATTR "
+				     "has non-zero bytes after the data.attribute struct in the union",
+				     file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+
+		if (vpt->tmpl_da->flags.is_unknown) {
+			if (vpt->tmpl_da != (DICT_ATTR const *)&vpt->data.attribute.unknown.da) {
+				FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_ATTR "
+					     "da is marked as unknown, but does not point to the template's "
+					     "unknown da buffer", file, line);
+				fr_assert(0);
+				fr_exit_now(1);
+			}
+
+		} else {
+			DICT_ATTR const *da;
+
+			/*
+			 *	Attribute may be present with multiple names
+			 */
+			da = dict_attrbyname(vpt->tmpl_da->name);
+			if (!da) {
+				FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_ATTR "
+					     "attribute \"%s\" (%s) not found in global dictionary",
+					     file, line, vpt->tmpl_da->name,
+					     fr_int2str(dict_attr_types, vpt->tmpl_da->type, "<INVALID>"));
+				fr_assert(0);
+				fr_exit_now(1);
+			}
+
+			if ((da->type == PW_TYPE_COMBO_IP_ADDR) && (da->type != vpt->tmpl_da->type)) {
+				da = dict_attrbytype(vpt->tmpl_da->attr, vpt->tmpl_da->vendor, vpt->tmpl_da->type);
+				if (!da) {
+					FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_ATTR "
+						     "attribute \"%s\" variant (%s) not found in global dictionary",
+						     file, line, vpt->tmpl_da->name,
+						     fr_int2str(dict_attr_types, vpt->tmpl_da->type, "<INVALID>"));
+					fr_assert(0);
+					fr_exit_now(1);
+				}
+			}
+
+			if (da != vpt->tmpl_da) {
+				FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_ATTR "
+					     "dictionary pointer %p \"%s\" (%s) "
+					     "and global dictionary pointer %p \"%s\" (%s) differ",
+					     file, line,
+					     vpt->tmpl_da, vpt->tmpl_da->name,
+					     fr_int2str(dict_attr_types, vpt->tmpl_da->type, "<INVALID>"),
+					     da, da->name,
+					     fr_int2str(dict_attr_types, da->type, "<INVALID>"));
+				fr_assert(0);
+				fr_exit_now(1);
+			}
+		}
+		break;
+
+	case TMPL_TYPE_LIST:
+		if (CHECK_ZEROED(vpt->data.attribute)) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_LIST"
+				     "has non-zero bytes after the data.attribute struct in the union", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+
+		if (vpt->tmpl_da != NULL) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_LIST da pointer was NULL", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+		break;
+
+	case TMPL_TYPE_DATA:
+		if (CHECK_ZEROED(vpt->data.literal)) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA "
+				     "has non-zero bytes after the data.literal struct in the union",
+				     file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+
+		if (vpt->tmpl_data_type == PW_TYPE_INVALID) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA type was "
+				     "PW_TYPE_INVALID (uninitialised)", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+
+		if (vpt->tmpl_data_type >= PW_TYPE_MAX) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA type was "
+				     "%i (outside the range of PW_TYPEs)", file, line, vpt->tmpl_data_type);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+		/*
+		 *	Unlike VALUE_PAIRs we can't guarantee that VALUE_PAIR_TMPL buffers will
+		 *	be talloced. They may be allocated on the stack or in global variables.
+		 */
+		switch (vpt->tmpl_data_type) {
+		case PW_TYPE_STRING:
+		if (vpt->tmpl_data.vp_strvalue[vpt->tmpl_data_length] != '\0') {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA char buffer not \\0 "
+				     "terminated", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+			break;
+
+		case PW_TYPE_TLV:
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA is of type TLV",
+				     file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+
+		case PW_TYPE_OCTETS:
+			break;
+
+		default:
+			if (vpt->tmpl_data_length == 0) {
+				FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA data pointer not NULL "
+				             "but len field is zero", file, line);
+				fr_assert(0);
+				fr_exit_now(1);
+			}
+		}
+
+		break;
+
+	case TMPL_TYPE_REGEX:
+		/*
+		 *	iflag field is used for non compiled regexes too.
+		 */
+		if (CHECK_ZEROED(vpt->data.preg)) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX "
+				     "has non-zero bytes after the data.preg struct in the union", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+
+		if (vpt->tmpl_preg != NULL) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX "
+				     "preg field was not nULL", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+
+		if ((vpt->tmpl_iflag != true) && (vpt->tmpl_iflag != false)) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX "
+				     "iflag field was neither true or false", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+
+		if ((vpt->tmpl_mflag != true) && (vpt->tmpl_mflag != false)) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX "
+				     "mflag field was neither true or false", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+
+		break;
+
+	case TMPL_TYPE_REGEX_STRUCT:
+		if (CHECK_ZEROED(vpt->data.preg)) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX_STRUCT "
+				     "has non-zero bytes after the data.preg struct in the union", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+
+		if (vpt->tmpl_preg == NULL) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX_STRUCT "
+				     "comp field was NULL", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+
+		if ((vpt->tmpl_iflag != true) && (vpt->tmpl_iflag != false)) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX_STRUCT "
+				     "iflag field was neither true or false", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+
+		if ((vpt->tmpl_mflag != true) && (vpt->tmpl_mflag != false)) {
+			FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX "
+				     "mflag field was neither true or false", file, line);
+			fr_assert(0);
+			fr_exit_now(1);
+		}
+		break;
+
+	case TMPL_TYPE_UNKNOWN:
+		rad_assert(0);
+	}
+}
+#endif
diff --git a/src/main/unittest.c b/src/main/unittest.c
new file mode 100644
index 0000000..72fdadc
--- /dev/null
+++ b/src/main/unittest.c
@@ -0,0 +1,982 @@
+/*
+ * unittest.c	Unit test wrapper for the RADIUS daemon.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2000-2013  The FreeRADIUS server project
+ * Copyright 2013  Alan DeKok <aland@ox.org>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/state.h>
+#include <freeradius-devel/rad_assert.h>
+#include <freeradius-devel/event.h>
+
+#ifdef HAVE_GETOPT_H
+#	include <getopt.h>
+#endif
+
+#include <ctype.h>
+
+/*
+ *  Global variables.
+ */
+char const *radacct_dir = NULL;
+char const *radlog_dir = NULL;
+bool log_stripped_names = false;
+
+static bool memory_report = false;
+static bool filedone = false;
+
+char const *radiusd_version = "FreeRADIUS Version " RADIUSD_VERSION_STRING
+#ifdef RADIUSD_VERSION_COMMIT
+" (git #" STRINGIFY(RADIUSD_VERSION_COMMIT) ")"
+#endif
+", for host " HOSTINFO
+#ifndef ENABLE_REPRODUCIBLE_BUILDS
+", built on " __DATE__ " at " __TIME__
+#endif
+;
+
+fr_event_list_t	*el = NULL;
+
+/*
+ *	Static functions.
+ */
+static void usage(int);
+
+void listen_free(UNUSED rad_listen_t **head)
+{
+	/* do nothing */
+}
+
+void request_inject(UNUSED REQUEST *request)
+{
+	/* do nothing */
+}
+
+#ifdef WITH_RADIUSV11
+int fr_radiusv11_client_init(UNUSED fr_tls_server_conf_t *tls);
+
+int fr_radiusv11_client_init(UNUSED fr_tls_server_conf_t *tls)
+{
+	return 0;
+}
+#endif
+
+static rad_listen_t *listen_alloc(void *ctx)
+{
+	rad_listen_t *this;
+
+	this = talloc_zero(ctx, rad_listen_t);
+	if (!this) return NULL;
+
+	this->type = RAD_LISTEN_AUTH;
+	this->recv = NULL;
+	this->send = NULL;
+	this->print = NULL;
+	this->encode = NULL;
+	this->decode = NULL;
+
+	/*
+	 *	We probably don't care about this.  We can always add
+	 *	fields later.
+	 */
+	this->data = talloc_zero(this, listen_socket_t);
+	if (!this->data) {
+		talloc_free(this);
+		return NULL;
+	}
+
+	return this;
+}
+
+static RADCLIENT *client_alloc(void *ctx)
+{
+	RADCLIENT *client;
+
+	client = talloc_zero(ctx, RADCLIENT);
+	if (!client) return NULL;
+
+	return client;
+}
+
+static REQUEST *request_setup(FILE *fp)
+{
+	VALUE_PAIR	*vp;
+	REQUEST		*request;
+	vp_cursor_t	cursor;
+	struct timeval	now;
+
+	/*
+	 *	Create and initialize the new request.
+	 */
+	request = request_alloc(NULL);
+	gettimeofday(&now, NULL);
+	request->timestamp = now.tv_sec;
+
+	request->packet = rad_alloc(request, false);
+	if (!request->packet) {
+		ERROR("No memory");
+		talloc_free(request);
+		return NULL;
+	}
+	request->packet->timestamp = now;
+
+	request->reply = rad_alloc(request, false);
+	if (!request->reply) {
+		ERROR("No memory");
+		talloc_free(request);
+		return NULL;
+	}
+
+	request->listener = listen_alloc(request);
+	request->client = client_alloc(request);
+
+	request->number = 0;
+
+	request->master_state = REQUEST_ACTIVE;
+	request->child_state = REQUEST_RUNNING;
+	request->handle = NULL;
+	request->server = talloc_typed_strdup(request, "default");
+
+	request->root = &main_config;
+
+	/*
+	 *	Read packet from fp
+	 */
+	if (fr_pair_list_afrom_file(request->packet, &request->packet->vps, fp, &filedone) < 0) {
+		fr_perror("unittest");
+		talloc_free(request);
+		return NULL;
+	}
+
+	/*
+	 *	Set the defaults for IPs, etc.
+	 */
+	request->packet->code = PW_CODE_ACCESS_REQUEST;
+
+	request->packet->src_ipaddr.af = AF_INET;
+	request->packet->src_ipaddr.ipaddr.ip4addr.s_addr = htonl(INADDR_LOOPBACK);
+	request->packet->src_port = 18120;
+
+	request->packet->dst_ipaddr.af = AF_INET;
+	request->packet->dst_ipaddr.ipaddr.ip4addr.s_addr = htonl(INADDR_LOOPBACK);
+	request->packet->dst_port = 1812;
+
+	/*
+	 *	Copied from radclient
+	 *
+	 *	Fix up Digest-Attributes issues
+	 */
+	for (vp = fr_cursor_init(&cursor, &request->packet->vps);
+	     vp;
+	     vp = fr_cursor_next(&cursor)) {
+		/*
+		 *	Double quoted strings get marked up as xlat expansions,
+		 *	but we don't support that here.
+		 */
+		if (vp->type == VT_XLAT) {
+			vp->vp_strvalue = vp->value.xlat;
+			vp->value.xlat = NULL;
+			vp->type = VT_DATA;
+		}
+
+		if (!vp->da->vendor) switch (vp->da->attr) {
+		default:
+			break;
+
+			/*
+			 *	Allow it to set the packet type in
+			 *	the attributes read from the file.
+			 */
+		case PW_PACKET_TYPE:
+			request->packet->code = vp->vp_integer;
+			break;
+
+		case PW_PACKET_DST_PORT:
+			request->packet->dst_port = (vp->vp_integer & 0xffff);
+			break;
+
+		case PW_PACKET_DST_IP_ADDRESS:
+			request->packet->dst_ipaddr.af = AF_INET;
+			request->packet->dst_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+			request->packet->dst_ipaddr.prefix = 32;
+			break;
+
+		case PW_PACKET_DST_IPV6_ADDRESS:
+			request->packet->dst_ipaddr.af = AF_INET6;
+			request->packet->dst_ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr;
+			request->packet->dst_ipaddr.prefix = 128;
+			break;
+
+		case PW_PACKET_SRC_PORT:
+			request->packet->src_port = (vp->vp_integer & 0xffff);
+			break;
+
+		case PW_PACKET_SRC_IP_ADDRESS:
+			request->packet->src_ipaddr.af = AF_INET;
+			request->packet->src_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+			request->packet->src_ipaddr.prefix = 32;
+			break;
+
+		case PW_PACKET_SRC_IPV6_ADDRESS:
+			request->packet->src_ipaddr.af = AF_INET6;
+			request->packet->src_ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr;
+			request->packet->src_ipaddr.prefix = 128;
+			break;
+
+		case PW_CHAP_PASSWORD: {
+			int i, already_hex = 0;
+
+			/*
+			 *	If it's 17 octets, it *might* be already encoded.
+			 *	Or, it might just be a 17-character password (maybe UTF-8)
+			 *	Check it for non-printable characters.  The odds of ALL
+			 *	of the characters being 32..255 is (1-7/8)^17, or (1/8)^17,
+			 *	or 1/(2^51), which is pretty much zero.
+			 */
+			if (vp->vp_length == 17) {
+				for (i = 0; i < 17; i++) {
+					if (vp->vp_octets[i] < 32) {
+						already_hex = 1;
+						break;
+					}
+				}
+			}
+
+			/*
+			 *	Allow the user to specify ASCII or hex CHAP-Password
+			 */
+			if (!already_hex) {
+				uint8_t *p;
+				size_t len, len2;
+
+				len = len2 = vp->vp_length;
+				if (len2 < 17) len2 = 17;
+
+				p = talloc_zero_array(vp, uint8_t, len2);
+
+				memcpy(p, vp->vp_strvalue, len);
+
+				rad_chap_encode(request->packet,
+						p,
+						fr_rand() & 0xff, vp);
+				vp->vp_octets = p;
+				vp->vp_length = 17;
+			}
+		}
+			break;
+
+		case PW_DIGEST_REALM:
+		case PW_DIGEST_NONCE:
+		case PW_DIGEST_METHOD:
+		case PW_DIGEST_URI:
+		case PW_DIGEST_QOP:
+		case PW_DIGEST_ALGORITHM:
+		case PW_DIGEST_BODY_DIGEST:
+		case PW_DIGEST_CNONCE:
+		case PW_DIGEST_NONCE_COUNT:
+		case PW_DIGEST_USER_NAME:
+			/* overlapping! */
+		{
+			DICT_ATTR const *da;
+			uint8_t *p, *q;
+
+			p = talloc_array(vp, uint8_t, vp->vp_length + 2);
+
+			memcpy(p + 2, vp->vp_octets, vp->vp_length);
+			p[0] = vp->da->attr - PW_DIGEST_REALM + 1;
+			vp->vp_length += 2;
+			p[1] = vp->vp_length;
+
+			da = dict_attrbyvalue(PW_DIGEST_ATTRIBUTES, 0);
+			rad_assert(da != NULL);
+			vp->da = da;
+
+			/*
+			 *	Re-do fr_pair_value_memsteal ourselves,
+			 *	because we play games with
+			 *	vp->da, and fr_pair_value_memsteal goes
+			 *	to GREAT lengths to sanitize
+			 *	and fix and change and
+			 *	double-check the various
+			 *	fields.
+			 */
+			memcpy(&q, &vp->vp_octets, sizeof(q));
+			talloc_free(q);
+
+			vp->vp_octets = talloc_steal(vp, p);
+			vp->type = VT_DATA;
+
+			VERIFY_VP(vp);
+		}
+
+		break;
+		}
+	} /* loop over the VP's we read in */
+
+	if (rad_debug_lvl) {
+		for (vp = fr_cursor_init(&cursor, &request->packet->vps);
+		     vp;
+		     vp = fr_cursor_next(&cursor)) {
+			/*
+			 *	Take this opportunity to verify all the VALUE_PAIRs are still valid.
+			 */
+			if (!talloc_get_type(vp, VALUE_PAIR)) {
+				ERROR("Expected VALUE_PAIR pointer got \"%s\"", talloc_get_name(vp));
+
+				fr_log_talloc_report(vp);
+				rad_assert(0);
+			}
+
+			vp_print(fr_log_fp, vp);
+		}
+		fflush(fr_log_fp);
+	}
+
+	/*
+	 *	Build the reply template from the request.
+	 */
+	request->reply->sockfd = request->packet->sockfd;
+	request->reply->dst_ipaddr = request->packet->src_ipaddr;
+	request->reply->src_ipaddr = request->packet->dst_ipaddr;
+	request->reply->dst_port = request->packet->src_port;
+	request->reply->src_port = request->packet->dst_port;
+	request->reply->id = request->packet->id;
+	request->reply->code = 0; /* UNKNOWN code */
+	memcpy(request->reply->vector, request->packet->vector,
+	       sizeof(request->reply->vector));
+	request->reply->vps = NULL;
+	request->reply->data = NULL;
+	request->reply->data_len = 0;
+
+	/*
+	 *	Debugging
+	 */
+	request->log.lvl = rad_debug_lvl;
+	request->log.func = vradlog_request;
+
+	request->username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY);
+	request->password = fr_pair_find_by_num(request->packet->vps, PW_USER_PASSWORD, 0, TAG_ANY);
+
+	return request;
+}
+
+
+static void print_packet(FILE *fp, RADIUS_PACKET *packet)
+{
+	VALUE_PAIR *vp;
+	vp_cursor_t cursor;
+
+	if (!packet) {
+		fprintf(fp, "\n");
+		return;
+	}
+
+	fprintf(fp, "%s\n", fr_packet_codes[packet->code]);
+
+	for (vp = fr_cursor_init(&cursor, &packet->vps);
+	     vp;
+	     vp = fr_cursor_next(&cursor)) {
+		/*
+		 *	Take this opportunity to verify all the VALUE_PAIRs are still valid.
+		 */
+		if (!talloc_get_type(vp, VALUE_PAIR)) {
+			ERROR("Expected VALUE_PAIR pointer got \"%s\"", talloc_get_name(vp));
+
+			fr_log_talloc_report(vp);
+			rad_assert(0);
+		}
+
+		vp_print(fp, vp);
+	}
+	fflush(fp);
+}
+
+
+#include <freeradius-devel/modpriv.h>
+
+/*
+ *	%{poke:sql.foo=bar}
+ */
+static ssize_t xlat_poke(UNUSED void *instance, REQUEST *request,
+			 char const *fmt, char *out, size_t outlen)
+{
+	int i;
+	void *data, *base;
+	char *p, *q;
+	module_instance_t *mi;
+	char *buffer;
+	CONF_SECTION *modules;
+	CONF_PAIR *cp;
+	CONF_PARSER const *variables;
+	size_t len;
+
+	rad_assert(outlen > 1);
+	rad_assert(request != NULL);
+	rad_assert(fmt != NULL);
+	rad_assert(out != NULL);
+
+	*out = '\0';
+
+	modules = cf_section_sub_find(request->root->config, "modules");
+	if (!modules) return 0;
+
+	buffer = talloc_strdup(request, fmt);
+	if (!buffer) return 0;
+
+	p = strchr(buffer, '.');
+	if (!p) return 0;
+
+	*(p++) = '\0';
+
+	mi = module_find(modules, buffer);
+	if (!mi) {
+		RDEBUG("Failed finding module '%s'", buffer);
+	fail:
+		talloc_free(buffer);
+		return 0;
+	}
+
+	q = strchr(p, '=');
+	if (!q) {
+		RDEBUG("Failed finding '=' in string '%s'", fmt);
+		goto fail;
+	}
+
+	*(q++) = '\0';
+
+	if (strchr(p, '.') != NULL) {
+		RDEBUG("Can't do sub-sections right now");
+		goto fail;
+	}
+
+	cp = cf_pair_find(mi->cs, p);
+	if (!cp) {
+		RDEBUG("No such item '%s'", p);
+		goto fail;
+	}
+
+	/*
+	 *	Copy the old value to the output buffer, that way
+	 *	tests can restore it later, if they need to.
+	 */
+	len = strlcpy(out, cf_pair_value(cp), outlen);
+
+	if (cf_pair_replace(mi->cs, cp, q) < 0) {
+		RDEBUG("Failed replacing pair");
+		goto fail;
+	}
+
+	base = mi->insthandle;
+	variables = mi->entry->module->config;
+
+	/*
+	 *	Handle the known configuration parameters.
+	 */
+	for (i = 0; variables[i].name != NULL; i++) {
+		int ret;
+
+		if (variables[i].type == PW_TYPE_SUBSECTION) continue;
+		/* else it's a CONF_PAIR */
+
+		/*
+		 *	Not the pair we want.  Skip it.
+		 */
+		if (strcmp(variables[i].name, p) != 0) continue;
+
+		if (variables[i].data) {
+			data = variables[i].data; /* prefer this. */
+		} else if (base) {
+			data = ((char *)base) + variables[i].offset;
+		} else {
+			DEBUG2("Internal sanity check 2 failed in cf_section_parse");
+			goto fail;
+		}
+
+		/*
+		 *	Parse the pair we found, or a default value.
+		 */
+		ret = cf_item_parse(mi->cs, variables[i].name, variables[i].type, data, variables[i].dflt);
+		if (ret < 0) {
+			DEBUG2("Failed inserting new value into module instance data");
+			goto fail;
+		}
+		break;		/* we found it, don't do any more */
+	}
+
+	talloc_free(buffer);
+
+	return len;
+}
+
+
+/*
+ *	Read a file compose of xlat's and expected results
+ */
+static bool do_xlats(char const *filename, FILE *fp)
+{
+	int		lineno = 0;
+	ssize_t		len;
+	char		*p;
+	char		input[8192];
+	char		output[8192];
+	REQUEST		*request;
+	struct timeval	now;
+
+	/*
+	 *	Create and initialize the new request.
+	 */
+	request = request_alloc(NULL);
+	gettimeofday(&now, NULL);
+	request->timestamp = now.tv_sec;
+
+	request->log.lvl = rad_debug_lvl;
+	request->log.func = vradlog_request;
+
+	output[0] = '\0';
+
+	while (fgets(input, sizeof(input), fp) != NULL) {
+		lineno++;
+
+		/*
+		 *	Ignore blank lines and comments
+		 */
+		p = input;
+		while (isspace((uint8_t) *p)) p++;
+
+		if (*p < ' ') continue;
+		if (*p == '#') continue;
+
+		p = strchr(p, '\n');
+		if (!p) {
+			if (!feof(fp)) {
+				fprintf(stderr, "Line %d too long in %s\n",
+					lineno, filename);
+				TALLOC_FREE(request);
+				return false;
+			}
+		} else {
+			*p = '\0';
+		}
+
+		/*
+		 *	Look for "xlat"
+		 */
+		if (strncmp(input, "xlat ", 5) == 0) {
+			ssize_t slen;
+			char const *error = NULL;
+			char *fmt = talloc_typed_strdup(NULL, input + 5);
+			xlat_exp_t *head;
+
+			slen = xlat_tokenize(fmt, fmt, &head, &error);
+			if (slen <= 0) {
+				talloc_free(fmt);
+				snprintf(output, sizeof(output), "ERROR offset %d '%s'", (int) -slen, error);
+				continue;
+			}
+
+			if (input[slen + 5] != '\0') {
+				talloc_free(fmt);
+				snprintf(output, sizeof(output), "ERROR offset %d 'Too much text' ::%s::", (int) slen, input + slen + 5);
+				continue;
+			}
+
+			len = radius_xlat_struct(output, sizeof(output), request, head, NULL, NULL);
+			if (len < 0) {
+				snprintf(output, sizeof(output), "ERROR expanding xlat: %s", fr_strerror());
+				continue;
+			}
+
+			TALLOC_FREE(fmt); /* also frees 'head' */
+			continue;
+		}
+
+		/*
+		 *	Look for "data".
+		 */
+		if (strncmp(input, "data ", 5) == 0) {
+			if (strcmp(input + 5, output) != 0) {
+				fprintf(stderr, "Mismatch at line %d of %s\n\tgot      : %s\n\texpected : %s\n",
+					lineno, filename, output, input + 5);
+				TALLOC_FREE(request);
+				return false;
+			}
+			continue;
+		}
+
+		fprintf(stderr, "Unknown keyword in %s[%d]\n", filename, lineno);
+		TALLOC_FREE(request);
+		return false;
+	}
+
+	TALLOC_FREE(request);
+	return true;
+}
+
+/*
+ *	Dummy event_list_corral
+ */
+fr_event_list_t *radius_event_list_corral(UNUSED event_corral_t hint) {
+	if (!el) {
+		el = fr_event_list_create(NULL, NULL);
+	}
+
+	return el;
+}
+
+/*
+ *	The main guy.
+ */
+int main(int argc, char *argv[])
+{
+	int rcode = EXIT_SUCCESS;
+	int argval;
+	const char *input_file = NULL;
+	const char *output_file = NULL;
+	const char *filter_file = NULL;
+	FILE *fp;
+	REQUEST *request = NULL;
+	VALUE_PAIR *vp;
+	VALUE_PAIR *filter_vps = NULL;
+	bool xlat_only = false;
+	fr_state_t *state = NULL;
+
+	fr_talloc_fault_setup();
+
+	/*
+	 *	If the server was built with debugging enabled always install
+	 *	the basic fatal signal handlers.
+	 */
+#ifndef NDEBUG
+	if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) {
+		fr_perror("unittest");
+		exit(EXIT_FAILURE);
+	}
+#endif
+
+	rad_debug_lvl = 0;
+	set_radius_dir(NULL, RADIUS_DIR);
+
+	/*
+	 *	Ensure that the configuration is initialized.
+	 */
+	memset(&main_config, 0, sizeof(main_config));
+	main_config.myip.af = AF_UNSPEC;
+	main_config.port = 0;
+	main_config.name = "radiusd";
+
+	/*
+	 *	The tests should have only IPs, not host names.
+	 */
+	fr_hostname_lookups = false;
+
+	/*
+	 *	We always log to stdout.
+	 */
+	fr_log_fp = stdout;
+	default_log.dst = L_DST_STDOUT;
+	default_log.fd = STDOUT_FILENO;
+
+	/*  Process the options.  */
+	while ((argval = getopt(argc, argv, "d:D:f:hi:mMn:o:O:xX")) != EOF) {
+
+		switch (argval) {
+			case 'd':
+				set_radius_dir(NULL, optarg);
+				break;
+
+			case 'D':
+				main_config.dictionary_dir = talloc_typed_strdup(NULL, optarg);
+				break;
+
+			case 'f':
+				filter_file = optarg;
+				break;
+
+			case 'h':
+				usage(0);
+				break;
+
+			case 'i':
+				input_file = optarg;
+				break;
+
+			case 'm':
+				main_config.debug_memory = true;
+				break;
+
+			case 'M':
+				memory_report = true;
+				main_config.debug_memory = true;
+				break;
+
+			case 'n':
+				main_config.name = optarg;
+				break;
+
+			case 'o':
+				output_file = optarg;
+				break;
+
+			case 'O':
+				if (strcmp(optarg, "xlat_only") == 0) {
+					xlat_only = true;
+					break;
+				}
+
+				fprintf(stderr, "Unknown option '%s'\n", optarg);
+				exit(EXIT_FAILURE);
+
+			case 'X':
+				rad_debug_lvl += 2;
+				main_config.log_auth = true;
+				main_config.log_auth_badpass = true;
+				main_config.log_auth_goodpass = true;
+				break;
+
+			case 'x':
+				rad_debug_lvl++;
+				break;
+
+			default:
+				usage(1);
+				break;
+		}
+	}
+
+	if (rad_debug_lvl) version_print();
+	fr_debug_lvl = rad_debug_lvl;
+
+	/*
+	 *	Mismatch between the binary and the libraries it depends on
+	 */
+	if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) {
+		fr_perror("radiusd");
+		exit(EXIT_FAILURE);
+	}
+
+	/*
+	 *  Initialising OpenSSL once, here, is safer than having individual modules do it.
+	 */
+#ifdef HAVE_OPENSSL_CRYPTO_H
+	tls_global_init(false, false);
+#endif
+
+	if (xlat_register("poke", xlat_poke, NULL, NULL) < 0) {
+		rcode = EXIT_FAILURE;
+		goto finish;
+	}
+
+	/*  Read the configuration files, BEFORE doing anything else.  */
+	if (main_config_init() < 0) {
+		rcode = EXIT_FAILURE;
+		goto finish;
+	}
+
+	/*
+	 *  Load the modules
+	 */
+	if (modules_init(main_config.config) < 0) {
+		rcode = EXIT_FAILURE;
+		goto finish;
+	}
+
+	state =fr_state_init(NULL);
+
+	/*
+	 *  Set the panic action (if required)
+	 */
+	{
+		char const *panic_action = NULL;
+
+		panic_action = getenv("PANIC_ACTION");
+		if (!panic_action) panic_action = main_config.panic_action;
+
+		if (panic_action && (fr_fault_setup(panic_action, argv[0]) < 0)) {
+			fr_perror("radiusd");
+			exit(EXIT_FAILURE);
+		}
+	}
+
+	setlinebuf(stdout); /* unbuffered output */
+
+	if (!input_file || (strcmp(input_file, "-") == 0)) {
+		fp = stdin;
+	} else {
+		fp = fopen(input_file, "r");
+		if (!fp) {
+			fprintf(stderr, "Failed reading %s: %s\n",
+				input_file, fr_syserror(errno));
+			goto finish;
+		}
+	}
+
+	/*
+	 *	For simplicity, read xlat's.
+	 */
+	if (xlat_only) {
+		if (!do_xlats(input_file, fp)) rcode = EXIT_FAILURE;
+		if (input_file) fclose(fp);
+		goto finish;
+	}
+
+	/*
+	 *	Grab the VPs from stdin, or from the file.
+	 */
+	request = request_setup(fp);
+	if (!request) {
+		fprintf(stderr, "Failed reading input: %s\n", fr_strerror());
+		rcode = EXIT_FAILURE;
+		goto finish;
+	}
+
+	/*
+	 *	No filter file, OR there's no more input, OR we're
+	 *	reading from a file, and it's different from the
+	 *	filter file.
+	 */
+	if (!filter_file || filedone ||
+	    ((input_file != NULL) && (strcmp(filter_file, input_file) != 0))) {
+		if (output_file) {
+			fclose(fp);
+			fp = NULL;
+		}
+		filedone = false;
+	}
+
+	/*
+	 *	There is a filter file.  If necessary, open it.  If we
+	 *	already are reading it via "input_file", then we don't
+	 *	need to re-open it.
+	 */
+	if (filter_file) {
+		if (!fp) {
+			fp = fopen(filter_file, "r");
+			if (!fp) {
+				fprintf(stderr, "Failed reading %s: %s\n", filter_file, strerror(errno));
+				rcode = EXIT_FAILURE;
+				goto finish;
+			}
+		}
+
+
+		if (fr_pair_list_afrom_file(request, &filter_vps, fp, &filedone) < 0) {
+			fprintf(stderr, "Failed reading attributes from %s: %s\n",
+				filter_file, fr_strerror());
+			rcode = EXIT_FAILURE;
+			goto finish;
+		}
+
+		/*
+		 *	FIXME: loop over input packets.
+		 */
+		fclose(fp);
+	}
+
+	rad_virtual_server(request);
+
+	if (!output_file || (strcmp(output_file, "-") == 0)) {
+		fp = stdout;
+	} else {
+		fp = fopen(output_file, "w");
+		if (!fp) {
+			fprintf(stderr, "Failed writing %s: %s\n",
+				output_file, fr_syserror(errno));
+			exit(EXIT_FAILURE);
+		}
+	}
+
+	print_packet(fp, request->reply);
+
+	if (output_file) fclose(fp);
+
+	/*
+	 *	Update the list with the response type.
+	 */
+	vp = radius_pair_create(request->reply, &request->reply->vps,
+			       PW_RESPONSE_PACKET_TYPE, 0);
+	vp->vp_integer = request->reply->code;
+
+	{
+		VALUE_PAIR const *failed[2];
+
+		if (filter_vps && !fr_pair_validate(failed, filter_vps, request->reply->vps)) {
+			fr_pair_validate_debug(request, failed);
+			fr_perror("Output file %s does not match attributes in filter %s (%s)",
+				  output_file ? output_file : input_file, filter_file, fr_strerror());
+			rcode = EXIT_FAILURE;
+			goto finish;
+		}
+	}
+
+	INFO("Exiting normally");
+
+finish:
+	talloc_free(request);
+
+	/*
+	 *	Detach any modules.
+	 */
+	modules_free();
+
+	xlat_unregister("poke", xlat_poke, NULL);
+
+	xlat_free();		/* modules may have xlat's */
+
+	fr_state_delete(state);
+
+	/*
+	 *	Free the configuration items.
+	 */
+	main_config_free();
+
+	if (el) talloc_free(el);
+
+	if (memory_report) {
+		INFO("Allocated memory at time of report:");
+		fr_log_talloc_report(NULL);
+	}
+
+	return rcode;
+}
+
+
+/*
+ *  Display the syntax for starting this program.
+ */
+static void NEVER_RETURNS usage(int status)
+{
+	FILE *output = status?stderr:stdout;
+
+	fprintf(output, "Usage: %s [options]\n", main_config.name);
+	fprintf(output, "Options:\n");
+	fprintf(output, "  -d raddb_dir  Configuration files are in \"raddb_dir/*\".\n");
+	fprintf(output, "  -D dict_dir   Dictionary files are in \"dict_dir/*\".\n");
+	fprintf(output, "  -f file       Filter reply against attributes in 'file'.\n");
+	fprintf(output, "  -h            Print this help message.\n");
+	fprintf(output, "  -i file       File containing request attributes.\n");
+	fprintf(output, "  -m            On SIGINT or SIGQUIT exit cleanly instead of immediately.\n");
+	fprintf(output, "  -n name       Read raddb/name.conf instead of raddb/radiusd.conf.\n");
+	fprintf(output, "  -X            Turn on full debugging.\n");
+	fprintf(output, "  -x            Turn on additional debugging. (-xx gives more debugging).\n");
+	exit(status);
+}
diff --git a/src/main/unittest.mk b/src/main/unittest.mk
new file mode 100644
index 0000000..edd4f13
--- /dev/null
+++ b/src/main/unittest.mk
@@ -0,0 +1,25 @@
+TARGET	:= unittest
+SOURCES := acct.c auth.c client.c crypt.c files.c \
+		  mainconfig.c modules.c modcall.c \
+		  unittest.c soh.c state.c connection.c \
+		  session.c threads.c version.c  \
+		  realms.c
+
+ifneq ($(OPENSSL_LIBS),)
+SOURCES		+= cb.c tls.c
+endif
+
+SRC_CFLAGS	:= -DHOSTINFO=\"${HOSTINFO}\"
+TGT_INSTALLDIR  :=
+TGT_LDLIBS	:= $(LIBS) $(OPENSSL_LIBS) $(SYSTEMD_LIBS) $(LCRYPT)
+TGT_PREREQS	:= libfreeradius-server.a libfreeradius-radius.a
+
+# Libraries can't depend on libraries (oops), so make the binary
+# depend on the EAP code...
+ifneq "$(filter rlm_eap_%,${ALL_TGTS})" ""
+TGT_PREREQS	+= libfreeradius-eap.a
+endif
+
+ifneq ($(MAKECMDGOALS),scan)
+SRC_CFLAGS	+= -DBUILT_WITH_CPPFLAGS=\"$(CPPFLAGS)\" -DBUILT_WITH_CFLAGS=\"$(CFLAGS)\" -DBUILT_WITH_LDFLAGS=\"$(LDFLAGS)\" -DBUILT_WITH_LIBS=\"$(LIBS)\"
+endif
diff --git a/src/main/util.c b/src/main/util.c
new file mode 100644
index 0000000..b216cc9
--- /dev/null
+++ b/src/main/util.c
@@ -0,0 +1,1732 @@
+/*
+ * util.c	Various utility functions.
+ *
+ * Version:     $Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 2000,2006  The FreeRADIUS server project
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <ctype.h>
+
+#include <sys/stat.h>
+#include <fcntl.h>
+
+/*
+ *	The signal() function in Solaris 2.5.1 sets SA_NODEFER in
+ *	sa_flags, which causes grief if signal() is called in the
+ *	handler before the cause of the signal has been cleared.
+ *	(Infinite recursion).
+ *
+ *	The same problem appears on HPUX, so we avoid it, if we can.
+ *
+ *	Using sigaction() to reset the signal handler fixes the problem,
+ *	so where available, we prefer that solution.
+ */
+
+void (*reset_signal(int signo, void (*func)(int)))(int)
+{
+#ifdef HAVE_SIGACTION
+	struct sigaction act, oact;
+
+	memset(&act, 0, sizeof(act));
+	act.sa_handler = func;
+	sigemptyset(&act.sa_mask);
+	act.sa_flags = 0;
+#ifdef  SA_INTERRUPT		/* SunOS */
+	act.sa_flags |= SA_INTERRUPT;
+#endif
+	if (sigaction(signo, &act, &oact) < 0)
+		return SIG_ERR;
+	return oact.sa_handler;
+#else
+
+	/*
+	 *	re-set by calling the 'signal' function, which
+	 *	may cause infinite recursion and core dumps due to
+	 *	stack growth.
+	 *
+	 *	However, the system is too dumb to implement sigaction(),
+	 *	so we don't have a choice.
+	 */
+	signal(signo, func);
+
+	return NULL;
+#endif
+}
+
+/*
+ *	Per-request data, added by modules...
+ */
+struct request_data_t {
+	request_data_t	*next;
+
+	void		*unique_ptr;
+	int		unique_int;
+	void		*opaque;
+	bool		free_opaque;
+};
+
+/*
+ *	Add opaque data (with a "free" function) to a REQUEST.
+ *
+ *	The unique ptr is meant to be a module configuration,
+ *	and the unique integer allows the caller to have multiple
+ *	opaque data associated with a REQUEST.
+ */
+int request_data_add(REQUEST *request, void *unique_ptr, int unique_int, void *opaque, bool free_opaque)
+{
+	request_data_t *this, **last, *next;
+
+	/*
+	 *	Some simple sanity checks.
+	 */
+	if (!request || !opaque) return -1;
+
+	this = next = NULL;
+	for (last = &(request->data);
+	     *last != NULL;
+	     last = &((*last)->next)) {
+		if (((*last)->unique_ptr == unique_ptr) &&
+		    ((*last)->unique_int == unique_int)) {
+			this = *last;
+			next = this->next;
+
+			/*
+			 *	If caller requires custom behaviour on free
+			 *	they must set a destructor.
+			 */
+			if (this->opaque && this->free_opaque) talloc_free(this->opaque);
+
+			break;	/* replace the existing entry */
+		}
+	}
+
+	/*
+	 *	Only alloc new memory if we're not replacing
+	 *	an existing entry.
+	 */
+	if (!this) this = talloc_zero(request, request_data_t);
+	if (!this) return -1;
+
+	this->next = next;
+	this->unique_ptr = unique_ptr;
+	this->unique_int = unique_int;
+	this->opaque = opaque;
+	this->free_opaque = free_opaque;
+
+	*last = this;
+
+	return 0;
+}
+
+/*
+ *	Get opaque data from a request.
+ */
+void *request_data_get(REQUEST *request, void *unique_ptr, int unique_int)
+{
+	request_data_t **last;
+
+	if (!request) return NULL;
+
+	for (last = &(request->data);
+	     *last != NULL;
+	     last = &((*last)->next)) {
+		if (((*last)->unique_ptr == unique_ptr) &&
+		    ((*last)->unique_int == unique_int)) {
+			request_data_t *this;
+			void *ptr;
+
+			this = *last;
+			ptr = this->opaque;
+
+			/*
+			 *	Remove the entry from the list, and free it.
+			 */
+			*last = this->next;
+			talloc_free(this);
+
+			return ptr; 		/* don't free it, the caller does that */
+		}
+	}
+
+	return NULL;		/* wasn't found, too bad... */
+}
+
+/*
+ *	Get opaque data from a request without removing it.
+ */
+void *request_data_reference(REQUEST *request, void *unique_ptr, int unique_int)
+{
+	request_data_t **last;
+
+	for (last = &(request->data);
+	     *last != NULL;
+	     last = &((*last)->next)) {
+		if (((*last)->unique_ptr == unique_ptr) &&
+		    ((*last)->unique_int == unique_int)) {
+			return (*last)->opaque;
+		}
+	}
+
+	return NULL;		/* wasn't found, too bad... */
+}
+
+/** Create possibly many directories.
+ *
+ * @note that the input directory name is NOT treated as a constant. This is so that
+ *	 if an error is returned, the 'directory' ptr points to the name of the file
+ *	 which caused the error.
+ *
+ * @param dir path to directory to create.
+ * @param mode for new directories.
+ * @param uid to set on new directories, may be -1 to use effective uid.
+ * @param gid to set on new directories, may be -1 to use effective gid.
+ * @return 0 on success, -1 on error. Error available as errno.
+ */
+int rad_mkdir(char *dir, mode_t mode, uid_t uid, gid_t gid)
+{
+	int rcode, fd;
+	char *p;
+
+	/*
+	 *	Try to make the dir.  If it exists, chmod it.
+	 *	If a path doesn't exist, that's OK.  Otherwise
+	 *	return with an error.
+	 *
+	 *	Directories permissions are initially set so
+	 *	that only we should have access. This prevents
+	 *	an attacker removing them and swapping them
+	 *	out for a link to somewhere else.
+	 *	We change them to the correct permissions later.
+	 */
+	rcode = mkdir(dir, 0700);
+	if (rcode < 0) {
+		switch (errno) {
+		case EEXIST:
+			return 0; /* don't change permissions */
+
+		case ENOENT:
+			break;
+
+		default:
+			return rcode;
+		}
+
+		/*
+		 *	A component in the dir path doesn't
+		 *	exist.  Look for the LAST dir name.  Try
+		 *	to create that.  If there's an error, we leave
+		 *	the dir path as the one at which the
+		 *	error occured.
+		 */
+		p = strrchr(dir, FR_DIR_SEP);
+		if (!p || (p == dir)) return -1;
+
+		*p = '\0';
+		rcode = rad_mkdir(dir, mode, uid, gid);
+		if (rcode < 0) return rcode;
+
+		/*
+		 *	Reset the dir path, and try again to
+		 *	make the dir.
+		 */
+		*p = FR_DIR_SEP;
+		rcode = mkdir(dir, 0700);
+		if (rcode < 0) return rcode;
+	} /* else we successfully created the dir */
+
+	/*
+	 *	Set the permissions on the directory we created
+	 *	this should never fail unless there's a race.
+	 */
+	fd = open(dir, O_DIRECTORY);
+	if (fd < 0) return -1;
+
+	rcode = fchmod(fd, mode);
+	if (rcode < 0) {
+		close(fd);
+		return rcode;
+	}
+
+	if ((uid != (uid_t)-1) || (gid != (gid_t)-1)) {
+		rad_suid_up();
+		rcode = fchown(fd, uid, gid);
+		rad_suid_down();
+	}
+	close(fd);
+
+	return rcode;
+}
+
+/** Ensures that a filename cannot walk up the directory structure
+ *
+ * Also sanitizes control chars.
+ *
+ * @param request Current request (may be NULL).
+ * @param out Output buffer.
+ * @param outlen Size of the output buffer.
+ * @param in string to escape.
+ * @param arg Context arguments (unused, should be NULL).
+ */
+size_t rad_filename_make_safe(UNUSED REQUEST *request, char *out, size_t outlen, char const *in, UNUSED void *arg)
+{
+	char const *q = in;
+	char *p = out;
+	size_t left = outlen;
+
+	while (*q) {
+		if (*q != '/') {
+			if (left < 2) break;
+
+			/*
+			 *	Smash control characters and spaces to
+			 *	something simpler.
+			 */
+			if (*q < ' ') {
+				*(p++) = '_';
+				q++;
+				continue;
+			}
+
+			*(p++) = *(q++);
+			left--;
+			continue;
+		}
+
+		/*
+		 *	For now, allow slashes in the expanded
+		 *	filename.  This allows the admin to set
+		 *	attributes which create sub-directories.
+		 *	Unfortunately, it also allows users to send
+		 *	attributes which *may* end up creating
+		 *	sub-directories.
+		 */
+		if (left < 2) break;
+		*(p++) = *(q++);
+
+		/*
+		 *	Get rid of ////../.././///.///..//
+		 */
+	redo:
+		/*
+		 *	Get rid of ////
+		 */
+		if (*q == '/') {
+			q++;
+			goto redo;
+		}
+
+		/*
+		 *	Get rid of /./././
+		 */
+		if ((q[0] == '.') &&
+		    (q[1] == '/')) {
+			q += 2;
+			goto redo;
+		}
+
+		/*
+		 *	Get rid of /../../../
+		 */
+		if ((q[0] == '.') && (q[1] == '.') &&
+		    (q[2] == '/')) {
+			q += 3;
+			goto redo;
+		}
+	}
+	*p = '\0';
+
+	return (p - out);
+}
+
+/** Escapes the raw string such that it should be safe to use as part of a file path
+ *
+ * This function is designed to produce a string that's still readable but portable
+ * across the majority of file systems.
+ *
+ * For security reasons it cannot remove characters from the name, and must not allow
+ * collisions to occur between different strings.
+ *
+ * With that in mind '-' has been chosen as the escape character, and will be double
+ * escaped '-' -> '--' to avoid collisions.
+ *
+ * Escaping should be reversible if the original string needs to be extracted.
+ *
+ * @note function takes additional arguments so that it may be used as an xlat escape
+ *	function but it's fine to call it directly.
+ *
+ * @note OSX/Unix/NTFS/VFAT have a max filename size of 255 bytes.
+ *
+ * @param request Current request (may be NULL).
+ * @param out Output buffer.
+ * @param outlen Size of the output buffer.
+ * @param in string to escape.
+ * @param arg Context arguments (unused, should be NULL).
+ */
+size_t rad_filename_escape(UNUSED REQUEST *request, char *out, size_t outlen, char const *in, UNUSED void *arg)
+{
+	size_t freespace = outlen;
+
+	while (*in != '\0') {
+		size_t utf8_len;
+
+		/*
+		 *	Encode multibyte UTF8 chars
+		 */
+		utf8_len = fr_utf8_char((uint8_t const *) in, -1);
+		if (utf8_len > 1) {
+			if (freespace <= (utf8_len * 3)) break;
+
+			switch (utf8_len) {
+			case 2:
+				snprintf(out, freespace, "-%x-%x", in[0], in[1]);
+				break;
+
+			case 3:
+				snprintf(out, freespace, "-%x-%x-%x", in[0], in[1], in[2]);
+				break;
+
+			case 4:
+				snprintf(out, freespace, "-%x-%x-%x-%x", in[0], in[1], in[2], in[3]);
+				break;
+			}
+
+			freespace -= (utf8_len * 3);
+			out += (utf8_len * 3);
+			in += utf8_len;
+
+			continue;
+		}
+
+		/*
+		 *	Safe chars
+		 */
+		if (((*in >= 'A') && (*in <= 'Z')) ||
+		    ((*in >= 'a') && (*in <= 'z')) ||
+		    ((*in >= '0') && (*in <= '9')) ||
+		    (*in == '_')) {
+		    	if (freespace <= 1) break;
+
+		 	*out++ = *in++;
+		 	freespace--;
+		 	continue;
+		}
+		if (freespace <= 2) break;
+
+		/*
+		 *	Double escape '-' (like \\)
+		 */
+		if (*in == '-') {
+			*out++ = '-';
+			*out++ = '-';
+
+			freespace -= 2;
+			in++;
+			continue;
+		}
+
+		/*
+		 *	Unsafe chars
+		 */
+		*out++ = '-';
+		fr_bin2hex(out, (uint8_t const *)in++, 1);
+		out += 2;
+		freespace -= 3;
+	}
+	*out = '\0';
+
+	return outlen - freespace;
+}
+
+/** Converts data stored in a file name back to its original form
+ *
+ * @param out Where to write the unescaped string (may be the same as in).
+ * @param outlen Length of the output buffer.
+ * @param in Input filename.
+ * @param inlen Length of input.
+ * @return number of bytes written to output buffer, or offset where parse error
+ *	occurred on failure.
+ */
+ssize_t rad_filename_unescape(char *out, size_t outlen, char const *in, size_t inlen)
+{
+	char const *p, *end = in + inlen;
+	size_t freespace = outlen;
+
+	for (p = in; p < end; p++) {
+		if (freespace <= 1) break;
+
+		if (((*p >= 'A') && (*p <= 'Z')) ||
+		    ((*p >= 'a') && (*p <= 'z')) ||
+		    ((*p >= '0') && (*p <= '9')) ||
+		    (*p == '_')) {
+		 	*out++ = *p;
+		 	freespace--;
+		 	continue;
+		}
+
+		if (p[0] == '-') {
+			/*
+			 *	End of input, '-' needs at least one extra char after
+			 *	it to be valid.
+			 */
+			if ((end - p) < 2) return in - p;
+			if (p[1] == '-') {
+				p++;
+				*out++ = '-';
+				freespace--;
+				continue;
+			}
+
+			/*
+			 *	End of input, '-' must be followed by <hex><hex>
+			 *	but there aren't enough chars left
+			 */
+			if ((end - p) < 3) return in - p;
+
+			/*
+			 *	If hex2bin returns 0 the next two chars weren't hexits.
+			 */
+			if (fr_hex2bin((uint8_t *) out, 1, in, 1) == 0) return in - (p + 1);
+			in += 2;
+			out++;
+			freespace--;
+		}
+
+		return in - p; /* offset we found the bad char at */
+	}
+	*out = '\0';
+
+	return outlen - freespace;	/* how many bytes were written */
+}
+
+/*
+ *	Allocate memory, or exit.
+ *
+ *	This call ALWAYS succeeds!
+ */
+void *rad_malloc(size_t size)
+{
+	void *ptr = malloc(size);
+
+	if (ptr == NULL) {
+		ERROR("no memory");
+		fr_exit(1);
+	}
+
+	return ptr;
+}
+
+
+void rad_const_free(void const *ptr)
+{
+	void *tmp;
+	if (!ptr) return;
+
+	memcpy(&tmp, &ptr, sizeof(tmp));
+	talloc_free(tmp);
+}
+
+
+/*
+ *	Logs an error message and aborts the program
+ *
+ */
+
+void NEVER_RETURNS rad_assert_fail(char const *file, unsigned int line, char const *expr)
+{
+	ERROR("ASSERT FAILED %s[%u]: %s", file, line, expr);
+	fr_fault(SIGABRT);
+	fr_exit_now(1);
+}
+
+/*
+ *	Free a REQUEST struct.
+ */
+static int _request_free(REQUEST *request)
+{
+	rad_assert(!request->in_request_hash);
+#ifdef WITH_PROXY
+	rad_assert(!request->in_proxy_hash);
+#endif
+	rad_assert(!request->ev);
+
+#ifdef WITH_COA
+	rad_assert(request->coa == NULL);
+#endif
+
+#ifndef NDEBUG
+	request->magic = 0x01020304;	/* set the request to be nonsense */
+#endif
+	request->client = NULL;
+#ifdef WITH_PROXY
+	request->home_server = NULL;
+#endif
+
+	/*
+	 *	This is parented separately.
+	 */
+	if (request->state_ctx) {
+		talloc_free(request->state_ctx);
+	}
+
+	return 0;
+}
+
+/*
+ *	Create a new REQUEST data structure.
+ */
+REQUEST *request_alloc(TALLOC_CTX *ctx)
+{
+	REQUEST *request;
+
+	request = talloc_zero(ctx, REQUEST);
+	if (!request) return NULL;
+	talloc_set_destructor(request, _request_free);
+#ifndef NDEBUG
+	request->magic = REQUEST_MAGIC;
+#endif
+#ifdef WITH_PROXY
+	request->proxy = NULL;
+#endif
+	request->reply = NULL;
+#ifdef WITH_PROXY
+	request->proxy_reply = NULL;
+#endif
+	request->config = NULL;
+	request->username = NULL;
+	request->password = NULL;
+	request->timestamp = time(NULL);
+	request->log.lvl = rad_debug_lvl; /* Default to global debug level */
+
+	request->module = "";
+	request->component = "<core>";
+	request->log.func = vradlog_request;
+
+	request->state_ctx = talloc_init("session-state");
+
+	return request;
+}
+
+
+/*
+ *	Create a new REQUEST, based on an old one.
+ *
+ *	This function allows modules to inject fake requests
+ *	into the server, for tunneled protocols like TTLS & PEAP.
+ */
+REQUEST *request_alloc_fake(REQUEST *request)
+{
+	REQUEST *fake;
+
+	fake = request_alloc(request);
+	if (!fake) return NULL;
+
+	fake->number = request->number;
+#ifdef HAVE_PTHREAD_H
+	fake->child_pid = request->child_pid;
+#endif
+	fake->parent = request;
+	fake->root = request->root;
+	fake->client = request->client;
+
+	/*
+	 *	For new server support.
+	 *
+	 *	FIXME: Key instead off of a "virtual server" data structure.
+	 *
+	 *	FIXME: Permit different servers for inner && outer sessions?
+	 */
+	fake->server = request->server;
+
+	fake->packet = rad_alloc(fake, true);
+	if (!fake->packet) {
+		talloc_free(fake);
+		return NULL;
+	}
+
+	fake->reply = rad_alloc(fake, false);
+	if (!fake->reply) {
+		talloc_free(fake);
+		return NULL;
+	}
+
+	fake->master_state = REQUEST_ACTIVE;
+	fake->child_state = REQUEST_RUNNING;
+
+	/*
+	 *	Fill in the fake request.
+	 */
+	fake->packet->sockfd = -1;
+	fake->packet->src_ipaddr = request->packet->src_ipaddr;
+	fake->packet->src_port = request->packet->src_port;
+	fake->packet->dst_ipaddr = request->packet->dst_ipaddr;
+	fake->packet->dst_port = 0;
+
+	/*
+	 *	This isn't STRICTLY required, as the fake request MUST NEVER
+	 *	be put into the request list.  However, it's still reasonable
+	 *	practice.
+	 */
+	fake->packet->id = fake->number & 0xff;
+	fake->packet->code = request->packet->code;
+	fake->timestamp = request->timestamp;
+	fake->packet->timestamp = request->packet->timestamp;
+
+	/*
+	 *	Required for new identity support
+	 */
+	fake->listener = request->listener;
+
+	/*
+	 *	Fill in the fake reply, based on the fake request.
+	 */
+	fake->reply->sockfd = fake->packet->sockfd;
+	fake->reply->src_ipaddr = fake->packet->dst_ipaddr;
+	fake->reply->src_port = fake->packet->dst_port;
+	fake->reply->dst_ipaddr = fake->packet->src_ipaddr;
+	fake->reply->dst_port = fake->packet->src_port;
+	fake->reply->id = fake->packet->id;
+	fake->reply->code = 0; /* UNKNOWN code */
+
+	/*
+	 *	Copy debug information.
+	 */
+	memcpy(&(fake->log), &(request->log), sizeof(fake->log));
+	fake->log.indent = 0;	/* Apart from the indent which we reset */
+
+	return fake;
+}
+
+#ifdef WITH_COA
+static int null_handler(UNUSED REQUEST *request)
+{
+	return 0;
+}
+
+REQUEST *request_alloc_coa(REQUEST *request)
+{
+	if (!request || request->coa) return NULL;
+
+	/*
+	 *	Originate CoA requests only when necessary.
+	 */
+	if ((request->packet->code != PW_CODE_ACCESS_REQUEST) &&
+	    (request->packet->code != PW_CODE_ACCOUNTING_REQUEST)) return NULL;
+
+	request->coa = request_alloc_fake(request);
+	if (!request->coa) return NULL;
+
+	request->coa->handle = null_handler;
+	request->coa->options = RAD_REQUEST_OPTION_COA;	/* is a CoA packet */
+	request->coa->packet->code = 0; /* unknown, as of yet */
+	request->coa->child_state = REQUEST_RUNNING;
+	request->coa->proxy = rad_alloc(request->coa, false);
+	if (!request->coa->proxy) {
+		TALLOC_FREE(request->coa);
+		return NULL;
+	}
+
+	return request->coa;
+}
+#endif
+
+/*
+ *	Copy a quoted string.
+ */
+int rad_copy_string(char *to, char const *from)
+{
+	int length = 0;
+	char quote = *from;
+
+	do {
+		if (*from == '\\') {
+			*(to++) = *(from++);
+			length++;
+		}
+		*(to++) = *(from++);
+		length++;
+	} while (*from && (*from != quote));
+
+	if (*from != quote) return -1; /* not properly quoted */
+
+	*(to++) = quote;
+	length++;
+	*to = '\0';
+
+	return length;
+}
+
+/*
+ *	Copy a quoted string but without the quotes. The length
+ *	returned is the number of chars written; the number of
+ *	characters consumed is 2 more than this.
+ */
+int rad_copy_string_bare(char *to, char const *from)
+{
+	int length = 0;
+	char quote = *from;
+
+	from++;
+	while (*from && (*from != quote)) {
+		if (*from == '\\') {
+			*(to++) = *(from++);
+			length++;
+		}
+		*(to++) = *(from++);
+		length++;
+	}
+
+	if (*from != quote) return -1; /* not properly quoted */
+
+	*to = '\0';
+
+	return length;
+}
+
+
+/*
+ *	Copy a %{} string.
+ */
+int rad_copy_variable(char *to, char const *from)
+{
+	int length = 0;
+	int sublen;
+
+	*(to++) = *(from++);
+	length++;
+
+	while (*from) {
+		switch (*from) {
+		case '"':
+		case '\'':
+			sublen = rad_copy_string(to, from);
+			if (sublen < 0) return sublen;
+			from += sublen;
+			to += sublen;
+			length += sublen;
+			break;
+
+		case '}':	/* end of variable expansion */
+			*(to++) = *(from++);
+			*to = '\0';
+			length++;
+			return length; /* proper end of variable */
+
+		case '\\':
+			*(to++) = *(from++);
+			*(to++) = *(from++);
+			length += 2;
+			break;
+
+		case '%':	/* start of variable expansion */
+			if (from[1] == '{') {
+				*(to++) = *(from++);
+				length++;
+
+				sublen = rad_copy_variable(to, from);
+				if (sublen < 0) return sublen;
+				from += sublen;
+				to += sublen;
+				length += sublen;
+				break;
+			} /* else FIXME: catch %%{ ?*/
+
+			/* FALL-THROUGH */
+		default:
+			*(to++) = *(from++);
+			length++;
+			break;
+		}
+	} /* loop over the input string */
+
+	/*
+	 *	We ended the string before a trailing '}'
+	 */
+
+	return -1;
+}
+
+#ifndef USEC
+#define USEC 1000000
+#endif
+
+uint32_t rad_pps(uint32_t *past, uint32_t *present, time_t *then, struct timeval *now)
+{
+	uint32_t pps;
+
+	if (*then != now->tv_sec) {
+		*then = now->tv_sec;
+		*past = *present;
+		*present = 0;
+	}
+
+	/*
+	 *	Bootstrap PPS by looking at a percentage of
+	 *	the previous PPS.  This lets us take a moving
+	 *	count, without doing a moving average.  If
+	 *	we're a fraction "f" (0..1) into the current
+	 *	second, we can get a good guess for PPS by
+	 *	doing:
+	 *
+	 *	PPS = pps_now + pps_old * (1 - f)
+	 *
+	 *	It's an instantaneous measurement, rather than
+	 *	a moving average.  This will hopefully let it
+	 *	respond better to sudden spikes.
+	 *
+	 *	Doing the calculations by thousands allows us
+	 *	to not overflow 2^32, AND to not underflow
+	 *	when we divide by USEC.
+	 */
+	pps = USEC - now->tv_usec; /* useconds left in previous second */
+	pps /= 1000;		   /* scale to milliseconds */
+	pps *= *past;		   /* multiply by past count to get fraction */
+	pps /= 1000;		   /* scale to usec again */
+	pps += *present;	   /* add in current count */
+
+	return pps;
+}
+
+/** Split string into words and expand each one
+ *
+ * @param request Current request.
+ * @param cmd string to split.
+ * @param max_argc the maximum number of arguments to split into.
+ * @param argv Where to write the pointers into argv_buf.
+ * @param can_fail If false, stop processing if any of the xlat expansions fail.
+ * @param argv_buflen size of argv_buf.
+ * @param argv_buf temporary buffer we used to mangle/expand cmd.
+ *	Pointers to offsets of this buffer will be written to argv.
+ * @return argc or -1 on failure.
+ */
+
+int rad_expand_xlat(REQUEST *request, char const *cmd,
+		    int max_argc, char const *argv[], bool can_fail,
+		    size_t argv_buflen, char *argv_buf)
+{
+	char const *from;
+	char *to;
+	int argc = -1;
+	int i;
+	int left;
+
+	if (strlen(cmd) > (argv_buflen - 1)) {
+		ERROR("rad_expand_xlat: Command line is too long");
+		return -1;
+	}
+
+	/*
+	 *	Check for bad escapes.
+	 */
+	if (cmd[strlen(cmd) - 1] == '\\') {
+		ERROR("rad_expand_xlat: Command line has final backslash, without a following character");
+		return -1;
+	}
+
+	strlcpy(argv_buf, cmd, argv_buflen);
+
+	/*
+	 *	Split the string into argv's BEFORE doing radius_xlat...
+	 */
+	from = cmd;
+	to = argv_buf;
+	argc = 0;
+	while (*from) {
+		int length;
+
+		/*
+		 *	Skip spaces.
+		 */
+		if ((*from == ' ') || (*from == '\t')) {
+			from++;
+			continue;
+		}
+
+		argv[argc] = to;
+		argc++;
+
+		if (argc >= (max_argc - 1)) break;
+
+		/*
+		 *	Copy the argv over to our buffer.
+		 */
+		while (*from && (*from != ' ') && (*from != '\t')) {
+			if (to >= argv_buf + argv_buflen - 1) {
+				ERROR("rad_expand_xlat: Ran out of space in command line");
+				return -1;
+			}
+
+			switch (*from) {
+			case '"':
+			case '\'':
+				length = rad_copy_string_bare(to, from);
+				if (length < 0) {
+					ERROR("rad_expand_xlat: Invalid string passed as argument");
+					return -1;
+				}
+				from += length+2;
+				to += length;
+				break;
+
+			case '%':
+				if (from[1] == '{') {
+					*(to++) = *(from++);
+
+					length = rad_copy_variable(to, from);
+					if (length < 0) {
+						ERROR("rad_expand_xlat: Invalid variable expansion passed as argument");
+						return -1;
+					}
+					from += length;
+					to += length;
+				} else { /* FIXME: catch %%{ ? */
+					*(to++) = *(from++);
+				}
+				break;
+
+			case '\\':
+				if (from[1] == ' ') from++;
+				/* FALL-THROUGH */
+
+			default:
+				*(to++) = *(from++);
+			}
+		} /* end of string, or found a space */
+
+		*(to++) = '\0';	/* terminate the string */
+	}
+
+	/*
+	 *	We have to have SOMETHING, at least.
+	 */
+	if (argc <= 0) {
+		ERROR("rad_expand_xlat: Empty command line");
+		return -1;
+	}
+
+	/*
+	 *	Expand each string, as appropriate.
+	 */
+	left = argv_buf + argv_buflen - to;
+	for (i = 0; i < argc; i++) {
+		int sublen;
+
+		/*
+		 *	Don't touch argv's which won't be translated.
+		 */
+		if (strchr(argv[i], '%') == NULL) continue;
+
+		if (!request) continue;
+
+		sublen = radius_xlat(to, left - 1, request, argv[i], NULL, NULL);
+		if (sublen <= 0) {
+			if (can_fail) {
+				/*
+				 *	Fail to be backwards compatible.
+				 *
+				 *	It's yucky, but it won't break anything,
+				 *	and it won't cause security problems.
+				 */
+				sublen = 0;
+			} else {
+				ERROR("rad_expand_xlat: xlat failed");
+				return -1;
+			}
+		}
+
+		argv[i] = to;
+		to += sublen;
+		*(to++) = '\0';
+		left -= sublen;
+		left--;
+
+		if (left <= 0) {
+			ERROR("rad_expand_xlat: Ran out of space while expanding arguments");
+			return -1;
+		}
+	}
+	argv[argc] = NULL;
+
+	return argc;
+}
+
+#ifndef NDEBUG
+/*
+ *	Verify a packet.
+ */
+static void verify_packet(char const *file, int line, REQUEST *request, RADIUS_PACKET *packet, char const *name)
+{
+	TALLOC_CTX *parent;
+
+	if (!packet) {
+		fprintf(stderr, "CONSISTENCY CHECK FAILED %s[%i]: RADIUS_PACKET %s pointer was NULL", file, line, name);
+		fr_assert(0);
+		fr_exit_now(0);
+	}
+
+	parent = talloc_parent(packet);
+	if (parent != request) {
+		ERROR("CONSISTENCY CHECK FAILED %s[%i]: Expected RADIUS_PACKET %s to be parented by %p (%s), "
+		      "but parented by %p (%s)", file, line, name, request, talloc_get_name(request),
+		      parent, parent ? talloc_get_name(parent) : "NULL");
+
+		fr_log_talloc_report(packet);
+		if (parent) fr_log_talloc_report(parent);
+
+		rad_assert(0);
+	}
+
+	VERIFY_PACKET(packet);
+
+	if (!packet->vps) return;
+
+#ifdef WITH_VERIFY_PTR
+	fr_pair_list_verify(file, line, packet, packet->vps, name);
+#endif
+}
+/*
+ *	Catch horrible talloc errors.
+ */
+void verify_request(char const *file, int line, REQUEST *request)
+{
+	if (!request) {
+		fprintf(stderr, "CONSISTENCY CHECK FAILED %s[%i]: REQUEST pointer was NULL", file, line);
+		fr_assert(0);
+		fr_exit_now(0);
+	}
+
+	(void) talloc_get_type_abort(request, REQUEST);
+
+#ifdef WITH_VERIFY_PTR
+	fr_pair_list_verify(file, line, request, request->config, "config");
+	fr_pair_list_verify(file, line, request->state_ctx, request->state, "state");
+#endif
+
+	if (request->packet) verify_packet(file, line, request, request->packet, "request");
+	if (request->reply) verify_packet(file, line, request, request->reply, "reply");
+#ifdef WITH_PROXY
+	if (request->proxy) verify_packet(file, line, request, request->proxy, "proxy-request");
+	if (request->proxy_reply) verify_packet(file, line, request, request->proxy_reply, "proxy-reply");
+#endif
+
+#ifdef WITH_COA
+	if (request->coa) {
+		void *parent;
+
+		(void) talloc_get_type_abort(request->coa, REQUEST);
+		parent = talloc_parent(request->coa);
+
+		rad_assert(parent == request);
+
+		verify_request(file, line, request->coa);
+	}
+#endif
+}
+#endif
+
+/** Convert mode_t into humanly readable permissions flags
+ *
+ * @author Jonathan Leffler.
+ *
+ * @param mode to convert.
+ * @param out Where to write the string to, must be exactly 10 bytes long.
+ */
+void rad_mode_to_str(char out[10], mode_t mode)
+{
+    static char const *rwx[] = {"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"};
+
+    strcpy(&out[0], rwx[(mode >> 6) & 0x07]);
+    strcpy(&out[3], rwx[(mode >> 3) & 0x07]);
+    strcpy(&out[6], rwx[(mode & 7)]);
+    if (mode & S_ISUID) out[2] = (mode & 0100) ? 's' : 'S';
+    if (mode & S_ISGID) out[5] = (mode & 0010) ? 's' : 'l';
+    if (mode & S_ISVTX) out[8] = (mode & 0100) ? 't' : 'T';
+    out[9] = '\0';
+}
+
+void rad_mode_to_oct(char out[5], mode_t mode)
+{
+	out[0] = '0' + ((mode >> 9) & 0x07);
+	out[1] = '0' + ((mode >> 6) & 0x07);
+	out[2] = '0' + ((mode >> 3) & 0x07);
+	out[3] = '0' + (mode & 0x07);
+	out[4] = '\0';
+}
+
+/** Resolve a uid to a passwd entry
+ *
+ * Resolves a uid to a passwd entry. The memory to hold the
+ * passwd entry is talloced under ctx, and must be freed when no
+ * longer required.
+ *
+ * @param ctx to allocate passwd entry in.
+ * @param out Where to write pointer to entry.
+ * @param uid to resolve.
+ * @return 0 on success, -1 on error.
+ */
+int rad_getpwuid(TALLOC_CTX *ctx, struct passwd **out, uid_t uid)
+{
+	static size_t len;
+	uint8_t *buff;
+	int ret;
+
+	*out = NULL;
+
+	/*
+	 *	We assume this won't change between calls,
+	 *	and that the value is the same, so races don't
+	 *	matter.
+	 */
+	if (len == 0) {
+#ifdef _SC_GETPW_R_SIZE_MAX
+		long int sc_len;
+
+		sc_len = sysconf(_SC_GETPW_R_SIZE_MAX);
+		if (sc_len <= 0) sc_len = 1024;
+		len = (size_t)sc_len;
+#else
+		len = 1024;
+#endif
+	}
+
+	buff = talloc_array(ctx, uint8_t, sizeof(struct passwd) + len);
+	if (!buff) return -1;
+
+	/*
+	 *	In some cases we may need to dynamically
+	 *	grow the string buffer.
+	 */
+	while ((ret = getpwuid_r(uid, (struct passwd *)buff, (char *)(buff + sizeof(struct passwd)),
+				 talloc_array_length(buff) - sizeof(struct passwd), out)) == ERANGE) {
+		buff = talloc_realloc_size(ctx, buff, talloc_array_length(buff) * 2);
+		if (!buff) {
+			talloc_free(buff);
+			return -1;
+		}
+	}
+
+	if ((ret != 0) || !*out) {
+		fr_strerror_printf("Failed resolving UID: %s", fr_syserror(ret));
+		talloc_free(buff);
+		errno = ret;
+		return -1;
+	}
+
+	talloc_set_type(buff, struct passwd);
+	*out = (struct passwd *)buff;
+
+	return 0;
+}
+
+/** Resolve a username to a passwd entry
+ *
+ * Resolves a username to a passwd entry. The memory to hold the
+ * passwd entry is talloced under ctx, and must be freed when no
+ * longer required.
+ *
+ * @param ctx to allocate passwd entry in.
+ * @param out Where to write pointer to entry.
+ * @param name to resolve.
+ * @return 0 on success, -1 on error.
+ */
+int rad_getpwnam(TALLOC_CTX *ctx, struct passwd **out, char const *name)
+{
+	static size_t len;
+	uint8_t *buff;
+	int ret;
+
+	*out = NULL;
+
+	/*
+	 *	We assume this won't change between calls,
+	 *	and that the value is the same, so races don't
+	 *	matter.
+	 */
+	if (len == 0) {
+#ifdef _SC_GETPW_R_SIZE_MAX
+		long int sc_len;
+
+		sc_len = sysconf(_SC_GETPW_R_SIZE_MAX);
+		if (sc_len <= 0) sc_len = 1024;
+		len = (size_t)sc_len;
+#else
+		sc_len = 1024;
+#endif
+	}
+
+	buff = talloc_array(ctx, uint8_t, sizeof(struct passwd) + len);
+	if (!buff) return -1;
+
+	/*
+	 *	In some cases we may need to dynamically
+	 *	grow the string buffer.
+	 */
+	while ((ret = getpwnam_r(name, (struct passwd *)buff, (char *)(buff + sizeof(struct passwd)),
+				 talloc_array_length(buff) - sizeof(struct passwd), out)) == ERANGE) {
+		buff = talloc_realloc_size(ctx, buff, talloc_array_length(buff) * 2);
+		if (!buff) {
+			talloc_free(buff);
+			return -1;
+		}
+	}
+
+	if ((ret != 0) || !*out) {
+		fr_strerror_printf("Failed resolving UID: %s", fr_syserror(ret));
+		talloc_free(buff);
+		errno = ret;
+		return -1;
+	}
+
+	talloc_set_type(buff, struct passwd);
+	*out = (struct passwd *)buff;
+
+	return 0;
+}
+
+/** Resolve a gid to a group database entry
+ *
+ * Resolves a gid to a group database entry. The memory to hold the
+ * group entry is talloced under ctx, and must be freed when no
+ * longer required.
+ *
+ * @param ctx to allocate passwd entry in.
+ * @param out Where to write pointer to entry.
+ * @param gid to resolve.
+ * @return 0 on success, -1 on error.
+ */
+int rad_getgrgid(TALLOC_CTX *ctx, struct group **out, gid_t gid)
+{
+	static size_t len;
+	uint8_t *buff;
+	int ret;
+
+	*out = NULL;
+
+	/*
+	 *	We assume this won't change between calls,
+	 *	and that the value is the same, so races don't
+	 *	matter.
+	 */
+	if (len == 0) {
+#ifdef _SC_GETGR_R_SIZE_MAX
+		long int sc_len;
+
+		sc_len = sysconf(_SC_GETGR_R_SIZE_MAX);
+		if (sc_len <= 0) sc_len = 1024;
+		len = (size_t)sc_len;
+#else
+		sc_len = 1024;
+#endif
+	}
+
+	buff = talloc_array(ctx, uint8_t, sizeof(struct group) + len);
+	if (!buff) return -1;
+
+	/*
+	 *	In some cases we may need to dynamically
+	 *	grow the string buffer.
+	 */
+	while ((ret = getgrgid_r(gid, (struct group *)buff, (char *)(buff + sizeof(struct group)),
+				 talloc_array_length(buff) - sizeof(struct group), out)) == ERANGE) {
+		buff = talloc_realloc_size(ctx, buff, talloc_array_length(buff) * 2);
+		if (!buff) {
+			talloc_free(buff);
+			return -1;
+		}
+	}
+
+	if ((ret != 0) || !*out) {
+		fr_strerror_printf("Failed resolving GID: %s", fr_syserror(ret));
+		talloc_free(buff);
+		errno = ret;
+		return -1;
+	}
+
+	talloc_set_type(buff, struct group);
+	*out = (struct group *)buff;
+
+	return 0;
+}
+
+/** Resolve a group name to a group database entry
+ *
+ * Resolves a group name to a group database entry.
+ * The memory to hold the group entry is talloced under ctx,
+ * and must be freed when no longer required.
+ *
+ * @param ctx to allocate passwd entry in.
+ * @param out Where to write pointer to entry.
+ * @param name to resolve.
+ * @return 0 on success, -1 on error.
+ */
+int rad_getgrnam(TALLOC_CTX *ctx, struct group **out, char const *name)
+{
+	static size_t len;
+	uint8_t *buff;
+	int ret;
+
+	*out = NULL;
+
+	/*
+	 *	We assume this won't change between calls,
+	 *	and that the value is the same, so races don't
+	 *	matter.
+	 */
+	if (len == 0) {
+#ifdef _SC_GETGR_R_SIZE_MAX
+		long int sc_len;
+
+		sc_len = sysconf(_SC_GETGR_R_SIZE_MAX);
+		if (sc_len <= 0) sc_len = 1024;
+		len = (size_t)sc_len;
+#else
+		len = 1024;
+#endif
+	}
+
+	buff = talloc_array(ctx, uint8_t, sizeof(struct group) + len);
+	if (!buff) return -1;
+
+	/*
+	 *	In some cases we may need to dynamically
+	 *	grow the string buffer.
+	 */
+	while ((ret = getgrnam_r(name, (struct group *)buff, (char *)(buff + sizeof(struct group)),
+				 talloc_array_length(buff) - sizeof(struct group), out)) == ERANGE) {
+		buff = talloc_realloc_size(ctx, buff, talloc_array_length(buff) * 2);
+		if (!buff) {
+			talloc_free(buff);
+			return -1;
+		}
+	}
+
+	if ((ret != 0) || !*out) {
+		fr_strerror_printf("Failed resolving GID: %s", fr_syserror(ret));
+		talloc_free(buff);
+		errno = ret;
+		return -1;
+	}
+
+	talloc_set_type(buff, struct group);
+	*out = (struct group *)buff;
+
+	return 0;
+}
+
+/** Resolve a group name to a GID
+ *
+ * @param ctx TALLOC_CTX for temporary allocations.
+ * @param name of group.
+ * @param out where to write gid.
+ * @return 0 on success, -1 on error;
+ */
+int rad_getgid(TALLOC_CTX *ctx, gid_t *out, char const *name)
+{
+	int ret;
+	struct group *result;
+
+	ret = rad_getgrnam(ctx, &result, name);
+	if (ret < 0) return -1;
+
+	*out = result->gr_gid;
+	talloc_free(result);
+	return 0;
+}
+
+/** Print uid to a string
+ *
+ * @note The reason for taking a fixed buffer is pure laziness.
+ *	 It means the caller doesn't have to free the string.
+ *
+ * @note Will always \0 terminate the buffer, even on error.
+ *
+ * @param ctx TALLOC_CTX for temporary allocations.
+ * @param out Where to write the uid string.
+ * @param outlen length of output buffer.
+ * @param uid to resolve.
+ * @return 0 on success, -1 on failure.
+ */
+int rad_prints_uid(TALLOC_CTX *ctx, char *out, size_t outlen, uid_t uid)
+{
+	struct passwd *result;
+
+	rad_assert(outlen > 0);
+
+	*out = '\0';
+
+	if (rad_getpwuid(ctx, &result, uid) < 0) return -1;
+	strlcpy(out, result->pw_name, outlen);
+	talloc_free(result);
+
+	return 0;
+}
+
+/** Print gid to a string
+ *
+ * @note The reason for taking a fixed buffer is pure laziness.
+ *	 It means the caller doesn't have to free the string.
+ *
+ * @note Will always \0 terminate the buffer, even on error.
+ *
+ * @param ctx TALLOC_CTX for temporary allocations.
+ * @param out Where to write the uid string.
+ * @param outlen length of output buffer.
+ * @param gid to resolve.
+ * @return 0 on success, -1 on failure.
+ */
+int rad_prints_gid(TALLOC_CTX *ctx, char *out, size_t outlen, gid_t gid)
+{
+	struct group *result;
+
+	rad_assert(outlen > 0);
+
+	*out = '\0';
+
+	if (rad_getgrgid(ctx, &result, gid) < 0) return -1;
+	strlcpy(out, result->gr_name, outlen);
+	talloc_free(result);
+
+	return 0;
+}
+
+#ifdef HAVE_SETUID
+static bool doing_setuid = false;
+static uid_t suid_down_uid = (uid_t)-1;
+
+/** Set the uid and gid used when dropping privileges
+ *
+ * @note if this function hasn't been called, rad_suid_down will have no effect.
+ *
+ * @param uid to drop down to.
+ */
+void rad_suid_set_down_uid(uid_t uid)
+{
+	suid_down_uid = uid;
+	doing_setuid = true;
+}
+
+#  if defined(HAVE_SETRESUID) && defined (HAVE_GETRESUID)
+void rad_suid_up(void)
+{
+	uid_t ruid, euid, suid;
+
+	if (getresuid(&ruid, &euid, &suid) < 0) {
+		ERROR("Failed getting saved UID's");
+		fr_exit_now(1);
+	}
+
+	if (setresuid(-1, suid, -1) < 0) {
+		ERROR("Failed switching to privileged user");
+		fr_exit_now(1);
+	}
+
+	if (geteuid() != suid) {
+		ERROR("Switched to unknown UID");
+		fr_exit_now(1);
+	}
+}
+
+void rad_suid_down(void)
+{
+	if (!doing_setuid) return;
+
+	if (setresuid(-1, suid_down_uid, geteuid()) < 0) {
+		struct passwd *passwd;
+		char const *name;
+
+		name = (rad_getpwuid(NULL, &passwd, suid_down_uid) < 0) ? "unknown" : passwd->pw_name;
+		ERROR("Failed switching to uid %s: %s", name, fr_syserror(errno));
+		talloc_free(passwd);
+		fr_exit_now(1);
+	}
+
+	if (geteuid() != suid_down_uid) {
+		ERROR("Failed switching uid: UID is incorrect");
+		fr_exit_now(1);
+	}
+
+	fr_reset_dumpable();
+}
+
+void rad_suid_down_permanent(void)
+{
+	if (!doing_setuid) return;
+
+	if (setresuid(suid_down_uid, suid_down_uid, suid_down_uid) < 0) {
+		struct passwd *passwd;
+		char const *name;
+
+		name = (rad_getpwuid(NULL, &passwd, suid_down_uid) < 0) ? "unknown" : passwd->pw_name;
+		ERROR("Failed in permanent switch to uid %s: %s", name, fr_syserror(errno));
+		talloc_free(passwd);
+		fr_exit_now(1);
+	}
+
+	if (geteuid() != suid_down_uid) {
+		ERROR("Switched to unknown uid");
+		fr_exit_now(1);
+	}
+
+	fr_reset_dumpable();
+}
+#  else
+/*
+ *	Much less secure...
+ */
+void rad_suid_up(void)
+{
+	if (!doing_setuid) return;
+
+	if (seteuid(0) < 0) {
+		ERROR("Failed switching up to euid 0: %s", fr_syserror(errno));
+		fr_exit_now(1);
+	}
+
+}
+
+void rad_suid_down(void)
+{
+	if (!doing_setuid) return;
+
+	if (geteuid() == suid_down_uid) return;
+
+	if (seteuid(suid_down_uid) < 0) {
+		struct passwd *passwd;
+		char const *name;
+
+		name = (rad_getpwuid(NULL, &passwd, suid_down_uid) < 0) ? "unknown": passwd->pw_name;
+		ERROR("Failed switching to euid %s: %s", name, fr_syserror(errno));
+		talloc_free(passwd);
+		fr_exit_now(1);
+	}
+
+	fr_reset_dumpable();
+}
+
+void rad_suid_down_permanent(void)
+{
+	if (!doing_setuid) return;
+
+	/*
+	 *	Already done.  Don't do anything else.
+	 */
+	if (getuid() == suid_down_uid) return;
+
+	/*
+	 *	We're root, but running as a normal user.  Fix that,
+	 *	so we can call setuid().
+	 */
+	if (geteuid() == suid_down_uid) {
+		rad_suid_up();
+	}
+
+	if (setuid(suid_down_uid) < 0) {
+		struct passwd *passwd;
+		char const *name;
+
+		name = (rad_getpwuid(NULL, &passwd, suid_down_uid) < 0) ? "unknown": passwd->pw_name;
+		ERROR("Failed switching permanently to uid %s: %s", name, fr_syserror(errno));
+		talloc_free(passwd);
+		fr_exit_now(1);
+	}
+
+	fr_reset_dumpable();
+}
+#  endif /* HAVE_SETRESUID && HAVE_GETRESUID */
+#else  /* HAVE_SETUID */
+void rad_suid_set_down_uid(uid_t uid)
+{
+}
+void rad_suid_up(void)
+{
+}
+void rad_suid_down(void)
+{
+	fr_reset_dumpable();
+}
+void rad_suid_down_permanent(void)
+{
+	fr_reset_dumpable();
+}
+#endif /* HAVE_SETUID */
+
+/** Alter the effective user id
+ *
+ * @param uid to set
+ * @return 0 on success -1 on failure.
+ */
+int rad_seuid(uid_t uid)
+{
+	if (seteuid(uid) < 0) {
+		struct passwd *passwd;
+
+		if (rad_getpwuid(NULL, &passwd, uid) < 0) return -1;
+		fr_strerror_printf("Failed setting euid to %s", passwd->pw_name);
+		talloc_free(passwd);
+
+		return -1;
+	}
+	return 0;
+}
+
+/** Alter the effective user id
+ *
+ * @param gid to set
+ * @return 0 on success -1 on failure.
+ */
+int rad_segid(gid_t gid)
+{
+	if (setegid(gid) < 0) {
+		struct group *group;
+
+		if (rad_getgrgid(NULL, &group, gid) < 0) return -1;
+		fr_strerror_printf("Failed setting egid to %s", group->gr_name);
+		talloc_free(group);
+
+		return -1;
+	}
+	return 0;
+}
+
+/** Determine the elapsed time between two timevals
+  *
+  * @param end timeval nearest to the present
+  * @param start timeval furthest from the present
+  * @param elapsed Where to write the elapsed time
+  */
+void rad_tv_sub(struct timeval const *end, struct timeval const *start, struct timeval *elapsed)
+{
+	elapsed->tv_sec = end->tv_sec - start->tv_sec;
+	if (elapsed->tv_sec > 0) {
+		elapsed->tv_sec--;
+		elapsed->tv_usec = USEC;
+	} else {
+		elapsed->tv_usec = 0;
+	}
+	elapsed->tv_usec += end->tv_usec;
+	elapsed->tv_usec -= start->tv_usec;
+
+	if (elapsed->tv_usec >= USEC) {
+		elapsed->tv_usec -= USEC;
+		elapsed->tv_sec++;
+	}
+}
diff --git a/src/main/version.c b/src/main/version.c
new file mode 100644
index 0000000..2fe3428
--- /dev/null
+++ b/src/main/version.c
@@ -0,0 +1,625 @@
+/*
+ * version.c	Print version number and exit.
+ *
+ * Version:	$Id$
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Copyright 1999-2019  The FreeRADIUS server project
+ * Copyright 2012  Alan DeKok <aland@ox.org>
+ * Copyright 2000  Chris Parker <cparker@starnetusa.com>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+USES_APPLE_DEPRECATED_API	/* OpenSSL API has been deprecated by Apple */
+
+static uint64_t	libmagic = RADIUSD_MAGIC_NUMBER;
+char const	*radiusd_version_short = RADIUSD_VERSION_STRING;
+
+#ifdef HAVE_OPENSSL_CRYPTO_H
+#  include <openssl/crypto.h>
+#  include <openssl/opensslv.h>
+
+static long ssl_built = OPENSSL_VERSION_NUMBER;
+
+/** Check built and linked versions of OpenSSL match
+ *
+ * OpenSSL version number consists of:
+ * MNNFFPPS: major minor fix patch status
+ *
+ * Where status >= 0 && < 10 means beta, and status 10 means release.
+ *
+ *	https://wiki.openssl.org/index.php/Versioning
+ *
+ * Startup check for whether the linked version of OpenSSL matches the
+ * version the server was built against.
+ *
+ * @return 0 if ok, else -1
+ */
+int ssl_check_consistency(void)
+{
+	long ssl_linked;
+
+	ssl_linked = SSLeay();
+
+	/*
+	 *	Major and minor versions mismatch, that's bad.
+	 */
+	if ((ssl_linked & 0xfff00000) != (ssl_built & 0xfff00000)) goto mismatch;
+
+	/*
+	 *	1.1.0 and later export all of the APIs we need, so we
+	 *	don't care about mismatches in fix / patch / status
+	 *	fields.  If the major && minor fields match, that's
+	 *	good enough.
+	 */
+	if ((ssl_linked & 0xfff00000) >= 0x10100000) return 0;
+
+	/*
+	 *	Before 1.1.0, we need all kinds of stupid checks to
+	 *	see if it might work.
+	 */
+
+	/*
+	 *	Status mismatch always triggers error.
+	 */
+	if ((ssl_linked & 0x0000000f) != (ssl_built & 0x0000000f)) {
+	mismatch:
+		ERROR("libssl version mismatch.  built: %lx linked: %lx",
+		      (unsigned long) ssl_built,
+		      (unsigned long) ssl_linked);
+
+		return -1;
+	}
+
+	/*
+	 *	Use the OpenSSH approach and relax fix checks after version
+	 *	1.0.0 and only allow moving backwards within a patch
+	 *	series.
+	 */
+	if (ssl_built & 0xf0000000) {
+		if ((ssl_built & 0xfffff000) != (ssl_linked & 0xfffff000) ||
+		    (ssl_built & 0x00000ff0) > (ssl_linked & 0x00000ff0)) goto mismatch;
+	/*
+	 *	Before 1.0.0 we require the same major minor and fix version
+	 *	and ignore the patch number.
+	 */
+	} else if ((ssl_built & 0xfffff000) != (ssl_linked & 0xfffff000)) goto mismatch;
+
+	return 0;
+}
+
+/** Convert a version number to a text string
+ *
+ * @note Not thread safe.
+ *
+ * @param v version to convert.
+ * @return pointer to a static buffer containing the version string.
+ */
+char const *ssl_version_by_num(uint32_t v)
+{
+	/* 2 (%s) + 1 (.) + 2 (%i) + 1 (.) + 2 (%i) + 1 (c) + 8 (%s) + \0 */
+	static char buffer[18];
+	char *p = buffer;
+
+	p += sprintf(p, "%u.%u.%u",
+		     (0xf0000000 & v) >> 28,
+		     (0x0ff00000 & v) >> 20,
+		     (0x000ff000 & v) >> 12);
+
+	if ((0x00000ff0 & v) >> 4) {
+		*p++ =  (char) (0x60 + ((0x00000ff0 & v) >> 4));
+	}
+
+	*p++ = ' ';
+
+	/*
+	 *	Development (0)
+	 */
+	if ((0x0000000f & v) == 0) {
+		strcpy(p, "dev");
+	/*
+	 *	Beta (1-14)
+	 */
+	} else if ((0x0000000f & v) <= 14) {
+		sprintf(p, "beta %u", 0x0000000f & v);
+	} else {
+		strcpy(p, "release");
+	}
+
+	return buffer;
+}
+
+/** Return the linked SSL version number as a string
+ *
+ * @return pointer to a static buffer containing the version string.
+ */
+char const *ssl_version_num(void)
+{
+	long ssl_linked;
+
+	ssl_linked = SSLeay();
+	return ssl_version_by_num((uint32_t)ssl_linked);
+}
+
+/** Convert two openssl version numbers into a range string
+ *
+ * @note Not thread safe.
+ *
+ * @param low version to convert.
+ * @param high version to convert.
+ * @return pointer to a static buffer containing the version range string.
+ */
+char const *ssl_version_range(uint32_t low, uint32_t high)
+{
+	/* 12 (version) + 3 ( - ) + 12 (version) */
+	static char buffer[28];
+	char *p = buffer;
+
+	p += strlcpy(p, ssl_version_by_num(low), sizeof(buffer));
+	p += strlcpy(p, " - ", sizeof(buffer) - (p - buffer));
+	strlcpy(p, ssl_version_by_num(high), sizeof(buffer) - (p - buffer));
+
+	return buffer;
+}
+
+/** Print the current linked version of Openssl
+ *
+ * Print the currently linked version of the OpenSSL library.
+ *
+ * @note Not thread safe.
+ * @return pointer to a static buffer containing libssl version information.
+ */
+char const *ssl_version(void)
+{
+	static char buffer[256];
+
+	uint32_t v = SSLeay();
+
+	snprintf(buffer, sizeof(buffer), "%s 0x%.8x (%s)",
+		 SSLeay_version(SSLEAY_VERSION),		/* Not all builds include a useful version number */
+		 v,
+		 ssl_version_by_num(v));
+
+	return buffer;
+}
+#  else
+int ssl_check_consistency(void) {
+	return 0;
+}
+
+char const *ssl_version_num(void)
+{
+	return "not linked";
+}
+
+char const *ssl_version(void)
+{
+	return "not linked";
+}
+#endif /* ifdef HAVE_OPENSSL_CRYPTO_H */
+
+/** Check if the application linking to the library has the correct magic number
+ *
+ * @param magic number as defined by RADIUSD_MAGIC_NUMBER
+ * @returns 0 on success, -1 on prefix mismatch, -2 on version mismatch -3 on commit mismatch.
+ */
+int rad_check_lib_magic(uint64_t magic)
+{
+	if (MAGIC_PREFIX(magic) != MAGIC_PREFIX(libmagic)) {
+		ERROR("Application and libfreeradius-server magic number (prefix) mismatch."
+		      "  application: %x library: %x",
+		      MAGIC_PREFIX(magic), MAGIC_PREFIX(libmagic));
+		return -1;
+	}
+
+	if (MAGIC_VERSION(magic) != MAGIC_VERSION(libmagic)) {
+		ERROR("Application and libfreeradius-server magic number (version) mismatch."
+		      "  application: %lx library: %lx",
+		      (unsigned long) MAGIC_VERSION(magic), (unsigned long) MAGIC_VERSION(libmagic));
+		return -2;
+	}
+
+	if (MAGIC_COMMIT(magic) != MAGIC_COMMIT(libmagic)) {
+		ERROR("Application and libfreeradius-server magic number (commit) mismatch."
+		      "  application: %lx library: %lx",
+		      (unsigned long) MAGIC_COMMIT(magic), (unsigned long) MAGIC_COMMIT(libmagic));
+		return -3;
+	}
+
+	return 0;
+}
+
+/** Add a feature flag to the main configuration
+ *
+ * Add a feature flag (yes/no) to the 'feature' subsection
+ * off the main config.
+ *
+ * This allows the user to create configurations that work with
+ * across multiple environments.
+ *
+ * @param cs to add feature pair to.
+ * @param name of feature.
+ * @param enabled Whether the feature is present/enabled.
+ * @return 0 on success else -1.
+ */
+int version_add_feature(CONF_SECTION *cs, char const *name, bool enabled)
+{
+	if (!cs) return -1;
+
+	if (!cf_pair_find(cs, name)) {
+		CONF_PAIR *cp;
+
+		cp = cf_pair_alloc(cs, name, enabled ? "yes" : "no",
+				   T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+		if (!cp) return -1;
+		cf_pair_add(cs, cp);
+	}
+
+	return 0;
+}
+
+/** Add a library/server version pair to the main configuration
+ *
+ * Add a version number to the 'version' subsection off the main
+ * config.
+ *
+ * Because of the optimisations in the configuration parser, these
+ * may be checked using regular expressions without a performance
+ * penalty.
+ *
+ * The version pairs are there primarily to work around defects
+ * in libraries or the server.
+ *
+ * @param cs to add feature pair to.
+ * @param name of library or feature.
+ * @param version Humanly readable version text.
+ * @return 0 on success else -1.
+ */
+int version_add_number(CONF_SECTION *cs, char const *name, char const *version)
+{
+	CONF_PAIR *old;
+
+	if (!cs) return -1;
+
+	old = cf_pair_find(cs, name);
+	if (!old) {
+		CONF_PAIR *cp;
+
+		cp = cf_pair_alloc(cs, name, version, T_OP_SET, T_BARE_WORD, T_SINGLE_QUOTED_STRING);
+		if (!cp) return -1;
+
+		cf_pair_add(cs, cp);
+	} else {
+		WARN("Replacing user version.%s (%s) with %s", name, cf_pair_value(old), version);
+
+		cf_pair_replace(cs, old, version);
+	}
+
+	return 0;
+}
+
+
+/** Initialise core feature flags
+ *
+ * @param cs Where to add the CONF_PAIRS, if null pairs will be added
+ *	to the 'feature' section of the main config.
+ */
+void version_init_features(CONF_SECTION *cs)
+{
+	version_add_feature(cs, "accounting",
+#ifdef WITH_ACCOUNTING
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "authentication", true);
+
+	version_add_feature(cs, "ascend-binary-attributes",
+#ifdef WITH_ASCEND_BINARY
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "coa",
+#ifdef WITH_COA
+				true
+#else
+				false
+#endif
+				);
+
+
+	version_add_feature(cs, "recv-coa-from-home-server",
+#ifdef WITH_COA_TUNNEL
+			        true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "control-socket",
+#ifdef WITH_COMMAND_SOCKET
+				true
+#else
+				false
+#endif
+				);
+
+
+	version_add_feature(cs, "detail",
+#ifdef WITH_DETAIL
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "dhcp",
+#ifdef WITH_DHCP
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "dynamic-clients",
+#ifdef WITH_DYNAMIC_CLIENTS
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "osfc2",
+#ifdef OSFC2
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "proxy",
+#ifdef WITH_PROXY
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "regex-pcre",
+#ifdef HAVE_PCRE
+				true
+#else
+				false
+#endif
+				);
+
+#if !defined(HAVE_PCRE) && defined(HAVE_REGEX)
+	version_add_feature(cs, "regex-posix", true);
+	version_add_feature(cs, "regex-posix-extended",
+#  ifdef HAVE_REG_EXTENDED
+				true
+#  else
+				false
+#  endif
+				);
+#else
+	version_add_feature(cs, "regex-posix", false);
+	version_add_feature(cs, "regex-posix-extended", false);
+#endif
+
+	version_add_feature(cs, "session-management",
+#ifdef WITH_SESSION_MGMT
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "stats",
+#ifdef WITH_STATS
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "systemd",
+#ifdef HAVE_SYSTEMD
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "tcp",
+#ifdef WITH_TCP
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "threads",
+#ifdef WITH_THREADS
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "tls",
+#ifdef WITH_TLS
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "unlang",
+#ifdef WITH_UNLANG
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "vmps",
+#ifdef WITH_VMPS
+				true
+#else
+				false
+#endif
+				);
+
+	version_add_feature(cs, "developer",
+#ifndef NDEBUG
+				true
+#else
+				false
+#endif
+				);
+}
+
+/** Initialise core version flags
+ *
+ * @param cs Where to add the CONF_PAIRS, if null pairs will be added
+ *	to the 'version' section of the main config.
+ */
+void version_init_numbers(CONF_SECTION *cs)
+{
+	char buffer[128];
+
+	version_add_number(cs, "freeradius-server", radiusd_version_short);
+
+	snprintf(buffer, sizeof(buffer), "%i.%i.*", talloc_version_major(), talloc_version_minor());
+	version_add_number(cs, "talloc", buffer);
+
+	version_add_number(cs, "ssl", ssl_version_num());
+
+#if defined(HAVE_REGEX) && defined(HAVE_PCRE)
+	version_add_number(cs, "pcre", pcre_version());
+#endif
+}
+
+static char const *spaces = "                                    ";	/* 40 */
+
+/*
+ *	Display the revision number for this program
+ */
+void version_print(void)
+{
+	CONF_SECTION *features, *versions;
+	CONF_ITEM *ci;
+	CONF_PAIR *cp;
+
+	if (DEBUG_ENABLED3) {
+		int max = 0, len;
+
+		MEM(features = cf_section_alloc(NULL, "feature", NULL));
+		version_init_features(features);
+
+		MEM(versions = cf_section_alloc(NULL, "version", NULL));
+		version_init_numbers(versions);
+
+		DEBUG2("Server was built with: ");
+
+		for (ci = cf_item_find_next(features, NULL);
+		     ci;
+		     ci = cf_item_find_next(features, ci)) {
+			len = talloc_array_length(cf_pair_attr(cf_item_to_pair(ci)));
+			if (max < len) max = len;
+		}
+
+		for (ci = cf_item_find_next(versions, NULL);
+		     ci;
+		     ci = cf_item_find_next(versions, ci)) {
+			len = talloc_array_length(cf_pair_attr(cf_item_to_pair(ci)));
+			if (max < len) max = len;
+		}
+
+
+		for (ci = cf_item_find_next(features, NULL);
+		     ci;
+		     ci = cf_item_find_next(features, ci)) {
+		     	char const *attr;
+
+			cp = cf_item_to_pair(ci);
+			attr = cf_pair_attr(cp);
+
+			DEBUG2("  %s%.*s : %s", attr,
+			       (int)(max - talloc_array_length(attr)), spaces,  cf_pair_value(cp));
+		}
+
+		talloc_free(features);
+
+		DEBUG2("Server core libs:");
+
+		for (ci = cf_item_find_next(versions, NULL);
+		     ci;
+		     ci = cf_item_find_next(versions, ci)) {
+		     	char const *attr;
+
+			cp = cf_item_to_pair(ci);
+			attr = cf_pair_attr(cp);
+
+			DEBUG2("  %s%.*s : %s", attr,
+			       (int)(max - talloc_array_length(attr)), spaces,  cf_pair_value(cp));
+		}
+
+		talloc_free(versions);
+
+		DEBUG2("Endianness:");
+#if defined(FR_LITTLE_ENDIAN)
+		DEBUG2("  little");
+#elif defined(FR_BIG_ENDIAN)
+		DEBUG2("  big");
+#else
+		DEBUG2("  unknown");
+#endif
+
+		DEBUG2("Compilation flags:");
+#ifdef BUILT_WITH_CPPFLAGS
+		DEBUG2("  cppflags : " BUILT_WITH_CPPFLAGS);
+#endif
+#ifdef BUILT_WITH_CFLAGS
+		DEBUG2("  cflags   : " BUILT_WITH_CFLAGS);
+#endif
+#ifdef BUILT_WITH_LDFLAGS
+		DEBUG2("  ldflags  : " BUILT_WITH_LDFLAGS);
+#endif
+#ifdef BUILT_WITH_LIBS
+		DEBUG2("  libs     : " BUILT_WITH_LIBS);
+#endif
+		DEBUG2("  ");
+	}
+	INFO("FreeRADIUS Version " RADIUSD_VERSION_STRING);
+	INFO("Copyright (C) 1999-2022 The FreeRADIUS server project and contributors");
+	INFO("There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A");
+	INFO("PARTICULAR PURPOSE");
+	INFO("You may redistribute copies of FreeRADIUS under the terms of the");
+	INFO("GNU General Public License");
+	INFO("For more information about these matters, see the file named COPYRIGHT");
+
+	fflush(NULL);
+}
+
diff --git a/src/main/xlat.c b/src/main/xlat.c
new file mode 100644
index 0000000..4bd0a37
--- /dev/null
+++ b/src/main/xlat.c
@@ -0,0 +1,2696 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @file xlat.c
+ * @brief String expansion ("translation"). Implements %Attribute -> value
+ *
+ * @copyright 2000,2006  The FreeRADIUS server project
+ * @copyright 2000  Alan DeKok <aland@ox.org>
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/parser.h>
+#include <freeradius-devel/rad_assert.h>
+#include <freeradius-devel/base64.h>
+
+#include <ctype.h>
+
+typedef struct xlat_t {
+	char			name[MAX_STRING_LEN];	//!< Name of the xlat expansion.
+	int			length;			//!< Length of name.
+	void			*instance;		//!< Module instance passed to xlat and escape functions.
+	xlat_func_t		func;			//!< xlat function.
+	xlat_escape_t	escape;			//!< Escape function to apply to dynamic input to func.
+	bool			internal;		//!< If true, cannot be redefined.
+} xlat_t;
+
+typedef enum {
+	XLAT_LITERAL,		//!< Literal string
+	XLAT_PERCENT,		//!< Literal string with %v
+	XLAT_MODULE,		//!< xlat module
+	XLAT_VIRTUAL,		//!< virtual attribute
+	XLAT_ATTRIBUTE,		//!< xlat attribute
+#ifdef HAVE_REGEX
+	XLAT_REGEX,		//!< regex reference
+#endif
+	XLAT_ALTERNATE		//!< xlat conditional syntax :-
+} xlat_state_t;
+
+struct xlat_exp {
+	char const *fmt;	//!< The format string.
+	size_t len;		//!< Length of the format string.
+
+	xlat_state_t type;	//!< type of this expansion.
+	xlat_exp_t *next;	//!< Next in the list.
+
+	xlat_exp_t *child;	//!< Nested expansion.
+	xlat_exp_t *alternate;	//!< Alternative expansion if this one expanded to a zero length string.
+
+	vp_tmpl_t attr;	//!< An attribute template.
+	xlat_t const *xlat;	//!< The xlat expansion to expand format with.
+};
+
+static rbtree_t *xlat_root = NULL;
+
+#ifdef WITH_UNLANG
+static char const * const xlat_foreach_names[] = {"Foreach-Variable-0",
+						  "Foreach-Variable-1",
+						  "Foreach-Variable-2",
+						  "Foreach-Variable-3",
+						  "Foreach-Variable-4",
+						  "Foreach-Variable-5",
+						  "Foreach-Variable-6",
+						  "Foreach-Variable-7",
+						  "Foreach-Variable-8",
+						  "Foreach-Variable-9",
+						  NULL};
+#endif
+
+
+static int xlat_inst[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };	/* up to 10 for foreach */
+
+static char *xlat_getvp(TALLOC_CTX *ctx, REQUEST *request, vp_tmpl_t const *vpt,
+			bool escape, bool return_null, char const *concat);
+
+/** Concatenation
+ *
+ */
+static ssize_t xlat_concat(UNUSED void *instance, REQUEST *request,
+			   char const *fmt, char *out, size_t outlen)
+{
+	ssize_t slen;
+	vp_tmpl_t vpt;
+	char *str;
+	char const *concat;
+	char buffer[2];
+
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	slen = tmpl_from_attr_substr(&vpt, fmt,  REQUEST_CURRENT, PAIR_LIST_REQUEST, false, false);
+	if (slen <= 0) {
+		RDEBUG("%s", fr_strerror());
+		return -1;
+	}
+
+	fmt += slen;
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	if (!*fmt) {
+		concat = ",";
+	} else {
+		buffer[0] = *fmt;
+		buffer[1] = '\0';
+
+		concat = buffer;
+	}
+
+	str = xlat_getvp(request, request, &vpt, true, true, concat);
+	if (!str) return 0;
+
+	strlcpy(out, str, outlen);
+	talloc_free(str);
+
+	return strlen(out);
+}
+
+/** Print length of its RHS.
+ *
+ */
+static ssize_t xlat_strlen(UNUSED void *instance, UNUSED REQUEST *request,
+			   char const *fmt, char *out, size_t outlen)
+{
+	snprintf(out, outlen, "%u", (unsigned int) strlen(fmt));
+	return strlen(out);
+}
+
+/** Print the size of the attribute in bytes.
+ *
+ */
+static ssize_t xlat_length(UNUSED void *instance, REQUEST *request,
+			   char const *fmt, char *out, size_t outlen)
+{
+	VALUE_PAIR *vp;
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) {
+		*out = '\0';
+		return 0;
+	}
+
+	snprintf(out, outlen, "%zu", vp->vp_length);
+	return strlen(out);
+}
+
+/** Print data as integer, not as VALUE.
+ *
+ */
+static ssize_t xlat_integer(UNUSED void *instance, REQUEST *request,
+			    char const *fmt, char *out, size_t outlen)
+{
+	VALUE_PAIR 	*vp;
+
+	uint64_t 	int64 = 0;	/* Needs to be initialised to zero */
+	uint32_t	int32 = 0;	/* Needs to be initialised to zero */
+
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) {
+		*out = '\0';
+		return 0;
+	}
+
+	switch (vp->da->type) {
+	case PW_TYPE_OCTETS:
+	case PW_TYPE_STRING:
+		if (vp->vp_length > 8) {
+			break;
+		}
+
+		if (vp->vp_length > 4) {
+			memcpy(&int64, vp->vp_octets, vp->vp_length);
+			return snprintf(out, outlen, "%" PRIu64, htonll(int64));
+		}
+
+		memcpy(&int32, vp->vp_octets, vp->vp_length);
+		return snprintf(out, outlen, "%i", htonl(int32));
+
+	case PW_TYPE_INTEGER64:
+		return snprintf(out, outlen, "%" PRIu64, vp->vp_integer64);
+
+	/*
+	 *	IP addresses are treated specially, as parsing functions assume the value
+	 *	is bigendian and will convert it for us.
+	 */
+	case PW_TYPE_IPV4_ADDR:
+		return snprintf(out, outlen, "%u", htonl(vp->vp_ipaddr));
+
+	case PW_TYPE_IPV4_PREFIX:
+		return snprintf(out, outlen, "%u", htonl((*(uint32_t *)(&vp->vp_ipv4prefix[2]))));
+
+	case PW_TYPE_INTEGER:
+		return snprintf(out, outlen, "%u", vp->vp_integer);
+
+	case PW_TYPE_DATE:
+		return snprintf(out, outlen, "%u", vp->vp_date);
+
+	case PW_TYPE_BYTE:
+		return snprintf(out, outlen, "%u", (unsigned int) vp->vp_byte);
+
+	case PW_TYPE_SHORT:
+		return snprintf(out, outlen, "%u", (unsigned int) vp->vp_short);
+
+	/*
+	 *	Ethernet is weird... It's network related, so we assume to it should be
+	 *	bigendian.
+	 */
+	case PW_TYPE_ETHERNET:
+		memcpy(&int64, vp->vp_ether, vp->vp_length);
+		return snprintf(out, outlen, "%" PRIu64, htonll(int64));
+
+	case PW_TYPE_SIGNED:
+		return snprintf(out, outlen, "%i", vp->vp_signed);
+
+	case PW_TYPE_IPV6_ADDR:
+		return fr_prints_uint128(out, outlen, ntohlll(*(uint128_t const *) &vp->vp_ipv6addr));
+
+	case PW_TYPE_IPV6_PREFIX:
+		return fr_prints_uint128(out, outlen, ntohlll(*(uint128_t const *) &vp->vp_ipv6prefix[2]));
+
+	default:
+		break;
+	}
+
+	REDEBUG("Type '%s' of length %zu cannot be converted to integer",
+		fr_int2str(dict_attr_types, vp->da->type, "???"), vp->vp_length);
+	*out = '\0';
+
+	return -1;
+}
+
+/** Print data as hex, not as VALUE.
+ *
+ */
+static ssize_t xlat_hex(UNUSED void *instance, REQUEST *request,
+			char const *fmt, char *out, size_t outlen)
+{
+	size_t i;
+	VALUE_PAIR *vp;
+	uint8_t const *p;
+	ssize_t	ret;
+	size_t	len;
+	value_data_t dst;
+	uint8_t const *buff = NULL;
+
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) {
+	error:
+		*out = '\0';
+		return -1;
+	}
+
+	/*
+	 *	The easy case.
+	 */
+	if (vp->da->type == PW_TYPE_OCTETS) {
+		p = vp->vp_octets;
+		len = vp->vp_length;
+	/*
+	 *	Cast the value_data_t of the VP to an octets string and
+	 *	print that as hex.
+	 */
+	} else {
+		ret = value_data_cast(request, &dst, PW_TYPE_OCTETS, NULL, vp->da->type,
+				      NULL, &vp->data, vp->vp_length);
+		if (ret < 0) {
+			REDEBUG("%s", fr_strerror());
+			goto error;
+		}
+		len = (size_t) ret;
+		p = buff = dst.octets;
+	}
+
+	rad_assert(p);
+
+	/*
+	 *	Don't truncate the data.
+	 */
+	if (outlen < (len * 2)) {
+		rad_const_free(buff);
+		goto error;
+	}
+
+	for (i = 0; i < len; i++) {
+		snprintf(out + 2*i, 3, "%02x", p[i]);
+	}
+	rad_const_free(buff);
+
+	return len * 2;
+}
+
+/** Return the tag of an attribute reference
+ *
+ */
+static ssize_t xlat_tag(UNUSED void *instance, REQUEST *request,
+		        char const *fmt, char *out, size_t outlen)
+{
+	VALUE_PAIR *vp;
+
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) {
+		*out = '\0';
+		return 0;
+	}
+
+	if (!vp->da->flags.has_tag || !TAG_VALID(vp->tag)) {
+		*out = '\0';
+		return 0;
+	}
+
+	return snprintf(out, outlen, "%u", vp->tag);
+}
+
+/** Return the vendor of an attribute reference
+ *
+ */
+static ssize_t xlat_vendor(UNUSED void *instance, REQUEST *request,
+		           char const *fmt, char *out, size_t outlen)
+{
+	VALUE_PAIR *vp;
+	DICT_VENDOR *vendor;
+
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) {
+		*out = '\0';
+		return 0;
+	}
+
+	vendor = dict_vendorbyvalue(vp->da->vendor);
+	if (!vendor) {
+		*out = '\0';
+		return 0;
+	}
+	strlcpy(out, vendor->name, outlen);
+
+	return vendor->length;
+}
+
+/** Return the vendor number of an attribute reference
+ *
+ */
+static ssize_t xlat_vendor_num(UNUSED void *instance, REQUEST *request,
+		               char const *fmt, char *out, size_t outlen)
+{
+	VALUE_PAIR *vp;
+
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) {
+		*out = '\0';
+		return 0;
+	}
+
+	return snprintf(out, outlen, "%u", vp->da->vendor);
+}
+
+/** Return the attribute name of an attribute reference
+ *
+ */
+static ssize_t xlat_attr(UNUSED void *instance, REQUEST *request,
+			 char const *fmt, char *out, size_t outlen)
+{
+	VALUE_PAIR *vp;
+
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) {
+		*out = '\0';
+		return 0;
+	}
+	strlcpy(out, vp->da->name, outlen);
+
+	return strlen(vp->da->name);
+}
+
+/** Return the attribute number of an attribute reference
+ *
+ */
+static ssize_t xlat_attr_num(UNUSED void *instance, REQUEST *request,
+		             char const *fmt, char *out, size_t outlen)
+{
+	VALUE_PAIR *vp;
+
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) {
+		*out = '\0';
+		return 0;
+	}
+
+	return snprintf(out, outlen, "%u", vp->da->attr);
+}
+
+/** Print out attribute info
+ *
+ * Prints out all instances of a current attribute, or all attributes in a list.
+ *
+ * At higher debugging levels, also prints out alternative decodings of the same
+ * value. This is helpful to determine types for unknown attributes of long
+ * passed vendors, or just crazy/broken NAS.
+ *
+ * This expands to a zero length string.
+ */
+static ssize_t xlat_debug_attr(UNUSED void *instance, REQUEST *request, char const *fmt,
+			       char *out, UNUSED size_t outlen)
+{
+	VALUE_PAIR *vp;
+	vp_cursor_t cursor;
+
+	vp_tmpl_t vpt;
+
+	if (!RDEBUG_ENABLED2) {
+		*out = '\0';
+		return -1;
+	}
+
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	if (tmpl_from_attr_str(&vpt, fmt, REQUEST_CURRENT, PAIR_LIST_REQUEST, false, false) <= 0) {
+		RDEBUG("%s", fr_strerror());
+		return -1;
+	}
+
+	RIDEBUG("Attributes matching \"%s\"", fmt);
+
+	RINDENT();
+	for (vp = tmpl_cursor_init(NULL, &cursor, request, &vpt);
+	     vp;
+	     vp = tmpl_cursor_next(&cursor, &vpt)) {
+		FR_NAME_NUMBER const *type;
+		char *value;
+
+		value = vp_aprints_value(vp, vp, '\'');
+		if (vp->da->flags.has_tag) {
+			RIDEBUG2("&%s:%s:%i %s %s",
+				fr_int2str(pair_lists, vpt.tmpl_list, "<INVALID>"),
+				vp->da->name,
+				vp->tag,
+				fr_int2str(fr_tokens, vp->op, "<INVALID>"),
+				value);
+		} else {
+			RIDEBUG2("&%s:%s %s %s",
+				fr_int2str(pair_lists, vpt.tmpl_list, "<INVALID>"),
+				vp->da->name,
+				fr_int2str(fr_tokens, vp->op, "<INVALID>"),
+				value);
+		}
+		talloc_free(value);
+
+		if (!RDEBUG_ENABLED3) continue;
+
+		if (vp->da->vendor) {
+			DICT_VENDOR *dv;
+
+			dv = dict_vendorbyvalue(vp->da->vendor);
+			RIDEBUG2("Vendor : %i (%s)", vp->da->vendor, dv ? dv->name : "unknown");
+		}
+		RIDEBUG2("Type   : %s", fr_int2str(dict_attr_types, vp->da->type, "<INVALID>"));
+		RIDEBUG2("Length : %zu", vp->vp_length);
+
+		if (!RDEBUG_ENABLED4) continue;
+
+		type = dict_attr_types;
+		while (type->name) {
+			int pad;
+
+			value_data_t *dst = NULL;
+
+			ssize_t ret;
+
+			if ((PW_TYPE) type->number == vp->da->type) {
+				goto next_type;
+			}
+
+			switch (type->number) {
+			case PW_TYPE_INVALID:		/* Not real type */
+			case PW_TYPE_MAX:		/* Not real type */
+			case PW_TYPE_EXTENDED:		/* Not safe/appropriate */
+			case PW_TYPE_LONG_EXTENDED:	/* Not safe/appropriate */
+			case PW_TYPE_TLV:		/* Not safe/appropriate */
+			case PW_TYPE_EVS:		/* Not safe/appropriate */
+			case PW_TYPE_VSA:		/* @fixme We need special behaviour for these */
+			case PW_TYPE_COMBO_IP_ADDR:	/* Covered by IPv4 address IPv6 address */
+			case PW_TYPE_COMBO_IP_PREFIX:	/* Covered by IPv4 address IPv6 address */
+			case PW_TYPE_TIMEVAL:		/* Not a VALUE_PAIR type */
+				goto next_type;
+
+			default:
+				break;
+			}
+
+			dst = talloc_zero(vp, value_data_t);
+			ret = value_data_cast(dst, dst, type->number, NULL, vp->da->type, vp->da,
+					      &vp->data, vp->vp_length);
+			if (ret < 0) goto next_type;	/* We expect some to fail */
+
+			value = value_data_aprints(dst, type->number, NULL, dst, (size_t)ret, '\'');
+			if (!value) goto next_type;
+
+			if ((pad = (11 - strlen(type->name))) < 0) {
+				pad = 0;
+			}
+
+			RINDENT();
+			RDEBUG2("as %s%*s: %s", type->name, pad, " ", value);
+			REXDENT();
+			talloc_free(value);
+
+		next_type:
+			talloc_free(dst);
+			type++;
+		}
+	}
+	REXDENT();
+
+	*out = '\0';
+	return 0;
+}
+
+/** Processes fmt as a map string and applies it to the current request
+ *
+ * e.g. "%{map:&User-Name := 'foo'}"
+ *
+ * Allows sets of modifications to be cached and then applied.
+ * Useful for processing generic attributes from LDAP.
+ */
+static ssize_t xlat_map(UNUSED void *instance, REQUEST *request,
+			char const *fmt, char *out, size_t outlen)
+{
+	vp_map_t *map = NULL;
+	int ret;
+
+	if (map_afrom_attr_str(request, &map, fmt,
+			       REQUEST_CURRENT, PAIR_LIST_REQUEST,
+			       REQUEST_CURRENT, PAIR_LIST_REQUEST) < 0) {
+		REDEBUG("Failed parsing \"%s\" as map: %s", fmt, fr_strerror());
+		return -1;
+	}
+
+	RINDENT();
+	ret = map_to_request(request, map, map_to_vp, NULL);
+	REXDENT();
+	talloc_free(map);
+	if (ret < 0) return strlcpy(out, "0", outlen);
+
+	return strlcpy(out, "1", outlen);
+}
+
+/** Prints the current module processing the request
+ *
+ */
+static ssize_t xlat_module(UNUSED void *instance, REQUEST *request,
+			   UNUSED char const *fmt, char *out, size_t outlen)
+{
+	strlcpy(out, request->module, outlen);
+
+	return strlen(out);
+}
+
+#if defined(HAVE_REGEX) && defined(HAVE_PCRE)
+static ssize_t xlat_regex(UNUSED void *instance, REQUEST *request,
+			  char const *fmt, char *out, size_t outlen)
+{
+	char *p;
+	size_t len;
+
+	if (regex_request_to_sub_named(request, &p, request, fmt) < 0) {
+		*out = '\0';
+		return 0;
+	}
+
+	len = talloc_array_length(p);
+	if (len > outlen) {
+		RDEBUG("Insufficient buffer space to write subcapture value, needed %zu bytes, have %zu bytes",
+		       len, outlen);
+		return -1;
+	}
+	strlcpy(out, p, outlen);
+
+	return len - 1; /* - \0 */
+}
+#endif
+
+#ifdef WITH_UNLANG
+/** Implements the Foreach-Variable-X
+ *
+ * @see modcall()
+ */
+static ssize_t xlat_foreach(void *instance, REQUEST *request,
+			    UNUSED char const *fmt, char *out, size_t outlen)
+{
+	VALUE_PAIR	**pvp;
+	size_t		len;
+
+	/*
+	 *	See modcall, "FOREACH" for how this works.
+	 */
+	pvp = (VALUE_PAIR **) request_data_reference(request, (void *)radius_get_vp, *(int*) instance);
+	if (!pvp || !*pvp) {
+		*out = '\0';
+		return 0;
+	}
+
+	len = vp_prints_value(out, outlen, *pvp, 0);
+	if (is_truncated(len, outlen)) {
+		RDEBUG("Insufficient buffer space to write foreach value");
+		return -1;
+	}
+
+	return len;
+}
+#endif
+
+/** Print data as string, if possible.
+ *
+ * If attribute "Foo" is defined as "octets" it will normally
+ * be printed as 0x0a0a0a. The xlat "%{string:Foo}" will instead
+ * expand to "\n\n\n"
+ */
+static ssize_t xlat_string(UNUSED void *instance, REQUEST *request,
+			   char const *fmt, char *out, size_t outlen)
+{
+	size_t len;
+	ssize_t ret;
+	VALUE_PAIR *vp;
+	uint8_t const *p;
+
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	if (outlen < 3) {
+	nothing:
+		*out = '\0';
+		return 0;
+	}
+
+	if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) goto nothing;
+
+	ret = rad_vp2data(&p, vp);
+	if (ret < 0) {
+		return ret;
+	}
+
+	switch (vp->da->type) {
+	case PW_TYPE_OCTETS:
+		len = fr_prints(out, outlen, (char const *) p, vp->vp_length, '"');
+		break;
+
+		/*
+		 *	Note that "%{string:...}" is NOT binary safe!
+		 *	It is explicitly used to get rid of embedded zeros.
+		 */
+	case PW_TYPE_STRING:
+		len = strlcpy(out, vp->vp_strvalue, outlen);
+		break;
+
+	default:
+		len = fr_prints(out, outlen, (char const *) p, ret, '\0');
+		break;
+	}
+
+	return len;
+}
+
+/** xlat expand string attribute value
+ *
+ */
+static ssize_t xlat_xlat(UNUSED void *instance, REQUEST *request,
+			char const *fmt, char *out, size_t outlen)
+{
+	VALUE_PAIR *vp;
+
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	if (outlen < 3) {
+	nothing:
+		*out = '\0';
+		return 0;
+	}
+
+	if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) goto nothing;
+
+	if (vp->da->type != PW_TYPE_STRING) goto nothing;
+
+	return radius_xlat(out, outlen, request, vp->vp_strvalue, NULL, NULL);
+}
+
+/** Dynamically change the debugging level for the current request
+ *
+ * Example %{debug:3}
+ */
+static ssize_t xlat_debug(UNUSED void *instance, REQUEST *request,
+			  char const *fmt, char *out, size_t outlen)
+{
+	int level = 0;
+
+	/*
+	 *  Expand to previous (or current) level
+	 */
+	snprintf(out, outlen, "%d", request->log.lvl);
+
+	/*
+	 *  Assume we just want to get the current value and NOT set it to 0
+	 */
+	if (!*fmt)
+		goto done;
+
+	level = atoi(fmt);
+	if (level == 0) {
+		request->log.lvl = RAD_REQUEST_LVL_NONE;
+		request->log.func = NULL;
+	} else {
+		if (level > 4) level = 4;
+
+		request->log.lvl = level;
+		request->log.func = vradlog_request;
+	}
+
+	done:
+	return strlen(out);
+}
+
+/*
+ *	Compare two xlat_t structs, based ONLY on the module name.
+ */
+static int xlat_cmp(void const *one, void const *two)
+{
+	xlat_t const *a = one;
+	xlat_t const *b = two;
+
+	if (a->length != b->length) {
+		return a->length - b->length;
+	}
+
+	return memcmp(a->name, b->name, a->length);
+}
+
+
+/*
+ *	find the appropriate registered xlat function.
+ */
+static xlat_t *xlat_find(char const *name)
+{
+	xlat_t my_xlat;
+
+	strlcpy(my_xlat.name, name, sizeof(my_xlat.name));
+	my_xlat.length = strlen(my_xlat.name);
+
+	return rbtree_finddata(xlat_root, &my_xlat);
+}
+
+
+/** Register an xlat function.
+ *
+ * @param[in] name xlat name.
+ * @param[in] func xlat function to be called.
+ * @param[in] escape function to sanitize any sub expansions passed to the xlat function.
+ * @param[in] instance of module that's registering the xlat function.
+ * @return 0 on success, -1 on failure
+ */
+int xlat_register(char const *name, xlat_func_t func, xlat_escape_t escape, void *instance)
+{
+	xlat_t	*c;
+	xlat_t	my_xlat;
+	rbnode_t *node;
+
+	if (!name || !*name) {
+		DEBUG("xlat_register: Invalid xlat name");
+		return -1;
+	}
+
+	/*
+	 *	First time around, build up the tree...
+	 *
+	 *	FIXME: This code should be hoisted out of this function,
+	 *	and into a global "initialization".  But it isn't critical...
+	 */
+	if (!xlat_root) {
+#ifdef WITH_UNLANG
+		int i;
+#endif
+
+		xlat_root = rbtree_create(NULL, xlat_cmp, NULL, RBTREE_FLAG_REPLACE);
+		if (!xlat_root) {
+			DEBUG("xlat_register: Failed to create tree");
+			return -1;
+		}
+
+#ifdef WITH_UNLANG
+		for (i = 0; xlat_foreach_names[i] != NULL; i++) {
+			xlat_register(xlat_foreach_names[i],
+				      xlat_foreach, NULL, &xlat_inst[i]);
+			c = xlat_find(xlat_foreach_names[i]);
+			rad_assert(c != NULL);
+			c->internal = true;
+		}
+#endif
+
+#define XLAT_REGISTER(_x) xlat_register(STRINGIFY(_x), xlat_ ## _x, NULL, NULL); \
+		c = xlat_find(STRINGIFY(_x)); \
+		rad_assert(c != NULL); \
+		c->internal = true
+
+		XLAT_REGISTER(concat);
+		XLAT_REGISTER(integer);
+		XLAT_REGISTER(strlen);
+		XLAT_REGISTER(length);
+		XLAT_REGISTER(hex);
+		XLAT_REGISTER(tag);
+		XLAT_REGISTER(vendor);
+		XLAT_REGISTER(vendor_num);
+		XLAT_REGISTER(attr);
+		XLAT_REGISTER(attr_num);
+		XLAT_REGISTER(string);
+		XLAT_REGISTER(xlat);
+		XLAT_REGISTER(map);
+		XLAT_REGISTER(module);
+		XLAT_REGISTER(debug_attr);
+#if defined(HAVE_REGEX) && defined(HAVE_PCRE)
+		XLAT_REGISTER(regex);
+#endif
+
+		xlat_register("debug", xlat_debug, NULL, &xlat_inst[0]);
+		c = xlat_find("debug");
+		rad_assert(c != NULL);
+		c->internal = true;
+	}
+
+	/*
+	 *	If it already exists, replace the instance.
+	 */
+	strlcpy(my_xlat.name, name, sizeof(my_xlat.name));
+	my_xlat.length = strlen(my_xlat.name);
+	c = rbtree_finddata(xlat_root, &my_xlat);
+	if (c) {
+		if (c->internal) {
+			DEBUG("xlat_register: Cannot re-define internal xlat");
+			return -1;
+		}
+
+		c->func = func;
+		c->escape = escape;
+		c->instance = instance;
+		return 0;
+	}
+
+	/*
+	 *	Doesn't exist.  Create it.
+	 */
+	c = talloc_zero(xlat_root, xlat_t);
+
+	c->func = func;
+	c->escape = escape;
+	strlcpy(c->name, name, sizeof(c->name));
+	c->length = strlen(c->name);
+	c->instance = instance;
+
+	node = rbtree_insert_node(xlat_root, c);
+	if (!node) {
+		talloc_free(c);
+		return -1;
+	}
+
+	/*
+	 *	Ensure that the data is deleted when the node is
+	 *	deleted.
+	 *
+	 *	@todo: Maybe this should be the other way around...
+	 *	when a thing IN the tree is deleted, it's automatically
+	 *	removed from the tree.  But for now, this works.
+	 */
+	(void) talloc_steal(node, c);
+	return 0;
+}
+
+/** Unregister an xlat function
+ *
+ * We can only have one function to call per name, so the passing of "func"
+ * here is extraneous.
+ *
+ * @param[in] name xlat to unregister.
+ * @param[in] func unused.
+ * @param[in] instance data.
+ */
+void xlat_unregister(char const *name, UNUSED xlat_func_t func, void *instance)
+{
+	xlat_t	*c;
+	xlat_t		my_xlat;
+
+	if (!name || !xlat_root) return;
+
+	strlcpy(my_xlat.name, name, sizeof(my_xlat.name));
+	my_xlat.length = strlen(my_xlat.name);
+
+	c = rbtree_finddata(xlat_root, &my_xlat);
+	if (!c) return;
+
+	if (c->instance != instance) return;
+
+	rbtree_deletebydata(xlat_root, c);
+}
+
+static int xlat_unregister_callback(void *instance, void *data)
+{
+	xlat_t *c = (xlat_t *) data;
+
+	if (c->instance != instance) return 0; /* keep walking */
+
+	return 2;		/* delete it */
+}
+
+void xlat_unregister_module(void *instance)
+{
+	rbtree_walk(xlat_root, RBTREE_DELETE_ORDER, xlat_unregister_callback, instance);
+}
+
+/*
+ *	Internal redundant handler for xlats
+ */
+typedef enum xlat_redundant_type_t {
+	XLAT_INVALID = 0,
+	XLAT_REDUNDANT,
+	XLAT_LOAD_BALANCE,
+	XLAT_REDUNDANT_LOAD_BALANCE,
+} xlat_redundant_type_t;
+
+typedef struct xlat_redundant_t {
+	xlat_redundant_type_t type;
+	uint32_t	count;
+	CONF_SECTION *cs;
+} xlat_redundant_t;
+
+
+static ssize_t xlat_redundant(void *instance, REQUEST *request,
+			      char const *fmt, char *out, size_t outlen)
+{
+	xlat_redundant_t *xr = instance;
+	CONF_ITEM *ci;
+	char const *name;
+	xlat_t *xlat;
+
+	rad_assert(xr->type == XLAT_REDUNDANT);
+
+	/*
+	 *	Pick the first xlat which succeeds
+	 */
+	for (ci = cf_item_find_next(xr->cs, NULL);
+	     ci != NULL;
+	     ci = cf_item_find_next(xr->cs, ci)) {
+		ssize_t rcode;
+
+		if (!cf_item_is_pair(ci)) continue;
+
+		name = cf_pair_attr(cf_item_to_pair(ci));
+		rad_assert(name != NULL);
+
+		xlat = xlat_find(name);
+		if (!xlat) continue;
+
+		rcode = xlat->func(xlat->instance, request, fmt, out, outlen);
+		if (rcode <= 0) continue;
+		return rcode;
+	}
+
+	/*
+	 *	Everything failed.  Oh well.
+	 */
+	*out  = 0;
+	return 0;
+}
+
+
+static ssize_t xlat_load_balance(void *instance, REQUEST *request,
+			      char const *fmt, char *out, size_t outlen)
+{
+	uint32_t count = 0;
+	xlat_redundant_t *xr = instance;
+	CONF_ITEM *ci;
+	CONF_ITEM *found = NULL;
+	char const *name;
+	xlat_t *xlat;
+
+	/*
+	 *	Choose a child at random.
+	 */
+	for (ci = cf_item_find_next(xr->cs, NULL);
+	     ci != NULL;
+	     ci = cf_item_find_next(xr->cs, ci)) {
+		if (!cf_item_is_pair(ci)) continue;
+		count++;
+
+		/*
+		 *	Replace the previously found one with a random
+		 *	new one.
+		 */
+		if ((count * (fr_rand() & 0xffff)) < (uint32_t) 0x10000) {
+			found = ci;
+		}
+	}
+
+	/*
+	 *	Plain load balancing: do one child, and only one child.
+	 */
+	if (xr->type == XLAT_LOAD_BALANCE) {
+		name = cf_pair_attr(cf_item_to_pair(found));
+		rad_assert(name != NULL);
+
+		xlat = xlat_find(name);
+		if (!xlat) return -1;
+
+		return xlat->func(xlat->instance, request, fmt, out, outlen);
+	}
+
+	rad_assert(xr->type == XLAT_REDUNDANT_LOAD_BALANCE);
+
+	/*
+	 *	Try the random one we found.  If it fails, keep going
+	 *	through the rest of the children.
+	 */
+	ci = found;
+	do {
+		name = cf_pair_attr(cf_item_to_pair(ci));
+		rad_assert(name != NULL);
+
+		xlat = xlat_find(name);
+		if (xlat) {
+			ssize_t rcode;
+
+			rcode = xlat->func(xlat->instance, request, fmt, out, outlen);
+			if (rcode > 0) return rcode;
+		}
+
+		/*
+		 *	Go to the next one, wrapping around at the end.
+		 */
+		ci = cf_item_find_next(xr->cs, ci);
+		if (!ci) ci = cf_item_find_next(xr->cs, NULL);
+	} while (ci != found);
+
+	return -1;
+}
+
+
+bool xlat_register_redundant(CONF_SECTION *cs)
+{
+	char const *name1, *name2;
+	xlat_redundant_t *xr;
+
+	name1 = cf_section_name1(cs);
+	name2 = cf_section_name2(cs);
+
+	if (!name2) return false;
+
+	if (xlat_find(name2)) {
+		cf_log_err_cs(cs, "An expansion is already registered for this name");
+		return false;
+	}
+
+	xr = talloc_zero(cs, xlat_redundant_t);
+	if (!xr) return false;
+
+	if (strcmp(name1, "redundant") == 0) {
+		xr->type = XLAT_REDUNDANT;
+
+	} else if (strcmp(name1, "redundant-load-balance") == 0) {
+		xr->type = XLAT_REDUNDANT_LOAD_BALANCE;
+
+	} else if (strcmp(name1, "load-balance") == 0) {
+		xr->type = XLAT_LOAD_BALANCE;
+
+	} else {
+		return false;
+	}
+
+	xr->cs = cs;
+
+	/*
+	 *	Get the number of children for load balancing.
+	 */
+	if (xr->type == XLAT_REDUNDANT) {
+		if (xlat_register(name2, xlat_redundant, NULL, xr) < 0) {
+			talloc_free(xr);
+			return false;
+		}
+
+	} else {
+		CONF_ITEM *ci;
+
+		for (ci = cf_item_find_next(cs, NULL);
+		     ci != NULL;
+		     ci = cf_item_find_next(cs, ci)) {
+			if (!cf_item_is_pair(ci)) continue;
+
+			if (!xlat_find(cf_pair_attr(cf_item_to_pair(ci)))) {
+				talloc_free(xr);
+				return false;
+			}
+
+			xr->count++;
+		}
+
+		if (xlat_register(name2, xlat_load_balance, NULL, xr) < 0) {
+			talloc_free(xr);
+			return false;
+		}
+	}
+
+	return true;
+}
+
+
+/** Crappy temporary function to add attribute ref support to xlats
+ *
+ * This needs to die, and hopefully will die, when xlat functions accept
+ * xlat node structures.
+ *
+ * Provides either a pointer to a buffer which contains the value of the reference VALUE_PAIR
+ * in an architecture independent format. Or a pointer to the start of the fmt string.
+ *
+ * The pointer is only guaranteed to be valid between calls to xlat_fmt_to_ref,
+ * and so long as the source VALUE_PAIR is not freed.
+ *
+ * @param out where to write a pointer to the buffer to the data the xlat function needs to work on.
+ * @param request current request.
+ * @param fmt string.
+ * @returns the length of the data or -1 on error.
+ */
+ssize_t xlat_fmt_to_ref(uint8_t const **out, REQUEST *request, char const *fmt)
+{
+	VALUE_PAIR *vp;
+
+	while (isspace((uint8_t) *fmt)) fmt++;
+
+	if (fmt[0] == '&') {
+		if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) {
+			*out = NULL;
+			return -1;
+		}
+
+		return rad_vp2data(out, vp);
+	}
+
+	*out = (uint8_t const *)fmt;
+	return strlen(fmt);
+}
+
+/** De-register all xlat functions, used mainly for debugging.
+ *
+ */
+void xlat_free(void)
+{
+	rbtree_free(xlat_root);
+}
+
+#ifdef DEBUG_XLAT
+#  define XLAT_DEBUG DEBUG3
+#else
+#  define XLAT_DEBUG(...)
+#endif
+
+static ssize_t xlat_tokenize_expansion(TALLOC_CTX *ctx, char *fmt, xlat_exp_t **head,
+				       char const **error);
+static ssize_t xlat_tokenize_literal(TALLOC_CTX *ctx, char *fmt, xlat_exp_t **head,
+				     bool brace, char const **error);
+static size_t xlat_process(char **out, REQUEST *request, xlat_exp_t const * const head,
+			   xlat_escape_t escape, void *escape_ctx);
+
+static ssize_t xlat_tokenize_alternation(TALLOC_CTX *ctx, char *fmt, xlat_exp_t **head,
+					 char const **error)
+{
+	ssize_t slen;
+	char *p;
+	xlat_exp_t *node;
+
+	rad_assert(fmt[0] == '%');
+	rad_assert(fmt[1] == '{');
+	rad_assert(fmt[2] == '%');
+	rad_assert(fmt[3] == '{');
+
+	XLAT_DEBUG("ALTERNATE <-- %s", fmt);
+
+	node = talloc_zero(ctx, xlat_exp_t);
+	node->type = XLAT_ALTERNATE;
+
+	p = fmt + 2;
+	slen = xlat_tokenize_expansion(node, p, &node->child, error);
+	if (slen <= 0) {
+		talloc_free(node);
+		return slen - (p - fmt);
+	}
+	p += slen;
+
+	if (p[0] != ':') {
+		talloc_free(node);
+		*error = "Expected ':' after first expansion";
+		return -(p - fmt);
+	}
+	p++;
+
+	if (p[0] != '-') {
+		talloc_free(node);
+		*error = "Expected '-' after ':'";
+		return -(p - fmt);
+	}
+	p++;
+
+	/*
+	 *	Allow the RHS to be empty as a special case.
+	 */
+	if (*p == '}') {
+		/*
+		 *	Hack up an empty string.
+		 */
+		node->alternate = talloc_zero(node, xlat_exp_t);
+		node->alternate->type = XLAT_LITERAL;
+		node->alternate->fmt = talloc_typed_strdup(node->alternate, "");
+		*(p++) = '\0';
+
+	} else {
+		slen = xlat_tokenize_literal(node, p,  &node->alternate, true, error);
+		if (slen <= 0) {
+			talloc_free(node);
+			return slen - (p - fmt);
+		}
+
+		if (!node->alternate) {
+			talloc_free(node);
+			*error = "Empty expansion is invalid";
+			return -(p - fmt);
+		}
+		p += slen;
+	}
+
+	*head = node;
+	return p - fmt;
+}
+
+static ssize_t xlat_tokenize_expansion(TALLOC_CTX *ctx, char *fmt, xlat_exp_t **head,
+				       char const **error)
+{
+	ssize_t slen;
+	char *p, *q;
+	xlat_exp_t *node;
+	long num;
+
+	rad_assert(fmt[0] == '%');
+	rad_assert(fmt[1] == '{');
+
+	/*
+	 *	%{%{...}:-bar}
+	 */
+	if ((fmt[2] == '%') && (fmt[3] == '{')) return xlat_tokenize_alternation(ctx, fmt, head, error);
+
+	XLAT_DEBUG("EXPANSION <-- %s", fmt);
+	node = talloc_zero(ctx, xlat_exp_t);
+	node->fmt = fmt + 2;
+	node->len = 0;
+
+#ifdef HAVE_REGEX
+	/*
+	 *	Handle regex's specially.
+	 */
+	p = fmt + 2;
+	num = strtol(p, &q, 10);
+	if (p != q && (*q == '}')) {
+		XLAT_DEBUG("REGEX <-- %s", fmt);
+		*q = '\0';
+
+		if ((num > REQUEST_MAX_REGEX) || (num < 0)) {
+			talloc_free(node);
+			*error = "Invalid regex reference.  Must be in range 0-" STRINGIFY(REQUEST_MAX_REGEX);
+			return -2;
+		}
+		node->attr.tmpl_num = num;
+
+		node->type = XLAT_REGEX;
+		*head = node;
+
+		return (q - fmt) + 1;
+	}
+#endif /* HAVE_REGEX */
+
+	/*
+	 *	%{Attr-Name}
+	 *	%{Attr-Name[#]}
+	 *	%{Tunnel-Password:1}
+	 *	%{Tunnel-Password:1[#]}
+	 *	%{request:Attr-Name}
+	 *	%{request:Tunnel-Password:1}
+	 *	%{request:Tunnel-Password:1[#]}
+	 *	%{mod:foo}
+	 */
+
+	/*
+	 *	This is for efficiency, so we don't search for an xlat,
+	 *	when what's being referenced is obviously an attribute.
+	 */
+	p = fmt + 2;
+	for (q = p; *q != '\0'; q++) {
+		if (*q == ':') break;
+
+		if (isspace((uint8_t) *q)) break;
+
+		if (*q == '[') continue;
+
+		if (*q == '}') break;
+	}
+
+	/*
+	 *	Check for empty expressions %{}
+	 */
+	if ((*q == '}') && (q == p)) {
+		talloc_free(node);
+		*error = "Empty expression is invalid";
+		return -(p - fmt);
+	}
+
+	/*
+	 *	Might be a module name reference.
+	 *
+	 *	If it's not, it's an attribute or parse error.
+	 */
+	if (*q == ':') {
+		*q = '\0';
+		node->xlat = xlat_find(node->fmt);
+		if (node->xlat) {
+			/*
+			 *	%{mod:foo}
+			 */
+			node->type = XLAT_MODULE;
+
+			p = q + 1;
+			XLAT_DEBUG("MOD <-- %s ... %s", node->fmt, p);
+
+			slen = xlat_tokenize_literal(node, p, &node->child, true, error);
+			if (slen < 0) {
+				talloc_free(node);
+				return slen - (p - fmt);
+			}
+			p += slen;
+
+			*head = node;
+			rad_assert(node->next == NULL);
+
+			return p - fmt;
+		}
+		*q = ':';	/* Avoids a strdup */
+	}
+
+	/*
+	 *	The first token ends with:
+	 *	- '[' - Which is an attribute index, so it must be an attribute.
+	 *      - '}' - The end of the expansion, which means it was a bareword.
+	 */
+	slen = tmpl_from_attr_substr(&node->attr, p, REQUEST_CURRENT, PAIR_LIST_REQUEST, true, true);
+	if (slen <= 0) {
+		/*
+		 *	If the parse error occurred before the ':'
+		 *	then the error is changed to 'Unknown module',
+		 *	as it was more likely to be a bad module name,
+		 *	than a request qualifier.
+		 */
+		if ((*q == ':') && ((p + (slen * -1)) < q)) {
+			*error = "Unknown module";
+		} else {
+			*error = fr_strerror();
+		}
+
+		talloc_free(node);
+		return slen - (p - fmt);
+	}
+
+	/*
+	 *	Might be a virtual XLAT attribute
+	 */
+	if (node->attr.type == TMPL_TYPE_ATTR_UNDEFINED) {
+		node->xlat = xlat_find(node->attr.tmpl_unknown_name);
+		if (node->xlat && node->xlat->instance && !node->xlat->internal) {
+			talloc_free(node);
+			*error = "Missing content in expansion";
+			return -(p - fmt) - slen;
+		}
+
+		if (node->xlat) {
+			node->type = XLAT_VIRTUAL;
+			node->fmt = node->attr.tmpl_unknown_name;
+
+			XLAT_DEBUG("VIRTUAL <-- %s", node->fmt);
+			*head = node;
+			rad_assert(node->next == NULL);
+			q++;
+			return q - fmt;
+		}
+
+		talloc_free(node);
+		*error = "Unknown attribute";
+		return -(p - fmt);
+	}
+
+	/*
+	 *	Might be a list, too...
+	 */
+	node->type = XLAT_ATTRIBUTE;
+	p += slen;
+
+	if (*p != '}') {
+		talloc_free(node);
+		*error = "No matching closing brace";
+		return -1;	/* second character of format string */
+	}
+	*p++ = '\0';
+	*head = node;
+	rad_assert(node->next == NULL);
+
+	return p - fmt;
+}
+
+
+static ssize_t xlat_tokenize_literal(TALLOC_CTX *ctx, char *fmt, xlat_exp_t **head,
+				     bool brace, char const **error)
+{
+	char *p;
+	xlat_exp_t *node;
+
+	if (!*fmt) return 0;
+
+	XLAT_DEBUG("LITERAL <-- %s", fmt);
+
+	node = talloc_zero(ctx, xlat_exp_t);
+	node->fmt = fmt;
+	node->len = 0;
+	node->type = XLAT_LITERAL;
+
+	p = fmt;
+
+	while (*p) {
+		if (*p == '\\') {
+			if (!p[1]) {
+				talloc_free(node);
+				*error = "Invalid escape at end of string";
+				return -(p - fmt);
+			}
+
+			p += 2;
+			node->len += 2;
+			continue;
+		}
+
+		/*
+		 *	Process the expansion.
+		 */
+		if ((p[0] == '%') && (p[1] == '{')) {
+			ssize_t slen;
+
+			XLAT_DEBUG("EXPANSION-2 <-- %s", node->fmt);
+
+			slen = xlat_tokenize_expansion(node, p, &node->next, error);
+			if (slen <= 0) {
+				talloc_free(node);
+				return slen - (p - fmt);
+			}
+			*p = '\0'; /* end the literal */
+			p += slen;
+
+			rad_assert(node->next != NULL);
+
+			/*
+			 *	Short-circuit the recursive call.
+			 *	This saves another function call and
+			 *	memory allocation.
+			 */
+			if (!*p) break;
+
+			/*
+			 *	"foo %{User-Name} bar"
+			 *	LITERAL		"foo "
+			 *	EXPANSION	User-Name
+			 *	LITERAL		" bar"
+			 */
+			slen = xlat_tokenize_literal(node->next, p, &(node->next->next), brace, error);
+			rad_assert(slen != 0);
+			if (slen < 0) {
+				talloc_free(node);
+				return slen - (p - fmt);
+			}
+
+			brace = false; /* it was found above, or else the above code errored out */
+			p += slen;
+			break;	/* stop processing the string */
+		}
+
+		/*
+		 *	Check for valid single-character expansions.
+		 */
+		if (p[0] == '%') {
+			ssize_t slen;
+			xlat_exp_t *next;
+
+			if (!p[1] || !strchr("%}cdelmntCDGHIMSTYv", p[1])) {
+				talloc_free(node);
+				*error = "Invalid variable expansion";
+				p++;
+				return - (p - fmt);
+			}
+
+			next = talloc_zero(node, xlat_exp_t);
+			next->len = 1;
+
+			switch (p[1]) {
+			case '%':
+			case '}':
+				next->fmt = talloc_strndup(next, p + 1, 1);
+
+				XLAT_DEBUG("LITERAL-ESCAPED <-- %s", next->fmt);
+				next->type = XLAT_LITERAL;
+				break;
+
+			default:
+				next->fmt = p + 1;
+
+				XLAT_DEBUG("PERCENT <-- %c", *next->fmt);
+				next->type = XLAT_PERCENT;
+				break;
+			}
+
+			node->next = next;
+			*p = '\0';
+			p += 2;
+
+			if (!*p) break;
+
+			/*
+			 *	And recurse.
+			 */
+			slen = xlat_tokenize_literal(node->next, p, &(node->next->next), brace, error);
+			rad_assert(slen != 0);
+			if (slen < 0) {
+				talloc_free(node);
+				return slen - (p - fmt);
+			}
+
+			brace = false; /* it was found above, or else the above code errored out */
+			p += slen;
+			break;	/* stop processing the string */
+		}
+
+		/*
+		 *	If required, eat the brace.
+		 */
+		if (brace && (*p == '}')) {
+			brace = false;
+			*p = '\0';
+			p++;
+			break;
+		}
+
+		p++;
+		node->len++;
+	}
+
+	/*
+	 *	We were told to look for a brace, but we ran off of
+	 *	the end of the string before we found one.
+	 */
+	if (brace) {
+		*error = "Missing closing brace at end of string";
+		return -(p - fmt);
+	}
+
+	/*
+	 *	Squash zero-width literals
+	 */
+	if (node->len > 0) {
+		*head = node;
+
+	} else {
+		(void) talloc_steal(ctx, node->next);
+		*head = node->next;
+		talloc_free(node);
+	}
+
+	return p - fmt;
+}
+
+
+static char const xlat_tabs[] = "																																																																																																																																";
+
+static void xlat_tokenize_debug(xlat_exp_t const *node, int lvl)
+{
+	rad_assert(node != NULL);
+
+	if (lvl >= (int) sizeof(xlat_tabs)) lvl = sizeof(xlat_tabs);
+
+	while (node) {
+		switch (node->type) {
+		case XLAT_LITERAL:
+			DEBUG("%.*sliteral --> %s", lvl, xlat_tabs, node->fmt);
+			break;
+
+		case XLAT_PERCENT:
+			DEBUG("%.*spercent --> %c", lvl, xlat_tabs, node->fmt[0]);
+			break;
+
+		case XLAT_ATTRIBUTE:
+			rad_assert(node->attr.tmpl_da != NULL);
+			DEBUG("%.*sattribute --> %s", lvl, xlat_tabs, node->attr.tmpl_da->name);
+			rad_assert(node->child == NULL);
+			if ((node->attr.tmpl_tag != TAG_ANY) || (node->attr.tmpl_num != NUM_ANY)) {
+				DEBUG("%.*s{", lvl, xlat_tabs);
+
+				DEBUG("%.*sref  %d", lvl + 1, xlat_tabs, node->attr.tmpl_request);
+				DEBUG("%.*slist %d", lvl + 1, xlat_tabs, node->attr.tmpl_list);
+
+				if (node->attr.tmpl_tag != TAG_ANY) {
+					DEBUG("%.*stag %d", lvl + 1, xlat_tabs, node->attr.tmpl_tag);
+				}
+				if (node->attr.tmpl_num != NUM_ANY) {
+					if (node->attr.tmpl_num == NUM_COUNT) {
+						DEBUG("%.*s[#]", lvl + 1, xlat_tabs);
+					} else if (node->attr.tmpl_num == NUM_ALL) {
+						DEBUG("%.*s[*]", lvl + 1, xlat_tabs);
+					} else {
+						DEBUG("%.*s[%d]", lvl + 1, xlat_tabs, node->attr.tmpl_num);
+					}
+				}
+
+				DEBUG("%.*s}", lvl, xlat_tabs);
+			}
+			break;
+
+		case XLAT_VIRTUAL:
+			rad_assert(node->fmt != NULL);
+			DEBUG("%.*svirtual --> %s", lvl, xlat_tabs, node->fmt);
+			break;
+
+		case XLAT_MODULE:
+			rad_assert(node->xlat != NULL);
+			DEBUG("%.*sxlat --> %s", lvl, xlat_tabs, node->xlat->name);
+			if (node->child) {
+				DEBUG("%.*s{", lvl, xlat_tabs);
+				xlat_tokenize_debug(node->child, lvl + 1);
+				DEBUG("%.*s}", lvl, xlat_tabs);
+			}
+			break;
+
+#ifdef HAVE_REGEX
+		case XLAT_REGEX:
+			DEBUG("%.*sregex-var --> %d", lvl, xlat_tabs, node->attr.tmpl_num);
+			break;
+#endif
+
+		case XLAT_ALTERNATE:
+			DEBUG("%.*sXLAT-IF {", lvl, xlat_tabs);
+			xlat_tokenize_debug(node->child, lvl + 1);
+			DEBUG("%.*s}", lvl, xlat_tabs);
+			DEBUG("%.*sXLAT-ELSE {", lvl, xlat_tabs);
+			xlat_tokenize_debug(node->alternate, lvl + 1);
+			DEBUG("%.*s}", lvl, xlat_tabs);
+			break;
+		}
+		node = node->next;
+	}
+}
+
+size_t xlat_sprint(char *buffer, size_t bufsize, xlat_exp_t const *node)
+{
+	size_t len;
+	char *p, *end;
+
+	if (!node) {
+		*buffer = '\0';
+		return 0;
+	}
+
+	p = buffer;
+	end = buffer + bufsize;
+
+	while (node) {
+		switch (node->type) {
+		case XLAT_LITERAL:
+			strlcpy(p, node->fmt, end - p);
+			p += strlen(p);
+			break;
+
+		case XLAT_PERCENT:
+			p[0] = '%';
+			p[1] = node->fmt[0];
+			p += 2;
+			break;
+
+		case XLAT_ATTRIBUTE:
+			*(p++) = '%';
+			*(p++) = '{';
+
+			/*
+			 *	The node MAY NOT be an attribute.  It
+			 *	may be a list.
+			 */
+			tmpl_prints(p, end - p, &node->attr, NULL);
+			if (*p == '&') {
+				memmove(p, p + 1, strlen(p + 1) + 1);
+			}
+			p += strlen(p);
+			*(p++) = '}';
+			break;
+#ifdef HAVE_REGEX
+		case XLAT_REGEX:
+			snprintf(p, end - p, "%%{%i}", node->attr.tmpl_num);
+			p += strlen(p);
+			break;
+#endif
+		case XLAT_VIRTUAL:
+			*(p++) = '%';
+			*(p++) = '{';
+			strlcpy(p, node->fmt, end - p);
+			p += strlen(p);
+			*(p++) = '}';
+			break;
+
+		case XLAT_MODULE:
+			*(p++) = '%';
+			*(p++) = '{';
+			strlcpy(p, node->xlat->name, end - p);
+			p += strlen(p);
+			*(p++) = ':';
+			rad_assert(node->child != NULL);
+			len = xlat_sprint(p, end - p, node->child);
+			p += len;
+			*(p++) = '}';
+			break;
+
+		case XLAT_ALTERNATE:
+			*(p++) = '%';
+			*(p++) = '{';
+
+			len = xlat_sprint(p, end - p, node->child);
+			p += len;
+
+			*(p++) = ':';
+			*(p++) = '-';
+
+			len = xlat_sprint(p, end - p, node->alternate);
+			p += len;
+
+			*(p++) = '}';
+			break;
+		}
+
+
+		if (p == end) break;
+
+		node = node->next;
+	}
+
+	*p = '\0';
+
+	return p - buffer;
+}
+
+ssize_t xlat_tokenize(TALLOC_CTX *ctx, char *fmt, xlat_exp_t **head,
+		      char const **error)
+{
+	return xlat_tokenize_literal(ctx, fmt, head, false, error);
+}
+
+
+/** Tokenize an xlat expansion
+ *
+ * @param[in] request the input request.  Memory will be attached here.
+ * @param[in] fmt the format string to expand
+ * @param[out] head the head of the xlat list / tree structure.
+ */
+static ssize_t xlat_tokenize_request(REQUEST *request, char const *fmt, xlat_exp_t **head)
+{
+	ssize_t slen;
+	char *tokens;
+	char const *error = NULL;
+
+	*head = NULL;
+
+	/*
+	 *	Copy the original format string to a buffer so that
+	 *	the later functions can mangle it in-place, which is
+	 *	much faster.
+	 */
+	tokens = talloc_typed_strdup(request, fmt);
+	if (!tokens) {
+		error = "Out of memory";
+		return -1;
+	}
+
+	slen = xlat_tokenize_literal(request, tokens, head, false, &error);
+
+	/*
+	 *	Zero length expansion, return a zero length node.
+	 */
+	if (slen == 0) {
+		*head = talloc_zero(request, xlat_exp_t);
+	}
+
+	/*
+	 *	Output something like:
+	 *
+	 *	"format string"
+	 *	"       ^ error was here"
+	 */
+	if (slen < 0) {
+		talloc_free(tokens);
+
+		if (!error) error = "Unknown error";
+
+		REMARKER(fmt, -slen, error);
+		return slen;
+	}
+
+	if (*head && (rad_debug_lvl > 2)) {
+		DEBUG("%s", fmt);
+		DEBUG("Parsed xlat tree:");
+		xlat_tokenize_debug(*head, 0);
+	}
+
+	/*
+	 *	All of the nodes point to offsets in the "tokens"
+	 *	string.  Let's ensure that free'ing head will free
+	 *	"tokens", too.
+	 */
+	(void) talloc_steal(*head, tokens);
+
+	return slen;
+}
+
+
+static char *xlat_getvp(TALLOC_CTX *ctx, REQUEST *request, vp_tmpl_t const *vpt,
+			bool escape, bool return_null, char const *concat)
+{
+	VALUE_PAIR *vp = NULL, *virtual = NULL;
+	RADIUS_PACKET *packet = NULL;
+	DICT_VALUE *dv;
+	char *ret = NULL;
+
+	vp_cursor_t cursor;
+	char quote = escape ? '"' : '\0';
+
+	rad_assert((vpt->type == TMPL_TYPE_ATTR) || (vpt->type == TMPL_TYPE_LIST));
+
+	/*
+	 *	We only support count and concatenate operations on lists.
+	 */
+	if (vpt->type == TMPL_TYPE_LIST) {
+		vp = tmpl_cursor_init(NULL, &cursor, request, vpt);
+		goto do_print;
+	}
+
+	/*
+	 *	See if we're dealing with an attribute in the request
+	 *
+	 *	This allows users to manipulate virtual attributes as if
+	 *	they were real ones.
+	 */
+	vp = tmpl_cursor_init(NULL, &cursor, request, vpt);
+	if (vp) goto do_print;
+
+	/*
+	 *	We didn't find the VP in a list.
+	 *	If it's not a virtual one, and we're not meant to
+	 *	be counting it, return.
+	 */
+	if (!vpt->tmpl_da->flags.virtual) {
+		if (vpt->tmpl_num == NUM_COUNT) goto do_print;
+		return NULL;
+	}
+
+	/*
+	 *	Switch out the request to the one specified by the template
+	 */
+	if (radius_request(&request, vpt->tmpl_request) < 0) return NULL;
+
+	/*
+	 *	Some non-packet expansions
+	 */
+	switch (vpt->tmpl_da->attr) {
+	default:
+		break;		/* ignore them */
+
+	case PW_CLIENT_SHORTNAME:
+		if (vpt->tmpl_num == NUM_COUNT) goto count_virtual;
+		if (request->client && request->client->shortname) {
+			return talloc_typed_strdup(ctx, request->client->shortname);
+		}
+		return talloc_typed_strdup(ctx, "<UNKNOWN-CLIENT>");
+
+	case PW_REQUEST_PROCESSING_STAGE:
+		if (vpt->tmpl_num == NUM_COUNT) goto count_virtual;
+		if (request->component) {
+			return talloc_typed_strdup(ctx, request->component);
+		}
+		return talloc_typed_strdup(ctx, "server_core");
+
+	case PW_VIRTUAL_SERVER:
+		if (vpt->tmpl_num == NUM_COUNT) goto count_virtual;
+		if (!request->server) return NULL;
+		return talloc_typed_strdup(ctx, request->server);
+
+	case PW_MODULE_RETURN_CODE:
+		if (vpt->tmpl_num == NUM_COUNT) goto count_virtual;
+		if (!request->rcode) return NULL;
+		return talloc_typed_strdup(ctx, fr_int2str(modreturn_table, request->rcode, ""));
+	}
+
+	/*
+	 *	All of the attributes must now refer to a packet.
+	 *	If there's no packet, we can't print any attribute
+	 *	referencing it.
+	 */
+	packet = radius_packet(request, vpt->tmpl_list);
+	if (!packet) {
+		if (return_null) return NULL;
+		return vp_aprints_type(ctx, vpt->tmpl_da->type);
+	}
+
+	vp = NULL;
+	switch (vpt->tmpl_da->attr) {
+	default:
+		break;
+
+	case PW_PACKET_TYPE:
+		dv = dict_valbyattr(PW_PACKET_TYPE, 0, packet->code);
+		if (dv) return talloc_typed_strdup(ctx, dv->name);
+		return talloc_typed_asprintf(ctx, "%d", packet->code);
+
+	case PW_RESPONSE_PACKET_TYPE:
+	{
+		int code = 0;
+
+#ifdef WITH_PROXY
+		if (request->proxy_reply && (!request->reply || !request->reply->code)) {
+			code = request->proxy_reply->code;
+		} else
+#endif
+			if (request->reply) {
+				code = request->reply->code;
+			}
+
+		if (!code) return NULL;
+
+		if (code >= FR_MAX_PACKET_CODE) {
+			return talloc_typed_asprintf(ctx, "%d", packet->code);
+		}
+
+		return talloc_typed_strdup(ctx, fr_packet_codes[code]);
+	}
+
+	/*
+	 *	Virtual attributes which require a temporary VALUE_PAIR
+	 *	to be allocated. We can't use stack allocated memory
+	 *	because of the talloc checks sprinkled throughout the
+	 *	various VP functions.
+	 */
+	case PW_PACKET_AUTHENTICATION_VECTOR:
+		virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da);
+		fr_pair_value_memcpy(virtual, packet->vector, sizeof(packet->vector));
+		vp = virtual;
+		break;
+
+	case PW_CLIENT_IP_ADDRESS:
+	case PW_PACKET_SRC_IP_ADDRESS:
+		if (packet->src_ipaddr.af == AF_INET) {
+			virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da);
+			virtual->vp_ipaddr = packet->src_ipaddr.ipaddr.ip4addr.s_addr;
+			vp = virtual;
+		}
+		break;
+
+	case PW_PACKET_DST_IP_ADDRESS:
+		if (packet->dst_ipaddr.af == AF_INET) {
+			virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da);
+			virtual->vp_ipaddr = packet->dst_ipaddr.ipaddr.ip4addr.s_addr;
+			vp = virtual;
+		}
+		break;
+
+	case PW_PACKET_SRC_IPV6_ADDRESS:
+		if (packet->src_ipaddr.af == AF_INET6) {
+			virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da);
+			memcpy(&virtual->vp_ipv6addr,
+			       &packet->src_ipaddr.ipaddr.ip6addr,
+			       sizeof(packet->src_ipaddr.ipaddr.ip6addr));
+			vp = virtual;
+		}
+		break;
+
+	case PW_PACKET_DST_IPV6_ADDRESS:
+		if (packet->dst_ipaddr.af == AF_INET6) {
+			virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da);
+			memcpy(&virtual->vp_ipv6addr,
+			       &packet->dst_ipaddr.ipaddr.ip6addr,
+			       sizeof(packet->dst_ipaddr.ipaddr.ip6addr));
+			vp = virtual;
+		}
+		break;
+
+	case PW_PACKET_SRC_PORT:
+		virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da);
+		virtual->vp_integer = packet->src_port;
+		vp = virtual;
+		break;
+
+	case PW_PACKET_DST_PORT:
+		virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da);
+		virtual->vp_integer = packet->dst_port;
+		vp = virtual;
+		break;
+	}
+
+	/*
+	 *	Fake various operations for virtual attributes.
+	 */
+	if (virtual) {
+		if (vpt->tmpl_num != NUM_ANY) switch (vpt->tmpl_num) {
+		/*
+		 *	[n] is NULL (we only have [0])
+		 */
+		default:
+			goto finish;
+		/*
+		 *	[*] means only one.
+		 */
+		case NUM_ALL:
+			break;
+
+		/*
+		 *	[#] means 1 (as there's only one)
+		 */
+		case NUM_COUNT:
+		count_virtual:
+			ret = talloc_strdup(ctx, "1");
+			goto finish;
+
+		/*
+		 *	[0] is fine (get the first instance)
+		 */
+		case 0:
+			break;
+		}
+		goto print;
+	}
+
+do_print:
+	switch (vpt->tmpl_num) {
+	/*
+	 *	Return a count of the VPs.
+	 */
+	case NUM_COUNT:
+	{
+		int count = 0;
+
+		for (vp = tmpl_cursor_init(NULL, &cursor, request, vpt);
+		     vp;
+		     vp = tmpl_cursor_next(&cursor, vpt)) count++;
+
+		return talloc_typed_asprintf(ctx, "%d", count);
+	}
+
+
+	/*
+	 *	Concatenate all values together,
+	 *	separated by commas.
+	 */
+	case NUM_ALL:
+	{
+		char *p, *q;
+
+		if (!fr_cursor_current(&cursor)) return NULL;
+		p = vp_aprints_value(ctx, vp, quote);
+		if (!p) return NULL;
+
+		while ((vp = tmpl_cursor_next(&cursor, vpt)) != NULL) {
+			q = vp_aprints_value(ctx, vp, quote);
+			if (!q) return NULL;
+			p = talloc_strdup_append(p, concat);
+			p = talloc_strdup_append(p, q);
+		}
+
+		return p;
+	}
+
+	default:
+		/*
+		 *	The cursor was set to the correct
+		 *	position above by tmpl_cursor_init.
+		 */
+		vp = fr_cursor_current(&cursor);
+		break;
+	}
+
+	if (!vp) {
+		if (return_null) return NULL;
+		return vp_aprints_type(ctx, vpt->tmpl_da->type);
+	}
+
+print:
+	ret = vp_aprints_value(ctx, vp, quote);
+
+finish:
+	talloc_free(virtual);
+	return ret;
+}
+
+#ifdef DEBUG_XLAT
+static const char xlat_spaces[] = "                                                                                                                                                                                                                                                                ";
+#endif
+
+static char *xlat_aprint(TALLOC_CTX *ctx, REQUEST *request, xlat_exp_t const * const node,
+			 xlat_escape_t escape, void *escape_ctx,
+#ifndef DEBUG_XLAT
+			 UNUSED
+#endif
+			 int lvl)
+{
+	ssize_t rcode;
+	char *str = NULL, *child;
+	char const *p;
+
+	XLAT_DEBUG("%.*sxlat aprint %d %s", lvl, xlat_spaces, node->type, node->fmt);
+
+	switch (node->type) {
+		/*
+		 *	Don't escape this.
+		 */
+	case XLAT_LITERAL:
+		XLAT_DEBUG("%.*sxlat_aprint LITERAL", lvl, xlat_spaces);
+		return talloc_typed_strdup(ctx, node->fmt);
+
+		/*
+		 *	Do a one-character expansion.
+		 */
+	case XLAT_PERCENT:
+	{
+		char *nl;
+		size_t freespace = 256;
+		struct tm ts;
+		time_t when;
+		int usec;
+
+		XLAT_DEBUG("%.*sxlat_aprint PERCENT", lvl, xlat_spaces);
+
+		str = talloc_array(ctx, char, freespace); /* @todo do better allocation */
+		p = node->fmt;
+
+		when = request->timestamp;
+		usec = 0;
+		if (request->packet) {
+			when = request->packet->timestamp.tv_sec;
+			usec = request->packet->timestamp.tv_usec;
+		}
+
+		switch (*p) {
+		case '%':
+			str[0] = '%';
+			str[1] = '\0';
+			break;
+
+		case 'c':	/* current epoch time seconds */
+			snprintf(str, freespace, "%" PRIu64, (uint64_t) time(NULL));
+			break;
+
+		case 'd': /* request day */
+			if (!localtime_r(&when, &ts)) goto error;
+			strftime(str, freespace, "%d", &ts);
+			break;
+
+		case 'e': /* request second */
+			if (!localtime_r(&when, &ts)) goto error;
+
+			snprintf(str, freespace, "%d", ts.tm_sec);
+			break;
+
+		case 'l': /* request timestamp */
+			snprintf(str, freespace, "%lu",
+				 (unsigned long) when);
+			break;
+
+		case 'm': /* request month */
+			if (!localtime_r(&when, &ts)) goto error;
+			strftime(str, freespace, "%m", &ts);
+			break;
+
+		case 'n': /* Request Number*/
+			snprintf(str, freespace, "%u", request->number);
+			break;
+
+		case 't': /* request timestamp */
+			CTIME_R(&when, str, freespace);
+			nl = strchr(str, '\n');
+			if (nl) *nl = '\0';
+			break;
+
+		case 'C':	/* current epoch time microseconds */
+			{
+				struct timeval tv;
+
+				gettimeofday(&tv, NULL);
+
+				snprintf(str, freespace, "%" PRIu64, (uint64_t) tv.tv_usec);
+			}
+			break;
+
+		case 'D': /* request date */
+			if (!localtime_r(&when, &ts)) goto error;
+			strftime(str, freespace, "%Y%m%d", &ts);
+			break;
+
+		case 'G': /* request minute */
+			if (!localtime_r(&when, &ts)) goto error;
+			strftime(str, freespace, "%M", &ts);
+			break;
+
+		case 'H': /* request hour */
+			if (!localtime_r(&when, &ts)) goto error;
+			strftime(str, freespace, "%H", &ts);
+			break;
+
+		case 'I': /* Request ID */
+			if (request->packet) {
+				snprintf(str, freespace, "%i", request->packet->id);
+			}
+			break;
+
+		case 'M': /* request microsecond component */
+			snprintf(str, freespace, "%06u", (unsigned int) usec);
+			break;
+
+		case 'S': /* request timestamp in SQL format*/
+			if (!localtime_r(&when, &ts)) goto error;
+			strftime(str, freespace, "%Y-%m-%d %H:%M:%S", &ts);
+			break;
+
+		case 'T': /* request timestamp */
+			if (!localtime_r(&when, &ts)) goto error;
+			nl = str + strftime(str, freespace, "%Y-%m-%d-%H.%M.%S", &ts);
+			rad_assert(((str + freespace) - nl) >= 8);
+			snprintf(nl, (str + freespace) - nl, ".%06d",  usec);
+			break;
+
+		case 'Y': /* request year */
+			if (!localtime_r(&when, &ts)) {
+				error:
+				REDEBUG("Failed converting packet timestamp to localtime: %s", fr_syserror(errno));
+				talloc_free(str);
+				return NULL;
+			}
+			strftime(str, freespace, "%Y", &ts);
+			break;
+
+		case 'v': /* Version of code */
+			RWDEBUG("%%v is deprecated and will be removed.  Use ${version.freeradius-server}");
+			snprintf(str, freespace, "%s", radiusd_version_short);
+			break;
+
+		default:
+			rad_assert(0 == 1);
+			break;
+		}
+	}
+		break;
+
+	case XLAT_ATTRIBUTE:
+		XLAT_DEBUG("%.*sxlat_aprint ATTRIBUTE", lvl, xlat_spaces);
+
+		/*
+		 *	Some attributes are virtual <sigh>
+		 */
+		str = xlat_getvp(ctx, request, &node->attr, escape ? false : true, true, ",");
+		if (str) {
+			XLAT_DEBUG("%.*sEXPAND attr %s", lvl, xlat_spaces, node->attr.tmpl_da->name);
+			XLAT_DEBUG("%.*s       ---> %s", lvl ,xlat_spaces, str);
+		}
+		break;
+
+	case XLAT_VIRTUAL:
+		XLAT_DEBUG("xlat_aprint VIRTUAL");
+		str = talloc_array(ctx, char, 2048); /* FIXME: have the module call talloc_typed_asprintf */
+		rcode = node->xlat->func(node->xlat->instance, request, NULL, str, 2048);
+		if (rcode < 0) {
+			talloc_free(str);
+			return NULL;
+		}
+		RDEBUG2("EXPAND %s", node->xlat->name);
+		RDEBUG2("   --> %s", str);
+
+		/*
+		 *	Resize the buffer to the correct size.
+		 */
+		if (rcode == 0) {
+			talloc_free(str);
+			str = talloc_strdup(ctx, "");
+		} else if (rcode < 2047) {
+			child = talloc_memdup(ctx, str, rcode + 1);
+			talloc_free(str);
+			str = child;
+		}
+		break;
+
+	case XLAT_MODULE:
+		XLAT_DEBUG("xlat_aprint MODULE");
+
+		if (node->child) {
+			if (xlat_process(&child, request, node->child, node->xlat->escape, node->xlat->instance) == 0) {
+				return NULL;
+			}
+
+			XLAT_DEBUG("%.*sEXPAND mod %s %s", lvl, xlat_spaces, node->fmt, node->child->fmt);
+		} else {
+			XLAT_DEBUG("%.*sEXPAND mod %s", lvl, xlat_spaces, node->fmt);
+			child = talloc_typed_strdup(ctx, "");
+		}
+
+		XLAT_DEBUG("%.*s      ---> %s", lvl, xlat_spaces, child);
+
+		/*
+		 *	Smash \n --> CR.
+		 *
+		 *	The OUTPUT of xlat is a "raw" string.  The INPUT is a printable string.
+		 *
+		 *	This is really the reverse of fr_prints().
+		 */
+		if (cf_new_escape && *child) {
+			ssize_t slen;
+			PW_TYPE type;
+			value_data_t data;
+
+			type = PW_TYPE_STRING;
+			slen = value_data_from_str(request, &data, &type, NULL, child, talloc_array_length(child) - 1, '"');
+			if (slen <= 0) {
+				talloc_free(child);
+				return NULL;
+			}
+
+			talloc_free(child);
+			child = data.ptr;
+
+		} else {
+			char *q;
+
+			p = q = child;
+			while (*p) {
+				if (*p == '\\') switch (p[1]) {
+					default:
+						*(q++) = p[1];
+						p += 2;
+						continue;
+
+					case 'n':
+						*(q++) = '\n';
+						p += 2;
+						continue;
+
+					case 't':
+						*(q++) = '\t';
+						p += 2;
+						continue;
+					}
+
+				*(q++) = *(p++);
+			}
+			*q = '\0';
+		}
+
+		str = talloc_array(ctx, char, 2048); /* FIXME: have the module call talloc_typed_asprintf */
+		*str = '\0';	/* Be sure the string is NULL terminated, we now only free on error */
+
+		rcode = node->xlat->func(node->xlat->instance, request, child, str, 2048);
+		talloc_free(child);
+		if (rcode < 0) {
+			talloc_free(str);
+			return NULL;
+		}
+		break;
+
+#ifdef HAVE_REGEX
+	case XLAT_REGEX:
+		XLAT_DEBUG("%.*sxlat_aprint REGEX", lvl, xlat_spaces);
+		if (regex_request_to_sub(ctx, &str, request, node->attr.tmpl_num) < 0) return NULL;
+
+		break;
+#endif
+
+	case XLAT_ALTERNATE:
+		XLAT_DEBUG("%.*sxlat_aprint ALTERNATE", lvl, xlat_spaces);
+		rad_assert(node->child != NULL);
+		rad_assert(node->alternate != NULL);
+
+		/*
+		 *	Call xlat_process recursively.  The child /
+		 *	alternate nodes may have "next" pointers, and
+		 *	those need to be expanded.
+		 */
+		if (xlat_process(&str, request, node->child, escape, escape_ctx) > 0) {
+			XLAT_DEBUG("%.*sALTERNATE got first string: %s", lvl, xlat_spaces, str);
+		} else {
+			(void) xlat_process(&str, request, node->alternate, escape, escape_ctx);
+			XLAT_DEBUG("%.*sALTERNATE got alternate string %s", lvl, xlat_spaces, str);
+		}
+		break;
+	}
+
+	/*
+	 *	If there's no data, return that, instead of an empty string.
+	 */
+	if (str && !str[0]) {
+		talloc_free(str);
+		return NULL;
+	}
+
+	/*
+	 *	Escape the non-literals we found above.
+	 */
+	if (str && escape) {
+		size_t len;
+		char *escaped;
+
+		len = talloc_array_length(str) * 3;
+
+		escaped = talloc_array(ctx, char, len);
+		escape(request, escaped, len, str, escape_ctx);
+		talloc_free(str);
+		str = escaped;
+	}
+
+	return str;
+}
+
+
+static size_t xlat_process(char **out, REQUEST *request, xlat_exp_t const * const head,
+			   xlat_escape_t escape, void *escape_ctx)
+{
+	int i, list;
+	size_t total;
+	char **array, *answer;
+	xlat_exp_t const *node;
+
+	*out = NULL;
+
+	/*
+	 *	There are no nodes to process, so the result is a zero
+	 *	length string.
+	 */
+	if (!head) {
+		*out = talloc_zero_array(request, char, 1);
+		return 0;
+	}
+
+	/*
+	 *	Hack for speed.  If it's one expansion, just allocate
+	 *	that and return, instead of allocating an intermediary
+	 *	array.
+	 */
+	if (!head->next) {
+		/*
+		 *	Pass the MAIN escape function.  Recursive
+		 *	calls will call node-specific escape
+		 *	functions.
+		 */
+		answer = xlat_aprint(request, request, head, escape, escape_ctx, 0);
+		if (!answer) {
+			*out = talloc_zero_array(request, char, 1);
+			return 0;
+		}
+		*out = answer;
+		return strlen(answer);
+	}
+
+	list = 0;		/* FIXME: calculate this once */
+	for (node = head; node != NULL; node = node->next) {
+		list++;
+	}
+
+	array = talloc_array(request, char *, list);
+	if (!array) return -1;
+
+	for (node = head, i = 0; node != NULL; node = node->next, i++) {
+		array[i] = xlat_aprint(array, request, node, escape, escape_ctx, 0); /* may be NULL */
+	}
+
+	total = 0;
+	for (i = 0; i < list; i++) {
+		if (array[i]) total += strlen(array[i]); /* FIXME: calculate strlen once */
+	}
+
+	if (!total) {
+		talloc_free(array);
+		*out = talloc_zero_array(request, char, 1);
+		return 0;
+	}
+
+	answer = talloc_array(request, char, total + 1);
+
+	total = 0;
+	for (i = 0; i < list; i++) {
+		size_t len;
+
+		if (array[i]) {
+			len = strlen(array[i]);
+			memcpy(answer + total, array[i], len);
+			total += len;
+		}
+	}
+	answer[total] = '\0';
+	talloc_free(array);	/* and child entries */
+
+	*out = answer;
+	return total;
+}
+
+
+/** Replace %whatever in a string.
+ *
+ * See 'doc/configuration/variables.rst' for more information.
+ *
+ * @param[out] out Where to write pointer to output buffer.
+ * @param[in] outlen Size of out.
+ * @param[in] request current request.
+ * @param[in] node the xlat structure to expand
+ * @param[in] escape function to escape final value e.g. SQL quoting.
+ * @param[in] escape_ctx pointer to pass to escape function.
+ * @return length of string written @bug should really have -1 for failure
+ */
+static ssize_t xlat_expand_struct(char **out, size_t outlen, REQUEST *request, xlat_exp_t const *node,
+				  xlat_escape_t escape, void *escape_ctx)
+{
+	char *buff;
+	ssize_t len;
+
+	rad_assert(node != NULL);
+
+	len = xlat_process(&buff, request, node, escape, escape_ctx);
+	if ((len < 0) || !buff) {
+		rad_assert(buff == NULL);
+		if (*out) *out[0] = '\0';
+		return len;
+	}
+
+	len = strlen(buff);
+
+	/*
+	 *	If out doesn't point to an existing buffer
+	 *	copy the pointer to our buffer over.
+	 */
+	if (!*out) {
+		*out = buff;
+		return len;
+	}
+
+	/*
+	 *	Otherwise copy the malloced buffer to the fixed one.
+	 */
+	strlcpy(*out, buff, outlen);
+	talloc_free(buff);
+	return len;
+}
+
+static ssize_t xlat_expand(char **out, size_t outlen, REQUEST *request, char const *fmt,
+			   xlat_escape_t escape, void *escape_ctx) CC_HINT(nonnull (1, 3, 4));
+
+/** Replace %whatever in a string.
+ *
+ * See 'doc/configuration/variables.rst' for more information.
+ *
+ * @param[out] out Where to write pointer to output buffer.
+ * @param[in] outlen Size of out.
+ * @param[in] request current request.
+ * @param[in] fmt string to expand.
+ * @param[in] escape function to escape final value e.g. SQL quoting.
+ * @param[in] escape_ctx pointer to pass to escape function.
+ * @return length of string written @bug should really have -1 for failure
+ */
+static ssize_t xlat_expand(char **out, size_t outlen, REQUEST *request, char const *fmt,
+			   xlat_escape_t escape, void *escape_ctx)
+{
+	ssize_t len;
+	xlat_exp_t *node;
+
+	/*
+	 *	Give better errors than the old code.
+	 */
+	len = xlat_tokenize_request(request, fmt, &node);
+	if (len == 0) {
+		if (*out) {
+			*out[0] = '\0';
+		} else {
+			*out = talloc_zero_array(request, char, 1);
+		}
+		return 0;
+	}
+
+	if (len < 0) {
+		if (*out) *out[0] = '\0';
+		return -1;
+	}
+
+	len = xlat_expand_struct(out, outlen, request, node, escape, escape_ctx);
+	talloc_free(node);
+
+	RDEBUG2("EXPAND %s", fmt);
+	RDEBUG2("   --> %s", *out);
+
+	return len;
+}
+
+/** Try to convert an xlat to a tmpl for efficiency
+ *
+ * @param ctx to allocate new vp_tmpl_t in.
+ * @param node to convert.
+ * @return NULL if unable to convert (not necessarily error), or a new vp_tmpl_t.
+ */
+vp_tmpl_t *xlat_to_tmpl_attr(TALLOC_CTX *ctx, xlat_exp_t *node)
+{
+	vp_tmpl_t *vpt;
+
+	if (node->next || (node->type != XLAT_ATTRIBUTE) || (node->attr.type != TMPL_TYPE_ATTR)) return NULL;
+
+	/*
+	 *   Concat means something completely different as an attribute reference
+	 *   Count isn't implemented.
+	 */
+	if ((node->attr.tmpl_num == NUM_COUNT) || (node->attr.tmpl_num == NUM_ALL)) return NULL;
+
+	vpt = tmpl_alloc(ctx, TMPL_TYPE_ATTR, node->fmt, -1);
+	if (!vpt) return NULL;
+	memcpy(&vpt->data, &node->attr.data, sizeof(vpt->data));
+
+	VERIFY_TMPL(vpt);
+
+	return vpt;
+}
+
+/** Try to convert attr tmpl to an xlat for &attr[*] and artificially constructing expansions
+ *
+ * @param ctx to allocate new xlat_expt_t in.
+ * @param vpt to convert.
+ * @return NULL if unable to convert (not necessarily error), or a new vp_tmpl_t.
+ */
+xlat_exp_t *xlat_from_tmpl_attr(TALLOC_CTX *ctx, vp_tmpl_t *vpt)
+{
+	xlat_exp_t *node;
+
+	if (vpt->type != TMPL_TYPE_ATTR) return NULL;
+
+	node = talloc_zero(ctx, xlat_exp_t);
+	node->type = XLAT_ATTRIBUTE;
+	node->fmt = talloc_bstrndup(node, vpt->name, vpt->len);
+	tmpl_init(&node->attr, TMPL_TYPE_ATTR, node->fmt, talloc_array_length(node->fmt) - 1);
+	memcpy(&node->attr.data, &vpt->data, sizeof(vpt->data));
+
+	return node;
+}
+
+ssize_t radius_xlat(char *out, size_t outlen, REQUEST *request, char const *fmt, xlat_escape_t escape, void *ctx)
+{
+	return xlat_expand(&out, outlen, request, fmt, escape, ctx);
+}
+
+ssize_t radius_xlat_struct(char *out, size_t outlen, REQUEST *request, xlat_exp_t const *xlat, xlat_escape_t escape, void *ctx)
+{
+	return xlat_expand_struct(&out, outlen, request, xlat, escape, ctx);
+}
+
+ssize_t radius_axlat(char **out, REQUEST *request, char const *fmt, xlat_escape_t escape, void *ctx)
+{
+	*out = NULL;
+	return xlat_expand(out, 0, request, fmt, escape, ctx);
+}
+
+ssize_t radius_axlat_struct(char **out, REQUEST *request, xlat_exp_t const *xlat, xlat_escape_t escape, void *ctx)
+{
+	*out = NULL;
+	return xlat_expand_struct(out, 0, request, xlat, escape, ctx);
+}
-- 
cgit v1.2.3