[CORD-1429] Add DHCPv6 packet serializer and deserializer

Change-Id: I06338590ccb6c0c3a5d56c89dd23d85646ea159b
diff --git a/utils/misc/src/main/java/org/onlab/packet/DHCP6.java b/utils/misc/src/main/java/org/onlab/packet/DHCP6.java
new file mode 100644
index 0000000..b6aa691
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/DHCP6.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright 2017-present 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 com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Representation of an DHCPv6 Packet.
+ * Base on RFC-3315.
+ */
+public class DHCP6 extends BasePacket {
+    // size of different field of option
+    private static final int OPT_CODE_SIZE = 2;
+    private static final int OPT_LEN_SIZE = 2;
+
+    // default size of DHCPv6 payload (without options)
+    private static final int DHCP6_DEFAULT_SIZE = 4;
+
+    // default size of DHCPv6 relay message payload (without options)
+    private static final int DHCP6_RELAY_MSG_SIZE = 34;
+    private static final int IPV6_ADDR_LEN = 16;
+
+    // masks & offsets for default DHCPv6 header
+    private static final int MSG_TYPE_OFFSET = 24;
+    private static final int TRANSACTION_ID_MASK = 0x00ffffff;
+
+    // Relay message types
+    private static final Set<Byte> RELAY_MSG_TYPES =
+            ImmutableSet.of(MsgType.RELAY_FORW.value,
+                            MsgType.RELAY_REPL.value);
+
+    /**
+     * DHCPv6 message type.
+     */
+    public enum MsgType {
+        SOLICIT((byte) 1), ADVERTISE((byte) 2), REQUEST((byte) 3),
+        CONFIRM((byte) 4), RENEW((byte) 5), REBIND((byte) 6),
+        REPLY((byte) 7), RELEASE((byte) 8), DECLINE((byte) 9),
+        RECONFIGURE((byte) 10), INFORMATION_REQUEST((byte) 11),
+        RELAY_FORW((byte) 12), RELAY_REPL((byte) 13);
+
+        protected byte value;
+        MsgType(final byte value) {
+            this.value = value;
+        }
+        public byte value() {
+            return this.value;
+        }
+    }
+
+    /**
+     * DHCPv6 option code.
+     */
+    public enum OptionCode {
+        CLIENTID((short) 1), SERVERID((short) 2), IA_NA((short) 3), IA_TA((short) 4),
+        IAADDR((short) 5), ORO((short) 6), PREFERENCE((short) 7), ELAPSED_TIME((short) 8),
+        RELAY_MSG((short) 9), AUTH((short) 11), UNICAST((short) 12),
+        STATUS_CODE((short) 13), RAPID_COMMIT((short) 14), USER_CLASS((short) 15),
+        VENDOR_CLASS((short) 16), VENDOR_OPTS((short) 17), INTERFACE_ID((short) 18),
+        RECONF_MSG((short) 19), RECONF_ACCEPT((short) 20);
+
+        protected short value;
+        OptionCode(final short value) {
+            this.value = value;
+        }
+        public short value() {
+            return this.value;
+        }
+    }
+
+    // general field
+    private byte msgType; // 1 byte
+    private List<DHCP6Option> options;
+
+    // non-relay field
+    private int transactionId; // 3 bytes
+
+    // relay field
+    private byte hopCount; // 1 byte
+    private byte[] linkAddress; // 16 bytes
+    private byte[] peerAddress; // 16 bytes
+
+    /**
+     * Creates new DHCPv6 object.
+     */
+    public DHCP6() {
+        options = Lists.newArrayList();
+    }
+
+    @Override
+    public byte[] serialize() {
+        int payloadLength = options.stream()
+                .mapToInt(DHCP6Option::getLength)
+                .sum();
+
+        // 2 bytes code and 2 bytes length
+        payloadLength += options.size() * (OPT_CODE_SIZE + OPT_LEN_SIZE);
+
+        if (RELAY_MSG_TYPES.contains(msgType)) {
+            payloadLength += DHCP6_RELAY_MSG_SIZE;
+        } else {
+            payloadLength += DHCP6_DEFAULT_SIZE;
+        }
+
+        ByteBuffer bb = ByteBuffer.allocate(payloadLength);
+
+        if (RELAY_MSG_TYPES.contains(msgType)) {
+            bb.put(msgType);
+            bb.put(hopCount);
+            bb.put(linkAddress);
+            bb.put(peerAddress);
+        } else {
+            int defaultHeader = ((int) msgType) << MSG_TYPE_OFFSET | (transactionId & TRANSACTION_ID_MASK);
+            bb.putInt(defaultHeader);
+        }
+
+        // serialize options
+        options.forEach(option -> {
+            bb.putShort(option.getCode());
+            bb.putShort(option.getLength());
+            bb.put(option.getData());
+        });
+
+        return bb.array();
+    }
+
+    /**
+     * Returns a deserializer for DHCPv6.
+     *
+     * @return the deserializer for DHCPv6
+     */
+    public static Deserializer<DHCP6> deserializer() {
+        return (data, offset, length) -> {
+            DHCP6 dhcp6 = new DHCP6();
+
+            checkNotNull(data);
+
+            if (offset < 0 || length < 0 ||
+                    length > data.length || offset >= data.length ||
+                    offset + length > data.length) {
+                throw new DeserializationException("Illegal offset or length");
+            }
+
+            final ByteBuffer bb = ByteBuffer.wrap(data, offset, length);
+            if (bb.remaining() < DHCP6.DHCP6_DEFAULT_SIZE) {
+                throw new DeserializationException(
+                        "Buffer underflow while reading DHCPv6 option");
+            }
+
+            // peek message type
+            dhcp6.msgType = bb.array()[0];
+            if (RELAY_MSG_TYPES.contains(dhcp6.msgType)) {
+                bb.get(); // drop message type
+                dhcp6.hopCount = bb.get();
+                dhcp6.linkAddress = new byte[IPV6_ADDR_LEN];
+                dhcp6.peerAddress = new byte[IPV6_ADDR_LEN];
+
+                bb.get(dhcp6.linkAddress);
+                bb.get(dhcp6.peerAddress);
+            } else {
+                // get msg type + transaction id (1 + 3 bytes)
+                int defaultHeader = bb.getInt();
+                dhcp6.transactionId = defaultHeader & TRANSACTION_ID_MASK;
+            }
+
+            dhcp6.options = Lists.newArrayList();
+            while (bb.remaining() >= OPT_CODE_SIZE) {
+                DHCP6Option option = new DHCP6Option();
+                short code = bb.getShort();
+                if (bb.remaining() < OPT_LEN_SIZE) {
+                    throw new DeserializationException(
+                            "Buffer underflow while reading DHCPv6 option");
+                }
+
+                short optionLen = bb.getShort();
+                if (bb.remaining() < optionLen) {
+                    throw new DeserializationException(
+                            "Buffer underflow while reading DHCPv6 option");
+                }
+
+                byte[] optionData = new byte[optionLen];
+                bb.get(optionData);
+
+                option.setCode(code);
+                option.setLength(optionLen);
+                option.setData(optionData);
+                dhcp6.options.add(option);
+            }
+
+            return dhcp6;
+        };
+    }
+
+    @Override
+    public IPacket deserialize(byte[] data, int offset, int length) {
+        try {
+            return deserializer().deserialize(data, offset, length);
+        } catch (DeserializationException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Gets the message type of this DHCPv6 packet.
+     *
+     * @return the message type
+     */
+    public byte getMsgType() {
+        return msgType;
+    }
+
+    /**
+     * Gets options from this DHCPv6 packet.
+     *
+     * @return DHCPv6 options
+     */
+    public List<DHCP6Option> getOptions() {
+        return options;
+    }
+
+    /**
+     * Gets the transaction ID of this DHCPv6 packet.
+     *
+     * @return the transaction ID
+     */
+    public int getTransactionId() {
+        return transactionId;
+    }
+
+    /**
+     * Gets the hop count of this DHCPv6 relay message.
+     *
+     * @return the hop count
+     */
+    public byte getHopCount() {
+        return hopCount;
+    }
+
+    /**
+     * Gets the link address of this DHCPv6 relay message.
+     *
+     * @return the link address
+     */
+    public byte[] getLinkAddress() {
+        return linkAddress;
+    }
+
+    /**
+     * Gets the peer address of this DHCPv6 relay message.
+     *
+     * @return the link address
+     */
+    public byte[] getPeerAddress() {
+        return peerAddress;
+    }
+
+    /**
+     * Sets message type.
+     *
+     * @param msgType the message type
+     */
+    public void setMsgType(byte msgType) {
+        this.msgType = msgType;
+    }
+
+    /**
+     * Sets options.
+     *
+     * @param options the options
+     */
+    public void setOptions(List<DHCP6Option> options) {
+        this.options = options;
+    }
+
+    /**
+     * Sets transaction id.
+     *
+     * @param transactionId the transaction id
+     */
+    public void setTransactionId(int transactionId) {
+        this.transactionId = transactionId;
+    }
+
+    /**
+     * Sets hop count.
+     *
+     * @param hopCount the hop count
+     */
+    public void setHopCount(byte hopCount) {
+        this.hopCount = hopCount;
+    }
+
+    /**
+     * Sets link address.
+     *
+     * @param linkAddress the link address
+     */
+    public void setLinkAddress(byte[] linkAddress) {
+        this.linkAddress = linkAddress;
+    }
+
+    /**
+     * Sets peer address.
+     *
+     * @param peerAddress the peer address
+     */
+    public void setPeerAddress(byte[] peerAddress) {
+        this.peerAddress = peerAddress;
+    }
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/DHCP6Option.java b/utils/misc/src/main/java/org/onlab/packet/DHCP6Option.java
new file mode 100644
index 0000000..f70a1c5
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/DHCP6Option.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2017-present 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;
+
+/**
+ * Representation of an DHCPv6 Option.
+ * Base on RFC-3315.
+ */
+public class DHCP6Option {
+    private short code;
+    private short length;
+    private byte[] data;
+
+    /**
+     * Sets the code of this option.
+     *
+     * @param code the code to set
+     */
+    public void setCode(short code) {
+        this.code = code;
+    }
+
+    /**
+     * Sets the data and length of this option.
+     *
+     * @param data the data to set
+     */
+    public void setData(byte[] data) {
+        this.data = data;
+    }
+
+    /**
+     * Sets length of this option.
+     *
+     * @param length the length to set
+     */
+    public void setLength(short length) {
+        this.length = length;
+    }
+
+    /**
+     * Gets the code of this option.
+     *
+     * @return the code
+     */
+    public short getCode() {
+        return code;
+    }
+
+    /**
+     * Gets the length of this option.
+     *
+     * @return the length of this option
+     */
+    public short getLength() {
+        return length;
+    }
+
+    /**
+     * Gets the data of this option.
+     *
+     * @return the data of this option
+     */
+    public byte[] getData() {
+        return data;
+    }
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/UDP.java b/utils/misc/src/main/java/org/onlab/packet/UDP.java
index edf99af..6849654 100644
--- a/utils/misc/src/main/java/org/onlab/packet/UDP.java
+++ b/utils/misc/src/main/java/org/onlab/packet/UDP.java
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-
-
 package org.onlab.packet;
 
 import java.nio.ByteBuffer;
@@ -32,12 +30,16 @@
 public class UDP extends BasePacket {
     public static final Map<Integer, Deserializer<? extends IPacket>> PORT_DESERIALIZER_MAP =
             ImmutableMap.<Integer, Deserializer<? extends IPacket>>builder()
-                .put(UDP.DHCP_SERVER_PORT, DHCP.deserializer())
-                .put(UDP.DHCP_CLIENT_PORT, DHCP.deserializer())
-                .build();
+                    .put(UDP.DHCP_SERVER_PORT, DHCP.deserializer())
+                    .put(UDP.DHCP_CLIENT_PORT, DHCP.deserializer())
+                    .put(UDP.DHCP_V6_SERVER_PORT, DHCP6.deserializer())
+                    .put(UDP.DHCP_V6_CLIENT_PORT, DHCP6.deserializer())
+                    .build();
 
     public static final int DHCP_SERVER_PORT = 67;
     public static final int DHCP_CLIENT_PORT = 68;
+    public static final int DHCP_V6_SERVER_PORT = 547;
+    public static final int DHCP_V6_CLIENT_PORT = 546;
 
     private static final short UDP_HEADER_LENGTH = 8;
 
diff --git a/utils/misc/src/test/java/org/onlab/packet/Dhcp6Test.java b/utils/misc/src/test/java/org/onlab/packet/Dhcp6Test.java
new file mode 100644
index 0000000..927266d
--- /dev/null
+++ b/utils/misc/src/test/java/org/onlab/packet/Dhcp6Test.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2017-present 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 com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import java.nio.ByteBuffer;
+
+public class Dhcp6Test {
+    private static final int OPT_CLIENT_ID = 0xBEEFBEEF;
+    private static final byte[] OPT_CLIENT_ID_BYTE_ARR =
+            {(byte) 0xBE, (byte) 0xEF, (byte) 0xBE, (byte) 0xEF};
+    private static final short OPT_CLIENT_ID_SIZE = 4;
+    private static final int OPT_AUTH = 0xBA11BA11;
+    private static final byte[] OPT_AUTH_BYTE_AR =
+            {(byte) 0xBA, 0x11, (byte) 0xBA, 0x11};
+    private static final short OPT_AUTH_SIZE = 4;
+    private static final int TRANSACTION_ID = 0xC0FFEE;
+    private static final byte[] TRANSACTION_ID_BYTE_ARR =
+            {(byte) 0xC0, (byte) 0xFF, (byte) 0xEE};
+    private static final byte HOP_COUNT = 3;
+    private static final Ip6Address LINK_ADDRESS = Ip6Address.valueOf("1111:2222::8888");
+    private static final Ip6Address PEER_ADDRESS = Ip6Address.valueOf("3333:4444::9999");
+    Deserializer<DHCP6> deserializer = DHCP6.deserializer();
+    private byte[] byteHeader;
+
+    @Test
+    public void testDeserializeBadInput() throws Exception {
+        PacketTestUtils.testDeserializeBadInput(deserializer);
+    }
+
+    /**
+     * Truncated a simple DHCPv6 payload.
+     */
+    @Test
+    public void testDeserializeTruncated() throws Exception {
+        ByteBuffer bb = ByteBuffer.allocate(4);
+        bb.put(DHCP6.MsgType.REQUEST.value());
+        bb.put(TRANSACTION_ID_BYTE_ARR);
+        byteHeader = bb.array();
+
+        PacketTestUtils.testDeserializeTruncated(deserializer, byteHeader);
+    }
+
+    /**
+     * Basic DHCPv6 header with one msg type and one transaction id.
+     */
+    @Test
+    public void testDeserializeDefaultPayload() throws Exception {
+        ByteBuffer bb = ByteBuffer.allocate(12);
+        bb.put(DHCP6.MsgType.REQUEST.value());
+        bb.put(TRANSACTION_ID_BYTE_ARR);
+
+        // put a simple client id (4 bytes)
+        bb.putShort(DHCP6.OptionCode.CLIENTID.value());
+        bb.putShort(OPT_CLIENT_ID_SIZE);
+        bb.putInt(OPT_CLIENT_ID);
+        byteHeader = bb.array();
+
+        DHCP6 dhcp6 = deserializer.deserialize(byteHeader, 0, byteHeader.length);
+        assertEquals(dhcp6.getMsgType(), DHCP6.MsgType.REQUEST.value());
+        assertEquals(dhcp6.getTransactionId(), TRANSACTION_ID);
+        assertEquals(dhcp6.getOptions().size(), 1);
+
+        DHCP6Option clientIdOption = dhcp6.getOptions().get(0);
+        assertEquals(clientIdOption.getCode(), DHCP6.OptionCode.CLIENTID.value());
+        assertArrayEquals(clientIdOption.getData(), OPT_CLIENT_ID_BYTE_ARR);
+    }
+
+    /**
+     * DHCPv6 header with relay agent information.
+     */
+    @Test
+    public void testDeserializeRelayAgent() throws Exception {
+        ByteBuffer bb = ByteBuffer.allocate(42);
+        bb.put(DHCP6.MsgType.RELAY_FORW.value());
+        bb.put(HOP_COUNT);
+
+        bb.put(LINK_ADDRESS.toOctets());
+        bb.put(PEER_ADDRESS.toOctets());
+
+        // put a simple client id (4 bytes)
+        bb.putShort(DHCP6.OptionCode.CLIENTID.value());
+        bb.putShort(OPT_CLIENT_ID_SIZE);
+        bb.putInt(OPT_CLIENT_ID);
+        byteHeader = bb.array();
+
+        DHCP6 dhcp6 = deserializer.deserialize(byteHeader, 0, byteHeader.length);
+        assertEquals(dhcp6.getMsgType(), DHCP6.MsgType.RELAY_FORW.value());
+        assertEquals(dhcp6.getHopCount(), HOP_COUNT);
+        assertArrayEquals(dhcp6.getLinkAddress(), LINK_ADDRESS.toOctets());
+        assertArrayEquals(dhcp6.getPeerAddress(), PEER_ADDRESS.toOctets());
+        assertEquals(dhcp6.getOptions().size(), 1);
+
+        DHCP6Option clientIdOption = dhcp6.getOptions().get(0);
+        assertEquals(clientIdOption.getCode(), DHCP6.OptionCode.CLIENTID.value());
+        assertArrayEquals(clientIdOption.getData(), OPT_CLIENT_ID_BYTE_ARR);
+    }
+
+    /**
+     * Serialize DHCPv6 header with default payload and options.
+     */
+    @Test
+    public void testSerializeDefaultPayload() throws Exception {
+        DHCP6 dhcp6 = new DHCP6();
+        dhcp6.setMsgType(DHCP6.MsgType.REQUEST.value());
+        dhcp6.setTransactionId(TRANSACTION_ID);
+
+        DHCP6Option opt1 = new DHCP6Option();
+        opt1.setCode(DHCP6.OptionCode.CLIENTID.value());
+        opt1.setLength(OPT_CLIENT_ID_SIZE);
+        opt1.setData(OPT_CLIENT_ID_BYTE_ARR);
+
+
+        DHCP6Option opt2 = new DHCP6Option();
+        opt2.setCode(DHCP6.OptionCode.AUTH.value());
+        opt2.setLength(OPT_AUTH_SIZE);
+        opt2.setData(OPT_AUTH_BYTE_AR);
+
+        dhcp6.setOptions(ImmutableList.of(opt1, opt2));
+
+        byte[] serialized = dhcp6.serialize();
+        ByteBuffer expected = ByteBuffer.allocate(20)
+                .put(DHCP6.MsgType.REQUEST.value())
+                .put(TRANSACTION_ID_BYTE_ARR)
+                .putShort(DHCP6.OptionCode.CLIENTID.value())
+                .putShort(OPT_CLIENT_ID_SIZE)
+                .putInt(OPT_CLIENT_ID)
+                .putShort(DHCP6.OptionCode.AUTH.value())
+                .putShort(OPT_AUTH_SIZE)
+                .putInt(OPT_AUTH);
+
+        assertArrayEquals(serialized, expected.array());
+    }
+
+    /**
+     * Serialize DHCPv6 header with relay agent payload and options.
+     */
+    @Test
+    public void testSerializeRelayAgent() throws Exception {
+        DHCP6 dhcp6 = new DHCP6();
+        dhcp6.setMsgType(DHCP6.MsgType.RELAY_FORW.value());
+        dhcp6.setHopCount(HOP_COUNT);
+        dhcp6.setLinkAddress(LINK_ADDRESS.toOctets());
+        dhcp6.setPeerAddress(PEER_ADDRESS.toOctets());
+
+        DHCP6Option opt1 = new DHCP6Option();
+        opt1.setCode(DHCP6.OptionCode.CLIENTID.value());
+        opt1.setLength(OPT_CLIENT_ID_SIZE);
+        opt1.setData(OPT_CLIENT_ID_BYTE_ARR);
+
+
+        DHCP6Option opt2 = new DHCP6Option();
+        opt2.setCode(DHCP6.OptionCode.AUTH.value());
+        opt2.setLength(OPT_AUTH_SIZE);
+        opt2.setData(OPT_AUTH_BYTE_AR);
+
+        dhcp6.setOptions(ImmutableList.of(opt1, opt2));
+
+        byte[] serialized = dhcp6.serialize();
+        ByteBuffer expected = ByteBuffer.allocate(50)
+                .put(DHCP6.MsgType.RELAY_FORW.value())
+                .put(HOP_COUNT)
+                .put(LINK_ADDRESS.toOctets())
+                .put(PEER_ADDRESS.toOctets())
+                .putShort(DHCP6.OptionCode.CLIENTID.value())
+                .putShort(OPT_CLIENT_ID_SIZE)
+                .putInt(OPT_CLIENT_ID)
+                .putShort(DHCP6.OptionCode.AUTH.value())
+                .putShort(OPT_AUTH_SIZE)
+                .putInt(OPT_AUTH);
+
+        assertArrayEquals(serialized, expected.array());
+    }
+}