/*	$NetBSD: ip_dstlist.c,v 1.3 2014/06/28 07:59:26 darrenr Exp $	*/

/*
 * Copyright (C) 2012 by Darren Reed.
 *
 * See the IPFILTER.LICENCE file for details on licencing.
 */
#if defined(KERNEL) || defined(_KERNEL)
# undef KERNEL
# undef _KERNEL
# define        KERNEL	1
# define        _KERNEL	1
#endif
#if defined(__osf__)
# define _PROTO_NET_H_
#endif
#include <sys/errno.h>
#include <sys/types.h>
#include <sys/param.h>
#include <sys/file.h>
#if !defined(_KERNEL) && !defined(__KERNEL__)
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
# define _KERNEL
# ifdef __OpenBSD__
struct file;
# endif
# include <sys/uio.h>
# undef _KERNEL
#else
# include <sys/systm.h>
# if defined(NetBSD) && (__NetBSD_Version__ >= 104000000)
#  include <sys/proc.h>
# endif
#endif
#include <sys/time.h>
#if !defined(linux)
# include <sys/protosw.h>
#endif
#include <sys/socket.h>
#if defined(_KERNEL) && (!defined(__SVR4) && !defined(__svr4__))
# include <sys/mbuf.h>
#endif
#if defined(__SVR4) || defined(__svr4__)
# include <sys/filio.h>
# include <sys/byteorder.h>
# ifdef _KERNEL
#  include <sys/dditypes.h>
# endif
# include <sys/stream.h>
# include <sys/kmem.h>
#endif
#if defined(__FreeBSD_version) && (__FreeBSD_version >= 300000)
# include <sys/malloc.h>
#endif

#include <net/if.h>
#include <netinet/in.h>

#include "netinet/ip_compat.h"
#include "netinet/ip_fil.h"
#include "netinet/ip_nat.h"
#include "netinet/ip_lookup.h"
#include "netinet/ip_dstlist.h"

/* END OF INCLUDES */

#ifdef HAS_SYS_MD5_H
# include <sys/md5.h>
#else
# include "md5.h"
#endif

#if !defined(lint)
static const char rcsid[] = "@(#)Id: ip_dstlist.c,v 1.1.1.2 2012/07/22 13:44:12 darrenr Exp";
#endif

typedef struct ipf_dstl_softc_s {
	ippool_dst_t	*dstlist[LOOKUP_POOL_SZ];
	ippool_dst_t	**tails[LOOKUP_POOL_SZ];
	ipf_dstl_stat_t	stats;
} ipf_dstl_softc_t;


static void *ipf_dstlist_soft_create __P((ipf_main_softc_t *));
static void ipf_dstlist_soft_destroy __P((ipf_main_softc_t *, void *));
static int ipf_dstlist_soft_init __P((ipf_main_softc_t *, void *));
static void ipf_dstlist_soft_fini __P((ipf_main_softc_t *, void *));
static int ipf_dstlist_addr_find __P((ipf_main_softc_t *, void *, int,
				      void *, u_int));
static size_t ipf_dstlist_flush __P((ipf_main_softc_t *, void *,
				     iplookupflush_t *));
static int ipf_dstlist_iter_deref __P((ipf_main_softc_t *, void *, int, int,
				       void *));
static int ipf_dstlist_iter_next __P((ipf_main_softc_t *, void *, ipftoken_t *,
				      ipflookupiter_t *));
static int ipf_dstlist_node_add __P((ipf_main_softc_t *, void *,
				     iplookupop_t *, int));
static int ipf_dstlist_node_del __P((ipf_main_softc_t *, void *,
				     iplookupop_t *, int));
static int ipf_dstlist_stats_get __P((ipf_main_softc_t *, void *,
				      iplookupop_t *));
static int ipf_dstlist_table_add __P((ipf_main_softc_t *, void *,
				      iplookupop_t *));
static int ipf_dstlist_table_del __P((ipf_main_softc_t *, void *,
				      iplookupop_t *));
static int ipf_dstlist_table_deref __P((ipf_main_softc_t *, void *, void *));
static void *ipf_dstlist_table_find __P((void *, int, char *));
static void ipf_dstlist_table_free __P((ipf_dstl_softc_t *, ippool_dst_t *));
static void ipf_dstlist_table_remove __P((ipf_main_softc_t *,
					  ipf_dstl_softc_t *, ippool_dst_t *));
static void ipf_dstlist_table_clearnodes __P((ipf_dstl_softc_t *,
					      ippool_dst_t *));
static ipf_dstnode_t *ipf_dstlist_select __P((fr_info_t *, ippool_dst_t *));
static void *ipf_dstlist_select_ref __P((void *, int, char *));
static void ipf_dstlist_node_free __P((ipf_dstl_softc_t *, ippool_dst_t *, ipf_dstnode_t *));
static int ipf_dstlist_node_deref __P((void *, ipf_dstnode_t *));
static void ipf_dstlist_expire __P((ipf_main_softc_t *, void *));
static void ipf_dstlist_sync __P((ipf_main_softc_t *, void *));

ipf_lookup_t ipf_dstlist_backend = {
	IPLT_DSTLIST,
	ipf_dstlist_soft_create,
	ipf_dstlist_soft_destroy,
	ipf_dstlist_soft_init,
	ipf_dstlist_soft_fini,
	ipf_dstlist_addr_find,
	ipf_dstlist_flush,
	ipf_dstlist_iter_deref,
	ipf_dstlist_iter_next,
	ipf_dstlist_node_add,
	ipf_dstlist_node_del,
	ipf_dstlist_stats_get,
	ipf_dstlist_table_add,
	ipf_dstlist_table_del,
	ipf_dstlist_table_deref,
	ipf_dstlist_table_find,
	ipf_dstlist_select_ref,
	ipf_dstlist_select_node,
	ipf_dstlist_expire,
	ipf_dstlist_sync
};


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_soft_create                                     */
/* Returns:     int - 0 = success, else error                               */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*                                                                          */
/* Allocating a chunk of memory filled with 0's is enough for the current   */
/* soft context used with destination lists.                                */
/* ------------------------------------------------------------------------ */
static void *
ipf_dstlist_soft_create(softc)
	ipf_main_softc_t *softc;
{
	ipf_dstl_softc_t *softd;
	int i;

	KMALLOC(softd, ipf_dstl_softc_t *);
	if (softd == NULL) {
		IPFERROR(120028);
		return NULL;
	}

	bzero((char *)softd, sizeof(*softd));
	for (i = 0; i <= IPL_LOGMAX; i++)
		softd->tails[i] = &softd->dstlist[i];

	return softd;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_soft_destroy                                    */
/* Returns:     Nil                                                         */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*                                                                          */
/* For destination lists, the only thing we have to do when destroying the  */
/* soft context is free it!                                                 */
/* ------------------------------------------------------------------------ */
static void
ipf_dstlist_soft_destroy(softc, arg)
	ipf_main_softc_t *softc;
	void *arg;
{
	ipf_dstl_softc_t *softd = arg;

	KFREE(softd);
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_soft_init                                       */
/* Returns:     int - 0 = success, else error                               */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*                                                                          */
/* There is currently no soft context for destination list management.      */
/* ------------------------------------------------------------------------ */
static int
ipf_dstlist_soft_init(softc, arg)
	ipf_main_softc_t *softc;
	void *arg;
{
	return 0;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_soft_fini                                       */
/* Returns:     Nil                                                         */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*                                                                          */
/* There is currently no soft context for destination list management.      */
/* ------------------------------------------------------------------------ */
static void
ipf_dstlist_soft_fini(softc, arg)
	ipf_main_softc_t *softc;
	void *arg;
{
	ipf_dstl_softc_t *softd = arg;
	int i;

	for (i = -1; i <= IPL_LOGMAX; i++) {
		while (softd->dstlist[i + 1] != NULL) {
			ipf_dstlist_table_remove(softc, softd,
						 softd->dstlist[i + 1]);
		}
	}

	ASSERT(softd->stats.ipls_numderefnodes == 0);
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_addr_find                                       */
/* Returns:     int - 0 = success, else error                               */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg1(I)  - pointer to local context to use                  */
/*              arg2(I)  - pointer to local context to use                  */
/*              arg3(I)  - pointer to local context to use                  */
/*              arg4(I)  - pointer to local context to use                  */
/*                                                                          */
/* There is currently no such thing as searching a destination list for an  */
/* address so this function becomes a no-op. Its presence is required as    */
/* ipf_lookup_res_name() stores the "addr_find" function pointer in the     */
/* pointer passed in to it as funcptr, although it could be a generic null- */
/* op function rather than a specific one.                                  */
/* ------------------------------------------------------------------------ */
/*ARGSUSED*/
static int
ipf_dstlist_addr_find(softc, arg1, arg2, arg3, arg4)
	ipf_main_softc_t *softc;
	void *arg1, *arg3;
	int arg2;
	u_int arg4;
{
	return -1;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_flush                                           */
/* Returns:     int      - number of objects deleted                        */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*              fop(I)   - pointer to lookup flush operation data           */
/*                                                                          */
/* Flush all of the destination tables that match the data passed in with   */
/* the iplookupflush_t. There are two ways to match objects: the device for */
/* which they are to be used with and their name.                           */
/* ------------------------------------------------------------------------ */
static size_t
ipf_dstlist_flush(softc, arg, fop)
	ipf_main_softc_t *softc;
	void *arg;
	iplookupflush_t *fop;
{
	ipf_dstl_softc_t *softd = arg;
	ippool_dst_t *node, *next;
	int n, i;

	for (n = 0, i = -1; i <= IPL_LOGMAX; i++) {
		if (fop->iplf_unit != IPLT_ALL && fop->iplf_unit != i)
			continue;
		for (node = softd->dstlist[i + 1]; node != NULL; node = next) {
			next = node->ipld_next;

			if ((*fop->iplf_name != '\0') &&
			    strncmp(fop->iplf_name, node->ipld_name,
				    FR_GROUPLEN))
				continue;

			ipf_dstlist_table_remove(softc, softd, node);
			n++;
		}
	}
	return n;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_iter_deref                                      */
/* Returns:     int      - 0 = success, else error                          */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*              otype(I) - type of data structure to iterate through        */
/*              unit(I)  - device we are working with                       */
/*              data(I)  - address of object in kernel space                */
/*                                                                          */
/* This function is called when the iteration token is being free'd and is  */
/* responsible for dropping the reference count of the structure it points  */
/* to.                                                                      */
/* ------------------------------------------------------------------------ */
static int
ipf_dstlist_iter_deref(softc, arg, otype, unit, data)
	ipf_main_softc_t *softc;
	void *arg;
	int otype, unit;
	void *data;
{
	if (data == NULL) {
		IPFERROR(120001);
		return EINVAL;
	}

	if (unit < -1 || unit > IPL_LOGMAX) {
		IPFERROR(120002);
		return EINVAL;
	}

	switch (otype)
	{
	case IPFLOOKUPITER_LIST :
		ipf_dstlist_table_deref(softc, arg, (ippool_dst_t *)data);
		break;

	case IPFLOOKUPITER_NODE :
		ipf_dstlist_node_deref(arg, (ipf_dstnode_t *)data);
		break;
	}

	return 0;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_iter_next                                       */
/* Returns:     int - 0 = success, else error                               */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*              op(I)    - pointer to lookup operation data                 */
/*              uid(I)   - uid of process doing the ioctl                   */
/*                                                                          */
/* This function is responsible for either selecting the next destination   */
/* list or node on a destination list to be returned as a user process      */
/* iterates through the list of destination lists or nodes.                 */
/* ------------------------------------------------------------------------ */
static int
ipf_dstlist_iter_next(softc, arg, token, iter)
	ipf_main_softc_t *softc;
	void *arg;
	ipftoken_t *token;
	ipflookupiter_t *iter;
{
	ipf_dstnode_t zn, *nextnode = NULL, *node = NULL;
	ippool_dst_t zero, *next = NULL, *dsttab = NULL;
	ipf_dstl_softc_t *softd = arg;
	int err = 0;
	void *hint;

	switch (iter->ili_otype)
	{
	case IPFLOOKUPITER_LIST :
		dsttab = token->ipt_data;
		if (dsttab == NULL) {
			next = softd->dstlist[(int)iter->ili_unit + 1];
		} else {
			next = dsttab->ipld_next;
		}

		if (next != NULL) {
			ATOMIC_INC32(next->ipld_ref);
			token->ipt_data = next;
			hint = next->ipld_next;
		} else {
			bzero((char *)&zero, sizeof(zero));
			next = &zero;
			token->ipt_data = NULL;
			hint = NULL;
		}
		break;

	case IPFLOOKUPITER_NODE :
		node = token->ipt_data;
		if (node == NULL) {
			dsttab = ipf_dstlist_table_find(arg, iter->ili_unit,
							iter->ili_name);
			if (dsttab == NULL) {
				IPFERROR(120004);
				err = ESRCH;
				nextnode = NULL;
			} else {
				if (dsttab->ipld_dests == NULL)
					nextnode = NULL;
				else
					nextnode = *dsttab->ipld_dests;
				dsttab = NULL;
			}
		} else {
			nextnode = node->ipfd_next;
		}

		if (nextnode != NULL) {
			MUTEX_ENTER(&nextnode->ipfd_lock);
			nextnode->ipfd_ref++;
			MUTEX_EXIT(&nextnode->ipfd_lock);
			token->ipt_data = nextnode;
			hint = nextnode->ipfd_next;
		} else {
			bzero((char *)&zn, sizeof(zn));
			nextnode = &zn;
			token->ipt_data = NULL;
			hint = NULL;
		}
		break;
	default :
		IPFERROR(120003);
		err = EINVAL;
		break;
	}

	if (err != 0)
		return err;

	switch (iter->ili_otype)
	{
	case IPFLOOKUPITER_LIST :
		if (dsttab != NULL)
			ipf_dstlist_table_deref(softc, arg, dsttab);
		err = COPYOUT(next, iter->ili_data, sizeof(*next));
		if (err != 0) {
			IPFERROR(120005);
			err = EFAULT;
		}
		break;

	case IPFLOOKUPITER_NODE :
		if (node != NULL)
			ipf_dstlist_node_deref(arg, node);
		err = COPYOUT(nextnode, iter->ili_data, sizeof(*nextnode));
		if (err != 0) {
			IPFERROR(120006);
			err = EFAULT;
		}
		break;
	}

	if (hint == NULL)
		ipf_token_mark_complete(token);

	return err;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_node_add                                        */
/* Returns:     int - 0 = success, else error                               */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*              op(I)    - pointer to lookup operation data                 */
/*              uid(I)   - uid of process doing the ioctl                   */
/* Locks:       WRITE(ipf_poolrw)                                           */
/*                                                                          */
/* Add a new node to a destination list. To do this, we only copy in the    */
/* frdest_t structure because that contains the only data required from the */
/* application to create a new node. The frdest_t doesn't contain the name  */
/* itself. When loading filter rules, fd_name is a 'pointer' to the name.   */
/* In this case, the 'pointer' does not work, instead it is the length of   */
/* the name and the name is immediately following the frdest_t structure.   */
/* fd_name must include the trailing \0, so it should be strlen(str) + 1.   */
/* For simple sanity checking, an upper bound on the size of fd_name is     */
/* imposed - 128.                                                          */
/* ------------------------------------------------------------------------ */
static int
ipf_dstlist_node_add(softc, arg, op, uid)
	ipf_main_softc_t *softc;
	void *arg;
	iplookupop_t *op;
	int uid;
{
	ipf_dstl_softc_t *softd = arg;
	ipf_dstnode_t *node, **nodes;
	ippool_dst_t *d;
	frdest_t dest;
	int err;

	if (op->iplo_size < sizeof(frdest_t)) {
		IPFERROR(120007);
		return EINVAL;
	}

	err = COPYIN(op->iplo_struct, &dest, sizeof(dest));
	if (err != 0) {
		IPFERROR(120009);
		return EFAULT;
	}

	d = ipf_dstlist_table_find(arg, op->iplo_unit, op->iplo_name);
	if (d == NULL) {
		IPFERROR(120010);
		return ESRCH;
	}

	switch (dest.fd_addr.adf_family)
	{
	case AF_INET :
	case AF_INET6 :
		break;
	default :
		IPFERROR(120019);
		return EINVAL;
	}

	if (dest.fd_name < -1 || dest.fd_name > 128) {
		IPFERROR(120018);
		return EINVAL;
	}

	KMALLOCS(node, ipf_dstnode_t *, sizeof(*node) + dest.fd_name);
	if (node == NULL) {
		softd->stats.ipls_nomem++;
		IPFERROR(120008);
		return ENOMEM;
	}
	bzero((char *)node, sizeof(*node) + dest.fd_name);

	bcopy(&dest, &node->ipfd_dest, sizeof(dest));
	node->ipfd_size = sizeof(*node) + dest.fd_name;

	if (dest.fd_name > 0) {
		/*
		 * fd_name starts out as the length of the string to copy
		 * in (including \0) and ends up being the offset from
		 * fd_names (0).
		 */
		err = COPYIN((char *)op->iplo_struct + sizeof(dest),
			     node->ipfd_names, dest.fd_name);
		if (err != 0) {
			IPFERROR(120017);
			KFREES(node, node->ipfd_size);
			return EFAULT;
		}
		node->ipfd_dest.fd_name = 0;
	} else {
		node->ipfd_dest.fd_name = -1;
	}

	if (d->ipld_nodes == d->ipld_maxnodes) {
		KMALLOCS(nodes, ipf_dstnode_t **,
			 sizeof(*nodes) * (d->ipld_maxnodes + 1));
		if (nodes == NULL) {
			softd->stats.ipls_nomem++;
			IPFERROR(120022);
			KFREES(node, node->ipfd_size);
			return ENOMEM;
		}
		if (d->ipld_dests != NULL) {
			bcopy(d->ipld_dests, nodes,
			      sizeof(*nodes) * d->ipld_maxnodes);
			KFREES(d->ipld_dests, sizeof(*nodes) * d->ipld_nodes);
			nodes[0]->ipfd_pnext = nodes;
		}
		d->ipld_dests = nodes;
		d->ipld_maxnodes++;
	}
	d->ipld_dests[d->ipld_nodes] = node;
	d->ipld_nodes++;

	if (d->ipld_nodes == 1) {
		node->ipfd_pnext = d->ipld_dests;
	} else if (d->ipld_nodes > 1) {
		node->ipfd_pnext = &d->ipld_dests[d->ipld_nodes - 2]->ipfd_next;
	}
	*node->ipfd_pnext = node;

	MUTEX_INIT(&node->ipfd_lock, "ipf dst node lock");
	node->ipfd_uid = uid;
	node->ipfd_ref = 1;
	if (node->ipfd_dest.fd_name == 0)
		(void) ipf_resolvedest(softc, node->ipfd_names,
				       &node->ipfd_dest, AF_INET);
#ifdef USE_INET6
	if (node->ipfd_dest.fd_name == 0 &&
	    node->ipfd_dest.fd_ptr == (void *)-1)
		(void) ipf_resolvedest(softc, node->ipfd_names,
				       &node->ipfd_dest, AF_INET6);
#endif

	softd->stats.ipls_numnodes++;

	return 0;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_node_deref                                      */
/* Returns:     int - 0 = success, else error                               */
/* Parameters:  arg(I)  - pointer to local context to use                   */
/*              node(I) - pointer to destionation node to free              */
/*                                                                          */
/* Dereference the use count by one. If it drops to zero then we can assume */
/* that it has been removed from any lists/tables and is ripe for freeing.  */
/* The pointer to context is required for the purpose of maintaining        */
/* statistics.                                                              */
/* ------------------------------------------------------------------------ */
static int
ipf_dstlist_node_deref(arg, node)
	void *arg;
	ipf_dstnode_t *node;
{
	ipf_dstl_softc_t *softd = arg;
	int ref;

	MUTEX_ENTER(&node->ipfd_lock);
	ref = --node->ipfd_ref;
	MUTEX_EXIT(&node->ipfd_lock);

	if (ref > 0)
		return 0;

	if ((node->ipfd_flags & IPDST_DELETE) != 0)
		softd->stats.ipls_numderefnodes--;
	MUTEX_DESTROY(&node->ipfd_lock);
	KFREES(node, node->ipfd_size);
	softd->stats.ipls_numnodes--;

	return 0;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_node_del                                        */
/* Returns:     int      - 0 = success, else error                          */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*              op(I)    - pointer to lookup operation data                 */
/*              uid(I)   - uid of process doing the ioctl                   */
/*                                                                          */
/* Look for a matching destination node on the named table and free it if   */
/* found. Because the name embedded in the frdest_t is variable in length,  */
/* it is necessary to allocate some memory locally, to complete this op.    */
/* ------------------------------------------------------------------------ */
static int
ipf_dstlist_node_del(softc, arg, op, uid)
	ipf_main_softc_t *softc;
	void *arg;
	iplookupop_t *op;
	int uid;
{
	ipf_dstl_softc_t *softd = arg;
	ipf_dstnode_t *node;
	frdest_t frd, *temp;
	ippool_dst_t *d;
	size_t size;
	int err;

	d = ipf_dstlist_table_find(arg, op->iplo_unit, op->iplo_name);
	if (d == NULL) {
		IPFERROR(120012);
		return ESRCH;
	}

	err = COPYIN(op->iplo_struct, &frd, sizeof(frd));
	if (err != 0) {
		IPFERROR(120011);
		return EFAULT;
	}

	size = sizeof(*temp) + frd.fd_name;
	KMALLOCS(temp, frdest_t *, size);
	if (temp == NULL) {
		softd->stats.ipls_nomem++;
		IPFERROR(120026);
		return ENOMEM;
	}

	err = COPYIN(op->iplo_struct, temp, size);
	if (err != 0) {
		IPFERROR(120027);
		return EFAULT;
	}

	MUTEX_ENTER(&d->ipld_lock);
	for (node = *d->ipld_dests; node != NULL; node = node->ipfd_next) {
		if ((uid != 0) && (node->ipfd_uid != uid))
			continue;
		if (node->ipfd_size != size)
			continue;
		if (!bcmp(&node->ipfd_dest.fd_ip6, &frd.fd_ip6,
			  size - offsetof(frdest_t, fd_ip6))) {
			ipf_dstlist_node_free(softd, d, node);
			MUTEX_EXIT(&d->ipld_lock);
			KFREES(temp, size);
			return 0;
		}
	}
	MUTEX_EXIT(&d->ipld_lock);
	KFREES(temp, size);

	return ESRCH;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_node_free                                       */
/* Returns:     Nil                                                         */
/* Parameters:  softd(I) - pointer to the destination list context          */
/*              d(I)     - pointer to destination list                      */
/*              node(I)  - pointer to node to free                          */
/* Locks:       MUTEX(ipld_lock) or WRITE(ipf_poolrw)                       */
/*                                                                          */
/* Free the destination node by first removing it from any lists and then   */
/* checking if this was the last reference held to the object. While the    */
/* array of pointers to nodes is compacted, its size isn't reduced (by way  */
/* of allocating a new smaller one and copying) because the belief is that  */
/* it is likely the array will again reach that size.                       */
/* ------------------------------------------------------------------------ */
static void
ipf_dstlist_node_free(softd, d, node)
	ipf_dstl_softc_t *softd;
	ippool_dst_t *d;
	ipf_dstnode_t *node;
{
	int i;

	/*
	 * Compact the array of pointers to nodes.
	 */
	for (i = 0; i < d->ipld_nodes; i++)
		if (d->ipld_dests[i] == node)
			break;
	if (d->ipld_nodes - i > 1) {
		bcopy(&d->ipld_dests[i + 1], &d->ipld_dests[i],
		      sizeof(*d->ipld_dests) * (d->ipld_nodes - i - 1));
	}
	d->ipld_nodes--;

	if (node->ipfd_pnext != NULL)
		*node->ipfd_pnext = node->ipfd_next;
	if (node->ipfd_next != NULL)
		node->ipfd_next->ipfd_pnext = node->ipfd_pnext;
	node->ipfd_pnext = NULL;
	node->ipfd_next = NULL;

	if ((node->ipfd_flags & IPDST_DELETE) == 0) {
		softd->stats.ipls_numderefnodes++;
		node->ipfd_flags |= IPDST_DELETE;
	}

	ipf_dstlist_node_deref(softd, node);
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_stats_get                                       */
/* Returns:     int - 0 = success, else error                               */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*              op(I)    - pointer to lookup operation data                 */
/*                                                                          */
/* Return the current statistics for destination lists. This may be for all */
/* of them or just information pertaining to a particular table.            */
/* ------------------------------------------------------------------------ */
/*ARGSUSED*/
static int
ipf_dstlist_stats_get(softc, arg, op)
	ipf_main_softc_t *softc;
	void *arg;
	iplookupop_t *op;
{
	ipf_dstl_softc_t *softd = arg;
	ipf_dstl_stat_t stats;
	int unit, i, err = 0;

	if (op->iplo_size != sizeof(ipf_dstl_stat_t)) {
		IPFERROR(120023);
		return EINVAL;
	}

	stats = softd->stats;
	unit = op->iplo_unit;
	if (unit == IPL_LOGALL) {
		for (i = 0; i <= IPL_LOGMAX; i++)
			stats.ipls_list[i] = softd->dstlist[i];
	} else if (unit >= 0 && unit <= IPL_LOGMAX) {
		void *ptr;

		if (op->iplo_name[0] != '\0')
			ptr = ipf_dstlist_table_find(softd, unit,
						     op->iplo_name);
		else
			ptr = softd->dstlist[unit + 1];
		stats.ipls_list[unit] = ptr;
	} else {
		IPFERROR(120024);
		err = EINVAL;
	}

	if (err == 0) {
		err = COPYOUT(&stats, op->iplo_struct, sizeof(stats));
		if (err != 0) {
			IPFERROR(120025);
			return EFAULT;
		}
	}
	return 0;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_table_add                                       */
/* Returns:     int      - 0 = success, else error                          */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*              op(I)    - pointer to lookup operation data                 */
/*                                                                          */
/* Add a new destination table to the list of those available for the given */
/* device. Because we seldom operate on these objects (find/add/delete),    */
/* they are just kept in a simple linked list.                              */
/* ------------------------------------------------------------------------ */
static int
ipf_dstlist_table_add(softc, arg, op)
	ipf_main_softc_t *softc;
	void *arg;
	iplookupop_t *op;
{
	ipf_dstl_softc_t *softd = arg;
	ippool_dst_t user, *d, *new;
	int unit, err;

	d = ipf_dstlist_table_find(arg, op->iplo_unit, op->iplo_name);
	if (d != NULL) {
		IPFERROR(120013);
		return EEXIST;
	}

	err = COPYIN(op->iplo_struct, &user, sizeof(user));
	if (err != 0) {
		IPFERROR(120021);
		return EFAULT;
	}

	KMALLOC(new, ippool_dst_t *);
	if (new == NULL) {
		softd->stats.ipls_nomem++;
		IPFERROR(120014);
		return ENOMEM;
	}
	bzero((char *)new, sizeof(*new));

	MUTEX_INIT(&new->ipld_lock, "ipf dst table lock");

	strncpy(new->ipld_name, op->iplo_name, FR_GROUPLEN);
	unit = op->iplo_unit;
	new->ipld_unit = unit;
	new->ipld_policy = user.ipld_policy;
	new->ipld_seed = ipf_random();
	new->ipld_ref = 1;

	new->ipld_pnext = softd->tails[unit + 1];
	*softd->tails[unit + 1] = new;
	softd->tails[unit + 1] = &new->ipld_next;
	softd->stats.ipls_numlists++;

	return 0;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_table_del                                       */
/* Returns:     int - 0 = success, else error                               */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*              op(I)    - pointer to lookup operation data                 */
/*                                                                          */
/* Find a named destinstion list table and delete it. If there are other    */
/* references to it, the caller isn't told.                                 */
/* ------------------------------------------------------------------------ */
static int
ipf_dstlist_table_del(softc, arg, op)
	ipf_main_softc_t *softc;
	void *arg;
	iplookupop_t *op;
{
	ippool_dst_t *d;

	d = ipf_dstlist_table_find(arg, op->iplo_unit, op->iplo_name);
	if (d == NULL) {
		IPFERROR(120015);
		return ESRCH;
	}

	if (d->ipld_dests != NULL) {
		IPFERROR(120016);
		return EBUSY;
	}

	ipf_dstlist_table_remove(softc, arg, d);

	return 0;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_table_remove                                    */
/* Returns:     Nil                                                         */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              softd(I) - pointer to the destination list context          */
/*              d(I)     - pointer to destination list                      */
/*                                                                          */
/* Remove a given destination list from existance. While the IPDST_DELETE   */
/* flag is set every time we call this function and the reference count is  */
/* non-zero, the "numdereflists" counter is always incremented because the  */
/* decision about whether it will be freed or not is not made here. This    */
/* means that the only action the code can take here is to treat it as if   */
/* it will become a detached.                                               */
/* ------------------------------------------------------------------------ */
static void
ipf_dstlist_table_remove(softc, softd, d)
	ipf_main_softc_t *softc;
	ipf_dstl_softc_t *softd;
	ippool_dst_t *d;
{

	if (softd->tails[d->ipld_unit + 1] == &d->ipld_next)
		softd->tails[d->ipld_unit + 1] = d->ipld_pnext;

	if (d->ipld_pnext != NULL)
		*d->ipld_pnext = d->ipld_next;
	if (d->ipld_next != NULL)
		d->ipld_next->ipld_pnext = d->ipld_pnext;
	d->ipld_pnext = NULL;
	d->ipld_next = NULL;

	ipf_dstlist_table_clearnodes(softd, d);

	softd->stats.ipls_numdereflists++;
	d->ipld_flags |= IPDST_DELETE;

	ipf_dstlist_table_deref(softc, softd, d);
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_table_free                                      */
/* Returns:     Nil                                                         */
/* Parameters:  softd(I) - pointer to the destination list context          */
/*              d(I)   - pointer to destination list                        */
/*                                                                          */
/* Free up a destination list data structure and any other memory that was  */
/* directly allocated as part of creating it. Individual destination list   */
/* nodes are not freed. It is assumed the caller will have already emptied  */
/* the destination list.                                                    */
/* ------------------------------------------------------------------------ */
static void
ipf_dstlist_table_free(softd, d)
	ipf_dstl_softc_t *softd;
	ippool_dst_t *d;
{
	MUTEX_DESTROY(&d->ipld_lock);

	if ((d->ipld_flags & IPDST_DELETE) != 0)
		softd->stats.ipls_numdereflists--;
	softd->stats.ipls_numlists--;

	if (d->ipld_dests != NULL) {
		KFREES(d->ipld_dests,
		       d->ipld_maxnodes * sizeof(*d->ipld_dests));
	}

	KFREE(d);
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_table_deref                                     */
/* Returns:     int - 0 = success, else error                               */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*              op(I)    - pointer to lookup operation data                 */
/*                                                                          */
/* Drops the reference count on a destination list table object and free's  */
/* it if 0 has been reached.                                                */
/* ------------------------------------------------------------------------ */
static int
ipf_dstlist_table_deref(softc, arg, table)
	ipf_main_softc_t *softc;
	void *arg;
	void *table;
{
	ippool_dst_t *d = table;

	d->ipld_ref--;
	if (d->ipld_ref > 0)
		return d->ipld_ref;

	ipf_dstlist_table_free(arg, d);

	return 0;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_table_clearnodes                                */
/* Returns:     Nil                                                         */
/* Parameters:  softd(I) - pointer to the destination list context          */
/*              dst(I)   - pointer to destination list                      */
/*                                                                          */
/* Free all of the destination nodes attached to the given table.           */
/* ------------------------------------------------------------------------ */
static void
ipf_dstlist_table_clearnodes(softd, dst)
	ipf_dstl_softc_t *softd;
	ippool_dst_t *dst;
{
	ipf_dstnode_t *node;

	if (dst->ipld_dests == NULL)
		return;

	while ((node = *dst->ipld_dests) != NULL) {
		ipf_dstlist_node_free(softd, dst, node);
	}
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_table_find                                      */
/* Returns:     int      - 0 = success, else error                          */
/* Parameters:  arg(I)   - pointer to local context to use                  */
/*              unit(I)  - device we are working with                       */
/*              name(I)  - destination table name to find                   */
/*                                                                          */
/* Return a pointer to a destination table that matches the unit+name that  */
/* is passed in.                                                            */
/* ------------------------------------------------------------------------ */
static void *
ipf_dstlist_table_find(arg, unit, name)
	void *arg;
	int unit;
	char *name;
{
	ipf_dstl_softc_t *softd = arg;
	ippool_dst_t *d;

	for (d = softd->dstlist[unit + 1]; d != NULL; d = d->ipld_next) {
		if ((d->ipld_unit == unit) &&
		    !strncmp(d->ipld_name, name, FR_GROUPLEN)) {
			return d;
		}
	}

	return NULL;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_select_ref                                      */
/* Returns:     void *   - NULL = failure, else pointer to table            */
/* Parameters:  arg(I)   - pointer to local context to use                  */
/*              unit(I)  - device we are working with                       */
/*              name(I)  - destination table name to find                   */
/*                                                                          */
/* Attempt to find a destination table that matches the name passed in and  */
/* if successful, bump up the reference count on it because we intend to    */
/* store the pointer to it somewhere else.                                  */
/* ------------------------------------------------------------------------ */
static void *
ipf_dstlist_select_ref(arg, unit, name)
	void *arg;
	int unit;
	char *name;
{
	ippool_dst_t *d;

	d = ipf_dstlist_table_find(arg, unit, name);
	if (d != NULL) {
		MUTEX_ENTER(&d->ipld_lock);
		d->ipld_ref++;
		MUTEX_EXIT(&d->ipld_lock);
	}
	return d;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_select                                          */
/* Returns:     void * - NULL = failure, else pointer to table              */
/* Parameters:  fin(I) - pointer to packet information                      */
/*              d(I)   - pointer to destination list                        */
/*                                                                          */
/* Find the next node in the destination list to be used according to the   */
/* defined policy. Of these, "connection" is the most expensive policy to   */
/* implement as it always looks for the node with the least number of       */
/* connections associated with it.                                          */
/*                                                                          */
/* The hashes exclude the port numbers so that all protocols map to the     */
/* same destination. Otherwise, someone doing a ping would target a         */
/* different server than their TCP connection, etc. MD-5 is used to         */
/* transform the addressese into something random that the other end could  */
/* not easily guess and use in an attack. ipld_seed introduces an unknown   */
/* into the hash calculation to increase the difficult of an attacker       */
/* guessing the bucket.                                                     */
/*                                                                          */
/* One final comment: mixing different address families in a single pool    */
/* will currently result in failures as the address family of the node is   */
/* only matched up with that in the packet as the last step. While this can */
/* be coded around for the weighted connection and round-robin models, it   */
/* cannot be supported for the hash/random models as they do not search and */
/* nor is the algorithm conducive to searching.                             */
/* ------------------------------------------------------------------------ */
static ipf_dstnode_t *
ipf_dstlist_select(fin, d)
	fr_info_t *fin;
	ippool_dst_t *d;
{
	ipf_dstnode_t *node, *sel;
	int connects;
	u_32_t hash[4];
	MD5_CTX ctx;
	int family;
	int x;

	if (d->ipld_dests == NULL || *d->ipld_dests == NULL)
		return NULL;

	family = fin->fin_family;

	MUTEX_ENTER(&d->ipld_lock);

	switch (d->ipld_policy)
	{
	case IPLDP_ROUNDROBIN:
		sel = d->ipld_selected;
		if (sel == NULL) {
			sel = *d->ipld_dests;
		} else {
			sel = sel->ipfd_next;
			if (sel == NULL)
				sel = *d->ipld_dests;
		}
		break;

	case IPLDP_CONNECTION:
		if (d->ipld_selected == NULL) {
			sel = *d->ipld_dests;
			break;
		}

		sel = d->ipld_selected;
		connects = 0x7fffffff;
		node = sel->ipfd_next;
		if (node == NULL)
			node = *d->ipld_dests;
		while (node != d->ipld_selected) {
			if (node->ipfd_states == 0) {
				sel = node;
				break;
			}
			if (node->ipfd_states < connects) {
				sel = node;
				connects = node->ipfd_states;
			}
			node = node->ipfd_next;
			if (node == NULL)
				node = *d->ipld_dests;
		}
		break;

	case IPLDP_RANDOM :
		x = ipf_random() % d->ipld_nodes;
		sel = d->ipld_dests[x];
		break;

	case IPLDP_HASHED :
		MD5Init(&ctx);
		MD5Update(&ctx, (u_char *)&d->ipld_seed, sizeof(d->ipld_seed));
		MD5Update(&ctx, (u_char *)&fin->fin_src6,
			  sizeof(fin->fin_src6));
		MD5Update(&ctx, (u_char *)&fin->fin_dst6,
			  sizeof(fin->fin_dst6));
		MD5Final((u_char *)hash, &ctx);
		x = ntohl(hash[0]) % d->ipld_nodes;
		sel = d->ipld_dests[x];
		break;

	case IPLDP_SRCHASH :
		MD5Init(&ctx);
		MD5Update(&ctx, (u_char *)&d->ipld_seed, sizeof(d->ipld_seed));
		MD5Update(&ctx, (u_char *)&fin->fin_src6,
			  sizeof(fin->fin_src6));
		MD5Final((u_char *)hash, &ctx);
		x = ntohl(hash[0]) % d->ipld_nodes;
		sel = d->ipld_dests[x];
		break;

	case IPLDP_DSTHASH :
		MD5Init(&ctx);
		MD5Update(&ctx, (u_char *)&d->ipld_seed, sizeof(d->ipld_seed));
		MD5Update(&ctx, (u_char *)&fin->fin_dst6,
			  sizeof(fin->fin_dst6));
		MD5Final((u_char *)hash, &ctx);
		x = ntohl(hash[0]) % d->ipld_nodes;
		sel = d->ipld_dests[x];
		break;

	default :
		sel = NULL;
		break;
	}

	if (sel->ipfd_dest.fd_addr.adf_family != family)
		sel = NULL;
	d->ipld_selected = sel;

	MUTEX_EXIT(&d->ipld_lock);

	return sel;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_select_node                                     */
/* Returns:     int      - -1 == failure, 0 == success                      */
/* Parameters:  fin(I)   - pointer to packet information                    */
/*              group(I) - destination pool to search                       */
/*              addr(I)  - pointer to store selected address                */
/*              pfdp(O)  - pointer to storage for selected destination node */
/*                                                                          */
/* This function is only responsible for obtaining the next IP address for  */
/* use and storing it in the caller's address space (addr). "addr" is only  */
/* used for storage if pfdp is NULL. No permanent reference is currently    */
/* kept on the node.                                                        */
/* ------------------------------------------------------------------------ */
int
ipf_dstlist_select_node(fin, group, addr, pfdp)
	fr_info_t *fin;
	void *group;
	u_32_t *addr;
	frdest_t *pfdp;
{
#ifdef USE_MUTEXES
	ipf_main_softc_t *softc = fin->fin_main_soft;
#endif
	ippool_dst_t *d = group;
	ipf_dstnode_t *node;
	frdest_t *fdp;

	READ_ENTER(&softc->ipf_poolrw);

	node = ipf_dstlist_select(fin, d);
	if (node == NULL) {
		RWLOCK_EXIT(&softc->ipf_poolrw);
		return -1;
	}

	if (pfdp != NULL) {
		bcopy(&node->ipfd_dest, pfdp, sizeof(*pfdp));
	} else {
		if (fin->fin_family == AF_INET) {
			addr[0] = node->ipfd_dest.fd_addr.adf_addr.i6[0];
		} else if (fin->fin_family == AF_INET6) {
			addr[0] = node->ipfd_dest.fd_addr.adf_addr.i6[0];
			addr[1] = node->ipfd_dest.fd_addr.adf_addr.i6[1];
			addr[2] = node->ipfd_dest.fd_addr.adf_addr.i6[2];
			addr[3] = node->ipfd_dest.fd_addr.adf_addr.i6[3];
		}
	}

	fdp = &node->ipfd_dest;
	if (fdp->fd_ptr == NULL)
		fdp->fd_ptr = fin->fin_ifp;

	MUTEX_ENTER(&node->ipfd_lock);
	node->ipfd_states++;
	MUTEX_EXIT(&node->ipfd_lock);

	RWLOCK_EXIT(&softc->ipf_poolrw);

	return 0;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_expire                                          */
/* Returns:     Nil                                                         */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*                                                                          */
/* There are currently no objects to expire in destination lists.           */
/* ------------------------------------------------------------------------ */
static void
ipf_dstlist_expire(softc, arg)
	ipf_main_softc_t *softc;
	void *arg;
{
	return;
}


/* ------------------------------------------------------------------------ */
/* Function:    ipf_dstlist_sync                                            */
/* Returns:     Nil                                                         */
/* Parameters:  softc(I) - pointer to soft context main structure           */
/*              arg(I)   - pointer to local context to use                  */
/*                                                                          */
/* When a network interface appears or disappears, we need to revalidate    */
/* all of the network interface names that have been configured as a target */
/* in a destination list.                                                   */
/* ------------------------------------------------------------------------ */
void
ipf_dstlist_sync(softc, arg)
	ipf_main_softc_t *softc;
	void *arg;
{
	ipf_dstl_softc_t *softd = arg;
	ipf_dstnode_t *node;
	ippool_dst_t *list;
	int i;
	int j;

	for (i = 0; i < IPL_LOGMAX; i++) {
		for (list = softd->dstlist[i]; list != NULL;
		     list = list->ipld_next) {
			for (j = 0; j < list->ipld_maxnodes; j++) {
				node = list->ipld_dests[j];
				if (node == NULL)
					continue;
				if (node->ipfd_dest.fd_name == -1)
					continue;
				(void) ipf_resolvedest(softc,
						       node->ipfd_names,
						       &node->ipfd_dest,
						       AF_INET);
			}
		}
	}
}