view extensions/radius_gw/rgw_clients.c @ 388:1a4902b216f8

Improved initial handling of RADIUS messages
author Sebastien Decugis <sdecugis@nict.go.jp>
date Fri, 29 May 2009 12:57:43 +0900
parents 03b512313cc1
children 9d9c37868957
line wrap: on
line source

/*********************************************************************************************************
* 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. */

#include "radius_gw.h"

/* How many bytes of secret keys to dump? */
#define KEY_DUMP_BYTES	16

/* Ordered lists of clients. The order relationship is a memcmp on the address zone. 
   For same addresses, the port is compared.
   A given address cannot be added with a 0-port and another port value.
 */
static struct rg_list cli_ip, 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 rg_list		chain;
	
	/* Reference count */
	int			refcount;
	
	/* The address and optional port. */
	union {
		struct sockaddr		*sa; /* generic pointer */
		struct sockaddr_in	*sin;
		struct sockaddr_in6	*sin6;
	};
	
	/* The FQDN 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. [0] for auth, [1] for acct. */
	struct {
		uint16_t		port;
		uint8_t			id;
		struct radius_msg * 	ans; /* to be able to resend the lost answer */
	} last[2];
};


/* create a new rgw_client. the arguments are moved into the structure. */
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));
	rg_list_init(&tmp->chain);
	
	/* 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 = g_pconf->diameter_realm;
	
	/* 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 */
		ASSERT( rg_list_is_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);
	}
}


/* Function to look for an existing rgw_client, or the previous element. 
   The cli_mtx must be held when calling this func. 
   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. */
#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;													\
		}
static int client_search(struct rgw_client ** res, struct sockaddr * ip_port )
{
	int ret = 0;
	int cmp;
	struct rg_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;
}

#define KEY_DUMP_BUF_SIZE (KEY_DUMP_BYTES * 3 + KEY_DUMP_BYTES / 4 + 3)

/* Display the first KEY_DUMP_BYTES bytes of a secret key */
static void client_key_dump(char * keydump /* size: KEY_DUMP_BUF_SIZE */, char * key, size_t keylen)
{
	int i, j, idx;

	memset(keydump, 0, KEY_DUMP_BUF_SIZE);
	idx = 0;

	for (i = 0; i < KEY_DUMP_BYTES / 8; i++) {
		for (j = 0; j < 8; j++) {
			if (idx >= keylen)
				break;

			if (j == 4) {
				*keydump = ' '; keydump++;
			}

			sprintf(keydump, "%02hhX ", key[idx]);
			keydump += 3;
			idx ++;
		}
		if (idx >= keylen)
			break;		
		*keydump = '\t'; keydump++;
	}
	if (keylen > idx) {
		sprintf(keydump, "...");
	}
}


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_EXT_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 */
			ASSERT(0);
		}
		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 */
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)) {
		TRACE_DEBUG(INFO, "Message received with a NAS-IP-Address or NAS-IPv6-Address different from the sender's. Discarding...");
		return EINVAL;
	}
	
	/* Now check the nas_id */
	if (nas_id) {
		/* 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 * oh, size_t oh_len, char * or, size_t or_len,  char **fqdn, char **realm, int strict)
{
	TRACE_ENTRY("%p %p %g %p %g %p %p", cli, oh, oh_len, or, or_len, fqdn, realm);
	CHECK_PARAMS(cli && fqdn && realm);
	
	if (oh && oh_len) {
		if (strncasecmp(oh, cli->fqdn, oh_len)) {
			TRACE_DEBUG(INFO, "Received unexpected '%.*s' Origin-Host in RADIUS State attribute, replacing with '%s'", oh_len, oh, cli->fqdn);
			if (strict)
				return EINVAL;
		}
	}
	
	if (or && or_len) {
		if (strncasecmp(or, cli->realm, or_len)) {
			TRACE_DEBUG(INFO, "Received unexpected '%.*s' Origin-Realm in RADIUS State attribute, replacing with '%s'", or_len, or, cli->realm);
			if (strict)
				return EINVAL;
		}
	}
	
	*fqdn = cli->fqdn;
	*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_init(void)
{
	rg_list_init(&cli_ip);
	rg_list_init(&cli_ip6);
	return 0;
}

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 )) {
		char ipstr[INET6_ADDRSTRLEN];
		char keydump[KEY_DUMP_BUF_SIZE];
		char portstr[8];
		int ret;
		
		if (ret = getnameinfo(ip_port, sizeof(struct sockaddr_storage), &ipstr[0], INET6_ADDRSTRLEN, &portstr[0], sizeof(portstr), NI_NUMERICHOST | NI_NUMERICSERV)) {
			TRACE_DEBUG(INFO, "Error adding client: %s", gai_strerror(ret));
			return EINVAL;
		}
		
		client_key_dump(&keydump[0], *key, keylen);
		
		TRACE_DEBUG(FULL, "Adding client [%s]:%s with %d bytes key: %s", ipstr, portstr, keylen, keydump);
	}
	
	/* 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 );
		rg_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");
			goto end;
		}
		
		TRACE_DEBUG(INFO, "Error: conflicting entries");	
		log_error("Error adding a RADIUS client in conflict with a previous entry.\n");
		{
			char ipstr[INET6_ADDRSTRLEN];
			char keydump[KEY_DUMP_BUF_SIZE];
			char portstr[8];
			int rc;

			if (rc = getnameinfo(prev->sa, sizeof(struct sockaddr_storage), &ipstr[0], INET6_ADDRSTRLEN, &portstr[0], sizeof(portstr), NI_NUMERICHOST | NI_NUMERICSERV)) {
				TRACE_DEBUG(INFO, "Previous entry: ERROR (%s)", gai_strerror(rc));
			} else {
				client_key_dump(&keydump[0], prev->key.data, prev->key.len);
				log_error( "Previous entry: [%s]:%s, key (%db): %s\n", ipstr, portstr, prev->key.len, keydump);
			}
			
			if (rc = getnameinfo(ip_port, sizeof(struct sockaddr_storage), &ipstr[0], INET6_ADDRSTRLEN, &portstr[0], sizeof(portstr), NI_NUMERICHOST | NI_NUMERICSERV)) {
				TRACE_DEBUG(INFO, "New entry: ERROR (%s)", gai_strerror(rc));
			} else {
				client_key_dump(&keydump[0], *key, keylen);
				log_error( "New entry:      [%s]:%s, key (%db): %s\n", ipstr, portstr, keylen, keydump);
			}
		}
	}
end:
	/* release the lists */
	CHECK_POSIX( pthread_mutex_unlock(&cli_mtx) );
	
	return ret;
}

static void dump_cli_list(struct rg_list *senti)
{
	struct rgw_client * client = NULL;
	struct rg_list *ref = NULL;
	char ipstr[INET6_ADDRSTRLEN];
	char keydump[KEY_DUMP_BUF_SIZE];
	char portstr[8];
	int rc;
	
	for (ref = senti->next; ref != senti; ref = ref->next) {
		client = (struct rgw_client *)ref;
		if (rc = getnameinfo(client->sa, sizeof(struct sockaddr_storage), &ipstr[0], INET6_ADDRSTRLEN, &portstr[0], sizeof(portstr), NI_NUMERICHOST | NI_NUMERICSERV)) {
			TRACE_DEBUG(INFO, "Error dumping entry: %s", gai_strerror(rc));
			continue;
		}
		client_key_dump(&keydump[0], client->key.data, client->key.len);
		log_debug("   [%s]:%s, %d bytes: %s\n", ipstr, portstr, client->key.len, keydump);
	}
}

void rgw_clients_dump(void)
{
	if ( ! TRACE_BOOL(FULL) )
		return;
	
	CHECK_POSIX_DO( pthread_mutex_lock(&cli_mtx), /* ignore error */ );
	
	if (!rg_list_is_empty(&cli_ip))
		log_debug(" RADIUS IP clients list:\n");
	dump_cli_list(&cli_ip);
		
	if (!rg_list_is_empty(&cli_ip6))
		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 rg_list * client;
	
	TRACE_ENTRY();
	
	CHECK_POSIX_DO( pthread_mutex_lock(&cli_mtx), /* ignore error */ );
	
	/* empty the lists */
	while ( ! rg_list_is_empty(&cli_ip) ) {
		client = cli_ip.next;
		rg_list_unlink(client);
		client_unlink((struct rgw_client *)client);
	}
	while (! rg_list_is_empty(&cli_ip6)) {
		client = cli_ip6.next;
		rg_list_unlink(client);
		client_unlink((struct rgw_client *)client);
	}
	
	CHECK_POSIX_DO( pthread_mutex_unlock(&cli_mtx), /* ignore error */ );
	
}

"Welcome to our mercurial repository"