blob: 00fab236828f36adb2ee0e4abf08043e6038e77d [file] [log] [blame]
#
# Copyright 2017 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from scapy.fields import ByteEnumField, FieldLenField, IPField, \
FieldListField, XShortField, ConditionalField, BitField, ShortField, \
PacketListField
from scapy.fields import ByteField
from scapy.layers.inet import IP, bind_layers
from scapy.layers.inet import IPOption_Router_Alert
from scapy.layers.l2 import Ether
from scapy.packet import Packet
from scapy.utils import atol, hexdump
from scapy.utils import checksum
IGMP_TYPE_MEMBERSHIP_QUERY = 0x11
IGMP_TYPE_V3_MEMBERSHIP_REPORT = 0x22
IGMP_TYPE_V1_MEMBERSHIP_REPORT = 0x12
IGMP_TYPE_V2_MEMBERSHIP_REPORT = 0x16
IGMP_TYPE_V2_LEAVE_GROUP = 0x17
IGMP_V3_GR_TYPE_INCLUDE = 0x01
IGMP_V3_GR_TYPE_EXCLUDE = 0x02
IGMP_V3_GR_TYPE_CHANGE_TO_INCLUDE = 0x03
IGMP_V3_GR_TYPE_CHANGE_TO_EXCLUDE = 0x04
IGMP_V3_GR_TYPE_ALLOW_NEW = 0x05
IGMP_V3_GR_TYPE_BLOCK_OLD = 0x06
"""
IGMPV3_ALL_ROUTERS = '224.0.0.22'
IGMPv3 = 3
IP_SRC = '1.2.3.4'
ETHERTYPE_IP = 0x0800
IGMP_DST_MAC = "01:00:5e:00:01:01"
IGMP_SRC_MAC = "5a:e1:ac:ec:4d:a1"
"""
class IGMPv3gr(Packet):
"""IGMPv3 Group Record, used in membership report"""
name = "IGMPv3gr"
igmp_v3_gr_types = {
IGMP_V3_GR_TYPE_INCLUDE: "Include Mode",
IGMP_V3_GR_TYPE_EXCLUDE: "Exclude Mode",
IGMP_V3_GR_TYPE_CHANGE_TO_INCLUDE: "Change to Include Mode",
IGMP_V3_GR_TYPE_CHANGE_TO_EXCLUDE: "Change to Exclude Mode",
IGMP_V3_GR_TYPE_ALLOW_NEW: "Allow New Sources",
IGMP_V3_GR_TYPE_BLOCK_OLD: "Block Old Sources"
}
fields_desc = [
ByteEnumField("rtype", IGMP_V3_GR_TYPE_INCLUDE, igmp_v3_gr_types),
ByteField("aux_data_len", 0),
FieldLenField("numsrc", None, count_of="sources"),
IPField("mcaddr", "0.0.0.0"),
FieldListField("sources", None, IPField("src", "0.0.0.0"), "numsrc")
]
def post_build(self, pkt, payload):
pkt += payload
if self.aux_data_len != 0:
print "WARNING: Auxiliary Data Length must be zero (0)"
return pkt
class IGMPv3(Packet):
name = "IGMPv3"
igmp_v3_types = {
IGMP_TYPE_MEMBERSHIP_QUERY: "Membership Query",
IGMP_TYPE_V3_MEMBERSHIP_REPORT: " Version 3 Mebership Report",
IGMP_TYPE_V2_MEMBERSHIP_REPORT: " Version 2 Mebership Report",
IGMP_TYPE_V1_MEMBERSHIP_REPORT: " Version 1 Mebership Report",
IGMP_TYPE_V2_LEAVE_GROUP: "Version 2 Leave Group"
}
fields_desc = [
ByteEnumField("type", IGMP_TYPE_MEMBERSHIP_QUERY, igmp_v3_types),
ByteField("max_resp_code", 0),
XShortField("checksum", None),
#IPField("group_address", "0.0.0.0"),
# membership query fields
ConditionalField(IPField("gaddr", "0.0.0.0"),
lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY),
ConditionalField(BitField("resv", 0, 4),
lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY),
ConditionalField(BitField("s", 0, 1),
lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY),
ConditionalField(BitField("qrv", 0, 3),
lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY),
ConditionalField(ByteField("qqic", 0),
lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY),
ConditionalField(FieldLenField("numsrc", None, count_of="srcs"),
lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY),
ConditionalField(FieldListField("srcs", None,
IPField("src", "0.0.0.0"), "numsrc"),
lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY),
# membership report fields
ConditionalField(
ShortField("resv2", 0),
lambda pkt: pkt.type == IGMP_TYPE_V3_MEMBERSHIP_REPORT),
ConditionalField(
FieldLenField("numgrp", None, count_of="grps"),
lambda pkt: pkt.type == IGMP_TYPE_V3_MEMBERSHIP_REPORT),
ConditionalField(
PacketListField("grps", [], IGMPv3gr),
lambda pkt: pkt.type == IGMP_TYPE_V3_MEMBERSHIP_REPORT)
# TODO: v2 and v3 membership reports?
]
def post_build(self, pkt, payload):
pkt += payload
# max_resp_code field is reserved (0)
if self.type in [IGMP_TYPE_V3_MEMBERSHIP_REPORT,]:
mrc = 0
else:
mrc = self.encode_float(self.max_resp_code)
pkt = pkt[:1] + chr(mrc) + pkt[2:]
if self.checksum is None:
chksum = checksum(pkt)
pkt = pkt[:2] + chr(chksum >> 8) + chr(chksum & 0xff) + pkt[4:]
return pkt
def encode_float(self, value):
"""Encode max response time value per RFC 3376."""
if value < 128:
return value
if value > 31743:
return 255
exp = 0
value >>= 3
while value > 31:
exp += 1
value >>= 1
return 0x80 | (exp << 4) | (value & 0xf)
def decode_float(self, code):
if code < 128:
return code
mant = code & 0xf
exp = (code >> 4) & 0x7
return (mant | 0x10) << (exp + 3)
@staticmethod
def is_valid_mcaddr(ip):
byte1 = atol(ip) >> 24 & 0xff
return (byte1 & 0xf0) == 0xe0
@staticmethod
def fixup(pkt):
"""Fixes up the underlying IP() and Ether() headers."""
assert pkt.haslayer(IGMPv3), \
"This packet is not an IGMPv4 packet; cannot fix it up"
igmp = pkt.getlayer(IGMPv3)
if pkt.haslayer(IP):
ip = pkt.getlayer(IP)
ip.ttl = 1
ip.proto = 2
ip.tos = 0xc0
ip.options = [IPOption_Router_Alert()]
if igmp.type == IGMP_TYPE_MEMBERSHIP_QUERY:
if igmp.gaddr == "0.0.0.0":
ip.dst = "224.0.0.1"
else:
assert IGMPv3.is_valid_mcaddr(igmp.gaddr), \
"IGMP membership query with invalid mcast address"
ip.dst = igmp.gaddr
elif igmp.type == IGMP_TYPE_V2_LEAVE_GROUP and \
IGMPv3.is_valid_mcaddr(igmp.gaddr):
ip.dst = "224.0.0.2"
elif igmp.type in (IGMP_TYPE_V1_MEMBERSHIP_REPORT,
IGMP_TYPE_V2_MEMBERSHIP_REPORT) and \
IGMPv3.is_valid_mcaddr(igmp.gaddr):
ip.dst = igmp.gaddr
# We do not need to fixup the ether layer, it is done by scapy
#
# if pkt.haslayer(Ether):
# eth = pkt.getlayer(Ether)
# ip_long = atol(ip.dst)
# ether.dst = '01:00:5e:%02x:%02x:%02x' % (
# (ip_long >> 16) & 0x7f, (ip_long >> 8) & 0xff,
# ip_long & 0xff )
return pkt
bind_layers(IP, IGMPv3, frag=0, proto=2, ttl=1, tos=0xc0)
bind_layers(IGMPv3, IGMPv3gr, frag=0, proto=2)
bind_layers(IGMPv3gr, IGMPv3gr, frag=0, proto=2)
if __name__ == "__main__":
print "test float encoding"
from math import log
max_expected_error = 1.0 / (2<<3) # four bit precision
p = IGMPv3()
for v in range(0, 31745):
c = p.encode_float(v)
d = p.decode_float(c)
rel_err = float(v-d)/v if v!=0 else 0.0
assert rel_err <= max_expected_error
print "construct membership query - general query"
mq = IGMPv3(type=IGMP_TYPE_MEMBERSHIP_QUERY, max_resp_code=120)
hexdump(str(mq))
print "construct membership query - group-specific query"
mq = IGMPv3(type=IGMP_TYPE_MEMBERSHIP_QUERY, max_resp_code=120,
gaddr="224.0.0.1")
hexdump(str(mq))
print "construct membership query - group-and-source-specific query"
mq = IGMPv3(type=IGMP_TYPE_MEMBERSHIP_QUERY, max_resp_code=120,
gaddr="224.0.0.1")
mq.srcs = ['1.2.3.4', '5.6.7.8']
hexdump(str(mq))
print "fixup"
mq = IGMPv3(type=IGMP_TYPE_MEMBERSHIP_QUERY)
mq.srcs = ['1.2.3.4', '5.6.7.8']
pkt = Ether() / IP() / mq
print "before fixup:"
hexdump(str(pkt))
print "after fixup:"
IGMPv3.fixup(pkt)
hexdump(str(pkt))
print "construct v3 membership report - join a single group"
mr = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30,
gaddr="224.0.0.1")
mr.grps = [IGMPv3gr( rtype=IGMP_V3_GR_TYPE_EXCLUDE, mcaddr="229.10.20.30")]
hexdump(mr)
print "construct v3 membership report - join two groups"
mr = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30,
gaddr="224.0.0.1")
mr.grps = [
IGMPv3gr(rtype=IGMP_V3_GR_TYPE_EXCLUDE, mcaddr="229.10.20.30"),
IGMPv3gr(rtype=IGMP_V3_GR_TYPE_EXCLUDE, mcaddr="229.10.20.31")
]
hexdump(mr)
print "construct v3 membership report - leave a group"
mr = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30,
gaddr="224.0.0.1")
mr.grps = [IGMPv3gr(rtype=IGMP_V3_GR_TYPE_INCLUDE, mcaddr="229.10.20.30")]
hexdump(mr)
print "all ok"