Serialize / deserialize functions for IGMP, IGMPv3 Membership
Query and IGMPv3 Membership Report.  IGMP has been added to
the IPv4 deserialization map.

Change-Id: I6d46c3771b6589f1cbd839c58521ffab94b5e230
diff --git a/utils/misc/src/main/java/org/onlab/packet/IGMP.java b/utils/misc/src/main/java/org/onlab/packet/IGMP.java
new file mode 100644
index 0000000..9d5535d
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/IGMP.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onlab.packet;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+
+import static org.slf4j.LoggerFactory.getLogger;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.packet.PacketUtils.checkInput;
+
+
+/**
+ * Implements IGMP control packet format.
+ */
+public class IGMP extends BasePacket {
+    private final Logger log = getLogger(getClass());
+
+    public static final byte TYPE_IGMPV3_MEMBERSHIP_QUERY = 0x11;
+    public static final byte TYPE_IGMPV1_MEMBERSHIP_REPORT = 0x12;
+    public static final byte TYPE_IGMPV2_MEMBERSHIP_REPORT = 0x16;
+    public static final byte TYPE_IGMPV2_LEAVE_GROUP = 0x17;
+    public static final byte TYPE_IGMPV3_MEMBERSHIP_REPORT = 0x22;
+    public static final Map<Byte, Deserializer<? extends IPacket>> PROTOCOL_DESERIALIZER_MAP = new HashMap<>();
+
+    public static final int MINIMUM_HEADER_LEN = 12;
+
+    List<IGMPGroup> groups = new ArrayList<>();
+
+    // Fields contained in the IGMP header
+    private byte igmpType;
+    private byte resField = 0;
+    private short checksum = 0;
+
+    private byte[] unsupportTypeData;
+
+    public IGMP() {
+    }
+
+    /**
+     * Get the IGMP message type.
+     *
+     * @return the IGMP message type
+     */
+    public byte getIgmpType() {
+        return igmpType;
+    }
+
+    /**
+     * Set the IGMP message type.
+     *
+     * @param msgType IGMP message type
+     */
+    public void setIgmpType(byte msgType) {
+        igmpType = msgType;
+    }
+
+    /**
+     * Get the checksum of this message.
+     *
+     * @return the checksum
+     */
+    public short getChecksum() {
+        return checksum;
+    }
+
+    /**
+     * get the Max Resp Code.
+     *
+     * @return The Maximum Time allowed before before sending a responding report.
+     */
+    public byte getMaxRespField() {
+        return resField;
+    }
+
+    /**
+     * Set the Max Resp Code.
+     *
+     * @param respCode the Maximum Response Code.
+     */
+    public void setMaxRespCode(byte respCode) {
+        if (igmpType != IGMP.TYPE_IGMPV3_MEMBERSHIP_QUERY) {
+            log.debug("Requesting the max response code for an incorrect field: ");
+        }
+        this.resField = respCode;
+    }
+
+    /**
+     * Get the list of IGMPGroups.  The group objects will be either IGMPQuery or IGMPMembership
+     * depending on the IGMP message type.  For IGMP Query, the groups list should only be
+     * one group.
+     *
+     * @return The list of IGMP groups.
+     */
+    public List<IGMPGroup> getGroups() {
+        return groups;
+    }
+
+    /**
+     * Add a multicast group to this IGMP message.
+     *
+     * @param group the IGMPGroup will be IGMPQuery or IGMPMembership depending on the message type.
+     * @return true if group was valid and added, false otherwise.
+     */
+    public boolean addGroup(IGMPGroup group) {
+        checkNotNull(group);
+        switch (this.igmpType) {
+            case TYPE_IGMPV3_MEMBERSHIP_QUERY:
+                if (group instanceof IGMPMembership) {
+                    return false;
+                }
+
+                if (group.sources.size() > 1) {
+                    return false;
+                }
+                break;
+
+            case TYPE_IGMPV3_MEMBERSHIP_REPORT:
+                if (group instanceof IGMPMembership) {
+                    return false;
+                }
+                break;
+
+            default:
+                log.debug("Warning no IGMP message type has been set");
+        }
+
+        this.groups.add(group);
+        return true;
+    }
+
+    /**
+     * Serialize this IGMP packet.  This will take care
+     * of serializing IGMPv3 Queries and IGMPv3 Membership
+     * Reports.
+     *
+     * @return the serialized IGMP message
+     */
+    @Override
+    public byte[] serialize() {
+        byte [] data = new byte[8915];
+
+        ByteBuffer bb = ByteBuffer.wrap(data);
+        bb.put(this.getIgmpType());
+
+        // reserved or max resp code depending on type.
+        bb.put(this.resField);
+
+        // Must calculate checksum
+        bb.putShort((short) 0);
+
+        switch (this.igmpType) {
+
+            case IGMP.TYPE_IGMPV3_MEMBERSHIP_REPORT:
+                // reserved
+                bb.putShort((short) 0);
+                // Number of groups
+                bb.putShort((short) groups.size());
+                // Fall through
+
+            case IGMP.TYPE_IGMPV3_MEMBERSHIP_QUERY:
+
+                for (IGMPGroup grp : groups) {
+                    grp.serialize(bb);
+                }
+                break;
+
+            default:
+                bb.put(this.unsupportTypeData);
+                break;
+        }
+
+        int size = bb.position();
+        bb.position(0);
+        byte [] rdata = new byte[size];
+        bb.get(rdata, 0, size);
+        return rdata;
+    }
+
+    /**
+     * Deserialize an IGMP message.
+     *
+     * @param data bytes to deserialize
+     * @param offset offset to start deserializing from
+     * @param length length of the data to deserialize
+     * @return populated IGMP object
+     */
+    @Override
+    public IPacket deserialize(final byte[] data, final int offset,
+                               final int length) {
+
+        IGMP igmp = new IGMP();
+        try {
+            igmp = IGMP.deserializer().deserialize(data, offset, length);
+        } catch (DeserializationException e) {
+            log.error(e.getStackTrace().toString());
+            return this;
+        }
+        this.igmpType = igmp.igmpType;
+        this.resField = igmp.resField;
+        this.checksum = igmp.checksum;
+        this.groups = igmp.groups;
+        return this;
+    }
+
+    /**
+     * Deserializer function for IPv4 packets.
+     *
+     * @return deserializer function
+     */
+    public static Deserializer<IGMP> deserializer() {
+        return (data, offset, length) -> {
+            checkInput(data, offset, length, MINIMUM_HEADER_LEN);
+
+            IGMP igmp = new IGMP();
+
+            ByteBuffer bb = ByteBuffer.wrap(data);
+            igmp.igmpType = bb.get();
+            igmp.resField = bb.get();
+            igmp.checksum = bb.getShort();
+            int len = MINIMUM_HEADER_LEN;
+            String msg;
+
+            switch (igmp.igmpType) {
+
+                case TYPE_IGMPV3_MEMBERSHIP_QUERY:
+                    IGMPQuery qgroup = new IGMPQuery();
+                    qgroup.deserialize(bb);
+                    igmp.groups.add(qgroup);
+                    break;
+
+                case TYPE_IGMPV3_MEMBERSHIP_REPORT:
+                    bb.getShort();  // Ignore resvd
+                    int ngrps = bb.getShort();
+
+                    for (; ngrps > 0; ngrps--) {
+                        IGMPMembership mgroup = new IGMPMembership();
+                        mgroup.deserialize(bb);
+                        igmp.groups.add(mgroup);
+                    }
+                    break;
+
+                /*
+                 * NOTE: according to the IGMPv3 spec. These previous IGMP type fields
+                 * must be supported.  At this time we are going to <b>assume</b> we run
+                 * in a modern network where all devices are IGMPv3 capable.
+                 */
+                case TYPE_IGMPV1_MEMBERSHIP_REPORT:
+                case TYPE_IGMPV2_MEMBERSHIP_REPORT:
+                case TYPE_IGMPV2_LEAVE_GROUP:
+                    igmp.unsupportTypeData = bb.array();  // Is this the entire array?
+                    msg = "IGMP message type: " + igmp.igmpType + " is not supported";
+                    igmp.log.debug(msg);
+                    break;
+
+                default:
+                    msg = "IGMP message type: " + igmp.igmpType + " is not recodnized";
+                    igmp.log.debug(msg);
+                    break;
+            }
+            return igmp;
+        };
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        if (!(obj instanceof IGMP)) {
+            return false;
+        }
+        final IGMP other = (IGMP) obj;
+        if (this.igmpType != other.igmpType) {
+            return false;
+        }
+        if (this.resField != other.resField) {
+            return false;
+        }
+        if (this.checksum != other.checksum) {
+            return false;
+        }
+        if (this.groups.size() != other.groups.size()) {
+            return false;
+        }
+        // TODO: equals should be true regardless of order.
+        if (!groups.equals(other.groups)) {
+            return false;
+        }
+        return true;
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        final int prime = 2521;
+        int result = super.hashCode();
+        result = prime * result + this.igmpType;
+        result = prime * result + this.groups.size();
+        result = prime * result + this.resField;
+        result = prime * result + this.checksum;
+        result = prime * result + this.groups.hashCode();
+        return result;
+    }
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/IGMPGroup.java b/utils/misc/src/main/java/org/onlab/packet/IGMPGroup.java
new file mode 100644
index 0000000..70ff556
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/IGMPGroup.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onlab.packet;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class to represent Groups for membership query and reports.
+ */
+public abstract class IGMPGroup {
+
+    protected int auxInfo;
+    protected IpAddress gaddr;
+    protected List<IpAddress> sources = new ArrayList<>();
+
+    public IGMPGroup() {
+    }
+
+    /**
+     * Initialize this object with a multicast group address and additional info.
+     *
+     * @param gaddr: the multicast group address for this message type.
+     * @param auxInfo: additional info potentially used by IGMPQuery
+     */
+    public IGMPGroup(IpAddress gaddr, int auxInfo) {
+        this.gaddr = gaddr;
+        this.auxInfo = auxInfo;
+    }
+
+    /**
+     * Get the multicast group address.
+     *
+     * @return the group address
+     */
+    public IpAddress getGaddr() {
+        return this.gaddr;
+    }
+
+    /**
+     * Get the auxillary info.
+     *
+     * @return the auxillary info
+     */
+    public int getAuxInfo() {
+        return this.auxInfo;
+    }
+
+    /**
+     * Add a unicast source address to this message.
+     *
+     * @param saddr IPv4 unicast source address
+     */
+    public void addSource(IpAddress saddr) {
+        sources.add(saddr);
+    }
+
+    /**
+     * Return the list of source addresses.
+     *
+     * @return list of source addresses
+     */
+    public List<IpAddress> getSources() {
+        return sources;
+    }
+
+    /**
+     * Deserialize an IGMPQuery or IGMPMembership message.
+     *
+     * @param bb the ByteBuffer wrapping the serialized message.  The position of the
+     *           ByteBuffer should be pointing at the head of either message type.
+     * @return An object populated with the respective IGMPGroup subclass
+     * @throws DeserializationException in case deserialization goes wrong
+     */
+    public abstract IGMPGroup deserialize(ByteBuffer bb) throws DeserializationException;
+
+    /**
+     * Serialize the IGMPGroup subclass.
+     *
+     * @param bb the ByteBuffer to write into, positioned at the next spot to be written to.
+     * @return The serialized message
+     */
+    public abstract byte[] serialize(ByteBuffer bb);
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/IGMPMembership.java b/utils/misc/src/main/java/org/onlab/packet/IGMPMembership.java
new file mode 100644
index 0000000..174bd6f
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/IGMPMembership.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onlab.packet;
+
+import java.nio.ByteBuffer;
+import static org.onlab.packet.PacketUtils.checkBufferLength;
+
+public class IGMPMembership extends IGMPGroup {
+
+    public static final byte MODE_IS_INCLUDE = 0x1;
+    public static final byte MODE_IS_EXCLUDE = 0x2;
+    public static final byte CHANGE_TO_INCLUDE_MODE = 0x3;
+    public static final byte CHANGE_TO_EXCLUDE_MODE = 0x4;
+    public static final byte ALLOW_NEW_SOURCES = 0x5;
+    public static final byte BLOCK_OLD_SOURCES = 0x6;
+
+    private final int minGroupRecordLen = Ip4Address.BYTE_LENGTH + 4;
+
+    protected byte recordType;
+    protected byte auxDataLength = 0;
+    protected byte[] auxData;
+
+    /**
+     * Constructor initialized with a multicast group address.
+     *
+     * @param gaddr A multicast group address.
+     */
+    public IGMPMembership(Ip4Address gaddr) {
+        super(gaddr, 0);
+    }
+
+    /**
+     * Default constructor.
+     */
+    public IGMPMembership() {
+        super();
+    }
+
+    /**
+     * Serialize this Membership Report.
+     *
+     * @param bb the ByteBuffer to write into, positioned at the next spot to be written to.
+     * @return serialized IGMP message.
+     */
+    @Override
+    public byte[] serialize(ByteBuffer bb) {
+
+        bb.put(recordType);
+        bb.put(auxDataLength);      // reserved
+        bb.putShort((short) sources.size());
+        bb.put(gaddr.toOctets());
+        for (IpAddress ipaddr : sources) {
+            bb.put(ipaddr.toOctets());
+        }
+
+        if (auxDataLength > 0) {
+            bb.put(auxData);
+        }
+
+        return bb.array();
+    }
+
+    /**
+     * Deserialize the IGMP Membership report packet.
+     *
+     * @param bb the ByteBuffer wrapping the serialized message.  The position of the
+     *           ByteBuffer should be pointing at the head of either message type.
+     * @return
+     * @throws DeserializationException
+     */
+    public IGMPGroup deserialize(ByteBuffer bb) throws DeserializationException {
+
+        // Make sure there is enough buffer to read the header,
+        // including the number of sources
+        checkBufferLength(bb.remaining(), 0, minGroupRecordLen);
+        recordType = bb.get();
+        auxDataLength = bb.get();
+        int nsrcs = bb.getShort();
+
+        gaddr = Ip4Address.valueOf(bb.getInt());
+
+        // Make sure we have enough buffer to hold all of these sources
+        checkBufferLength(bb.remaining(), 0, Ip4Address.BYTE_LENGTH * nsrcs);
+        for (; nsrcs > 0; nsrcs--) {
+            Ip4Address src = Ip4Address.valueOf(bb.getInt());
+            this.sources.add(src);
+        }
+
+        if (auxDataLength > 0) {
+            auxData = new byte[auxDataLength];
+            bb.get(auxData, 0, auxDataLength);
+        }
+        return this;
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.lang.Object#equals()
+     */
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof IGMPMembership)) {
+            return false;
+        }
+        IGMPMembership other = (IGMPMembership) obj;
+
+        if (!this.gaddr.equals(other.gaddr)) {
+            return false;
+        }
+        if (this.recordType != other.recordType) {
+            return false;
+        }
+        if (this.auxDataLength != other.auxDataLength) {
+            return false;
+        }
+        if (this.sources.size() != other.sources.size()) {
+            return false;
+        }
+        // TODO: make these tolerant of order
+        if (!this.sources.equals(other.sources)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        final int prime = 2521;
+        int result = super.hashCode();
+        result = prime * result + this.gaddr.hashCode();
+        result = prime * result + this.recordType;
+        result = prime * result + this.auxDataLength;
+        result = prime * result + this.sources.hashCode();
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/utils/misc/src/main/java/org/onlab/packet/IGMPQuery.java b/utils/misc/src/main/java/org/onlab/packet/IGMPQuery.java
new file mode 100644
index 0000000..378ca10
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/IGMPQuery.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onlab.packet;
+
+import java.nio.ByteBuffer;
+
+public class IGMPQuery extends IGMPGroup {
+
+    // Bits and bytes after the group address
+    private byte resv = 0;
+    private boolean sbit = false;
+    private byte qrv = 2;
+    private byte qqic = 0x7d;
+
+    /**
+     * Create IGMP Query message.
+     *
+     * @param gaddr initiaze with a group address.
+     * @param auxInfo auxillary info.
+     */
+    public IGMPQuery(IpAddress gaddr, int auxInfo) {
+        super(gaddr, auxInfo);
+    }
+
+    /**
+     * Create IGMP Query message.
+     */
+    public IGMPQuery() {
+        super();
+    }
+
+    /**
+     * Is the S flag set?  Telling adjacent routers to suppress normal timer updates.
+     *
+     * @return true if the flag is set, false otherwise
+     */
+    public boolean isSbit() {
+        return sbit;
+    }
+
+    /**
+     * Set the S flag.  Default is false.
+     *
+     * @param sbit true or false
+     */
+    public void setSbit(boolean sbit) {
+        this.sbit = sbit;
+    }
+
+    /**
+     * Get the Querier Robustness Variable.
+     *
+     * @return
+     */
+    public byte getQrv() {
+        return qrv;
+    }
+
+    /**
+     * Set the Querier Robustness Variable. Default is 2.
+     *
+     * @param qrv
+     */
+    public void setQrv(byte qrv) {
+        this.qrv = qrv;
+    }
+
+    /**
+     * Get the reserved field.  Should be zero, but ignored regardless of it's value.
+     *
+     * @return the reserved field.
+     */
+    public byte getResv() {
+        return resv;
+    }
+
+    /**
+     * Set the reserved field.  Should be 0 and ignored by receivers.
+     *
+     * @param resv the reserved field.
+     */
+    public void setResv(byte resv) {
+        this.resv = resv;
+    }
+
+    /**
+     * Serialize this IGMPQuery.
+     *
+     * @param bb the ByteBuffer to write into, positioned at the next spot to be written to.
+     * @return the serialized message
+     */
+    @Override
+    public byte[] serialize(ByteBuffer bb) {
+
+        bb.put(gaddr.toOctets());
+        byte fld = (byte) (0x7 & qrv);
+        bb.put(fld);
+        bb.put(qqic);
+        bb.putShort((short) sources.size());
+        for (IpAddress ipaddr : sources) {
+            bb.put(ipaddr.toOctets());
+        }
+        return bb.array();
+    }
+
+    /**
+     * Deserialize the IGMP Query group structure.
+     *
+     * @param bb ByteBuffer pointing at the IGMP Query group address
+     * @return the IGMP Group object
+     */
+    public IGMPGroup deserialize(ByteBuffer bb) throws DeserializationException {
+
+        gaddr = Ip4Address.valueOf(bb.getInt());
+        byte fld = bb.get();
+
+        // Just ignore the reserved bits
+        resv = 0;
+        this.sbit = ((fld & 0x8) == 0x8);
+        qrv = (byte) (fld & 0x7);
+
+        // QQIC field
+        qqic = bb.get();
+
+        // Get the number of sources.
+        short nsrcs = bb.getShort();
+
+        // Do a sanity check on the amount of space we have in our buffer.
+        int lengthNeeded = (Ip4Address.BYTE_LENGTH * nsrcs);
+        PacketUtils.checkHeaderLength(bb.remaining(), lengthNeeded);
+
+        for (; nsrcs > 0; nsrcs--) {
+            Ip4Address ipaddr = Ip4Address.valueOf(bb.getInt());
+            this.sources.add(ipaddr);
+        }
+        return this;
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.lang.Object#equals()
+     */
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof IGMPQuery)) {
+            return false;
+        }
+        IGMPQuery other = (IGMPQuery) obj;
+
+        if (this.sbit != other.sbit) {
+            return false;
+        }
+        if (this.qrv != other.qrv) {
+            return false;
+        }
+        if (this.qqic != other.qqic) {
+            return false;
+        }
+        if (this.sources.size() != other.sources.size()) {
+            return false;
+        }
+
+        // TODO: make these tolerant of order
+        if (!this.sources.equals(other.sources)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        final int prime = 2521;
+        int result = super.hashCode();
+        result = prime * result + this.gaddr.hashCode();
+        result = prime * result + this.qqic;
+        result = prime * result + this.qrv;
+        result = prime * result + this.sources.hashCode();
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/utils/misc/src/main/java/org/onlab/packet/IPv4.java b/utils/misc/src/main/java/org/onlab/packet/IPv4.java
index 0d75245..d75b50a 100644
--- a/utils/misc/src/main/java/org/onlab/packet/IPv4.java
+++ b/utils/misc/src/main/java/org/onlab/packet/IPv4.java
@@ -32,6 +32,7 @@
  */
 public class IPv4 extends BasePacket {
     public static final byte PROTOCOL_ICMP = 0x1;
+    public static final byte PROTOCOL_IGMP = 0x2;
     public static final byte PROTOCOL_TCP = 0x6;
     public static final byte PROTOCOL_UDP = 0x11;
     public static final Map<Byte, Deserializer<? extends IPacket>> PROTOCOL_DESERIALIZER_MAP =
@@ -39,6 +40,7 @@
 
     static {
         IPv4.PROTOCOL_DESERIALIZER_MAP.put(IPv4.PROTOCOL_ICMP, ICMP.deserializer());
+        IPv4.PROTOCOL_DESERIALIZER_MAP.put(IPv4.PROTOCOL_IGMP, IGMP.deserializer());
         IPv4.PROTOCOL_DESERIALIZER_MAP.put(IPv4.PROTOCOL_TCP, TCP.deserializer());
         IPv4.PROTOCOL_DESERIALIZER_MAP.put(IPv4.PROTOCOL_UDP, UDP.deserializer());
     }
diff --git a/utils/misc/src/test/java/org/onlab/packet/IGMPTest.java b/utils/misc/src/test/java/org/onlab/packet/IGMPTest.java
new file mode 100644
index 0000000..a25f721
--- /dev/null
+++ b/utils/misc/src/test/java/org/onlab/packet/IGMPTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onlab.packet;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static junit.framework.Assert.assertTrue;
+
+/**
+ * Unit tests for IGMP class.
+ */
+public class IGMPTest {
+    private Deserializer<IGMP> deserializer;
+
+    private IGMP igmpQuery;
+    private IGMP igmpMembership;
+
+    private Ip4Address gaddr1;
+    private Ip4Address gaddr2;
+    private Ip4Address saddr1;
+    private Ip4Address saddr2;
+
+    @Before
+    public void setUp() throws Exception {
+        gaddr1 = Ip4Address.valueOf(0xe1010101);
+        gaddr2 = Ip4Address.valueOf(0xe2020202);
+        saddr1 = Ip4Address.valueOf(0x0a010101);
+        saddr2 = Ip4Address.valueOf(0x0b020202);
+
+        deserializer = IGMP.deserializer();
+
+        // Create an IGMP Query object
+        igmpQuery = new IGMP();
+        igmpQuery.setIgmpType(IGMP.TYPE_IGMPV3_MEMBERSHIP_QUERY);
+        igmpQuery.setMaxRespCode((byte) 0x7f);
+        IGMPQuery q = new IGMPQuery(gaddr1, (byte) 0x7f);
+        q.addSource(saddr1);
+        q.addSource(saddr2);
+        q.setSbit(false);
+        igmpQuery.groups.add(q);
+
+        // Create an IGMP Membership Object
+        igmpMembership = new IGMP();
+        igmpMembership.setIgmpType(IGMP.TYPE_IGMPV3_MEMBERSHIP_REPORT);
+        IGMPMembership g1 = new IGMPMembership(gaddr1);
+        g1.addSource(saddr1);
+        g1.addSource(saddr2);
+        igmpMembership.groups.add(g1);
+        IGMPMembership g2 = new IGMPMembership(gaddr2);
+        g2.addSource(saddr1);
+        g2.addSource(saddr2);
+        igmpMembership.groups.add(g2);
+    }
+
+    @Test
+    public void testDeserializeBadInput() throws Exception {
+        PacketTestUtils.testDeserializeBadInput(deserializer);
+    }
+
+    @Test
+    public void testDeserializeTruncated() throws Exception {
+        byte [] bits = igmpQuery.serialize();
+        PacketTestUtils.testDeserializeTruncated(deserializer, bits);
+
+        bits = igmpMembership.serialize();
+        PacketTestUtils.testDeserializeTruncated(deserializer, bits);
+    }
+
+    @Test
+    public void testDeserializeQuery() throws Exception {
+        byte [] data = igmpQuery.serialize();
+        IGMP igmp = deserializer.deserialize(data, 0, data.length);
+        assertTrue(igmp.equals(igmpQuery));
+    }
+
+    @Test
+    public void testDeserializeMembership() throws Exception {
+        byte [] data = igmpMembership.serialize();
+        IGMP igmp = deserializer.deserialize(data, 0, data.length);
+        assertTrue(igmp.equals(igmpMembership));
+    }
+}