/*
 * Copyright (c) 2002 INRIA. All rights reserved.
 *
 * Implementation of Internet Group Management Protocol, Version 3.
 * Developed by Hitoshi Asaeda, INRIA, February 2002.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. 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.
 * 3. Neither the name of INRIA nor the names of its contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE 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 INSTITUTE 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.
 */

#include "opt_inet.h"
#include "opt_mrouting.h"

#include <sys/param.h>
#include <sys/errno.h>
#include <sys/malloc.h>
#include <sys/socket.h>
#include <sys/socketvar.h>
#include <sys/systm.h>
#include <sys/protosw.h>

#include <net/if.h>

#include <netinet/in_systm.h>
#include <netinet/in.h>
#include <netinet/in_var.h>
#include <netinet/ip.h>
#include <netinet/ip_var.h>
#include <netinet/in_pcb.h>
#include <netinet/ip_mroute.h>
#include <netinet/igmp_var.h>

static int in_get_msfstate(struct in_multi *);
static int in_merge_inc_cur(struct in_multi *);
static int in_copy_inc_cur(struct in_multi *);
static void in_clear_report(struct in_multi *, u_int);
static int in_merge_report(struct in_multi *, struct in_addr_source *, u_int8_t);

#define IAS_LIST_ALLOC(iasl) do {					\
	MALLOC((iasl), struct in_addr_slist *,				\
		sizeof(struct in_addr_slist), M_MSFILTER, M_NOWAIT);	\
	if ((iasl) == NULL) {						\
		error = ENOBUFS;					\
		break;							\
	}								\
	bzero((iasl), sizeof(struct in_addr_slist));			\
	MALLOC((iasl)->head, struct ias_head *,				\
		sizeof(struct ias_head), M_MSFILTER, M_NOWAIT);		\
	if ((iasl)->head == NULL) {					\
		FREE((iasl), M_MSFILTER);				\
		error = ENOBUFS;					\
		break;							\
	}								\
	LIST_INIT((iasl)->head);					\
	(iasl)->numsrc = 0;						\
	(iasl)->mode = 0;						\
} while (/*CONSTCOND*/ 0)

/*
 * Add source addresses to multicast address record.
 */
int
in_addmultisrc(struct in_multi *inm, u_int mode, struct sockaddr_storage *ss)
{
	struct in_addr_slist *iasl = NULL;
	struct in_addr_source *ias;
	struct sockaddr_in *sin;
	int error = 0;
	extern int igmpmaxsrcfilter;

	if (inm->inm_source == NULL) {
		/*
		 * Even if upstream router does not control IGMPv3, inm_source
		 * is allocated, in order to change the behavior to an IGMPv3
		 * capable node gracefully.
		 */
		MALLOC(inm->inm_source, struct in_multi_source *,
				sizeof(struct in_multi_source),
				M_MSFILTER, M_NOWAIT);
		if (inm->inm_source == NULL)
			return ENOBUFS;
		bzero(inm->inm_source, sizeof(struct in_multi_source));

		IAS_LIST_ALLOC(inm->inm_source->ims_cur);
		if (error != 0) {
			FREE(inm->inm_source, M_MSFILTER);
			return error;
		}
		IAS_LIST_ALLOC(inm->inm_source->ims_rec);
		if (error != 0) {
			FREE(inm->inm_source->ims_cur->head, M_MSFILTER);
			FREE(inm->inm_source->ims_cur, M_MSFILTER);
			FREE(inm->inm_source, M_MSFILTER);
			return error;
		}
		inm->inm_source->ims_mode = MCAST_INCLUDE;
		inm->inm_source->ims_grpjoin = 0;
		inm->inm_source->ims_timer = 0;
		inm->inm_source->ims_robvar = 0;
		inm->inm_state = IGMP_IDLE_MEMBER; /* set by joingroup()? */
	}

	/*
	 * If mode is MCAST_EXCLUDE, the request must be (*,G) join.
	 */
	if (mode == MCAST_EXCLUDE) {
		++inm->inm_source->ims_grpjoin;
		goto after_source_list_addition;
	}

	/*
	 * INCLUDE mode join requires a non-INADDR_ANY source address.
	 */
	if (ss == NULL)
		return EINVAL;
	sin = SIN(ss);
	if (SIN_ADDR(sin) == INADDR_ANY)
		return EINVAL;

	if (inm->inm_source->ims_in == NULL ||
	    LIST_EMPTY(inm->inm_source->ims_in->head)) {
		MALLOC(ias, struct in_addr_source *, sizeof(*ias),
		    M_MSFILTER, M_NOWAIT);
		if (ias == NULL)
			return ENOBUFS;

		bcopy(sin, &ias->ias_addr, sin->sin_len);
		ias->ias_refcount = 1;
		if (inm->inm_source->ims_in == NULL) {
			IAS_LIST_ALLOC(inm->inm_source->ims_in);
			if (error != 0) {
				FREE(ias, M_MSFILTER);
				return error;
			}
		}
		LIST_INSERT_HEAD(inm->inm_source->ims_in->head, ias, ias_list);
		inm->inm_source->ims_in->numsrc = 1;
		goto after_source_list_addition;
	}

	iasl = inm->inm_source->ims_in;
	/*
	 * Kernel accepts to keep as many requested sources as possible,
	 * but if the number of the limitation is already reached to max,
	 * it stops this procedure and returns error.
	 */
	if (iasl->numsrc >= igmpmaxsrcfilter) {
#ifdef IGMPV3_DEBUG
		printf("in_addmultisrc: number of source already reached max filter count.\n");
#endif
		return EOPNOTSUPP; /* XXX */
	}

	if ((error = in_merge_msfaddr(iasl, sin, IMS_ADD_SOURCE)) != 0)
		return error;

after_source_list_addition:
	if ((error = in_get_msfstate(inm)) != 0) {
#ifdef IGMPV3_DEBUG
		printf("in_addmultisrc: in_get_msfstate returns %d\n", error);
#endif
		if (mode == MCAST_EXCLUDE)
			--inm->inm_source->ims_grpjoin;
		else
			in_undomultisrc(inm, ss, IMS_ADD_SOURCE);
		return error;
	}

	return 0;
}

/*
 * Delete source addresses from multicast address record.
 */
int
in_delmultisrc(struct in_multi *inm, u_int mode, struct sockaddr_storage *ss)
{
	struct in_addr_slist *iasl = NULL;
	struct in_addr_source *ias, *nias;
	struct sockaddr_in *sin;
	int error;

	/*
	 * If mode is MCAST_EXCLUDE or MCAST_INCLUDE with null source address,
	 * the request must be (*,G) leave.
	 */
	if ((mode == MCAST_EXCLUDE) && (inm->inm_source->ims_grpjoin > 0)) {
		--inm->inm_source->ims_grpjoin;
		goto after_source_list_deletion;
	} else if ((mode == MCAST_INCLUDE) && (ss == NULL)) {
		goto after_source_list_deletion;
	}

	if (ss == NULL)
		return EINVAL;
	sin = SIN(ss);
	if (SIN_ADDR(sin) == INADDR_ANY)
		return EINVAL;

	if (inm->inm_source->ims_in == NULL ||
	    LIST_EMPTY(inm->inm_source->ims_in->head))
		return EADDRNOTAVAIL;

	iasl = inm->inm_source->ims_in;

	if ((error = in_merge_msfaddr(iasl, sin, IMS_DELETE_SOURCE)) != 0)
		return error; /* XXX should never happen, eliminated?? */

after_source_list_deletion:
	if ((error = in_get_msfstate(inm)) != 0) {
#ifdef IGMPV3_DEBUG
		printf("in_delmultisrc: in_get_msfstate returns %d\n", error);
#endif
		if (mode == MCAST_EXCLUDE)
			++inm->inm_source->ims_grpjoin;
		else
			in_undomultisrc(inm, ss, IMS_DELETE_SOURCE);
		return error; /* XXX should never happen, eliminated?? */
	}

	/*
	 * Each source whose ias_refcount is 0 is removed after the process
	 * to merge each source has done successfully.
	 */
	if (mode == MCAST_INCLUDE) {
		for (ias = LIST_FIRST(iasl->head); ias; ias = nias) {
			nias = LIST_NEXT(ias, ias_list);
			if (ias->ias_refcount == 0) {
				LIST_REMOVE(ias, ias_list);
				FREE(ias, M_MSFILTER);
			}
		}
	}

	return 0;
}

/*
 * Undo INCLUDE source list modification.
 */
void
in_undomultisrc(struct in_multi *inm, struct sockaddr_storage *ss, int req)
{
	struct sockaddr_in *sin;
	struct in_addr_source *ias;
	u_int16_t numsrc = 0;

	sin = SIN(ss);
	LIST_FOREACH(ias, inmm_src->ims_in->head, ias_list) {
		if (SS_COMPARE(&ias->ias_addr, <, sin)) {
			++numsrc;
			continue;
		}
		if (SS_COMPARE(&ias->ias_addr, >, sin)) {
			/* XXX strange. this should never occur. */
			printf("in_undomultisrc: list corrupted. panic!\n");
			continue; /* XXX */
		}

		/* same src addr found */
		if (req == IMS_ADD_SOURCE) {
			if (--ias->ias_refcount == 0) {
				LIST_REMOVE(ias, ias_list);
				FREE(ias, M_MSFILTER);
			} else {
				++numsrc;
			}
		} else { /* IMS_DELETE_SOURCE */
			++ias->ias_refcount;
			++numsrc;
		}
	}

	if (numsrc == 0) {
		FREE(inm->inm_source->ims_in->head, M_MSFILTER);
		FREE(inm->inm_source->ims_in, M_MSFILTER);
		inm->inm_source->ims_in = NULL;
	} else {
		inm->inm_source->ims_in->numsrc = numsrc;
	}
}

/*
 * Get new source filter mode and source list head when the multicast
 * reception state of an interface is changed.
 */
int
in_get_msfstate(struct in_multi *inm)
{
	struct in_addr_source *ias;
	u_int filter;
	u_int8_t cmd;
	int error = 0;

	/* Case 1: Some socket requested (*,G) join. */
	if (inmm_src->ims_grpjoin != 0) {
		/* IN{NULL} -> EX{NULL} */
		if (LIST_EMPTY(inmm_src->ims_cur->head)) {
			if (inmm_src->ims_mode == MCAST_INCLUDE)
				in_clear_all_report(inm);
			goto change_state_1;
		}

		/* IN{non NULL} -> EX{NULL} */
		if (inmm_src->ims_mode == MCAST_INCLUDE)
			in_clear_all_report(inm);

		in_free_msflist(inmm_src->ims_cur);

	change_state_1:
		error = in_merge_report(inm, NULL, CHANGE_TO_EXCLUDE_MODE);
		if (error != 0)
			return error;
		inmm_src->ims_mode = MCAST_EXCLUDE;
		inmm_src->ims_cur->numsrc = 0;
		return 0;
	}

	/* Case 2: All sockets left from this group. */
	if ((inmm_src->ims_in == NULL) ||
	    ((inmm_src->ims_in != NULL) && (inmm_src->ims_in->numsrc == 0))) {
		/* EX{NULL} -> IN{NULL} */
		if (LIST_EMPTY(inmm_src->ims_cur->head)) {
			if (inmm_src->ims_mode == MCAST_EXCLUDE)
				in_clear_all_report(inm);
			goto change_state_2;
		}

		/* IN{non NULL} -> IN{NULL} */
		LIST_FOREACH(ias, inmm_src->ims_cur->head, ias_list) {
			error = in_merge_report(inm, ias, BLOCK_OLD_SOURCES);
			if (error != 0) {
				/*
			 	 * If error occured, clear pending report and
				 * return error.
				 */
				in_clear_report(inm, REPORT_FILTER1);
				return error;
			}
		}

		in_free_msflist(inmm_src->ims_cur);

	change_state_2:
		inmm_src->ims_mode = MCAST_INCLUDE;
		inmm_src->ims_cur->numsrc = 0;
		return 0;
	}

	/* Case 3: INCLUDE with non-NULL source list was set for this group. */
	/* IN{NULL} -> IN{non NULL} or EX{NULL} -> IN{non NULL} */
	if (LIST_EMPTY(inmm_src->ims_cur->head)) {
		/* Note: in_copy_inc_cur(inm) inserts numsrc */
		if ((error = in_copy_inc_cur(inm)) != 0)
			return error;

		if (inm->inm_source->ims_mode == MCAST_INCLUDE) {
			filter = REPORT_FILTER1;
			cmd = ALLOW_NEW_SOURCES;
		} else {
			filter = REPORT_FILTER4;
			cmd = CHANGE_TO_INCLUDE_MODE;
			in_clear_all_report(inm);
		}
		LIST_FOREACH(ias, inmm_src->ims_cur->head, ias_list) {
			error = in_merge_report(inm, ias, cmd);
			if (error != 0) {
				/*
				 * If error occured, clear not only pending
				 * report but curhead, and return error.
				 */
				 in_free_msflist(inmm_src->ims_cur);
				 inmm_src->ims_cur->numsrc = 0;
				 in_clear_report(inm, filter);
				 return error;
			}
		}
		goto change_state_3;
	}

	/* IN{non NULL} -> IN{non NULL} */
	if (inmm_src->ims_mode == MCAST_INCLUDE) {
		/* Note: in_merge_inc_cur(inm) inserts numsrc */
		if ((error = in_merge_inc_cur(inm)) != 0)
			return error;

	}

change_state_3:
	inmm_src->ims_mode = MCAST_INCLUDE;
	return 0;
}

/*
 * Merge INCLUDE source list to the current head. This also merge pending
 * report if needed. All INCLUDE sources are compared with sources of curhead.
 * After this function is completed, new timer for merged report will be set.
 */
static int
in_merge_inc_cur(struct in_multi *inm)
{
	struct ias_head head;
	struct in_addr_source *ias, *curias, *newias, *lastp;
	struct sockaddr_in *sin;
	u_int16_t numsrc; /* temporary */
	int error;

	ias = curias = newias = lastp = NULL;
	numsrc = inmm_src->ims_cur->numsrc;
	LIST_FIRST(&head) = LIST_FIRST(inmm_src->ims_in->head);
	LIST_FOREACH(curias, inmm_src->ims_cur->head, ias_list) {
		lastp = curias;
		LIST_FOREACH(ias, &head, ias_list) {
			sin = &ias->ias_addr;
                       /* sanity check */
			if (curias->ias_addr.sin_family != sin->sin_family)
				continue;
			if (SS_COMPARE(&curias->ias_addr, ==, sin)) {
				if (ias->ias_refcount == 0) {
					error = in_merge_report(inm, curias,
							BLOCK_OLD_SOURCES);
					if (error != 0) {
						in_clear_report(inm,
								REPORT_FILTER1);
						return error;
					}
					/* removed later */
					curias->ias_refcount = 0;
					--numsrc;
				} else {
					/* not be 0 to keep it in ims_cur */
					curias->ias_refcount = 1;
				}
				LIST_FIRST(&head) = LIST_NEXT(ias, ias_list);
				break;
			}

			if (SS_COMPARE(&curias->ias_addr, <, sin)) {
				error = in_merge_report(inm, curias,
							BLOCK_OLD_SOURCES);
				if (error != 0) {
					/*
					 * If error occured, undo curhead
					 * modification, clear pending report,
					 * and return error.
					 * XXX BTW, do we really need to clear
					 * pending report?
					 */
					in_clear_report(inm, REPORT_FILTER1);
					return error;
				}
				curias->ias_refcount = 0;
				--numsrc;
				LIST_FIRST(&head) = ias;
				break;
			}

			/* ias should be recorded in new curhead here */
			MALLOC(newias, struct in_addr_source *, sizeof(*newias),
			       M_MSFILTER, M_NOWAIT);
			if (newias == NULL) {
				in_clear_report(inm, REPORT_FILTER1); /* XXX */
				return ENOBUFS;
			}
			error = in_merge_report(inm, ias, ALLOW_NEW_SOURCES);
			if (error != 0) {
				in_clear_report(inm, REPORT_FILTER1); /* XXX */
				return error;
			}
			bcopy(sin, &newias->ias_addr, sin->sin_len);
			newias->ias_refcount = 1;
			++numsrc;
			LIST_INSERT_BEFORE(curias, newias, ias_list);
		}
		if (!ias) {
			LIST_INIT(&head); /* stop list scan */
			error = in_merge_report(inm, curias, BLOCK_OLD_SOURCES);
			if (error != 0) {
				in_clear_report(inm, REPORT_FILTER1); /* XXX */
				return error;
			}
			curias->ias_refcount = 0;
			--numsrc;
		}
	}

	if (ias == NULL)
		/* already finished merging each ias in curhead */
		goto after_merge_inc_cur;

	LIST_FOREACH(ias, &head, ias_list) {
		if (ias->ias_refcount == 0)
			continue; /* removed from ims_in and not reported */

		MALLOC(newias, struct in_addr_source *, sizeof(*newias),
		       M_MSFILTER, M_NOWAIT);
		if (newias == NULL) {
			in_clear_report(inm, REPORT_FILTER1); /* XXX */
			return ENOBUFS;
		}
		error = in_merge_report(inm, ias, ALLOW_NEW_SOURCES);
		if (error != 0) {
			in_clear_report(inm, REPORT_FILTER1); /* XXX */
			return error;
		}
		newias->ias_addr = ias->ias_addr;
		newias->ias_refcount = 1;
		++numsrc;
		LIST_INSERT_AFTER(lastp, newias, ias_list);
		lastp = newias;
	}

after_merge_inc_cur:
	inmm_src->ims_cur->numsrc = numsrc;

	/* clean up */
	LIST_FOREACH(ias, inmm_src->ims_cur->head, ias_list) {
		if (ias->ias_refcount == 0) {
			LIST_REMOVE(ias, ias_list);
			FREE(ias, M_MSFILTER);
		}
	}

	return 0;
}

/*
 * Copy INCLUDE source list from original head to new one, in order to 
 * make a new current state source list. All INCLUDE sources except
 * ias_refcount = 0 are copied.
 */
static int
in_copy_inc_cur(struct in_multi *inm)
{
	struct in_addr_slist *iasl;
	struct in_addr_source *ias, *newias, *lastp = NULL;
	u_int16_t numsrc; /* temporary */

	iasl = inmm_src->ims_cur;
	numsrc = 0;
	LIST_FOREACH(ias, inmm_src->ims_in->head, ias_list) {
		if (ias->ias_refcount == 0)
			continue;
		MALLOC(newias, struct in_addr_source *, sizeof(*newias),
		       M_MSFILTER, M_NOWAIT);
		if (newias == NULL) {
			in_free_msflist(iasl);
			iasl->numsrc = 0;
			return ENOBUFS;
		}
		if (LIST_EMPTY(iasl->head))
			LIST_INSERT_HEAD(iasl->head, newias, ias_list);
		else
			LIST_INSERT_AFTER(lastp, newias, ias_list);
		newias->ias_addr = ias->ias_addr;
		newias->ias_refcount = 1;
		++numsrc;
		lastp = newias;
	}
	iasl->numsrc = numsrc;
	return 0;
}

void
in_clear_all_report(struct in_multi *inm)
{
	in_clear_report(inm, REPORT_FILTER1); /* covering FILTER2 */
	in_clear_report(inm, REPORT_FILTER3);
	in_clear_report(inm, REPORT_FILTER4);
}

/*
 * If pending source merge was failed, source filter mode and current list
 * head are updated (since these are correct) but new State-Change report
 * will not be sent. That change is notified by responce of later Queries.
 */
static void
in_clear_report(struct in_multi *inm, u_int filter)
{
	if ((filter == REPORT_FILTER1) || (filter == REPORT_FILTER2)) {
		if (inm->inm_source->ims_alw != NULL) {
			in_free_msflist(inm->inm_source->ims_alw);
			inm->inm_source->ims_alw->numsrc = 0;
		}
		if (inm->inm_source->ims_blk != NULL) {
			in_free_msflist(inm->inm_source->ims_blk);
			inm->inm_source->ims_blk->numsrc = 0;
		}
	/*
	 * TO_IN and TO_EX lists must be completely removed, because
	 * both record types can specify NULL source address list.
	 * (TO_IN() means (*,G) leave and TO_EX() means (*,G) join.)
	 */
	} else if (filter == REPORT_FILTER3) {
		if (inm->inm_source->ims_toex != NULL) {
			in_free_msflist(inm->inm_source->ims_toex);
			FREE(inm->inm_source->ims_toex->head, M_MSFILTER);
			FREE(inm->inm_source->ims_toex, M_MSFILTER);
			inm->inm_source->ims_toex = NULL;
		}
	} else {
		if (inm->inm_source->ims_toin != NULL) {
			in_free_msflist(inm->inm_source->ims_toin);
			FREE(inm->inm_source->ims_toin->head, M_MSFILTER);
			FREE(inm->inm_source->ims_toin, M_MSFILTER);
			inm->inm_source->ims_toin = NULL;
		}
	}
}

/*
 * The transmission of the merged State-Change Report terminates
 * retransmissions of the earlier State-Change Reports for the same multicast
 * address, and becomes the first of [Robustness Variable] transmissions of
 * State-Change Reports.
 */
static int
in_merge_report(struct in_multi *inm, struct in_addr_source *ias, u_int8_t type)
{
	struct in_addr_source *newias;
	struct sockaddr_in *sin;
	int error = 0;

	switch (type) {
	case ALLOW_NEW_SOURCES:
		if (ias == NULL)
			return EOPNOTSUPP;
		else
			sin = &ias->ias_addr;
		if (inm->inm_source->ims_alw == NULL) {
			IAS_LIST_ALLOC(inm->inm_source->ims_alw);
			if (error != 0)
				return error;
		}
		if (LIST_EMPTY(inm->inm_source->ims_alw->head)) {
			MALLOC(newias, struct in_addr_source *,
				sizeof(*newias), M_MSFILTER, M_NOWAIT);
			if (newias == NULL) {
				/*
				 * We don't remove ims_alw created above,
				 * since it may be needed to re-create later.
				 * This will be finally cleaned when every
				 * application leaves from this group.
				 */
				return ENOBUFS;
			}
			bcopy(sin, &newias->ias_addr, sin->sin_len);
			newias->ias_refcount = 1;
			LIST_INSERT_HEAD(inm->inm_source->ims_alw->head,
					 newias, ias_list);
			inm->inm_source->ims_alw->numsrc = 1;
		} else if ((error = in_merge_msfaddr(inm->inm_source->ims_alw,
					sin, IMS_ADD_SOURCE)) != 0) {
			return error;
		}

		return 0;

	case BLOCK_OLD_SOURCES:
		if (ias == NULL)
			return EOPNOTSUPP;
		else
			sin = &ias->ias_addr;
		if (inm->inm_source->ims_blk == NULL) {
			IAS_LIST_ALLOC(inm->inm_source->ims_blk);
			if (error != 0)
				return error;
		}
		if (LIST_EMPTY(inm->inm_source->ims_blk->head)) {
			MALLOC(newias, struct in_addr_source *,
				sizeof(*newias), M_MSFILTER, M_NOWAIT);
			if (newias == NULL) {
				return ENOBUFS;
			}
			bcopy(sin, &newias->ias_addr, sin->sin_len);
			newias->ias_refcount = 1;
			LIST_INSERT_HEAD(inm->inm_source->ims_blk->head,
					 newias, ias_list);
			inm->inm_source->ims_blk->numsrc = 1;
		} else if ((error = in_merge_msfaddr(inm->inm_source->ims_blk,
					sin, IMS_ADD_SOURCE)) != 0) {
			return error;
		}

		return 0;

	case CHANGE_TO_INCLUDE_MODE:
		if (ias == NULL)
			sin = NULL;
		else
			sin = &ias->ias_addr;
		if ((inm->inm_source->ims_toin == NULL) ||
		    (LIST_EMPTY(inm->inm_source->ims_toin->head))) {
			if (inm->inm_source->ims_toin == NULL) {
				IAS_LIST_ALLOC(inm->inm_source->ims_toin);
				if (error != 0)
					return error;
				MALLOC(newias, struct in_addr_source *,
					sizeof(*newias), M_MSFILTER, M_NOWAIT);
				if (newias == NULL) {
					return ENOBUFS;
				}
			} else {
				MALLOC(newias, struct in_addr_source *,
					sizeof(*newias), M_MSFILTER, M_NOWAIT);
				if (newias == NULL) {
					return ENOBUFS;
				}
			}
			if (ias != NULL) {
				bcopy(sin, &newias->ias_addr, sin->sin_len);
				newias->ias_refcount = 1;
				LIST_INSERT_HEAD
					(inm->inm_source->ims_toin->head,
					 newias, ias_list);
				inm->inm_source->ims_toin->numsrc = 1;
			}
		} else if ((error = in_merge_msfaddr(inm->inm_source->ims_toin,
					sin, IMS_ADD_SOURCE)) != 0) {
			return error;
		}

		return 0;

	case CHANGE_TO_EXCLUDE_MODE:
		if (ias != NULL)
			return EOPNOTSUPP; /* Must be NULL in LW-IGMPv3 */
		if (inm->inm_source->ims_toex == NULL) {
			IAS_LIST_ALLOC(inm->inm_source->ims_toex);
			if (error != 0)
				return error;
			inm->inm_source->ims_toex->numsrc = 0;
		}

		return 0;
	}
	return EOPNOTSUPP; /* XXX */
}

void
in_free_all_msflist(struct in_multi *inm)
{
	if ((inm == NULL) || (inm->inm_source == NULL))
		return;

	if (inm->inm_source->ims_cur != NULL) {
		in_free_msflist(inm->inm_source->ims_cur);
		if (inm->inm_source->ims_cur->head != NULL)
			FREE(inm->inm_source->ims_cur->head, M_MSFILTER);
		FREE(inm->inm_source->ims_cur, M_MSFILTER); 
	}
	if (inm->inm_source->ims_rec != NULL) {
		in_free_msflist(inm->inm_source->ims_rec);
		if (inm->inm_source->ims_rec->head != NULL)
			FREE(inm->inm_source->ims_rec->head, M_MSFILTER);
		FREE(inm->inm_source->ims_rec, M_MSFILTER);
	}
	if (inm->inm_source->ims_in != NULL) {
		in_free_msflist(inm->inm_source->ims_in);
		if (inm->inm_source->ims_in->head != NULL)
			FREE(inm->inm_source->ims_in->head, M_MSFILTER); 
		FREE(inm->inm_source->ims_in, M_MSFILTER);
	}
	if (inm->inm_source->ims_alw != NULL) {
		in_free_msflist(inm->inm_source->ims_alw);
		if (inm->inm_source->ims_alw->head != NULL)
			FREE(inm->inm_source->ims_alw->head, M_MSFILTER);
		FREE(inm->inm_source->ims_alw, M_MSFILTER);
	}
	if (inm->inm_source->ims_blk != NULL) {
		in_free_msflist(inm->inm_source->ims_blk);
		if (inm->inm_source->ims_blk->head != NULL)
			FREE(inm->inm_source->ims_blk->head, M_MSFILTER);
		FREE(inm->inm_source->ims_blk, M_MSFILTER);
	}
	if (inm->inm_source->ims_toin != NULL) {
		in_free_msflist(inm->inm_source->ims_toin);
		if (inm->inm_source->ims_toin->head != NULL)
			FREE(inm->inm_source->ims_toin->head, M_MSFILTER);
		FREE(inm->inm_source->ims_toin, M_MSFILTER);
	}
	if (inm->inm_source->ims_toex != NULL) {
		in_free_msflist(inm->inm_source->ims_toex);
		if (inm->inm_source->ims_toex->head != NULL)
			FREE(inm->inm_source->ims_toex->head, M_MSFILTER);
		FREE(inm->inm_source->ims_toex, M_MSFILTER);
	}
	FREE(inm->inm_source, M_MSFILTER);
}

void
in_free_msflist(struct in_addr_slist *iasl)
{
	struct in_addr_source *ias;

	if (iasl->head == NULL)
		return;
	LIST_FOREACH(ias, iasl->head, ias_list) {
		LIST_REMOVE(ias, ias_list);
		FREE(ias, M_MSFILTER);
	}
	LIST_INIT(iasl->head);
}

void
in_free_msfaddr(struct in_addr_slist *iasl, struct sockaddr_in *sin)
{
	struct in_addr_source *ias;

	if (iasl == NULL)
		return;
	LIST_FOREACH(ias, iasl->head, ias_list) {
		if (ias->ias_addr.sin_family != sin->sin_family)
			continue;

		if (SS_COMPARE(&ias->ias_addr, <, sin)) {
			continue;
		} else if (SS_COMPARE(&ias->ias_addr, ==, sin)) {
			LIST_REMOVE(ias, ias_list);
			FREE(ias, M_MSFILTER);
			--iasl->numsrc;
			break;
		} else {
			break; /* source address not match */
		}
	}
	if (iasl->numsrc == 0)
		LIST_INIT(iasl->head);
}

/*
 * Merge source address into appropriate filter's source list of the interface.
 * This should not be called when there is no iasl entry.
 * iasl's numsrc and ias's refcount are adjusted by this function.
 * The arguments are "target source list", "source to be merged", and "request
 * whether add or delete".
 */
int
in_merge_msfaddr(struct in_addr_slist *iasl, struct sockaddr_in *sin, int req)
{
	struct in_addr_source *ias, *newias, *lastp = NULL;

	LIST_FOREACH(ias, iasl->head, ias_list) {
		lastp = ias;

		/* sanity check */
		if (ias->ias_addr.sin_family != sin->sin_family)
			continue;

		if (SS_COMPARE(&ias->ias_addr, ==, sin)) {
			if (req == IMS_ADD_SOURCE) {
				++ias->ias_refcount;
				return 0;
			} else {
				if (--ias->ias_refcount == 0)
					--iasl->numsrc;
				return 0;
			}
		} else if (SS_COMPARE(&ias->ias_addr, >, sin)) {
			if (req == IMS_ADD_SOURCE) {
				MALLOC(newias, struct in_addr_source *,
					sizeof(*newias), M_MSFILTER, M_NOWAIT);
				if (newias == NULL)
					return ENOBUFS;
				LIST_INSERT_BEFORE(ias, newias, ias_list);
				bcopy(sin, &newias->ias_addr, sin->sin_len);
				newias->ias_refcount = 1;
				++iasl->numsrc;
				return 0;
			} else {
				return EADDRNOTAVAIL;
			}
		}
	}
	if (req == IMS_ADD_SOURCE) {
		MALLOC(newias, struct in_addr_source *, sizeof(*newias),
			M_MSFILTER, M_NOWAIT);
		if (newias == NULL) {
			return ENOBUFS;
		} else {
			if (LIST_EMPTY(iasl->head))
				LIST_INSERT_HEAD(iasl->head, newias, ias_list);
			else
				LIST_INSERT_AFTER(lastp, newias, ias_list);
			bcopy(sin, &newias->ias_addr, sin->sin_len);
			newias->ias_refcount = 1;
			++iasl->numsrc;
			return 0;
		}
	} else {
		return EADDRNOTAVAIL;
	}
	return 0;
}

/*
 * Followings are Protocol-Independent functions.
 * Keep the interoperability when some parts are modified.
 */

/*
 * In order to clean up filtered source list when group leave is requested,
 * each source address is inserted in a buffer.
 */
int
in_getmoptlist(struct sock_msf *msf, u_int16_t *numsrc,
    struct sockaddr_storage **oss, u_int *mode)
{
	struct sockaddr_storage *ss = NULL;
	struct sock_msf_source *msfsrc;
	int i;

	if (msf->msf_numsrc != 0) {
		MALLOC(ss, struct sockaddr_storage *,
		       sizeof(*ss) * msf->msf_numsrc, M_IPMOPTS, M_WAITOK);
		for (i = 0, msfsrc = LIST_FIRST(msf->msf_head);
				i < msf->msf_numsrc && msfsrc;
				i++, msfsrc = LIST_NEXT(msfsrc, list)) {
			/* Move unneeded sources to ss */
			bcopy(&msfsrc->src, &ss[i], sizeof(ss[i]));
		}
		if (i != msf->msf_numsrc || msfsrc != NULL)
			return EOPNOTSUPP; /* panic ? */
		*numsrc = msf->msf_numsrc;
		*mode = MCAST_INCLUDE;
	} else if (msf->msf_grpjoin == 1) {
		*numsrc = 0;
		*mode = MCAST_EXCLUDE;
	} else {
		return EADDRNOTAVAIL;
	}

	/* This allocated buffer must be freed by caller */
	*oss = ss;
	return 0;
}

/*
 * Set or delete source address to/from the msf.
 * If requested source address was already in the socket list when the
 * command is to add source filter, or if requested source address was not in
 * the socket list when the command is to delete source filter, return
 * EADDRNOTAVAIL.
 * If there is not enough memory, return ENOBUFS.
 */
int
in_setmoptaddr(struct sockaddr_storage *ss, struct sock_msf *msf, int optname)
{
	struct sock_msf_source *msfsrc, *newsrc, *lastp = NULL;
	int s = splsoftnet();

	if (!LIST_EMPTY(msf->msf_head))
		goto merge_msf_list;
	if (IGMP_MSFOFF_OPS(optname)) {
		splx(s);
		return EADDRNOTAVAIL;
	}

	/*
	 * Create multicast source filter list on the socket.
	 */
	msfsrc = (struct sock_msf_source *)malloc(sizeof(*msfsrc),
						  M_IPMOPTS, M_NOWAIT);
	if (msfsrc == NULL) {
		splx(s);
		return ENOBUFS;
	}
	bcopy(&ss[0], &msfsrc->src, sizeof(ss[0]));
	msfsrc->refcount = 2;
	LIST_INSERT_HEAD(msf->msf_head, msfsrc, list);
	msf->msf_numsrc = 1;
	splx(s);
	return 0;

	/*
	 * Merge to recorded msf list.
	 */
merge_msf_list:
	LIST_FOREACH(msfsrc, msf->msf_head, list) {
		lastp = msfsrc;

		if (ss->ss_family != msfsrc->src.ss_family)
			continue;
		if (SS_COMPARE(ss, >, &msfsrc->src))
			continue;
		if (SS_COMPARE(ss, ==, &msfsrc->src)) {
			if (IGMP_MSFON_OPS(optname)) {
				splx(s);
				return EADDRNOTAVAIL;
			}
			msfsrc->refcount = 0;
			break;
		}
		/* creates a new entry here */
		if (!IGMP_MSFON_OPS(optname)) {
			splx(s);
			return EADDRNOTAVAIL;
		}
		newsrc = (struct sock_msf_source *)malloc(sizeof(*newsrc),
							  M_IPMOPTS, M_NOWAIT);
		if (newsrc == NULL) {
			splx(s);
			return ENOBUFS;
		}
		bcopy(ss, &newsrc->src, ss->ss_len);
		newsrc->refcount = 2;
		LIST_INSERT_BEFORE(msfsrc, newsrc, list);
		break;
	}
	if (!msfsrc) {
		if (!IGMP_MSFON_OPS(optname)) {
			splx(s);
			return EADDRNOTAVAIL;
		}
		newsrc = (struct sock_msf_source *)malloc(sizeof(*newsrc),
							  M_IPMOPTS, M_NOWAIT);
		if (newsrc == NULL) {
			splx(s);
			return ENOBUFS;
		}
		bcopy(ss, &newsrc->src, ss->ss_len);
		newsrc->refcount = 2;
		LIST_INSERT_AFTER(lastp, newsrc, list);
	}

	if (IGMP_MSFON_OPS(optname))
		++msf->msf_numsrc;
	else if (IGMP_MSFOFF_OPS(optname))
		--msf->msf_numsrc;

	splx(s);
	return 0;
}

/*
 * Undo msf changes for Basic APIs.
 */
void
in_undomoptaddr(struct sock_msf *msf, int optname)
{
	struct sock_msf_source *msfsrc;

	LIST_FOREACH(msfsrc, msf->msf_head, list) {
		if (IGMP_MSFON_OPS(optname)) {
			if (msfsrc->refcount == 2) {
				LIST_REMOVE(msfsrc, list);
				FREE(msfsrc, M_IPMOPTS);
				--msf->msf_numsrc;
				break;
			}
		} else {
			if (msfsrc->refcount == 0) {
				msfsrc->refcount = 1;
				++msf->msf_numsrc;
				break;
			}
		}
	}

	if (msf->msf_numsrc == 0) {
		if (msf->msf_grpjoin == 0)
			IMO_MSF_FREE(msf);
		else
			LIST_INIT(msf->msf_head);
	}
}

/*
 * Confirm source address addition or deletion ordered by Basic APIs.
 * This adjusts refcount of added source or cleans up an unneeded source.
 * Note that numsrc was already adjusted.
 */
void
in_cleanmoptaddr(struct sock_msf *msf, int optname)
{
	struct sock_msf_source *msfsrc;

	LIST_FOREACH(msfsrc, msf->msf_head, list) {
		if (IGMP_MSFON_OPS(optname)) {
			if (msfsrc->refcount == 2) {
				msfsrc->refcount = 1;
				break;
			}
		} else {
			if (msfsrc->refcount == 0) {
				LIST_REMOVE(msfsrc, list);
				FREE(msfsrc, M_IPMOPTS);
				break;
			}
		}
	}

	if (msf->msf_numsrc == 0) {
		if (msf->msf_grpjoin == 0)
			IMO_MSF_FREE(msf);
		else
			LIST_INIT(msf->msf_head);
	}
}

void
in_freemoptlist(struct sock_msf *msf)
{
	struct sock_msf_source *msfsrc, *nmsfsrc;

	if (msf == NULL)
		return;
 
	if (msf->msf_head != NULL) {
		for (msfsrc = LIST_FIRST(msf->msf_head); msfsrc;
		    msfsrc = nmsfsrc) {
			nmsfsrc = LIST_NEXT(msfsrc, list);
			LIST_REMOVE(msfsrc, list);
			FREE(msfsrc, M_IPMOPTS);
		}
		LIST_INIT(msf->msf_head);
		msf->msf_numsrc = 0;
	}
}

int
sa_compare(struct sockaddr *a, struct sockaddr *b)
{
	char *addr_a, *addr_b;
	int i, size, diff;

	/* assumes a and b are in the same address family */
	if (a == NULL || b == NULL || a->sa_family != b->sa_family) {
#if defined(IGMPV3_DEBUG) || defined(MLDV2_DEBUG)
		printf("sa_compare: improper pair of sockaddrs is given\n");
#endif
		return 0;
	}

	/* extract address part from sockaddr */
	switch (a->sa_family) {
	case AF_INET:
		addr_a = (char *)&(SIN(a)->sin_addr);
		addr_b = (char *)&(SIN(b)->sin_addr);
		size = sizeof(SIN(a)->sin_addr);
		break;
	case AF_INET6:
		addr_a = (char *)&(SIN6(a)->sin6_addr);
		addr_b = (char *)&(SIN6(b)->sin6_addr);
		size = sizeof(SIN6(a)->sin6_addr);
		break;
	default:
		/* unsupported */
#if defined(IGMPV3_DEBUG) || defined(MLDV2_DEBUG)
		printf("sa_compare: unsupported sockaddr is given\n");
#endif
		return 0;
	}

	diff = 0;
	/* compare each byte in these addresses */
	for (i = 0; i < size; i++) {
		unsigned char byte_a = *addr_a++;
		unsigned char byte_b = *addr_b++;
		diff = byte_a - byte_b;
		if (diff != 0)
			return diff;
	}
	/* if everything is same, check sin6_scope_id (IPv6 only) */
	if (a->sa_family == AF_INET6 &&
	    IN6_IS_SCOPE_LINKLOCAL(&SIN6(a)->sin6_addr)) {
		int scope_a = SIN6(a)->sin6_scope_id;
		int scope_b = SIN6(b)->sin6_scope_id;

		return scope_a - scope_b;
	}

	/* everything is completely same */
	return 0;
}
