IGMP.java 10.1 KB
/*
 * 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.slf4j.Logger;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Arrays;

import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.onlab.packet.PacketUtils.checkInput;
import static org.slf4j.LoggerFactory.getLogger;


/**
 * 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();

            final ByteBuffer bb = ByteBuffer.wrap(data, offset, length);
            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 recognized";
                    igmp.unsupportTypeData = bb.array();
                    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;
    }

    @Override
    public String toString() {
        return toStringHelper(getClass())
                .add("igmpType", Byte.toString(igmpType))
                .add("resField", Byte.toString(resField))
                .add("checksum", Short.toString(checksum))
                .add("unsupportTypeData", Arrays.toString(unsupportTypeData))
                .toString();
        // TODO: need to handle groups
    }
}