/*
 * NetLabel Domain Hash Table
 *
 * This file manages the domain hash table that NetLabel uses to determine
 * which network labeling protocol to use for a given domain.  The NetLabel
 * system manages static and dynamic label mappings for network protocols such
 * as CIPSO and RIPSO.
 *
 * Author: Paul Moore <paul.moore@hp.com>
 *
 */

/*
 * (c) Copyright Hewlett-Packard Development Company, L.P., 2006
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 */

#include <linux/types.h>
#include <linux/rcupdate.h>
#include <linux/list.h>
#include <linux/skbuff.h>
#include <linux/spinlock.h>
#include <linux/string.h>
#include <net/netlabel.h>
#include <net/cipso_ipv4.h>
#include <asm/bug.h>

#include "netlabel_mgmt.h"
#include "netlabel_domainhash.h"

struct netlbl_domhsh_tbl {
	struct list_head *tbl;
	u32 size;
};

/* Domain hash table */
/* XXX - updates should be so rare that having one spinlock for the entire
 * hash table should be okay */
static DEFINE_SPINLOCK(netlbl_domhsh_lock);
static struct netlbl_domhsh_tbl *netlbl_domhsh = NULL;

/* Default domain mapping */
static DEFINE_SPINLOCK(netlbl_domhsh_def_lock);
static struct netlbl_dom_map *netlbl_domhsh_def = NULL;

/*
 * Domain Hash Table Helper Functions
 */

/**
 * netlbl_domhsh_free_entry - Frees a domain hash table entry
 * @entry: the entry's RCU field
 *
 * Description:
 * This function is designed to be used as a callback to the call_rcu()
 * function so that the memory allocated to a hash table entry can be released
 * safely.
 *
 */
static void netlbl_domhsh_free_entry(struct rcu_head *entry)
{
	struct netlbl_dom_map *ptr;

	ptr = container_of(entry, struct netlbl_dom_map, rcu);
	kfree(ptr->domain);
	kfree(ptr);
}

/**
 * netlbl_domhsh_hash - Hashing function for the domain hash table
 * @domain: the domain name to hash
 *
 * Description:
 * This is the hashing function for the domain hash table, it returns the
 * correct bucket number for the domain.  The caller is responsibile for
 * calling the rcu_read_[un]lock() functions.
 *
 */
static u32 netlbl_domhsh_hash(const char *key)
{
	u32 iter;
	u32 val;
	u32 len;

	/* This is taken (with slight modification) from
	 * security/selinux/ss/symtab.c:symhash() */

	for (iter = 0, val = 0, len = strlen(key); iter < len; iter++)
		val = (val << 4 | (val >> (8 * sizeof(u32) - 4))) ^ key[iter];
	return val & (rcu_dereference(netlbl_domhsh)->size - 1);
}

/**
 * netlbl_domhsh_search - Search for a domain entry
 * @domain: the domain
 * @def: return default if no match is found
 *
 * Description:
 * Searches the domain hash table and returns a pointer to the hash table
 * entry if found, otherwise NULL is returned.  If @def is non-zero and a
 * match is not found in the domain hash table the default mapping is returned
 * if it exists.  The caller is responsibile for the rcu hash table locks
 * (i.e. the caller much call rcu_read_[un]lock()).
 *
 */
static struct netlbl_dom_map *netlbl_domhsh_search(const char *domain, u32 def)
{
	u32 bkt;
	struct netlbl_dom_map *iter;

	if (domain != NULL) {
		bkt = netlbl_domhsh_hash(domain);
		list_for_each_entry_rcu(iter, &netlbl_domhsh->tbl[bkt], list)
			if (iter->valid && strcmp(iter->domain, domain) == 0)
				return iter;
	}

	if (def != 0) {
		iter = rcu_dereference(netlbl_domhsh_def);
		if (iter != NULL && iter->valid)
			return iter;
	}

	return NULL;
}

/*
 * Domain Hash Table Functions
 */

/**
 * netlbl_domhsh_init - Init for the domain hash
 * @size: the number of bits to use for the hash buckets
 *
 * Description:
 * Initializes the domain hash table, should be called only by
 * netlbl_user_init() during initialization.  Returns zero on success, non-zero
 * values on error.
 *
 */
int netlbl_domhsh_init(u32 size)
{
	u32 iter;
	struct netlbl_domhsh_tbl *hsh_tbl;

	if (size == 0)
		return -EINVAL;

	hsh_tbl = kmalloc(sizeof(*hsh_tbl), GFP_KERNEL);
	if (hsh_tbl == NULL)
		return -ENOMEM;
	hsh_tbl->size = 1 << size;
	hsh_tbl->tbl = kcalloc(hsh_tbl->size,
			       sizeof(struct list_head),
			       GFP_KERNEL);
	if (hsh_tbl->tbl == NULL) {
		kfree(hsh_tbl);
		return -ENOMEM;
	}
	for (iter = 0; iter < hsh_tbl->size; iter++)
		INIT_LIST_HEAD(&hsh_tbl->tbl[iter]);

	rcu_read_lock();
	spin_lock(&netlbl_domhsh_lock);
	rcu_assign_pointer(netlbl_domhsh, hsh_tbl);
	spin_unlock(&netlbl_domhsh_lock);
	rcu_read_unlock();

	return 0;
}

/**
 * netlbl_domhsh_add - Adds a entry to the domain hash table
 * @entry: the entry to add
 *
 * Description:
 * Adds a new entry to the domain hash table and handles any updates to the
 * lower level protocol handler (i.e. CIPSO).  Returns zero on success,
 * negative on failure.
 *
 */
int netlbl_domhsh_add(struct netlbl_dom_map *entry)
{
	int ret_val;
	u32 bkt;

	switch (entry->type) {
	case NETLBL_NLTYPE_UNLABELED:
		ret_val = 0;
		break;
	case NETLBL_NLTYPE_CIPSOV4:
		ret_val = cipso_v4_doi_domhsh_add(entry->type_def.cipsov4,
						  entry->domain);
		break;
	default:
		return -EINVAL;
	}
	if (ret_val != 0)
		return ret_val;

	entry->valid = 1;
	INIT_RCU_HEAD(&entry->rcu);

	ret_val = 0;
	rcu_read_lock();
	if (entry->domain != NULL) {
		bkt = netlbl_domhsh_hash(entry->domain);
		spin_lock(&netlbl_domhsh_lock);
		if (netlbl_domhsh_search(entry->domain, 0) == NULL)
			list_add_tail_rcu(&entry->list,
					  &netlbl_domhsh->tbl[bkt]);
		else
			ret_val = -EEXIST;
		spin_unlock(&netlbl_domhsh_lock);
	} else if (entry->domain == NULL) {
		INIT_LIST_HEAD(&entry->list);
		spin_lock(&netlbl_domhsh_def_lock);
		if (rcu_dereference(netlbl_domhsh_def) == NULL)
			rcu_assign_pointer(netlbl_domhsh_def, entry);
		else
			ret_val = -EEXIST;
		spin_unlock(&netlbl_domhsh_def_lock);
	} else
		ret_val = -EINVAL;
	rcu_read_unlock();

	if (ret_val != 0) {
		switch (entry->type) {
		case NETLBL_NLTYPE_CIPSOV4:
			if (cipso_v4_doi_domhsh_remove(entry->type_def.cipsov4,
						       entry->domain) != 0)
				BUG();
			break;
		}
	}

	return ret_val;
}

/**
 * netlbl_domhsh_add_default - Adds the default entry to the domain hash table
 * @entry: the entry to add
 *
 * Description:
 * Adds a new default entry to the domain hash table and handles any updates
 * to the lower level protocol handler (i.e. CIPSO).  Returns zero on success,
 * negative on failure.
 *
 */
int netlbl_domhsh_add_default(struct netlbl_dom_map *entry)
{
	return netlbl_domhsh_add(entry);
}

/**
 * netlbl_domhsh_remove - Removes an entry from the domain hash table
 * @domain: the domain to remove
 *
 * Description:
 * Removes an entry from the domain hash table and handles any updates to the
 * lower level protocol handler (i.e. CIPSO).  Returns zero on success,
 * negative on failure.
 *
 */
int netlbl_domhsh_remove(const char *domain)
{
	int ret_val = -ENOENT;
	struct netlbl_dom_map *entry;

	rcu_read_lock();
	if (domain != NULL)
		entry = netlbl_domhsh_search(domain, 0);
	else
		entry = netlbl_domhsh_search(domain, 1);
	if (entry == NULL)
		goto remove_return;
	switch (entry->type) {
	case NETLBL_NLTYPE_UNLABELED:
		break;
	case NETLBL_NLTYPE_CIPSOV4:
		ret_val = cipso_v4_doi_domhsh_remove(entry->type_def.cipsov4,
						     entry->domain);
		if (ret_val != 0)
			goto remove_return;
		break;
	}
	ret_val = 0;
	if (entry != rcu_dereference(netlbl_domhsh_def)) {
		spin_lock(&netlbl_domhsh_lock);
		if (entry->valid) {
			entry->valid = 0;
			list_del_rcu(&entry->list);
		} else
			ret_val = -ENOENT;
		spin_unlock(&netlbl_domhsh_lock);
	} else {
		spin_lock(&netlbl_domhsh_def_lock);
		if (entry->valid) {
			entry->valid = 0;
			rcu_assign_pointer(netlbl_domhsh_def, NULL);
		} else
			ret_val = -ENOENT;
		spin_unlock(&netlbl_domhsh_def_lock);
	}
	if (ret_val == 0)
		call_rcu(&entry->rcu, netlbl_domhsh_free_entry);

remove_return:
	rcu_read_unlock();
	return ret_val;
}

/**
 * netlbl_domhsh_remove_default - Removes the default entry from the table
 *
 * Description:
 * Removes/resets the default entry for the domain hash table and handles any
 * updates to the lower level protocol handler (i.e. CIPSO).  Returns zero on
 * success, non-zero on failure.
 *
 */
int netlbl_domhsh_remove_default(void)
{
	return netlbl_domhsh_remove(NULL);
}

/**
 * netlbl_domhsh_getentry - Get an entry from the domain hash table
 * @domain: the domain name to search for
 *
 * Description:
 * Look through the domain hash table searching for an entry to match @domain,
 * return a pointer to a copy of the entry or NULL.  The caller is responsibile
 * for ensuring that rcu_read_[un]lock() is called.
 *
 */
struct netlbl_dom_map *netlbl_domhsh_getentry(const char *domain)
{
	return netlbl_domhsh_search(domain, 1);
}

/**
 * netlbl_domhsh_dump - Dump the domain hash table into a sk_buff
 *
 * Description:
 * Dump the domain hash table into a buffer suitable for returning to an
 * application in response to a NetLabel management DOMAIN message.  This
 * function may fail if another process is growing the hash table at the same
 * time.  The returned sk_buff has room at the front of the sk_buff for
 * @headroom bytes.  See netlabel.h for the DOMAIN message format.  Returns a
 * pointer to a sk_buff on success, NULL on error.
 *
 */
struct sk_buff *netlbl_domhsh_dump(size_t headroom)
{
	struct sk_buff *skb = NULL;
	ssize_t buf_len;
	u32 bkt_iter;
	u32 dom_cnt = 0;
	struct netlbl_domhsh_tbl *hsh_tbl;
	struct netlbl_dom_map *list_iter;
	ssize_t tmp_len;

	buf_len = NETLBL_LEN_U32;
	rcu_read_lock();
	hsh_tbl = rcu_dereference(netlbl_domhsh);
	for (bkt_iter = 0; bkt_iter < hsh_tbl->size; bkt_iter++)
		list_for_each_entry_rcu(list_iter,
					&hsh_tbl->tbl[bkt_iter], list) {
			buf_len += NETLBL_LEN_U32 +
				nla_total_size(strlen(list_iter->domain) + 1);
			switch (list_iter->type) {
			case NETLBL_NLTYPE_UNLABELED:
				break;
			case NETLBL_NLTYPE_CIPSOV4:
				buf_len += 2 * NETLBL_LEN_U32;
				break;
			}
			dom_cnt++;
		}

	skb = netlbl_netlink_alloc_skb(headroom, buf_len, GFP_ATOMIC);
	if (skb == NULL)
		goto dump_failure;

	if (nla_put_u32(skb, NLA_U32, dom_cnt) != 0)
		goto dump_failure;
	buf_len -= NETLBL_LEN_U32;
	hsh_tbl = rcu_dereference(netlbl_domhsh);
	for (bkt_iter = 0; bkt_iter < hsh_tbl->size; bkt_iter++)
		list_for_each_entry_rcu(list_iter,
					&hsh_tbl->tbl[bkt_iter], list) {
			tmp_len = nla_total_size(strlen(list_iter->domain) +
						 1);
			if (buf_len < NETLBL_LEN_U32 + tmp_len)
				goto dump_failure;
			if (nla_put_string(skb,
					   NLA_STRING,
					   list_iter->domain) != 0)
				goto dump_failure;
			if (nla_put_u32(skb, NLA_U32, list_iter->type) != 0)
				goto dump_failure;
			buf_len -= NETLBL_LEN_U32 + tmp_len;
			switch (list_iter->type) {
			case NETLBL_NLTYPE_UNLABELED:
				break;
			case NETLBL_NLTYPE_CIPSOV4:
				if (buf_len < 2 * NETLBL_LEN_U32)
					goto dump_failure;
				if (nla_put_u32(skb,
				       NLA_U32,
				       list_iter->type_def.cipsov4->type) != 0)
					goto dump_failure;
				if (nla_put_u32(skb,
				       NLA_U32,
				       list_iter->type_def.cipsov4->doi) != 0)
					goto dump_failure;
				buf_len -= 2 * NETLBL_LEN_U32;
				break;
			}
		}
	rcu_read_unlock();

	return skb;

dump_failure:
	rcu_read_unlock();
	kfree_skb(skb);
	return NULL;
}

/**
 * netlbl_domhsh_dump_default - Dump the default domain mapping into a sk_buff
 *
 * Description:
 * Dump the default domain mapping into a buffer suitable for returning to an
 * application in response to a NetLabel management DEFDOMAIN message.  This
 * function may fail if another process is changing the default domain mapping
 * at the same time.  The returned sk_buff has room at the front of the
 * skb_buff for @headroom bytes.  See netlabel.h for the DEFDOMAIN message
 * format.  Returns a pointer to a sk_buff on success, NULL on error.
 *
 */
struct sk_buff *netlbl_domhsh_dump_default(size_t headroom)
{
	struct sk_buff *skb;
	ssize_t buf_len;
	struct netlbl_dom_map *entry;

	buf_len = NETLBL_LEN_U32;
	rcu_read_lock();
	entry = rcu_dereference(netlbl_domhsh_def);
	if (entry != NULL)
		switch (entry->type) {
		case NETLBL_NLTYPE_UNLABELED:
			break;
		case NETLBL_NLTYPE_CIPSOV4:
			buf_len += 2 * NETLBL_LEN_U32;
			break;
		}

	skb = netlbl_netlink_alloc_skb(headroom, buf_len, GFP_ATOMIC);
	if (skb == NULL)
		goto dump_default_failure;

	if (entry != rcu_dereference(netlbl_domhsh_def))
		goto dump_default_failure;
	if (entry != NULL) {
		if (nla_put_u32(skb, NLA_U32, entry->type) != 0)
			goto dump_default_failure;
		buf_len -= NETLBL_LEN_U32;
		switch (entry->type) {
		case NETLBL_NLTYPE_UNLABELED:
			break;
		case NETLBL_NLTYPE_CIPSOV4:
			if (buf_len < 2 * NETLBL_LEN_U32)
				goto dump_default_failure;
			if (nla_put_u32(skb,
					NLA_U32,
					entry->type_def.cipsov4->type) != 0)
				goto dump_default_failure;
			if (nla_put_u32(skb,
					NLA_U32,
					entry->type_def.cipsov4->doi) != 0)
				goto dump_default_failure;
			buf_len -= 2 * NETLBL_LEN_U32;
			break;
		}
	} else
		nla_put_u32(skb, NLA_U32, NETLBL_NLTYPE_NONE);
	rcu_read_unlock();

	return skb;

dump_default_failure:
	rcu_read_unlock();
	kfree_skb(skb);
	return NULL;
}