/*
 * netsniff-ng - the packet sniffing beast
 * Copyright (C) 2012 Christoph Jaeger <christoph@netsniff-ng.org>
 * Subject to the GPL, version 2.
 */

#include <inttypes.h>
#include <arpa/inet.h>
#include <asm/byteorder.h>
#include <netinet/in.h>

#include "proto.h"
#include "protos.h"
#include "csum.h"
#include "built_in.h"
#include "pkt_buff.h"

/* IGMPv0 (RFC-988) */
struct igmp_v0_msg {
	uint8_t  type;
	uint8_t  code;
	uint16_t checksum;
	uint32_t identifier;
	uint32_t group_address;
	uint64_t access_key;
} __packed;

/* igmp_v0_msg.type */
#define IGMP_V0_CREATE_GROUP_REQUEST  0x01
#define IGMP_V0_CREATE_GROUP_REPLY    0x02
#define IGMP_V0_JOIN_GROUP_REQUEST    0x03
#define IGMP_V0_JOIN_GROUP_REPLY      0x04
#define IGMP_V0_LEAVE_GROUP_REQUEST   0x05
#define IGMP_V0_LEAVE_GROUP_REPLY     0x06
#define IGMP_V0_CONFIRM_GROUP_REQUEST 0x07
#define IGMP_V0_CONFIRM_GROUP_REPLY   0x08

/* IGMPv1 (RFC-1054/RFC-1112, obsoletes RFC-988) */
struct igmp_v1_msg {
	union {
		uint8_t version__type;
		struct {
#if defined(__LITTLE_ENDIAN_BITFIELD)
			uint8_t type    :4,
				version :4;
#elif defined(__BIG_ENDIAN_BITFIELD)
			uint8_t version :4,
				type    :4;
#else
# error "Please fix <asm/byteorder.h>"
#endif
		};
	};
	uint8_t  unused; /* always zero */
	uint16_t checksum;
	uint32_t group_address;
} __packed;

/* igmp_v1_msg.version__type (!) */
/* IGMP_V1_MEMBERSHIP_QUERY 0x11 */
#define IGMP_V1_MEMBERSHIP_REPORT 0x12

/* IGMPv2 (RFC-2236) */
struct igmp_v2_msg {
	uint8_t  type;
	uint8_t  max_resp_time;
	uint16_t checksum;
	uint32_t group_address;
} __packed;

/* igmp_v2_msg.type */
/* IGMP_V2_MEMBERSHIP_QUERY 0x11 */
#define IGMP_V2_MEMBERSHIP_REPORT 0x16
#define IGMP_V2_LEAVE_GROUP       0x17

/*
 * RGMP (RFC-3488)
 * The RGMP message format resembles the IGMPv2 message format. All RGMP
 * messages are sent with TTL 1, to destination address 224.0.0.25.
 */
#define RGMP_LEAVE_GROUP 0xFC
#define RGMP_JOIN_GROUP  0xFD
#define RGMP_BYE         0xFE
#define RGMP_HELLO       0xFF

/* IGMPv3 (RFC-3376) */
struct igmp_v3_group_record {
	uint8_t  record_type;
	uint8_t  aux_data_len; /* always zero */
	uint16_t number_of_sources;
	uint32_t multicast_address;
	uint32_t source_addresses[0];
	/* auxiliary data (IGMPv3 does not define any) */
} __packed;

/* igmp_v3_group_record.record_type */
#define IGMP_V3_MODE_IS_INCLUDE        1
#define IGMP_V3_MODE_IS_EXCLUDE        2
#define IGMP_V3_CHANGE_TO_INCLUDE_MODE 3
#define IGMP_V3_CHANGE_TO_EXCLUDE_MODE 4
#define IGMP_V3_ALLOW_NEW_SOURCES      5
#define IGMP_V3_BLOCK_OLD_SOURCES      6

struct igmp_v3_membership_report {
	uint8_t  type;
	uint8_t  reserved1;
	uint16_t checksum;
	uint16_t reserved2;
	uint16_t number_of_group_records;
	struct igmp_v3_group_record group_records[0];
} __packed;

struct igmp_v3_membership_query {
	uint8_t  type;
	uint8_t  max_resp_code;
	uint16_t checksum;
	uint32_t group_address;
#if defined(__LITTLE_ENDIAN_BITFIELD)
	uint8_t qrv    :3,
		s_flag :1,
		       :4;
#elif defined(__BIG_ENDIAN_BITFIELD)
	uint8_t        :4,
		s_flag :1,
		qrv    :3;
#else
# error "Please fix <asm/byteorder.h>"
#endif
	uint8_t  qqic;
	uint16_t number_of_sources;
	uint32_t source_addresses[0];
} __packed;

#define IGMP_MEMBERSHIP_QUERY     0x11 /* v1/v2/v3 */
#define IGMP_V3_MEMBERSHIP_REPORT 0x22

#define EXP(x) (((x) & 0x70) >> 4)
#define MANT(x) ((x) & 0x0F)

#define DECODE_MAX_RESP_CODE(x) ((x) < 128 ? (x) : (MANT(x) | 0x10) << (EXP(x) + 3))
#define DECODE_QQIC(x)          ((x) < 128 ? (x) : (MANT(x) | 0x10) << (EXP(x) + 3))

static char *friendly_msg_type_name(uint8_t msg_type)
{
	switch (msg_type) {
	case IGMP_V0_CREATE_GROUP_REQUEST:
		return "Create Group Request";
	case IGMP_V0_CREATE_GROUP_REPLY:
		return "Create Group Reply";
	case IGMP_V0_JOIN_GROUP_REQUEST:
		return "Join Group Request";
	case IGMP_V0_JOIN_GROUP_REPLY:
		return "Join Group Reply";
	case IGMP_V0_LEAVE_GROUP_REQUEST:
		return "Leave Group Request";
	case IGMP_V0_LEAVE_GROUP_REPLY:
		return "Leave Group Reply";
	case IGMP_V0_CONFIRM_GROUP_REQUEST:
		return "Confirm Group Request";
	case IGMP_V0_CONFIRM_GROUP_REPLY:
		return "Confirm Group Reply";
	case IGMP_MEMBERSHIP_QUERY:
		return "Membership Query";
	case IGMP_V1_MEMBERSHIP_REPORT:
	case IGMP_V2_MEMBERSHIP_REPORT:
	case IGMP_V3_MEMBERSHIP_REPORT:
		return "Membership Report";
	case IGMP_V2_LEAVE_GROUP:
		return "Leave Group";
	case RGMP_HELLO:
		return "Hello";
	case RGMP_BYE:
		return "Bye";
	case RGMP_JOIN_GROUP:
		return "Join Group";
	case RGMP_LEAVE_GROUP:
		return "Leave Group";
	default:
		return NULL;
	}
}

#define PRINT_FRIENDLY_NAMED_MSG_TYPE(type)			\
	do {							\
		if (friendly_msg_type_name(type))		\
			tprintf("  Type (0x%.2x, %s)", type,	\
				friendly_msg_type_name(type));	\
		else						\
			tprintf("  Type (0x%.2x)", type);	\
	} while (0)

static char *friendly_group_rec_type_name(uint8_t rec_type)
{
	switch (rec_type) {
	case IGMP_V3_MODE_IS_INCLUDE:
		return "Mode Is Include";
	case IGMP_V3_MODE_IS_EXCLUDE:
		return "Mode Is Exclude";
	case IGMP_V3_CHANGE_TO_INCLUDE_MODE:
		return "Change To Include Mode";
	case IGMP_V3_CHANGE_TO_EXCLUDE_MODE:
		return "Change To Exclude Mode";
	case IGMP_V3_ALLOW_NEW_SOURCES:
		return "Allow New Sources";
	case IGMP_V3_BLOCK_OLD_SOURCES:
		return "Block Old Sources";
	default:
		return NULL;
	}
}

static void dissect_igmp_v0(struct pkt_buff *pkt)
{
	char     addr[INET_ADDRSTRLEN];
	uint16_t csum;

	static const char *reply_codes[] = {
		"Request Granted",
		"Request Denied, No Resources",
		"Request Denied, Invalid Code",
		"Request Denied, Invalid Group Address",
		"Request Denied, Invalid Access Key"
	};

	struct igmp_v0_msg *msg =
		(struct igmp_v0_msg *) pkt_pull(pkt, sizeof(*msg));

	if (msg == NULL)
		return;

	tprintf(" [ IGMPv0");
	PRINT_FRIENDLY_NAMED_MSG_TYPE(msg->type);

	switch (msg->type) {
	case IGMP_V0_CREATE_GROUP_REQUEST:
		switch (msg->code) {
		case 0:
			tprintf(", Code (%u, %s)", msg->code, "Public");
			break;
		case 1:
			tprintf(", Code (%u, %s)", msg->code, "Private");
			break;
		default:
			tprintf(", Code (%u)", msg->code);
		}
		break;
	case IGMP_V0_CREATE_GROUP_REPLY:
	case IGMP_V0_JOIN_GROUP_REPLY:
	case IGMP_V0_LEAVE_GROUP_REPLY:
	case IGMP_V0_CONFIRM_GROUP_REPLY:
		if (msg->code < 5)
			tprintf(", Code (%u, %s)", msg->code, reply_codes[msg->code]);
		else
			tprintf(", Code (%u, Request Pending, Retry In %u Seconds)",
				msg->code, msg->code);
		break;
	default:
		tprintf(", Code (%u)", msg->code);
	}

	csum = calc_csum(msg, sizeof(*msg) + pkt_len(pkt));
	tprintf(", CSum (0x%.4x) is %s", ntohs(msg->checksum), csum ?
		colorize_start_full(black, red) "bogus (!)" colorize_end() : "ok");
	if (csum)
		tprintf(" - %s should be %x%s", colorize_start_full(black, red),
			csum_expected(msg->checksum, csum), colorize_end());
	tprintf(", Id (%u)", ntohs(msg->identifier));
	inet_ntop(AF_INET, &msg->group_address, addr, sizeof(addr));
	tprintf(", Group Addr (%s)", addr);
	tprintf(", Access Key (0x%.16"PRIx64")", msg->access_key);
	tprintf(" ]\n");
}

static void dissect_igmp_v1(struct pkt_buff *pkt)
{
	char     addr[INET_ADDRSTRLEN];
	uint16_t csum;

	struct igmp_v1_msg *msg =
		(struct igmp_v1_msg *) pkt_pull(pkt, sizeof(*msg));

	if (msg == NULL)
		return;

	tprintf(" [ IGMPv1");
	PRINT_FRIENDLY_NAMED_MSG_TYPE(msg->version__type);
	csum = calc_csum(msg, sizeof(*msg) + pkt_len(pkt));
	tprintf(", CSum (0x%.4x) is %s", ntohs(msg->checksum), csum ?
		colorize_start_full(black, red) "bogus (!)" colorize_end() : "ok");
	if (csum)
		tprintf(" - %s should be %x%s", colorize_start_full(black, red),
			csum_expected(msg->checksum, csum), colorize_end());
	inet_ntop(AF_INET, &msg->group_address, addr, sizeof(addr));
	tprintf(", Group Addr (%s)", addr);
	tprintf(" ]\n");
}

static void dissect_igmp_v2(struct pkt_buff *pkt)
{
	char     addr[INET_ADDRSTRLEN];
	uint16_t csum;

	struct igmp_v2_msg *msg =
		(struct igmp_v2_msg *) pkt_pull(pkt, sizeof(*msg));

	if (msg == NULL)
		return;

	switch (msg->type) {
	case RGMP_HELLO:
	case RGMP_BYE:
	case RGMP_JOIN_GROUP:
	case RGMP_LEAVE_GROUP:
		tprintf(" [ IGMPv2 (RGMP)");
		break;
	default:
		tprintf(" [ IGMPv2");
		break;
	}

	PRINT_FRIENDLY_NAMED_MSG_TYPE(msg->type);
	tprintf(", Max Resp Time (%u)", msg->max_resp_time);
	csum = calc_csum(msg, sizeof(*msg) + pkt_len(pkt));
	tprintf(", CSum (0x%.4x) is %s", ntohs(msg->checksum), csum ?
		colorize_start_full(black, red) "bogus (!)" colorize_end() : "ok");
	if (csum)
		tprintf(" - %s should be %x%s", colorize_start_full(black, red),
			csum_expected(msg->checksum, csum), colorize_end());
	inet_ntop(AF_INET, &msg->group_address, addr, sizeof(addr));
	tprintf(", Group Addr (%s)", addr);
	tprintf(" ]\n");
}

static void dissect_igmp_v3_membership_query(struct pkt_buff *pkt)
{
	char      addr[INET_ADDRSTRLEN];
	size_t    n;
	uint16_t  csum;
	uint32_t *src_addr;

	struct igmp_v3_membership_query *msg =
		(struct igmp_v3_membership_query *) pkt_pull(pkt, sizeof(*msg));

	if (msg == NULL)
		return;

	tprintf(" [ IGMPv3");
	PRINT_FRIENDLY_NAMED_MSG_TYPE(msg->type);
	tprintf(", Max Resp Code (0x%.2x => %u)", msg->max_resp_code,
		DECODE_MAX_RESP_CODE(msg->max_resp_code));
	csum = calc_csum(msg, sizeof(*msg) + pkt_len(pkt));
	tprintf(", CSum (0x%.4x) is %s", ntohs(msg->checksum), csum ?
		colorize_start_full(black, red) "bogus (!)" colorize_end() : "ok");
	if (csum)
		tprintf(" - %s should be %x%s", colorize_start_full(black, red),
			csum_expected(msg->checksum, csum), colorize_end());
	inet_ntop(AF_INET, &msg->group_address, addr, sizeof(addr));
	/* S Flag (Suppress Router-Side Processing) */
	tprintf(", Suppress (%u)", msg->s_flag ? 1 : 0);
	/* QRV (Querier's Robustness Variable) */
	tprintf(", QRV (%u)", msg->qrv);
	/* QQIC (Querier's Query Interval Code) */
	tprintf(", QQIC (0x%.2x => %u)", msg->qqic, DECODE_QQIC(msg->qqic));
	tprintf(", Group Addr (%s)", addr);
	n = ntohs(msg->number_of_sources);
	tprintf(", Num Src (%zu)", n);

	if (n--) {
		src_addr = (uint32_t *) pkt_pull(pkt, sizeof(*src_addr));
		if (src_addr != NULL) {
			inet_ntop(AF_INET, src_addr, addr, sizeof(addr));
			tprintf(", Src Addr (%s", addr);
			while (n--) {
				src_addr = (uint32_t *)
					pkt_pull(pkt, sizeof(*src_addr));
				if (src_addr == NULL)
					break;
				inet_ntop(AF_INET, src_addr, addr, sizeof(addr));
				tprintf(", %s", addr);
			}
			tprintf(")");
		}
	}
	tprintf(" ]\n");
}

static void dissect_igmp_v3_membership_report(struct pkt_buff *pkt)
{
	char      addr[INET_ADDRSTRLEN];
	size_t    m, n;
	uint16_t  csum;
	uint32_t *src_addr;

	struct igmp_v3_group_record      *rec;
	struct igmp_v3_membership_report *msg =
		(struct igmp_v3_membership_report *) pkt_pull(pkt, sizeof(*msg));

	if (msg == NULL)
		return;

	tprintf(" [ IGMPv3");
	PRINT_FRIENDLY_NAMED_MSG_TYPE(msg->type);
	csum = calc_csum(msg, sizeof(*msg) + pkt_len(pkt));
	tprintf(", CSum (0x%.4x) is %s", ntohs(msg->checksum), csum ?
		colorize_start_full(black, red) "bogus (!)" colorize_end() : "ok");
	if (csum)
		tprintf(" - %s should be %x%s", colorize_start_full(black, red),
			csum_expected(msg->checksum, csum), colorize_end());
	m = ntohs(msg->number_of_group_records);
	tprintf(", Num Group Rec (%zu)", m);
	tprintf(" ]\n");

	while (m--) {
		rec = (struct igmp_v3_group_record *) pkt_pull(pkt, sizeof(*rec));

		if (rec == NULL)
			break;

		tprintf("   [ Group Record");
		if (friendly_group_rec_type_name(rec->record_type))
			tprintf("  Type (%u, %s)", rec->record_type,
				friendly_group_rec_type_name(rec->record_type));
		else
			tprintf("  Type (%u)", rec->record_type);
		n = ntohs(rec->number_of_sources);
		tprintf(", Num Src (%zu)", n);
		inet_ntop(AF_INET, &rec->multicast_address, addr, sizeof(addr));
		tprintf(", Multicast Addr (%s)", addr);

		if (n--) {
			src_addr = (uint32_t *) pkt_pull(pkt, sizeof(*src_addr));
			if (src_addr != NULL) {
				inet_ntop(AF_INET, src_addr, addr, sizeof(addr));
				tprintf(", Src Addr (%s", addr);
				while (n--) {
					src_addr = (uint32_t *)
						pkt_pull(pkt, sizeof(*src_addr));
					if (src_addr == NULL)
						break;
					inet_ntop(AF_INET, src_addr, addr, sizeof(addr));
					tprintf(", %s", addr);
				}
				tprintf(")");
			}
		}

		tprintf(" ]\n");
	}
	tprintf("\n");
}

static void igmp(struct pkt_buff *pkt)
{
	switch (*pkt_peek(pkt)) {
	case IGMP_V0_CREATE_GROUP_REQUEST:
	case IGMP_V0_CREATE_GROUP_REPLY:
	case IGMP_V0_JOIN_GROUP_REQUEST:
	case IGMP_V0_JOIN_GROUP_REPLY:
	case IGMP_V0_LEAVE_GROUP_REQUEST:
	case IGMP_V0_LEAVE_GROUP_REPLY:
	case IGMP_V0_CONFIRM_GROUP_REQUEST:
	case IGMP_V0_CONFIRM_GROUP_REPLY:
		if (pkt_len(pkt) == sizeof(struct igmp_v0_msg))
			dissect_igmp_v0(pkt);
		break;
	case IGMP_MEMBERSHIP_QUERY: /* v1/v2/v3 */
		if (pkt_len(pkt) >= sizeof(struct igmp_v3_membership_query))
			dissect_igmp_v3_membership_query(pkt);
		else if (pkt_len(pkt) == sizeof(struct igmp_v2_msg)
			&& *(pkt_peek(pkt) + 1))
			dissect_igmp_v2(pkt);
		else if (pkt_len(pkt) == sizeof(struct igmp_v1_msg))
			dissect_igmp_v1(pkt);
		break;
	case IGMP_V1_MEMBERSHIP_REPORT:
		if (pkt_len(pkt) == sizeof(struct igmp_v1_msg))
			dissect_igmp_v1(pkt);
		break;
	case RGMP_HELLO:
	case RGMP_BYE:
	case RGMP_JOIN_GROUP:
	case RGMP_LEAVE_GROUP:
	case IGMP_V2_MEMBERSHIP_REPORT:
	case IGMP_V2_LEAVE_GROUP:
		if (pkt_len(pkt) == sizeof(struct igmp_v2_msg))
			dissect_igmp_v2(pkt);
		break;
	case IGMP_V3_MEMBERSHIP_REPORT:
		if (pkt_len(pkt) >= sizeof(struct igmp_v3_membership_report))
			dissect_igmp_v3_membership_report(pkt);
		break;
	}
}

static void igmp_less(struct pkt_buff *pkt)
{
	int version = -1;

	switch (*pkt_peek(pkt)) {
	case IGMP_V0_CREATE_GROUP_REQUEST:
	case IGMP_V0_CREATE_GROUP_REPLY:
	case IGMP_V0_JOIN_GROUP_REQUEST:
	case IGMP_V0_JOIN_GROUP_REPLY:
	case IGMP_V0_LEAVE_GROUP_REQUEST:
	case IGMP_V0_LEAVE_GROUP_REPLY:
	case IGMP_V0_CONFIRM_GROUP_REQUEST:
	case IGMP_V0_CONFIRM_GROUP_REPLY:
		if (pkt_len(pkt) == sizeof(struct igmp_v0_msg))
			version = 0;
		break;
	case IGMP_MEMBERSHIP_QUERY: /* v1/v2/v3 */
		if (pkt_len(pkt) >= sizeof(struct igmp_v3_membership_query))
			version = 3;
		else if (pkt_len(pkt) == sizeof(struct igmp_v2_msg)
			&& *(pkt_peek(pkt) + 1))
			version = 2;
		else if (pkt_len(pkt) == sizeof(struct igmp_v1_msg))
			version = 1;
		break;
	case IGMP_V1_MEMBERSHIP_REPORT:
		if (pkt_len(pkt) == sizeof(struct igmp_v1_msg))
			version = 1;
		break;
	case RGMP_HELLO:
	case RGMP_BYE:
	case RGMP_JOIN_GROUP:
	case RGMP_LEAVE_GROUP:
	case IGMP_V2_MEMBERSHIP_REPORT:
	case IGMP_V2_LEAVE_GROUP:
		if (pkt_len(pkt) == sizeof(struct igmp_v2_msg))
			version = 2;
		break;
	case IGMP_V3_MEMBERSHIP_REPORT:
		if (pkt_len(pkt) >= sizeof(struct igmp_v3_membership_report))
			version = 3;
		break;
	}

	if (version < 0 || version > 3)
		return;

	switch (*pkt_peek(pkt)) {
	case RGMP_HELLO:
	case RGMP_BYE:
	case RGMP_JOIN_GROUP:
	case RGMP_LEAVE_GROUP:
		tprintf(" IGMPv2 (RGMP)");
		break;
	default:
		tprintf(" IGMPv%u", version);
		break;
	}
	PRINT_FRIENDLY_NAMED_MSG_TYPE(*pkt_peek(pkt));
}

struct protocol igmp_ops = {
	.key = 0x02,
	.print_full = igmp,
	.print_less = igmp_less,
};
