[CORD-1431] Support DHCPv6 by HostLocationProvider

Change-Id: I46d268ae4f61949e2cf48f58afa6bb72e8bf4c22
diff --git a/providers/host/src/main/java/org/onosproject/provider/host/impl/HostLocationProvider.java b/providers/host/src/main/java/org/onosproject/provider/host/impl/HostLocationProvider.java
index 1ead471..445afad 100644
--- a/providers/host/src/main/java/org/onosproject/provider/host/impl/HostLocationProvider.java
+++ b/providers/host/src/main/java/org/onosproject/provider/host/impl/HostLocationProvider.java
@@ -23,17 +23,25 @@
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.onlab.packet.ARP;
+import org.onlab.packet.BasePacket;
 import org.onlab.packet.DHCP;
+import org.onlab.packet.DHCP6;
 import org.onlab.packet.Ethernet;
 import org.onlab.packet.ICMP6;
 import org.onlab.packet.IPacket;
 import org.onlab.packet.IPv4;
 import org.onlab.packet.IPv6;
+import org.onlab.packet.Ip6Address;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.TpPort;
 import org.onlab.packet.UDP;
 import org.onlab.packet.VlanId;
+import org.onlab.packet.dhcp.Dhcp6ClientIdOption;
+import org.onlab.packet.dhcp.Dhcp6IaAddressOption;
+import org.onlab.packet.dhcp.Dhcp6IaNaOption;
+import org.onlab.packet.dhcp.Dhcp6IaTaOption;
+import org.onlab.packet.dhcp.Dhcp6RelayOption;
 import org.onlab.packet.ipv6.IExtensionHeader;
 import org.onlab.packet.ndp.NeighborAdvertisement;
 import org.onlab.packet.ndp.NeighborSolicitation;
@@ -145,6 +153,11 @@
                     "Host Location Provider; default is false")
     private boolean useDhcp = false;
 
+    @Property(name = "useDhcp6", boolValue = false,
+            label = "Use DHCPv6 for neighbor discovery by the " +
+                    "Host Location Provider; default is false")
+    private boolean useDhcp6 = false;
+
     @Property(name = "requestInterceptsEnabled", boolValue = true,
             label = "Enable requesting packet intercepts")
     private boolean requestInterceptsEnabled = true;
@@ -509,10 +522,18 @@
                         pkt.getPayload() instanceof IExtensionHeader) {
                     pkt = pkt.getPayload();
                 }
-
-                // Neighbor Discovery Protocol
                 pkt = pkt.getPayload();
+
+                // DHCPv6 protocol
+                DHCP6 dhcp6 = findDhcp6(pkt).orElse(null);
+                if (dhcp6 != null && useDhcp6) {
+                    createOrUpdateHost(hid, srcMac, vlan, hloc, null);
+                    handleDhcp6(dhcp6, vlan);
+                    return;
+                }
+
                 if (pkt != null && pkt instanceof ICMP6) {
+                    // Neighbor Discovery Protocol
                     pkt = pkt.getPayload();
                     // RouterSolicitation, RouterAdvertisement
                     if (pkt != null && (pkt instanceof RouterAdvertisement ||
@@ -541,6 +562,88 @@
             }
         }
 
+        /**
+         * Handles DHCPv6 packet, if message type is ACK, update IP address
+         * according to DHCPv6 payload (IA Address option).
+         *
+         * @param dhcp6 the DHCPv6 payload
+         * @param vlanId the vlan of this packet
+         */
+        private void handleDhcp6(DHCP6 dhcp6, VlanId vlanId) {
+            // extract the relay message if exist
+            while (dhcp6 != null && DHCP6.RELAY_MSG_TYPES.contains(dhcp6.getMsgType())) {
+                dhcp6 = dhcp6.getOptions().stream()
+                        .filter(opt -> opt instanceof Dhcp6RelayOption)
+                        .map(BasePacket::getPayload)
+                        .map(pld -> (DHCP6) pld)
+                        .findFirst()
+                        .orElse(null);
+            }
+
+            if (dhcp6 == null) {
+                // Can't find dhcp payload
+                log.warn("Can't find dhcp payload from relay message");
+                return;
+            }
+
+            if (dhcp6.getMsgType() != DHCP6.MsgType.REPLY.value()) {
+                // Update IP address only when we received REPLY message
+                return;
+            }
+            Optional<Dhcp6ClientIdOption> clientIdOption = dhcp6.getOptions()
+                    .stream()
+                    .filter(opt -> opt instanceof Dhcp6ClientIdOption)
+                    .map(opt -> (Dhcp6ClientIdOption) opt)
+                    .findFirst();
+
+            if (!clientIdOption.isPresent()) {
+                // invalid DHCPv6 option
+                log.warn("Can't find client ID from DHCPv6 {}", dhcp6);
+                return;
+            }
+
+            byte[] linkLayerAddr = clientIdOption.get().getDuid().getLinkLayerAddress();
+            if (linkLayerAddr == null || linkLayerAddr.length != 6) {
+                // No any mac address found
+                log.warn("Can't find client mac from option {}", clientIdOption);
+                return;
+            }
+            MacAddress clientMac = MacAddress.valueOf(linkLayerAddr);
+
+            // Extract IPv6 address from IA NA ot IA TA option
+            Optional<Dhcp6IaNaOption> iaNaOption = dhcp6.getOptions()
+                    .stream()
+                    .filter(opt -> opt instanceof Dhcp6IaNaOption)
+                    .map(opt -> (Dhcp6IaNaOption) opt)
+                    .findFirst();
+            Optional<Dhcp6IaTaOption> iaTaOption = dhcp6.getOptions()
+                    .stream()
+                    .filter(opt -> opt instanceof Dhcp6IaTaOption)
+                    .map(opt -> (Dhcp6IaTaOption) opt)
+                    .findFirst();
+            Optional<Dhcp6IaAddressOption> iaAddressOption;
+            if (iaNaOption.isPresent()) {
+                iaAddressOption = iaNaOption.get().getOptions().stream()
+                        .filter(opt -> opt instanceof Dhcp6IaAddressOption)
+                        .map(opt -> (Dhcp6IaAddressOption) opt)
+                        .findFirst();
+            } else if (iaTaOption.isPresent()) {
+                iaAddressOption = iaTaOption.get().getOptions().stream()
+                        .filter(opt -> opt instanceof Dhcp6IaAddressOption)
+                        .map(opt -> (Dhcp6IaAddressOption) opt)
+                        .findFirst();
+            } else {
+                iaAddressOption = Optional.empty();
+            }
+            if (iaAddressOption.isPresent()) {
+                Ip6Address ip = iaAddressOption.get().getIp6Address();
+                HostId hostId = HostId.hostId(clientMac, vlanId);
+                updateHostIp(hostId, ip);
+            } else {
+                log.warn("Can't find IPv6 address from DHCPv6 {}", dhcp6);
+            }
+        }
+
         private Optional<DHCP> findDhcp(Ethernet eth) {
             IPacket pkt = eth.getPayload();
             return Stream.of(pkt)
@@ -555,6 +658,17 @@
                     .map(p -> (DHCP) p)
                     .findFirst();
         }
+
+        private Optional<DHCP6> findDhcp6(IPacket pkt) {
+            return Stream.of(pkt)
+                    .filter(Objects::nonNull)
+                    .filter(p -> p instanceof UDP)
+                    .map(IPacket::getPayload)
+                    .filter(Objects::nonNull)
+                    .filter(p -> p instanceof DHCP6)
+                    .map(p -> (DHCP6) p)
+                    .findFirst();
+        }
     }
 
     // Auxiliary listener to device events.
diff --git a/providers/host/src/test/java/org/onosproject/provider/host/impl/HostLocationProviderTest.java b/providers/host/src/test/java/org/onosproject/provider/host/impl/HostLocationProviderTest.java
index 72cb6d8..7cd566f 100644
--- a/providers/host/src/test/java/org/onosproject/provider/host/impl/HostLocationProviderTest.java
+++ b/providers/host/src/test/java/org/onosproject/provider/host/impl/HostLocationProviderTest.java
@@ -27,7 +27,12 @@
 import org.onlab.packet.ARP;
 import org.onlab.packet.ChassisId;
 import org.onlab.packet.DHCP;
-import org.onlab.packet.dhcp.CircuitId;
+import org.onlab.packet.DHCP6;
+import org.onlab.packet.dhcp.Dhcp6ClientIdOption;
+import org.onlab.packet.dhcp.Dhcp6Duid;
+import org.onlab.packet.dhcp.Dhcp6IaAddressOption;
+import org.onlab.packet.dhcp.Dhcp6IaNaOption;
+import org.onlab.packet.dhcp.Dhcp6Option;
 import org.onlab.packet.dhcp.DhcpOption;
 import org.onlab.packet.Ethernet;
 import org.onlab.packet.ICMP6;
@@ -38,7 +43,6 @@
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.UDP;
 import org.onlab.packet.VlanId;
-import org.onlab.packet.dhcp.DhcpRelayAgentOption;
 import org.onlab.packet.ndp.NeighborAdvertisement;
 import org.onlab.packet.ndp.NeighborSolicitation;
 import org.onlab.packet.ndp.RouterAdvertisement;
@@ -153,7 +157,6 @@
     private static final byte[] IP3 = new byte[]{10, 0, 0, 2};
     private static final IpAddress IP_ADDRESS3 =
             IpAddress.valueOf(IpAddress.Version.INET, IP3);
-
     private static final HostLocation LOCATION3 =
             new HostLocation(deviceId(DEV1), portNumber(INPORT2), 0L);
     private static final DefaultHost HOST3 =
@@ -161,6 +164,11 @@
                     VLAN, LOCATION3,
                     ImmutableSet.of(IP_ADDRESS3));
 
+    // DHCP6 Server
+    private static final MacAddress DHCP6_SERVER_MAC = MacAddress.valueOf("00:00:44:00:00:04");
+    private static final IpAddress DHCP6_SERVER_IP =
+            IpAddress.valueOf("2000::1:1000");
+
     // Gateway information for relay agent
     private static final InterfaceIpAddress GW_IFACE_ADDR = InterfaceIpAddress.valueOf("10.0.1.1/32");
     private static final Interface GW_IFACE = new Interface("gateway",
@@ -369,7 +377,7 @@
         assertThat(descr.vlan(), is(VLAN));
 
         // DHCP Ack
-        testProcessor.process(new TestDhcpAckPacketContext(DEV1, false));
+        testProcessor.process(new TestDhcpAckPacketContext(DEV1));
         assertThat("receiveDhcpAck. Two additional host descriptions expected",
                 providerService.descriptions.size(), is(3));
 
@@ -391,6 +399,46 @@
     }
 
     /**
+     * When receiving DHCPv6 REQUEST, update MAC, location of client.
+     * When receiving DHCPv6 ACK, update MAC, location of server and IP of client.
+     */
+    @Test
+    public void receiveDhcp6() {
+        TestUtils.setField(provider, "useDhcp6", true);
+        // DHCP Request
+        testProcessor.process(new TestDhcp6RequestPacketContext(DEV4, VLAN));
+        assertThat("receiveDhcpRequest. One host description expected",
+                   providerService.descriptions.size(), is(1));
+        // Should learn the MAC and location of DHCP client
+        HostDescription descr = providerService.descriptions.get(0);
+        assertThat(descr.location(), is(LOCATION2));
+        assertThat(descr.hwAddress(), is(MAC2));
+        assertThat(descr.ipAddress().size(), is(0));
+        assertThat(descr.vlan(), is(VLAN));
+
+        // DHCP Ack
+        testProcessor.process(new TestDhcp6AckPacketContext(DEV1));
+        assertThat("receiveDhcpAck. Two additional host descriptions expected",
+                   providerService.descriptions.size(), is(3));
+
+        // Should also learn the MAC, location of DHCP server
+        HostDescription descr2 = providerService.descriptions.get(1);
+        assertThat(descr2.location(), is(LOCATION3));
+        assertThat(descr2.hwAddress(), is(DHCP6_SERVER_MAC));
+        assertThat(descr2.ipAddress().size(), is(0));
+        assertThat(descr2.vlan(), is(VLAN));
+
+        // Should update the IP address of the DHCP client.
+        HostDescription descr3 = providerService.descriptions.get(2);
+        assertThat(descr3.location(), is(LOCATION2));
+        assertThat(descr3.hwAddress(), is(MAC2));
+        assertThat(descr3.ipAddress().size(), is(1));
+        IpAddress ip = descr3.ipAddress().iterator().next();
+        assertThat(ip, is(IP_ADDRESS2.getIp6Address()));
+        assertThat(descr3.vlan(), is(VLAN));
+    }
+
+    /**
      * The host store should not updated when we disabled "useDhcp".
      */
     @Test
@@ -635,6 +683,7 @@
                                             ByteBuffer.wrap(eth.serialize()));
         }
     }
+
     /**
      * Generates DHCP REQUEST packet.
      */
@@ -686,12 +735,10 @@
      */
     private class TestDhcpAckPacketContext extends PacketContextAdapter {
         private final String deviceId;
-        private final boolean withRelayinfo;
 
-        public TestDhcpAckPacketContext(String deviceId, boolean withRelayInfo) {
+        public TestDhcpAckPacketContext(String deviceId) {
             super(0, null, null, false);
             this.deviceId = deviceId;
-            this.withRelayinfo = withRelayInfo;
         }
 
         @Override
@@ -705,23 +752,7 @@
             dhcpOption.setLength((byte) 1);
 
             DHCP dhcp = new DHCP();
-
-            if (withRelayinfo) {
-                CircuitId cid = new CircuitId(LOCATION.toString(), VLAN_100);
-                byte[] circuitId = cid.serialize();
-                DhcpOption circuitIdSubOption = new DhcpOption();
-                circuitIdSubOption.setCode(DhcpRelayAgentOption.RelayAgentInfoOptions.CIRCUIT_ID.getValue());
-                circuitIdSubOption.setData(circuitId);
-                circuitIdSubOption.setLength((byte) circuitId.length);
-
-                DhcpRelayAgentOption relayInfoOption = new DhcpRelayAgentOption();
-                relayInfoOption.setCode(DHCP.DHCPOptionCode.OptionCode_CircuitID.getValue());
-                relayInfoOption.addSubOption(circuitIdSubOption);
-                dhcp.setOptions(ImmutableList.of(dhcpOption, relayInfoOption));
-                dhcp.setGatewayIPAddress(GW_IFACE_ADDR.ipAddress().getIp4Address().toInt());
-            } else {
-                dhcp.setOptions(ImmutableList.of(dhcpOption));
-            }
+            dhcp.setOptions(ImmutableList.of(dhcpOption));
 
             dhcp.setClientHardwareAddress(MAC.toBytes());
             dhcp.setYourIPAddress(IP_ADDRESS.getIp4Address().toInt());
@@ -748,6 +779,118 @@
     }
 
     /**
+     * Generates DHCPv6 REQUEST packet.
+     */
+    private class TestDhcp6RequestPacketContext extends PacketContextAdapter {
+        private final String deviceId;
+        private final VlanId vlanId;
+
+        public TestDhcp6RequestPacketContext(String deviceId, VlanId vlanId) {
+            super(0, null, null, false);
+            this.deviceId = deviceId;
+            this.vlanId = vlanId;
+        }
+
+        @Override
+        public InboundPacket inPacket() {
+
+            DHCP6 dhcp6 = new DHCP6();
+            dhcp6.setMsgType(DHCP6.MsgType.REQUEST.value());
+            List<Dhcp6Option> options = Lists.newArrayList();
+
+            // IA address
+            Dhcp6IaAddressOption iaAddressOption = new Dhcp6IaAddressOption();
+            iaAddressOption.setIp6Address(IP_ADDRESS2.getIp6Address());
+
+            // IA NA
+            Dhcp6IaNaOption iaNaOption = new Dhcp6IaNaOption();
+            iaNaOption.setOptions(ImmutableList.of(iaAddressOption));
+            options.add(iaNaOption);
+
+            dhcp6.setOptions(options);
+
+            UDP udp = new UDP();
+            udp.setPayload(dhcp6);
+            udp.setSourcePort(UDP.DHCP_V6_CLIENT_PORT);
+            udp.setDestinationPort(UDP.DHCP_V6_SERVER_PORT);
+            IPv6 ipv6 = new IPv6();
+            ipv6.setPayload(udp);
+            ipv6.setDestinationAddress(Ip6Address.ZERO.toOctets());
+            ipv6.setSourceAddress(Ip6Address.ZERO.toOctets());
+            ipv6.setNextHeader(IPv6.PROTOCOL_UDP);
+            Ethernet eth = new Ethernet();
+            eth.setEtherType(Ethernet.TYPE_IPV6)
+                    .setVlanID(this.vlanId.toShort())
+                    .setSourceMACAddress(MAC2)
+                    .setDestinationMACAddress(DHCP6_SERVER_MAC)
+                    .setPayload(ipv6);
+            ConnectPoint receivedFrom = new ConnectPoint(deviceId(deviceId),
+                                                         portNumber(INPORT));
+            return new DefaultInboundPacket(receivedFrom, eth,
+                                            ByteBuffer.wrap(eth.serialize()));
+        }
+    }
+
+    /**
+     * Generates DHCPv6 ACK packet.
+     */
+    private class TestDhcp6AckPacketContext extends PacketContextAdapter {
+        private final String deviceId;
+
+        public TestDhcp6AckPacketContext(String deviceId) {
+            super(0, null, null, false);
+            this.deviceId = deviceId;
+        }
+
+        @Override
+        public InboundPacket inPacket() {
+            DHCP6 dhcp6 = new DHCP6();
+            dhcp6.setMsgType(DHCP6.MsgType.REPLY.value());
+            List<Dhcp6Option> options = Lists.newArrayList();
+
+            // IA address
+            Dhcp6IaAddressOption iaAddressOption = new Dhcp6IaAddressOption();
+            iaAddressOption.setIp6Address(IP_ADDRESS2.getIp6Address());
+
+            // IA NA
+            Dhcp6IaNaOption iaNaOption = new Dhcp6IaNaOption();
+            iaNaOption.setOptions(ImmutableList.of(iaAddressOption));
+            options.add(iaNaOption);
+
+            // Client ID
+            Dhcp6Duid duid = new Dhcp6Duid();
+            duid.setDuidType(Dhcp6Duid.DuidType.DUID_LLT);
+            duid.setHardwareType((short) 1);
+            duid.setDuidTime(0);
+            duid.setLinkLayerAddress(MAC2.toBytes());
+            Dhcp6ClientIdOption clientIdOption = new Dhcp6ClientIdOption();
+            clientIdOption.setDuid(duid);
+            options.add(clientIdOption);
+            dhcp6.setOptions(options);
+
+            UDP udp = new UDP();
+            udp.setPayload(dhcp6);
+            udp.setSourcePort(UDP.DHCP_V6_CLIENT_PORT);
+            udp.setDestinationPort(UDP.DHCP_V6_SERVER_PORT);
+            IPv6 ipv6 = new IPv6();
+            ipv6.setPayload(udp);
+            ipv6.setDestinationAddress(Ip6Address.ZERO.toOctets());
+            ipv6.setSourceAddress(Ip6Address.ZERO.toOctets());
+            ipv6.setNextHeader(IPv6.PROTOCOL_UDP);
+            Ethernet eth = new Ethernet();
+            eth.setEtherType(Ethernet.TYPE_IPV6)
+                    .setVlanID(VLAN.toShort())
+                    .setSourceMACAddress(DHCP6_SERVER_MAC)
+                    .setDestinationMACAddress(MAC2)
+                    .setPayload(ipv6);
+            ConnectPoint receivedFrom = new ConnectPoint(deviceId(deviceId),
+                                                         portNumber(INPORT2));
+            return new DefaultInboundPacket(receivedFrom, eth,
+                                            ByteBuffer.wrap(eth.serialize()));
+        }
+    }
+
+    /**
      * Generates NeighborAdvertisement packet.
      */
     private class TestNaPacketContext extends PacketContextAdapter {