diff --git a/etc/schema.json b/etc/schema.json index 9df20702ac..124a41867a 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -6619,6 +6619,24 @@ } } }, + "igmp": { + "type": "object", + "additionalProperties": false, + "properties": { + "malformed": { + "type": "integer", + "description": "IGMP with malformed data" + }, + "pkt_too_small": { + "type": "integer", + "description": "IGMP packets too small to fit a IGMP header" + }, + "v3_pkt_too_small": { + "type": "integer", + "description": "IGMPv3 packets too small to fit a IGMP header" + } + } + }, "ipraw": { "type": "object", "additionalProperties": false, @@ -7058,6 +7076,10 @@ "type": "integer", "description": "Number of IEEE802.1ah packets decoded" }, + "igmp": { + "type": "integer", + "description": "Number of IGMP packets decoded" + }, "invalid": { "type": "integer", "description": "Number of invalid packets decoded" diff --git a/src/Makefile.am b/src/Makefile.am index 502efd4f52..8b6181336b 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -74,6 +74,7 @@ noinst_HEADERS = \ decode-gre.h \ decode-icmpv4.h \ decode-icmpv6.h \ + decode-igmp.h \ decode-ipv4.h \ decode-ipv6.h \ decode-mpls.h \ @@ -651,6 +652,7 @@ libsuricata_c_a_SOURCES = \ decode-gre.c \ decode-icmpv4.c \ decode-icmpv6.c \ + decode-igmp.c \ decode-ipv4.c \ decode-ipv6.c \ decode-mpls.c \ diff --git a/src/decode-events.c b/src/decode-events.c index 3af8fa048a..32ce23dcda 100644 --- a/src/decode-events.c +++ b/src/decode-events.c @@ -614,6 +614,20 @@ const struct DecodeEvents_ DEvents[] = { NSH_UNKNOWN_PAYLOAD, }, + /* IGMP events */ + { + "decoder.igmp.pkt_too_small", + IGMP_PKT_TOO_SMALL, + }, + { + "decoder.igmp.v3_pkt_too_small", + IGMP_V3_PKT_TOO_SMALL, + }, + { + "decoder.igmp.malformed", + IGMP_MALFORMED, + }, + /* GENERIC EVENTS */ { "decoder.too_many_layers", diff --git a/src/decode-events.h b/src/decode-events.h index 67c0f52598..dc958c9c45 100644 --- a/src/decode-events.h +++ b/src/decode-events.h @@ -229,6 +229,11 @@ enum { NSH_UNSUPPORTED_TYPE, NSH_UNKNOWN_PAYLOAD, + /* IGMP events */ + IGMP_PKT_TOO_SMALL, /**< packet too small to fit the basic IGMP header */ + IGMP_V3_PKT_TOO_SMALL, /**< packet too small to fit the IGMPv3 header */ + IGMP_MALFORMED, + /* generic events */ GENERIC_TOO_MANY_LAYERS, diff --git a/src/decode-igmp.c b/src/decode-igmp.c new file mode 100644 index 0000000000..796bf7d26c --- /dev/null +++ b/src/decode-igmp.c @@ -0,0 +1,177 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * 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 + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + */ + +#include "suricata-common.h" +#include "decode.h" +#include "util-debug.h" + +#define IGMP_TYPE_MEMBERSHIP_QUERY 0x11 +#define IGMP_TYPE_MEMBERSHIP_REPORT_V1 0x12 +#define IGMP_TYPE_MEMBERSHIP_REPORT_V2 0x16 +#define IGMP_TYPE_LEAVE_GROUP_V2 0x17 +#define IGMP_TYPE_MEMBERSHIP_REPORT_V3 0x22 + +/* Data size: IP segment size (64k), minus IP header + RA option (24), + * minus IGMPv3 header (12). + * Divided by 4 bytes per address. */ +#define IGMP_V3_MAX_N_SOURCES (65535 - 24 - 12) / 4 + +typedef struct IGMPv3MemberQueryHdr_ { + uint8_t type; + uint8_t max_resp_time; + uint16_t checksum; + uint32_t group_address; + uint8_t s_qrv; + uint8_t qqic; + uint16_t n_sources; + /* Followed by source addresses */ +} IGMPv3MemberQueryHdr; + +typedef struct IGMPv3MemberReportGroupRecord_ { + uint8_t type; + uint8_t aux_data_len; /**< in units of 32-bit words */ + uint16_t n_sources; + uint32_t mcast_addr; + // source address + // aux data +} IGMPv3MemberReportGroupRecord; + +typedef struct IGMPv3MemberReportHdr_ { + uint8_t type; + uint8_t res; + uint16_t checksum; + uint16_t flags; + uint16_t n_group_recs; + /* Followed by group records */ + +} IGMPv3MemberReportHdr; + +int DecodeIGMP(ThreadVars *tv, DecodeThreadVars *dtv, Packet *p, const uint8_t *pkt, uint32_t len) +{ + StatsCounterIncr(&tv->stats, dtv->counter_igmp); + + if (len < sizeof(IGMPHdr)) { + ENGINE_SET_INVALID_EVENT(p, IGMP_PKT_TOO_SMALL); + return TM_ECODE_FAILED; + } + + const IGMPHdr *igmp = PacketSetIGMP(p, pkt); + p->proto = IPPROTO_IGMP; + uint8_t version; + + /* For IGMPv3 Membership Query, we need to handle additional fields */ + if (igmp->type == IGMP_TYPE_MEMBERSHIP_QUERY) { + if (len >= 12) { + const IGMPv3MemberQueryHdr *igmpv3 = (const IGMPv3MemberQueryHdr *)pkt; + const uint32_t n_sources = SCNtohs(igmpv3->n_sources); + if (n_sources > IGMP_V3_MAX_N_SOURCES) { + ENGINE_SET_INVALID_EVENT(p, IGMP_MALFORMED); + return TM_ECODE_FAILED; + } + + const uint32_t header_len = + sizeof(IGMPv3MemberQueryHdr) + (n_sources * sizeof(uint32_t)); + if (len < header_len) { + SCLogDebug("len %u < header_len %u", len, header_len); + ENGINE_SET_INVALID_EVENT(p, IGMP_V3_PKT_TOO_SMALL); + return TM_ECODE_FAILED; + } + if (header_len >= UINT16_MAX) { + ENGINE_SET_INVALID_EVENT(p, IGMP_MALFORMED); + return TM_ECODE_FAILED; + } + p->l4.vars.igmp.hlen = (uint16_t)header_len; + p->payload = (uint8_t *)pkt + header_len; + p->payload_len = (uint16_t)(len - header_len); + version = 3; + } else { + if (igmp->max_resp_time == 0) { + version = 1; + } else { + version = 2; + } + p->l4.vars.igmp.hlen = (uint16_t)sizeof(IGMPHdr); + p->payload = (uint8_t *)pkt + sizeof(IGMPHdr); + p->payload_len = (uint16_t)(len - sizeof(IGMPHdr)); + } + + } else if (igmp->type == IGMP_TYPE_MEMBERSHIP_REPORT_V3) { + const IGMPv3MemberReportHdr *igmpv3 = (const IGMPv3MemberReportHdr *)pkt; + const uint32_t n_group_recs = SCNtohs(igmpv3->n_group_recs); + uint32_t header_len = sizeof(IGMPv3MemberReportHdr); + + /* parse group records */ + for (uint32_t i = 0; i < n_group_recs; i++) { + if (len - header_len < sizeof(IGMPv3MemberReportGroupRecord)) { + SCLogDebug("len %u - header_len %u < IGMPv3MemberReportGroupRecord %u", len, + header_len, (uint32_t)sizeof(IGMPv3MemberReportGroupRecord)); + ENGINE_SET_INVALID_EVENT(p, IGMP_V3_PKT_TOO_SMALL); + return TM_ECODE_FAILED; + } + + const IGMPv3MemberReportGroupRecord *grec = + (const IGMPv3MemberReportGroupRecord *)(pkt + header_len); + header_len += sizeof(IGMPv3MemberReportGroupRecord); + + if (len - header_len < (uint32_t)(grec->aux_data_len * sizeof(uint32_t))) { + SCLogDebug("len %u - header_len %u aux_data_len %u", len, header_len, + (uint32_t)grec->aux_data_len); + ENGINE_SET_INVALID_EVENT(p, IGMP_MALFORMED); + return TM_ECODE_FAILED; + } + header_len += (uint32_t)(grec->aux_data_len * sizeof(uint32_t)); + + uint32_t sources_len = (uint32_t)SCNtohs(grec->n_sources) * (uint32_t)sizeof(uint32_t); + if (len - header_len < sources_len) { + SCLogDebug("len %u - header_len %u sources_len %u", len, header_len, sources_len); + ENGINE_SET_INVALID_EVENT(p, IGMP_MALFORMED); + return TM_ECODE_FAILED; + } + header_len += sources_len; + } + if (header_len >= UINT16_MAX) { + ENGINE_SET_INVALID_EVENT(p, IGMP_MALFORMED); + return TM_ECODE_FAILED; + } + p->l4.vars.igmp.hlen = (uint16_t)header_len; + p->payload = (uint8_t *)pkt + header_len; + p->payload_len = (uint16_t)(len - header_len); + version = 3; + + } else { + if (igmp->type == IGMP_TYPE_MEMBERSHIP_REPORT_V1) { + version = 1; + + } else if (igmp->type == IGMP_TYPE_MEMBERSHIP_REPORT_V2 || + igmp->type == IGMP_TYPE_LEAVE_GROUP_V2) { + version = 2; + } else { + version = 255; + } + + p->l4.vars.igmp.hlen = (uint16_t)sizeof(IGMPHdr); + p->payload = (uint8_t *)pkt + sizeof(IGMPHdr); + p->payload_len = (uint16_t)(len - sizeof(IGMPHdr)); + } + p->l4.vars.igmp.version = version; + + return TM_ECODE_OK; +} diff --git a/src/decode-igmp.h b/src/decode-igmp.h new file mode 100644 index 0000000000..68fc0322ae --- /dev/null +++ b/src/decode-igmp.h @@ -0,0 +1,37 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * 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 + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + */ + +#ifndef SURICATA_DECODE_IGMP_H +#define SURICATA_DECODE_IGMP_H + +typedef struct IGMPHdr_ { + uint8_t type; + uint8_t max_resp_time; + uint16_t checksum; + uint32_t group_address; +} IGMPHdr; + +typedef struct IGMPVars_ { + uint16_t hlen; /**< length of the header */ + uint8_t version; +} IGMPVars; + +#endif /* SURICATA_DECODE_IGMP_H */ diff --git a/src/decode-ipv4.c b/src/decode-ipv4.c index 86ea095c62..82c04580c2 100644 --- a/src/decode-ipv4.c +++ b/src/decode-ipv4.c @@ -611,6 +611,9 @@ int DecodeIPV4(ThreadVars *tv, DecodeThreadVars *dtv, Packet *p, case IPPROTO_ICMPV6: ENGINE_SET_INVALID_EVENT(p, IPV4_WITH_ICMPV6); break; + case IPPROTO_IGMP: + DecodeIGMP(tv, dtv, p, data, data_len); + break; default: SCLogDebug("unknown protocol type: %" PRIx8 "", p->proto); diff --git a/src/decode.c b/src/decode.c index 7ed9261b61..726cb4d6b4 100644 --- a/src/decode.c +++ b/src/decode.c @@ -659,6 +659,7 @@ void DecodeRegisterPerfCounters(DecodeThreadVars *dtv, ThreadVars *tv) dtv->counter_esp = StatsRegisterCounter("decoder.esp", &tv->stats); dtv->counter_icmpv4 = StatsRegisterCounter("decoder.icmpv4", &tv->stats); dtv->counter_icmpv6 = StatsRegisterCounter("decoder.icmpv6", &tv->stats); + dtv->counter_igmp = StatsRegisterCounter("decoder.igmp", &tv->stats); dtv->counter_ppp = StatsRegisterCounter("decoder.ppp", &tv->stats); dtv->counter_pppoe = StatsRegisterCounter("decoder.pppoe", &tv->stats); dtv->counter_geneve = StatsRegisterCounter("decoder.geneve", &tv->stats); diff --git a/src/decode.h b/src/decode.h index f3c675d925..cef221f3ca 100644 --- a/src/decode.h +++ b/src/decode.h @@ -85,6 +85,7 @@ enum PktSrcEnum { #include "decode-ipv6.h" #include "decode-icmpv4.h" #include "decode-icmpv6.h" +#include "decode-igmp.h" #include "decode-tcp.h" #include "decode-udp.h" #include "decode-sctp.h" @@ -456,6 +457,7 @@ enum PacketL4Types { PACKET_L4_UDP, PACKET_L4_ICMPV4, PACKET_L4_ICMPV6, + PACKET_L4_IGMP, PACKET_L4_SCTP, PACKET_L4_GRE, PACKET_L4_ESP, @@ -473,11 +475,13 @@ struct PacketL4 { SCTPHdr *sctph; GREHdr *greh; ESPHdr *esph; + IGMPHdr *igmph; } hdrs; union L4Vars { TCPVars tcp; ICMPV4Vars icmpv4; ICMPV6Vars icmpv6; + IGMPVars igmp; } vars; }; @@ -954,6 +958,25 @@ static inline bool PacketIsARP(const Packet *p) return p->l3.type == PACKET_L3_ARP; } +static inline IGMPHdr *PacketSetIGMP(Packet *p, const uint8_t *buf) +{ + DEBUG_VALIDATE_BUG_ON(p->l4.type != PACKET_L4_UNKNOWN); + p->l4.type = PACKET_L4_IGMP; + p->l4.hdrs.igmph = (IGMPHdr *)buf; + return p->l4.hdrs.igmph; +} + +static inline const IGMPHdr *PacketGetIGMP(const Packet *p) +{ + DEBUG_VALIDATE_BUG_ON(p->l4.type != PACKET_L4_IGMP); + return p->l4.hdrs.igmph; +} + +static inline bool PacketIsIGMP(const Packet *p) +{ + return p->l4.type == PACKET_L4_IGMP; +} + /** \brief Structure to hold thread specific data for all decode modules */ typedef struct DecodeThreadVars_ { @@ -982,6 +1005,7 @@ typedef struct DecodeThreadVars_ StatsCounterId counter_udp; StatsCounterId counter_icmpv4; StatsCounterId counter_icmpv6; + StatsCounterId counter_igmp; StatsCounterId counter_arp; StatsCounterId counter_ethertype_unknown; @@ -1170,6 +1194,7 @@ int DecodeCHDLC(ThreadVars *, DecodeThreadVars *, Packet *, const uint8_t *, uin int DecodeTEMPLATE(ThreadVars *, DecodeThreadVars *, Packet *, const uint8_t *, uint32_t); int DecodeNSH(ThreadVars *, DecodeThreadVars *, Packet *, const uint8_t *, uint32_t); int DecodeARP(ThreadVars *, DecodeThreadVars *, Packet *, const uint8_t *, uint32_t); +int DecodeIGMP(ThreadVars *, DecodeThreadVars *, Packet *, const uint8_t *, uint32_t); #ifdef UNITTESTS void DecodeIPV6FragHeader(Packet *p, const uint8_t *pkt, diff --git a/src/output-json.c b/src/output-json.c index 1b7464e35e..fc110435af 100644 --- a/src/output-json.c +++ b/src/output-json.c @@ -924,6 +924,14 @@ SCJsonBuilder *CreateEveHeader(const Packet *p, enum SCOutputJsonLogDirection di SCJbSetUint(js, "icmp_code", PacketGetICMPv6(p)->code); } break; + case IPPROTO_IGMP: + if (PacketIsIGMP(p)) { + SCJbOpenObject(js, "igmp"); + SCJbSetUint(js, "type", PacketGetIGMP(p)->type); + SCJbSetUint(js, "version", p->l4.vars.igmp.version); + SCJbClose(js); + } + break; } SCJbSetString(js, "pkt_src", PktSrcToString(p->pkt_src));