blob: c8b5f85e5bc4bad8439a1963ddb5b1bbc40bd8dc [file] [log] [blame]
/*
* Copyright 2015-present Open Networking Foundation
*
* 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 com.google.common.collect.ImmutableList;
import org.slf4j.Logger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
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 abstract class IGMP extends BasePacket {
private static final Logger log = getLogger(IGMP.class);
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;
List<IGMPGroup> groups = new ArrayList<>();
// Fields contained in the IGMP header
protected byte igmpType;
protected byte resField = 0;
protected 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 abstract void setMaxRespCode(byte 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 ImmutableList.copyOf(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 abstract boolean addGroup(IGMPGroup group);
/**
* Serialize this IGMP packet. This will take care
* of serializing IGMPv3 Queries and IGMPv3 Membership
* Reports.
*
* @return the serialized IGMP message
*/
@java.lang.SuppressWarnings("squid:S128") // suppress switch fall through warning
@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);
if (this instanceof IGMPv3) {
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;
}
} else if (this instanceof IGMPv2) {
if (this.groups.isEmpty()) {
bb.putInt(0);
} else {
bb.putInt(groups.get(0).getGaddr().getIp4Address().toInt());
}
} else {
throw new UnsupportedOperationException();
}
int size = bb.position();
// compute checksum if needed
if (this.checksum == 0) {
bb.rewind();
int accumulation = 0;
for (int i = 0; i < size * 2; ++i) {
accumulation += 0xffff & bb.getShort();
}
accumulation = (accumulation >> 16 & 0xffff)
+ (accumulation & 0xffff);
this.checksum = (short) (~accumulation & 0xffff);
bb.putShort(2, this.checksum);
}
bb.position(0);
byte[] rdata = new byte[size];
bb.get(rdata, 0, size);
return rdata;
}
/**
* Deserializer function for IPv4 packets.
*
* @return deserializer function
*/
public static Deserializer<IGMP> deserializer() {
return (data, offset, length) -> {
checkInput(data, offset, length, IGMPv2.HEADER_LENGTH);
final ByteBuffer bb = ByteBuffer.wrap(data, offset, length);
byte igmpType = bb.get();
boolean isV2;
if (igmpType == TYPE_IGMPV2_MEMBERSHIP_REPORT || igmpType == TYPE_IGMPV2_LEAVE_GROUP ||
length == IGMPv2.HEADER_LENGTH) {
isV2 = true;
} else {
isV2 = false;
}
IGMP igmp = isV2 ? new IGMPv2() : new IGMPv3();
igmp.igmpType = igmpType;
igmp.resField = bb.get();
igmp.checksum = bb.getShort();
if (isV2) {
igmp.addGroup(new IGMPQuery(IpAddress.valueOf(bb.getInt()), 0));
if (igmp.validChecksum()) {
return igmp;
}
throw new DeserializationException("invalid checksum");
}
// second check for IGMPv3
checkInput(data, offset, length, IGMPv3.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;
};
}
/**
* Validates the message's checksum.
*
* @return true if valid, false if not
*/
protected abstract boolean validChecksum();
/*
* (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
}
public static class IGMPv3 extends IGMP {
public static final int MINIMUM_HEADER_LEN = 12;
@Override
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;
}
@Override
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 IGMPQuery) {
return false;
}
break;
default:
log.debug("Warning no IGMP message type has been set");
}
this.groups.add(group);
return true;
}
@Override
protected boolean validChecksum() {
return true; //FIXME
}
}
public static class IGMPv2 extends IGMP {
public static final int HEADER_LENGTH = 8;
@Override
public void setMaxRespCode(byte respCode) {
this.resField = respCode;
}
@Override
public boolean addGroup(IGMPGroup group) {
if (groups.isEmpty()) {
groups = ImmutableList.of(group);
return true;
}
return false;
}
@Override
protected boolean validChecksum() {
int accumulation = (((int) this.igmpType) & 0xff) << 8;
accumulation += ((int) this.resField) & 0xff;
if (!groups.isEmpty()) {
int ipaddr = groups.get(0).getGaddr().getIp4Address().toInt();
accumulation += (ipaddr >> 16) & 0xffff;
accumulation += ipaddr & 0xffff;
}
accumulation = (accumulation >> 16 & 0xffff)
+ (accumulation & 0xffff);
short checksum = (short) (~accumulation & 0xffff);
return checksum == this.checksum;
}
}
}