/* vim: set filetype=c:
 *
 **************************************************************************
 *
 * Boot-ROM-Code to load an operating system across a TCP/IP network.
 *
 * Module:  igmp.c86
 * Purpose: Internet Group Management Protocol handling
 * Entries: igmp_recv, igmp_check, igmp_join, igmp_leave
 *
 **************************************************************************
 *
 * Copyright (C) 2003-2007 Gero Kuhlmann <gero@gkminix.han.de>
 *
 *  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
 *  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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: igmp.c86,v 1.6 2007/01/06 18:30:47 gkminix Exp $
 */


#include <common.h86>
#include <romconfig.h86>
#include <i386/string.h86>
#include <i386/misc.h86>
#include <i386/time.h86>
#include <i386/conio.h86>
#include <net/ethernet.h86>
#include <net/in.h86>
#include <pxe/general.h86>
#include <pxe/undi.h86>
#include "headers/net.h86"
#include "ip.h86"
#include "igmp.h86"
#include "undi.h86"



/*
 **************************************************************************
 * 
 * IGMP client states
 */
#define STATE_NONMEMBER	0
#define STATE_IDLE	1
#define STATE_DELAY	2



/*
 **************************************************************************
 * 
 * Global variables
 */
static unsigned long timer;			/* IGMP report timer	*/
static short state = STATE_NONMEMBER;		/* IGMP client state	*/
static bool_t lastflag;				/* last client flag	*/
static t_ipaddr mcastaddr;			/* our multicast addr	*/



/*
 **************************************************************************
 * 
 * Send buffers: these get shared among various modules to save memory
 */
struct igmphdr igmpbuf asm("_protobuf") __attribute__((common));
struct {
	struct iphdr i;
	__u32        option;
} ipbuf asm("_linkbuf") __attribute__((common));
size_t sizes[2] asm("_sendsizes") __attribute__((common));
__fptr bufs[2] asm("_sendbufs") __attribute__((common));



/*
 **************************************************************************
 * 
 * Send an IGMP packet
 */
static void send_igmp(__u8 pkt_type)
{
  __u8 hw[ETH_ALEN];

  /* Determine the hardware address of the recipient */
  if (undi_get_mcast(mcastaddr, hw) != PXENV_STATUS_SUCCESS)
	return;

  /* Prepare IGMP buffer */
  igmpbuf.igmp_type = pkt_type;
  igmpbuf.igmp_max_time = 0;
  igmpbuf.igmp_checksum = 0;
  igmpbuf.igmp_group = mcastaddr;
  igmpbuf.igmp_checksum = ipchksum((__u8 *)&igmpbuf, sizeof(igmpbuf));

  /* Prepare IP header */
  ipbuf.option           = htonl(IGMP_OPTION);
  ipbuf.i.ip_ihl_version = ((IP_VERSION << 4) |
					((sizeof(ipbuf)/4) & 0x0f)) & 0xff;
  ipbuf.i.ip_tos         = IP_STD_TOS;
  ipbuf.i.ip_tot_len     = htons(sizeof(ipbuf) + sizeof(igmpbuf));
  ipbuf.i.ip_id          = htons(++ippacketid);
  ipbuf.i.ip_frag_off    = 0;
  ipbuf.i.ip_check       = 0;
  ipbuf.i.ip_ttl         = IGMP_IP_TTL;
  ipbuf.i.ip_protocol    = IPPROTO_IGMP;
  ipbuf.i.ip_daddr       = mcastaddr;
  ipbuf.i.ip_saddr       = myipaddr;
  ipbuf.i.ip_check       = ipchksum((__u8 *)&ipbuf, sizeof(ipbuf));

  /* Send the packet using UNDI */
  sizes[0] = sizeof(ipbuf);
  sizes[1] = sizeof(igmpbuf);
  bufs[0] = MK_FP(getds(), &ipbuf);
  bufs[1] = MK_FP(getds(), &igmpbuf);
  (void)undi_send(hw, UNDI_PROT_IP, 2, sizes, bufs);
}

#define send_report()	send_igmp(IGMP_V2_REPORT)
#define send_leave()	send_igmp(IGMP_LEAVE)



/*
 **************************************************************************
 * 
 * Compute IGMP timer value
 */
static void settimer(__u8 max_time)
{
  if (max_time == 0)
	timer = 0;
  else
	timer = get_ticks() + (random(0xFFFF) %
			(unsigned long)((max_time * TICKS_PER_SECOND) / 10));
}



/*
 **************************************************************************
 * 
 * Receive an IGMP packet
 */
void igmp_recv(__u8 *buf, size_t ipsize)
{
#define igmpp ((struct igmphdr *)buf)

  /*
   * We can assume that the packet is an IP packet, so we just have
   * to do some checks on the IGMP header.
   */
  if (
      /* The IP payload has to include at least the IGMP header */
      ipsize < sizeof(struct igmphdr) ||

      /* The IP payload checksum has to be correct */
      ipchksum((__u8 *)igmpp, ipsize) != 0)
	return;

  /*
   * If this packet is a report, we have to be in the DELAY state. If the
   * group matches our multicast address, we have to stop the reply timer.
   */
  if (igmpp->igmp_type == IGMP_V2_REPORT ||
      igmpp->igmp_type == IGMP_V1_REPORT) {
	if (state == STATE_DELAY && igmpp->igmp_group == mcastaddr) {
		state = STATE_IDLE;
		lastflag = FALSE;
		settimer(0);
	}
	return;
  }

  /*
   * For any query packet, we switch into the DELAY state, and wait until
   * we have to send a report.
   */
  if (igmpp->igmp_type == IGMP_QUERY) {
	if (state != STATE_NONMEMBER &&
            (igmpp->igmp_group == htonl(IP_ANY) ||
	     igmpp->igmp_group == mcastaddr)) {
		state = STATE_DELAY;
		lastflag = TRUE;
		settimer(igmpp->igmp_max_time);
	}
	return;
  }

  /* Don't do anything for unknown packet types */
  return;

#undef igmpp
}



/*
 **************************************************************************
 * 
 * Check if the IGMP timer has expired
 */
void igmp_check(void)
{
  if (state == STATE_DELAY && timer > get_ticks()) {
	state = STATE_IDLE;
	lastflag = TRUE;
	send_report();
	settimer(0);
  }
}



/*
 **************************************************************************
 * 
 * Join a multicast group
 */
void igmp_join(t_ipaddr mcast)
{
  if (state == STATE_NONMEMBER) {
	mcastaddr = mcast;
	state = STATE_DELAY;
	lastflag = TRUE;
	send_report();
	settimer(IGMP_DEF_TIME);
  }
}



/*
 **************************************************************************
 * 
 * Leave a multicast group
 */
void igmp_leave(void)
{
  if (state != STATE_NONMEMBER) {
	state = STATE_NONMEMBER;
	if (lastflag) {
		lastflag = FALSE;
		send_leave();
	}
	settimer(0);
  }
}

