adding igmp capability to tester container

Change-Id: Ib1632da1ad8864afdc3e148e840929690365c66b
diff --git a/docker/Dockerfile.tester b/docker/Dockerfile.tester
index a7778df..617ef13 100644
--- a/docker/Dockerfile.tester
+++ b/docker/Dockerfile.tester
@@ -6,4 +6,9 @@
 
 RUN apt update && apt install -y iperf isc-dhcp-client tcpdump net-tools iproute2 iputils-ping wpasupplicant
 
+COPY config/igmp.py /usr/local/bin
+COPY config/igmpv3.py /usr/local/bin
+
+RUN chmod 755 /usr/local/bin/igmp.py
+
 COPY config/wpa_supplicant.conf /etc/wpa_supplicant/
diff --git a/docker/config/igmp.py b/docker/config/igmp.py
new file mode 100644
index 0000000..9c18f84
--- /dev/null
+++ b/docker/config/igmp.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+#
+# 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 igmpv3 import IGMPv3, IGMP_TYPE_V3_MEMBERSHIP_REPORT, IGMP_V3_GR_TYPE_EXCLUDE, IGMPv3gr, IGMP_V3_GR_TYPE_INCLUDE
+from scapy.data import ETH_P_IP
+from scapy.layers.inet import IP
+from scapy.layers.l2 import Ether
+from scapy.sendrecv import sendp
+import argparse
+
+"""
+Send an igmp join
+"""
+
+IGMP_ETH = Ether(type=ETH_P_IP)
+IGMP_IP = IP(dst='224.0.0.22')
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+
+    _help = ('Emit a join message')
+    parser.add_argument('-j', '--join',
+                        dest='join',
+                        action='store_true',
+                        default=False,
+                        help=_help)
+
+    _help = ('Emit a leave message')
+    parser.add_argument('-l', '--leave',
+                        dest='join',
+                        action='store_false',
+                        default=False,
+                        help=_help)
+
+    _help = ('Group address to use')
+    parser.add_argument('-m', '--mcaddr',
+                        dest='mcaddr',
+                        action='store',
+                        default='229.10.20.30',
+                        help = _help)
+
+    _help = ('Interface to use')
+    parser.add_argument('-i', '--iface',
+                        dest='iface',
+                        action='store',
+                        default='eth0',
+                        help = _help)
+
+    return parser.parse_args()
+
+
+def send(igmp):
+    ip_pkt = IGMP_ETH/IGMP_IP
+    pkt = ip_pkt/igmp
+    IGMPv3.fixup(pkt)
+
+    sendp(pkt, iface=args.iface)
+
+def send_join(args):
+    igmp = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30, gaddr="224.0.0.22")
+    igmp.grps = [IGMPv3gr( rtype=IGMP_V3_GR_TYPE_EXCLUDE, mcaddr=args.mcaddr)]
+
+    send(igmp)
+
+
+def send_leave(args):
+
+    igmp = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30, gaddr="224.0.0.22c")
+    igmp.grps = [IGMPv3gr(rtype=IGMP_V3_GR_TYPE_INCLUDE, mcaddr=args.mcaddr)]
+
+    send(igmp)
+
+
+if __name__ == '__main__':
+    args = parse_args()
+
+    if args.join:
+        send_join(args)
+    else:
+        send_leave(args)
diff --git a/docker/config/igmpv3.py b/docker/config/igmpv3.py
new file mode 100644
index 0000000..7e348bc
--- /dev/null
+++ b/docker/config/igmpv3.py
@@ -0,0 +1,253 @@
+# Copyright 2016-present Ciena Corporation
+#
+# 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 socket import *
+from struct import *
+from scapy.all import *
+from itertools import *
+
+IGMP_TYPE_MEMBERSHIP_QUERY     = 0x11
+IGMP_TYPE_V3_MEMBERSHIP_REPORT = 0x22
+IGMP_TYPE_V3_MEMBERSHIP_REPORT_NEGATIVE = 0xdd
+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_INCLUDE_NEGATIVE  = 0xaa
+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_INCLUDE_NEGATIVE: "Include Mode in negative scenario",
+        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
+
+        if self.type in [IGMP_TYPE_V3_MEMBERSHIP_REPORT,]: # max_resp_code field is reserved (0)
+            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, invalid_ttl = None):
+        """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)
+            if invalid_ttl is None:
+               ip.ttl = 1
+            else:
+               ip.ttl = 20
+            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,'no')
+    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"