diff --git a/scapy/contrib/igmp.py b/scapy/contrib/igmp.py index 34892d81cce05764d1f072b1f184bbd089aca439..3e06980373e6b105b90201469ba3073439b12563 100644 --- a/scapy/contrib/igmp.py +++ b/scapy/contrib/igmp.py @@ -43,7 +43,7 @@ sendp(a/b/c, iface="en0") Parameters: type IGMP type field, 0x11, 0x12, 0x16 or 0x17 - mrtime Maximum Response time (zero for v1) + mrcode Maximum Response time (zero for v1) gaddr Multicast Group Address 224.x.x.x/4 See RFC2236, Section 2. Introduction for definitions of proper @@ -58,7 +58,7 @@ IGMPv2 message format http://www.faqs.org/rfcs/rfc2236.html 0x17 : "Leave Group"} fields_desc = [ ByteEnumField("type", 0x11, igmptypes), - ByteField("mrtime", 20), + ByteField("mrcode", 20), XShortField("chksum", None), IPField("gaddr", "0.0.0.0")] @@ -76,35 +76,68 @@ IGMPv2 message format http://www.faqs.org/rfcs/rfc2236.html p = p[:2]+chr(ck>>8)+chr(ck&0xff)+p[4:] return p + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 4: + if ord(_pkt[0]) in [0x22, 0x30, 0x31, 0x32]: + return IGMPv3 + if ord(_pkt[0]) == 0x11 and len(_pkt) >= 12: + return IGMPv3 + return IGMP + def igmpize(self): - """Applies IGMP rules to the packet""" + """Called to explicitly fixup the packet according to the IGMP RFC + + The rules are: + General: + 1. the Max Response time is meaningful only in Membership Queries and should be zero + IP: + 1. Send General Group Query to 224.0.0.1 (all systems) + 2. Send Leave Group to 224.0.0.2 (all routers) + 3a.Otherwise send the packet to the group address + 3b.Send reports/joins to the group address + 4. ttl = 1 (RFC 2236, section 2) + 5. send the packet with the router alert IP option (RFC 2236, section 2) + Ether: + 1. Recalculate destination + + Returns: + True The tuple ether/ip/self passed all check and represents + a proper IGMP packet. + False One of more validation checks failed and no fields + were adjusted. + + The function will examine the IGMP message to assure proper format. + Corrections will be attempted if possible. The IP header is then properly + adjusted to ensure correct formatting and assignment. The Ethernet header + is then adjusted to the proper IGMP packet format. + """ gaddr = self.gaddr if self.gaddr else "0.0.0.0" underlayer = self.underlayer - # The rules are: - # 1. the Max Response time is meaningful only in Membership Queries and should be zero - # otherwise (RFC 2236, section 2.2) - if self.type != 0x11: # Rule 1 - self.mrtime = 0 + if not self.type in [0x11, 0x30]: # General Rule 1 + self.mrcode = 0 if isinstance(underlayer, IP): if (self.type == 0x11): if (gaddr == "0.0.0.0"): underlayer.dst = "224.0.0.1" # IP rule 1 elif isValidMCAddr(gaddr): - underlayer.dst = gaddr # IP rule 3a + underlayer.dst = gaddr # IP rule 3a else: warning("Invalid IGMP Group Address detected !") return False elif ((self.type == 0x17) and isValidMCAddr(gaddr)): - underlayer.dst = "224.0.0.2" # IP rule 2 + underlayer.dst = "224.0.0.2" # IP rule 2 elif ((self.type == 0x12) or (self.type == 0x16)) and (isValidMCAddr(gaddr)): underlayer.dst = gaddr # IP rule 3b else: warning("Invalid IGMP Type detected !") return False + if not any(isinstance(x, IPOption_Router_Alert) for x in underlayer.options): + underlayer.options.append(IPOption_Router_Alert()) _root = self.firstlayer() if _root.haslayer(Ether): # Force recalculate Ether dst - _root[Ether].dst = getmacbyip(underlayer.dst) + _root[Ether].dst = getmacbyip(underlayer.dst) # Ether rule 1 return True def mysummary(self): @@ -116,5 +149,4 @@ IGMPv2 message format http://www.faqs.org/rfcs/rfc2236.html bind_layers( IP, IGMP, frag=0, proto=2, - ttl=1, - options=[IPOption_Router_Alert()]) + ttl=1) diff --git a/scapy/contrib/igmp.uts b/scapy/contrib/igmp.uts index 27dbfbdddd2137e257407a42e8b7721a1992710b..951ca802bc0fe10c96b0b22d6065cd9405da32bb 100644 --- a/scapy/contrib/igmp.uts +++ b/scapy/contrib/igmp.uts @@ -11,7 +11,7 @@ b=IP(src="1.2.3.4") c=IGMP(gaddr="0.0.0.0") x = a/b/c x[IGMP].igmpize() -assert x.mrtime == 20 +assert x.mrcode == 20 assert x[IP].dst == "224.0.0.1" = Build IGMP - Custom membership @@ -21,7 +21,7 @@ b=IP(src="1.2.3.4") c=IGMP(gaddr="224.0.1.2") x = a/b/c x[IGMP].igmpize() -assert x.mrtime == 20 +assert x.mrcode == 20 assert x[IP].dst == "224.0.1.2" = Build IGMP - LG @@ -32,21 +32,21 @@ c=IGMP(type=0x17, gaddr="224.2.3.4") x = a/b/c x[IGMP].igmpize() assert x.dst == "01:00:5e:00:00:02" -assert x.mrtime == 0 +assert x.mrcode == 0 assert x[IP].dst == "224.0.0.2" = Change IGMP params x = Ether(src="00:01:02:03:04:05")/IP()/IGMP() x[IGMP].igmpize() -assert x.mrtime == 20 +assert x.mrcode == 20 assert x[IP].dst == "224.0.0.1" x = Ether(src="00:01:02:03:04:05")/IP()/IGMP(gaddr="224.2.3.4", type=0x12) -x.mrtime = 1 +x.mrcode = 1 x[IGMP].igmpize() x = Ether(str(x)) -assert x.mrtime == 0 +assert x.mrcode == 0 x.gaddr = "224.3.2.4" x[IGMP].igmpize() @@ -65,9 +65,9 @@ assert IGMP().mysummary() == "IGMP Group Membership Query 0.0.0.0" x = Ether(src="00:01:02:03:04:05")/IP(dst="192.168.0.1")/IGMP(gaddr="www.google.fr", type=0x11) x = Ether(str(x)) -x[IGMP].igmpize() +assert not x[IGMP].igmpize() assert x[IP].dst == "192.168.0.1" x = Ether(src="00:01:02:03:04:05")/IP(dst="192.168.0.1")/IGMP(gaddr="124.0.2.1", type=0x00) -x[IGMP].igmpize() +assert not x[IGMP].igmpize() assert x[IP].dst == "192.168.0.1" \ No newline at end of file diff --git a/scapy/contrib/igmpv3.py b/scapy/contrib/igmpv3.py index 1469abc9fe0b9ef0a0a6741867bcf784fd21f470..fca2c81d4f9042f719fe64ff4c5cde3936bd25be 100644 --- a/scapy/contrib/igmpv3.py +++ b/scapy/contrib/igmpv3.py @@ -21,7 +21,7 @@ from __future__ import print_function from scapy.packet import * from scapy.fields import * from scapy.layers.inet import * -from scapy.contrib.igmp import isValidMCAddr +from scapy.contrib.igmp import IGMP """ Based on the following references http://www.iana.org/assignments/igmp-type-numbers @@ -29,10 +29,6 @@ from scapy.contrib.igmp import isValidMCAddr """ -# TODO: Merge IGMPv3 packet Bindlayers correct for -# membership source/Group records -# ConditionalField parameters for IGMPv3 commented out -# # See RFC3376, Section 4. Message Formats for definitions of proper IGMPv3 message format # http://www.faqs.org/rfcs/rfc3376.html # @@ -40,245 +36,133 @@ from scapy.contrib.igmp import isValidMCAddr # http://www.faqs.org/rfcs/rfc4286.html # -#import sys, socket, struct, time -print("IGMPv3 is still under development - Nov 2010") +class IGMPv3(IGMP): + """IGMP Message Class for v3. + This class is derived from class Packet. + The fields defined below are a + direct interpretation of the v3 Membership Query Message. + Fields 'type' through 'qqic' are directly assignable. + For 'numsrc', do not assign a value. + Instead add to the 'srcaddrs' list to auto-set 'numsrc'. To + assign values to 'srcaddrs', use the following methods: + c = IGMPv3() + c.srcaddrs = ['1.2.3.4', '5.6.7.8'] + c.srcaddrs += ['192.168.10.24'] + At this point, 'c.numsrc' is three (3) -class IGMPv3gr(Packet): - """IGMP Group Record for IGMPv3 Membership Report + 'chksum' is automagically calculated before the packet is sent. - This class is derived from class Packet and should be concatenated to an - instantiation of class IGMPv3. Within the IGMPv3 instantiation, the numgrp - element will need to be manipulated to indicate the proper number of - group records. - """ - name = "IGMPv3gr" - igmpv3grtypes = { 1 : "Mode Is Include", - 2 : "Mode Is Exclude", - 3 : "Change To Include Mode", - 4 : "Change To Exclude Mode", - 5 : "Allow New Sources", - 6 : "Block Old Sources"} + 'mrcode' is also the Advertisement Interval field - fields_desc = [ ByteEnumField("rtype", 1, igmpv3grtypes), - ByteField("auxdlen",0), - FieldLenField("numsrc", None, count_of="srcaddrs"), - IPField("maddr", "0.0.0.0"), - FieldListField("srcaddrs", [], IPField("sa", "0.0.0.0"), "numsrc") ] - #show_indent=0 -#-------------------------------------------------------------------------- - def post_build(self, p, pay): - """Called implicitly before a packet is sent. """ - p += pay - if self.auxdlen != 0: - print("NOTICE: A properly formatted and complaint V3 Group Record should have an Auxiliary Data length of zero (0).") - print(" Subsequent Group Records are lost!") - return p -#-------------------------------------------------------------------------- - def mysummary(self): - """Display a summary of the IGMPv3 group record.""" - return self.sprintf("IGMPv3 Group Record %IGMPv3gr.type% %IGMPv3gr.maddr%") - - -class IGMPv3(Packet): - """IGMP Message Class for v3. - - This class is derived from class Packet. - The fields defined below are a - direct interpretation of the v3 Membership Query Message. - Fields 'type' through 'qqic' are directly assignable. - For 'numsrc', do not assign a value. - Instead add to the 'srcaddrs' list to auto-set 'numsrc'. To - assign values to 'srcaddrs', use the following methods: - c = IGMPv3() - c.srcaddrs = ['1.2.3.4', '5.6.7.8'] - c.srcaddrs += ['192.168.10.24'] - At this point, 'c.numsrc' is three (3) - - 'chksum' is automagically calculated before the packet is sent. - - 'mrcode' is also the Advertisement Interval field - - """ - name = "IGMPv3" - igmpv3types = { 0x11 : "Membership Query", - 0x22 : "Version 3 Membership Report", - 0x30 : "Multicast Router Advertisement", - 0x31 : "Multicast Router Solicitation", - 0x32 : "Multicast Router Termination"} - - fields_desc = [ ByteEnumField("type", 0x11, igmpv3types), - ByteField("mrcode",0), - XShortField("chksum", None), - IPField("gaddr", "0.0.0.0") - ] - # use float_encode() - - # if type = 0x11 (Membership Query), the next field is group address - # ConditionalField(IPField("gaddr", "0.0.0.0"), "type", lambda x:x==0x11), - # else if type = 0x22 (Membership Report), the next fields are - # reserved and number of group records - #ConditionalField(ShortField("rsvd2", 0), "type", lambda x:x==0x22), - #ConditionalField(ShortField("numgrp", 0), "type", lambda x:x==0x22), -# FieldLenField("numgrp", None, "grprecs")] - # else if type = 0x30 (Multicast Router Advertisement), the next fields are - # query interval and robustness - #ConditionalField(ShortField("qryIntvl", 0), "type", lambda x:x==0x30), - #ConditionalField(ShortField("robust", 0), "type", lambda x:x==0x30), -# The following are only present for membership queries - # ConditionalField(BitField("resv", 0, 4), "type", lambda x:x==0x11), - # ConditionalField(BitField("s", 0, 1), "type", lambda x:x==0x11), - # ConditionalField(BitField("qrv", 0, 3), "type", lambda x:x==0x11), - # ConditionalField(ByteField("qqic",0), "type", lambda x:x==0x11), - # ConditionalField(FieldLenField("numsrc", None, "srcaddrs"), "type", lambda x:x==0x11), - # ConditionalField(FieldListField("srcaddrs", None, IPField("sa", "0.0.0.0"), "numsrc"), "type", lambda x:x==0x11), - -#-------------------------------------------------------------------------- - def float_encode(self, value): - """Convert the integer value to its IGMPv3 encoded time value if needed. - - If value < 128, return the value specified. If >= 128, encode as a floating - point value. Value can be 0 - 31744. - """ - if value < 128: - code = value - elif value > 31743: - code = 255 - else: - exp=0 - value>>=3 - while(value>31): - exp+=1 - value>>=1 - exp<<=4 - code = 0x80 | exp | (value & 0x0F) - return code - -#-------------------------------------------------------------------------- - def post_build(self, p, pay): - """Called implicitly before a packet is sent to compute and place IGMPv3 checksum. - - Parameters: - self The instantiation of an IGMPv3 class - p The IGMPv3 message in hex in network byte order - pay Additional payload for the IGMPv3 message - """ - p += pay - if self.type in [0, 0x31, 0x32, 0x22]: # for these, field is reserved (0) - p = p[:1]+chr(0)+p[2:] - if self.chksum is None: - ck = checksum(p) - p = p[:2]+chr(ck>>8)+chr(ck&0xff)+p[4:] - return p - -#-------------------------------------------------------------------------- - def mysummary(self): - """Display a summary of the IGMPv3 object.""" - - if isinstance(self.underlayer, IP): - return self.underlayer.sprintf("IGMPv3: %IP.src% > %IP.dst% %IGMPv3.type% %IGMPv3.gaddr%") - else: - return self.sprintf("IGMPv3 %IGMPv3.type% %IGMPv3.gaddr%") - -#-------------------------------------------------------------------------- - def igmpize(self, ip=None, ether=None): - """Called to explicitly fixup associated IP and Ethernet headers - - Parameters: - self The instantiation of an IGMP class. - ip The instantiation of the associated IP class. - ether The instantiation of the associated Ethernet. - - Returns: - True The tuple ether/ip/self passed all check and represents - a proper IGMP packet. - False One of more validation checks failed and no fields - were adjusted. - - The function will examine the IGMP message to assure proper format. - Corrections will be attempted if possible. The IP header is then properly - adjusted to ensure correct formatting and assignment. The Ethernet header - is then adjusted to the proper IGMP packet format. - """ - -# The rules are: -# 1. ttl = 1 (RFC 2236, section 2) -# igmp_binds = [ (IP, IGMP, { "proto": 2 , "ttl": 1 }), -# 2. tos = 0xC0 (RFC 3376, section 4) -# (IP, IGMPv3, { "proto": 2 , "ttl": 1, "tos":0xc0 }), -# (IGMPv3, IGMPv3gr, { }) ] -# The rules are: -# 1. the Max Response time is meaningful only in Membership Queries and should be zero -# otherwise (RFC 2236, section 2.2) - - if (self.type != 0x11): #rule 1 - self.mrtime = 0 - - if (self.adjust_ip(ip) == True): - if (self.adjust_ether(ip, ether) == True): return True - return False - -#-------------------------------------------------------------------------- - def adjust_ether (self, ip=None, ether=None): - """Called to explicitly fixup an associated Ethernet header - - The function adjusts the ethernet header destination MAC address based on - the destination IP address. - """ -# The rules are: -# 1. send to the group mac address address corresponding to the IP.dst - if ip != None and ip.haslayer(IP) and ether != None and ether.haslayer(Ether): - iplong = atol(ip.dst) - ether.dst = "01:00:5e:%02x:%02x:%02x" % ( (iplong>>16)&0x7F, (iplong>>8)&0xFF, (iplong)&0xFF ) - # print "igmpize ip " + ip.dst + " as mac " + ether.dst - return True - else: - return False - -#-------------------------------------------------------------------------- - def adjust_ip (self, ip=None): - """Called to explicitly fixup an associated IP header - - The function adjusts the IP header based on conformance rules - and the group address encoded in the IGMP message. - The rules are: - 1. Send General Group Query to 224.0.0.1 (all systems) - 2. Send Leave Group to 224.0.0.2 (all routers) - 3a.Otherwise send the packet to the group address - 3b.Send reports/joins to the group address - 4. ttl = 1 (RFC 2236, section 2) - 5. send the packet with the router alert IP option (RFC 2236, section 2) - """ - if ip != None and ip.haslayer(IP): - if (self.type == 0x11): - if (self.gaddr == "0.0.0.0"): - ip.dst = "224.0.0.1" # IP rule 1 - retCode = True - elif isValidMCAddr(self.gaddr): - ip.dst = self.gaddr # IP rule 3a - retCode = True + name = "IGMPv3" + igmpv3types = { 0x11 : "Membership Query", + 0x22 : "Version 3 Membership Report", + 0x30 : "Multicast Router Advertisement", + 0x31 : "Multicast Router Solicitation", + 0x32 : "Multicast Router Termination"} + + fields_desc = [ ByteEnumField("type", 0x11, igmpv3types), + ByteField("mrcode", 20), + XShortField("chksum", None)] + + def float_encode(self, value): + """Convert the integer value to its IGMPv3 encoded time value if needed. + + If value < 128, return the value specified. If >= 128, encode as a floating + point value. Value can be 0 - 31744. + """ + if value < 128: + code = value + elif value > 31743: + code = 255 + else: + exp=0 + value>>=3 + while(value>31): + exp+=1 + value>>=1 + exp<<=4 + code = 0x80 | exp | (value & 0x0F) + return code + + + def mysummary(self): + """Display a summary of the IGMPv3 object.""" + if isinstance(self.underlayer, IP): + return self.underlayer.sprintf("IGMPv3: %IP.src% > %IP.dst% %IGMPv3.type%") else: - print("Warning: Using invalid Group Address") - retCode = False - elif ((self.type == 0x17) and isValidMCAddr(self.gaddr)): - ip.dst = "224.0.0.2" # IP rule 2 - retCode = True - elif ((self.type == 0x12) or (self.type == 0x16)) and (isValidMCAddr(self.gaddr)): - ip.dst = self.gaddr # IP rule 3b - retCode = True - else: - print("Warning: Using invalid IGMP Type") - retCode = False - else: - print("Warning: No IGMP Group Address set") - retCode = False - if retCode == True: - ip.ttl=1 # IP Rule 4 - ip.options=[IPOption_Router_Alert()] # IP rule 5 - return retCode + return self.sprintf("IGMPv3 %IGMPv3.type%") + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 4: + if ord(_pkt[0]) in [0x12, 0x16, 0x17]: + return IGMP + elif ord(_pkt[0]) == 0x11 and len(_pkt) < 12: + return IGMP + return IGMPv3 + +class IGMPv3mq(Packet): + """IGMPv3 Membership Query. + Payload of IGMPv3 when type=0x11""" + name = "IGMPv3gr" + fields_desc = [ IPField("gaddr", "0.0.0.0"), + BitField("resv", 0, 4), + BitField("s", 0, 1), + BitField("qrv", 0, 3), + ByteField("qqic",0), + FieldLenField("numsrc", None, count_of="srcaddrs"), + FieldListField("srcaddrs", None, IPField("sa", "0.0.0.0"), count_from=lambda x: x.numsrc)] +class IGMPv3gr(Packet): + """IGMP Group Record for IGMPv3 Membership Report -bind_layers(IP, IGMPv3, frag=0, proto=2, ttl=1, tos=0xc0) -bind_layers(IGMPv3, IGMPv3gr) -bind_layers(IGMPv3gr, IGMPv3gr) + This class is derived from class Packet and should be added in the records + of an instantiation of class IGMPv3mr. + """ + name = "IGMPv3gr" + igmpv3grtypes = { 1 : "Mode Is Include", + 2 : "Mode Is Exclude", + 3 : "Change To Include Mode", + 4 : "Change To Exclude Mode", + 5 : "Allow New Sources", + 6 : "Block Old Sources"} + + fields_desc = [ ByteEnumField("rtype", 1, igmpv3grtypes), + ByteField("auxdlen",0), + FieldLenField("numsrc", None, count_of="srcaddrs"), + IPField("maddr", "0.0.0.0"), + FieldListField("srcaddrs", [], IPField("sa", "0.0.0.0"), count_from=lambda x: x.numsrc) ] + + def mysummary(self): + """Display a summary of the IGMPv3 group record.""" + return self.sprintf("IGMPv3 Group Record %IGMPv3gr.type% %IGMPv3gr.maddr%") + + def default_payload_class(self, payload): + return conf.padding_layer + +class IGMPv3mr(Packet): + """IGMP Membership Report extension for IGMPv3. + Payload of IGMPv3 when type=0x22""" + name = "IGMPv3mr" + fields_desc = [ ByteField("res2", 0), + FieldLenField("numgrp", None, count_of="records"), + PacketListField("records", [], IGMPv3gr, count_from=lambda x: x.numgrp)] + +class IGMPv3mra(Packet): + """IGMP Multicas Router Advertisement extension for IGMPv3. + Payload of IGMPv3 when type=0x30""" + name = "IGMPv3mra" + fields_desc = [ ShortField("qryIntvl", 0), + ShortField("robust", 0)] + +bind_layers(IP, IGMPv3, frag=0, + proto=2, + ttl=1, + tos=0xc0) + +bind_layers(IGMPv3, IGMPv3mq, type=0x11) +bind_layers(IGMPv3, IGMPv3mr, type=0x22) +bind_layers(IGMPv3, IGMPv3mra, type=0x30) diff --git a/scapy/contrib/igmpv3.uts b/scapy/contrib/igmpv3.uts new file mode 100644 index 0000000000000000000000000000000000000000..ef52eb369e34244cd4c4edb34371330dede00d0b --- /dev/null +++ b/scapy/contrib/igmpv3.uts @@ -0,0 +1,46 @@ +############## +% IGMPv3 tests +############## + ++ Basic IGMPv3 tests + += Build IGMPv3 - Basic + +a=Ether(src="00:01:02:03:04:05") +b=IP(src="1.2.3.4") +c=IGMPv3()/IGMPv3mq() +x = a/b/c +x[IGMPv3].igmpize() +assert x.mrcode == 20 +assert x[IP].dst == "224.0.0.1" +assert isinstance(IGMP(str(x[IGMPv3])), IGMPv3) + += Dissect IGMPv3 - IGMPv3mq + +x = Ether(b'\x14\x0cv\x8f\xfe(\x00\x01\x02\x03\x04\x05\x08\x00F\xc0\x00$\x00\x01\x00\x00\x01\x02\xe4h\xc0\xa8\x00\x01\x7f\x00\x00\x01\x94\x04\x00\x00\x11\x14\x0e\xe9\xe0\x00\x00\x02\x00\x00\x00\x00') +assert IGMPv3 in x +assert IGMPv3mq in x +assert x[IGMPv3mq].gaddr == "224.0.0.2" +assert x.summary() == "Ether / IP / IGMPv3: 192.168.0.1 > 127.0.0.1 Membership Query / IGMPv3mq" +assert isinstance(IGMP(str(x[IGMPv3])), IGMPv3) + += Dissect IGMPv3 - IGMPv3mr + +x = Ether(b'\x14\x0cv\x8f\xfe(\x00\x01\x02\x03\x04\x05\x08\x00F\xc0\x00;\x00\x01\x00\x00\x01\x02\xe4Q\xc0\xa8\x00\x01\x7f\x00\x00\x01\x94\x04\x00\x00"\x14w\xcc\x00\x00\x02\x01\x00\x00\x02{{{{\xc0\xa8\x00\x01\xc0\xa8\x84\xf7\x01\x00\x00\x01\x04\x04\x04\x04\x0c\x0c\x0c\x0c') +assert IGMPv3 in x +assert IGMPv3mr in x +assert len(x[IGMPv3mr].records) == 2 +assert x[IGMPv3mr].records[0].srcaddrs == ["192.168.0.1", "192.168.132.247"] +assert x[IGMPv3mr].records[1].maddr == "4.4.4.4" +assert isinstance(IGMP(str(x[IGMPv3])), IGMPv3) + += Dissect IGMPv3 - IGMPv3mra + +x = Ether(b'\x14\x0cv\x8f\xfe(\x00\x01\x02\x03\x04\x05\x08\x00F\xc0\x00 \x00\x01\x00\x00\x01\x02\xe4l\xc0\xa8\x00\x01\x7f\x00\x00\x01\x94\x04\x00\x000\x14\xcf\xe6\x00\x03\x00\x02') +assert IGMPv3 in x +assert IGMPv3mra in x +assert x[IGMPv3mra].qryIntvl == 3 +assert x[IGMPv3mra].robust == 2 +assert isinstance(IGMP(str(x[IGMPv3])), IGMPv3) + += IGMP vs IVMPv3 tests