Zsolt Haraszti | fe52550 | 2016-03-02 05:18:52 +0000 | [diff] [blame] | 1 | from socket import * |
| 2 | from struct import * |
| 3 | from scapy.all import * |
| 4 | from itertools import * |
| 5 | |
| 6 | IGMP_TYPE_MEMBERSHIP_QUERY = 0x11 |
| 7 | IGMP_TYPE_V3_MEMBERSHIP_REPORT = 0x22 |
| 8 | IGMP_TYPE_V1_MEMBERSHIP_REPORT = 0x12 |
| 9 | IGMP_TYPE_V2_MEMBERSHIP_REPORT = 0x16 |
| 10 | IGMP_TYPE_V2_LEAVE_GROUP = 0x17 |
| 11 | |
| 12 | IGMP_V3_GR_TYPE_INCLUDE = 0x01 |
| 13 | IGMP_V3_GR_TYPE_EXCLUDE = 0x02 |
| 14 | IGMP_V3_GR_TYPE_CHANGE_TO_INCLUDE = 0x03 |
| 15 | IGMP_V3_GR_TYPE_CHANGE_TO_EXCLUDE = 0x04 |
| 16 | IGMP_V3_GR_TYPE_ALLOW_NEW = 0x05 |
| 17 | IGMP_V3_GR_TYPE_BLOCK_OLD = 0x06 |
| 18 | |
| 19 | """ |
| 20 | IGMPV3_ALL_ROUTERS = '224.0.0.22' |
| 21 | IGMPv3 = 3 |
| 22 | IP_SRC = '1.2.3.4' |
| 23 | ETHERTYPE_IP = 0x0800 |
| 24 | IGMP_DST_MAC = "01:00:5e:00:01:01" |
| 25 | IGMP_SRC_MAC = "5a:e1:ac:ec:4d:a1" |
| 26 | """ |
| 27 | |
Zsolt Haraszti | ecf8945 | 2016-03-01 22:38:13 -0800 | [diff] [blame] | 28 | |
| 29 | class IGMPv3gr(Packet): |
| 30 | """IGMPv3 Group Record, used in membership report""" |
| 31 | |
| 32 | name = "IGMPv3gr" |
| 33 | |
| 34 | igmp_v3_gr_types = { |
| 35 | IGMP_V3_GR_TYPE_INCLUDE: "Include Mode", |
| 36 | IGMP_V3_GR_TYPE_EXCLUDE: "Exclude Mode", |
| 37 | IGMP_V3_GR_TYPE_CHANGE_TO_INCLUDE: "Change to Include Mode", |
| 38 | IGMP_V3_GR_TYPE_CHANGE_TO_EXCLUDE: "Change to Exclude Mode", |
| 39 | IGMP_V3_GR_TYPE_ALLOW_NEW: "Allow New Sources", |
| 40 | IGMP_V3_GR_TYPE_BLOCK_OLD: "Block Old Sources" |
| 41 | } |
| 42 | |
| 43 | fields_desc = [ |
| 44 | ByteEnumField("rtype", IGMP_V3_GR_TYPE_INCLUDE, igmp_v3_gr_types), |
| 45 | ByteField("aux_data_len", 0), |
| 46 | FieldLenField("numsrc", None, count_of="sources"), |
| 47 | IPField("mcaddr", "0.0.0.0"), |
| 48 | FieldListField("sources", None, IPField("src", "0.0.0.0"), "numsrc") |
| 49 | ] |
| 50 | |
| 51 | def post_build(self, pkt, payload): |
| 52 | pkt += payload |
| 53 | if self.aux_data_len != 0: |
| 54 | print "WARNING: Auxiliary Data Length must be zero (0)" |
| 55 | return pkt |
| 56 | |
| 57 | |
Zsolt Haraszti | fe52550 | 2016-03-02 05:18:52 +0000 | [diff] [blame] | 58 | class IGMPv3(Packet): |
| 59 | |
| 60 | name = "IGMPv3" |
| 61 | |
| 62 | igmp_v3_types = { |
| 63 | IGMP_TYPE_MEMBERSHIP_QUERY: "Membership Query", |
| 64 | IGMP_TYPE_V3_MEMBERSHIP_REPORT: " Version 3 Mebership Report", |
| 65 | IGMP_TYPE_V2_MEMBERSHIP_REPORT: " Version 2 Mebership Report", |
| 66 | IGMP_TYPE_V1_MEMBERSHIP_REPORT: " Version 1 Mebership Report", |
| 67 | IGMP_TYPE_V2_LEAVE_GROUP: "Version 2 Leave Group" |
| 68 | } |
| 69 | |
| 70 | fields_desc = [ |
| 71 | ByteEnumField("type", IGMP_TYPE_MEMBERSHIP_QUERY, igmp_v3_types), |
| 72 | ByteField("max_resp_code", 0), |
| 73 | XShortField("checksum", None), |
| 74 | #IPField("group_address", "0.0.0.0"), |
| 75 | |
| 76 | # membership query fields |
| 77 | ConditionalField(IPField("gaddr", "0.0.0.0"), lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
| 78 | ConditionalField(BitField("resv", 0, 4), lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
| 79 | ConditionalField(BitField("s", 0, 1), lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
| 80 | ConditionalField(BitField("qrv", 0, 3), lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
| 81 | ConditionalField(ByteField("qqic", 0), lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
| 82 | ConditionalField(FieldLenField("numsrc", None, count_of="srcs"), lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
Zsolt Haraszti | ecf8945 | 2016-03-01 22:38:13 -0800 | [diff] [blame] | 83 | ConditionalField(FieldListField("srcs", None, IPField("src", "0.0.0.0"), "numsrc"), lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
Zsolt Haraszti | fe52550 | 2016-03-02 05:18:52 +0000 | [diff] [blame] | 84 | |
| 85 | # membership report fields |
Zsolt Haraszti | ecf8945 | 2016-03-01 22:38:13 -0800 | [diff] [blame] | 86 | ConditionalField(ShortField("resv2", 0), lambda pkt: pkt.type == IGMP_TYPE_V3_MEMBERSHIP_REPORT), |
| 87 | ConditionalField(FieldLenField("numgrp", None, count_of="grps"), lambda pkt: pkt.type == IGMP_TYPE_V3_MEMBERSHIP_REPORT), |
| 88 | ConditionalField(PacketListField("grps", [], IGMPv3gr), lambda pkt: pkt.type == IGMP_TYPE_V3_MEMBERSHIP_REPORT) |
| 89 | |
| 90 | # TODO: v2 and v3 membership reports? |
Zsolt Haraszti | fe52550 | 2016-03-02 05:18:52 +0000 | [diff] [blame] | 91 | |
| 92 | ] |
| 93 | |
| 94 | def post_build(self, pkt, payload): |
| 95 | |
| 96 | pkt += payload |
| 97 | |
| 98 | if self.type in [IGMP_TYPE_V3_MEMBERSHIP_REPORT,]: # max_resp_code field is reserved (0) |
| 99 | mrc = 0 |
| 100 | else: |
| 101 | mrc = self.encode_float(self.max_resp_code) |
| 102 | pkt = pkt[:1] + chr(mrc) + pkt[2:] |
| 103 | |
| 104 | if self.checksum is None: |
| 105 | chksum = checksum(pkt) |
| 106 | pkt = pkt[:2] + chr(chksum >> 8) + chr(chksum & 0xff) + pkt[4:] |
| 107 | |
| 108 | return pkt |
| 109 | |
| 110 | def encode_float(self, value): |
| 111 | """Encode max response time value per RFC 3376.""" |
| 112 | if value < 128: |
| 113 | return value |
| 114 | if value > 31743: |
| 115 | return 255 |
| 116 | exp = 0 |
| 117 | value >>= 3 |
| 118 | while value > 31: |
| 119 | exp += 1 |
| 120 | value >>= 1 |
| 121 | return 0x80 | (exp << 4) | (value & 0xf) |
| 122 | |
| 123 | |
| 124 | def decode_float(self, code): |
| 125 | if code < 128: |
| 126 | return code |
| 127 | mant = code & 0xf |
| 128 | exp = (code >> 4) & 0x7 |
| 129 | return (mant | 0x10) << (exp + 3) |
| 130 | |
| 131 | @staticmethod |
| 132 | def is_valid_mcaddr(ip): |
| 133 | byte1 = atol(ip) >> 24 & 0xff |
| 134 | return (byte1 & 0xf0) == 0xe0 |
| 135 | |
| 136 | @staticmethod |
| 137 | def fixup(pkt): |
| 138 | """Fixes up the underlying IP() and Ether() headers.""" |
| 139 | assert pkt.haslayer(IGMPv3), "This packet is not an IGMPv4 packet; cannot fix it up" |
| 140 | |
| 141 | igmp = pkt.getlayer(IGMPv3) |
| 142 | |
| 143 | if pkt.haslayer(IP): |
| 144 | ip = pkt.getlayer(IP) |
| 145 | ip.ttl = 1 |
| 146 | ip.proto = 2 |
| 147 | ip.tos = 0xc0 |
Zsolt Haraszti | d057140 | 2016-03-02 18:40:50 -0800 | [diff] [blame] | 148 | #ip.options = [IPOption_Router_Alert()] |
Zsolt Haraszti | fe52550 | 2016-03-02 05:18:52 +0000 | [diff] [blame] | 149 | |
| 150 | if igmp.type == IGMP_TYPE_MEMBERSHIP_QUERY: |
| 151 | if igmp.gaddr == "0.0.0.0": |
| 152 | ip.dst = "224.0.0.1" |
| 153 | else: |
| 154 | assert IGMPv3.is_valid_mcaddr(igmp.gaddr), "IGMP membership query with invalid mcast address" |
| 155 | ip.dst = igmp.gaddr |
| 156 | |
| 157 | elif igmp.type == IGMP_TYPE_V2_LEAVE_GROUP and IGMPv3.is_valid_mcaddr(igmp.gaddr): |
| 158 | ip.dst = "224.0.0.2" |
| 159 | |
| 160 | elif (igmp.type in (IGMP_TYPE_V1_MEMBERSHIP_REPORT, IGMP_TYPE_V2_MEMBERSHIP_REPORT) and |
| 161 | IGMPv3.is_valid_mcaddr(igmp.gaddr)): |
| 162 | ip.dst = igmp.gaddr |
| 163 | |
| 164 | # We do not need to fixup the ether layer, it is done by scapy |
| 165 | # |
| 166 | # if pkt.haslayer(Ether): |
| 167 | # eth = pkt.getlayer(Ether) |
| 168 | # ip_long = atol(ip.dst) |
| 169 | # ether.dst = '01:00:5e:%02x:%02x:%02x' % ( (ip_long >> 16) & 0x7f, (ip_long >> 8) & 0xff, ip_long & 0xff ) |
| 170 | |
| 171 | |
| 172 | return pkt |
| 173 | |
| 174 | |
Zsolt Haraszti | fe52550 | 2016-03-02 05:18:52 +0000 | [diff] [blame] | 175 | bind_layers(IP, IGMPv3, frag=0, proto=2, ttl=1, tos=0xc0) |
| 176 | bind_layers(IGMPv3, IGMPv3gr, frag=0, proto=2) |
| 177 | bind_layers(IGMPv3gr, IGMPv3gr, frag=0, proto=2) |
| 178 | |
| 179 | |
| 180 | if __name__ == "__main__": |
| 181 | |
| 182 | print "test float encoding" |
| 183 | from math import log |
| 184 | max_expected_error = 1.0 / (2<<3) # four bit precision |
| 185 | p = IGMPv3() |
| 186 | for v in range(0, 31745): |
| 187 | c = p.encode_float(v) |
| 188 | d = p.decode_float(c) |
| 189 | rel_err = float(v-d)/v if v!=0 else 0.0 |
| 190 | assert rel_err <= max_expected_error |
| 191 | |
| 192 | print "construct membership query - general query" |
| 193 | mq = IGMPv3(type=IGMP_TYPE_MEMBERSHIP_QUERY, max_resp_code=120) |
| 194 | hexdump(str(mq)) |
| 195 | |
| 196 | print "construct membership query - group-specific query" |
| 197 | mq = IGMPv3(type=IGMP_TYPE_MEMBERSHIP_QUERY, max_resp_code=120, gaddr="224.0.0.1") |
| 198 | hexdump(str(mq)) |
| 199 | |
| 200 | print "construct membership query - group-and-source-specific query" |
| 201 | mq = IGMPv3(type=IGMP_TYPE_MEMBERSHIP_QUERY, max_resp_code=120, gaddr="224.0.0.1") |
| 202 | mq.srcs = ['1.2.3.4', '5.6.7.8'] |
| 203 | hexdump(str(mq)) |
| 204 | |
| 205 | print "fixup" |
| 206 | mq = IGMPv3(type=IGMP_TYPE_MEMBERSHIP_QUERY) |
| 207 | mq.srcs = ['1.2.3.4', '5.6.7.8'] |
| 208 | pkt = Ether() / IP() / mq |
| 209 | print "before fixup:" |
| 210 | hexdump(str(pkt)) |
| 211 | |
| 212 | print "after fixup:" |
| 213 | IGMPv3.fixup(pkt) |
| 214 | hexdump(str(pkt)) |
| 215 | |
Zsolt Haraszti | ecf8945 | 2016-03-01 22:38:13 -0800 | [diff] [blame] | 216 | print "construct v3 membership report - join a single group" |
| 217 | mr = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30, gaddr="224.0.0.1") |
| 218 | mr.grps = [IGMPv3gr( rtype=IGMP_V3_GR_TYPE_EXCLUDE, mcaddr="229.10.20.30")] |
| 219 | hexdump(mr) |
| 220 | |
| 221 | print "construct v3 membership report - join two groups" |
| 222 | mr = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30, gaddr="224.0.0.1") |
| 223 | mr.grps = [ |
| 224 | IGMPv3gr(rtype=IGMP_V3_GR_TYPE_EXCLUDE, mcaddr="229.10.20.30"), |
| 225 | IGMPv3gr(rtype=IGMP_V3_GR_TYPE_EXCLUDE, mcaddr="229.10.20.31") |
| 226 | ] |
| 227 | hexdump(mr) |
| 228 | |
| 229 | print "construct v3 membership report - leave a group" |
| 230 | mr = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30, gaddr="224.0.0.1") |
| 231 | mr.grps = [IGMPv3gr(rtype=IGMP_V3_GR_TYPE_INCLUDE, mcaddr="229.10.20.30")] |
| 232 | hexdump(mr) |
| 233 | |
Zsolt Haraszti | fe52550 | 2016-03-02 05:18:52 +0000 | [diff] [blame] | 234 | print "all ok" |