[CORD-1614] Refactor DHCP relay app

Change-Id: Id4a281526aa5469abe5732e5d2d42d34074907e9
diff --git a/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp4HandlerImpl.java b/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp4HandlerImpl.java
new file mode 100644
index 0000000..9abe49e
--- /dev/null
+++ b/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp4HandlerImpl.java
@@ -0,0 +1,670 @@
+/*
+ * Copyright 2017-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.onosproject.dhcprelay;
+
+import com.google.common.collect.Sets;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.packet.BasePacket;
+import org.onlab.packet.DHCP;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.UDP;
+import org.onlab.packet.VlanId;
+import org.onlab.packet.dhcp.CircuitId;
+import org.onlab.packet.dhcp.DhcpOption;
+import org.onlab.packet.dhcp.DhcpRelayAgentOption;
+import org.onosproject.dhcprelay.api.DhcpHandler;
+import org.onosproject.dhcprelay.store.DhcpRecord;
+import org.onosproject.dhcprelay.store.DhcpRelayStore;
+import org.onosproject.incubator.net.intf.Interface;
+import org.onosproject.incubator.net.intf.InterfaceService;
+import org.onosproject.incubator.net.routing.Route;
+import org.onosproject.incubator.net.routing.RouteStore;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.DefaultHostDescription;
+import org.onosproject.net.host.HostDescription;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.host.HostStore;
+import org.onosproject.net.host.InterfaceIpAddress;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.packet.PacketService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_CircuitID;
+import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_END;
+import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_MessageType;
+import static org.onlab.packet.MacAddress.valueOf;
+import static org.onlab.packet.dhcp.DhcpRelayAgentOption.RelayAgentInfoOptions.CIRCUIT_ID;
+
+@Component
+@Service
+@Property(name = "version", value = "4")
+public class Dhcp4HandlerImpl implements DhcpHandler {
+    private static Logger log = LoggerFactory.getLogger(Dhcp4HandlerImpl.class);
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DhcpRelayStore dhcpRelayStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected PacketService packetService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostStore hostStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected RouteStore routeStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected InterfaceService interfaceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostService hostService;
+
+    private Ip4Address dhcpServerIp = null;
+    // dhcp server may be connected directly to the SDN network or
+    // via an external gateway. When connected directly, the dhcpConnectPoint, dhcpConnectMac,
+    // and dhcpConnectVlan refer to the server. When connected via the gateway, they refer
+    // to the gateway.
+    private ConnectPoint dhcpServerConnectPoint = null;
+    private MacAddress dhcpConnectMac = null;
+    private VlanId dhcpConnectVlan = null;
+    private Ip4Address dhcpGatewayIp = null;
+
+    @Override
+    public void setDhcpServerIp(IpAddress dhcpServerIp) {
+        checkNotNull(dhcpServerIp, "DHCP server IP can't be null");
+        checkState(dhcpServerIp.isIp4(), "Invalid server IP for DHCPv4 relay handler");
+        this.dhcpServerIp = dhcpServerIp.getIp4Address();
+    }
+
+    @Override
+    public void setDhcpServerConnectPoint(ConnectPoint dhcpServerConnectPoint) {
+        checkNotNull(dhcpServerConnectPoint, "Server connect point can't null");
+        this.dhcpServerConnectPoint = dhcpServerConnectPoint;
+    }
+
+    @Override
+    public void setDhcpConnectMac(MacAddress dhcpConnectMac) {
+        this.dhcpConnectMac = dhcpConnectMac;
+    }
+
+    @Override
+    public void setDhcpConnectVlan(VlanId dhcpConnectVlan) {
+        this.dhcpConnectVlan = dhcpConnectVlan;
+    }
+
+    @Override
+    public void setDhcpGatewayIp(IpAddress dhcpGatewayIp) {
+        if (dhcpGatewayIp != null) {
+            checkState(dhcpGatewayIp.isIp4(), "Invalid gateway IP for DHCPv4 relay handler");
+            this.dhcpGatewayIp = dhcpGatewayIp.getIp4Address();
+        } else {
+            // removes gateway config
+            this.dhcpGatewayIp = null;
+        }
+    }
+
+    @Override
+    public Optional<IpAddress> getDhcpServerIp() {
+        return Optional.ofNullable(dhcpServerIp);
+    }
+
+    @Override
+    public Optional<IpAddress> getDhcpGatewayIp() {
+        return Optional.ofNullable(dhcpGatewayIp);
+    }
+
+    @Override
+    public Optional<MacAddress> getDhcpConnectMac() {
+        return Optional.ofNullable(dhcpConnectMac);
+    }
+
+    @Override
+    public void processDhcpPacket(PacketContext context, BasePacket payload) {
+        checkNotNull(payload, "DHCP payload can't be null");
+        checkState(payload instanceof DHCP, "Payload is not a DHCP");
+        DHCP dhcpPayload = (DHCP) payload;
+        if (!configured()) {
+            log.warn("Missing DHCP relay server config. Abort packet processing");
+            return;
+        }
+
+        ConnectPoint inPort = context.inPacket().receivedFrom();
+        Set<Interface> clientServerInterfaces = interfaceService.getInterfacesByPort(inPort);
+        // ignore the packets if dhcp client interface is not configured on onos.
+        if (clientServerInterfaces.isEmpty()) {
+            log.warn("Virtual interface is not configured on {}", inPort);
+            return;
+        }
+        checkNotNull(dhcpPayload, "Can't find DHCP payload");
+        Ethernet packet = context.inPacket().parsed();
+        DHCP.MsgType incomingPacketType = dhcpPayload.getOptions().stream()
+                .filter(dhcpOption -> dhcpOption.getCode() == OptionCode_MessageType.getValue())
+                .map(DhcpOption::getData)
+                .map(data -> DHCP.MsgType.getType(data[0]))
+                .findFirst()
+                .orElse(null);
+        checkNotNull(incomingPacketType, "Can't get message type from DHCP payload {}", dhcpPayload);
+        switch (incomingPacketType) {
+            case DHCPDISCOVER:
+                // add the gatewayip as virtual interface ip for server to understand
+                // the lease to be assigned and forward the packet to dhcp server.
+                Ethernet ethernetPacketDiscover =
+                        processDhcpPacketFromClient(context, packet, clientServerInterfaces);
+
+                if (ethernetPacketDiscover != null) {
+                    writeRequestDhcpRecord(inPort, packet, dhcpPayload);
+                    handleDhcpDiscoverAndRequest(ethernetPacketDiscover);
+                }
+                break;
+            case DHCPOFFER:
+                //reply to dhcp client.
+                Ethernet ethernetPacketOffer = processDhcpPacketFromServer(packet);
+                if (ethernetPacketOffer != null) {
+                    writeResponseDhcpRecord(ethernetPacketOffer, dhcpPayload);
+                    handleDhcpOffer(ethernetPacketOffer, dhcpPayload);
+                }
+                break;
+            case DHCPREQUEST:
+                // add the gateway ip as virtual interface ip for server to understand
+                // the lease to be assigned and forward the packet to dhcp server.
+                Ethernet ethernetPacketRequest =
+                        processDhcpPacketFromClient(context, packet, clientServerInterfaces);
+                if (ethernetPacketRequest != null) {
+                    writeRequestDhcpRecord(inPort, packet, dhcpPayload);
+                    handleDhcpDiscoverAndRequest(ethernetPacketRequest);
+                }
+                break;
+            case DHCPACK:
+                // reply to dhcp client.
+                Ethernet ethernetPacketAck = processDhcpPacketFromServer(packet);
+                if (ethernetPacketAck != null) {
+                    writeResponseDhcpRecord(ethernetPacketAck, dhcpPayload);
+                    handleDhcpAck(ethernetPacketAck, dhcpPayload);
+                }
+                break;
+            case DHCPRELEASE:
+                // TODO: release the ip address from client
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Checks if this app has been configured.
+     *
+     * @return true if all information we need have been initialized
+     */
+    public boolean configured() {
+        return dhcpServerConnectPoint != null && dhcpServerIp != null;
+    }
+
+    /**
+     * Returns the first interface ip out of a set of interfaces or null.
+     *
+     * @param intfs interfaces of one connect port
+     * @return the first interface IP; null if not exists an IP address in
+     *         these interfaces
+     */
+    private Ip4Address getRelayAgentIPv4Address(Set<Interface> intfs) {
+        return intfs.stream()
+                .map(Interface::ipAddressesList)
+                .flatMap(List::stream)
+                .map(InterfaceIpAddress::ipAddress)
+                .filter(IpAddress::isIp4)
+                .map(IpAddress::getIp4Address)
+                .findFirst()
+                .orElse(null);
+    }
+
+    /**
+     * Build the DHCP discover/request packet with gateway IP(unicast packet).
+     *
+     * @param context the packet context
+     * @param ethernetPacket the ethernet payload to process
+     * @param clientInterfaces interfaces which belongs to input port
+     * @return processed packet
+     */
+    private Ethernet processDhcpPacketFromClient(PacketContext context,
+                                                 Ethernet ethernetPacket,
+                                                 Set<Interface> clientInterfaces) {
+        Ip4Address relayAgentIp = getRelayAgentIPv4Address(clientInterfaces);
+        MacAddress relayAgentMac = clientInterfaces.iterator().next().mac();
+        if (relayAgentIp == null || relayAgentMac == null) {
+            log.warn("Missing DHCP relay agent interface Ipv4 addr config for "
+                             + "packet from client on port: {}. Aborting packet processing",
+                     clientInterfaces.iterator().next().connectPoint());
+            return null;
+        }
+        if (dhcpConnectMac == null) {
+            log.warn("DHCP {} not yet resolved .. Aborting DHCP "
+                             + "packet processing from client on port: {}",
+                     (dhcpGatewayIp == null) ? "server IP " + dhcpServerIp
+                             : "gateway IP " + dhcpGatewayIp,
+                     clientInterfaces.iterator().next().connectPoint());
+            return null;
+        }
+        // get dhcp header.
+        Ethernet etherReply = (Ethernet) ethernetPacket.clone();
+        etherReply.setSourceMACAddress(relayAgentMac);
+        etherReply.setDestinationMACAddress(dhcpConnectMac);
+        etherReply.setVlanID(dhcpConnectVlan.toShort());
+        IPv4 ipv4Packet = (IPv4) etherReply.getPayload();
+        ipv4Packet.setSourceAddress(relayAgentIp.toInt());
+        ipv4Packet.setDestinationAddress(dhcpServerIp.toInt());
+        UDP udpPacket = (UDP) ipv4Packet.getPayload();
+        DHCP dhcpPacket = (DHCP) udpPacket.getPayload();
+
+        // If there is no relay agent option(option 82), add one to DHCP payload
+        boolean containsRelayAgentOption = dhcpPacket.getOptions().stream()
+                .map(DhcpOption::getCode)
+                .anyMatch(code -> code == OptionCode_CircuitID.getValue());
+
+        if (!containsRelayAgentOption) {
+            ConnectPoint inPort = context.inPacket().receivedFrom();
+            VlanId vlanId = VlanId.vlanId(ethernetPacket.getVlanID());
+            // add connected in port and vlan
+            CircuitId cid = new CircuitId(inPort.toString(), vlanId);
+            byte[] circuitId = cid.serialize();
+            DhcpOption circuitIdSubOpt = new DhcpOption();
+            circuitIdSubOpt
+                    .setCode(CIRCUIT_ID.getValue())
+                    .setLength((byte) circuitId.length)
+                    .setData(circuitId);
+
+            DhcpRelayAgentOption newRelayAgentOpt = new DhcpRelayAgentOption();
+            newRelayAgentOpt.setCode(OptionCode_CircuitID.getValue());
+            newRelayAgentOpt.addSubOption(circuitIdSubOpt);
+
+            // Removes END option  first
+            List<DhcpOption> options = dhcpPacket.getOptions().stream()
+                    .filter(opt -> opt.getCode() != OptionCode_END.getValue())
+                    .collect(Collectors.toList());
+
+            // push relay agent option
+            options.add(newRelayAgentOpt);
+
+            // make sure option 255(End) is the last option
+            DhcpOption endOption = new DhcpOption();
+            endOption.setCode(OptionCode_END.getValue());
+            options.add(endOption);
+
+            dhcpPacket.setOptions(options);
+            dhcpPacket.setGatewayIPAddress(relayAgentIp.toInt());
+        }
+
+        udpPacket.setPayload(dhcpPacket);
+        udpPacket.setSourcePort(UDP.DHCP_CLIENT_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).
+     *
+     * @param location the location which DHCP packet comes from
+     * @param ethernet the DHCP packet
+     * @param dhcpPayload the DHCP payload
+     */
+    private void writeRequestDhcpRecord(ConnectPoint location,
+                                        Ethernet ethernet,
+                                        DHCP dhcpPayload) {
+        VlanId vlanId = VlanId.vlanId(ethernet.getVlanID());
+        MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
+        HostId hostId = HostId.hostId(macAddress, vlanId);
+        DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
+        if (record == null) {
+            record = new DhcpRecord(HostId.hostId(macAddress, vlanId));
+        } else {
+            record = record.clone();
+        }
+        record.addLocation(new HostLocation(location, System.currentTimeMillis()));
+        record.ip4Status(dhcpPayload.getPacketType());
+        record.setDirectlyConnected(directlyConnected(dhcpPayload));
+        if (!directlyConnected(dhcpPayload)) {
+            // Update gateway mac address if the host is not directly connected
+            record.nextHop(ethernet.getSourceMAC());
+        }
+        record.updateLastSeen();
+        dhcpRelayStore.updateDhcpRecord(HostId.hostId(macAddress, vlanId), record);
+    }
+
+    /**
+     * Writes DHCP record to the store according to the response DHCP packet (Offer, Ack).
+     *
+     * @param ethernet the DHCP packet
+     * @param dhcpPayload the DHCP payload
+     */
+    private void writeResponseDhcpRecord(Ethernet ethernet,
+                                         DHCP dhcpPayload) {
+        Optional<Interface> outInterface = getOutputInterface(ethernet, dhcpPayload);
+        if (!outInterface.isPresent()) {
+            log.warn("Failed to determine where to send {}", dhcpPayload.getPacketType());
+            return;
+        }
+
+        Interface outIface = outInterface.get();
+        ConnectPoint location = outIface.connectPoint();
+        VlanId vlanId = outIface.vlan();
+        MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
+        HostId hostId = HostId.hostId(macAddress, vlanId);
+        DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
+        if (record == null) {
+            record = new DhcpRecord(HostId.hostId(macAddress, vlanId));
+        } else {
+            record = record.clone();
+        }
+        record.addLocation(new HostLocation(location, System.currentTimeMillis()));
+        if (dhcpPayload.getPacketType() == DHCP.MsgType.DHCPACK) {
+            record.ip4Address(Ip4Address.valueOf(dhcpPayload.getYourIPAddress()));
+        }
+        record.ip4Status(dhcpPayload.getPacketType());
+        record.setDirectlyConnected(directlyConnected(dhcpPayload));
+        record.updateLastSeen();
+        dhcpRelayStore.updateDhcpRecord(HostId.hostId(macAddress, vlanId), record);
+    }
+
+    /**
+     * 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 processDhcpPacketFromServer(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 outInterface = getOutputInterface(ethernetPacket, dhcpPayload).orElse(null);
+
+        if (outInterface == null) {
+            log.warn("Cannot find the interface for the DHCP {}", dhcpPayload);
+            return null;
+        }
+
+        etherReply.setDestinationMACAddress(dhcpPayload.getClientHardwareAddress());
+        etherReply.setVlanID(outInterface.vlan().toShort());
+        // we leave the srcMac from the original packet
+
+        // figure out the relay agent IP corresponding to the original request
+        Ip4Address relayAgentIP = getRelayAgentIPv4Address(
+                interfaceService.getInterfacesByPort(outInterface.connectPoint()));
+        if (relayAgentIP == null) {
+            log.warn("Cannot determine relay agent interface Ipv4 addr for host {}/{}. "
+                             + "Aborting relay for dhcp packet from server {}",
+                     etherReply.getDestinationMAC(), outInterface.vlan(),
+                     ethernetPacket);
+            return null;
+        }
+        // SRC_IP: relay agent IP
+        // DST_IP: offered IP
+        ipv4Packet.setSourceAddress(relayAgentIP.toInt());
+        ipv4Packet.setDestinationAddress(dhcpPayload.getYourIPAddress());
+        udpPacket.setSourcePort(UDP.DHCP_SERVER_PORT);
+        if (directlyConnected(dhcpPayload)) {
+            udpPacket.setDestinationPort(UDP.DHCP_CLIENT_PORT);
+        } else {
+            // forward to another dhcp relay
+            udpPacket.setDestinationPort(UDP.DHCP_SERVER_PORT);
+        }
+
+        udpPacket.setPayload(dhcpPayload);
+        ipv4Packet.setPayload(udpPacket);
+        etherReply.setPayload(ipv4Packet);
+        return etherReply;
+    }
+
+
+    /**
+     * Check if the host is directly connected to the network or not.
+     *
+     * @param dhcpPayload the dhcp payload
+     * @return true if the host is directly connected to the network; false otherwise
+     */
+    private boolean directlyConnected(DHCP dhcpPayload) {
+        DhcpOption relayAgentOption = dhcpPayload.getOption(OptionCode_CircuitID);
+
+        // Doesn't contains relay option
+        if (relayAgentOption == null) {
+            return true;
+        }
+
+        IpAddress gatewayIp = IpAddress.valueOf(dhcpPayload.getGatewayIPAddress());
+        Set<Interface> gatewayInterfaces = interfaceService.getInterfacesByIp(gatewayIp);
+
+        // Contains relay option, and added by ONOS
+        if (!gatewayInterfaces.isEmpty()) {
+            return true;
+        }
+
+        // Relay option added by other relay agent
+        return false;
+    }
+
+
+    /**
+     * Send the DHCP ack to the requester host.
+     * Modify Host or Route store according to the type of DHCP.
+     *
+     * @param ethernetPacketAck the packet
+     * @param dhcpPayload the DHCP data
+     */
+    private void handleDhcpAck(Ethernet ethernetPacketAck, DHCP dhcpPayload) {
+        Optional<Interface> outInterface = getOutputInterface(ethernetPacketAck, dhcpPayload);
+        if (!outInterface.isPresent()) {
+            log.warn("Can't find output interface for dhcp: {}", dhcpPayload);
+            return;
+        }
+
+        Interface outIface = outInterface.get();
+        HostLocation hostLocation = new HostLocation(outIface.connectPoint(), System.currentTimeMillis());
+        MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
+        VlanId vlanId = outIface.vlan();
+        HostId hostId = HostId.hostId(macAddress, vlanId);
+        Ip4Address ip = Ip4Address.valueOf(dhcpPayload.getYourIPAddress());
+
+        if (directlyConnected(dhcpPayload)) {
+            // Add to host store if it connect to network directly
+            Set<IpAddress> ips = Sets.newHashSet(ip);
+            HostDescription desc = new DefaultHostDescription(macAddress, vlanId,
+                                                              hostLocation, ips);
+
+            // Replace the ip when dhcp server give the host new ip address
+            hostStore.createOrUpdateHost(DhcpRelayManager.PROVIDER_ID, hostId, desc, false);
+        } else {
+            // Add to route store if it does not connect to network directly
+            // Get gateway host IP according to host mac address
+            DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
+
+            if (record == null) {
+                log.warn("Can't find DHCP record of host {}", hostId);
+                return;
+            }
+
+            MacAddress gwMac = record.nextHop().orElse(null);
+            if (gwMac == null) {
+                log.warn("Can't find gateway mac address from record {}", record);
+                return;
+            }
+
+            HostId gwHostId = HostId.hostId(gwMac, record.vlanId());
+            Host gwHost = hostService.getHost(gwHostId);
+
+            if (gwHost == null) {
+                log.warn("Can't find gateway 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;
+            }
+
+            Route route = new Route(Route.Source.STATIC, ip.toIpPrefix(), nextHopIp);
+            routeStore.updateRoute(route);
+        }
+        sendResponseToClient(ethernetPacketAck, dhcpPayload);
+    }
+
+    /**
+     * forward the packet to ConnectPoint where the DHCP server is attached.
+     *
+     * @param packet the packet
+     */
+    private void handleDhcpDiscoverAndRequest(Ethernet packet) {
+        // send packet to dhcp server connect point.
+        if (dhcpServerConnectPoint != null) {
+            TrafficTreatment t = DefaultTrafficTreatment.builder()
+                    .setOutput(dhcpServerConnectPoint.port()).build();
+            OutboundPacket o = new DefaultOutboundPacket(
+                    dhcpServerConnectPoint.deviceId(), t, ByteBuffer.wrap(packet.serialize()));
+            if (log.isTraceEnabled()) {
+                log.trace("Relaying packet to dhcp server {}", packet);
+            }
+            packetService.emit(o);
+        } else {
+            log.warn("Can't find DHCP server connect point, abort.");
+        }
+    }
+
+
+    /**
+     * Gets output interface of a dhcp packet.
+     * If option 82 exists in the dhcp packet and the option was sent by
+     * ONOS (gateway address exists in ONOS interfaces), use the connect
+     * point and vlan id from circuit id; otherwise, find host by destination
+     * address and use vlan id from sender (dhcp server).
+     *
+     * @param ethPacket the ethernet packet
+     * @param dhcpPayload the dhcp packet
+     * @return an interface represent the output port and vlan; empty value
+     *         if the host or circuit id not found
+     */
+    private Optional<Interface> getOutputInterface(Ethernet ethPacket, DHCP dhcpPayload) {
+        VlanId originalPacketVlanId = VlanId.vlanId(ethPacket.getVlanID());
+        IpAddress gatewayIpAddress = Ip4Address.valueOf(dhcpPayload.getGatewayIPAddress());
+        Set<Interface> gatewayInterfaces = interfaceService.getInterfacesByIp(gatewayIpAddress);
+        DhcpRelayAgentOption option = (DhcpRelayAgentOption) dhcpPayload.getOption(OptionCode_CircuitID);
+
+        // Sent by ONOS, and contains circuit id
+        if (!gatewayInterfaces.isEmpty() && option != null) {
+            DhcpOption circuitIdSubOption = option.getSubOption(CIRCUIT_ID.getValue());
+            try {
+                CircuitId circuitId = CircuitId.deserialize(circuitIdSubOption.getData());
+                ConnectPoint connectPoint = ConnectPoint.deviceConnectPoint(circuitId.connectPoint());
+                VlanId vlanId = circuitId.vlanId();
+                return Optional.of(new Interface(null, connectPoint, null, null, vlanId));
+            } catch (IllegalArgumentException ex) {
+                // invalid circuit format, didn't sent by ONOS
+                log.debug("Invalid circuit {}, use information from dhcp payload",
+                          circuitIdSubOption.getData());
+            }
+        }
+
+        // Use Vlan Id from DHCP server if DHCP relay circuit id was not
+        // sent by ONOS or circuit Id can't be parsed
+        MacAddress dstMac = valueOf(dhcpPayload.getClientHardwareAddress());
+        Optional<DhcpRecord> dhcpRecord = dhcpRelayStore.getDhcpRecord(HostId.hostId(dstMac, originalPacketVlanId));
+        return dhcpRecord
+                .map(DhcpRecord::locations)
+                .orElse(Collections.emptySet())
+                .stream()
+                .reduce((hl1, hl2) -> {
+                    if (hl1 == null || hl2 == null) {
+                        return hl1 == null ? hl2 : hl1;
+                    }
+                    return hl1.time() > hl2.time() ? hl1 : hl2;
+                })
+                .map(lastLocation -> new Interface(null, lastLocation, null, null, originalPacketVlanId));
+    }
+
+    /**
+     * Handles DHCP offer packet.
+     *
+     * @param ethPacket the packet
+     * @param dhcpPayload the DHCP data
+     */
+    private void handleDhcpOffer(Ethernet ethPacket, DHCP dhcpPayload) {
+        // TODO: removes option 82 if necessary
+        sendResponseToClient(ethPacket, dhcpPayload);
+    }
+
+    /**
+     * Send the response DHCP to the requester host.
+     *
+     * @param ethPacket the packet
+     * @param dhcpPayload the DHCP data
+     */
+    private void sendResponseToClient(Ethernet ethPacket, DHCP dhcpPayload) {
+        Optional<Interface> outInterface = getOutputInterface(ethPacket, dhcpPayload);
+        outInterface.ifPresent(theInterface -> {
+            TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                    .setOutput(theInterface.connectPoint().port())
+                    .build();
+            OutboundPacket o = new DefaultOutboundPacket(
+                    theInterface.connectPoint().deviceId(),
+                    treatment,
+                    ByteBuffer.wrap(ethPacket.serialize()));
+            if (log.isTraceEnabled()) {
+                log.trace("Relaying packet to DHCP client {} via {}, vlan {}",
+                          ethPacket,
+                          theInterface.connectPoint(),
+                          theInterface.vlan());
+            }
+            packetService.emit(o);
+        });
+    }
+}