/*      $NetBSD: igmp.c,v 1.14 2009/04/17 16:05:43 lukem Exp $  */

/*
* The mrouted program is covered by the license in the accompanying file
* named "LICENSE".  Use of the mrouted program represents acceptance of
* the terms and conditions listed in that file.
*
* The mrouted program is COPYRIGHT 1989 by The Board of Trustees of
* Leland Stanford Junior University.
*/


#include "defs.h"


/*
* Exported variables.
*/
char            *recv_buf;                   /* input packet buffer         */
char            *send_buf;                   /* output packet buffer        */
size_t          send_buflen;                 /* output packet buffer        */
int             igmp_socket;                 /* socket for all network I/O  */
u_int32_t       allhosts_group;              /* All hosts addr in net order */
u_int32_t       allrtrs_group;               /* All-Routers "  in net order */
u_int32_t       dvmrp_group;                 /* DVMRP grp addr in net order */
u_int32_t       dvmrp_genid;                 /* IGMP generation id          */

/*
* Local function definitions.
*/
/* u_char promoted to u_int */
static const char * packet_kind(u_int type, u_int code);
static int      igmp_log_level(u_int type, u_int code);

/*
* Open and initialize the igmp socket, and fill in the non-changing
* IP header fields in the output packet buffer.
*/
void
init_igmp(void)
{
   struct ip *ip;

   recv_buf = malloc(RECV_BUF_SIZE);
   send_buf = malloc(RECV_BUF_SIZE);
   send_buflen = RECV_BUF_SIZE;

   if ((igmp_socket = socket(AF_INET, SOCK_RAW, IPPROTO_IGMP)) < 0)
       logit(LOG_ERR, errno, "IGMP socket");

   k_hdr_include(TRUE);        /* include IP header when sending */
   k_set_rcvbuf(48*1024);      /* lots of input buffering        */
   k_set_ttl(1);               /* restrict multicasts to one hop */
   k_set_loop(FALSE);          /* disable multicast loopback     */

   ip         = (struct ip *)send_buf;
   ip->ip_hl  = sizeof(struct ip) >> 2;
   ip->ip_v   = IPVERSION;
   ip->ip_tos = 0;
   ip->ip_off = 0;
   ip->ip_p   = IPPROTO_IGMP;
   ip->ip_ttl = MAXTTL;        /* applies to unicasts only */

   allhosts_group = htonl(INADDR_ALLHOSTS_GROUP);
   dvmrp_group    = htonl(INADDR_DVMRP_GROUP);
   allrtrs_group  = htonl(INADDR_ALLRTRS_GROUP);
}

#define PIM_QUERY        0
#define PIM_REGISTER     1
#define PIM_REGISTER_STOP       2
#define PIM_JOIN_PRUNE   3
#define PIM_RP_REACHABLE 4
#define PIM_ASSERT       5
#define PIM_GRAFT        6
#define PIM_GRAFT_ACK    7

static const char *
packet_kind(u_int type, u_int code)
{
   switch (type) {
       case IGMP_HOST_MEMBERSHIP_QUERY:        return "membership query  ";
       case IGMP_v1_HOST_MEMBERSHIP_REPORT:    return "v1 member report  ";
       case IGMP_v2_HOST_MEMBERSHIP_REPORT:    return "v2 member report  ";
       case IGMP_HOST_LEAVE_MESSAGE:           return "leave message     ";
       case IGMP_DVMRP:
         switch (code) {
           case DVMRP_PROBE:                   return "neighbor probe    ";
           case DVMRP_REPORT:                  return "route report      ";
           case DVMRP_ASK_NEIGHBORS:           return "neighbor request  ";
           case DVMRP_NEIGHBORS:               return "neighbor list     ";
           case DVMRP_ASK_NEIGHBORS2:          return "neighbor request 2";
           case DVMRP_NEIGHBORS2:              return "neighbor list 2   ";
           case DVMRP_PRUNE:                   return "prune message     ";
           case DVMRP_GRAFT:                   return "graft message     ";
           case DVMRP_GRAFT_ACK:               return "graft message ack ";
           case DVMRP_INFO_REQUEST:            return "info request      ";
           case DVMRP_INFO_REPLY:              return "info reply        ";
           default:                            return "unknown DVMRP msg ";
         }
       case IGMP_PIM:
         switch (code) {
           case PIM_QUERY:                     return "PIM Router-Query  ";
           case PIM_REGISTER:                  return "PIM Register      ";
           case PIM_REGISTER_STOP:             return "PIM Register-Stop ";
           case PIM_JOIN_PRUNE:                return "PIM Join/Prune    ";
           case PIM_RP_REACHABLE:              return "PIM RP-Reachable  ";
           case PIM_ASSERT:                    return "PIM Assert        ";
           case PIM_GRAFT:                     return "PIM Graft         ";
           case PIM_GRAFT_ACK:                 return "PIM Graft-Ack     ";
           default:                            return "unknown PIM msg   ";
         }
       case IGMP_MTRACE_QUERY:                 return "IGMP trace query  ";
       case IGMP_MTRACE_REPLY:                 return "IGMP trace reply  ";
       default:                                return "unknown IGMP msg  ";
   }
}

/*
* Process a newly received IGMP packet that is sitting in the input
* packet buffer.
*/
void
accept_igmp(int recvlen)
{
   u_int32_t src, dst, group;
   struct ip *ip;
   struct igmp *igmp;
   int ipdatalen, iphdrlen, igmpdatalen;

   if (recvlen < (int)sizeof(struct ip)) {
       logit(LOG_WARNING, 0,
           "received packet too short (%u bytes) for IP header", recvlen);
       return;
   }

   ip        = (struct ip *)recv_buf;
   src       = ip->ip_src.s_addr;
   dst       = ip->ip_dst.s_addr;

   /*
    * this is most likely a message from the kernel indicating that
    * a new src grp pair message has arrived and so, it would be
    * necessary to install a route into the kernel for this.
    */
   if (ip->ip_p == 0) {
       if (src == 0 || dst == 0)
           logit(LOG_WARNING, 0, "kernel request not accurate");
       else
           add_table_entry(src, dst);
       return;
   }

   iphdrlen  = ip->ip_hl << 2;
   ipdatalen = ip->ip_len;
   if (iphdrlen + ipdatalen != recvlen) {
       logit(LOG_WARNING, 0,
           "received packet from %s shorter (%u bytes) than hdr+data length (%u+%u)",
           inet_fmt(src), recvlen, iphdrlen, ipdatalen);
       return;
   }

   igmp        = (struct igmp *)(recv_buf + iphdrlen);
   group       = igmp->igmp_group.s_addr;
   igmpdatalen = ipdatalen - IGMP_MINLEN;
   if (igmpdatalen < 0) {
       logit(LOG_WARNING, 0,
           "received IP data field too short (%u bytes) for IGMP, from %s",
           ipdatalen, inet_fmt(src));
       return;
   }

   logit(LOG_DEBUG, 0, "RECV %s from %-15s to %s",
       packet_kind(igmp->igmp_type, igmp->igmp_code),
       inet_fmt(src), inet_fmt(dst));

   switch (igmp->igmp_type) {

       case IGMP_HOST_MEMBERSHIP_QUERY:
           accept_membership_query(src, dst, group, igmp->igmp_code);
           return;

       case IGMP_v1_HOST_MEMBERSHIP_REPORT:
       case IGMP_v2_HOST_MEMBERSHIP_REPORT:
           accept_group_report(src, dst, group, igmp->igmp_type);
           return;

       case IGMP_HOST_LEAVE_MESSAGE:
           accept_leave_message(src, dst, group);
           return;

       case IGMP_DVMRP:
           group = ntohl(group);

           switch (igmp->igmp_code) {
               case DVMRP_PROBE:
                   accept_probe(src, dst,
                                (char *)(igmp+1), igmpdatalen, group);
                   return;

               case DVMRP_REPORT:
                   accept_report(src, dst,
                                 (char *)(igmp+1), igmpdatalen, group);
                   return;

               case DVMRP_ASK_NEIGHBORS:
                   accept_neighbor_request(src, dst);
                   return;

               case DVMRP_ASK_NEIGHBORS2:
                   accept_neighbor_request2(src, dst);
                   return;

               case DVMRP_NEIGHBORS:
                   accept_neighbors(src, dst, (u_char *)(igmp+1), igmpdatalen,
                                            group);
                   return;

               case DVMRP_NEIGHBORS2:
                   accept_neighbors2(src, dst, (u_char *)(igmp+1), igmpdatalen,
                                            group);
                   return;

               case DVMRP_PRUNE:
                   accept_prune(src, dst, (char *)(igmp+1), igmpdatalen);
                   return;

               case DVMRP_GRAFT:
                   accept_graft(src, dst, (char *)(igmp+1), igmpdatalen);
                   return;

               case DVMRP_GRAFT_ACK:
                   accept_g_ack(src, dst, (char *)(igmp+1), igmpdatalen);
                   return;

               case DVMRP_INFO_REQUEST:
                   accept_info_request(src, dst, (u_char *)(igmp+1),
                               igmpdatalen);
                   return;

               case DVMRP_INFO_REPLY:
                   accept_info_reply(src, dst, (u_char *)(igmp+1), igmpdatalen);
                   return;

               default:
                   logit(LOG_INFO, 0,
                    "ignoring unknown DVMRP message code %u from %s to %s",
                    igmp->igmp_code, inet_fmt(src),
                    inet_fmt(dst));
                   return;
           }

       case IGMP_PIM:
           return;

       case IGMP_MTRACE_REPLY:
           return;

       case IGMP_MTRACE_QUERY:
           accept_mtrace(src, dst, group, (char *)(igmp+1),
                  igmp->igmp_code, igmpdatalen);
           return;

       default:
           logit(LOG_INFO, 0,
               "ignoring unknown IGMP message type %x from %s to %s",
               igmp->igmp_type, inet_fmt(src),
               inet_fmt(dst));
           return;
   }
}

/*
* Some IGMP messages are more important than others.  This routine
* determines the logging level at which to log a send error (often
* "No route to host").  This is important when there is asymmetric
* reachability and someone is trying to, i.e., mrinfo me periodically.
*/
static int
igmp_log_level(u_int type, u_int code)
{
   switch (type) {
       case IGMP_MTRACE_REPLY:
           return LOG_INFO;

       case IGMP_DVMRP:
         switch (code) {
           case DVMRP_NEIGHBORS:
           case DVMRP_NEIGHBORS2:
               return LOG_INFO;
         }
   }
   return LOG_WARNING;
}

/*
* Construct an IGMP message in the output packet buffer.  The caller may
* have already placed data in that buffer, of length 'datalen'.  Then send
* the message from the interface with IP address 'src' to destination 'dst'.
*/
void
send_igmp(u_int32_t src, u_int32_t dst, int type, int code, u_int32_t group,
         int datalen)
{
   struct sockaddr_in sdst;
   struct ip *ip;
   struct igmp *igmp;
   int setloop;

   setloop                 = 0;
   ip                      = (struct ip *)send_buf;
   ip->ip_src.s_addr       = src;
   ip->ip_dst.s_addr       = dst;
   ip->ip_len              = MIN_IP_HEADER_LEN + IGMP_MINLEN + datalen;

   igmp                    = (struct igmp *)(send_buf + MIN_IP_HEADER_LEN);
   igmp->igmp_type         = type;
   igmp->igmp_code         = code;
   igmp->igmp_group.s_addr = group;
   igmp->igmp_cksum        = 0;
   igmp->igmp_cksum        = inet_cksum((u_int16_t *)igmp,
                                        IGMP_MINLEN + datalen);

   if (IN_MULTICAST(ntohl(dst))) {
       k_set_if(src);
       if (type != IGMP_DVMRP) {
           setloop = 1;
           k_set_loop(TRUE);
       }
   }

   bzero(&sdst, sizeof(sdst));
   sdst.sin_family = AF_INET;
#if (defined(BSD) && (BSD >= 199103))
   sdst.sin_len = sizeof(sdst);
#endif
   sdst.sin_addr.s_addr = dst;
   if (sendto(igmp_socket, send_buf, ip->ip_len, 0,
                       (struct sockaddr *)&sdst, sizeof(sdst)) < 0) {
       if (errno == ENETDOWN)
           check_vif_state();
       else
           logit(igmp_log_level(type, code), errno,
               "sendto to %s on %s",
               inet_fmt(dst), inet_fmt(src));
   }

   if (setloop)
           k_set_loop(FALSE);

   logit(LOG_DEBUG, 0, "SENT %s from %-15s to %s",
       packet_kind(type, code), inet_fmt(src),
       inet_fmt(dst));
}