Matt Jeanneret | f1e9c5d | 2019-02-08 07:41:29 -0500 | [diff] [blame^] | 1 | # |
| 2 | # Copyright 2017 the original author or authors. |
| 3 | # |
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | # you may not use this file except in compliance with the License. |
| 6 | # You may obtain a copy of the License at |
| 7 | # |
| 8 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | # |
| 10 | # Unless required by applicable law or agreed to in writing, software |
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | # See the License for the specific language governing permissions and |
| 14 | # limitations under the License. |
| 15 | # |
| 16 | |
| 17 | from scapy.fields import ByteEnumField, FieldLenField, IPField, \ |
| 18 | FieldListField, XShortField, ConditionalField, BitField, ShortField, \ |
| 19 | PacketListField |
| 20 | from scapy.fields import ByteField |
| 21 | from scapy.layers.inet import IP, bind_layers |
| 22 | from scapy.layers.inet import IPOption_Router_Alert |
| 23 | from scapy.layers.l2 import Ether |
| 24 | from scapy.packet import Packet |
| 25 | from scapy.utils import atol, hexdump |
| 26 | from scapy.utils import checksum |
| 27 | |
| 28 | IGMP_TYPE_MEMBERSHIP_QUERY = 0x11 |
| 29 | IGMP_TYPE_V3_MEMBERSHIP_REPORT = 0x22 |
| 30 | IGMP_TYPE_V1_MEMBERSHIP_REPORT = 0x12 |
| 31 | IGMP_TYPE_V2_MEMBERSHIP_REPORT = 0x16 |
| 32 | IGMP_TYPE_V2_LEAVE_GROUP = 0x17 |
| 33 | |
| 34 | IGMP_V3_GR_TYPE_INCLUDE = 0x01 |
| 35 | IGMP_V3_GR_TYPE_EXCLUDE = 0x02 |
| 36 | IGMP_V3_GR_TYPE_CHANGE_TO_INCLUDE = 0x03 |
| 37 | IGMP_V3_GR_TYPE_CHANGE_TO_EXCLUDE = 0x04 |
| 38 | IGMP_V3_GR_TYPE_ALLOW_NEW = 0x05 |
| 39 | IGMP_V3_GR_TYPE_BLOCK_OLD = 0x06 |
| 40 | |
| 41 | |
| 42 | """ |
| 43 | IGMPV3_ALL_ROUTERS = '224.0.0.22' |
| 44 | IGMPv3 = 3 |
| 45 | IP_SRC = '1.2.3.4' |
| 46 | ETHERTYPE_IP = 0x0800 |
| 47 | IGMP_DST_MAC = "01:00:5e:00:01:01" |
| 48 | IGMP_SRC_MAC = "5a:e1:ac:ec:4d:a1" |
| 49 | """ |
| 50 | |
| 51 | |
| 52 | class IGMPv3gr(Packet): |
| 53 | """IGMPv3 Group Record, used in membership report""" |
| 54 | |
| 55 | name = "IGMPv3gr" |
| 56 | |
| 57 | igmp_v3_gr_types = { |
| 58 | IGMP_V3_GR_TYPE_INCLUDE: "Include Mode", |
| 59 | IGMP_V3_GR_TYPE_EXCLUDE: "Exclude Mode", |
| 60 | IGMP_V3_GR_TYPE_CHANGE_TO_INCLUDE: "Change to Include Mode", |
| 61 | IGMP_V3_GR_TYPE_CHANGE_TO_EXCLUDE: "Change to Exclude Mode", |
| 62 | IGMP_V3_GR_TYPE_ALLOW_NEW: "Allow New Sources", |
| 63 | IGMP_V3_GR_TYPE_BLOCK_OLD: "Block Old Sources" |
| 64 | } |
| 65 | |
| 66 | fields_desc = [ |
| 67 | ByteEnumField("rtype", IGMP_V3_GR_TYPE_INCLUDE, igmp_v3_gr_types), |
| 68 | ByteField("aux_data_len", 0), |
| 69 | FieldLenField("numsrc", None, count_of="sources"), |
| 70 | IPField("mcaddr", "0.0.0.0"), |
| 71 | FieldListField("sources", None, IPField("src", "0.0.0.0"), "numsrc") |
| 72 | ] |
| 73 | |
| 74 | def post_build(self, pkt, payload): |
| 75 | pkt += payload |
| 76 | if self.aux_data_len != 0: |
| 77 | print "WARNING: Auxiliary Data Length must be zero (0)" |
| 78 | return pkt |
| 79 | |
| 80 | |
| 81 | class IGMPv3(Packet): |
| 82 | |
| 83 | name = "IGMPv3" |
| 84 | |
| 85 | igmp_v3_types = { |
| 86 | IGMP_TYPE_MEMBERSHIP_QUERY: "Membership Query", |
| 87 | IGMP_TYPE_V3_MEMBERSHIP_REPORT: " Version 3 Mebership Report", |
| 88 | IGMP_TYPE_V2_MEMBERSHIP_REPORT: " Version 2 Mebership Report", |
| 89 | IGMP_TYPE_V1_MEMBERSHIP_REPORT: " Version 1 Mebership Report", |
| 90 | IGMP_TYPE_V2_LEAVE_GROUP: "Version 2 Leave Group" |
| 91 | } |
| 92 | |
| 93 | fields_desc = [ |
| 94 | ByteEnumField("type", IGMP_TYPE_MEMBERSHIP_QUERY, igmp_v3_types), |
| 95 | ByteField("max_resp_code", 0), |
| 96 | XShortField("checksum", None), |
| 97 | #IPField("group_address", "0.0.0.0"), |
| 98 | |
| 99 | # membership query fields |
| 100 | ConditionalField(IPField("gaddr", "0.0.0.0"), |
| 101 | lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
| 102 | ConditionalField(BitField("resv", 0, 4), |
| 103 | lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
| 104 | ConditionalField(BitField("s", 0, 1), |
| 105 | lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
| 106 | ConditionalField(BitField("qrv", 0, 3), |
| 107 | lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
| 108 | ConditionalField(ByteField("qqic", 0), |
| 109 | lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
| 110 | ConditionalField(FieldLenField("numsrc", None, count_of="srcs"), |
| 111 | lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
| 112 | ConditionalField(FieldListField("srcs", None, |
| 113 | IPField("src", "0.0.0.0"), "numsrc"), |
| 114 | lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY), |
| 115 | |
| 116 | # membership report fields |
| 117 | ConditionalField( |
| 118 | ShortField("resv2", 0), |
| 119 | lambda pkt: pkt.type == IGMP_TYPE_V3_MEMBERSHIP_REPORT), |
| 120 | ConditionalField( |
| 121 | FieldLenField("numgrp", None, count_of="grps"), |
| 122 | lambda pkt: pkt.type == IGMP_TYPE_V3_MEMBERSHIP_REPORT), |
| 123 | ConditionalField( |
| 124 | PacketListField("grps", [], IGMPv3gr), |
| 125 | lambda pkt: pkt.type == IGMP_TYPE_V3_MEMBERSHIP_REPORT) |
| 126 | |
| 127 | # TODO: v2 and v3 membership reports? |
| 128 | |
| 129 | ] |
| 130 | |
| 131 | def post_build(self, pkt, payload): |
| 132 | |
| 133 | pkt += payload |
| 134 | |
| 135 | # max_resp_code field is reserved (0) |
| 136 | if self.type in [IGMP_TYPE_V3_MEMBERSHIP_REPORT,]: |
| 137 | mrc = 0 |
| 138 | else: |
| 139 | mrc = self.encode_float(self.max_resp_code) |
| 140 | pkt = pkt[:1] + chr(mrc) + pkt[2:] |
| 141 | |
| 142 | if self.checksum is None: |
| 143 | chksum = checksum(pkt) |
| 144 | pkt = pkt[:2] + chr(chksum >> 8) + chr(chksum & 0xff) + pkt[4:] |
| 145 | |
| 146 | return pkt |
| 147 | |
| 148 | def encode_float(self, value): |
| 149 | """Encode max response time value per RFC 3376.""" |
| 150 | if value < 128: |
| 151 | return value |
| 152 | if value > 31743: |
| 153 | return 255 |
| 154 | exp = 0 |
| 155 | value >>= 3 |
| 156 | while value > 31: |
| 157 | exp += 1 |
| 158 | value >>= 1 |
| 159 | return 0x80 | (exp << 4) | (value & 0xf) |
| 160 | |
| 161 | |
| 162 | def decode_float(self, code): |
| 163 | if code < 128: |
| 164 | return code |
| 165 | mant = code & 0xf |
| 166 | exp = (code >> 4) & 0x7 |
| 167 | return (mant | 0x10) << (exp + 3) |
| 168 | |
| 169 | @staticmethod |
| 170 | def is_valid_mcaddr(ip): |
| 171 | byte1 = atol(ip) >> 24 & 0xff |
| 172 | return (byte1 & 0xf0) == 0xe0 |
| 173 | |
| 174 | @staticmethod |
| 175 | def fixup(pkt): |
| 176 | """Fixes up the underlying IP() and Ether() headers.""" |
| 177 | assert pkt.haslayer(IGMPv3), \ |
| 178 | "This packet is not an IGMPv4 packet; cannot fix it up" |
| 179 | |
| 180 | igmp = pkt.getlayer(IGMPv3) |
| 181 | |
| 182 | if pkt.haslayer(IP): |
| 183 | ip = pkt.getlayer(IP) |
| 184 | ip.ttl = 1 |
| 185 | ip.proto = 2 |
| 186 | ip.tos = 0xc0 |
| 187 | ip.options = [IPOption_Router_Alert()] |
| 188 | |
| 189 | if igmp.type == IGMP_TYPE_MEMBERSHIP_QUERY: |
| 190 | if igmp.gaddr == "0.0.0.0": |
| 191 | ip.dst = "224.0.0.1" |
| 192 | else: |
| 193 | assert IGMPv3.is_valid_mcaddr(igmp.gaddr), \ |
| 194 | "IGMP membership query with invalid mcast address" |
| 195 | ip.dst = igmp.gaddr |
| 196 | |
| 197 | elif igmp.type == IGMP_TYPE_V2_LEAVE_GROUP and \ |
| 198 | IGMPv3.is_valid_mcaddr(igmp.gaddr): |
| 199 | ip.dst = "224.0.0.2" |
| 200 | |
| 201 | elif igmp.type in (IGMP_TYPE_V1_MEMBERSHIP_REPORT, |
| 202 | IGMP_TYPE_V2_MEMBERSHIP_REPORT) and \ |
| 203 | IGMPv3.is_valid_mcaddr(igmp.gaddr): |
| 204 | ip.dst = igmp.gaddr |
| 205 | |
| 206 | # We do not need to fixup the ether layer, it is done by scapy |
| 207 | # |
| 208 | # if pkt.haslayer(Ether): |
| 209 | # eth = pkt.getlayer(Ether) |
| 210 | # ip_long = atol(ip.dst) |
| 211 | # ether.dst = '01:00:5e:%02x:%02x:%02x' % ( |
| 212 | # (ip_long >> 16) & 0x7f, (ip_long >> 8) & 0xff, |
| 213 | # ip_long & 0xff ) |
| 214 | |
| 215 | return pkt |
| 216 | |
| 217 | |
| 218 | bind_layers(IP, IGMPv3, frag=0, proto=2, ttl=1, tos=0xc0) |
| 219 | bind_layers(IGMPv3, IGMPv3gr, frag=0, proto=2) |
| 220 | bind_layers(IGMPv3gr, IGMPv3gr, frag=0, proto=2) |
| 221 | |
| 222 | |
| 223 | if __name__ == "__main__": |
| 224 | |
| 225 | print "test float encoding" |
| 226 | from math import log |
| 227 | max_expected_error = 1.0 / (2<<3) # four bit precision |
| 228 | p = IGMPv3() |
| 229 | for v in range(0, 31745): |
| 230 | c = p.encode_float(v) |
| 231 | d = p.decode_float(c) |
| 232 | rel_err = float(v-d)/v if v!=0 else 0.0 |
| 233 | assert rel_err <= max_expected_error |
| 234 | |
| 235 | print "construct membership query - general query" |
| 236 | mq = IGMPv3(type=IGMP_TYPE_MEMBERSHIP_QUERY, max_resp_code=120) |
| 237 | hexdump(str(mq)) |
| 238 | |
| 239 | print "construct membership query - group-specific query" |
| 240 | mq = IGMPv3(type=IGMP_TYPE_MEMBERSHIP_QUERY, max_resp_code=120, |
| 241 | gaddr="224.0.0.1") |
| 242 | hexdump(str(mq)) |
| 243 | |
| 244 | print "construct membership query - group-and-source-specific query" |
| 245 | mq = IGMPv3(type=IGMP_TYPE_MEMBERSHIP_QUERY, max_resp_code=120, |
| 246 | gaddr="224.0.0.1") |
| 247 | mq.srcs = ['1.2.3.4', '5.6.7.8'] |
| 248 | hexdump(str(mq)) |
| 249 | |
| 250 | print "fixup" |
| 251 | mq = IGMPv3(type=IGMP_TYPE_MEMBERSHIP_QUERY) |
| 252 | mq.srcs = ['1.2.3.4', '5.6.7.8'] |
| 253 | pkt = Ether() / IP() / mq |
| 254 | print "before fixup:" |
| 255 | hexdump(str(pkt)) |
| 256 | |
| 257 | print "after fixup:" |
| 258 | IGMPv3.fixup(pkt) |
| 259 | hexdump(str(pkt)) |
| 260 | |
| 261 | print "construct v3 membership report - join a single group" |
| 262 | mr = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30, |
| 263 | gaddr="224.0.0.1") |
| 264 | mr.grps = [IGMPv3gr( rtype=IGMP_V3_GR_TYPE_EXCLUDE, mcaddr="229.10.20.30")] |
| 265 | hexdump(mr) |
| 266 | |
| 267 | print "construct v3 membership report - join two groups" |
| 268 | mr = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30, |
| 269 | gaddr="224.0.0.1") |
| 270 | mr.grps = [ |
| 271 | IGMPv3gr(rtype=IGMP_V3_GR_TYPE_EXCLUDE, mcaddr="229.10.20.30"), |
| 272 | IGMPv3gr(rtype=IGMP_V3_GR_TYPE_EXCLUDE, mcaddr="229.10.20.31") |
| 273 | ] |
| 274 | hexdump(mr) |
| 275 | |
| 276 | print "construct v3 membership report - leave a group" |
| 277 | mr = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30, |
| 278 | gaddr="224.0.0.1") |
| 279 | mr.grps = [IGMPv3gr(rtype=IGMP_V3_GR_TYPE_INCLUDE, mcaddr="229.10.20.30")] |
| 280 | hexdump(mr) |
| 281 | |
| 282 | print "all ok" |