diff extensions/app_radgw/rgw_clients.c @ 254:a857024cb48b

Ported the RADIUS/Diameter translation code from waaad project. Not tested yet. Gateway plugins to come later.
author Sebastien Decugis <sdecugis@nict.go.jp>
date Wed, 14 Apr 2010 18:30:22 +0900
parents
children 5df55136361b
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extensions/app_radgw/rgw_clients.c	Wed Apr 14 18:30:22 2010 +0900
@@ -0,0 +1,610 @@
+/*********************************************************************************************************
+* Software License Agreement (BSD License)                                                               *
+* Author: Sebastien Decugis <sdecugis@nict.go.jp>							 *
+*													 *
+* Copyright (c) 2009, WIDE Project and NICT								 *
+* All rights reserved.											 *
+* 													 *
+* Redistribution and use of this software in source and binary forms, with or without modification, are  *
+* permitted provided that the following conditions are met:						 *
+* 													 *
+* * Redistributions of source code must retain the above 						 *
+*   copyright notice, this list of conditions and the 							 *
+*   following disclaimer.										 *
+*    													 *
+* * Redistributions in binary form must reproduce the above 						 *
+*   copyright notice, this list of conditions and the 							 *
+*   following disclaimer in the documentation and/or other						 *
+*   materials provided with the distribution.								 *
+* 													 *
+* * Neither the name of the WIDE Project or NICT nor the 						 *
+*   names of its contributors may be used to endorse or 						 *
+*   promote products derived from this software without 						 *
+*   specific prior written permission of WIDE Project and 						 *
+*   NICT.												 *
+* 													 *
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED *
+* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A *
+* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR *
+* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 	 *
+* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 	 *
+* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR *
+* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF   *
+* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.								 *
+*********************************************************************************************************/
+
+/* Manage the list of RADIUS clients, along with their shared secrets. */
+
+/* Probably some changes are needed to support RADIUS Proxies */
+
+#include "rgw.h"
+
+/* Ordered lists of clients. The order relationship is a memcmp on the address zone. 
+   For same addresses, the port is compared.
+   The same address cannot be added twice, once with a 0-port and once with another port value.
+ */
+static struct fd_list cli_ip = FD_LIST_INITIALIZER(cli_ip);
+static struct fd_list cli_ip6 = FD_LIST_INITIALIZER(cli_ip6);
+
+/* Mutex to protect the previous lists */
+static pthread_mutex_t cli_mtx = PTHREAD_MUTEX_INITIALIZER;
+
+/* Structure describing one client */
+struct rgw_client {
+	/* Link information in global list */
+	struct fd_list		chain;
+	
+	/* Reference count */
+	int			refcount;
+	
+	/* The address and optional port (alloc'd during configuration file parsing). */
+	union {
+		struct sockaddr		*sa; /* generic pointer */
+		struct sockaddr_in	*sin;
+		struct sockaddr_in6	*sin6;
+	};
+	
+	/* The FQDN, realm, and optional aliases */
+	char 			*fqdn;
+	char			*realm;
+	char			**aliases;
+	size_t			 aliases_nb;
+	
+	/* The secret key data. */
+	struct {
+		unsigned char * data;
+		size_t		len;
+	} 			key;
+	
+	/* information of previous msg received, for duplicate checks. */
+	struct {
+		uint16_t		port;
+		uint8_t			id;
+		struct radius_msg * 	ans; /* to be able to resend a lost answer */
+	} last[2]; /*[0] for auth, [1] for acct. */
+};
+
+
+
+/* create a new rgw_client. the arguments are moved into the structure (to limit malloc & free calls). */
+static int client_create(struct rgw_client ** res, struct sockaddr ** ip_port, unsigned char ** key, size_t keylen )
+{
+	struct rgw_client *tmp = NULL;
+	char buf[255];
+	int ret;
+	
+	/* Search FQDN for the client */
+	ret = getnameinfo( *ip_port, sizeof(struct sockaddr_storage), &buf[0], sizeof(buf), NULL, 0, 0 );
+	if (ret) {
+		TRACE_DEBUG(INFO, "Unable to resolve peer name: %s", gai_strerror(ret));
+		return EINVAL;
+	}
+	
+	/* Create the new object */
+	CHECK_MALLOC( tmp = malloc(sizeof (struct rgw_client)) );
+	memset(tmp, 0, sizeof(struct rgw_client));
+	fd_list_init(&tmp->chain, NULL);
+	
+	/* Copy the fqdn */
+	CHECK_MALLOC( tmp->fqdn = strdup(buf) );
+	/* Find an appropriate realm */
+	tmp->realm = strchr(tmp->fqdn, '.');
+	if (tmp->realm)
+		tmp->realm += 1;
+	if ((!tmp->realm) || (*tmp->realm == '\0')) /* in case the fqdn was "localhost." for example, if it is possible... */
+		tmp->realm = fd_g_config->cnf_diamrlm;
+	
+	/* move the sa info reference */
+	tmp->sa = *ip_port;
+	*ip_port = NULL;
+	
+	/* move the key material */
+	tmp->key.data = *key;
+	tmp->key.len = keylen;
+	*key = NULL;
+	
+	/* Done! */
+	*res = tmp;
+	return 0;
+}
+
+
+/* Decrease refcount on a client; the lock must be held when this function is called. */
+static void client_unlink(struct rgw_client * client)
+{
+	client->refcount -= 1;
+	
+	if (client->refcount <= 0) {
+		int idx;
+		/* to be sure: the refcount should be 0 only when client_fini is called */
+		ASSERT( FD_IS_LIST_EMPTY(&client->chain) );
+		
+		/* Free the data */
+		for (idx = 0; idx < client->aliases_nb; idx++)
+			free(client->aliases[idx]);
+		free(client->aliases);
+		free(client->fqdn);
+		free(client->sa);
+		free(client->key.data);
+		free(client);
+	}
+}
+
+
+/* Macro to avoid duplicting the code in the next function */
+#define client_search_family( _family_ )												\
+		case AF_INET##_family_: {												\
+			struct sockaddr_in##_family_ * sin##_family_ = (struct sockaddr_in##_family_ *)ip_port;				\
+			for (ref = cli_ip##_family_.next; ref != &cli_ip##_family_; ref = ref->next) {					\
+				cmp = memcmp(&sin##_family_->sin##_family_##_addr, 							\
+					     &((struct rgw_client *)ref)->sin##_family_->sin##_family_##_addr, 				\
+					     sizeof(struct in##_family_##_addr));							\
+				if (cmp > 0) continue; /* search further in the list */							\
+				if (cmp < 0) break; /* this IP is not in the list */							\
+				/* Now compare the ports as follow: */									\
+				     /* If the ip_port we are searching does not contain a port, just return the first match result */	\
+				if ( (sin##_family_->sin##_family_##_port == 0) 							\
+				     /* If the entry in the list does not contain a port, return it as a match */			\
+				  || (((struct rgw_client *)ref)->sin##_family_->sin##_family_##_port == 0) 				\
+				     /* If both ports are equal, it is a match */							\
+				  || (sin##_family_->sin##_family_##_port == 								\
+				  		((struct rgw_client *)ref)->sin##_family_->sin##_family_##_port)) {			\
+					*res = (struct rgw_client *)ref;								\
+					return EEXIST;											\
+				}													\
+				/* Otherwise, the list is ordered by port value (byte order does not matter */				\
+				if (sin##_family_->sin##_family_##_port 								\
+					> ((struct rgw_client *)ref)->sin##_family_->sin##_family_##_port) continue;			\
+				else break;												\
+			}														\
+			*res = (struct rgw_client *)(ref->prev);									\
+			return ENOENT;													\
+		}
+/* Function to look for an existing rgw_client, or the previous element. 
+   The cli_mtx must be held when calling this function. 
+   Returns ENOENT if the matching client does not exist, and res points to the previous element in the list. 
+   Returns EEXIST if the matching client is found, and res points to this element. 
+   Returns other error code on other error. */
+static int client_search(struct rgw_client ** res, struct sockaddr * ip_port )
+{
+	int ret = 0;
+	int cmp;
+	struct fd_list *ref = NULL;
+	
+	CHECK_PARAMS(res && ip_port);
+	
+	switch (ip_port->sa_family) {
+		client_search_family()
+				break;
+		
+		client_search_family( 6 )
+				break;
+	}
+	
+	/* We're never supposed to reach this point */
+	ASSERT(0);
+	return EINVAL;
+}
+
+int rgw_clients_getkey(struct rgw_client * cli, unsigned char **key, size_t *key_len)
+{
+	CHECK_PARAMS( cli && key && key_len );
+	*key = cli->key.data;
+	*key_len = cli->key.len;
+	return 0;
+}
+
+int rgw_clients_search(struct sockaddr * ip_port, struct rgw_client ** ref)
+{
+	int ret = 0;
+	
+	TRACE_ENTRY("%p %p", ip_port, ref);
+	
+	CHECK_PARAMS(ip_port && ref);
+	
+	CHECK_POSIX( pthread_mutex_lock(&cli_mtx) );
+
+	ret = client_search(ref, ip_port);
+	if (ret == EEXIST) {
+		(*ref)->refcount ++;
+		ret = 0;
+	} else {
+		*ref = NULL;
+	}
+	
+	CHECK_POSIX( pthread_mutex_unlock(&cli_mtx) );
+	
+	return ret;
+}
+
+int rgw_clients_check_dup(struct rgw_radius_msg_meta **msg, struct rgw_client *cli)
+{
+	int idx;
+	
+	TRACE_ENTRY("%p %p", msg, cli);
+	
+	CHECK_PARAMS( msg && cli );
+	
+	if ((*msg)->serv_type == RGW_PLG_TYPE_AUTH)
+		idx = 0;
+	else
+		idx = 1;
+	
+	if ((cli->last[idx].id == (*msg)->radius.hdr->identifier) && (cli->last[idx].port == (*msg)->port)) {
+		/* Duplicate! */
+		TRACE_DEBUG(INFO, "Received duplicated RADIUS message (id: %02hhx, port: %hu).", (*msg)->radius.hdr->identifier, ntohs((*msg)->port));
+		if (cli->last[idx].ans) {
+			/* Resend the answer */
+			CHECK_FCT_DO( rgw_servers_send((*msg)->serv_type, cli->last[idx].ans->buf, cli->last[idx].ans->buf_used, cli->sa, (*msg)->port),  );
+		}
+		rgw_msg_free(msg);
+	} else {
+		/* Update information for new message */
+		if (cli->last[idx].ans) {
+			/* Free it */
+			radius_msg_free(cli->last[idx].ans);
+			free(cli->last[idx].ans);
+			cli->last[idx].ans = NULL;
+		}
+		cli->last[idx].id = (*msg)->radius.hdr->identifier;
+		cli->last[idx].port = (*msg)->port;
+	}
+	
+	return 0;
+}
+
+/* Check that the NAS-IP-Adress or NAS-Identifier is coherent with the IP the packet was received from */
+/* Also update the client list of aliases if needed */
+/* NOTE: This function will require changes to allow RADIUS Proxy on the path... */
+int rgw_clients_check_origin(struct rgw_radius_msg_meta *msg, struct rgw_client *cli)
+{
+	int idx;
+	struct radius_attr_hdr *nas_ip = NULL, *nas_ip6 = NULL, *nas_id = NULL;
+	
+	TRACE_ENTRY("%p %p", msg, cli);
+	CHECK_PARAMS(msg && cli && !msg->valid_nas_info );
+		
+	/* Find the relevant attributes, if any */
+	for (idx = 0; idx < msg->radius.attr_used; idx++) {
+		struct radius_attr_hdr * attr = (struct radius_attr_hdr *)(msg->radius.buf + msg->radius.attr_pos[idx]);
+		unsigned char * attr_val = (unsigned char *)(attr + 1);
+		size_t attr_len = attr->length - sizeof(struct radius_attr_hdr);
+		
+		if ((attr->type == RADIUS_ATTR_NAS_IP_ADDRESS) && (attr_len = 4)) {
+			nas_ip = attr;
+			continue;
+		}
+			
+		if ((attr->type == RADIUS_ATTR_NAS_IDENTIFIER) && (attr_len > 0)) {
+			nas_id = attr;
+			continue;
+		}
+			
+		if ((attr->type == RADIUS_ATTR_NAS_IPV6_ADDRESS) && (attr_len = 16)) {
+			nas_ip6 = attr;
+			continue;
+		}
+	}
+		
+	if (!nas_ip && !nas_ip6 && !nas_id) {
+		TRACE_DEBUG(FULL, "The message does not contain any NAS identification attribute.");
+		goto end;
+	}
+	
+	/* Check if the message was received from the IP in NAS-IP-Address attribute */
+	if (nas_ip && (cli->sa->sa_family == AF_INET) && !memcmp(nas_ip+1, &cli->sin->sin_addr, sizeof(struct in_addr))) {
+		TRACE_DEBUG(FULL, "NAS-IP-Address contains the same address as the message was received from.");
+		msg->valid_nas_info |= 1;
+	}
+	if (nas_ip6 && (cli->sa->sa_family == AF_INET6) && !memcmp(nas_ip6+1, &cli->sin6->sin6_addr, sizeof(struct in6_addr))) {
+		TRACE_DEBUG(FULL, "NAS-IPv6-Address contains the same address as the message was received from.");
+		msg->valid_nas_info |= 1;
+	}
+	
+	/* If these conditions are not met, the message is probably forged (well, this might be false...) */
+	if ((! msg->valid_nas_info) && (nas_ip || nas_ip6)) {
+		/*
+				In RADIUS it would be possible for a rogue NAS to forge the NAS-IP-
+				Address attribute value.  Diameter/RADIUS translation agents MUST
+				check a received NAS-IP-Address or NAS-IPv6-Address attribute against
+				the source address of the RADIUS packet.  If they do not match and
+				the Diameter/RADIUS translation agent does not know whether the
+				packet was sent by a RADIUS proxy or NAS (e.g., no Proxy-State
+				attribute), then by default it is assumed that the source address
+				corresponds to a RADIUS proxy, and that the NAS Address is behind
+				that proxy, potentially with some additional RADIUS proxies in
+				between.  The Diameter/RADIUS translation agent MUST insert entries
+				in the Route-Record AVP corresponding to the apparent route.  This
+				implies doing a reverse lookup on the source address and NAS-IP-
+				Address or NAS-IPv6-Address attributes to determine the corresponding
+				FQDNs.
+
+				If the source address and the NAS-IP-Address or NAS-IPv6-Address do
+				not match, and the Diameter/RADIUS translation agent knows that it is
+				talking directly to the NAS (e.g., there are no RADIUS proxies
+				between it and the NAS), then the error should be logged, and the
+				packet MUST be discarded.
+
+				Diameter agents and servers MUST check whether the NAS-IP-Address AVP
+				corresponds to an entry in the Route-Record AVP.  This is done by
+				doing a reverse lookup (PTR RR) for the NAS-IP-Address to retrieve
+				the corresponding FQDN, and by checking for a match with the Route-
+				Record AVP.  If no match is found, then an error is logged, but no
+				other action is taken.
+		*/
+		TRACE_DEBUG(INFO, "Message received with a NAS-IP-Address or NAS-IPv6-Address different from the sender's. Discarding...");
+		return ENOTSUP;
+	}
+	
+	/* Now check the nas_id */
+	if (nas_id) {
+		/*
+			In RADIUS it would be possible for a rogue NAS to forge the NAS-
+			Identifier attribute.  Diameter/RADIUS translation agents SHOULD
+			attempt to check a received NAS-Identifier attribute against the
+			source address of the RADIUS packet, by doing an A/AAAA RR query.  If
+			the NAS-Identifier attribute contains an FQDN, then such a query
+			would resolve to an IP address matching the source address.  However,
+			the NAS-Identifier attribute is not required to contain an FQDN, so
+			such a query could fail.  If it fails, an error should be logged, but
+			no action should be taken, other than a reverse lookup on the source
+			address and insert the resulting FQDN into the Route-Record AVP.
+
+			Diameter agents and servers SHOULD check whether a NAS-Identifier AVP
+			corresponds to an entry in the Route-Record AVP.  If no match is
+			found, then an error is logged, but no other action is taken.
+		*/
+	
+		/* copy the alias */
+		char * str;
+		int found, ret;
+		struct addrinfo hint, *res;
+		CHECK_MALLOC( str = malloc(nas_id->length - sizeof(struct radius_attr_hdr) + 1) );
+		memcpy(str, nas_id + 1, nas_id->length - sizeof(struct radius_attr_hdr));
+		str[nas_id->length - sizeof(struct radius_attr_hdr)] = '\0';
+		
+		/* Check if this alias is already in the aliases list */
+		if (!strcasecmp(str, cli->fqdn)) {
+			TRACE_DEBUG(FULL, "NAS-Identifier contains the fqdn of the NAS");
+			found = 1;
+		} else {
+			for (idx = 0; idx < cli->aliases_nb; idx++) {
+				if (!strcasecmp(str, cli->aliases[idx])) {
+					TRACE_DEBUG(FULL, "NAS-Identifier valid value found in the cache");
+					found = 1;
+					break;
+				}
+			}
+		}
+		
+		if (found) {
+			free(str);
+			msg->valid_nas_info |= 2;
+			goto end;
+		}
+		
+		/* Now check if this alias is valid for this peer */
+		memset(&hint, 0, sizeof(hint));
+		hint.ai_family = cli->sa->sa_family;
+		hint.ai_flags  = AI_CANONNAME;
+		ret = getaddrinfo(str, NULL, &hint, &res);
+		if (ret) {
+			TRACE_DEBUG(INFO, "Error while resolving NAS-Identifier value '%s': %s. Discarding message...", str, gai_strerror(ret));
+			free(str);
+			return EINVAL;
+		}
+		if (strcasecmp(cli->fqdn, res->ai_canonname)) {
+			TRACE_DEBUG(INFO, "The NAS-Identifier value is not valid: '%s' resolved to '%s', expected '%s'. Discarding...", str, res->ai_canonname, cli->fqdn);
+			free(str);
+			freeaddrinfo(res);
+			return EINVAL;
+		}
+		
+		/* It is a valid alias, save it */
+		freeaddrinfo(res);
+		CHECK_MALLOC( cli->aliases = realloc(cli->aliases, (cli->aliases_nb + 1) * sizeof(char *)) );
+		cli->aliases[cli->aliases_nb + 1] = str;
+		cli->aliases_nb ++;
+		TRACE_DEBUG(FULL, "Saved valid alias for client: '%s' -> '%s'", str, cli->fqdn);
+		msg->valid_nas_info |= 2;
+	}
+end:	
+	return 0;
+}
+
+int rgw_clients_get_origin(struct rgw_client *cli, char **fqdn, char **realm)
+{
+	TRACE_ENTRY("%p %p %p", cli, fqdn, realm);
+	CHECK_PARAMS(cli && fqdn);
+	
+	*fqdn = cli->fqdn;
+	if (realm)
+		*realm= cli->realm;
+	return 0;
+}
+
+
+void rgw_clients_dispose(struct rgw_client ** ref)
+{
+	TRACE_ENTRY("%p", ref);
+	CHECK_PARAMS_DO(ref, return);
+	
+	CHECK_POSIX_DO( pthread_mutex_lock(&cli_mtx),  );
+	client_unlink(*ref);
+	*ref = NULL;
+	CHECK_POSIX_DO( pthread_mutex_unlock(&cli_mtx), );
+}
+
+int rgw_clients_add( struct sockaddr * ip_port, unsigned char ** key, size_t keylen )
+{
+	struct rgw_client * prev = NULL, *new = NULL;
+	int ret;
+	
+	TRACE_ENTRY("%p %p %lu", ip_port, key, keylen);
+	
+	CHECK_PARAMS( ip_port && key && *key && keylen );
+	CHECK_PARAMS( (ip_port->sa_family == AF_INET) || (ip_port->sa_family == AF_INET6) );
+	
+	/* Dump the entry in debug mode */
+	if (TRACE_BOOL(FULL + 1 )) {
+		TRACE_DEBUG(FULL, "Adding client:\n");
+		TRACE_DEBUG_sSA(FULL, 	 "\tIP : ", ip_port, NI_NUMERICHOST | NI_NUMERICSERV, "\n" );
+		TRACE_DEBUG_BUFFER(FULL, "\tKey: [", *key, keylen, "]\n" );
+	}
+	
+	/* Lock the lists */
+	CHECK_POSIX( pthread_mutex_lock(&cli_mtx) );
+	
+	/* Check if the same entry does not already exist */
+	ret = client_search(&prev, ip_port );
+	if (ret == ENOENT) {
+		/* No duplicate found, Ok to add */
+		CHECK_FCT_DO( ret = client_create( &new, &ip_port, key, keylen ), goto end );
+		fd_list_insert_after(&prev->chain, &new->chain);
+		new->refcount++;
+		ret = 0;
+		goto end;
+	}
+	
+	if (ret == EEXIST) {
+		/* Check if the key is the same, then skip or return an error */
+		if ((keylen == prev->key.len ) && ( ! memcmp(*key, prev->key.data, keylen) )) {
+			TRACE_DEBUG(INFO, "Skipping duplicate client description");
+			ret = 0;
+			goto end;
+		}
+		
+		fd_log_debug("ERROR: Conflicting RADIUS clients descriptions!\n");
+		TRACE_DEBUG(NONE, "Previous entry:\n");
+		TRACE_DEBUG_sSA(NONE, 	 "\tIP : ", prev->sa, NI_NUMERICHOST | NI_NUMERICSERV, "\n" );
+		TRACE_DEBUG_BUFFER(NONE, "\tKey: [", prev->key.data, prev->key.len, "]\n" );
+		TRACE_DEBUG(NONE, "Conflicting entry:\n");
+		TRACE_DEBUG_sSA(NONE, 	 "\tIP : ", ip_port, NI_NUMERICHOST | NI_NUMERICSERV, "\n" );
+		TRACE_DEBUG_BUFFER(NONE, "\tKey: [", *key, keylen, "]\n" );
+	}
+end:
+	/* release the lists */
+	CHECK_POSIX( pthread_mutex_unlock(&cli_mtx) );
+	
+	return ret;
+}
+
+static void dump_cli_list(struct fd_list *senti)
+{
+	struct rgw_client * client = NULL;
+	struct fd_list *ref = NULL;
+	
+	for (ref = senti->next; ref != senti; ref = ref->next) {
+		client = (struct rgw_client *)ref;
+		TRACE_DEBUG_sSA(NONE, 	 "  - ", client->sa, NI_NUMERICHOST | NI_NUMERICSERV, "\n" );
+		TRACE_DEBUG_BUFFER(NONE, "     [", client->key.data, client->key.len, "]\n" );
+	}
+}
+
+void rgw_clients_dump(void)
+{
+	if ( ! TRACE_BOOL(FULL) )
+		return;
+	
+	CHECK_POSIX_DO( pthread_mutex_lock(&cli_mtx), /* ignore error */ );
+	
+	if (!FD_IS_LIST_EMPTY(&cli_ip))
+		fd_log_debug(" RADIUS IP clients list:\n");
+	dump_cli_list(&cli_ip);
+		
+	if (!FD_IS_LIST_EMPTY(&cli_ip6))
+		fd_log_debug(" RADIUS IPv6 clients list:\n");
+	dump_cli_list(&cli_ip6);
+		
+	CHECK_POSIX_DO( pthread_mutex_unlock(&cli_mtx), /* ignore error */ );
+}
+
+void rgw_clients_fini(void)
+{
+	struct fd_list * client;
+	
+	TRACE_ENTRY();
+	
+	CHECK_POSIX_DO( pthread_mutex_lock(&cli_mtx), /* ignore error */ );
+	
+	/* empty the lists */
+	while ( ! FD_IS_LIST_EMPTY(&cli_ip) ) {
+		client = cli_ip.next;
+		fd_list_unlink(client);
+		client_unlink((struct rgw_client *)client);
+	}
+	while (! FD_IS_LIST_EMPTY(&cli_ip6)) {
+		client = cli_ip6.next;
+		fd_list_unlink(client);
+		client_unlink((struct rgw_client *)client);
+	}
+	
+	CHECK_POSIX_DO( pthread_mutex_unlock(&cli_mtx), /* ignore error */ );
+	
+}
+
+int rgw_client_finish_send(struct radius_msg ** msg, struct rgw_radius_msg_meta * req, struct rgw_client * cli)
+{
+	int idx;
+	
+	TRACE_ENTRY("%p %p %p", msg, req, cli);
+	CHECK_PARAMS( msg && *msg && cli );
+	
+	if (!req) {
+		/* We don't support this case yet */
+		ASSERT(0);
+		return ENOTSUP;
+	}
+	
+	if (radius_msg_finish_srv(*msg, cli->key.data, cli->key.len, req->radius.hdr->authenticator)) {
+		TRACE_DEBUG(INFO, "An error occurred while preparing the RADIUS answer");
+		radius_msg_free(*msg);
+		free(*msg);
+		*msg = NULL;
+		return EINVAL;
+	}
+	
+	/* Debug */
+	TRACE_DEBUG(FULL, "RADIUS message ready for sending:");
+	rgw_msg_dump((struct rgw_radius_msg_meta *)*msg);
+
+	/* Send the message */
+	CHECK_FCT( rgw_servers_send(req->serv_type, (*msg)->buf, (*msg)->buf_used, cli->sa, req->port) );
+
+	/* update the duplicate cache in rgw_clients */
+	if (req->serv_type == RGW_PLG_TYPE_AUTH)
+		idx = 0;
+	else
+		idx = 1;
+	if (cli->last[idx].ans) {
+		/* Free it */
+		radius_msg_free(cli->last[idx].ans);
+		free(cli->last[idx].ans);
+	}
+	cli->last[idx].ans = *msg;
+	*msg = NULL;
+	
+	/* Finished */
+	return 0;
+}
+
"Welcome to our mercurial repository"