Implement DHCPv4 Lease Query

Change-Id: Ic24b2825bbb359e4044d59e0eaaa1097d0d5351c
diff --git a/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp4HandlerImpl.java b/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp4HandlerImpl.java
index d15a34a..d49b9a9 100644
--- a/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp4HandlerImpl.java
+++ b/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp4HandlerImpl.java
@@ -362,6 +362,8 @@
                     handleDhcpDiscoverAndRequest(ethernetPacketRequest, dhcpPayload);
                 }
                 break;
+            case DHCPDECLINE:
+                break;
             case DHCPACK:
                 // reply to dhcp client.
                 Ethernet ethernetPacketAck = processDhcpPacketFromServer(packet);
@@ -371,9 +373,25 @@
                     sendResponseToClient(ethernetPacketAck, dhcpPayload);
                 }
                 break;
+            case DHCPNAK:
+                break;
             case DHCPRELEASE:
                 // TODO: release the ip address from client
                 break;
+            case DHCPINFORM:
+                break;
+            case DHCPFORCERENEW:
+                break;
+            case DHCPLEASEQUERY:
+                handleLeaseQueryMsg(context, packet, dhcpPayload);
+                break;
+            case DHCPLEASEACTIVE:
+                handleLeaseQueryActivateMsg(packet, dhcpPayload);
+                break;
+            case DHCPLEASEUNASSIGNED:
+            case DHCPLEASEUNKNOWN:
+                handleLeaseQueryUnknown(packet, dhcpPayload);
+                break;
             default:
                 break;
         }
@@ -455,6 +473,156 @@
         return iface.vlanTagged().contains(vlanId);
     }
 
+    private void handleLeaseQueryActivateMsg(Ethernet packet, DHCP dhcpPayload) {
+        log.debug("LQ: Got DHCPLEASEACTIVE packet!");
+
+        // TODO: release the ip address from client
+        MacAddress clientMacAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
+        VlanId vlanId = VlanId.vlanId(packet.getVlanID());
+        HostId hostId = HostId.hostId(clientMacAddress, vlanId);
+        DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
+
+        if (record == null) {
+            log.warn("Can't find record for host {} when processing DHCPLEASEACTIVE", hostId);
+            return;
+        }
+
+        // need to update routes
+        log.debug("Lease Query for Client results in DHCPLEASEACTIVE - route needs to be modified");
+        // get current route
+        // find the ip of that client with the DhcpRelay store
+
+        Ip4Address clientIP = record.ip4Address().orElse(null);
+        log.debug("LQ: IP of host is " + clientIP.getIp4Address());
+
+        MacAddress nextHopMac = record.nextHop().orElse(null);
+        log.debug("LQ: MAC of resulting *OLD* NH for that host is " + nextHopMac.toString());
+
+        // find the new NH by looking at the src MAC of the dhcp request
+        // from the LQ store
+        MacAddress newNextHopMac = record.nextHopTemp().orElse(null);
+        log.debug("LQ: MAC of resulting *NEW* NH for that host is " + newNextHopMac.toString());
+
+        log.debug("LQ: updating dhcp relay record with new NH");
+        record.nextHop(newNextHopMac);
+
+        // find the next hop IP from its mac
+        HostId gwHostId = HostId.hostId(newNextHopMac, vlanId);
+        Host gwHost = hostService.getHost(gwHostId);
+
+        if (gwHost == null) {
+            log.warn("Can't find gateway for new NH host " + gwHostId);
+            return;
+        }
+
+        Ip4Address nextHopIp = gwHost.ipAddresses()
+                .stream()
+                .filter(IpAddress::isIp4)
+                .map(IpAddress::getIp4Address)
+                .findFirst()
+                .orElse(null);
+
+        if (nextHopIp == null) {
+            log.warn("Can't find IP address of gateway " + gwHost);
+            return;
+        }
+
+        log.debug("LQ: *NEW* NH IP for host is " + nextHopIp.getIp4Address());
+        Route route = new Route(Route.Source.STATIC, clientIP.toIpPrefix(), nextHopIp);
+        routeStore.updateRoute(route);
+
+        // and forward to client
+        Ethernet ethernetPacket = processLeaseQueryFromServer(packet);
+        if (ethernetPacket != null) {
+            sendResponseToClient(ethernetPacket, dhcpPayload);
+        }
+    }
+
+    /**
+     *
+     */
+    private void handleLeaseQueryMsg(PacketContext context, Ethernet packet, DHCP dhcpPayload) {
+        log.debug("LQ: Got DHCPLEASEQUERY packet!");
+        MacAddress clientMacAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
+        log.debug("LQ: got DHCPLEASEQUERY with MAC " + clientMacAddress.toString());
+        // add the client mac (hostid) of this request to a store (the entry will be removed with
+        // the reply sent to the originator)
+        VlanId vlanId = VlanId.vlanId(packet.getVlanID());
+        HostId hId = HostId.hostId(clientMacAddress, vlanId);
+        DhcpRecord record = dhcpRelayStore.getDhcpRecord(hId).orElse(null);
+        if (record != null) {
+            //new NH is to be taken from src mac of LQ packet
+            MacAddress newNextHop = packet.getSourceMAC();
+            record.nextHopTemp(newNextHop);
+            record.ip4Status(dhcpPayload.getPacketType());
+            record.updateLastSeen();
+
+            // do a basic routing of the packet (this is unicast routing
+            // not a relay operation like for other broadcast dhcp packets
+            Ethernet ethernetPacketLQ = processLeaseQueryFromAgent(context, packet);
+            // and forward to server
+            handleDhcpDiscoverAndRequest(ethernetPacketLQ, dhcpPayload);
+        } else {
+            log.warn("LQ: Error! - DHCP relay record for that client not found - ignoring LQ!");
+        }
+    }
+
+    private void handleLeaseQueryUnknown(Ethernet packet, DHCP dhcpPayload) {
+        log.debug("Lease Query for Client results in DHCPLEASEUNASSIGNED or " +
+                          "DHCPLEASEUNKNOWN - removing route & forwarding reply to originator");
+        MacAddress clientMacAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
+        VlanId vlanId = VlanId.vlanId(packet.getVlanID());
+        HostId hostId = HostId.hostId(clientMacAddress, vlanId);
+        DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
+
+        if (record == null) {
+            log.warn("Can't find record for host {} when handling LQ UNKNOWN/UNASSIGNED message", hostId);
+            return;
+        }
+
+        Ip4Address clientIP = record.ip4Address().orElse(null);
+        log.debug("LQ: IP of host is " + clientIP.getIp4Address());
+
+        // find the new NH by looking at the src MAC of the dhcp request
+        // from the LQ store
+        MacAddress nextHopMac = record.nextHop().orElse(null);
+        log.debug("LQ: MAC of resulting *Existing* NH for that route is " + nextHopMac.toString());
+
+        // find the next hop IP from its mac
+        HostId gwHostId = HostId.hostId(nextHopMac, vlanId);
+        Host gwHost = hostService.getHost(gwHostId);
+
+        if (gwHost == null) {
+            log.warn("Can't find gateway for new NH host " + gwHostId);
+            return;
+        }
+
+        Ip4Address nextHopIp = gwHost.ipAddresses()
+                .stream()
+                .filter(IpAddress::isIp4)
+                .map(IpAddress::getIp4Address)
+                .findFirst()
+                .orElse(null);
+
+        if (nextHopIp == null) {
+            log.warn("Can't find IP address of gateway {}", gwHost);
+            return;
+        }
+
+        log.debug("LQ: *Existing* NH IP for host is " + nextHopIp.getIp4Address() + " removing route for it");
+        Route route = new Route(Route.Source.STATIC, clientIP.toIpPrefix(), nextHopIp);
+        routeStore.removeRoute(route);
+
+        // remove from temp store
+        dhcpRelayStore.removeDhcpRecord(hostId);
+
+        // and forward to client
+        Ethernet ethernetPacket = processLeaseQueryFromServer(packet);
+        if (ethernetPacket != null) {
+            sendResponseToClient(ethernetPacket, dhcpPayload);
+        }
+    }
+
     /**
      * Build the DHCP discover/request packet with gateway IP(unicast packet).
      *
@@ -470,7 +638,6 @@
         UDP udpPacket = (UDP) ipv4Packet.getPayload();
         DHCP dhcpPacket = (DHCP) udpPacket.getPayload();
 
-
         VlanId dhcpConnectVlan = null;
         MacAddress dhcpConnectMac = null;
         Ip4Address dhcpServerIp = null;
@@ -611,6 +778,102 @@
         return etherReply;
     }
 
+
+    /**
+     * Do a basic routing for a packet from client (used for LQ processing).
+     *
+     * @param context the packet context
+     * @param ethernetPacket the ethernet payload to process
+     * @return processed packet
+     */
+    private Ethernet processLeaseQueryFromAgent(PacketContext context,
+                                                Ethernet ethernetPacket) {
+        // get dhcp header.
+        Ethernet etherReply = (Ethernet) ethernetPacket.clone();
+        IPv4 ipv4Packet = (IPv4) etherReply.getPayload();
+        UDP udpPacket = (UDP) ipv4Packet.getPayload();
+        DHCP dhcpPacket = (DHCP) udpPacket.getPayload();
+
+        VlanId dhcpConnectVlan = null;
+        MacAddress dhcpConnectMac = null;
+        Ip4Address dhcpServerIp = null;
+
+        VlanId indirectDhcpConnectVlan = null;
+        MacAddress indirectDhcpConnectMac = null;
+        Ip4Address indirectDhcpServerIp = null;
+
+        if (!defaultServerInfoList.isEmpty()) {
+            DhcpServerInfo serverInfo = defaultServerInfoList.get(0);
+            dhcpConnectVlan = serverInfo.getDhcpConnectVlan().orElse(null);
+            dhcpConnectMac = serverInfo.getDhcpConnectMac().orElse(null);
+            dhcpServerIp = serverInfo.getDhcpServerIp4().orElse(null);
+        }
+
+        if (!indirectServerInfoList.isEmpty()) {
+            DhcpServerInfo indirectServerInfo = indirectServerInfoList.get(0);
+            indirectDhcpConnectVlan = indirectServerInfo.getDhcpConnectVlan().orElse(null);
+            indirectDhcpConnectMac = indirectServerInfo.getDhcpConnectMac().orElse(null);
+            indirectDhcpServerIp = indirectServerInfo.getDhcpServerIp4().orElse(null);
+        }
+
+        Ip4Address clientInterfaceIp =
+                interfaceService.getInterfacesByPort(context.inPacket().receivedFrom())
+                        .stream()
+                        .map(Interface::ipAddressesList)
+                        .flatMap(Collection::stream)
+                        .map(InterfaceIpAddress::ipAddress)
+                        .filter(IpAddress::isIp4)
+                        .map(IpAddress::getIp4Address)
+                        .findFirst()
+                        .orElse(null);
+        if (clientInterfaceIp == null) {
+            log.warn("Can't find interface IP for client interface for port {}",
+                     context.inPacket().receivedFrom());
+            return null;
+        }
+        boolean isDirectlyConnected = directlyConnected(dhcpPacket);
+        Interface serverInterface = isDirectlyConnected ? getDefaultServerInterface() : getIndirectServerInterface();
+        if (serverInterface == null) {
+            log.warn("Can't get {} server interface, ignore", isDirectlyConnected ? "direct" : "indirect");
+            return null;
+        }
+        Ip4Address ipFacingServer = getFirstIpFromInterface(serverInterface);
+        MacAddress macFacingServer = serverInterface.mac();
+        if (ipFacingServer == null || macFacingServer == null) {
+            log.warn("No IP address for server Interface {}", serverInterface);
+            return null;
+        }
+        if (dhcpConnectMac == null) {
+            log.warn("DHCP server/gateway not yet resolved .. Aborting DHCP "
+                             + "packet processing from client on port: {}",
+                     context.inPacket().receivedFrom());
+            return null;
+        }
+
+        etherReply.setSourceMACAddress(macFacingServer);
+        etherReply.setDestinationMACAddress(dhcpConnectMac);
+        etherReply.setVlanID(dhcpConnectVlan.toShort());
+        ipv4Packet.setSourceAddress(ipFacingServer.toInt());
+        ipv4Packet.setDestinationAddress(dhcpServerIp.toInt());
+
+        if (indirectDhcpServerIp != null) {
+            // Indirect case, replace destination to indirect dhcp server if exist
+            etherReply.setDestinationMACAddress(indirectDhcpConnectMac);
+            etherReply.setVlanID(indirectDhcpConnectVlan.toShort());
+            ipv4Packet.setDestinationAddress(indirectDhcpServerIp.toInt());
+        }
+
+        udpPacket.setPayload(dhcpPacket);
+        // As a DHCP relay, the source port should be server port( instead
+        // of client port.
+        udpPacket.setSourcePort(UDP.DHCP_SERVER_PORT);
+        udpPacket.setDestinationPort(UDP.DHCP_SERVER_PORT);
+        ipv4Packet.setPayload(udpPacket);
+        etherReply.setPayload(ipv4Packet);
+        return etherReply;
+    }
+
+
     /**
      * Writes DHCP record to the store according to the request DHCP packet (Discover, Request).
      *
@@ -762,6 +1025,66 @@
     }
 
     /**
+     * Build the DHCP offer/ack with proper client port.
+     *
+     * @param ethernetPacket the original packet comes from server
+     * @return new packet which will send to the client
+     */
+    private Ethernet processLeaseQueryFromServer(Ethernet ethernetPacket) {
+        // get dhcp header.
+        Ethernet etherReply = (Ethernet) ethernetPacket.clone();
+        IPv4 ipv4Packet = (IPv4) etherReply.getPayload();
+        UDP udpPacket = (UDP) ipv4Packet.getPayload();
+        DHCP dhcpPayload = (DHCP) udpPacket.getPayload();
+
+        // determine the vlanId of the client host - note that this vlan id
+        // could be different from the vlan in the packet from the server
+        Interface clientInterface = getClientInterface(ethernetPacket, dhcpPayload).orElse(null);
+
+        if (clientInterface == null) {
+            log.warn("Cannot find the interface for the DHCP {}", dhcpPayload);
+            return null;
+        }
+        VlanId vlanId;
+        if (clientInterface.vlanTagged().isEmpty()) {
+            vlanId = clientInterface.vlan();
+        } else {
+            // might be multiple vlan in same interface
+            vlanId = getVlanIdFromRelayAgentOption(dhcpPayload);
+        }
+        if (vlanId == null) {
+            vlanId = VlanId.NONE;
+        }
+        etherReply.setVlanID(vlanId.toShort());
+        etherReply.setSourceMACAddress(clientInterface.mac());
+
+        if (!directlyConnected(dhcpPayload)) {
+            // if client is indirectly connected, try use next hop mac address
+            MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
+            HostId hostId = HostId.hostId(macAddress, vlanId);
+            DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
+            if (record != null) {
+                // if next hop can be found, use mac address of next hop
+                record.nextHop().ifPresent(etherReply::setDestinationMACAddress);
+            } else {
+                // otherwise, discard the packet
+                log.warn("Can't find record for host id {}, discard packet", hostId);
+                return null;
+            }
+        } else {
+            etherReply.setDestinationMACAddress(dhcpPayload.getClientHardwareAddress());
+        }
+
+        // default is client port
+        udpPacket.setSourcePort(UDP.DHCP_SERVER_PORT);
+        udpPacket.setDestinationPort(UDP.DHCP_CLIENT_PORT);
+
+        udpPacket.setPayload(dhcpPayload);
+        ipv4Packet.setPayload(udpPacket);
+        etherReply.setPayload(ipv4Packet);
+        return etherReply;
+    }
+    /**
      * Extracts VLAN ID from relay agent option.
      *
      * @param dhcpPayload the DHCP payload
diff --git a/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/store/DhcpRecord.java b/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/store/DhcpRecord.java
index 029c2fc..51d8f98 100644
--- a/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/store/DhcpRecord.java
+++ b/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/store/DhcpRecord.java
@@ -41,6 +41,9 @@
     private final MacAddress macAddress;
     private final VlanId vlanId;
     private MacAddress nextHop;
+    // this will hold the potential next hop change in case
+    // of successfull LQ from another NH for a certain IP
+    private MacAddress nextHopTemp;
 
     private Ip4Address ip4Address;
     private DHCP.MsgType ip4Status;
@@ -256,6 +259,26 @@
     }
 
     /**
+     * Gets temporary nextHop mac address.
+     *
+     * @return the IPv4 nextHop mac address; empty if not exists
+     */
+    public Optional<MacAddress> nextHopTemp() {
+        return Optional.ofNullable(nextHopTemp);
+    }
+
+    /**
+     * Sets temporary nextHop mac address.
+     *
+     * @param nextHop the IPv4 nextHop mac address
+     * @return the DHCP record
+     */
+    public DhcpRecord nextHopTemp(MacAddress nextHop) {
+        this.nextHopTemp = nextHop;
+        return this;
+    }
+
+    /**
      * Clone this DHCP record.
      *
      * @return the DHCP record which cloned