Ticket #31: igmp.py

File igmp.py, 15.5 kB (added by David Sips, 2 years ago)

Another version for IGMP

Line 
1 #! /usr/bin/env python
2
3
4 #------------------------------------------------------------------------------
5 #------------------------------------------------------------------------------
6 #
7 # See RFC2236, Section 2. Introduction for definitions of proper IGMPv2 message format
8 #   http://www.faqs.org/rfcs/rfc2236.html
9 #
10
11 igmptypes = { 0x11 : "Group Membership Query",
12               0x12 : "Version 1 - Membership Report",
13               0x16 : "Version 2 - Membership Report",
14               0x17 : "Leave Group"}
15
16 class IGMP(Packet):
17   """IGMP Message Class for v1 and v2.
18
19   This class is derived from class Packet. You may need to "sanitize" the IP
20   and Ethernet headers before a full packet is sent.
21
22   """
23   name = "IGMP"
24   fields_desc = [ ByteEnumField("type", 0x11, igmptypes),
25                       ByteField("mrtime",0),
26                     XShortField("chksum", None),
27                         IPField("gaddr", "0.0.0.0")]
28
29 #------------------------------------------------------------------------------
30   def post_build(self, p, pay):
31     """Called implicitly before a packet is sent to compute and place IGMP checksum.
32
33     Parameters:
34       self    The instantiation of an IGMP class
35       p       The IGMP message in hex in network byte order
36       pay     Additional payload for the IGMP message
37     """
38     p += pay
39     if self.chksum is None:
40       ck = checksum(p)
41       p = p[:2]+chr(ck>>8)+chr(ck&0xff)+p[4:]
42     return p
43
44 #------------------------------------------------------------------------------
45   def mysummary(self):
46     """Display a summary of the IGMP object."""
47
48     if isinstance(self.underlayer, IP):
49       return self.underlayer.sprintf("IGMP: %IP.src% > %IP.dst% %IGMP.type% %IGMP.gaddr%")
50     else:
51       return self.sprintf("IGMP %IGMP.type% %IGMP.gaddr%")
52
53 #------------------------------------------------------------------------------
54   def sanitize (self, ip=None, ether=None):
55     """Called to explicitely fixup associated IP and Ethernet headers
56
57     Parameters:
58       self    The instantiation of an IGMP class.
59       ip      The instantiation of the associated IP class.
60       ether   The instantiation of the associated Ethernet.
61
62     Returns:
63       True    The tuple ether/ip/self passed all check and represents a proper
64               IGMP packet.
65       False   One of more validation checks failed and no fields were adjusted.
66
67     The function will examine the IGMP message to assure proper format.
68     Corrections will be attempted if possible. The IP header is then properly
69     adjusted to ensure correct formatting and assignment. The Ethernet header
70     is then adjusted to the proper IGMP packet format.
71     """
72
73 # The rules are:
74 #   1.  the Max Response time is meaningful only in Membership Queries and should be zero
75 #       otherwise (RFC 2236, section 2.2)
76     if (self.type != 0x11):         #rule 1
77       self.mrtime = 0
78     if (self.adjust_ip(ip) == True):
79       if (self.adjust_ether(ip, ether) == True): return True
80     return False
81
82 #------------------------------------------------------------------------------
83   def adjust_ether (self, ip=None, ether=None):
84     """Called to explicitely fixup an associated Ethernet header
85
86     Parameters:
87       self    The instantiation of an IGMP class.
88       ip      The instantiation of the associated IP class.
89       ether   The instantiation of the associated Ethernet.
90
91     Returns:
92       True    The tuple ether/ip/self passed all check and represents a proper
93               IGMP packet.
94       False   One of more validation checks failed and no fields were adjusted.
95
96     The function adjusts the ethernet header destination MAC address based on
97     the destination IP address.
98     """
99 # The rules are:
100 #   1. send the packet to the group mac address address corresponding to the IP
101     if ip != None and ip.haslayer(IP) and ether != None and ether.haslayer(Ether):
102       ipaddr = socket.ntohl(atol(ip.dst)) & 0x007FFFFF
103       macstr = "01:5e:00:%02x:%02x:%02x" %((ipaddr>>16)&0xFF, (ipaddr>>8)&0xFF, (ipaddr>>0)&0xFF)
104       ether.dst=macstr
105       return True
106     else:
107       return False
108
109 #------------------------------------------------------------------------------
110   def adjust_ip (self, ip=None):
111     """Called to explicitely fixup an associated IP header
112
113     Parameters:
114       self    The instantiation of an IGMP class.
115       ip      The instantiation of the associated IP class.
116
117     Returns:
118       True    The tuple ip/self passed all checks and represents a proper
119               IGMP packet.
120       False   One of more validation checks failed and no fields were adjusted.
121
122     The function adjusts the IP header based on conformance rules and the group
123     address encoded in the IGMP message.
124     """
125 # The rules are:
126 #   1. send the packet to the group address registering/reporting for
127 #     a. for General Group Query, send packet to 224.0.0.1 (all systems)
128 #   2. send the packet with the router alert IP option (RFC 2236, section 2)
129 #   3. ttl = 1 (RFC 2236, section 2)
130     if ip != None and ip.haslayer(IP):
131       if (self.type == 0x11):
132         if (self.gaddr == "0.0.0.0"):
133           ip.dst = "224.0.0.1"
134           retCode = True                     
135         elif (atol(self.gaddr)&0xff >= 224) and (atol(self.gaddr)&0xff <= 239):
136           ip.dst = self.gaddr
137           retCode = True
138         else:
139           retCode = False
140       elif ((self.type == 0x12) or (self.type == 0x16) or (self.type == 0x17)) and \
141            (atol(self.gaddr)&0xff >= 224) and (atol(self.gaddr)&0xff <= 239):
142           ip.dst = self.gaddr
143           retCode = True
144       elif (self.type == 0x22):
145         ip.dst = "224.0.0.22"
146         retCode = True
147       elif (self.type == 0x30) or (self.type == 0x32):
148         ip.dst = "224.0.0.106"
149         retCode = True
150       elif (self.type == 0x31):
151         ip.dst = "224.0.0.2"
152         retCode = True
153       else:
154         retCode = False
155     else:
156       retCode = False
157     if retCode == True:
158       ip.options="\x94\x04\x00\x00"   # set IP Router Alert option
159     return retCode
160
161 #------------------------------------------------------------------------------
162 #------------------------------------------------------------------------------
163 #
164 # See RFC3376, Section 4. Message Formats for definitions of proper IGMPv3 message format
165 #   http://www.faqs.org/rfcs/rfc3376.html
166 #
167 # See RFC4286, For definitions of proper messages for Multicast Router Discovery.
168 #   http://www.faqs.org/rfcs/rfc4286.html
169 #
170
171 igmpv3grtypes = { 1 : "Mode Is Include",
172                   2 : "Mode Is Exclude",
173                   3 : "Change To Include Mode",
174                   4 : "Change To Exclude Mode",
175                   5 : "Allow New Sources",
176                   6 : "Block Old Sources"}
177
178 class IGMPv3gr(Packet):
179   """IGMP Group Record for IGMPv3 Membership Report
180
181   This class is derived from class Packet and should be concatenated to an
182   instantiation of class IGMPv3. Within the IGMPv3 instantiation, the numgrp
183   element will need to be manipulated to indicate the proper number of
184   group records.
185   """
186   name = "IGMPv3gr"
187   fields_desc = [ ByteEnumField("rtype", 1, igmpv3grtypes),
188                       ByteField("auxdlen",0),
189                   FieldLenField("numsrc", None, "srcaddrs"),
190                         IPField("maddr", "0.0.0.0"),
191                  FieldListField("srcaddrs", None, IPField("sa", "0.0.0.0"), "numsrc") ]
192   show_indent=0
193 #------------------------------------------------------------------------------
194   def post_build(self, p, pay):
195     """Called implicitly before a packet is sent.
196     """
197     p += pay
198     if self.auxdlen != 0:
199       print "NOTICE: A properly formatted and complaint V3 Group Record should have an Auxiliary Data length of zero (0)."
200       print "        Subsequent Group Records are lost!"
201     return p
202 #------------------------------------------------------------------------------
203   def mysummary(self):
204     """Display a summary of the IGMPv3 group record."""
205     return self.sprintf("IGMPv3 Group Record %IGMPv3gr.type% %IGMPv3gr.maddr%")
206
207 igmpv3types = { 0x11 : "Membership Query",
208                 0x22 : "Version 3 Membership Report",
209                 0x30 : "Multicast Router Advertisement",
210                 0x31 : "Multicast Router Solicitation",
211                 0x32 : "Multicast Router Termination"}
212
213 class IGMPv3(IGMP):
214   """IGMP Message Class for v3.
215
216   This class is derived from class Packet.
217   The fields defined below are a
218   direct interpretation of the v3 Membership Query Message.
219   Fields 'type'  through 'qqic' are directly assignable.
220   For 'numsrc', do not assign a value.
221   Instead add to the 'srcaddrs' list to auto-set 'numsrc'. To
222   assign values to 'srcaddrs', use the following methods:
223     c = IGMPv3()
224     c.srcaddrs = ['1.2.3.4', '5.6.7.8']
225     c.srcaddrs += ['192.168.10.24']
226   At this point, 'c.numsrc' is three (3)
227
228   'chksum' is automagically calculated before the packet is sent.
229
230   'mrcode' is also the Advertisement Interval field
231
232   """
233   name = "IGMPv3"
234   fields_desc = [ ByteEnumField("type", 0x11, igmpv3types),
235                       ByteField("mrcode",0),              # use float_encode()
236                     XShortField("chksum", None),
237     # if type = 0x11 (Membership Query), the next field is group address
238        ConditionalField(IPField("gaddr", "0.0.0.0"), "type", lambda x:x==0x11),
239     # else if type = 0x22 (Membership Report), the next fields are
240     #         reserved and number of group records
241     ConditionalField(ShortField("rsvd2", 0), "type", lambda x:x==0x22),
242     ConditionalField(ShortField("numgrp", 0), "type", lambda x:x==0x22),
243 #                  FieldLenField("numgrp", None, "grprecs")]
244     # else if type = 0x30 (Multicast Router Advertisement), the next fields are
245     #         query interval and robustness
246     ConditionalField(ShortField("qryIntvl", 0), "type", lambda x:x==0x30),
247     ConditionalField(ShortField("robust", 0), "type", lambda x:x==0x30),
248 #  The following are only present for membership queries
249           ConditionalField(BitField("resv", 0, 4), "type", lambda x:x==0x11),
250           ConditionalField(BitField("s", 0, 1), "type", lambda x:x==0x11),
251           ConditionalField(BitField("qrv", 0, 3), "type", lambda x:x==0x11),
252          ConditionalField(ByteField("qqic",0), "type", lambda x:x==0x11),
253      ConditionalField(FieldLenField("numsrc", None, "srcaddrs"), "type", lambda x:x==0x11),
254     ConditionalField(FieldListField("srcaddrs", None, IPField("sa", "0.0.0.0"), "numsrc"), "type", lambda x:x==0x11),
255     ]
256
257 #------------------------------------------------------------------------------
258   def float_encode(self, value):
259     """Convert the integer value to its IGMPv3 encoded time value if needed.
260   
261     If value < 128, return the value specified. If >= 128, encode as a floating
262     point value. Value can be 0 - 31744.
263     """
264     if value < 128:
265       code = value
266     elif value > 31743:
267       code = 255
268     else:
269       exp=0
270       value>>=3
271       while(value>31):
272         exp+=1
273         value>>=1
274       exp<<=4
275       code = 0x80 | exp | (value & 0x0F)
276     return code
277
278 #------------------------------------------------------------------------------
279   def post_build(self, p, pay):
280     """Called implicitly before a packet is sent to compute and place IGMPv3 checksum.
281
282     Parameters:
283       self    The instantiation of an IGMPv3 class
284       p       The IGMPv3 message in hex in network byte order
285       pay     Additional payload for the IGMPv3 message
286     """
287     p += pay
288     if self.type in [0, 0x31, 0x32, 0x22]:   # for these, field is reserved (0)
289       p = p[:1]+chr(0)+p[2:]
290     if self.chksum is None:
291       ck = checksum(p)
292       p = p[:2]+chr(ck>>8)+chr(ck&0xff)+p[4:]
293     return p
294
295 #------------------------------------------------------------------------------
296   def mysummary(self):
297     """Display a summary of the IGMPv3 object."""
298
299     if isinstance(self.underlayer, IP):
300       return self.underlayer.sprintf("IGMPv3: %IP.src% > %IP.dst% %IGMPv3.type% %IGMPv3.gaddr%")
301     else:
302       return self.sprintf("IGMPv3 %IGMPv3.type% %IGMPv3.gaddr%")
303
304 # The rules are:
305 #   1.  ttl = 1 (RFC 2236, section 2)
306 igmp_binds = [ (IP, IGMP,   { "proto": 2 , "ttl": 1 }),
307 #   2.  tos = 0xC0 (RFC 3376, section 4)
308                (IP, IGMPv3, { "proto": 2 , "ttl": 1, "tos":0xc0 }),
309                (IGMPv3, IGMPv3gr, { }) ]
310
311 def test_igmp ():
312   a=Ether()
313   a.src="00:01:5a:29:00:AA"
314   a.dst="00:01:23:45:67:89"
315   b=IP()
316   b.src="1.2.3.4"
317   b.dst="5.6.7.8"
318   c=IGMP()
319   d=a/b/c
320   print "Send a simple IGMP packet with no fixup"
321   print "Ethernet, IP, IGMP"
322   sendp(d, iface="eth1")
323 #
324   c.sanitize(b, a)
325   d=a/b/c
326   print "Send a properly formatted General Query"
327   print "DA = 01:5e:00:00:00:01, DIP=224.0.0.1, Router Alert Option Set"
328   sendp(d, iface="eth1")
329 #
330   c.gaddr="224.1.2.3"
331   c.mrtime=30
332   c.sanitize(b, a)
333   d=a/b/c
334   print "Send a properly formatted Group-Specific Query w/ Max response time = 3.0s"
335   sendp(d, iface="eth1")
336 #
337   c.gaddr="224.2.3.4"
338   c.type=0x12
339   c.chksum=0xabcd
340   c.sanitize(b, a)
341   d=a/b/c
342   print "Send a properly formatted V1 membership report w/ bad checksum"
343   sendp(d, iface="eth1")
344 #
345   c.gaddr="224.3.4.5"
346   c.type=0x16
347   c.chksum=None
348   c.mrtime=0xee
349   c.sanitize(b, a)
350   d=a/b/c
351   print "Send a properly formatted V2 membership report"
352   sendp(d, iface="eth1")
353 #
354   c.gaddr="224.4.5.6"
355   c.type=0x17
356   c.sanitize(b, a)
357   d=a/b/c
358   print "Send a properly formatted leave group"
359   sendp(d, iface="eth1")
360   d.show()
361   d.show2()
362   d.mysummary()
363 #
364 def test_igmpv3 ():
365   a=Ether()
366   a.src="00:01:5a:29:00:AA"
367   a.dst="00:01:23:45:67:89"
368   b=IP()
369   b.src="1.2.3.4"
370   b.dst="5.6.7.8"
371   c=IGMPv3()
372   d=a/b/c
373   print "Send a simple IGMPv3 packet"
374   sendp(d, iface="eth1")
375 #
376   c.sanitize(b, a)
377   d=a/b/c
378   print "Send a properly formatted V3 General Membership Query"
379   print "DA = 01:5e:00:00:00:01, DIP=224.0.0.1, DSCP = 0x30, Router Alert Option Set"
380   sendp(d, iface="eth1")
381 #
382   c.gaddr="225.150.2.3"
383   c.mrcode=c.float_encode(240)
384   c.s=1
385   c.qrv=3
386   c.sanitize(b, a)
387   d=a/b/c
388   print "Send a properly formatted Group-Specific Query w/ Max response time = 24.0s"
389   print "Suppress router-side processing, QRV=3"
390   sendp(d, iface="eth1")
391   d.show()
392   d.show2()
393   d.mysummary()
394 #
395   b.src="0.0.0.0"
396   c.gaddr="225.162.3.4"
397   c.s=0
398   c.qqic=c.float_encode(109)
399   c.qrv=4
400   c.srcaddrs = ['1.2.3.4', '5.6.7.8']
401   c.sanitize(b, a)
402   d=a/b/c
403   print "Send a properly formatted Group/Source-Specific Query w/ QQIC = 109"
404   print "DA = 01:5e:00:22:03:04, DIP=225.162.3.4, DSCP = 0x30, Router Alert Option Set"
405   print "QRV=4, QQIC=109, Num Sources = 2 ('1.2.3.4' and '5.6.7.8')"
406   sendp(d, iface="eth1")
407 #
408   b.src="1.2.3.4"
409   b.dst="225.163.4.5"
410   c.type=0x22
411   c.numgrp=3
412   e=IGMPv3gr()
413   e.rtype=3
414   e.maddr="225.99.44.33"
415   e.srcaddrs=["192.168.77.12"]
416   e.srcaddrs+=["192.168.77.13"]
417   f=IGMPv3gr()
418   f.rtype=5
419   f.maddr="228.200.77.88"
420   f.srcaddrs=["192.168.77.14", "192.168.77.15"]
421   g=IGMPv3gr()
422   g.rtype=6
423   g.maddr="229.190.191.192"
424   g.auxdlen=2       
425   g.add_payload("\x0f\x0e\x0d\x0c\x0b\x0a\x09\x08")
426   c.sanitize(b, a)
427   d=a/b/c/e/f/g
428   print "Send a properly formatted V3 Membership Report. Three Group records, the last with auxilliary data"
429   sendp(d, iface="eth1")
430   d.show()
431   d.show2()
432   d.mysummary()
433 #
434   b.dst="225.213.4.5"
435   c.numgrp=0
436   c.sanitize(b, a)
437   d=a/b/c
438   print "Send a properly formatted V3 Membership Report"
439   sendp(d, iface="eth1")
440 #
441   c.type=0x30
442   c.mrcode=144
443   c.qryIntvl=204
444   c.robust=248
445   c.sanitize(b, a)
446   d=a/b/c
447   print "Send a properly formatted V3 Multicast Router Advertisement"
448   sendp(d, iface="eth1")
449 #
450   c.type=0x31
451   c.sanitize(b, a)
452   d=a/b/c
453   print "Send a properly formatted V3 Multicast Router Solicitation"
454   sendp(d, iface="eth1")
455 #
456   c.type=0x32
457   c.sanitize(b, a)
458   d=a/b/c
459   print "Send a properly formatted V3 Multicast Router Termination"
460   sendp(d, iface="eth1")
461   d.show()
462   d.show2()
463   d.mysummary()