/*	$OpenBSD: group.c,v 1.1 2006/06/01 14:12:20 norby Exp $ */

/*
 * Copyright (c) 2006 Esben Norby <norby@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <event.h>

#include "igmp.h"
#include "dvmrpd.h"
#include "dvmrp.h"
#include "dvmrpe.h"
#include "log.h"
#include "control.h"

void	dead_timer(int, short, void *);
int	start_dead_timer(struct group *);
int	start_dead_timer_all(struct group *);
int	stop_dead_timer(struct group *);

void	v1_host_timer(int, short, void *);
int	start_v1_host_timer(struct group *);
int	stop_v1_host_timer(struct group *);

void	retrans_timer(int, short, void *);
int	start_retrans_timer(struct group *);
int	stop_retrans_timer(struct group *);

extern struct dvmrpd_conf	*deconf;

#define MAX_ACTIONS	4

struct {
	int			state;
	enum group_event	event;
	enum group_action	action[MAX_ACTIONS];
	int			new_state;
} grp_fsm[] = {
    /* current state		event that happened	action(s) to take		resulting state */
    /* querier fsm */
    {GRP_STA_NO_MEMB_PRSNT,	GRP_EVT_V2_REPORT_RCVD,	{GRP_ACT_ADD_ROUTE,
							 GRP_ACT_START_TMR,
							 GRP_ACT_END},			GRP_STA_MEMB_PRSNT},
    {GRP_STA_NO_MEMB_PRSNT,	GRP_EVT_V1_REPORT_RCVD,	{GRP_ACT_ADD_ROUTE,
							 GRP_ACT_START_TMR,
							 GRP_ACT_START_V1_HOST_TMR,
							 GRP_ACT_END},			GRP_STA_MEMB_PRSNT},

    {GRP_STA_MEMB_PRSNT,	GRP_EVT_TMR_EXPIRED,	{GRP_ACT_DEL_ROUTE,
							 GRP_ACT_END},			GRP_STA_NO_MEMB_PRSNT},
    {GRP_STA_MEMB_PRSNT,	GRP_EVT_V2_REPORT_RCVD,	{GRP_ACT_START_TMR,
							 GRP_ACT_END},			0},
    {GRP_STA_MEMB_PRSNT,	GRP_EVT_V1_REPORT_RCVD,	{GRP_ACT_START_TMR,
							 GRP_ACT_START_V1_HOST_TMR,
							 GRP_ACT_END},			GRP_STA_V1_MEMB_PRSNT},
    {GRP_STA_MEMB_PRSNT,	GRP_EVT_LEAVE_RCVD,	{GRP_ACT_START_TMR_ALL,
							 GRP_ACT_START_RETRANS_TMR,
							 GRP_ACT_SEND_GRP_QUERY,
							 GRP_ACT_END},			GRP_STA_CHECK_MEMB},

    {GRP_STA_CHECK_MEMB,	GRP_EVT_V2_REPORT_RCVD,	{GRP_ACT_START_TMR,
							 GRP_ACT_END},			GRP_STA_MEMB_PRSNT},
    {GRP_STA_CHECK_MEMB,	GRP_EVT_TMR_EXPIRED,	{GRP_ACT_DEL_ROUTE,
							 GRP_ACT_CLR_RETRANS_TMR,
							 GRP_ACT_END},			GRP_STA_NO_MEMB_PRSNT},
    {GRP_STA_CHECK_MEMB,	GRP_EVT_RETRANS_TMR_EXP,{GRP_ACT_SEND_GRP_QUERY,
							 GRP_ACT_START_RETRANS_TMR,
							 GRP_ACT_END},			0},
    {GRP_STA_CHECK_MEMB,	GRP_EVT_V1_REPORT_RCVD,	{GRP_ACT_START_TMR,
							 GRP_ACT_START_V1_HOST_TMR,
							 GRP_ACT_END},			GRP_STA_V1_MEMB_PRSNT},

    {GRP_STA_V1_MEMB_PRSNT,	GRP_EVT_V1_HOST_TMR_EXP,{GRP_ACT_NOTHING,
							 GRP_ACT_END},			GRP_STA_MEMB_PRSNT},
    {GRP_STA_V1_MEMB_PRSNT,	GRP_EVT_V1_REPORT_RCVD,	{GRP_ACT_START_TMR,
							 GRP_ACT_START_V1_HOST_TMR,
							 GRP_ACT_END},			0},
    {GRP_STA_V1_MEMB_PRSNT,	GRP_EVT_V2_REPORT_RCVD,	{GRP_ACT_START_TMR,
							 GRP_ACT_END},			0},
    {GRP_STA_V1_MEMB_PRSNT,	GRP_EVT_TMR_EXPIRED,	{GRP_ACT_DEL_ROUTE,
							 GRP_ACT_END},			GRP_STA_NO_MEMB_PRSNT},

    /* non querier fsm */
    {GRP_STA_NO_MEMB_PRSNT,	GRP_EVT_REPORT_RCVD,	{GRP_ACT_ADD_ROUTE,
							 GRP_ACT_START_TMR,
							 GRP_ACT_END},			GRP_STA_MEMB_PRSNT},
    {GRP_STA_MEMB_PRSNT,	GRP_EVT_REPORT_RCVD,	{GRP_ACT_START_TMR,
							 GRP_ACT_END},			0},
    {GRP_STA_MEMB_PRSNT,	GRP_EVT_QUERY_RCVD,	{GRP_ACT_START_TMR_ALL,
							 GRP_ACT_END},			GRP_STA_CHECK_MEMB},

    {GRP_STA_CHECK_MEMB,	GRP_EVT_REPORT_RCVD,	{GRP_ACT_START_TMR,
							 GRP_ACT_END},			GRP_STA_MEMB_PRSNT},
    {-1,			GRP_EVT_NOTHING,	{GRP_ACT_NOTHING,
							 GRP_ACT_END},			0},
};

const char * const group_action_names[] = {
	"END MARKER",
	"START TIMER",
	"START ALL TIMER",
	"START RETRANSMISSION TIMER",
	"START V1 HOST TIMER",
	"SEND GROUP QUERY",
	"ADD ROUTE",
	"DEL ROUTE",
	"CLEAR RETRANSMISSION TIMER",
	"NOTHING"
};

static const char * const group_event_names[] = {
	"V2 REPORT RCVD",
	"V1 REPORT RCVD",
	"LEAVE RCVD",
	"TIMER EXPIRED",
	"RETRANS TIMER EXPIRED",
	"V1 HOST TIMER EXPIRED",
	"REPORT RCVD",
	"QUERY RCVD",
	"NOTHING"
};

int
group_fsm(struct group *group, enum group_event event)
{
	struct mfc	mfc;
	int		old_state;
	int		new_state = 0;
	int		i, j, ret = 0;

	old_state = group->state;

	for (i = 0; grp_fsm[i].state != -1; i++)
		if ((grp_fsm[i].state & old_state) &&
		    (grp_fsm[i].event == event)) {
			new_state = grp_fsm[i].new_state;
			break;
		}

	if (grp_fsm[i].state == -1) {
		/* XXX event outside of the defined fsm, ignore it. */
		log_debug("group_fsm: group %s, event '%s' not expected in "
		    "state '%s'", inet_ntoa(group->addr),
		    group_event_name(event), group_state_name(old_state));
		return (0);
	}

	for (j = 0; grp_fsm[i].action[j] != GRP_ACT_END; j++) {
		switch (grp_fsm[i].action[j]) {
		case GRP_ACT_START_TMR:
			ret = start_dead_timer(group);
			break;
		case GRP_ACT_START_TMR_ALL:
			ret = start_dead_timer_all(group);
			break;
		case GRP_ACT_START_RETRANS_TMR:
			ret = start_retrans_timer(group);
			break;
		case GRP_ACT_START_V1_HOST_TMR:
			ret = start_v1_host_timer(group);
			break;
		case GRP_ACT_SEND_GRP_QUERY:
			ret = send_igmp_query(group->iface, group);
			break;
		case GRP_ACT_ADD_ROUTE:
			mfc.origin.s_addr = 0;
			mfc.group = group->addr;
			mfc.ifindex = group->iface->ifindex;	/* XXX fixme */
			dvmrpe_imsg_compose_rde(IMSG_MFC_ADD, 0, 0, &mfc,
			    sizeof(mfc));
			break;
		case GRP_ACT_DEL_ROUTE:
			mfc.origin.s_addr = 0;
			mfc.group = group->addr;
			mfc.ifindex = group->iface->ifindex;	/* XXX fixme */
			dvmrpe_imsg_compose_rde(IMSG_MFC_DEL, 0, 0, &mfc,
			    sizeof(mfc));
			break;
		case GRP_ACT_CLR_RETRANS_TMR:
			ret = stop_retrans_timer(group);
			break;
		case GRP_ACT_NOTHING:
		case GRP_ACT_END:
			/* do nothing */
			break;
		}

		if (ret) {
			log_debug("group_fsm: error changing state for "
			    "group %s, event '%s', state '%s'",
			    inet_ntoa(group->addr), group_event_name(event),
			    group_state_name(old_state));
			return (-1);
		}
	}

	if (new_state != 0)
		group->state = new_state;

	for (j = 0; grp_fsm[i].action[j] != GRP_ACT_END; j++)
		log_debug("group_fsm: event '%s' resulted in action '%s' and "
		    "changing state for group %s from '%s' to '%s'",
		    group_event_name(event),
		    group_action_name(grp_fsm[i].action[j]),
		    inet_ntoa(group->addr), group_state_name(old_state),
		    group_state_name(group->state));

	return (ret);
}

/* timers */
void
dead_timer(int fd, short event, void *arg)
{
	struct group *group = arg;

	log_debug("dead_timer: %s", inet_ntoa(group->addr));

	group_fsm(group, GRP_EVT_TMR_EXPIRED);
}

int
start_dead_timer(struct group *group)
{
	struct timeval	tv;

	log_debug("start_dead_timer: %s", inet_ntoa(group->addr));

	/* Group Membership Interval */
	timerclear(&tv);
	tv.tv_sec = group->iface->robustness * group->iface->query_interval +
	    (group->iface->query_resp_interval / 2);

	return (evtimer_add(&group->dead_timer, &tv));
}

int
start_dead_timer_all(struct group *group)
{
	struct timeval	tv;

	log_debug("start_dead_timer_all: %s", inet_ntoa(group->addr));

	timerclear(&tv);
	if (group->iface->state == IF_STA_QUERIER) {
		/* querier */
		tv.tv_sec = group->iface->last_member_query_interval *
		    group->iface->last_member_query_cnt;
	} else {
		/* non querier */
		/* XXX max response time received in packet */
		tv.tv_sec = group->iface->recv_query_resp_interval *
		    group->iface->last_member_query_cnt;
	}

	return (evtimer_add(&group->dead_timer, &tv));
}

int
stop_dead_timer(struct group *group)
{
	log_debug("stop_dead_timer: %s", inet_ntoa(group->addr));

	return (evtimer_del(&group->dead_timer));
}

void
v1_host_timer(int fd, short event, void *arg)
{
	struct group *group = arg;

	log_debug("v1_host_timer: %s", inet_ntoa(group->addr));

	group_fsm(group, GRP_EVT_V1_HOST_TMR_EXP);
}

int
start_v1_host_timer(struct group *group)
{
	struct timeval	tv;

	log_debug("start_v1_host_timer: %s", inet_ntoa(group->addr));

	/* Group Membership Interval */
	timerclear(&tv);
	tv.tv_sec = group->iface->robustness * group->iface->query_interval +
	    (group->iface->query_resp_interval / 2);

	return (evtimer_add(&group->v1_host_timer, &tv));
}

int
stop_v1_host_timer(struct group *group)
{
	log_debug("stop_v1_host_timer: %s", inet_ntoa(group->addr));

	return (evtimer_del(&group->v1_host_timer));
}

void
retrans_timer(int fd, short event, void *arg)
{
	struct group *group = arg;
	struct timeval tv;

	log_debug("retrans_timer: %s", inet_ntoa(group->addr));

	send_igmp_query(group->iface, group);

	/* reschedule retrans_timer */
	if (group->state == GRP_STA_CHECK_MEMB) {
		timerclear(&tv);
		tv.tv_sec = group->iface->last_member_query_interval;
		evtimer_add(&group->retrans_timer, &tv);
	}
}

int
start_retrans_timer(struct group *group)
{
	struct timeval	tv;

	log_debug("start_retrans_timer: %s", inet_ntoa(group->addr));

	timerclear(&tv);
	tv.tv_sec = group->iface->last_member_query_interval;

	return (evtimer_add(&group->retrans_timer, &tv));
}

int
stop_retrans_timer(struct group *group)
{
	log_debug("stop_retrans_timer: %s", inet_ntoa(group->addr));

	return (evtimer_del(&group->retrans_timer));
}

/* group list */
struct group *
group_list_add(struct iface *iface, u_int32_t group)
{
	struct group	*ge;
	struct timeval	 now;

	/* validate group id */
	if (!IN_MULTICAST(htonl(group)))
		fatalx("group_list_add: invalid group");

	if ((ge = group_list_find(iface, group)) != NULL) {
		return (ge);
	}

	if ((ge = calloc(1, sizeof(*ge))) == NULL)
		fatal("group_list_add");

	ge->addr.s_addr = group;
	ge->state = GRP_STA_NO_MEMB_PRSNT;
	evtimer_set(&ge->dead_timer, dead_timer, ge);
	evtimer_set(&ge->v1_host_timer, v1_host_timer, ge);
	evtimer_set(&ge->retrans_timer, retrans_timer, ge);

	gettimeofday(&now, NULL);
	ge->uptime = now.tv_sec;

	TAILQ_INSERT_TAIL(&iface->group_list, ge, entry);
	iface->group_cnt++;

	ge->iface = iface;

	log_debug("group_list_add: interface %s, group %s", iface->name,
	    inet_ntoa(ge->addr));

	return (ge);
}

void
group_list_remove(struct iface *iface, struct group *group)
{
	log_debug("group_list_remove: interface %s, group %s", iface->name,
	    inet_ntoa(group->addr));

	/* stop timers */
	stop_dead_timer(group);
	start_v1_host_timer(group);
	stop_retrans_timer(group);

	TAILQ_REMOVE(&iface->group_list, group, entry);
	free(group);
	iface->group_cnt--;
}

struct group *
group_list_find(struct iface *iface, u_int32_t group)
{
	struct group	*ge = NULL;

	/* validate group id */
	if (!IN_MULTICAST(htonl(group)))
		fatalx("group_list_find: invalid group");

	TAILQ_FOREACH(ge, &iface->group_list, entry) {
		if (ge->addr.s_addr == group)
			return (ge);
	}

	return (ge);
}

void
group_list_clr(struct iface *iface)
{
	struct group	*ge;

	while ((ge = TAILQ_FIRST(&iface->group_list)) != NULL) {
		TAILQ_REMOVE(&iface->group_list, ge, entry);
		free(ge);
	}
	iface->group_cnt = 0;
}

int
group_list_empty(struct iface *iface)
{
	return (TAILQ_EMPTY(&iface->group_list));
}

void
group_list_dump(struct iface *iface, struct ctl_conn *c)
{
	struct group		*ge;
	struct ctl_group	*gctl;

	TAILQ_FOREACH(ge, &iface->group_list, entry) {
		gctl = group_to_ctl(ge);
		imsg_compose(&c->ibuf, IMSG_CTL_SHOW_IGMP, 0, 0, gctl,
		    sizeof(struct ctl_group));
	}
}

/* names */
const char *
group_event_name(int event)
{
	return (group_event_names[event]);
}

const char *
group_action_name(int action)
{
	return (group_action_names[action]);
}

struct ctl_group *
group_to_ctl(struct group *group)
{
	static struct ctl_group	 gctl;
	struct timeval		 tv, now, res;

	memcpy(&gctl.addr, &group->addr, sizeof(gctl.addr));

	gctl.state = group->state;

	gettimeofday(&now, NULL);
	if (evtimer_pending(&group->dead_timer, &tv)) {
		timersub(&tv, &now, &res);
		gctl.dead_timer = res.tv_sec;
	} else
		gctl.dead_timer = 0;

	if (group->state != GRP_STA_NO_MEMB_PRSNT) {
		gctl.uptime = now.tv_sec - group->uptime;
	} else
		gctl.uptime = 0;

	return (&gctl);
}