Use DHCP ACK to learn the DHCP client

This feature can be turn on/off via component config.
In addition, ARP interception can also be turned on/off now.

Change-Id: Ia3310fa3fb06821051fbf3363e51096d00781dbf
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 d91d437..1119eda 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,12 +23,17 @@
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.onlab.packet.ARP;
+import org.onlab.packet.DHCP;
+import org.onlab.packet.DHCPPacketType;
 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.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.ipv6.IExtensionHeader;
 import org.onlab.packet.ndp.NeighborAdvertisement;
@@ -120,10 +125,20 @@
             label = "Enable host removal on port/device down events")
     private boolean hostRemovalEnabled = true;
 
-    @Property(name = "ipv6NeighborDiscovery", boolValue = false,
+    @Property(name = "useArp", boolValue = true,
+            label = "Enable using ARP for neighbor discovery by the " +
+                    "Host Location Provider; default is true")
+    private boolean useArp = true;
+
+    @Property(name = "useIpv6ND", boolValue = false,
             label = "Enable using IPv6 Neighbor Discovery by the " +
                     "Host Location Provider; default is false")
-    private boolean ipv6NeighborDiscovery = false;
+    private boolean useIpv6ND = false;
+
+    @Property(name = "useDhcp", boolValue = false,
+            label = "Enable using DHCP for neighbor discovery by the " +
+                    "Host Location Provider; default is false")
+    private boolean useDhcp = false;
 
     @Property(name = "requestInterceptsEnabled", boolValue = true,
             label = "Enable requesting packet intercepts")
@@ -184,26 +199,52 @@
      * Request packet intercepts.
      */
     private void requestIntercepts() {
-        TrafficSelector.Builder selector = DefaultTrafficSelector.builder();
-        selector.matchEthType(Ethernet.TYPE_ARP);
-        packetService.requestPackets(selector.build(), PacketPriority.CONTROL, appId);
-
-        // IPv6 Neighbor Solicitation packet.
-        selector.matchEthType(Ethernet.TYPE_IPV6);
-        selector.matchIPProtocol(IPv6.PROTOCOL_ICMP6);
-        selector.matchIcmpv6Type(ICMP6.NEIGHBOR_SOLICITATION);
-        if (ipv6NeighborDiscovery) {
-            packetService.requestPackets(selector.build(), PacketPriority.CONTROL, appId);
+        // Use ARP
+        TrafficSelector arpSelector = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_ARP)
+                .build();
+        if (useArp) {
+            packetService.requestPackets(arpSelector, PacketPriority.CONTROL, appId);
         } else {
-            packetService.cancelPackets(selector.build(), PacketPriority.CONTROL, appId);
+            packetService.cancelPackets(arpSelector, PacketPriority.CONTROL, appId);
         }
 
-        // IPv6 Neighbor Advertisement packet.
-        selector.matchIcmpv6Type(ICMP6.NEIGHBOR_ADVERTISEMENT);
-        if (ipv6NeighborDiscovery) {
-            packetService.requestPackets(selector.build(), PacketPriority.CONTROL, appId);
+        // Use IPv6 Neighbor Discovery
+        TrafficSelector ipv6NsSelector = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_IPV6)
+                .matchIPProtocol(IPv6.PROTOCOL_ICMP6)
+                .matchIcmpv6Type(ICMP6.NEIGHBOR_SOLICITATION)
+                .build();
+        TrafficSelector ipv6NaSelector = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_IPV6)
+                .matchIPProtocol(IPv6.PROTOCOL_ICMP6)
+                .matchIcmpv6Type(ICMP6.NEIGHBOR_ADVERTISEMENT)
+                .build();
+        if (useIpv6ND) {
+            packetService.requestPackets(ipv6NsSelector, PacketPriority.CONTROL, appId);
+            packetService.requestPackets(ipv6NaSelector, PacketPriority.CONTROL, appId);
         } else {
-            packetService.cancelPackets(selector.build(), PacketPriority.CONTROL, appId);
+            packetService.cancelPackets(ipv6NsSelector, PacketPriority.CONTROL, appId);
+            packetService.cancelPackets(ipv6NaSelector, PacketPriority.CONTROL, appId);
+        }
+
+        // Use DHCP
+        TrafficSelector dhcpServerSelector = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_IPV4)
+                .matchIPProtocol(IPv4.PROTOCOL_UDP)
+                .matchUdpSrc(TpPort.tpPort(UDP.DHCP_SERVER_PORT))
+                .build();
+        TrafficSelector dhcpClientSelector = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_IPV4)
+                .matchIPProtocol(IPv4.PROTOCOL_UDP)
+                .matchUdpSrc(TpPort.tpPort(UDP.DHCP_CLIENT_PORT))
+                .build();
+        if (useDhcp) {
+            packetService.requestPackets(dhcpServerSelector, PacketPriority.CONTROL, appId);
+            packetService.requestPackets(dhcpClientSelector, PacketPriority.CONTROL, appId);
+        } else {
+            packetService.cancelPackets(dhcpServerSelector, PacketPriority.CONTROL, appId);
+            packetService.cancelPackets(dhcpClientSelector, PacketPriority.CONTROL, appId);
         }
     }
 
@@ -245,14 +286,34 @@
                      hostRemovalEnabled ? "enabled" : "disabled");
         }
 
-        flag = Tools.isPropertyEnabled(properties, "ipv6NeighborDiscovery");
+        flag = Tools.isPropertyEnabled(properties, "useArp");
+        if (flag == null) {
+            log.info("Using ARP is not configured, " +
+                    "using current value of {}", useArp);
+        } else {
+            useArp = flag;
+            log.info("Configured. Using ARP is {}",
+                    useArp ? "enabled" : "disabled");
+        }
+
+        flag = Tools.isPropertyEnabled(properties, "useIpv6ND");
         if (flag == null) {
             log.info("Using IPv6 Neighbor Discovery is not configured, " +
-                             "using current value of {}", ipv6NeighborDiscovery);
+                             "using current value of {}", useIpv6ND);
         } else {
-            ipv6NeighborDiscovery = flag;
+            useIpv6ND = flag;
             log.info("Configured. Using IPv6 Neighbor Discovery is {}",
-                     ipv6NeighborDiscovery ? "enabled" : "disabled");
+                     useIpv6ND ? "enabled" : "disabled");
+        }
+
+        flag = Tools.isPropertyEnabled(properties, "useDhcp");
+        if (flag == null) {
+            log.info("Using DHCP is not configured, " +
+                    "using current value of {}", useDhcp);
+        } else {
+            useDhcp = flag;
+            log.info("Configured. Using DHCP is {}",
+                    useDhcp ? "enabled" : "disabled");
         }
 
         flag = Tools.isPropertyEnabled(properties, "requestInterceptsEnabled");
@@ -332,7 +393,7 @@
 
     private class InternalHostProvider implements PacketProcessor {
         /**
-         * Update host location only.
+         * Updates host location only.
          *
          * @param hid  host ID
          * @param mac  source Mac address
@@ -350,7 +411,7 @@
         }
 
         /**
-         * Update host location and IP address.
+         * Updates host location and IP address.
          *
          * @param hid  host ID
          * @param mac  source Mac address
@@ -371,6 +432,28 @@
             }
         }
 
+        /**
+         * Updates host IP address for an existing host.
+         *
+         * @param hid host ID
+         * @param ip IP address
+         */
+        private void updateIp(HostId hid, IpAddress ip) {
+            Host host = hostService.getHost(hid);
+            if (host == null) {
+                log.debug("Fail to update IP for {}. Host does not exist");
+                return;
+            }
+
+            HostDescription desc =
+                    new DefaultHostDescription(hid.mac(), hid.vlanId(), host.location(), ip);
+            try {
+                providerService.hostDetected(hid, desc, false);
+            } catch (IllegalStateException e) {
+                log.debug("Host {} suppressed", hid);
+            }
+        }
+
         @Override
         public void process(PacketContext context) {
             if (context == null) {
@@ -412,7 +495,28 @@
                 updateLocationIP(hid, srcMac, vlan, hloc, ip);
 
             // IPv4: update location only
+            // DHCP ACK: additionally update IP of DHCP client
             } else if (eth.getEtherType() == Ethernet.TYPE_IPV4) {
+                IPacket pkt = eth.getPayload();
+                if (pkt != null && pkt instanceof IPv4) {
+                    pkt = pkt.getPayload();
+                    if (pkt != null && pkt instanceof UDP) {
+                        pkt = pkt.getPayload();
+                        if (pkt != null && pkt instanceof DHCP) {
+                            DHCP dhcp = (DHCP) pkt;
+                            if (dhcp.getOptions().stream()
+                                    .anyMatch(dhcpOption -> dhcpOption.getCode() ==
+                                            DHCP.DHCPOptionCode.OptionCode_MessageType.getValue() &&
+                                            dhcpOption.getLength() == 1 &&
+                                            dhcpOption.getData()[0] == DHCPPacketType.DHCPACK.getValue())) {
+                                MacAddress hostMac = MacAddress.valueOf(dhcp.getClientHardwareAddress());
+                                VlanId hostVlan = VlanId.vlanId(eth.getVlanID());
+                                HostId hostId = HostId.hostId(hostMac, hostVlan);
+                                updateIp(hostId, IpAddress.valueOf(dhcp.getYourIPAddress()));
+                            }
+                        }
+                    }
+                }
                 updateLocation(hid, srcMac, vlan, hloc);
 
             //
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 acf0a13..10a12ab 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
@@ -26,13 +26,19 @@
 import org.onlab.osgi.ComponentContextAdapter;
 import org.onlab.packet.ARP;
 import org.onlab.packet.ChassisId;
+import org.onlab.packet.DHCP;
+import org.onlab.packet.DHCPOption;
+import org.onlab.packet.DHCPPacketType;
 import org.onlab.packet.Ethernet;
+import org.onlab.packet.ICMP;
 import org.onlab.packet.ICMP6;
 import org.onlab.packet.IPv4;
 import org.onlab.packet.IPv6;
+import org.onlab.packet.Ip4Address;
 import org.onlab.packet.Ip6Address;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.MacAddress;
+import org.onlab.packet.UDP;
 import org.onlab.packet.VlanId;
 import org.onlab.packet.ndp.NeighborAdvertisement;
 import org.onlab.packet.ndp.NeighborSolicitation;
@@ -93,6 +99,7 @@
             new ProviderId("of", "org.onosproject.provider.host");
 
     private static final Integer INPORT = 10;
+    private static final Integer INPORT2 = 11;
     private static final String DEV1 = "of:1";
     private static final String DEV2 = "of:2";
     private static final String DEV3 = "of:3";
@@ -112,8 +119,8 @@
             new HostLocation(deviceId(DEV1), portNumber(INPORT), 0L);
     private static final DefaultHost HOST =
             new DefaultHost(PROVIDER_ID, hostId(MAC), MAC,
-                            vlanId(VlanId.UNTAGGED), LOCATION,
-                            ImmutableSet.of(IP_ADDRESS));
+                    VLAN, LOCATION,
+                    ImmutableSet.of(IP_ADDRESS));
 
     // IPv6 Host
     private static final MacAddress MAC2 = MacAddress.valueOf("00:00:22:00:00:02");
@@ -125,8 +132,21 @@
             new HostLocation(deviceId(DEV4), portNumber(INPORT), 0L);
     private static final DefaultHost HOST2 =
             new DefaultHost(PROVIDER_ID, hostId(MAC2), MAC2,
-                            vlanId(VlanId.UNTAGGED), LOCATION2,
-                            ImmutableSet.of(IP_ADDRESS2));
+                    VLAN, LOCATION2,
+                    ImmutableSet.of(IP_ADDRESS2));
+
+    // DHCP Server
+    private static final MacAddress MAC3 = MacAddress.valueOf("00:00:33:00:00:03");
+    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 =
+            new DefaultHost(PROVIDER_ID, hostId(MAC3), MAC3,
+                    VLAN, LOCATION3,
+                    ImmutableSet.of(IP_ADDRESS3));
 
     private static final ComponentContextAdapter CTX_FOR_REMOVE =
             new ComponentContextAdapter() {
@@ -234,12 +254,12 @@
         Device device = new DefaultDevice(ProviderId.NONE, deviceId(DEV1), SWITCH,
                                           "m", "h", "s", "n", new ChassisId(0L));
         deviceService.listener.event(new DeviceEvent(DEVICE_REMOVED, device));
-        assertEquals("incorrect remove count", 1, providerService.removeCount);
+        assertEquals("incorrect remove count", 2, providerService.removeCount);
 
         device = new DefaultDevice(ProviderId.NONE, deviceId(DEV4), SWITCH,
                                           "m", "h", "s", "n", new ChassisId(0L));
         deviceService.listener.event(new DeviceEvent(DEVICE_REMOVED, device));
-        assertEquals("incorrect remove count", 2, providerService.removeCount);
+        assertEquals("incorrect remove count", 3, providerService.removeCount);
     }
 
     @Test
@@ -251,12 +271,12 @@
         Device device = new DefaultDevice(ProviderId.NONE, deviceId(DEV1), SWITCH,
                                           "m", "h", "s", "n", new ChassisId(0L));
         deviceService.listener.event(new DeviceEvent(DEVICE_AVAILABILITY_CHANGED, device));
-        assertEquals("incorrect remove count", 1, providerService.removeCount);
+        assertEquals("incorrect remove count", 2, providerService.removeCount);
 
         device = new DefaultDevice(ProviderId.NONE, deviceId(DEV4), SWITCH,
                                           "m", "h", "s", "n", new ChassisId(0L));
         deviceService.listener.event(new DeviceEvent(DEVICE_AVAILABILITY_CHANGED, device));
-        assertEquals("incorrect remove count", 2, providerService.removeCount);
+        assertEquals("incorrect remove count", 3, providerService.removeCount);
     }
 
     @Test
@@ -309,6 +329,43 @@
     }
 
     /**
+     * When receiving DHCP REQUEST, update MAC, location of client.
+     * When receiving DHCP ACK, update MAC, location of server and IP of client.
+     */
+    @Test
+    public void testReceiveDhcp() {
+        // DHCP Request
+        testProcessor.process(new TestDhcpRequestPacketContext(DEV1));
+        assertThat("testReceiveDhcpRequest. 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(LOCATION));
+        assertThat(descr.hwAddress(), is(MAC));
+        assertThat(descr.ipAddress().size(), is(0));
+        assertThat(descr.vlan(), is(VLAN));
+
+        // DHCP Ack
+        testProcessor.process(new TestDhcpAckPacketContext(DEV1));
+        assertThat("testReceiveDhcpAck. Two additional host descriptions expected",
+                providerService.descriptions.size(), is(3));
+        // Should learn the IP of DHCP client
+        HostDescription descr2 = providerService.descriptions.get(1);
+        assertThat(descr2.location(), is(LOCATION));
+        assertThat(descr2.hwAddress(), is(MAC));
+        assertThat(descr2.ipAddress().size(), is(1));
+        assertTrue(descr2.ipAddress().contains(IP_ADDRESS));
+        assertThat(descr2.vlan(), is(VLAN));
+        // Should also learn the MAC, location of DHCP server
+        HostDescription descr3 = providerService.descriptions.get(2);
+        assertThat(descr3.location(), is(LOCATION3));
+        assertThat(descr3.hwAddress(), is(MAC3));
+        assertThat(descr3.ipAddress().size(), is(0));
+        assertThat(descr3.vlan(), is(VLAN));
+
+    }
+
+    /**
      * When receiving NeighborAdvertisement, updates location and IP.
      */
     @Test
@@ -533,7 +590,7 @@
     }
 
     /**
-     * Generates IPv6 Unicast packet.
+     * Generates IPv4 Unicast packet.
      */
     private class TestIpv4PacketContext implements PacketContext {
         private final String deviceId;
@@ -589,6 +646,152 @@
             return false;
         }
     }
+    /**
+     * Generates DHCP REQUEST packet.
+     */
+    private class TestDhcpRequestPacketContext implements PacketContext {
+        private final String deviceId;
+
+        public TestDhcpRequestPacketContext(String deviceId) {
+            this.deviceId = deviceId;
+        }
+
+        @Override
+        public long time() {
+            return 0;
+        }
+
+        @Override
+        public InboundPacket inPacket() {
+            byte[] dhcpMsgType = new byte[1];
+            dhcpMsgType[0] = (byte) DHCPPacketType.DHCPREQUEST.getValue();
+
+            DHCPOption dhcpOption = new DHCPOption();
+            dhcpOption.setCode(DHCP.DHCPOptionCode.OptionCode_MessageType.getValue());
+            dhcpOption.setData(dhcpMsgType);
+            dhcpOption.setLength((byte) 1);
+            DHCP dhcp = new DHCP();
+            dhcp.setOptions(Collections.singletonList(dhcpOption));
+            dhcp.setClientHardwareAddress(MAC.toBytes());
+            UDP udp = new UDP();
+            udp.setPayload(dhcp);
+            udp.setSourcePort(UDP.DHCP_CLIENT_PORT);
+            udp.setDestinationPort(UDP.DHCP_SERVER_PORT);
+            IPv4 ipv4 = new IPv4();
+            ipv4.setPayload(udp);
+            ipv4.setDestinationAddress(IP_ADDRESS3.toString());
+            ipv4.setSourceAddress(IP_ADDRESS.toString());
+            Ethernet eth = new Ethernet();
+            eth.setEtherType(Ethernet.TYPE_IPV4)
+                    .setVlanID(VLAN.toShort())
+                    .setSourceMACAddress(MAC)
+                    .setDestinationMACAddress(MAC3)
+                    .setPayload(ipv4);
+            ConnectPoint receivedFrom = new ConnectPoint(deviceId(deviceId),
+                    portNumber(INPORT));
+            return new DefaultInboundPacket(receivedFrom, eth,
+                    ByteBuffer.wrap(eth.serialize()));
+        }
+
+        @Override
+        public OutboundPacket outPacket() {
+            return null;
+        }
+
+        @Override
+        public TrafficTreatment.Builder treatmentBuilder() {
+            return null;
+        }
+
+        @Override
+        public void send() {
+
+        }
+
+        @Override
+        public boolean block() {
+            return false;
+        }
+
+        @Override
+        public boolean isHandled() {
+            return false;
+        }
+    }
+
+    /**
+     * Generates DHCP ACK packet.
+     */
+    private class TestDhcpAckPacketContext implements PacketContext {
+        private final String deviceId;
+
+        public TestDhcpAckPacketContext(String deviceId) {
+            this.deviceId = deviceId;
+        }
+
+        @Override
+        public long time() {
+            return 0;
+        }
+
+        @Override
+        public InboundPacket inPacket() {
+            byte[] dhcpMsgType = new byte[1];
+            dhcpMsgType[0] = (byte) DHCPPacketType.DHCPACK.getValue();
+
+            DHCPOption dhcpOption = new DHCPOption();
+            dhcpOption.setCode(DHCP.DHCPOptionCode.OptionCode_MessageType.getValue());
+            dhcpOption.setData(dhcpMsgType);
+            dhcpOption.setLength((byte) 1);
+            DHCP dhcp = new DHCP();
+            dhcp.setOptions(Collections.singletonList(dhcpOption));
+            dhcp.setClientHardwareAddress(MAC.toBytes());
+            dhcp.setYourIPAddress(IP_ADDRESS.getIp4Address().toInt());
+            UDP udp = new UDP();
+            udp.setPayload(dhcp);
+            udp.setSourcePort(UDP.DHCP_SERVER_PORT);
+            udp.setDestinationPort(UDP.DHCP_CLIENT_PORT);
+            IPv4 ipv4 = new IPv4();
+            ipv4.setPayload(udp);
+            ipv4.setDestinationAddress(IP_ADDRESS.toString());
+            ipv4.setSourceAddress(IP_ADDRESS3.toString());
+            Ethernet eth = new Ethernet();
+            eth.setEtherType(Ethernet.TYPE_IPV4)
+                    .setVlanID(VLAN.toShort())
+                    .setSourceMACAddress(MAC3)
+                    .setDestinationMACAddress(MAC)
+                    .setPayload(ipv4);
+            ConnectPoint receivedFrom = new ConnectPoint(deviceId(deviceId),
+                    portNumber(INPORT2));
+            return new DefaultInboundPacket(receivedFrom, eth,
+                    ByteBuffer.wrap(eth.serialize()));
+        }
+
+        @Override
+        public OutboundPacket outPacket() {
+            return null;
+        }
+
+        @Override
+        public TrafficTreatment.Builder treatmentBuilder() {
+            return null;
+        }
+
+        @Override
+        public void send() {
+
+        }
+
+        @Override
+        public boolean block() {
+            return false;
+        }
+
+        @Override
+        public boolean isHandled() {
+            return false;
+        }
+    }
 
     /**
      * Generates NeighborAdvertisement packet.
@@ -1035,10 +1238,13 @@
         public Set<Host> getConnectedHosts(ConnectPoint connectPoint) {
             ConnectPoint cp1 = new ConnectPoint(deviceId(DEV1), portNumber(INPORT));
             ConnectPoint cp2 = new ConnectPoint(deviceId(DEV4), portNumber(INPORT));
+            ConnectPoint cp3 = new ConnectPoint(deviceId(DEV1), portNumber(INPORT2));
             if (connectPoint.equals(cp1)) {
                 return ImmutableSet.of(HOST);
             } else if (connectPoint.equals(cp2)) {
                 return ImmutableSet.of(HOST2);
+            } else if (connectPoint.equals(cp3)) {
+                return ImmutableSet.of(HOST3);
             } else {
                 return ImmutableSet.of();
             }
@@ -1047,7 +1253,7 @@
         @Override
         public Set<Host> getConnectedHosts(DeviceId deviceId) {
             if (deviceId.equals(deviceId(DEV1))) {
-                return ImmutableSet.of(HOST);
+                return ImmutableSet.of(HOST, HOST3);
             } else if (deviceId.equals(deviceId(DEV4))) {
                 return ImmutableSet.of(HOST2);
             } else {
@@ -1055,5 +1261,17 @@
             }
         }
 
+        @Override
+        public Host getHost(HostId hostId) {
+            if (hostId.equals(HostId.hostId(MAC, VLAN))) {
+                return HOST;
+            } else if (hostId.equals(HostId.hostId(MAC2, VLAN))) {
+                return HOST2;
+            } else if (hostId.equals(HostId.hostId(MAC3, VLAN))) {
+                return HOST3;
+            }
+            return null;
+        }
+
     }
 }