REST API support for DHCP Relay

Change-Id: Icfefebd15ff43718493e5223254b23ec02ee0bab
diff --git a/apps/dhcprelay/app/BUCK b/apps/dhcprelay/app/BUCK
new file mode 100644
index 0000000..d698297
--- /dev/null
+++ b/apps/dhcprelay/app/BUCK
@@ -0,0 +1,21 @@
+COMPILE_DEPS = [
+    '//lib:CORE_DEPS',
+    '//lib:JACKSON',
+    '//lib:KRYO',
+    '//lib:org.apache.karaf.shell.console',
+    '//cli:onos-cli',
+    '//core/store/serializers:onos-core-serializers',
+    '//apps/route-service/api:onos-apps-route-service-api',
+    '//apps/routing/fpm/api:onos-apps-routing-fpm-api',
+]
+
+TEST_DEPS = [
+    '//lib:TEST',
+    '//apps/route-service/api:onos-apps-route-service-api-tests',
+    '//core/api:onos-api-tests',
+]
+
+osgi_jar_with_tests (
+    deps = COMPILE_DEPS,
+    test_deps = TEST_DEPS,
+)
diff --git a/apps/dhcprelay/app/BUILD b/apps/dhcprelay/app/BUILD
new file mode 100644
index 0000000..29e4908
--- /dev/null
+++ b/apps/dhcprelay/app/BUILD
@@ -0,0 +1,15 @@
+COMPILE_DEPS = CORE_DEPS + JACKSON + KRYO + CLI + [
+    "//core/store/serializers:onos-core-serializers",
+    "//apps/route-service/api:onos-apps-route-service-api",
+    "//apps/routing/fpm/api:onos-apps-routing-fpm-api",
+]
+
+TEST_DEPS = TEST + [
+    "//apps/route-service/api:onos-apps-route-service-api-tests",
+    "//core/api:onos-api-tests",
+]
+
+osgi_jar_with_tests(
+    test_deps = TEST_DEPS,
+    deps = COMPILE_DEPS,
+)
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/Dhcp4HandlerImpl.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/Dhcp4HandlerImpl.java
new file mode 100644
index 0000000..59310b5
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/Dhcp4HandlerImpl.java
@@ -0,0 +1,1985 @@
+/*
+ * 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.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.Sets;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Modified;
+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.TpPort;
+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.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.dhcprelay.api.DhcpHandler;
+import org.onosproject.dhcprelay.api.DhcpServerInfo;
+import org.onosproject.dhcprelay.config.DhcpServerConfig;
+import org.onosproject.dhcprelay.config.IgnoreDhcpConfig;
+import org.onosproject.dhcprelay.store.DhcpRecord;
+import org.onosproject.dhcprelay.store.DhcpRelayStore;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.behaviour.Pipeliner;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.Objective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.flowobjective.ObjectiveError;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostListener;
+import org.onosproject.net.host.HostProvider;
+import org.onosproject.net.host.HostProviderRegistry;
+import org.onosproject.net.host.HostProviderService;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.intf.InterfaceService;
+import org.onosproject.net.packet.PacketPriority;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.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.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.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+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;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.net.flowobjective.Objective.Operation.ADD;
+import static org.onosproject.net.flowobjective.Objective.Operation.REMOVE;
+
+
+@Component
+@Service
+@Property(name = "version", value = "4")
+public class Dhcp4HandlerImpl implements DhcpHandler, HostProvider {
+    public static final String DHCP_V4_RELAY_APP = "org.onosproject.Dhcp4HandlerImpl";
+    public static final ProviderId PROVIDER_ID = new ProviderId("dhcp4", DHCP_V4_RELAY_APP);
+    private static final String BROADCAST_IP = "255.255.255.255";
+    private static final int IGNORE_CONTROL_PRIORITY = PacketPriority.CONTROL.priorityValue() + 1000;
+
+    private static final String LQ_ROUTE_PROPERTY_NAME = "learnRouteFromLeasequery";
+
+    private static final TrafficSelector CLIENT_SERVER_SELECTOR = DefaultTrafficSelector.builder()
+            .matchEthType(Ethernet.TYPE_IPV4)
+            .matchIPProtocol(IPv4.PROTOCOL_UDP)
+            .matchIPSrc(Ip4Address.ZERO.toIpPrefix())
+            .matchIPDst(Ip4Address.valueOf(BROADCAST_IP).toIpPrefix())
+            .matchUdpSrc(TpPort.tpPort(UDP.DHCP_CLIENT_PORT))
+            .matchUdpDst(TpPort.tpPort(UDP.DHCP_SERVER_PORT))
+            .build();
+    private static final TrafficSelector SERVER_RELAY_SELECTOR = DefaultTrafficSelector.builder()
+            .matchEthType(Ethernet.TYPE_IPV4)
+            .matchIPProtocol(IPv4.PROTOCOL_UDP)
+            .matchUdpSrc(TpPort.tpPort(UDP.DHCP_SERVER_PORT))
+            .matchUdpDst(TpPort.tpPort(UDP.DHCP_SERVER_PORT))
+            .build();
+    static final Set<TrafficSelector> DHCP_SELECTORS = ImmutableSet.of(
+            CLIENT_SERVER_SELECTOR,
+            SERVER_RELAY_SELECTOR
+    );
+    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 RouteStore routeStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected InterfaceService interfaceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostService hostService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostProviderRegistry providerRegistry;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected FlowObjectiveService flowObjectiveService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ComponentConfigService cfgService;
+
+    protected HostProviderService providerService;
+    protected ApplicationId appId;
+    protected Multimap<DeviceId, VlanId> ignoredVlans = Multimaps.synchronizedMultimap(HashMultimap.create());
+    private InternalHostListener hostListener = new InternalHostListener();
+
+    private List<DhcpServerInfo> defaultServerInfoList = new CopyOnWriteArrayList<>();
+    private List<DhcpServerInfo> indirectServerInfoList = new CopyOnWriteArrayList<>();
+
+    @Property(name = Dhcp4HandlerImpl.LQ_ROUTE_PROPERTY_NAME, boolValue = false,
+            label = "Enable learning routing information from LQ")
+    private Boolean learnRouteFromLeasequery = Boolean.TRUE;
+
+    private Executor hostEventExecutor = newSingleThreadExecutor(
+        groupedThreads("dhcp4-event-host", "%d", log));
+
+    @Activate
+    protected void activate(ComponentContext context) {
+        cfgService.registerProperties(getClass());
+        modified(context);
+        appId = coreService.registerApplication(DHCP_V4_RELAY_APP);
+        hostService.addListener(hostListener);
+        providerService = providerRegistry.register(this);
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        cfgService.unregisterProperties(getClass(), false);
+        providerRegistry.unregister(this);
+        hostService.removeListener(hostListener);
+        defaultServerInfoList.forEach(this::stopMonitoringIps);
+        defaultServerInfoList.forEach(info -> info.getDhcpServerIp4().ifPresent(this::cancelDhcpPacket));
+        defaultServerInfoList.clear();
+        indirectServerInfoList.forEach(this::stopMonitoringIps);
+        indirectServerInfoList.forEach(info -> info.getDhcpServerIp4().ifPresent(this::cancelDhcpPacket));
+        indirectServerInfoList.clear();
+    }
+
+    @Modified
+    protected void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context.getProperties();
+        Boolean flag;
+        flag = Tools.isPropertyEnabled(properties, Dhcp4HandlerImpl.LQ_ROUTE_PROPERTY_NAME);
+        if (flag != null) {
+            learnRouteFromLeasequery = flag;
+            log.info("Learning routes from DHCP leasequery is {}",
+                    learnRouteFromLeasequery ? "enabled" : "disabled");
+        }
+    }
+
+    private void stopMonitoringIps(DhcpServerInfo serverInfo) {
+        serverInfo.getDhcpGatewayIp4().ifPresent(gatewayIp -> {
+            hostService.stopMonitoringIp(gatewayIp);
+        });
+        serverInfo.getDhcpServerIp4().ifPresent(serverIp -> {
+            hostService.stopMonitoringIp(serverIp);
+        });
+    }
+
+    @Override
+    public void setDefaultDhcpServerConfigs(Collection<DhcpServerConfig> configs) {
+        setDhcpServerConfigs(configs, defaultServerInfoList);
+    }
+
+    @Override
+    public void setIndirectDhcpServerConfigs(Collection<DhcpServerConfig> configs) {
+        setDhcpServerConfigs(configs, indirectServerInfoList);
+    }
+
+    @Override
+    public List<DhcpServerInfo> getDefaultDhcpServerInfoList() {
+        return defaultServerInfoList;
+    }
+
+    @Override
+    public List<DhcpServerInfo> getIndirectDhcpServerInfoList() {
+        return indirectServerInfoList;
+    }
+
+    @Override
+    public void updateIgnoreVlanConfig(IgnoreDhcpConfig config) {
+        if (config == null) {
+            ignoredVlans.forEach(((deviceId, vlanId) -> {
+                processIgnoreVlanRule(deviceId, vlanId, REMOVE);
+            }));
+            return;
+        }
+        config.ignoredVlans().forEach((deviceId, vlanId) -> {
+            if (ignoredVlans.get(deviceId).contains(vlanId)) {
+                // don't need to process if it already ignored
+                return;
+            }
+            processIgnoreVlanRule(deviceId, vlanId, ADD);
+        });
+
+        ignoredVlans.forEach((deviceId, vlanId) -> {
+            if (!config.ignoredVlans().get(deviceId).contains(vlanId)) {
+                // not contains in new config, remove it
+                processIgnoreVlanRule(deviceId, vlanId, REMOVE);
+            }
+        });
+    }
+
+    @Override
+    public void removeIgnoreVlanState(IgnoreDhcpConfig config) {
+        if (config == null) {
+            ignoredVlans.clear();
+            return;
+        }
+        config.ignoredVlans().forEach((deviceId, vlanId) -> {
+            ignoredVlans.remove(deviceId, vlanId);
+        });
+    }
+
+    public void setDhcpServerConfigs(Collection<DhcpServerConfig> configs, List<DhcpServerInfo> serverInfoList) {
+        if (configs.size() == 0) {
+            // no config to update
+            return;
+        }
+
+        Boolean isConfigValid = false;
+        for (DhcpServerConfig serverConfig : configs) {
+            if (serverConfig.getDhcpServerIp4().isPresent()) {
+                isConfigValid = true;
+                break;
+            }
+        }
+        if (!isConfigValid) {
+            log.warn("No IP V4 server address found.");
+            return;  // No IP V6 address found
+        }
+        // if (!serverInfoList.isEmpty()) {
+        for (DhcpServerInfo oldServerInfo : serverInfoList) {
+            log.info("In for (DhcpServerInfo oldServerInfo : serverInfoList) {");
+            // remove old server info
+            //DhcpServerInfo oldServerInfo = serverInfoList.remove(0);
+
+            // stop monitoring gateway or server
+            oldServerInfo.getDhcpGatewayIp4().ifPresent(gatewayIp -> {
+                hostService.stopMonitoringIp(gatewayIp);
+            });
+            oldServerInfo.getDhcpServerIp4().ifPresent(serverIp -> {
+                hostService.stopMonitoringIp(serverIp);
+                cancelDhcpPacket(serverIp);
+            });
+        }
+
+        // Create new server info according to the config
+        serverInfoList.clear();
+        for (DhcpServerConfig serverConfig : configs) {
+            log.debug("Create new server info according to the config");
+            DhcpServerInfo newServerInfo = new DhcpServerInfo(serverConfig,
+                    DhcpServerInfo.Version.DHCP_V4);
+            checkState(newServerInfo.getDhcpServerConnectPoint().isPresent(),
+                    "Connect point not exists");
+            checkState(newServerInfo.getDhcpServerIp4().isPresent(),
+                    "IP of DHCP server not exists");
+
+            log.debug("DHCP server connect point: {}", newServerInfo.getDhcpServerConnectPoint().orElse(null));
+            log.debug("DHCP server IP: {}", newServerInfo.getDhcpServerIp4().orElse(null));
+
+            Ip4Address serverIp = newServerInfo.getDhcpServerIp4().get();
+            Ip4Address ipToProbe;
+            if (newServerInfo.getDhcpGatewayIp4().isPresent()) {
+                ipToProbe = newServerInfo.getDhcpGatewayIp4().get();
+            } else {
+                ipToProbe = newServerInfo.getDhcpServerIp4().orElse(null);
+            }
+            log.info("Probe_IP {}", ipToProbe);
+            String hostToProbe = newServerInfo.getDhcpGatewayIp4()
+                    .map(ip -> "gateway").orElse("server");
+
+            log.debug("Probing to resolve {} IP {}", hostToProbe, ipToProbe);
+            hostService.startMonitoringIp(ipToProbe);
+
+            Set<Host> hosts = hostService.getHostsByIp(ipToProbe);
+            if (!hosts.isEmpty()) {
+                Host host = hosts.iterator().next();
+                newServerInfo.setDhcpConnectVlan(host.vlan());
+                newServerInfo.setDhcpConnectMac(host.mac());
+            }
+
+            // Add new server info
+            synchronized (this) {
+                //serverInfoList.clear();
+                serverInfoList.add(newServerInfo);
+            }
+
+            requestDhcpPacket(serverIp);
+        }
+    }
+
+    @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 default DHCP relay server config. Abort packet processing");
+            return;
+        }
+
+        ConnectPoint inPort = context.inPacket().receivedFrom();
+        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);
+        Set<Interface> receivingInterfaces = interfaceService.getInterfacesByPort(inPort);
+        //ignore the packets if dhcp client interface is not configured on onos.
+        if (receivingInterfaces.isEmpty()) {
+            log.warn("Virtual interface is not configured on {}", inPort);
+            return;
+        }
+        switch (incomingPacketType) {
+            case DHCPDISCOVER:
+                // Add the gateway IP as virtual interface IP for server to understand
+                // the lease to be assigned and forward the packet to dhcp server.
+                List<InternalPacket> ethernetClientPacket =
+                        processDhcpPacketFromClient(context, packet, receivingInterfaces);
+                for (InternalPacket internalPacket : ethernetClientPacket) {
+                    log.debug("DHCPDISCOVER from {} Forward to server", inPort);
+                    writeRequestDhcpRecord(inPort, packet, dhcpPayload);
+                    forwardPacket(internalPacket);
+                }
+                break;
+            case DHCPOFFER:
+                //reply to dhcp client.
+                InternalPacket ethernetPacketOffer = processDhcpPacketFromServer(context, packet);
+                if (ethernetPacketOffer != null) {
+                    writeResponseDhcpRecord(ethernetPacketOffer.getPacket(), dhcpPayload);
+                    sendResponseToClient(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.
+                List<InternalPacket> ethernetPacketRequest =
+                        processDhcpPacketFromClient(context, packet, receivingInterfaces);
+                for (InternalPacket internalPacket : ethernetPacketRequest) {
+                    log.debug("DHCPDISCOVER from {} Forward to server", inPort);
+                    writeRequestDhcpRecord(inPort, packet, dhcpPayload);
+                    forwardPacket(internalPacket);
+                }
+                break;
+            case DHCPDECLINE:
+                break;
+            case DHCPACK:
+                // reply to dhcp client.
+                InternalPacket ethernetPacketAck = processDhcpPacketFromServer(context, packet);
+                if (ethernetPacketAck != null) {
+                    writeResponseDhcpRecord(ethernetPacketAck.getPacket(), dhcpPayload);
+                    handleDhcpAck(ethernetPacketAck.getPacket(), dhcpPayload);
+                    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;
+        }
+    }
+
+    /**
+     * Checks if this app has been configured.
+     *
+     * @return true if all information we need have been initialized
+     */
+    private boolean configured() {
+        return !defaultServerInfoList.isEmpty();
+    }
+
+    /**
+     * Returns the first interface ip from interface.
+     *
+     * @param iface interface of one connect point
+     * @return the first interface IP; null if not exists an IP address in
+     *         these interfaces
+     */
+    private Ip4Address getFirstIpFromInterface(Interface iface) {
+        checkNotNull(iface, "Interface can't be null");
+        return iface.ipAddressesList().stream()
+                .map(InterfaceIpAddress::ipAddress)
+                .filter(IpAddress::isIp4)
+                .map(IpAddress::getIp4Address)
+                .findFirst()
+                .orElse(null);
+    }
+
+    /**
+     * Gets Interface facing to the server for default host.
+     *
+     * @return the Interface facing to the server; null if not found
+     */
+    private Interface getDefaultServerInterface() {
+        return getServerInterface(defaultServerInfoList);
+    }
+
+    /**
+     * Gets Interface facing to the server for indirect hosts.
+     * Use default server Interface if indirect server not configured.
+     *
+     * @return the Interface facing to the server; null if not found
+     */
+    private Interface getIndirectServerInterface() {
+        return getServerInterface(indirectServerInfoList);
+    }
+
+    private Interface getServerInterface(List<DhcpServerInfo> serverInfos) {
+        return serverInfos.stream()
+                .findFirst()
+                .map(serverInfo -> {
+                    ConnectPoint dhcpServerConnectPoint =
+                            serverInfo.getDhcpServerConnectPoint().orElse(null);
+                    VlanId dhcpConnectVlan = serverInfo.getDhcpConnectVlan().orElse(null);
+                    if (dhcpServerConnectPoint == null || dhcpConnectVlan == null) {
+                        return null;
+                    }
+                    return interfaceService.getInterfacesByPort(dhcpServerConnectPoint)
+                            .stream()
+                            .filter(iface -> interfaceContainsVlan(iface, dhcpConnectVlan))
+                            .findFirst()
+                            .orElse(null);
+                })
+                .orElse(null);
+    }
+
+    /**
+     * Determind if an Interface contains a vlan id.
+     *
+     * @param iface the Interface
+     * @param vlanId the vlan id
+     * @return true if the Interface contains the vlan id
+     */
+    private boolean interfaceContainsVlan(Interface iface, VlanId vlanId) {
+        if (vlanId.equals(VlanId.NONE)) {
+            // untagged packet, check if vlan untagged or vlan native is not NONE
+            return !iface.vlanUntagged().equals(VlanId.NONE) ||
+                    !iface.vlanNative().equals(VlanId.NONE);
+        }
+        // tagged packet, check if the interface contains the vlan
+        return iface.vlanTagged().contains(vlanId);
+    }
+
+    private void handleLeaseQueryActivateMsg(Ethernet packet, DHCP dhcpPayload) {
+        log.debug("LQ: Got DHCPLEASEACTIVE packet!");
+
+        if (learnRouteFromLeasequery) {
+            // 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.DHCP, clientIP.toIpPrefix(), nextHopIp);
+            routeStore.updateRoute(route);
+        }
+
+        // and forward to client
+        InternalPacket ethernetPacket = processLeaseQueryFromServer(packet);
+        if (ethernetPacket != null) {
+            sendResponseToClient(ethernetPacket, dhcpPayload);
+        }
+    }
+
+    private void handleLeaseQueryMsg(PacketContext context, Ethernet packet, DHCP dhcpPayload) {
+        // If this flag is enabled we expect that DHCPLEASEQUERY-ies are sent from an access concentrator
+        // where queried client is connected to. Otherwise, DHCPLEASEQUERY source may be a separate connected agent
+        if (learnRouteFromLeasequery) {
+            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
+                List<InternalPacket> ethernetPacketRequest = processLeaseQueryFromAgent(context, packet);
+                // and forward to server
+                for (InternalPacket internalPacket : ethernetPacketRequest) {
+                    log.debug("LeaseQueryMsg forward to server");
+                    forwardPacket(internalPacket);
+                }
+            } else {
+                log.warn("LQ: Error! - DHCP relay record for that client not found - ignoring LQ!");
+            }
+        } else {
+            log.debug("LQ: Got DHCPLEASEQUERY packet!");
+
+            int giaddr = dhcpPayload.getGatewayIPAddress();
+
+            log.debug("DHCPLEASEQUERY giaddr: {} ({}). Originators connectPoint: {}", giaddr,
+                    Ip4Address.valueOf(giaddr), context.inPacket().receivedFrom());
+
+            // do a basic routing of the packet (this is unicast routing
+            // not a relay operation like for other broadcast dhcp packets
+            List<InternalPacket> ethernetPacketRequest = processLeaseQueryFromAgent(context, packet);
+            // and forward to server
+            for (InternalPacket internalPacket : ethernetPacketRequest) {
+                log.trace("LeaseQueryMsg forward to server connected to {}", internalPacket.getDestLocation());
+                forwardPacket(internalPacket);
+            }
+        }
+    }
+
+    private void handleLeaseQueryUnknown(Ethernet packet, DHCP dhcpPayload) {
+        log.debug("Lease Query for Client results in DHCPLEASEUNASSIGNED or " +
+                          "DHCPLEASEUNKNOWN - removing route & forwarding reply to originator");
+        if (learnRouteFromLeasequery) {
+            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.DHCP, clientIP.toIpPrefix(), nextHopIp);
+            routeStore.removeRoute(route);
+
+            // remove from temp store
+            dhcpRelayStore.removeDhcpRecord(hostId);
+        }
+        // and forward to client
+        InternalPacket ethernetPacket = processLeaseQueryFromServer(packet);
+        if (ethernetPacket != null) {
+            sendResponseToClient(ethernetPacket, dhcpPayload);
+        }
+    }
+
+    /**
+     * Build the DHCP discover/request packet with gateway IP(unicast packet).
+     *
+     * @param context the packet context
+     * @param ethernetPacket the ethernet payload to process
+     * @return processed packet
+     */
+    private List<InternalPacket> processDhcpPacketFromClient(PacketContext context,
+                                                             Ethernet ethernetPacket,
+                                                             Set<Interface> clientInterfaces) {
+        ConnectPoint receivedFrom = context.inPacket().receivedFrom();
+        DeviceId receivedFromDevice = receivedFrom.deviceId();
+        Ip4Address relayAgentIp = null;
+        relayAgentIp = Dhcp4HandlerUtil.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 Lists.newArrayList();
+        }
+        log.debug("Multi DHCP V4 processDhcpPacketFromClient on port {}",
+                   clientInterfaces.iterator().next().connectPoint());
+
+        // get dhcp header.
+        Ethernet etherReply = (Ethernet) ethernetPacket.clone();
+        IPv4 ipv4Packet = (IPv4) etherReply.getPayload();
+        UDP udpPacket = (UDP) ipv4Packet.getPayload();
+        DHCP dhcpPacket = (DHCP) udpPacket.getPayload();
+
+
+        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 Lists.newArrayList();
+        }
+
+        boolean isDirectlyConnected = directlyConnected(dhcpPacket);
+        boolean directConnFlag = directlyConnected(dhcpPacket);
+
+        // Multi DHCP Start
+        ConnectPoint clientConnectionPoint = context.inPacket().receivedFrom();
+        VlanId vlanIdInUse = VlanId.vlanId(ethernetPacket.getVlanID());
+        Interface clientInterface = interfaceService.getInterfacesByPort(clientConnectionPoint)
+                .stream().filter(iface -> Dhcp4HandlerUtil.interfaceContainsVlan(iface, vlanIdInUse))
+                .findFirst()
+                .orElse(null);
+
+        List<InternalPacket> internalPackets = new ArrayList<>();
+        List<DhcpServerInfo> serverInfoList = findValidServerInfo(directConnFlag);
+        List<DhcpServerInfo> copyServerInfoList = new ArrayList<DhcpServerInfo>(serverInfoList);
+
+
+        for (DhcpServerInfo serverInfo : copyServerInfoList) {
+            etherReply = (Ethernet) ethernetPacket.clone();
+            ipv4Packet = (IPv4) etherReply.getPayload();
+            udpPacket = (UDP) ipv4Packet.getPayload();
+            dhcpPacket = (DHCP) udpPacket.getPayload();
+            if (!checkDhcpServerConnPt(directConnFlag, serverInfo)) {
+                log.warn("Can't get server connect point, ignore");
+                continue;
+            }
+            DhcpServerInfo newServerInfo = getHostInfoForServerInfo(serverInfo, serverInfoList);
+            if (newServerInfo == null) {
+                log.warn("Can't get server interface with host info resolved, ignore");
+                continue;
+            }
+
+            Interface serverInterface = getServerInterface(newServerInfo);
+            if (serverInterface == null) {
+                log.warn("Can't get server interface, ignore");
+                continue;
+            }
+
+            Ip4Address ipFacingServer = getFirstIpFromInterface(serverInterface);
+            MacAddress macFacingServer = serverInterface.mac();
+            log.debug("Interfacing server {} Mac : {} ", ipFacingServer, macFacingServer);
+            if (ipFacingServer == null || macFacingServer == null) {
+                log.warn("No IP address for server Interface {}", serverInterface);
+                continue;
+            }
+
+
+            etherReply.setSourceMACAddress(macFacingServer);
+            // set default info and replace with indirect if available later on.
+            if (newServerInfo.getDhcpConnectMac().isPresent()) {
+                etherReply.setDestinationMACAddress(newServerInfo.getDhcpConnectMac().get());
+            }
+            if (newServerInfo.getDhcpConnectVlan().isPresent()) {
+                etherReply.setVlanID(newServerInfo.getDhcpConnectVlan().get().toShort());
+            }
+            ipv4Packet.setSourceAddress(ipFacingServer.toInt());
+            ipv4Packet.setDestinationAddress(newServerInfo.getDhcpServerIp4().get().toInt());
+            log.debug("Directly connected {}", isDirectlyConnected);
+            log.debug("DHCP server IP: {}", newServerInfo.getDhcpServerIp4().get());
+            if (isDirectlyConnected) {
+
+                log.debug("Default DHCP server IP: {}", newServerInfo.getDhcpServerIp4().get());
+                if (newServerInfo.getDhcpConnectMac().isPresent()) {
+                    etherReply.setDestinationMACAddress(newServerInfo.getDhcpConnectMac().get());
+                }
+                if (newServerInfo.getDhcpConnectVlan().isPresent()) {
+                    etherReply.setVlanID(newServerInfo.getDhcpConnectVlan().get().toShort());
+                }
+
+                ipv4Packet.setDestinationAddress(newServerInfo.getDhcpServerIp4().get().toInt());
+
+
+                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);
+
+                relayAgentIp = serverInfo.getRelayAgentIp4(receivedFromDevice).orElse(null);
+
+                // Sets relay agent IP
+                int effectiveRelayAgentIp = relayAgentIp != null ?
+                        relayAgentIp.toInt() : clientInterfaceIp.toInt();
+                dhcpPacket.setGatewayIPAddress(effectiveRelayAgentIp);
+                log.debug("In Default, Relay Agent IP {}", effectiveRelayAgentIp);
+            } else {
+                if (!newServerInfo.getDhcpServerIp4().isPresent()) {
+                  // do nothing
+                } else if (!newServerInfo.getDhcpConnectMac().isPresent()) {
+                    continue;
+                } else {
+                    relayAgentIp = newServerInfo.getRelayAgentIp4(receivedFromDevice).orElse(null);
+                    // Sets relay agent IP
+                    int effectiveRelayAgentIp = relayAgentIp != null ?
+                            relayAgentIp.toInt() : clientInterfaceIp.toInt();
+                    Ip4Address effectiveRealRealyAgentIP = relayAgentIp != null ?
+                            relayAgentIp : clientInterfaceIp;
+                    dhcpPacket.setGatewayIPAddress(effectiveRelayAgentIp);
+                    ipv4Packet.setSourceAddress(effectiveRealRealyAgentIP.toInt());
+                    log.debug("Source IP address set as relay agent IP with value: {}", effectiveRealRealyAgentIP);
+                }
+            }
+
+            // Remove broadcast flag
+            dhcpPacket.setFlags((short) 0);
+
+            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);
+            ipv4Packet.setTtl((byte) 64);
+            etherReply.setPayload(ipv4Packet);
+            InternalPacket internalPacket = InternalPacket.internalPacket(etherReply,
+                    serverInfo.getDhcpServerConnectPoint().get());
+
+            internalPackets.add(internalPacket);
+        }
+        return internalPackets;
+    }
+
+
+    /**
+     * 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 List<InternalPacket> processLeaseQueryFromAgent(PacketContext context,
+                                                            Ethernet ethernetPacket) {
+        ConnectPoint receivedFrom = context.inPacket().receivedFrom();
+        DeviceId receivedFromDevice = receivedFrom.deviceId();
+
+        // get dhcp header.
+        Ethernet etherReply = (Ethernet) ethernetPacket.clone();
+        IPv4 ipv4Packet = (IPv4) etherReply.getPayload();
+        UDP udpPacket = (UDP) ipv4Packet.getPayload();
+        DHCP dhcpPacket = (DHCP) udpPacket.getPayload();
+
+        Ip4Address relayAgentIp;
+
+        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);
+        boolean directConnFlag = directlyConnected(dhcpPacket);
+
+        // Multi DHCP Start
+        List<InternalPacket> internalPackets = new ArrayList<>();
+        List<DhcpServerInfo> serverInfoList = findValidServerInfo(directConnFlag);
+        List<DhcpServerInfo> copyServerInfoList = new ArrayList<>(serverInfoList);
+
+        for (DhcpServerInfo serverInfo : copyServerInfoList) {
+             // get dhcp header.
+             etherReply = (Ethernet) ethernetPacket.clone();
+             ipv4Packet = (IPv4) etherReply.getPayload();
+             udpPacket = (UDP) ipv4Packet.getPayload();
+             dhcpPacket = (DHCP) udpPacket.getPayload();
+
+            if (!checkDhcpServerConnPt(directConnFlag, serverInfo)) {
+                log.warn("Can't get server connect point, ignore");
+                continue;
+            }
+            DhcpServerInfo newServerInfo = getHostInfoForServerInfo(serverInfo, serverInfoList);
+            if (newServerInfo == null) {
+                log.warn("Can't get server interface with host info resolved, ignore");
+                continue;
+            }
+
+            Interface serverInterface = getServerInterface(newServerInfo);
+            if (serverInterface == null) {
+                log.warn("Can't get server interface, ignore");
+                continue;
+            }
+            Ip4Address ipFacingServer = getFirstIpFromInterface(serverInterface);
+            MacAddress macFacingServer = serverInterface.mac();
+            if (ipFacingServer == null || macFacingServer == null) {
+                log.warn("No IP address for server Interface {}", serverInterface);
+                continue;
+            }
+
+            etherReply.setSourceMACAddress(macFacingServer);
+            etherReply.setDestinationMACAddress(newServerInfo.getDhcpConnectMac().get());
+            etherReply.setVlanID(newServerInfo.getDhcpConnectVlan().get().toShort());
+            ipv4Packet.setSourceAddress(ipFacingServer.toInt());
+            ipv4Packet.setDestinationAddress(newServerInfo.getDhcpServerIp4().get().toInt());
+            if (isDirectlyConnected) {
+                // set default info and replace with indirect if available later on.
+                if (newServerInfo.getDhcpConnectMac().isPresent()) {
+                    etherReply.setDestinationMACAddress(newServerInfo.getDhcpConnectMac().get());
+                }
+                if (newServerInfo.getDhcpConnectVlan().isPresent()) {
+                    etherReply.setVlanID(serverInfo.getDhcpConnectVlan().get().toShort());
+                }
+                if (learnRouteFromLeasequery) {
+                    relayAgentIp = newServerInfo.getRelayAgentIp4(receivedFromDevice).orElse(null);
+                    // Sets relay agent IP
+                    int effectiveRelayAgentIp = relayAgentIp != null ?
+                            relayAgentIp.toInt() : clientInterfaceIp.toInt();
+                    dhcpPacket.setGatewayIPAddress(effectiveRelayAgentIp);
+                }
+            } else {
+                if (!newServerInfo.getDhcpServerIp4().isPresent()) {
+                  //do nothing
+                } else if (!newServerInfo.getDhcpConnectMac().isPresent()) {
+                    continue;
+                } else if (learnRouteFromLeasequery) {
+                    relayAgentIp = newServerInfo.getRelayAgentIp4(receivedFromDevice).orElse(null);
+                    // Sets relay agent IP
+                    int effectiveRelayAgentIp = relayAgentIp != null ?
+                            relayAgentIp.toInt() : clientInterfaceIp.toInt();
+                    dhcpPacket.setGatewayIPAddress(effectiveRelayAgentIp);
+                    log.debug("Relay Agent IP {}", relayAgentIp);
+                }
+
+                log.trace("Indirect");
+            }
+
+            // Remove broadcast flag
+            dhcpPacket.setFlags((short) 0);
+
+            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);
+            ipv4Packet.setTtl((byte) 64);
+            etherReply.setPayload(ipv4Packet);
+            udpPacket.resetChecksum();
+            ////return etherReply;
+            InternalPacket internalPacket = InternalPacket.internalPacket(etherReply,
+                    newServerInfo.getDhcpServerConnectPoint().get());
+
+            internalPackets.add(internalPacket);
+        }
+        log.debug("num of processLeaseQueryFromAgent packets to send is: {}", internalPackets.size());
+
+        return internalPackets;
+    }
+
+
+    /**
+     * 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 = getClientInterface(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 = getVlanIdFromRelayAgentOption(dhcpPayload);
+        if (vlanId == null) {
+            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 InternalPacket processDhcpPacketFromServer(PacketContext context, 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;
+        ConnectPoint inPort = context.inPacket().receivedFrom();
+        boolean directConnFlag = directlyConnected(dhcpPayload);
+        DhcpServerInfo foundServerInfo = findServerInfoFromServer(directConnFlag, inPort);
+
+        if (foundServerInfo == null) {
+            log.warn("Cannot find server info for {} server, inPort {}",
+                      directConnFlag ? "direct" : "indirect", inPort);
+            return null;
+        } else {
+            if (Dhcp4HandlerUtil.isServerIpEmpty(foundServerInfo)) {
+                log.warn("Cannot find server info's ipaddress");
+                return null;
+            }
+        }
+        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);
+            if (((int) dhcpPayload.getFlags() & 0x8000) == 0x0000) {
+                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(MacAddress.BROADCAST);
+            }
+        } else {
+            etherReply.setDestinationMACAddress(dhcpPayload.getClientHardwareAddress());
+        }
+
+        // we leave the srcMac from the original packet
+        // figure out the relay agent IP corresponding to the original request
+        Ip4Address ipFacingClient = getFirstIpFromInterface(clientInterface);
+        if (ipFacingClient == null) {
+            log.warn("Cannot determine relay agent interface Ipv4 addr for host {}/{}. "
+                             + "Aborting relay for dhcp packet from server {}",
+                     etherReply.getDestinationMAC(), clientInterface.vlan(),
+                     ethernetPacket);
+            return null;
+        }
+        // SRC_IP: relay agent IP
+        // DST_IP: offered IP
+        ipv4Packet.setSourceAddress(ipFacingClient.toInt());
+        if (((int) dhcpPayload.getFlags() & 0x8000) == 0x0000) {
+            ipv4Packet.setDestinationAddress(dhcpPayload.getYourIPAddress());
+        } else {
+            ipv4Packet.setDestinationAddress(BROADCAST_IP);
+        }
+        udpPacket.setSourcePort(UDP.DHCP_SERVER_PORT);
+        if (directlyConnected(dhcpPayload)) {
+            udpPacket.setDestinationPort(UDP.DHCP_CLIENT_PORT);
+        } else {
+            // forward to another dhcp relay
+            // FIXME: Currently we assume the DHCP comes from a L2 relay with
+            // Option 82, this might not work if DHCP message comes from
+            // L3 relay.
+            udpPacket.setDestinationPort(UDP.DHCP_CLIENT_PORT);
+        }
+
+        udpPacket.setPayload(dhcpPayload);
+        ipv4Packet.setPayload(udpPacket);
+        etherReply.setPayload(ipv4Packet);
+        return InternalPacket.internalPacket(etherReply, clientInterface.connectPoint());
+    }
+
+    /**
+     * 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 InternalPacket 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 = null;
+        MacAddress destinationMac = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
+
+        if (!learnRouteFromLeasequery) {
+            int giaddr = ipv4Packet.getDestinationAddress();
+            IpAddress destinationAddress = Ip4Address.valueOf(giaddr);
+            log.debug("DHCPLEASEQUERYRESP giaddr: {}({})", giaddr, destinationAddress);
+
+            Host destinationHost = hostService.getHostsByIp(destinationAddress).stream().findFirst().orElse(null);
+            if (destinationHost != null) {
+                destinationMac = destinationHost.mac();
+                log.trace("DHCPLEASEQUERYRESP destination mac is: {}", destinationMac);
+                ConnectPoint destinationLocation = destinationHost.location();
+                log.trace("Lookup for client interface by destination location {}", destinationLocation);
+                clientInterface = interfaceService.getInterfacesByPort(destinationLocation)
+                        .stream()
+                        .filter(iface -> interfaceContainsVlan(iface, VlanId.vlanId(etherReply.getVlanID())))
+                        .findFirst()
+                        .orElse(null);
+                log.trace("Found Host {} by ip {}", destinationHost, destinationAddress);
+                log.debug("DHCPLEASEQUERYRESP Client interface: {}",
+                        (clientInterface != null ? clientInterface : "not resolved"));
+
+            }
+        } else {
+            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) && learnRouteFromLeasequery) {
+            // 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
+                Optional<MacAddress> nextHop = record.nextHopTemp();
+                if (!nextHop.isPresent()) {
+                    nextHop = record.nextHop();
+                }
+                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(destinationMac);
+        }
+
+        udpPacket.setSourcePort(UDP.DHCP_SERVER_PORT);
+        if (directlyConnected(dhcpPayload)) {
+            udpPacket.setDestinationPort(UDP.DHCP_CLIENT_PORT);
+        } else {
+            udpPacket.setDestinationPort(UDP.DHCP_SERVER_PORT);
+        }
+
+        udpPacket.setPayload(dhcpPayload);
+        ipv4Packet.setPayload(udpPacket);
+        etherReply.setPayload(ipv4Packet);
+        udpPacket.resetChecksum();
+
+        return InternalPacket.internalPacket(etherReply, clientInterface.connectPoint());
+    }
+    /**
+     * Extracts VLAN ID from relay agent option.
+     *
+     * @param dhcpPayload the DHCP payload
+     * @return VLAN ID from DHCP payload; null if not exists
+     */
+    private VlanId getVlanIdFromRelayAgentOption(DHCP dhcpPayload) {
+        DhcpRelayAgentOption option = (DhcpRelayAgentOption) dhcpPayload.getOption(OptionCode_CircuitID);
+        if (option == null) {
+            return null;
+        }
+        DhcpOption circuitIdSubOption = option.getSubOption(CIRCUIT_ID.getValue());
+        if (circuitIdSubOption == null) {
+            return null;
+        }
+        try {
+            CircuitId circuitId = CircuitId.deserialize(circuitIdSubOption.getData());
+            return circuitId.vlanId();
+        } catch (IllegalArgumentException e) {
+            // can't deserialize the circuit ID
+            return null;
+        }
+    }
+
+    /**
+     * Removes DHCP relay agent information option (option 82) from DHCP payload.
+     * Also reset giaddr to 0
+     *
+     * @param ethPacket the Ethernet packet to be processed
+     * @return Ethernet packet processed
+     */
+    private Ethernet removeRelayAgentOption(Ethernet ethPacket) {
+        Ethernet ethernet = (Ethernet) ethPacket.duplicate();
+        IPv4 ipv4 = (IPv4) ethernet.getPayload();
+        UDP udp = (UDP) ipv4.getPayload();
+        DHCP dhcpPayload = (DHCP) udp.getPayload();
+
+        // removes relay agent information option
+        List<DhcpOption> options = dhcpPayload.getOptions();
+        options = options.stream()
+                .filter(option -> option.getCode() != OptionCode_CircuitID.getValue())
+                .collect(Collectors.toList());
+        dhcpPayload.setOptions(options);
+        dhcpPayload.setGatewayIPAddress(0);
+
+        udp.setPayload(dhcpPayload);
+        ipv4.setPayload(udp);
+        ethernet.setPayload(ipv4);
+        return ethernet;
+    }
+
+    private boolean isDhcpPacketLeasequery(DHCP dhcpPacket) {
+        switch (dhcpPacket.getPacketType()) {
+            case DHCPLEASEACTIVE:
+            case DHCPLEASEQUERY:
+            case DHCPLEASEUNASSIGNED:
+            case DHCPLEASEUNKNOWN:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * 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) {
+        // leasequery is always indirect
+        if (isDhcpPacketLeasequery(dhcpPayload)) {
+            return false;
+        }
+
+        DhcpRelayAgentOption relayAgentOption =
+                (DhcpRelayAgentOption) dhcpPayload.getOption(OptionCode_CircuitID);
+
+        // Doesn't contains relay option
+        if (relayAgentOption == null) {
+            return true;
+        }
+
+        // check circuit id, if circuit id is invalid, we say it is an indirect host
+        DhcpOption circuitIdOpt = relayAgentOption.getSubOption(CIRCUIT_ID.getValue());
+
+        try {
+            CircuitId.deserialize(circuitIdOpt.getData());
+            return true;
+        } catch (Exception e) {
+            // invalid circuit id
+            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 = getClientInterface(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 = getVlanIdFromRelayAgentOption(dhcpPayload);
+        if (vlanId == null) {
+            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);
+            Host host = hostService.getHost(hostId);
+
+            Set<HostLocation> hostLocations = Sets.newHashSet(hostLocation);
+            if (host != null) {
+                // Dual homing support:
+                // if host exists, use old locations and new location
+                hostLocations.addAll(host.locations());
+            }
+            HostDescription desc = new DefaultHostDescription(macAddress, vlanId,
+                                                              hostLocations, ips, false);
+            // Add IP address when dhcp server give the host new ip address
+            providerService.hostDetected(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
+            // TODO: remove relay store here
+            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.DHCP, ip.toIpPrefix(), nextHopIp);
+            routeStore.replaceRoute(route);
+        }
+    }
+
+    /**
+     * Gets output interface of a dhcp packet.
+     * If option 82 exists in the dhcp packet and the option was sent by
+     * ONOS (circuit format is correct), 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> getClientInterface(Ethernet ethPacket, DHCP dhcpPayload) {
+        VlanId originalPacketVlanId = VlanId.vlanId(ethPacket.getVlanID());
+        DhcpRelayAgentOption option = (DhcpRelayAgentOption) dhcpPayload.getOption(OptionCode_CircuitID);
+
+        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 interfaceService.getInterfacesByPort(connectPoint)
+                    .stream()
+                    .filter(iface -> interfaceContainsVlan(iface, vlanId))
+                    .findFirst();
+        } 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
+        // TODO: remove relay store from this method
+        MacAddress dstMac = valueOf(dhcpPayload.getClientHardwareAddress());
+        VlanId filteredVlanId = getVlanIdFromDhcpRecord(dstMac, originalPacketVlanId);
+        // Get the vlan from the dhcp record
+        if (filteredVlanId == null) {
+            log.debug("not find the matching DHCP record for mac: {} and vlan: {}", dstMac, originalPacketVlanId);
+            return Optional.empty();
+        }
+
+        Optional<DhcpRecord> dhcpRecord = dhcpRelayStore.getDhcpRecord(HostId.hostId(dstMac, filteredVlanId));
+        ConnectPoint clientConnectPoint = dhcpRecord
+                .map(DhcpRecord::locations)
+                .orElse(Collections.emptySet())
+                .stream()
+                .reduce((hl1, hl2) -> {
+                    // find latest host connect point
+                    if (hl1 == null || hl2 == null) {
+                        return hl1 == null ? hl2 : hl1;
+                    }
+                    return hl1.time() > hl2.time() ? hl1 : hl2;
+                })
+                .orElse(null);
+
+        if (clientConnectPoint != null) {
+            return interfaceService.getInterfacesByPort(clientConnectPoint)
+                    .stream()
+                    .filter(iface -> interfaceContainsVlan(iface, filteredVlanId))
+                    .findFirst();
+        }
+        return Optional.empty();
+    }
+
+    /**
+     * Get the required vlanId in case the DCHP record has more than one vlanId for a given MAC.
+     *
+     * @param mac MAC address of the DHCP client
+     * @param vlan Expected vlan of the DHCP client
+     */
+    private VlanId getVlanIdFromDhcpRecord(MacAddress mac, VlanId vlan) {
+        // Get all the DHCP records matching with the mac address
+        // If only one entry is present then pick the vlan of that entry
+        // If more then one entry is present then look for an entry with matching vlan
+        // else return null
+        Collection<DhcpRecord> records = dhcpRelayStore.getDhcpRecords();
+        List<DhcpRecord> filteredRecords = new ArrayList<>();
+        for (DhcpRecord e: records) {
+            if (e.macAddress().equals(mac)) {
+                filteredRecords.add(e);
+            }
+        }
+        log.debug("getVlanIdFromDhcpRecord mac: {} vlan: {}", mac, vlan);
+        log.debug("filteredRecords are: {}", filteredRecords);
+        if (filteredRecords.size() == 1) {
+            log.debug("Only one DHCP record entry. Returning back the vlan of that DHCP record: {}", filteredRecords);
+            return filteredRecords.get(0).vlanId();
+        }
+        // Check in the DHCP filtered record for matching vlan
+        for (DhcpRecord e: filteredRecords) {
+            if (e.vlanId().equals(vlan)) {
+                log.debug("Found a matching vlan entry in the DHCP record:{}", e);
+                return vlan;
+            }
+        }
+        // Found nothing return null
+        log.debug("Returning null as no matching or more than one matching entry found");
+        return null;
+
+    }
+
+
+
+    /**
+     * Send the response DHCP to the requester host.
+     *
+     * @param thePacket the packet
+     * @param dhcpPayload the DHCP data
+     */
+    private void sendResponseToClient(InternalPacket thePacket, DHCP dhcpPayload) {
+        checkNotNull(thePacket, "Nothing to send");
+        checkNotNull(thePacket.getPacket(), "Packet to send must not be empty");
+        checkNotNull(thePacket.getDestLocation(), "Packet destination not be empty");
+
+        Ethernet ethPacket = thePacket.getPacket();
+        if (directlyConnected(dhcpPayload)) {
+            ethPacket = removeRelayAgentOption(ethPacket);
+        }
+
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                .setOutput(thePacket.getDestLocation().port())
+                .build();
+        OutboundPacket o = new DefaultOutboundPacket(
+                thePacket.getDestLocation().deviceId(),
+                treatment,
+                ByteBuffer.wrap(ethPacket.serialize()));
+        if (log.isTraceEnabled()) {
+            log.trace("Relaying packet to DHCP client {} via {}",
+                      ethPacket,
+                      thePacket.getDestLocation());
+        }
+        packetService.emit(o);
+    }
+
+    @Override
+    public void triggerProbe(Host host) {
+        // Do nothing here
+    }
+
+    @Override
+    public ProviderId id() {
+        return PROVIDER_ID;
+    }
+
+    class InternalHostListener implements HostListener {
+        @Override
+        public void event(HostEvent event) {
+            if (!configured()) {
+                return;
+            }
+            switch (event.type()) {
+                case HOST_ADDED:
+                case HOST_UPDATED:
+                case HOST_MOVED:
+                    log.trace("Scheduled host event {}", event);
+                    hostEventExecutor.execute(() -> hostUpdated(event.subject()));
+                    break;
+                case HOST_REMOVED:
+                    log.trace("Scheduled host event {}", event);
+                    hostEventExecutor.execute(() -> hostRemoved(event.subject()));
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Handle host updated.
+     * If the host is DHCP server or gateway, update connect mac and vlan.
+     *
+     * @param host the host
+     */
+    private void hostUpdated(Host host) {
+        hostUpdated(host, defaultServerInfoList);
+        hostUpdated(host, indirectServerInfoList);
+    }
+
+    private void hostUpdated(Host host, List<DhcpServerInfo> srverInfoList) {
+        srverInfoList.stream().forEach(serverInfo -> {
+            Ip4Address targetIp = serverInfo.getDhcpGatewayIp4().orElse(null);
+            Ip4Address serverIp = serverInfo.getDhcpServerIp4().orElse(null);
+            if (targetIp == null) {
+                targetIp = serverIp;
+            }
+            if (targetIp != null) {
+                if (host.ipAddresses().contains(targetIp)) {
+                    serverInfo.setDhcpConnectMac(host.mac());
+                    serverInfo.setDhcpConnectVlan(host.vlan());
+                    requestDhcpPacket(serverIp);
+                }
+            }
+        });
+    }
+
+
+    /**
+     * Handle host removed.
+     * If the host is DHCP server or gateway, unset connect mac and vlan.
+     *
+     * @param host the host
+     */
+    private void hostRemoved(Host host) {
+        hostRemoved(host, defaultServerInfoList);
+        hostRemoved(host, indirectServerInfoList);
+    }
+
+    private void hostRemoved(Host host, List<DhcpServerInfo> serverInfoList) {
+        serverInfoList.stream().forEach(serverInfo -> {
+            Ip4Address targetIp = serverInfo.getDhcpGatewayIp4().orElse(null);
+            Ip4Address serverIp = serverInfo.getDhcpServerIp4().orElse(null);
+            if (targetIp == null) {
+                targetIp = serverIp;
+            }
+
+            if (targetIp != null) {
+                if (host.ipAddresses().contains(targetIp)) {
+                    serverInfo.setDhcpConnectVlan(null);
+                    serverInfo.setDhcpConnectMac(null);
+                    cancelDhcpPacket(serverIp);
+                }
+            }
+        });
+    }
+
+    private void requestDhcpPacket(Ip4Address serverIp) {
+        requestServerDhcpPacket(serverIp);
+        requestClientDhcpPacket(serverIp);
+    }
+
+    private void cancelDhcpPacket(Ip4Address serverIp) {
+        cancelServerDhcpPacket(serverIp);
+        cancelClientDhcpPacket(serverIp);
+    }
+
+    private void cancelServerDhcpPacket(Ip4Address serverIp) {
+        TrafficSelector serverSelector =
+                DefaultTrafficSelector.builder(SERVER_RELAY_SELECTOR)
+                        .matchIPSrc(serverIp.toIpPrefix())
+                        .build();
+        packetService.cancelPackets(serverSelector,
+                                    PacketPriority.CONTROL,
+                                    appId);
+    }
+
+    private void requestServerDhcpPacket(Ip4Address serverIp) {
+        TrafficSelector serverSelector =
+                DefaultTrafficSelector.builder(SERVER_RELAY_SELECTOR)
+                        .matchIPSrc(serverIp.toIpPrefix())
+                        .build();
+        packetService.requestPackets(serverSelector,
+                                     PacketPriority.CONTROL,
+                                     appId);
+    }
+
+    private void cancelClientDhcpPacket(Ip4Address serverIp) {
+        // Packet comes from relay
+        TrafficSelector indirectClientSelector =
+                DefaultTrafficSelector.builder(SERVER_RELAY_SELECTOR)
+                        .matchIPDst(serverIp.toIpPrefix())
+                        .build();
+        packetService.cancelPackets(indirectClientSelector,
+                                    PacketPriority.CONTROL,
+                                    appId);
+
+        // Packet comes from client
+        packetService.cancelPackets(CLIENT_SERVER_SELECTOR,
+                                    PacketPriority.CONTROL,
+                                    appId);
+    }
+
+    private void requestClientDhcpPacket(Ip4Address serverIp) {
+        // Packet comes from relay
+        TrafficSelector indirectClientSelector =
+                DefaultTrafficSelector.builder(SERVER_RELAY_SELECTOR)
+                        .matchIPDst(serverIp.toIpPrefix())
+                        .build();
+        packetService.requestPackets(indirectClientSelector,
+                                     PacketPriority.CONTROL,
+                                     appId);
+
+        // Packet comes from client
+        packetService.requestPackets(CLIENT_SERVER_SELECTOR,
+                                     PacketPriority.CONTROL,
+                                     appId);
+    }
+
+    /**
+     * Process the ignore rules.
+     *
+     * @param deviceId the device id
+     * @param vlanId the vlan to be ignored
+     * @param op the operation, ADD to install; REMOVE to uninstall rules
+     */
+    private void processIgnoreVlanRule(DeviceId deviceId, VlanId vlanId, Objective.Operation op) {
+        AtomicInteger installedCount = new AtomicInteger(DHCP_SELECTORS.size());
+        DHCP_SELECTORS.forEach(trafficSelector -> {
+            TrafficSelector selector = DefaultTrafficSelector.builder(trafficSelector)
+                    .matchVlanId(vlanId)
+                    .build();
+
+            ForwardingObjective.Builder builder = DefaultForwardingObjective.builder()
+                    .withFlag(ForwardingObjective.Flag.VERSATILE)
+                    .withSelector(selector)
+                    .withPriority(IGNORE_CONTROL_PRIORITY)
+                    .withTreatment(DefaultTrafficTreatment.emptyTreatment())
+                    .fromApp(appId);
+
+
+            ObjectiveContext objectiveContext = new ObjectiveContext() {
+                @Override
+                public void onSuccess(Objective objective) {
+                    log.info("Ignore rule {} (Vlan id {}, device {}, selector {})",
+                             op, vlanId, deviceId, selector);
+                    int countDown = installedCount.decrementAndGet();
+                    if (countDown != 0) {
+                        return;
+                    }
+                    switch (op) {
+                        case ADD:
+                            ignoredVlans.put(deviceId, vlanId);
+                            break;
+                        case REMOVE:
+                            ignoredVlans.remove(deviceId, vlanId);
+                            break;
+                        default:
+                            log.warn("Unsupported objective operation {}", op);
+                            break;
+                    }
+                }
+
+                @Override
+                public void onError(Objective objective, ObjectiveError error) {
+                    log.warn("Can't {} ignore rule (vlan id {}, selector {}, device {}) due to {}",
+                             op, vlanId, selector, deviceId, error);
+                }
+            };
+
+            ForwardingObjective fwd;
+            switch (op) {
+                case ADD:
+                    fwd = builder.add(objectiveContext);
+                    break;
+                case REMOVE:
+                    fwd = builder.remove(objectiveContext);
+                    break;
+                default:
+                    log.warn("Unsupported objective operation {}", op);
+                    return;
+            }
+
+            Device device = deviceService.getDevice(deviceId);
+            if (device == null || !device.is(Pipeliner.class)) {
+                log.warn("Device {} is not available now, wait until device is available", deviceId);
+                return;
+            }
+            flowObjectiveService.apply(deviceId, fwd);
+        });
+    }
+
+    @Override
+    public void setDhcpFpmEnabled(Boolean enabled) {
+        // v4 does not use fpm. Do nothing.
+    }
+    private List<DhcpServerInfo> findValidServerInfo(boolean directConnFlag) {
+        List<DhcpServerInfo> validServerInfo;
+
+        if (directConnFlag || indirectServerInfoList.isEmpty()) {
+            validServerInfo = new ArrayList<DhcpServerInfo>(defaultServerInfoList);
+        } else {
+            validServerInfo = new ArrayList<DhcpServerInfo>(indirectServerInfoList);
+        }
+        return validServerInfo;
+    }
+
+
+    private boolean checkDhcpServerConnPt(boolean directConnFlag,
+                                          DhcpServerInfo serverInfo) {
+        if (serverInfo.getDhcpServerConnectPoint() == null) {
+            log.warn("DHCP4 server connect point for {} connPt {}",
+                    directConnFlag ? "direct" : "indirect", serverInfo.getDhcpServerConnectPoint());
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Checks if serverInfo's host info (mac and vlan) is filled in; if not, fills in.
+     *
+     * @param serverInfo server information
+     * @return newServerInfo if host info can be either found or filled in.
+     */
+    private DhcpServerInfo getHostInfoForServerInfo(DhcpServerInfo serverInfo, List<DhcpServerInfo> sererInfoList) {
+        DhcpServerInfo newServerInfo = null;
+        MacAddress  dhcpServerConnectMac = serverInfo.getDhcpConnectMac().orElse(null);
+        VlanId dhcpConnectVlan = serverInfo.getDhcpConnectVlan().orElse(null);
+        ConnectPoint dhcpServerConnectPoint = serverInfo.getDhcpServerConnectPoint().orElse(null);
+
+        if (dhcpServerConnectMac != null && dhcpConnectVlan != null) {
+            newServerInfo = serverInfo;
+            log.debug("DHCP server {} host info found. ConnectPt{}  Mac {} vlan {}", serverInfo.getDhcpServerIp4(),
+                    dhcpServerConnectPoint, dhcpServerConnectMac, dhcpConnectVlan);
+        } else {
+            log.warn("DHCP server {} not resolve yet connectPt {} mac {} vlan {}", serverInfo.getDhcpServerIp4(),
+                    dhcpServerConnectPoint, dhcpServerConnectMac, dhcpConnectVlan);
+
+            Ip4Address ipToProbe;
+            if (serverInfo.getDhcpGatewayIp4().isPresent()) {
+                ipToProbe = serverInfo.getDhcpGatewayIp4().get();
+            } else {
+                ipToProbe = serverInfo.getDhcpServerIp4().orElse(null);
+            }
+            String hostToProbe = serverInfo.getDhcpGatewayIp6()
+                    .map(ip -> "gateway").orElse("server");
+
+            log.warn("Dynamically probing to resolve {} IP {}", hostToProbe, ipToProbe);
+            hostService.startMonitoringIp(ipToProbe);
+
+            Set<Host> hosts = hostService.getHostsByIp(ipToProbe);
+            if (!hosts.isEmpty()) {
+                int serverInfoIndex = sererInfoList.indexOf(serverInfo);
+                Host host = hosts.iterator().next();
+                serverInfo.setDhcpConnectVlan(host.vlan());
+                serverInfo.setDhcpConnectMac(host.mac());
+                // replace the serverInfo in the list
+                sererInfoList.set(serverInfoIndex, serverInfo);
+                newServerInfo = serverInfo;
+                log.warn("Dynamically host found host {}", host);
+            } else {
+                log.warn("No host found host ip {} dynamically", ipToProbe);
+            }
+        }
+        return newServerInfo;
+    }
+
+    /**
+     * Gets Interface facing to the server for default host.
+     *
+     * @param serverInfo server information
+     * @return the Interface facing to the server; null if not found
+     */
+    private Interface getServerInterface(DhcpServerInfo serverInfo) {
+        Interface serverInterface = null;
+
+        ConnectPoint dhcpServerConnectPoint = serverInfo.getDhcpServerConnectPoint().orElse(null);
+        VlanId dhcpConnectVlan = serverInfo.getDhcpConnectVlan().orElse(null);
+
+        if (dhcpServerConnectPoint != null && dhcpConnectVlan != null) {
+            serverInterface = interfaceService.getInterfacesByPort(dhcpServerConnectPoint)
+                    .stream()
+                    .filter(iface -> Dhcp4HandlerUtil.interfaceContainsVlan(iface, dhcpConnectVlan))
+                    .findFirst()
+                    .orElse(null);
+        } else {
+            log.warn("DHCP server {} not resolve yet connectPoint {} vlan {}", serverInfo.getDhcpServerIp6(),
+                    dhcpServerConnectPoint, dhcpConnectVlan);
+        }
+
+        return serverInterface;
+    }
+
+    //forward the packet to ConnectPoint where the DHCP server is attached.
+    private void forwardPacket(InternalPacket packet) {
+        //send Packetout to dhcp server connectpoint.
+        if (packet.getDestLocation() != null) {
+            TrafficTreatment t = DefaultTrafficTreatment.builder()
+                    .setOutput(packet.getDestLocation().port()).build();
+            OutboundPacket o = new DefaultOutboundPacket(
+                    packet.getDestLocation().deviceId(), t, ByteBuffer.wrap(packet.getPacket().serialize()));
+            if (log.isTraceEnabled()) {
+                log.trace("Relaying packet to destination {}", packet.getDestLocation());
+            }
+            log.debug("packetService.emit(o) to port {}", packet.getDestLocation());
+            packetService.emit(o);
+        }
+    }
+
+
+    private DhcpServerInfo findServerInfoFromServer(boolean directConnFlag, ConnectPoint inPort) {
+        List<DhcpServerInfo> validServerInfoList = findValidServerInfo(directConnFlag);
+        DhcpServerInfo  foundServerInfo = null;
+        for (DhcpServerInfo serverInfo : validServerInfoList) {
+            if (inPort.equals(serverInfo.getDhcpServerConnectPoint().get())) {
+                foundServerInfo = serverInfo;
+                log.debug("ServerInfo found for Rcv port {} Server Connect Point {} for {}",
+                        inPort, serverInfo.getDhcpServerConnectPoint(), directConnFlag ? "direct" : "indirect");
+                break;
+            }
+        }
+        return foundServerInfo;
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/Dhcp4HandlerUtil.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/Dhcp4HandlerUtil.java
new file mode 100644
index 0000000..a88bda8
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/Dhcp4HandlerUtil.java
@@ -0,0 +1,86 @@
+/*
+ * 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 org.onlab.packet.Ip4Address;
+import org.onlab.packet.VlanId;
+import org.onlab.util.HexString;
+import org.onosproject.dhcprelay.api.DhcpServerInfo;
+import org.onosproject.net.host.InterfaceIpAddress;
+import org.onosproject.net.intf.Interface;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.Set;
+
+public final class Dhcp4HandlerUtil {
+    private static final Logger log = LoggerFactory.getLogger(Dhcp4HandlerUtil.class);
+
+    private Dhcp4HandlerUtil() {
+    }
+
+    /**
+     * Returns the first v4 interface ip out of a set of interfaces or null.
+     *
+     * @param intfs set of interfaces
+     * @return Ip4Address / null if not present
+     */
+    public static Ip4Address getRelayAgentIPv4Address(Set<Interface> intfs) {
+        for (Interface intf : intfs) {
+            for (InterfaceIpAddress ip : intf.ipAddressesList()) {
+                Ip4Address relayAgentIp = ip.ipAddress().getIp4Address();
+                if (relayAgentIp != null) {
+                    return relayAgentIp;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Determind if an Interface contains a vlan id.
+     *
+     * @param iface the Interface
+     * @param vlanId the vlan id
+     * @return true if the Interface contains the vlan id
+     */
+    public static boolean interfaceContainsVlan(Interface iface, VlanId vlanId) {
+        if (vlanId.equals(VlanId.NONE)) {
+            // untagged packet, check if vlan untagged or vlan native is not NONE
+            return !iface.vlanUntagged().equals(VlanId.NONE) ||
+                    !iface.vlanNative().equals(VlanId.NONE);
+        }
+        // tagged packet, check if the interface contains the vlan
+        return iface.vlanTagged().contains(vlanId);
+   }
+
+    /**
+     * Check if a given server info has v6 ipaddress.
+     *
+     * @param serverInfo server info to check
+     * @return true if server info has v6 ip address; false otherwise
+     */
+    public static boolean isServerIpEmpty(DhcpServerInfo serverInfo) {
+        if (!serverInfo.getDhcpServerIp4().isPresent()) {
+            log.warn("DhcpServerIp not available, use default DhcpServerIp {}",
+                    HexString.toHexString(serverInfo.getDhcpServerIp4().get().toOctets()));
+            return true;
+        }
+        return false;
+    }
+}
+
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerImpl.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerImpl.java
new file mode 100644
index 0000000..a5a9eb5
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerImpl.java
@@ -0,0 +1,1965 @@
+/*
+ * 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.HashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.Sets;
+import com.google.common.collect.ImmutableSet;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Modified;
+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.DHCP6;
+import org.onlab.packet.IPv6;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+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.Dhcp6ClientDataOption;
+import org.onlab.packet.dhcp.Dhcp6LeaseQueryOption;
+import org.onlab.packet.dhcp.Dhcp6RelayOption;
+import org.onlab.packet.dhcp.Dhcp6InterfaceIdOption;
+import org.onlab.packet.dhcp.Dhcp6Option;
+import org.onlab.packet.dhcp.Dhcp6IaNaOption;
+import org.onlab.packet.dhcp.Dhcp6IaTaOption;
+import org.onlab.packet.dhcp.Dhcp6IaPdOption;
+import org.onlab.packet.dhcp.Dhcp6IaAddressOption;
+import org.onlab.packet.dhcp.Dhcp6IaPrefixOption;
+import org.onlab.packet.dhcp.Dhcp6ClientIdOption;
+import org.onlab.packet.dhcp.Dhcp6Duid;
+import org.onlab.packet.DHCP6.MsgType;
+import org.onlab.util.HexString;
+import org.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.dhcprelay.api.DhcpHandler;
+import org.onosproject.dhcprelay.api.DhcpServerInfo;
+import org.onosproject.dhcprelay.config.IgnoreDhcpConfig;
+import org.onosproject.dhcprelay.store.DhcpRelayStore;
+import org.onosproject.dhcprelay.store.DhcpRecord;
+import org.onosproject.dhcprelay.store.DhcpFpmPrefixStore;
+import org.onosproject.dhcprelay.store.DhcpRelayCounters;
+import org.onosproject.dhcprelay.store.DhcpRelayCountersStore;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.routing.fpm.api.FpmRecord;
+import org.onosproject.net.behaviour.Pipeliner;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.Objective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.flowobjective.ObjectiveError;
+import org.onosproject.net.host.HostProvider;
+import org.onosproject.net.host.HostProviderRegistry;
+import org.onosproject.net.host.HostProviderService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.host.DefaultHostDescription;
+import org.onosproject.net.host.HostDescription;
+import org.onosproject.net.host.HostListener;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.intf.InterfaceService;
+import org.onosproject.net.packet.PacketPriority;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteStore;
+import org.onosproject.dhcprelay.config.DhcpServerConfig;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+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.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.net.flowobjective.Objective.Operation.ADD;
+import static org.onosproject.net.flowobjective.Objective.Operation.REMOVE;
+import java.util.concurrent.Semaphore;
+
+@Component
+@Service
+@Property(name = "version", value = "6")
+public class Dhcp6HandlerImpl implements DhcpHandler, HostProvider {
+    public static final String DHCP_V6_RELAY_APP = "org.onosproject.Dhcp6HandlerImpl";
+    public static final ProviderId PROVIDER_ID = new ProviderId("dhcp6", DHCP_V6_RELAY_APP);
+    private static final int IGNORE_CONTROL_PRIORITY = PacketPriority.CONTROL.priorityValue() + 1000;
+    private String gCount = "global";
+    private static final String LQ_ROUTE_PROPERTY_NAME = "learnRouteFromLeasequery";
+    private static final TrafficSelector CLIENT_SERVER_SELECTOR = DefaultTrafficSelector.builder()
+            .matchEthType(Ethernet.TYPE_IPV6)
+            .matchIPProtocol(IPv6.PROTOCOL_UDP)
+            .matchIPv6Src(IpPrefix.IPV6_LINK_LOCAL_PREFIX)
+            .matchIPv6Dst(Ip6Address.ALL_DHCP_RELAY_AGENTS_AND_SERVERS.toIpPrefix())
+            .matchUdpSrc(TpPort.tpPort(UDP.DHCP_V6_CLIENT_PORT))
+            .matchUdpDst(TpPort.tpPort(UDP.DHCP_V6_SERVER_PORT))
+            .build();
+    private static final TrafficSelector SERVER_RELAY_SELECTOR = DefaultTrafficSelector.builder()
+            .matchEthType(Ethernet.TYPE_IPV6)
+            .matchIPProtocol(IPv6.PROTOCOL_UDP)
+            .matchUdpSrc(TpPort.tpPort(UDP.DHCP_V6_SERVER_PORT))
+            .matchUdpDst(TpPort.tpPort(UDP.DHCP_V6_SERVER_PORT))
+            .build();
+    // lease query reply is from server to client (no relay in between) - so we need to
+    // catch that scenario also ..
+    private static final TrafficSelector LEASE_QUERY_RESPONSE_SELECTOR = DefaultTrafficSelector.builder()
+            .matchEthType(Ethernet.TYPE_IPV6)
+            .matchIPProtocol(IPv6.PROTOCOL_UDP)
+            .matchUdpSrc(TpPort.tpPort(UDP.DHCP_V6_SERVER_PORT))
+            .matchUdpDst(TpPort.tpPort(UDP.DHCP_V6_CLIENT_PORT))
+            .build();
+    static final Set<TrafficSelector> DHCP_SELECTORS = ImmutableSet.of(
+            CLIENT_SERVER_SELECTOR,
+            SERVER_RELAY_SELECTOR,
+            LEASE_QUERY_RESPONSE_SELECTOR
+    );
+    private static Logger log = LoggerFactory.getLogger(Dhcp6HandlerImpl.class);
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DhcpRelayStore dhcpRelayStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DhcpRelayCountersStore dhcpRelayCountersStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected PacketService packetService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected RouteStore routeStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected InterfaceService interfaceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostService hostService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostProviderRegistry providerRegistry;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DhcpFpmPrefixStore dhcpFpmPrefixStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected FlowObjectiveService flowObjectiveService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ComponentConfigService cfgService;
+
+    @Property(name = Dhcp6HandlerImpl.LQ_ROUTE_PROPERTY_NAME, boolValue = false,
+            label = "Enable learning routing information from LQ")
+    private Boolean learnRouteFromLeasequery = Boolean.TRUE;
+
+    protected HostProviderService providerService;
+    protected ApplicationId appId;
+    protected Multimap<DeviceId, VlanId> ignoredVlans = Multimaps.synchronizedMultimap(HashMultimap.create());
+    private InternalHostListener hostListener = new InternalHostListener();
+    private Boolean dhcpFpmEnabled = false;
+    private List<DhcpServerInfo> defaultServerInfoList = new CopyOnWriteArrayList<>();
+    private List<DhcpServerInfo> indirectServerInfoList = new CopyOnWriteArrayList<>();
+
+    private Executor hostEventExecutor = newSingleThreadExecutor(
+        groupedThreads("dhcp6-event-host", "%d", log));
+
+    private class IpAddressInfo {
+        Ip6Address ip6Address;
+        long    prefTime;
+    }
+    private class PdPrefixInfo {
+        IpPrefix pdPrefix;
+        long    prefTime;
+    }
+    protected int dhcp6PollInterval = 24 * 3600; // 24 hr period
+
+    // max 1 thread
+    static Semaphore recordSemaphore = new Semaphore(1);
+
+    // CLIENT message types
+    public static final Set<Byte> MSG_TYPE_FROM_CLIENT =
+            ImmutableSet.of(DHCP6.MsgType.SOLICIT.value(),
+                            DHCP6.MsgType.REQUEST.value(),
+                            DHCP6.MsgType.REBIND.value(),
+                            DHCP6.MsgType.RENEW.value(),
+                            DHCP6.MsgType.RELEASE.value(),
+                            DHCP6.MsgType.DECLINE.value(),
+                            DHCP6.MsgType.CONFIRM.value(),
+                            DHCP6.MsgType.RELAY_FORW.value(),
+                            DHCP6.MsgType.LEASEQUERY.value());
+    // SERVER message types
+    public static final Set<Byte> MSG_TYPE_FROM_SERVER =
+            ImmutableSet.of(DHCP6.MsgType.RELAY_REPL.value(),
+                            DHCP6.MsgType.LEASEQUERY_REPLY.value());
+
+    @Activate
+    protected void activate(ComponentContext context) {
+        cfgService.registerProperties(getClass());
+        modified(context);
+        appId = coreService.registerApplication(DHCP_V6_RELAY_APP);
+        providerService = providerRegistry.register(this);
+        hostService.addListener(hostListener);
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        cfgService.unregisterProperties(getClass(), false);
+        providerRegistry.unregister(this);
+        hostService.removeListener(hostListener);
+        defaultServerInfoList.forEach(this::stopMonitoringIps);
+        defaultServerInfoList.forEach(info -> info.getDhcpServerIp6().ifPresent(this::cancelDhcpPacket));
+        defaultServerInfoList.clear();
+        indirectServerInfoList.forEach(this::stopMonitoringIps);
+        indirectServerInfoList.forEach(info -> info.getDhcpServerIp6().ifPresent(this::cancelDhcpPacket));
+        indirectServerInfoList.clear();
+    }
+
+    @Modified
+    protected void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context.getProperties();
+        Boolean flag;
+        flag = Tools.isPropertyEnabled(properties, Dhcp6HandlerImpl.LQ_ROUTE_PROPERTY_NAME);
+        if (flag != null) {
+            learnRouteFromLeasequery = flag;
+            log.info("Learning routes from DHCP leasequery is {}",
+                    learnRouteFromLeasequery ? "enabled" : "disabled");
+        }
+    }
+
+    private void stopMonitoringIps(DhcpServerInfo serverInfo) {
+        serverInfo.getDhcpGatewayIp6().ifPresent(gatewayIp -> {
+            hostService.stopMonitoringIp(gatewayIp);
+        });
+        serverInfo.getDhcpServerIp6().ifPresent(serverIp -> {
+            hostService.stopMonitoringIp(serverIp);
+        });
+    }
+
+    @Override
+    public List<DhcpServerInfo> getDefaultDhcpServerInfoList() {
+        return defaultServerInfoList;
+    }
+
+    @Override
+    public List<DhcpServerInfo> getIndirectDhcpServerInfoList() {
+        return indirectServerInfoList;
+    }
+
+    @Override
+    public void updateIgnoreVlanConfig(IgnoreDhcpConfig config) {
+        if (config == null) {
+            ignoredVlans.forEach(((deviceId, vlanId) -> {
+                processIgnoreVlanRule(deviceId, vlanId, REMOVE);
+            }));
+            return;
+        }
+        config.ignoredVlans().forEach((deviceId, vlanId) -> {
+            if (ignoredVlans.get(deviceId).contains(vlanId)) {
+                // don't need to process if it already ignored
+                return;
+            }
+            processIgnoreVlanRule(deviceId, vlanId, ADD);
+        });
+        ignoredVlans.forEach((deviceId, vlanId) -> {
+            if (!config.ignoredVlans().get(deviceId).contains(vlanId)) {
+                // not contains in new config, remove it
+                processIgnoreVlanRule(deviceId, vlanId, REMOVE);
+            }
+        });
+    }
+
+    @Override
+    public void removeIgnoreVlanState(IgnoreDhcpConfig config) {
+        if (config == null) {
+            ignoredVlans.clear();
+            return;
+        }
+        config.ignoredVlans().forEach((deviceId, vlanId) -> {
+            ignoredVlans.remove(deviceId, vlanId);
+        });
+    }
+
+    public DhcpRecord getDhcpRelayRecordFor(Ip6Address clientAddress) {
+
+        Collection<DhcpRecord> records = dhcpRelayStore.getDhcpRecords();
+        DhcpRecord dr = null;
+        for (DhcpRecord e:records) {
+            if (e.ip6Address().isPresent()) {
+                if (e.ip6Address().get().equals(clientAddress)) {
+                    dr = e;
+                    break;
+                }
+            }
+        }
+        return dr;
+    }
+
+    public MacAddress findNextHopMacForIp6FromRelayStore(Ip6Address clientAddress,
+                                                         MacAddress clientMacAddress, VlanId vlanID) {
+
+        DhcpRecord dr = getDhcpRelayRecordFor(clientAddress);
+
+        if (dr != null) {
+           Optional<MacAddress> nextHopTempMac = dr.nextHopTemp();
+            if (nextHopTempMac.isPresent()) {
+                log.info("findNextHopForIp6FromRelayStore " + clientAddress + " got mac " + nextHopTempMac.toString());
+                return nextHopTempMac.get();
+            }
+        } else {
+            log.warn("findNextHopMacForIp6FromRelayStore could NOT find next hop for " + clientAddress);
+            return null;
+        }
+        return null;
+    }
+
+    public Ip6Address findNextHopIp6FromRelayStore(Ip6Address clientAddress) {
+
+        DhcpRecord dr = getDhcpRelayRecordFor(clientAddress);
+        if (dr != null) {
+            Optional<MacAddress> nextHopMac = dr.nextHop();
+            if (nextHopMac.isPresent()) {
+                // find the local ip6 from the host store
+                HostId gwHostId = HostId.hostId(nextHopMac.get(), dr.vlanId());
+                Host gwHost = hostService.getHost(gwHostId);
+                if (gwHost == null) {
+                    log.warn("Can't find next hop host ID {}", gwHostId);
+                    return null;
+                }
+                Ip6Address nextHopIp = gwHost.ipAddresses()
+                        .stream()
+                        .filter(IpAddress::isIp6)
+                        .filter(IpAddress::isLinkLocal)
+                        .map(IpAddress::getIp6Address)
+                        .findFirst()
+                        .orElse(null);
+
+                log.info("findNextHopIp6FromRelayStore " + clientAddress + " got mac " +
+                                 nextHopMac.toString() + " ip6 " + nextHopIp);
+                return nextHopIp;
+            }
+        } else {
+            log.warn("findNextHopIp6FromRelayStore could NOT find next hop for " + clientAddress);
+            return null;
+        }
+        return null;
+    }
+
+    private void setPotentialNextHopForIp6InRelayStore(Ip6Address clientAddress,
+                                                       VlanId vlanId, MacAddress nh) {
+        DhcpRecord dr = getDhcpRelayRecordFor(clientAddress);
+        if (dr != null) {
+            dr.nextHopTemp(nh);
+            log.debug("LQ6 potential NH mac " + nh.toString() + " UPDATED in RelayRecord client " + clientAddress);
+        } else {
+            log.warn("LQ6 potential NH mac" + nh.toString() +
+                             " NOT FOUND in RelayRecord for client - LQ rejected" + clientAddress);
+        }
+    }
+
+    public void handleLeaseQuery6ReplyMsg(PacketContext context, DHCP6 dhcp6Payload) {
+        ConnectPoint inPort = context.inPacket().receivedFrom();
+        log.info("Got LQV6-REPLY on port {}", inPort);
+        List<Dhcp6Option> lopt = dhcp6Payload.getOptions();
+        log.info("Options list: {}", lopt);
+        // find out if this lease is known is
+        Dhcp6ClientDataOption clientDataOption = dhcp6Payload.getOptions()
+                .stream()
+                .filter(opt -> opt instanceof Dhcp6ClientDataOption)
+                .map(pld -> (Dhcp6ClientDataOption) pld)
+                .findFirst()
+                .orElse(null);
+
+        if (clientDataOption == null) {
+            log.warn("clientDataOption option is not present, " +
+                             "lease is UNKNOWN - not adding any new route...");
+        } else {
+            Dhcp6IaAddressOption aiAddressOption = clientDataOption.getOptions()
+                    .stream()
+                    .filter(opt -> opt instanceof Dhcp6IaAddressOption)
+                    .map(pld -> (Dhcp6IaAddressOption) pld)
+                    .findFirst()
+                    .orElse(null);
+
+            Dhcp6ClientIdOption clientIdOption = clientDataOption.getOptions()
+                    .stream()
+                    .filter(opt -> opt instanceof Dhcp6ClientIdOption)
+                    .map(pld -> (Dhcp6ClientIdOption) pld)
+                    .findFirst()
+                    .orElse(null);
+
+            if (aiAddressOption == null) {
+                log.warn("clientDataOption from DHCP server does not " +
+                                 "contains Dhcp6IaAddressOption for the client - giving up...");
+            } else {
+                Ip6Address clientAddress = aiAddressOption.getIp6Address();
+                MacAddress clientMacAddress = MacAddress.valueOf(clientIdOption.getDuid().getLinkLayerAddress());
+                Ethernet packet = context.inPacket().parsed();
+                VlanId vlanId = VlanId.vlanId(packet.getVlanID());
+                MacAddress potentialNextHopMac =
+                        findNextHopMacForIp6FromRelayStore(clientAddress, clientMacAddress, vlanId);
+
+                if (potentialNextHopMac == null) {
+                    log.warn("Can't find next hop host mac for client {} mac:{}/{}",
+                             clientAddress, clientMacAddress, vlanId);
+                    return;
+                } else {
+                    log.info("Next hop mac for {}/{}/{} is {}", clientAddress,
+                             clientMacAddress, vlanId, potentialNextHopMac.toString());
+                }
+                // search the next hop in the hosts store
+                HostId gwHostId = HostId.hostId(potentialNextHopMac, vlanId);
+                Host gwHost = hostService.getHost(gwHostId);
+                if (gwHost == null) {
+                    log.warn("Can't find next hop host ID {}", gwHostId);
+                    return;
+                }
+                Ip6Address nextHopIp = gwHost.ipAddresses()
+                        .stream()
+                        .filter(IpAddress::isIp6)
+                        .filter(IpAddress::isLinkLocal)
+                        .map(IpAddress::getIp6Address)
+                        .findFirst()
+                        .orElse(null);
+                if (nextHopIp == null) {
+                    log.warn("Can't find IP6 address of next hop {}", gwHost);
+                    return;
+                }
+                log.info("client " + clientAddress + " is known !");
+                Route routeForIP6 = new Route(Route.Source.DHCP, clientAddress.toIpPrefix(), nextHopIp);
+                log.debug("updating route of Client for indirectly connected.");
+                log.debug("client ip: " + clientAddress + ", next hop ip6: " + nextHopIp);
+                routeStore.updateRoute(routeForIP6);
+            }
+        }
+    }
+
+    @Override
+    public void processDhcpPacket(PacketContext context, BasePacket payload) {
+        checkNotNull(payload, "DHCP6 payload can't be null");
+        checkState(payload instanceof DHCP6, "Payload is not a DHCP6");
+        DHCP6 dhcp6Payload = (DHCP6) payload;
+        Ethernet receivedPacket = context.inPacket().parsed();
+
+        if (!configured()) {
+            log.warn("Missing DHCP6 relay server config. " +
+                             "Abort packet processing dhcp6 payload {}", dhcp6Payload);
+            return;
+        }
+        byte msgTypeVal = dhcp6Payload.getMsgType();
+        MsgType msgType = DHCP6.MsgType.getType(msgTypeVal);
+        log.debug("msgType is {}", msgType);
+
+        ConnectPoint inPort = context.inPacket().receivedFrom();
+
+        if (inPort == null) {
+            log.warn("incoming ConnectPoint is null");
+        }
+        Set<Interface> receivingInterfaces = interfaceService.getInterfacesByPort(inPort);
+        //ignore the packets if dhcp client interface is not configured on onos.
+        if (receivingInterfaces.isEmpty()) {
+            log.warn("Virtual interface is not configured on {}", inPort);
+            return;
+        }
+
+        if (msgTypeVal == DHCP6.MsgType.LEASEQUERY.value()) {
+            List<InternalPacket> ethernetClientPackets =
+                    learnRouteFromLeasequery ?
+                        processLQ6PacketFromClient(context, receivedPacket, receivingInterfaces, dhcp6Payload) :
+                        processDhcp6ForwardOnly(context, receivedPacket, receivingInterfaces, dhcp6Payload);
+            for (InternalPacket internalPacket : ethernetClientPackets) {
+                forwardPacket(internalPacket);
+            }
+        } else if (msgTypeVal == DHCP6.MsgType.LEASEQUERY_REPLY.value() && learnRouteFromLeasequery) {
+            IPv6 clientIpv6 = (IPv6) receivedPacket.getPayload();
+            UDP clientUdp = (UDP) clientIpv6.getPayload();
+            DHCP6 clientDhcp6 = (DHCP6) clientUdp.getPayload();
+            Interface serverInterface = Dhcp6HandlerUtil.directlyConnected(clientDhcp6) ?
+                    getServerInterface() : getIndirectServerInterface();
+            InternalPacket ethernetPacketReply =
+                    Dhcp6HandlerUtil.processLQ6PacketFromServer(
+                            defaultServerInfoList, indirectServerInfoList,
+                            serverInterface, interfaceService,
+                            hostService,
+                            context, receivedPacket, receivingInterfaces);
+            if (ethernetPacketReply != null) {
+                forwardPacket(ethernetPacketReply);
+            }
+            handleLeaseQuery6ReplyMsg(context, dhcp6Payload);
+        } else if (MSG_TYPE_FROM_CLIENT.contains(msgTypeVal)) {
+            List<InternalPacket> ethernetClientPacket =
+                    processDhcp6PacketFromClient(context, receivedPacket, receivingInterfaces);
+            for (InternalPacket internalPacket : ethernetClientPacket) {
+                forwardPacket(internalPacket);
+            }
+        } else if (MSG_TYPE_FROM_SERVER.contains(msgTypeVal)) {
+            log.debug("calling processDhcp6PacketFromServer with RELAY_REPL {}, {}", receivedPacket, dhcp6Payload);
+            InternalPacket ethernetPacketReply =
+                    processDhcp6PacketFromServer(context, receivedPacket, receivingInterfaces);
+            if (ethernetPacketReply != null) {
+                forwardPacket(ethernetPacketReply);
+            }
+        } else {
+            log.warn("Not so fast, packet type {} not supported yet", msgTypeVal);
+        }
+    }
+
+    /**
+     * Checks if this app has been configured.
+     *
+     * @return true if all information we need have been initialized
+     */
+    public boolean configured() {
+        return !defaultServerInfoList.isEmpty();
+    }
+
+    @Override
+    public ProviderId id() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public void triggerProbe(Host host) {
+        // Do nothing here
+    }
+
+    //forward the packet to ConnectPoint where the DHCP server is attached.
+    private void forwardPacket(InternalPacket packet) {
+        //send Packetout to dhcp server connectpoint.
+        if (packet.getDestLocation() != null) {
+            TrafficTreatment t = DefaultTrafficTreatment.builder()
+                    .setOutput(packet.getDestLocation().port()).build();
+            OutboundPacket o = new DefaultOutboundPacket(
+                    packet.getDestLocation().deviceId(), t, ByteBuffer.wrap(packet.getPacket().serialize()));
+            packetService.emit(o);
+            if (log.isTraceEnabled()) {
+                IPv6 ip6 = (IPv6) packet.getPacket().getPayload();
+                UDP udp = (UDP) ip6.getPayload();
+                DHCP6 dhcp6  = (DHCP6) udp.getPayload();
+                log.trace("Relaying packet to destination {} eth: {} dhcp: {}",
+                        packet.getDestLocation(), packet.getPacket(), dhcp6);
+            }
+
+        }
+    }
+
+    /**
+     * extract from dhcp6 packet client ipv6 address of given by dhcp server.
+     *
+     * @param dhcp6 the dhcp6 packet
+     * @return IpAddressInfo  IpAddressInfo given by dhcp server, or null if not exists
+     */
+    private IpAddressInfo extractIpAddress(DHCP6 dhcp6) {
+        IpAddressInfo ipInfo = new IpAddressInfo();
+
+        log.debug("extractIpAddress  enters dhcp6 {}.", dhcp6);
+        // 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()) {
+            log.debug("Found IPv6 address from iaNaOption {}", iaNaOption);
+
+            iaAddressOption = iaNaOption.get().getOptions().stream()
+                    .filter(opt -> opt instanceof Dhcp6IaAddressOption)
+                    .map(opt -> (Dhcp6IaAddressOption) opt)
+                    .findFirst();
+        } else if (iaTaOption.isPresent()) {
+            log.debug("Found IPv6 address from iaTaOption {}", iaTaOption);
+
+            iaAddressOption = iaTaOption.get().getOptions().stream()
+                    .filter(opt -> opt instanceof Dhcp6IaAddressOption)
+                    .map(opt -> (Dhcp6IaAddressOption) opt)
+                    .findFirst();
+        } else {
+            log.info("No IPv6 address found from iaTaOption {}", iaTaOption);
+            iaAddressOption = Optional.empty();
+        }
+        if (iaAddressOption.isPresent()) {
+            ipInfo.ip6Address = iaAddressOption.get().getIp6Address();
+            ipInfo.prefTime = iaAddressOption.get().getPreferredLifetime();
+            log.debug("Found IPv6 address from iaAddressOption {}", iaAddressOption);
+        } else {
+            log.debug("Can't find IPv6 address from DHCPv6 {}", dhcp6);
+            return null;
+        }
+        return ipInfo;
+    }
+
+    /**
+     * extract from dhcp6 packet Prefix prefix provided by dhcp server.
+     *
+     * @param dhcp6 the dhcp6 payload
+     * @return IpPrefix Prefix Delegation prefix, or null if not exists.
+     */
+    private PdPrefixInfo extractPrefix(DHCP6 dhcp6) {
+        log.debug("extractPrefix  enters {}", dhcp6);
+
+        // extract prefix
+        PdPrefixInfo  pdPrefixInfo = new PdPrefixInfo();
+
+        Ip6Address prefixAddress = null;
+
+        // Extract IPv6 prefix from IA PD option
+        Optional<Dhcp6IaPdOption> iaPdOption = dhcp6.getOptions()
+                .stream()
+                .filter(opt -> opt instanceof Dhcp6IaPdOption)
+                .map(opt -> (Dhcp6IaPdOption) opt)
+                .findFirst();
+
+        Optional<Dhcp6IaPrefixOption> iaPrefixOption;
+        if (iaPdOption.isPresent()) {
+            log.debug("IA_PD option found {}", iaPdOption);
+
+            iaPrefixOption = iaPdOption.get().getOptions().stream()
+                    .filter(opt -> opt instanceof Dhcp6IaPrefixOption)
+                    .map(opt -> (Dhcp6IaPrefixOption) opt)
+                    .findFirst();
+        } else {
+            log.debug("IA_PD option NOT found");
+
+            iaPrefixOption = Optional.empty();
+        }
+        if (iaPrefixOption.isPresent()) {
+            log.debug("IAPrefix Option within IA_PD option found {}", iaPrefixOption);
+
+            prefixAddress = iaPrefixOption.get().getIp6Prefix();
+            int prefixLen = (int) iaPrefixOption.get().getPrefixLength();
+            log.debug("Prefix length is  {} bits", prefixLen);
+            pdPrefixInfo.pdPrefix = IpPrefix.valueOf(prefixAddress, prefixLen);
+            pdPrefixInfo.prefTime = iaPrefixOption.get().getPreferredLifetime();
+        } else {
+            log.debug("Can't find IPv6 prefix from DHCPv6 {}", dhcp6);
+            return null;
+        }
+        return pdPrefixInfo;
+    }
+
+    /**
+     * remove host or route and update dhcp relay record attributes.
+     *
+     * @param directConnFlag  flag to show that packet is from directly connected client
+     * @param location  client side connect point
+     * @param dhcp6Packet the dhcp6 payload
+     * @param clientPacket client's ethernet packet
+     * @param clientIpv6 client's Ipv6 packet
+     * @param clientInterface client interfaces
+     */
+    private void removeHostOrRoute(boolean directConnFlag, ConnectPoint location,
+                                   DHCP6 dhcp6Packet,
+                                   Ethernet clientPacket, IPv6 clientIpv6,
+                                   Interface clientInterface) {
+        log.debug("removeHostOrRoute  enters {}", dhcp6Packet);
+        VlanId vlanId = clientInterface.vlan();
+        MacAddress srcMac = clientPacket.getSourceMAC();  // could be gw or host
+        MacAddress leafClientMac;
+        byte leafMsgType;
+        log.debug("client mac {} client vlan {}", HexString.toHexString(srcMac.toBytes(), ":"), vlanId);
+
+        Dhcp6ClientIdOption clientIdOption = Dhcp6HandlerUtil.extractClientId(directConnFlag, dhcp6Packet);
+        if (clientIdOption != null) {
+            if ((clientIdOption.getDuid().getDuidType() == Dhcp6Duid.DuidType.DUID_LLT) ||
+                    (clientIdOption.getDuid().getDuidType() == Dhcp6Duid.DuidType.DUID_LL)) {
+                leafClientMac = MacAddress.valueOf(clientIdOption.getDuid().getLinkLayerAddress());
+            } else {
+                log.warn("Link-Layer Address not supported in CLIENTID option. No DhcpRelay Record created.");
+                dhcpRelayCountersStore.incrementCounter(gCount, DhcpRelayCounters.NO_LINKLOCAL_FAIL);
+                return;
+            }
+        } else {
+            log.warn("CLIENTID option NOT found. Don't create DhcpRelay Record.");
+            dhcpRelayCountersStore.incrementCounter(gCount, DhcpRelayCounters.NO_CLIENTID_FAIL);
+            return;
+        }
+
+        HostId leafHostId = HostId.hostId(leafClientMac, vlanId);
+        DhcpRecord record = dhcpRelayStore.getDhcpRecord(leafHostId).orElse(null);
+        if (record == null) {
+            record = new DhcpRecord(leafHostId);
+        }  else {
+            record = record.clone();
+        }
+
+        Boolean isMsgRelease = Dhcp6HandlerUtil.isDhcp6Release(dhcp6Packet);
+        IpAddressInfo ipInfo;
+        PdPrefixInfo pdInfo = null;
+        if (directConnFlag) {
+            // Add to host store if it is connected to network directly
+            ipInfo = extractIpAddress(dhcp6Packet);
+            if (ipInfo != null) {
+                if (isMsgRelease) {
+                    HostId hostId = HostId.hostId(srcMac, vlanId);
+                    log.debug("remove Host {} ip for directly connected.", hostId.toString());
+                    providerService.removeIpFromHost(hostId, ipInfo.ip6Address);
+                }
+            } else {
+                log.debug("ipAddress not found. Do not remove Host {} for directly connected.",
+                        HostId.hostId(srcMac, vlanId).toString());
+            }
+            leafMsgType = dhcp6Packet.getMsgType();
+        } else {
+            // Remove from route store if it is not connected to network directly
+            // pick out the first link-local ip address
+            IpAddress nextHopIp = getFirstIpByHost(directConnFlag, srcMac, vlanId);
+            if (nextHopIp == null) {
+                log.warn("Can't find link-local IP address of gateway mac {} vlanId {}", srcMac, vlanId);
+                dhcpRelayCountersStore.incrementCounter(gCount, DhcpRelayCounters.NO_LINKLOCAL_GW);
+                return;
+            }
+
+            DHCP6 leafDhcp = Dhcp6HandlerUtil.getDhcp6Leaf(dhcp6Packet);
+            ipInfo = extractIpAddress(leafDhcp);
+            if (ipInfo == null) {
+                log.debug("ip is null");
+            } else {
+                if (isMsgRelease) {
+                    Route routeForIP = new Route(Route.Source.DHCP, ipInfo.ip6Address.toIpPrefix(), nextHopIp);
+                    log.debug("removing route of 128 address for indirectly connected.");
+                    log.debug("128 ip {}, nexthop {}",
+                            HexString.toHexString(ipInfo.ip6Address.toOctets(), ":"),
+                            HexString.toHexString(nextHopIp.toOctets(), ":"));
+                    routeStore.removeRoute(routeForIP);
+                }
+            }
+
+            pdInfo = extractPrefix(leafDhcp);
+            if (pdInfo == null) {
+                log.debug("ipPrefix is null ");
+            } else {
+                if (isMsgRelease) {
+                    Route routeForPrefix = new Route(Route.Source.DHCP, pdInfo.pdPrefix, nextHopIp);
+                    log.debug("removing route of PD for indirectly connected.");
+                    log.debug("pd ip {}, nexthop {}",
+                            HexString.toHexString(pdInfo.pdPrefix.address().toOctets(), ":"),
+                            HexString.toHexString(nextHopIp.toOctets(), ":"));
+
+                    routeStore.removeRoute(routeForPrefix);
+                    if (this.dhcpFpmEnabled) {
+                        dhcpFpmPrefixStore.removeFpmRecord(pdInfo.pdPrefix);
+                    }
+                }
+            }
+            leafMsgType = leafDhcp.getMsgType();
+       }
+
+        if (isMsgRelease) {
+            log.debug("DHCP6 RELEASE msg.");
+            if (record != null) {
+                if (ipInfo != null) {
+                    log.debug("DhcpRelay Record ip6Address is set to null.");
+                    record.ip6Address(null);
+                }
+                if (pdInfo != null) {
+                    log.debug("DhcpRelay Record pdPrefix is set to null.");
+                }
+
+                if (!record.ip6Address().isPresent() && !record.pdPrefix().isPresent()) {
+                    log.warn("IP6 address and IP6 PD both are null. Remove record.");
+                    // do not remove a record. Let timer task handler it.
+                    //dhcpRelayStore.removeDhcpRecord(HostId.hostId(leafClientMac, vlanId));
+                }
+            }
+        }
+
+        if (record != null) {
+            record.getV6Counters().incrementCounter(Dhcp6HandlerUtil.getMsgTypeStr(leafMsgType));
+            record.addLocation(new HostLocation(location, System.currentTimeMillis()));
+            record.ip6Status(DHCP6.MsgType.getType(leafMsgType));
+            record.setDirectlyConnected(directConnFlag);
+            if (!directConnFlag) {
+                // Update gateway mac address if the host is not directly connected
+                record.nextHop(srcMac);
+            }
+            record.updateLastSeen();
+        }
+        dhcpRelayStore.updateDhcpRecord(leafHostId, record);
+        // TODO Use AtomicInteger for the counters
+        try {
+            recordSemaphore.acquire();
+            try {
+                dhcpRelayCountersStore.incrementCounter(gCount, Dhcp6HandlerUtil.getMsgTypeStr(leafMsgType));
+            } finally {
+                // calling release() after a successful acquire()
+                recordSemaphore.release();
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    /**
+     * add host or route and update dhcp relay record.
+     *
+     * @param directConnFlag  flag to show that packet is from directly connected client
+     * @param location  client side connect point
+     * @param dhcp6Relay the dhcp6 payload
+     * @param embeddedDhcp6 the dhcp6 payload within relay
+     * @param srcMac client gw/host macAddress
+     * @param clientInterface client interface
+     */
+    private void addHostOrRoute(boolean directConnFlag, ConnectPoint location, DHCP6 dhcp6Relay,
+                                DHCP6 embeddedDhcp6, MacAddress srcMac, Interface clientInterface) {
+        log.debug("addHostOrRoute entered.");
+        VlanId vlanId = clientInterface.vlan();
+        Boolean isMsgReply = Dhcp6HandlerUtil.isDhcp6Reply(dhcp6Relay);
+        MacAddress leafClientMac;
+        Byte leafMsgType;
+
+        Dhcp6ClientIdOption clientIdOption = Dhcp6HandlerUtil.extractClientId(directConnFlag, embeddedDhcp6);
+        if (clientIdOption != null) {
+            log.debug("CLIENTID option found {}", clientIdOption);
+            if ((clientIdOption.getDuid().getDuidType() == Dhcp6Duid.DuidType.DUID_LLT) ||
+                    (clientIdOption.getDuid().getDuidType() == Dhcp6Duid.DuidType.DUID_LL)) {
+                leafClientMac = MacAddress.valueOf(clientIdOption.getDuid().getLinkLayerAddress());
+            } else {
+                log.warn("Link-Layer Address not supported in CLIENTID option. No DhcpRelay Record created.");
+                dhcpRelayCountersStore.incrementCounter(gCount, DhcpRelayCounters.NO_LINKLOCAL_FAIL);
+                return;
+            }
+        } else {
+            log.warn("CLIENTID option NOT found. No DhcpRelay Record created.");
+            dhcpRelayCountersStore.incrementCounter(gCount, DhcpRelayCounters.NO_CLIENTID_FAIL);
+            return;
+        }
+        HostId leafHostId = HostId.hostId(leafClientMac, vlanId);
+        DhcpRecord record = dhcpRelayStore.getDhcpRecord(leafHostId).orElse(null);
+        if (record == null) {
+            record = new DhcpRecord(HostId.hostId(leafClientMac, vlanId));
+        } else {
+            record = record.clone();
+        }
+
+        IpAddressInfo ipInfo;
+        PdPrefixInfo pdInfo = null;
+        if (directConnFlag) {
+            // Add to host store if it connect to network directly
+            ipInfo = extractIpAddress(embeddedDhcp6);
+            if (ipInfo != null) {
+                if (isMsgReply) {
+                    Set<IpAddress> ips = Sets.newHashSet(ipInfo.ip6Address);
+                    HostId hostId = HostId.hostId(srcMac, vlanId);
+                    Host host = hostService.getHost(hostId);
+                    HostLocation hostLocation = new HostLocation(clientInterface.connectPoint(),
+                            System.currentTimeMillis());
+                    Set<HostLocation> hostLocations = Sets.newHashSet(hostLocation);
+                    if (host != null) {
+                        // Dual homing support:
+                        // if host exists, use old locations and new location
+                        hostLocations.addAll(host.locations());
+                    }
+                    HostDescription desc = new DefaultHostDescription(srcMac, vlanId, hostLocations, ips,
+                            false);
+                    log.debug("adding Host for directly connected.");
+                    log.debug("client mac {} client vlan {} hostlocation {}",
+                            HexString.toHexString(srcMac.toBytes(), ":"), vlanId, hostLocation.toString());
+                    // Replace the ip when dhcp server give the host new ip address
+                    providerService.hostDetected(hostId, desc, false);
+                }
+            } else {
+                log.warn("ipAddress not found. Do not add Host {} for directly connected.",
+                        HostId.hostId(srcMac, vlanId).toString());
+            }
+            leafMsgType = embeddedDhcp6.getMsgType();
+        } else {
+            // Add to route store if it does not connect to network directly
+            // pick out the first link-local ip address
+            IpAddress nextHopIp = getFirstIpByHost(directConnFlag, srcMac, vlanId);
+            if (nextHopIp == null) {
+                log.warn("Can't find link-local IP address of gateway mac {} vlanId {}", srcMac, vlanId);
+                dhcpRelayCountersStore.incrementCounter(gCount, DhcpRelayCounters.NO_LINKLOCAL_GW);
+                return;
+            }
+
+            DHCP6 leafDhcp = Dhcp6HandlerUtil.getDhcp6Leaf(embeddedDhcp6);
+            ipInfo = extractIpAddress(leafDhcp);
+            if (ipInfo == null) {
+                log.debug("ip is null");
+            } else {
+                if (isMsgReply) {
+                    Route routeForIP = new Route(Route.Source.DHCP, ipInfo.ip6Address.toIpPrefix(), nextHopIp);
+                    log.debug("adding Route of 128 address for indirectly connected.");
+                    routeStore.replaceRoute(routeForIP);
+                }
+            }
+
+            pdInfo = extractPrefix(leafDhcp);
+            if (pdInfo == null) {
+                log.debug("ipPrefix is null ");
+            } else {
+                if (isMsgReply) {
+                    Route routeForPrefix = new Route(Route.Source.DHCP, pdInfo.pdPrefix, nextHopIp);
+                    log.debug("adding Route of PD for indirectly connected.");
+                    routeStore.replaceRoute(routeForPrefix);
+                    if (this.dhcpFpmEnabled) {
+                        FpmRecord fpmRecord = new FpmRecord(pdInfo.pdPrefix, nextHopIp, FpmRecord.Type.DHCP_RELAY);
+                        dhcpFpmPrefixStore.addFpmRecord(pdInfo.pdPrefix, fpmRecord);
+                    }
+                }
+            }
+            leafMsgType = leafDhcp.getMsgType();
+        }
+        if (leafMsgType == DHCP6.MsgType.RELEASE.value() ||
+                (leafMsgType == DHCP6.MsgType.REPLY.value()) && ipInfo == null) {
+            log.warn("DHCP6 RELEASE/REPLY(null ip) from Server. MsgType {}", leafMsgType);
+            //return;
+        }
+
+        record.addLocation(new HostLocation(location, System.currentTimeMillis()));
+
+        if (leafMsgType == DHCP6.MsgType.REPLY.value()) {
+            if (ipInfo != null) {
+                log.debug("IP6 address is being stored into dhcp-relay store.");
+                log.debug("Client IP6 address {}", HexString.toHexString(ipInfo.ip6Address.toOctets(), ":"));
+                record.ip6Address(ipInfo.ip6Address);
+                record.updateAddrPrefTime(ipInfo.prefTime);
+                record.updateLastIp6Update();
+             } else {
+                log.debug("IP6 address is not returned from server. Maybe only PD is returned.");
+            }
+            if (pdInfo != null) {
+                log.debug("IP6 PD address {}",
+                        HexString.toHexString(pdInfo.pdPrefix.address().toOctets(), ":"));
+                record.pdPrefix(pdInfo.pdPrefix);
+                record.updatePdPrefTime(pdInfo.prefTime);
+                record.updateLastPdUpdate();
+            } else {
+                log.debug("IP6 PD address is not returned from server. Maybe only IPAddress is returned.");
+            }
+        }
+
+        record.getV6Counters().incrementCounter(Dhcp6HandlerUtil.getMsgTypeStr(leafMsgType));
+        record.ip6Status(DHCP6.MsgType.getType(leafMsgType));
+        record.setDirectlyConnected(directConnFlag);
+        record.updateLastSeen();
+        dhcpRelayStore.updateDhcpRecord(leafHostId, record);
+        // TODO Use AtomicInteger for the counters
+        try {
+            recordSemaphore.acquire();
+            try {
+                dhcpRelayCountersStore.incrementCounter(gCount, Dhcp6HandlerUtil.getMsgTypeStr(leafMsgType));
+            } finally {
+                // calling release() after a successful acquire()
+                recordSemaphore.release();
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    private List<InternalPacket> processDhcp6ForwardOnly(PacketContext context,
+                                                         Ethernet clientPacket,
+                                                         Set<Interface> clientInterfaces,
+                                                         DHCP6 dhcpPacket) {
+        ConnectPoint inPort = context.inPacket().receivedFrom();
+        log.trace("Got DHCPv6 on port {}", inPort);
+        boolean directConnFlag = Dhcp6HandlerUtil.directlyConnected(dhcpPacket);
+
+        List<InternalPacket> internalPackets = new ArrayList<>();
+        List<DhcpServerInfo> serverInfoList = findValidServerInfo(directConnFlag);
+
+        for (DhcpServerInfo dhcpServer : serverInfoList) {
+            Interface serverInterface = getServerInterface(dhcpServer);
+            if (serverInterface == null) {
+                log.warn("Can't get server interface, ignore");
+                continue;
+            }
+
+            Ethernet newPacket = Dhcp6HandlerUtil.buildDhcp6PacketFromClient(context,
+                    clientPacket, clientInterfaces, dhcpServer, serverInterface);
+            log.trace("Built packet for server {} : {}", dhcpServer, newPacket);
+            internalPackets.add(InternalPacket.internalPacket(newPacket,
+                    dhcpServer.getDhcpServerConnectPoint().get()));
+        }
+
+        return internalPackets;
+    }
+
+    private List<InternalPacket> processLQ6PacketFromClient(PacketContext context,
+                                                              Ethernet clientPacket,
+                                                              Set<Interface> clientInterfaces,
+                                                              DHCP6 dhcp6Payload) {
+        ConnectPoint inPort = context.inPacket().receivedFrom();
+        log.info("Got LQ-REQUEST V6 on port {}", inPort);
+        List<Dhcp6Option> lopt = dhcp6Payload.getOptions();
+        log.info("Options list: {}", lopt);
+        Dhcp6LeaseQueryOption lqoption = dhcp6Payload.getOptions()
+                .stream()
+                .filter(opt -> opt instanceof Dhcp6LeaseQueryOption)
+                .map(pld -> (Dhcp6LeaseQueryOption) pld)
+                .findFirst()
+                .orElse(null);
+
+        if (lqoption == null) {
+            // Can't find dhcp payload
+            log.warn("Can't find dhcp6 lease query message - aborting");
+            return null;
+        } else {
+            log.info("dhcp6 lqv6 options found: {}", lqoption);
+        }
+        log.warn("LQv6 for " + lqoption.linkAddress.toString() + " comes from " + inPort.toString());
+        Ethernet packet = context.inPacket().parsed();
+        Ip6Address clientAddress = lqoption.linkAddress;
+        IPv6 ipv6Packet = (IPv6) packet.getPayload();
+        Ip6Address nextHopIp = findNextHopIp6FromRelayStore(clientAddress);
+
+        // 1. only if there is a route to remove - remove it
+        if (nextHopIp != null) {
+            Route routeForIP6 = new Route(Route.Source.DHCP, clientAddress.toIpPrefix(), nextHopIp);
+            log.debug("Removing route of Client " + clientAddress +
+                              " for indirectly connected - next hop ip6 " + nextHopIp);
+            routeStore.removeRoute(routeForIP6);
+        }
+
+        // 2. note the potential NH this packet came from in case it's a known lease
+        //    this NH will then be used to build the route
+        MacAddress potentialNH = packet.getSourceMAC();
+        VlanId vlanId = VlanId.vlanId(packet.getVlanID());
+        setPotentialNextHopForIp6InRelayStore(clientAddress, vlanId, potentialNH);
+        // 3. route this LQ6 to all relevant servers
+        IPv6 clientIpv6 = (IPv6) clientPacket.getPayload();
+        UDP clientUdp = (UDP) clientIpv6.getPayload();
+        DHCP6 clientDhcp6 = (DHCP6) clientUdp.getPayload();
+
+        boolean directConnFlag = Dhcp6HandlerUtil.directlyConnected(clientDhcp6);
+        List<InternalPacket> internalPackets = new ArrayList<>();
+        List<DhcpServerInfo> serverInfoList = findValidServerInfo(directConnFlag);
+        List<DhcpServerInfo> copyServerInfoList = new ArrayList<DhcpServerInfo>(serverInfoList);
+
+        for (DhcpServerInfo serverInfo : copyServerInfoList) {
+            if (!Dhcp6HandlerUtil.checkDhcpServerConnPt(directConnFlag, serverInfo)) {
+                log.warn("Can't get server connect point, ignore");
+                continue;
+            }
+            DhcpServerInfo newServerInfo = getHostInfoForServerInfo(serverInfo, serverInfoList);
+            if (newServerInfo == null) {
+                log.warn("Can't get server interface with host info resolved, ignore");
+                continue;
+            }
+            Interface serverInterface = getServerInterface(newServerInfo);
+            if (serverInterface == null) {
+                log.warn("Can't get server interface, ignore");
+                continue;
+            }
+            Ethernet etherRouted = (Ethernet) clientPacket.clone();
+            MacAddress macFacingServer = serverInterface.mac();
+            if (macFacingServer == null) {
+                log.warn("No MAC address for server Interface {}", serverInterface);
+                return null;
+            }
+            etherRouted.setSourceMACAddress(macFacingServer);
+            etherRouted.setDestinationMACAddress(newServerInfo.getDhcpConnectMac().get());
+            InternalPacket internalPacket =
+                    InternalPacket.internalPacket(etherRouted,
+                              serverInfo.getDhcpServerConnectPoint().get());
+            internalPackets.add(internalPacket);
+            log.debug("Sending LQ to DHCP server {}", newServerInfo.getDhcpServerIp6());
+        }
+        log.debug("num of client packets to send is{}", internalPackets.size());
+
+        return internalPackets;
+    }
+
+    /**
+     * build the DHCP6 solicit/request packet with gatewayip.
+     *
+     * @param context packet context
+     * @param clientPacket client ethernet packet
+     * @param clientInterfaces set of client side interfaces
+     */
+    private List<InternalPacket> processDhcp6PacketFromClient(PacketContext context,
+                                                              Ethernet clientPacket,
+                                                              Set<Interface> clientInterfaces) {
+        ConnectPoint receivedFrom = context.inPacket().receivedFrom();
+        Ip6Address relayAgentIp = Dhcp6HandlerUtil.getRelayAgentIPv6Address(clientInterfaces);
+        MacAddress relayAgentMac = clientInterfaces.iterator().next().mac();
+        if (relayAgentIp == null || relayAgentMac == null) {
+            log.warn("Missing DHCP relay agent interface Ipv6 addr config for "
+                      + "packet from client on port: {}. Aborting packet processing",
+                      clientInterfaces.iterator().next().connectPoint());
+            dhcpRelayCountersStore.incrementCounter(gCount, DhcpRelayCounters.NO_CLIENT_INTF_MAC);
+            return Lists.newArrayList();
+        }
+
+        IPv6 clientIpv6 = (IPv6) clientPacket.getPayload();
+        UDP clientUdp = (UDP) clientIpv6.getPayload();
+        DHCP6 clientDhcp6 = (DHCP6) clientUdp.getPayload();
+
+        boolean directConnFlag = Dhcp6HandlerUtil.directlyConnected(clientDhcp6);
+
+        ConnectPoint clientConnectionPoint = context.inPacket().receivedFrom();
+        VlanId vlanIdInUse = VlanId.vlanId(clientPacket.getVlanID());
+        Interface clientInterface = interfaceService.getInterfacesByPort(clientConnectionPoint)
+                .stream().filter(iface -> Dhcp6HandlerUtil.interfaceContainsVlan(iface, vlanIdInUse))
+                .findFirst()
+                .orElse(null);
+
+        List<InternalPacket> internalPackets = new ArrayList<>();
+        List<DhcpServerInfo> serverInfoList = findValidServerInfo(directConnFlag);
+        List<DhcpServerInfo> copyServerInfoList = new ArrayList<DhcpServerInfo>(serverInfoList);
+
+        for (DhcpServerInfo serverInfo : copyServerInfoList) {
+            if (!Dhcp6HandlerUtil.checkDhcpServerConnPt(directConnFlag, serverInfo)) {
+                log.warn("Can't get server connect point, ignore");
+                continue;
+            }
+            DhcpServerInfo newServerInfo = getHostInfoForServerInfo(serverInfo, serverInfoList);
+            if (newServerInfo == null) {
+                log.warn("Can't get server interface with host info resolved, ignore");
+                continue;
+            }
+
+            Interface serverInterface = getServerInterface(newServerInfo);
+            if (serverInterface == null) {
+                log.warn("Can't get server interface, ignore");
+                continue;
+            }
+
+            Ethernet etherReply = Dhcp6HandlerUtil.buildDhcp6PacketFromClient(context, clientPacket,
+                                                              clientInterfaces, newServerInfo, serverInterface);
+            removeHostOrRoute(directConnFlag, clientConnectionPoint, clientDhcp6, clientPacket,
+                    clientIpv6, clientInterface);
+
+            InternalPacket internalPacket = InternalPacket.internalPacket(etherReply,
+                    serverInfo.getDhcpServerConnectPoint().get());
+            internalPackets.add(internalPacket);
+        }
+        log.debug("num of client packets to send is{}", internalPackets.size());
+
+        return internalPackets;
+    }
+
+    /**
+     * process the DHCP6 relay-reply packet from dhcp server.
+     *
+     * @param context packet context
+     * @param receivedPacket server ethernet packet
+     * @param recevingInterfaces set of server side interfaces
+     * @return internalPacket toward client
+     */
+    private InternalPacket processDhcp6PacketFromServer(PacketContext context,
+                                                        Ethernet receivedPacket, Set<Interface> recevingInterfaces) {
+        // get dhcp6 header.
+        Ethernet etherReply = receivedPacket.duplicate();
+        IPv6 ipv6Packet = (IPv6) etherReply.getPayload();
+        UDP udpPacket = (UDP) ipv6Packet.getPayload();
+        DHCP6 dhcp6Relay = (DHCP6) udpPacket.getPayload();
+        Boolean directConnFlag = Dhcp6HandlerUtil.directlyConnected(dhcp6Relay);
+
+        DHCP6 embeddedDhcp6 = dhcp6Relay.getOptions().stream()
+                .filter(opt -> opt instanceof Dhcp6RelayOption)
+                .map(BasePacket::getPayload)
+                .map(pld -> (DHCP6) pld)
+                .findFirst()
+                .orElse(null);
+        ConnectPoint inPort = context.inPacket().receivedFrom();
+        DhcpServerInfo foundServerInfo = findServerInfoFromServer(directConnFlag, inPort);
+
+        if (foundServerInfo == null) {
+            log.warn("Cannot find server info for {} server, inPort {}",
+                      directConnFlag ? "direct" : "indirect", inPort);
+            dhcpRelayCountersStore.incrementCounter(gCount, DhcpRelayCounters.NO_SERVER_INFO);
+            return null;
+        } else {
+            if (Dhcp6HandlerUtil.isServerIpEmpty(foundServerInfo)) {
+                log.warn("Cannot find server info's ipaddress");
+                dhcpRelayCountersStore.incrementCounter(gCount, DhcpRelayCounters.NO_SERVER_IP6ADDR);
+                return null;
+            }
+        }
+
+        Dhcp6InterfaceIdOption interfaceIdOption = dhcp6Relay.getOptions().stream()
+                .filter(opt -> opt instanceof Dhcp6InterfaceIdOption)
+                .map(opt -> (Dhcp6InterfaceIdOption) opt)
+                .findFirst()
+                .orElse(null);
+        if (interfaceIdOption == null) {
+            log.warn("Interface Id option is not present, abort packet...");
+            dhcpRelayCountersStore.incrementCounter(gCount, DhcpRelayCounters.OPTION_MISSING_FAIL);
+            return null;
+        }
+
+        MacAddress peerMac = interfaceIdOption.getMacAddress();
+        String clientConnectionPointStr = new String(interfaceIdOption.getInPort());
+        ConnectPoint clientConnectionPoint = ConnectPoint.deviceConnectPoint(clientConnectionPointStr);
+        VlanId vlanIdInUse = VlanId.vlanId(interfaceIdOption.getVlanId());
+        Interface clientInterface = interfaceService.getInterfacesByPort(clientConnectionPoint)
+                .stream().filter(iface -> Dhcp6HandlerUtil.interfaceContainsVlan(iface, vlanIdInUse))
+                .findFirst().orElse(null);
+        if (clientInterface == null) {
+            log.warn("Cannot get client interface for from packet, abort... vlan {}", vlanIdInUse.toString());
+            dhcpRelayCountersStore.incrementCounter(gCount, DhcpRelayCounters.NO_MATCHING_INTF);
+            return null;
+        }
+        etherReply.setVlanID(vlanIdInUse.toShort());
+
+        MacAddress relayAgentMac = clientInterface.mac();
+        if (relayAgentMac == null) {
+            log.warn("Can not get client interface mac, abort packet..");
+            dhcpRelayCountersStore.incrementCounter(gCount, DhcpRelayCounters.NO_CLIENT_INTF_MAC);
+            return null;
+        }
+        etherReply.setSourceMACAddress(relayAgentMac);
+
+        // find destMac
+        MacAddress clientMac;
+        Ip6Address peerAddress = Ip6Address.valueOf(dhcp6Relay.getPeerAddress());
+        Set<Host> clients = hostService.getHostsByIp(peerAddress);
+        if (clients.isEmpty()) {
+            log.trace("There's no host found for this address {}",
+                    HexString.toHexString(dhcp6Relay.getPeerAddress(), ":"));
+            log.trace("Let's look up interfaceId {}", HexString.toHexString(peerMac.toBytes(), ":"));
+            clientMac = peerMac;
+        } else {
+            clientMac = clients.iterator().next().mac();
+            if (clientMac == null) {
+                log.warn("No client mac address found, abort packet...");
+                dhcpRelayCountersStore.incrementCounter(gCount, DhcpRelayCounters.NO_CLIENT_INTF_MAC);
+                return null;
+            }
+            log.trace("Client mac address found from getHostByIp");
+        }
+        etherReply.setDestinationMACAddress(clientMac);
+        // ip header
+        ipv6Packet.setSourceAddress(dhcp6Relay.getLinkAddress());
+        ipv6Packet.setDestinationAddress(dhcp6Relay.getPeerAddress());
+        // udp header
+        udpPacket.setSourcePort(UDP.DHCP_V6_SERVER_PORT);
+        if (directConnFlag) {
+            udpPacket.setDestinationPort(UDP.DHCP_V6_CLIENT_PORT);
+        } else {
+            udpPacket.setDestinationPort(UDP.DHCP_V6_SERVER_PORT);
+        }
+
+
+        boolean hostOrRouteAllowed = learnRouteFromLeasequery ||
+                    Dhcp6HandlerUtil.getDhcp6LeafMessageType(dhcp6Relay) != MsgType.LEASEQUERY_REPLY;
+        log.debug("Can add host or route: {}", hostOrRouteAllowed);
+
+        if (hostOrRouteAllowed) {
+            // add host or route
+            addHostOrRoute(directConnFlag, clientConnectionPoint, dhcp6Relay, embeddedDhcp6,
+                    clientMac, clientInterface);
+        }
+
+        udpPacket.setPayload(embeddedDhcp6);
+        udpPacket.resetChecksum();
+        ipv6Packet.setPayload(udpPacket);
+        etherReply.setPayload(ipv6Packet);
+        return InternalPacket.internalPacket(etherReply, clientConnectionPoint);
+    }
+
+    @Override
+    public void setDhcpFpmEnabled(Boolean enabled) {
+       dhcpFpmEnabled = enabled;
+    }
+
+    @Override
+    public void setDefaultDhcpServerConfigs(Collection<DhcpServerConfig> configs) {
+        log.debug("setDefaultDhcpServerConfigs is called.");
+        setDhcpServerConfigs(configs, defaultServerInfoList);
+    }
+
+    @Override
+    public void setIndirectDhcpServerConfigs(Collection<DhcpServerConfig> configs) {
+        log.debug("setIndirectDhcpServerConfigs is called.");
+        setDhcpServerConfigs(configs, indirectServerInfoList);
+    }
+
+    public void setDhcpServerConfigs(Collection<DhcpServerConfig> configs, List<DhcpServerInfo> serverInfoList) {
+        log.debug("config size {}.", configs.size());
+
+        if (configs.size() == 0) {
+            // no config to update
+            return;
+        }
+        // TODO: currently we pick up first DHCP server config.
+        // Will use other server configs in the future for HA.
+        Boolean isConfigValid = false;
+        for (DhcpServerConfig serverConfig : configs) {
+            if (serverConfig.getDhcpServerIp6().isPresent()) {
+                isConfigValid = true;
+                break;
+            }
+        }
+        if (!isConfigValid) {
+            log.warn("No IP V6 server address found.");
+            return;  // No IP V6 address found
+        }
+        for (DhcpServerInfo oldServerInfo : serverInfoList) {
+            // stop monitoring gateway or server
+            oldServerInfo.getDhcpGatewayIp6().ifPresent(gatewayIp -> {
+                hostService.stopMonitoringIp(gatewayIp);
+            });
+            oldServerInfo.getDhcpServerIp6().ifPresent(serverIp -> {
+                hostService.stopMonitoringIp(serverIp);
+                cancelDhcpPacket(serverIp);
+            });
+        }
+        serverInfoList.clear();
+        for (DhcpServerConfig serverConfig : configs) {
+            // Create new server info according to the config
+            DhcpServerInfo newServerInfo = new DhcpServerInfo(serverConfig,
+                    DhcpServerInfo.Version.DHCP_V6);
+            checkState(newServerInfo.getDhcpServerConnectPoint().isPresent(),
+                    "Connect point not exists");
+            checkState(newServerInfo.getDhcpServerIp6().isPresent(),
+                    "IP of DHCP server not exists");
+
+            log.debug("DHCP server connect point: {}", newServerInfo.getDhcpServerConnectPoint().orElse(null));
+            log.debug("DHCP server IP: {}", newServerInfo.getDhcpServerIp6().orElse(null));
+
+            Ip6Address serverIp = newServerInfo.getDhcpServerIp6().get();
+            Ip6Address ipToProbe;
+            if (newServerInfo.getDhcpGatewayIp6().isPresent()) {
+                ipToProbe = newServerInfo.getDhcpGatewayIp6().get();
+            } else {
+                ipToProbe = newServerInfo.getDhcpServerIp6().orElse(null);
+            }
+            String hostToProbe = newServerInfo.getDhcpGatewayIp6()
+                    .map(ip -> "gateway").orElse("server");
+
+            log.warn("Probing to resolve {} IP {}", hostToProbe, ipToProbe);
+            hostService.startMonitoringIp(ipToProbe);
+
+            Set<Host> hosts = hostService.getHostsByIp(ipToProbe);
+            if (!hosts.isEmpty()) {
+                Host host = hosts.iterator().next();
+                newServerInfo.setDhcpConnectVlan(host.vlan());
+                newServerInfo.setDhcpConnectMac(host.mac());
+                log.warn("Host found host {}", host);
+
+            } else {
+                log.warn("No host found host ip {}", ipToProbe);
+            }
+            // Add new server info
+            synchronized (this) {
+                serverInfoList.add(newServerInfo);
+            }
+            if (!hosts.isEmpty()) {
+                requestDhcpPacket(serverIp);
+            }
+        }
+    }
+
+    class InternalHostListener implements HostListener {
+        @Override
+        public void event(HostEvent event) {
+            switch (event.type()) {
+                case HOST_ADDED:
+                case HOST_UPDATED:
+                case HOST_MOVED:
+                    log.trace("Scheduled host event {}", event);
+                    hostEventExecutor.execute(() -> hostUpdated(event.subject()));
+                    break;
+                case HOST_REMOVED:
+                    log.trace("Scheduled host event {}", event);
+                    hostEventExecutor.execute(() -> hostRemoved(event.subject()));
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Handle host updated.
+     * If the host is DHCP server or gateway, update connect mac and vlan.
+     *
+     * @param host the host
+     */
+    private void hostUpdated(Host host) {
+        hostUpdated(host, defaultServerInfoList);
+        hostUpdated(host, indirectServerInfoList);
+    }
+
+    private void hostUpdated(Host host, List<DhcpServerInfo> serverInfoList) {
+        serverInfoList.stream().forEach(serverInfo -> {
+            Ip6Address serverIp = serverInfo.getDhcpServerIp6().orElse(null);
+            Ip6Address targetIp = serverInfo.getDhcpGatewayIp6().orElse(null);
+
+            if (targetIp == null) {
+                targetIp = serverIp;
+            }
+            if (targetIp != null) {
+                if (host.ipAddresses().contains(targetIp)) {
+                    serverInfo.setDhcpConnectMac(host.mac());
+                    serverInfo.setDhcpConnectVlan(host.vlan());
+                    requestDhcpPacket(serverIp);
+                }
+            }
+        });
+    }
+    /**
+     * Handle host removed.
+     * If the host is DHCP server or gateway, unset connect mac and vlan.
+     *
+     * @param host the host
+     */
+    private void hostRemoved(Host host) {
+        hostRemoved(host, defaultServerInfoList);
+        hostRemoved(host, indirectServerInfoList);
+    }
+
+    private void hostRemoved(Host host, List<DhcpServerInfo> serverInfoList) {
+        serverInfoList.stream().forEach(serverInfo -> {
+            Ip6Address serverIp = serverInfo.getDhcpServerIp6().orElse(null);
+            Ip6Address targetIp = serverInfo.getDhcpGatewayIp6().orElse(null);
+
+            if (targetIp == null) {
+                targetIp = serverIp;
+            }
+            if (targetIp != null) {
+                if (host.ipAddresses().contains(targetIp)) {
+                    serverInfo.setDhcpConnectVlan(null);
+                    serverInfo.setDhcpConnectMac(null);
+                    cancelDhcpPacket(serverIp);
+                }
+            }
+        });
+    }
+
+   /**
+     * Gets Interface facing to the server for default host.
+     *
+     * @return the Interface facing to the server; null if not found
+     */
+    private Interface getServerInterface() {
+        DhcpServerInfo serverInfo;
+        ConnectPoint dhcpServerConnectPoint;
+        VlanId dhcpConnectVlan;
+
+        if (!defaultServerInfoList.isEmpty()) {
+            serverInfo = defaultServerInfoList.get(0);
+            dhcpServerConnectPoint = serverInfo.getDhcpServerConnectPoint().orElse(null);
+            dhcpConnectVlan = serverInfo.getDhcpConnectVlan().orElse(null);
+        } else {
+            return null;
+        }
+        if (dhcpServerConnectPoint == null || dhcpConnectVlan == null) {
+            log.info("Default DHCP server {} not resolve yet", serverInfo.getDhcpGatewayIp6());
+            return null;
+        }
+        return interfaceService.getInterfacesByPort(dhcpServerConnectPoint)
+                .stream()
+                .filter(iface -> Dhcp6HandlerUtil.interfaceContainsVlan(iface, dhcpConnectVlan))
+                .findFirst()
+                .orElse(null);
+    }
+
+    /**
+     * Gets Interface facing to the server for indirect hosts.
+     * Use default server Interface if indirect server not configured.
+     *
+     * @return the Interface facing to the server; null if not found
+     */
+    private Interface getIndirectServerInterface() {
+        DhcpServerInfo serverInfo;
+
+        ConnectPoint indirectDhcpServerConnectPoint;
+        VlanId indirectDhcpConnectVlan;
+
+        if (!indirectServerInfoList.isEmpty()) {
+            serverInfo = indirectServerInfoList.get(0);
+            indirectDhcpServerConnectPoint = serverInfo.getDhcpServerConnectPoint().orElse(null);
+            indirectDhcpConnectVlan = serverInfo.getDhcpConnectVlan().orElse(null);
+        } else {
+            return getServerInterface();
+        }
+        if (indirectDhcpServerConnectPoint == null || indirectDhcpConnectVlan == null) {
+            log.info("Indirect DHCP server {} not resolve yet", serverInfo.getDhcpGatewayIp6());
+            return null;
+        }
+        return interfaceService.getInterfacesByPort(indirectDhcpServerConnectPoint)
+                .stream()
+                .filter(iface -> Dhcp6HandlerUtil.interfaceContainsVlan(iface, indirectDhcpConnectVlan))
+                .findFirst()
+                .orElse(null);
+    }
+
+    /**
+     * Checks if serverInfo's host info (mac and vlan) is filled in; if not, fills in.
+     *
+     * @param serverInfo server information
+     * @return newServerInfo if host info can be either found or filled in.
+     */
+    private DhcpServerInfo getHostInfoForServerInfo(DhcpServerInfo serverInfo, List<DhcpServerInfo> sererInfoList) {
+        DhcpServerInfo newServerInfo = null;
+        MacAddress  dhcpServerConnectMac = serverInfo.getDhcpConnectMac().orElse(null);
+        VlanId dhcpConnectVlan = serverInfo.getDhcpConnectVlan().orElse(null);
+        ConnectPoint dhcpServerConnectPoint = serverInfo.getDhcpServerConnectPoint().orElse(null);
+
+        if (dhcpServerConnectMac != null && dhcpConnectVlan != null) {
+            newServerInfo = serverInfo;
+            log.debug("DHCP server {} host info found. ConnectPt{}  Mac {} vlan {}", serverInfo.getDhcpServerIp6(),
+                    dhcpServerConnectPoint, dhcpServerConnectMac, dhcpConnectVlan);
+        } else {
+            log.warn("DHCP server {} not resolve yet connectPt {} mac {} vlan {}", serverInfo.getDhcpServerIp6(),
+                    dhcpServerConnectPoint, dhcpServerConnectMac, dhcpConnectVlan);
+
+            Ip6Address ipToProbe;
+            if (serverInfo.getDhcpGatewayIp6().isPresent()) {
+                ipToProbe = serverInfo.getDhcpGatewayIp6().get();
+            } else {
+                ipToProbe = serverInfo.getDhcpServerIp6().orElse(null);
+            }
+            String hostToProbe = serverInfo.getDhcpGatewayIp6()
+                    .map(ip -> "gateway").orElse("server");
+
+            log.info("Dynamically probing to resolve {} IP {}", hostToProbe, ipToProbe);
+            hostService.startMonitoringIp(ipToProbe);
+
+            Set<Host> hosts = hostService.getHostsByIp(ipToProbe);
+            if (!hosts.isEmpty()) {
+                int serverInfoIndex = sererInfoList.indexOf(serverInfo);
+                Host host = hosts.iterator().next();
+                serverInfo.setDhcpConnectVlan(host.vlan());
+                serverInfo.setDhcpConnectMac(host.mac());
+                // replace the serverInfo in the list
+                sererInfoList.set(serverInfoIndex, serverInfo);
+                newServerInfo = serverInfo;
+                log.warn("Dynamically host found host {}", host);
+            } else {
+                log.warn("No host found host ip {} dynamically", ipToProbe);
+            }
+        }
+        return newServerInfo;
+    }
+
+    /**
+     * Gets Interface facing to the server for default host.
+     *
+     * @param serverInfo server information
+     * @return the Interface facing to the server; null if not found
+     */
+    private Interface getServerInterface(DhcpServerInfo serverInfo) {
+        Interface serverInterface = null;
+
+        ConnectPoint dhcpServerConnectPoint = serverInfo.getDhcpServerConnectPoint().orElse(null);
+        VlanId dhcpConnectVlan = serverInfo.getDhcpConnectVlan().orElse(null);
+
+        if (dhcpServerConnectPoint != null && dhcpConnectVlan != null) {
+        serverInterface = interfaceService.getInterfacesByPort(dhcpServerConnectPoint)
+                    .stream()
+                    .filter(iface -> Dhcp6HandlerUtil.interfaceContainsVlan(iface, dhcpConnectVlan))
+                    .findFirst()
+                    .orElse(null);
+        } else {
+            log.warn("DHCP server {} not resolve yet connectPoint {} vlan {}", serverInfo.getDhcpServerIp6(),
+                    dhcpServerConnectPoint, dhcpConnectVlan);
+        }
+
+        return serverInterface;
+    }
+
+    private void requestDhcpPacket(Ip6Address serverIp) {
+        requestServerDhcpPacket(serverIp);
+        requestClientDhcpPacket(serverIp);
+        requestServerLQPacket(serverIp);
+    }
+
+    private void cancelDhcpPacket(Ip6Address serverIp) {
+        cancelServerDhcpPacket(serverIp);
+        cancelClientDhcpPacket(serverIp);
+        cancelServerLQPacket(serverIp);
+    }
+
+    private void cancelServerDhcpPacket(Ip6Address serverIp) {
+        TrafficSelector serverSelector =
+                DefaultTrafficSelector.builder(SERVER_RELAY_SELECTOR)
+                        .matchIPv6Src(serverIp.toIpPrefix())
+                        .build();
+        packetService.cancelPackets(serverSelector,
+                                    PacketPriority.CONTROL,
+                                    appId);
+    }
+
+    private void cancelServerLQPacket(Ip6Address serverIp) {
+        TrafficSelector serverSelector =
+                DefaultTrafficSelector.builder(LEASE_QUERY_RESPONSE_SELECTOR)
+                        .matchIPv6Src(serverIp.toIpPrefix())
+                        .build();
+        packetService.cancelPackets(serverSelector,
+                                    PacketPriority.CONTROL,
+                                    appId);
+    }
+
+    private void requestServerDhcpPacket(Ip6Address serverIp) {
+        TrafficSelector serverSelector =
+                DefaultTrafficSelector.builder(SERVER_RELAY_SELECTOR)
+                        .matchIPv6Src(serverIp.toIpPrefix())
+                        .build();
+        packetService.requestPackets(serverSelector,
+                                     PacketPriority.CONTROL,
+                                     appId);
+    }
+
+    private void requestServerLQPacket(Ip6Address serverIp) {
+        TrafficSelector serverSelector =
+                DefaultTrafficSelector.builder(LEASE_QUERY_RESPONSE_SELECTOR)
+                        .matchIPv6Src(serverIp.toIpPrefix())
+                        .build();
+        packetService.requestPackets(serverSelector,
+                                     PacketPriority.CONTROL,
+                                     appId);
+    }
+
+    private void cancelClientDhcpPacket(Ip6Address serverIp) {
+        // Packet comes from relay
+        TrafficSelector indirectClientSelector =
+                DefaultTrafficSelector.builder(SERVER_RELAY_SELECTOR)
+                        .matchIPv6Dst(serverIp.toIpPrefix())
+                        .build();
+        packetService.cancelPackets(indirectClientSelector,
+                                    PacketPriority.CONTROL,
+                                    appId);
+        indirectClientSelector =
+                DefaultTrafficSelector.builder(SERVER_RELAY_SELECTOR)
+                        .matchIPv6Dst(Ip6Address.ALL_DHCP_RELAY_AGENTS_AND_SERVERS.toIpPrefix())
+                        .build();
+        packetService.cancelPackets(indirectClientSelector,
+                                    PacketPriority.CONTROL,
+                                    appId);
+        indirectClientSelector =
+                DefaultTrafficSelector.builder(SERVER_RELAY_SELECTOR)
+                        .matchIPv6Dst(Ip6Address.ALL_DHCP_SERVERS.toIpPrefix())
+                        .build();
+        packetService.cancelPackets(indirectClientSelector,
+                                    PacketPriority.CONTROL,
+                                    appId);
+
+        // Packet comes from client
+        packetService.cancelPackets(CLIENT_SERVER_SELECTOR,
+                                    PacketPriority.CONTROL,
+                                    appId);
+    }
+
+    private void requestClientDhcpPacket(Ip6Address serverIp) {
+        // Packet comes from relay
+        TrafficSelector indirectClientSelector =
+                DefaultTrafficSelector.builder(SERVER_RELAY_SELECTOR)
+                        .matchIPv6Dst(serverIp.toIpPrefix())
+                        .build();
+        packetService.requestPackets(indirectClientSelector,
+                                     PacketPriority.CONTROL,
+                                     appId);
+        indirectClientSelector =
+                DefaultTrafficSelector.builder(SERVER_RELAY_SELECTOR)
+                        .matchIPv6Dst(Ip6Address.ALL_DHCP_RELAY_AGENTS_AND_SERVERS.toIpPrefix())
+                        .build();
+        packetService.requestPackets(indirectClientSelector,
+                                     PacketPriority.CONTROL,
+                                     appId);
+        indirectClientSelector =
+                DefaultTrafficSelector.builder(SERVER_RELAY_SELECTOR)
+                        .matchIPv6Dst(Ip6Address.ALL_DHCP_SERVERS.toIpPrefix())
+                        .build();
+        packetService.requestPackets(indirectClientSelector,
+                                     PacketPriority.CONTROL,
+                                     appId);
+
+        // Packet comes from client
+        packetService.requestPackets(CLIENT_SERVER_SELECTOR,
+                                     PacketPriority.CONTROL,
+                                     appId);
+    }
+
+    /**
+     * Process the ignore rules.
+     *
+     * @param deviceId the device id
+     * @param vlanId the vlan to be ignored
+     * @param op the operation, ADD to install; REMOVE to uninstall rules
+     */
+    private void processIgnoreVlanRule(DeviceId deviceId, VlanId vlanId, Objective.Operation op) {
+        AtomicInteger installedCount = new AtomicInteger(DHCP_SELECTORS.size());
+        DHCP_SELECTORS.forEach(trafficSelector -> {
+            TrafficSelector selector = DefaultTrafficSelector.builder(trafficSelector)
+                    .matchVlanId(vlanId)
+                    .build();
+
+            ForwardingObjective.Builder builder = DefaultForwardingObjective.builder()
+                    .withFlag(ForwardingObjective.Flag.VERSATILE)
+                    .withSelector(selector)
+                    .withPriority(IGNORE_CONTROL_PRIORITY)
+                    .withTreatment(DefaultTrafficTreatment.emptyTreatment())
+                    .fromApp(appId);
+
+
+            ObjectiveContext objectiveContext = new ObjectiveContext() {
+                @Override
+                public void onSuccess(Objective objective) {
+                    log.info("Ignore rule {} (Vlan id {}, device {}, selector {})",
+                             op, vlanId, deviceId, selector);
+                    int countDown = installedCount.decrementAndGet();
+                    if (countDown != 0) {
+                        return;
+                    }
+                    switch (op) {
+                        case ADD:
+                            ignoredVlans.put(deviceId, vlanId);
+                            break;
+                        case REMOVE:
+                            ignoredVlans.remove(deviceId, vlanId);
+                            break;
+                        default:
+                            log.warn("Unsupported objective operation {}", op);
+                            break;
+                    }
+                }
+
+                @Override
+                public void onError(Objective objective, ObjectiveError error) {
+                    log.warn("Can't {} ignore rule (vlan id {}, selector {}, device {}) due to {}",
+                             op, vlanId, selector, deviceId, error);
+                }
+            };
+
+            ForwardingObjective fwd;
+            switch (op) {
+                case ADD:
+                    fwd = builder.add(objectiveContext);
+                    break;
+                case REMOVE:
+                    fwd = builder.remove(objectiveContext);
+                    break;
+                default:
+                    log.warn("Unsupported objective operation {}", op);
+                    return;
+            }
+
+            Device device = deviceService.getDevice(deviceId);
+            if (device == null || !device.is(Pipeliner.class)) {
+                log.warn("Device {} is not available now, wait until device is available", deviceId);
+                return;
+            }
+            flowObjectiveService.apply(deviceId, fwd);
+        });
+    }
+
+    /**
+     * Find first ipaddress for a given Host info i.e.  mac and vlan.
+     *
+     * @param clientMac client mac
+     * @param vlanId  packet's vlan
+     * @return next-hop link-local ipaddress for a given host
+     */
+    private IpAddress getFirstIpByHost(Boolean directConnFlag, MacAddress clientMac, VlanId vlanId) {
+        IpAddress nextHopIp;
+        // pick out the first link-local ip address
+        HostId gwHostId = HostId.hostId(clientMac, vlanId);
+        Host gwHost = hostService.getHost(gwHostId);
+        if (gwHost == null) {
+            log.warn("Can't find gateway host for hostId {}", gwHostId);
+            return null;
+        }
+        if (directConnFlag) {
+            nextHopIp = gwHost.ipAddresses()
+                    .stream()
+                    .filter(IpAddress::isIp6)
+                    .map(IpAddress::getIp6Address)
+                    .findFirst()
+                    .orElse(null);
+        } else {
+            nextHopIp = gwHost.ipAddresses()
+                    .stream()
+                    .filter(IpAddress::isIp6)
+                    .filter(ip6 -> ip6.isLinkLocal())
+                    .map(IpAddress::getIp6Address)
+                    .findFirst()
+                    .orElse(null);
+        }
+        return nextHopIp;
+    }
+
+    private List<DhcpServerInfo> findValidServerInfo(boolean directConnFlag) {
+        List<DhcpServerInfo> validServerInfo;
+
+        if (directConnFlag || indirectServerInfoList.isEmpty()) {
+            validServerInfo = new ArrayList<DhcpServerInfo>(defaultServerInfoList);
+        } else {
+            validServerInfo = new ArrayList<DhcpServerInfo>(indirectServerInfoList);
+        }
+        return validServerInfo;
+    }
+
+    private DhcpServerInfo findServerInfoFromServer(boolean directConnFlag, ConnectPoint inPort) {
+        List<DhcpServerInfo> validServerInfoList = findValidServerInfo(directConnFlag);
+        DhcpServerInfo  foundServerInfo = null;
+        for (DhcpServerInfo serverInfo : validServerInfoList) {
+            if (inPort.equals(serverInfo.getDhcpServerConnectPoint().get())) {
+                foundServerInfo = serverInfo;
+                log.debug("ServerInfo found for Rcv port {} Server Connect Point {} for {}",
+                        inPort, serverInfo.getDhcpServerConnectPoint(), directConnFlag ? "direct" : "indirect");
+                break;
+            }
+        }
+        return foundServerInfo;
+    }
+
+    /**
+     * Set the dhcp6 lease expiry poll interval value.
+     *
+     * @param val poll interval value in seconds
+     */
+    @Override
+    public void setDhcp6PollInterval(int val) {
+        dhcp6PollInterval = val;
+    }
+
+    /**
+     * get the dhcp6 lease expiry poll interval value.
+     * This is a private function
+     * @return  poll interval value in seconds
+     */
+    private int getDhcp6PollInterval() {
+        return dhcp6PollInterval;
+    }
+
+    /**
+     * Find lease-expired ipaddresses and pd prefixes.
+     * Removing host/route/fpm entries.
+     */
+    public void timeTick() {
+        long currentTime = System.currentTimeMillis();
+        Collection<DhcpRecord> records = dhcpRelayStore.getDhcpRecords();
+
+        log.debug("timeTick called currenttime {} records num {} ", currentTime, records.size());
+
+        records.forEach(record -> {
+                    boolean addrOrPdRemoved = false;
+                    DHCP6.MsgType ip6Status = record.ip6Status().orElse(null);
+                    if (ip6Status == null) {
+                        log.debug("record is not valid v6 record.");
+                        return;
+                    }
+
+                    if ((currentTime - record.getLastIp6Update()) >
+                            ((record.addrPrefTime() + getDhcp6PollInterval() / 2) * 1000)) {
+                        // remove ipaddress from host/route table
+                        IpAddress ip = record.ip6Address().orElse(null);
+                        if (ip != null) {
+                            if (record.directlyConnected()) {
+                                providerService.removeIpFromHost(HostId.hostId(record.macAddress(),
+                                        record.vlanId()), ip);
+                            } else {
+                                MacAddress gwMac = record.nextHop().orElse(null);
+                                if (gwMac == null) {
+                                    log.warn("Can't find gateway mac address from record {} for ip6Addr", record);
+                                    return;
+                                }
+                                IpAddress nextHopIp = getFirstIpByHost(record.directlyConnected(),
+                                        gwMac,
+                                        record.vlanId());
+                                Route route = new Route(Route.Source.DHCP, ip.toIpPrefix(), nextHopIp);
+                                routeStore.removeRoute(route);
+                            }
+                            record.updateAddrPrefTime(0);
+                            record.ip6Address(null);
+                            addrOrPdRemoved = true;
+                            dhcpRelayStore.updateDhcpRecord(HostId.hostId(record.macAddress(),
+                                    record.vlanId()), record);
+                            log.warn("IP6 address is set to null. delta {} lastUpdate {} addrPrefTime {}",
+                                    (currentTime - record.getLastIp6Update()), record.getLastIp6Update(),
+                                    record.addrPrefTime());
+                        }
+                    }
+                    if ((currentTime - record.getLastPdUpdate()) >
+                            ((record.pdPrefTime() + getDhcp6PollInterval() / 2) * 1000)) {
+                        // remove PD from route/fpm table
+                        IpPrefix pdIpPrefix = record.pdPrefix().orElse(null);
+                        if (pdIpPrefix != null) {
+                            if (record.directlyConnected()) {
+                                providerService.removeIpFromHost(HostId.hostId(record.macAddress(), record.vlanId()),
+                                        pdIpPrefix.address().getIp6Address());
+                            } else {
+                                MacAddress gwMac = record.nextHop().orElse(null);
+                                if (gwMac == null) {
+                                    log.warn("Can't find gateway mac address from record {} for PD prefix", record);
+                                    return;
+                                }
+                                IpAddress nextHopIp = getFirstIpByHost(record.directlyConnected(),
+                                        gwMac,
+                                        record.vlanId());
+                                Route route = new Route(Route.Source.DHCP, pdIpPrefix, nextHopIp);
+                                routeStore.removeRoute(route);
+                                if (this.dhcpFpmEnabled) {
+                                    dhcpFpmPrefixStore.removeFpmRecord(pdIpPrefix);
+                                }
+                            }
+                            record.updatePdPrefTime(0);
+                            record.pdPrefix(null);
+                            addrOrPdRemoved = true;
+                            dhcpRelayStore.updateDhcpRecord(HostId.hostId(record.macAddress(),
+                                    record.vlanId()), record);
+                            log.warn("PD prefix is set to null.delta {} pdPrefTime {}",
+                                    (currentTime - record.getLastPdUpdate()), record.pdPrefTime());
+                        }
+                    }
+                    if (addrOrPdRemoved &&
+                            !record.ip6Address().isPresent() && !record.pdPrefix().isPresent()) {
+                        log.warn("ip6Status {} IP6 address and IP6 PD both are null. Remove record.", ip6Status);
+                        dhcpRelayStore.removeDhcpRecord(HostId.hostId(record.macAddress(), record.vlanId()));
+                    }
+                }
+        );
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerUtil.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerUtil.java
new file mode 100644
index 0000000..05cc231
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerUtil.java
@@ -0,0 +1,648 @@
+/*
+ * 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 org.onlab.packet.BasePacket;
+import org.onlab.packet.DHCP6;
+import org.onlab.packet.DHCP6.MsgType;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.VlanId;
+import org.onlab.packet.dhcp.Dhcp6ClientIdOption;
+import org.onlab.packet.dhcp.Dhcp6RelayOption;
+import org.onlab.packet.dhcp.Dhcp6Option;
+
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IPv6;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.UDP;
+
+import org.onlab.util.HexString;
+import org.onosproject.dhcprelay.api.DhcpServerInfo;
+import org.onosproject.dhcprelay.store.DhcpRelayCounters;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.host.InterfaceIpAddress;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.DeviceId;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.Set;
+import java.util.List;
+import java.util.ArrayList;
+import org.onosproject.net.intf.InterfaceService;
+
+import org.onosproject.net.Host;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.HostLocation;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public final class Dhcp6HandlerUtil {
+
+    private static final Logger log = LoggerFactory.getLogger(Dhcp6HandlerUtil.class);
+
+    private Dhcp6HandlerUtil() {
+    }
+
+    // Returns the first v6 interface ip out of a set of interfaces or null.
+    // Checks all interfaces, and ignores v6 interface ips
+    public static Ip6Address getRelayAgentIPv6Address(Set<Interface> intfs) {
+        for (Interface intf : intfs) {
+            for (InterfaceIpAddress ip : intf.ipAddressesList()) {
+                Ip6Address relayAgentIp = ip.ipAddress().getIp6Address();
+                if (relayAgentIp != null) {
+                    return relayAgentIp;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the first interface ip from interface.
+     *
+     * @param iface interface of one connect point
+     * @return the first interface IP; null if not exists an IP address in
+     *         these interfaces
+     */
+    private static Ip6Address getFirstIpFromInterface(Interface iface) {
+        checkNotNull(iface, "Interface can't be null");
+        return iface.ipAddressesList().stream()
+                .map(InterfaceIpAddress::ipAddress)
+                .filter(IpAddress::isIp6)
+                .map(IpAddress::getIp6Address)
+                .findFirst()
+                .orElse(null);
+    }
+    /**
+     *
+     * process the LQ reply packet from dhcp server.
+     *
+     * @param defaultServerInfoList default server list
+     * @param indirectServerInfoList default indirect server list
+     * @param serverInterface server interface
+     * @param interfaceService interface service
+     * @param hostService host service
+     * @param context packet context
+     * @param receivedPacket server ethernet packet
+     * @param recevingInterfaces set of server side interfaces
+     * @return a packet ready to be sent to relevant output interface
+     */
+    public static InternalPacket processLQ6PacketFromServer(
+            List<DhcpServerInfo> defaultServerInfoList,
+            List<DhcpServerInfo> indirectServerInfoList,
+            Interface serverInterface,
+            InterfaceService interfaceService,
+            HostService hostService,
+            PacketContext context,
+            Ethernet receivedPacket, Set<Interface> recevingInterfaces) {
+        // get dhcp6 header.
+        Ethernet etherReply = (Ethernet) receivedPacket.clone();
+        IPv6 ipv6Packet = (IPv6) etherReply.getPayload();
+        UDP udpPacket = (UDP) ipv6Packet.getPayload();
+        DHCP6 lq6Reply = (DHCP6) udpPacket.getPayload();
+
+        // TODO: refactor
+        ConnectPoint receivedFrom = context.inPacket().receivedFrom();
+        DeviceId receivedFromDevice = receivedFrom.deviceId();
+        DhcpServerInfo serverInfo;
+        Ip6Address dhcpServerIp = null;
+        ConnectPoint dhcpServerConnectPoint = null;
+        MacAddress dhcpConnectMac = null;
+        VlanId dhcpConnectVlan = null;
+        Ip6Address dhcpGatewayIp = null;
+
+        // todo: refactor
+        Ip6Address indirectDhcpServerIp = null;
+        ConnectPoint indirectDhcpServerConnectPoint = null;
+        MacAddress indirectDhcpConnectMac = null;
+        VlanId indirectDhcpConnectVlan = null;
+        Ip6Address indirectDhcpGatewayIp = null;
+        Ip6Address indirectRelayAgentIpFromCfg = null;
+
+        if (!defaultServerInfoList.isEmpty()) {
+            serverInfo = defaultServerInfoList.get(0);
+            dhcpConnectMac = serverInfo.getDhcpConnectMac().orElse(null);
+            dhcpGatewayIp = serverInfo.getDhcpGatewayIp6().orElse(null);
+            dhcpServerIp = serverInfo.getDhcpServerIp6().orElse(null);
+            dhcpServerConnectPoint = serverInfo.getDhcpServerConnectPoint().orElse(null);
+            dhcpConnectVlan = serverInfo.getDhcpConnectVlan().orElse(null);
+        }
+
+        if (!indirectServerInfoList.isEmpty()) {
+            serverInfo = indirectServerInfoList.get(0);
+            indirectDhcpConnectMac = serverInfo.getDhcpConnectMac().orElse(null);
+            indirectDhcpGatewayIp = serverInfo.getDhcpGatewayIp6().orElse(null);
+            indirectDhcpServerIp = serverInfo.getDhcpServerIp6().orElse(null);
+            indirectDhcpServerConnectPoint = serverInfo.getDhcpServerConnectPoint().orElse(null);
+            indirectDhcpConnectVlan = serverInfo.getDhcpConnectVlan().orElse(null);
+            indirectRelayAgentIpFromCfg = serverInfo.getRelayAgentIp6(receivedFromDevice).orElse(null);
+        }
+
+        Boolean directConnFlag = directlyConnected(lq6Reply);
+        ConnectPoint inPort = context.inPacket().receivedFrom();
+        if ((directConnFlag || indirectDhcpServerIp == null)
+                && !inPort.equals(dhcpServerConnectPoint)) {
+            log.warn("Receiving port {} is not the same as server connect point {} for direct or indirect-null",
+                    inPort, dhcpServerConnectPoint);
+            return null;
+        }
+
+        if (!directConnFlag && indirectDhcpServerIp != null &&
+                !inPort.equals(indirectDhcpServerConnectPoint)) {
+            log.warn("Receiving port {} is not the same as server connect point {} for indirect",
+                    inPort, indirectDhcpServerConnectPoint);
+            return null;
+        }
+
+
+        Ip6Address nextHopIP =  Ip6Address.valueOf(ipv6Packet.getDestinationAddress());
+        // use hosts store to find out the next hop mac and connection point
+        Set<Host> hosts = hostService.getHostsByIp(nextHopIP);
+        Host host;
+        if (!hosts.isEmpty()) {
+            host = hosts.iterator().next();
+        } else {
+            log.warn("Host {} is not in store", nextHopIP);
+            return null;
+        }
+
+        HostLocation hl = host.location();
+        String clientConnectionPointStr = hl.toString(); // iterator().next());
+        ConnectPoint clientConnectionPoint = ConnectPoint.deviceConnectPoint(clientConnectionPointStr);
+
+
+        VlanId originalPacketVlanId = VlanId.vlanId(etherReply.getVlanID());
+        Interface iface;
+        iface = interfaceService.getInterfacesByPort(clientConnectionPoint)
+                .stream()
+                .filter(iface1 -> interfaceContainsVlan(iface1, originalPacketVlanId))
+                .findFirst()
+                .orElse(null);
+
+        etherReply.setSourceMACAddress(iface.mac());
+        etherReply.setDestinationMACAddress(host.mac());
+
+        // workaround for a bug where core sends src port as 547 (server)
+        udpPacket.setDestinationPort(UDP.DHCP_V6_SERVER_PORT);
+        udpPacket.setPayload(lq6Reply);
+        udpPacket.resetChecksum();
+        ipv6Packet.setPayload(udpPacket);
+        etherReply.setPayload(ipv6Packet);
+
+        return InternalPacket.internalPacket(etherReply, clientConnectionPoint);
+    }
+
+    /**
+     * extract DHCP6 payload from dhcp6 relay message within relay-forwrd/reply.
+     *
+     * @param dhcp6 dhcp6 relay-reply or relay-foward
+     * @return dhcp6Packet dhcp6 packet extracted from relay-message
+     */
+    public static DHCP6 dhcp6PacketFromRelayPacket(DHCP6 dhcp6) {
+
+        // extract the relay message if exist
+        DHCP6 dhcp6Payload = dhcp6.getOptions().stream()
+                .filter(opt -> opt instanceof Dhcp6RelayOption)
+                .map(BasePacket::getPayload)
+                .map(pld -> (DHCP6) pld)
+                .findFirst()
+                .orElse(null);
+        if (dhcp6Payload == null) {
+            // Can't find dhcp payload
+            log.debug("Can't find dhcp6 payload from relay message");
+        } else {
+            log.debug("dhcp6 payload found from relay message {}", dhcp6Payload);
+        }
+        return dhcp6Payload;
+    }
+
+    /**
+     * find the leaf DHCP6 packet from multi-level relay packet.
+     *
+     * @param relayPacket dhcp6 relay packet
+     * @return leafPacket non-relay dhcp6 packet
+     */
+    public static DHCP6 getDhcp6Leaf(DHCP6 relayPacket) {
+        DHCP6 dhcp6Parent = relayPacket;
+        DHCP6 dhcp6Child = null;
+
+        log.debug("getDhcp6Leaf entered.");
+        while (dhcp6Parent != null) {
+            dhcp6Child = dhcp6PacketFromRelayPacket(dhcp6Parent);
+            if (dhcp6Child != null) {
+                if (dhcp6Child.getMsgType() != DHCP6.MsgType.RELAY_FORW.value() &&
+                        dhcp6Child.getMsgType() != DHCP6.MsgType.RELAY_REPL.value()) {
+                    log.debug("leaf dhcp6 packet found.");
+                    break;
+                } else {
+                    // found another relay, go for another loop
+                    dhcp6Parent = dhcp6Child;
+                }
+            } else {
+                log.debug("Expected dhcp6 within relay pkt, but no dhcp6 leaf found.");
+                break;
+            }
+        }
+        return dhcp6Child;
+    }
+
+    /**
+     * Determine DHCP message type (direct DHCPv6 or wrapped into relay messages).
+     *
+     * @param relayPacket {@link DHCP6} packet to be parsed
+     * @return {@link DHCP6.MsgType} contained message type of dhcpv6 packet/relay-message
+     */
+    public static DHCP6.MsgType getDhcp6LeafMessageType(DHCP6 relayPacket) {
+        checkNotNull(relayPacket);
+        DHCP6 dhcp6Child = getDhcp6Leaf(relayPacket);
+        return DHCP6.MsgType.getType(dhcp6Child != null ? dhcp6Child.getMsgType() : relayPacket.getMsgType());
+    }
+
+    /**
+     * check if DHCP6 relay-reply is reply.
+     *
+     * @param relayPacket dhcp6 relay-reply
+     * @return boolean relay-reply contains ack
+     */
+    public static boolean isDhcp6Reply(DHCP6 relayPacket) {
+        DHCP6 leafDhcp6 = getDhcp6Leaf(relayPacket);
+        if (leafDhcp6 != null) {
+            if (leafDhcp6.getMsgType() == DHCP6.MsgType.REPLY.value()) {
+                log.debug("isDhcp6Reply  true.");
+                return true;  // must be directly connected
+            } else {
+                log.debug("isDhcp6Reply false. leaf dhcp6 is not replay. MsgType {}", leafDhcp6.getMsgType());
+            }
+        } else {
+            log.debug("isDhcp6Reply false. Expected dhcp6 within relay pkt but not found.");
+        }
+        log.debug("isDhcp6Reply  false.");
+        return false;
+    }
+
+    /**
+     * check if DHCP6 is release or relay-forward contains release.
+     *
+     * @param dhcp6Payload dhcp6 packet
+     * @return boolean dhcp6 contains release
+     */
+    public static  boolean isDhcp6Release(DHCP6 dhcp6Payload) {
+        if (dhcp6Payload.getMsgType() ==  DHCP6.MsgType.RELEASE.value()) {
+            log.debug("isDhcp6Release  true.");
+            return true;  // must be directly connected
+        } else {
+            DHCP6 dhcp6Leaf = getDhcp6Leaf(dhcp6Payload);
+            if (dhcp6Leaf != null) {
+                if (dhcp6Leaf.getMsgType() ==  DHCP6.MsgType.RELEASE.value()) {
+                    log.debug("isDhcp6Release  true. indirectlry connected");
+                    return true;
+                } else {
+                    log.debug("leaf dhcp6 is not release. MsgType {}",  dhcp6Leaf.getMsgType());
+                    return false;
+                }
+            } else {
+                log.debug("isDhcp6Release  false. dhcp6 is niether relay nor release.");
+                return false;
+            }
+        }
+    }
+
+
+    /**
+     * convert dhcp6 msgType to String.
+     *
+     * @param msgTypeVal msgType byte of dhcp6 packet
+     * @return String string value of dhcp6 msg type
+     */
+    public static String getMsgTypeStr(byte msgTypeVal) {
+        MsgType msgType = DHCP6.MsgType.getType(msgTypeVal);
+        return DHCP6.MsgType.getMsgTypeStr(msgType);
+    }
+
+    /**
+     * find the string of dhcp6 leaf packets's msg type.
+     *
+     * @param directConnFlag boolean value indicating direct/indirect connection
+     * @param dhcp6Packet dhcp6 packet
+     * @return String string value of dhcp6 leaf packet msg type
+     */
+    public static String findLeafMsgType(boolean directConnFlag, DHCP6  dhcp6Packet) {
+        if (directConnFlag) {
+            return getMsgTypeStr(dhcp6Packet.getMsgType());
+        } else {
+            DHCP6 leafDhcp = getDhcp6Leaf(dhcp6Packet);
+            if (leafDhcp != null) {
+                return getMsgTypeStr(leafDhcp.getMsgType());
+            } else {
+                return DhcpRelayCounters.INVALID_PACKET;
+            }
+        }
+    }
+
+    /**
+     * Determind if an Interface contains a vlan id.
+     *
+     * @param iface the Interface
+     * @param vlanId the vlan id
+     * @return true if the Interface contains the vlan id
+     */
+    public static boolean interfaceContainsVlan(Interface iface, VlanId vlanId) {
+        if (vlanId.equals(VlanId.NONE)) {
+            // untagged packet, check if vlan untagged or vlan native is not NONE
+            return !iface.vlanUntagged().equals(VlanId.NONE) ||
+                    !iface.vlanNative().equals(VlanId.NONE);
+        }
+        // tagged packet, check if the interface contains the vlan
+        return iface.vlanTagged().contains(vlanId);
+    }
+
+    /**
+     * Check if the host is directly connected to the network or not.
+     *
+     * @param dhcp6Payload the dhcp6 payload
+     * @return true if the host is directly connected to the network; false otherwise
+     */
+    public static boolean directlyConnected(DHCP6 dhcp6Payload) {
+
+        log.debug("directlyConnected enters");
+        if (dhcp6Payload.getMsgType() == DHCP6.MsgType.LEASEQUERY.value() ||
+                dhcp6Payload.getMsgType() == DHCP6.MsgType.LEASEQUERY_REPLY.value()) {
+            log.debug("directlyConnected false. MsgType {}", dhcp6Payload.getMsgType());
+
+            return false;
+        }
+
+        if (dhcp6Payload.getMsgType() != DHCP6.MsgType.RELAY_FORW.value() &&
+                dhcp6Payload.getMsgType() != DHCP6.MsgType.RELAY_REPL.value()) {
+            log.debug("directlyConnected true. MsgType {}", dhcp6Payload.getMsgType());
+
+            return true;
+        }
+        // Regardless of relay-forward or relay-replay, check if we see another relay message
+        DHCP6 dhcp6Payload2 = dhcp6PacketFromRelayPacket(dhcp6Payload);
+        if (dhcp6Payload2 != null) {
+            if (dhcp6Payload.getMsgType() == DHCP6.MsgType.RELAY_FORW.value()) {
+                log.debug("directlyConnected  false. 1st realy-foward, 2nd MsgType {}", dhcp6Payload2.getMsgType());
+                return false;
+            } else {
+                // relay-reply
+                if (dhcp6Payload2.getMsgType() != DHCP6.MsgType.RELAY_REPL.value()
+                        && dhcp6Payload2.getMsgType() != MsgType.LEASEQUERY_REPLY.value()) {
+                    log.debug("directlyConnected  true. 2nd MsgType {}", dhcp6Payload2.getMsgType());
+                    return true;  // must be directly connected
+                } else {
+                    log.debug("directlyConnected  false. 1st relay-reply, 2nd relay-reply MsgType {}",
+                            dhcp6Payload2.getMsgType());
+                    return false;  // must be indirectly connected
+                }
+            }
+        } else {
+            log.debug("directlyConnected  true.");
+            return true;
+        }
+    }
+    /**
+     * Check if a given server info has v6 ipaddress.
+     *
+     * @param serverInfo server info to check
+     * @return true if server info has v6 ip address; false otherwise
+     */
+    public static boolean isServerIpEmpty(DhcpServerInfo serverInfo) {
+        if (!serverInfo.getDhcpServerIp6().isPresent()) {
+            log.warn("DhcpServerIp not available, use default DhcpServerIp {}",
+                    HexString.toHexString(serverInfo.getDhcpServerIp6().get().toOctets()));
+            return true;
+        }
+        return false;
+    }
+
+    private static boolean isConnectMacEmpty(DhcpServerInfo serverInfo, Set<Interface> clientInterfaces) {
+        if (!serverInfo.getDhcpConnectMac().isPresent()) {
+            log.warn("DHCP6 {} not yet resolved .. Aborting DHCP "
+                            + "packet processing from client on port: {}",
+                    !serverInfo.getDhcpGatewayIp6().isPresent() ? "server IP " + serverInfo.getDhcpServerIp6()
+                            : "gateway IP " + serverInfo.getDhcpGatewayIp6(),
+                    clientInterfaces.iterator().next().connectPoint());
+            return true;
+        }
+        return false;
+    }
+
+    private static boolean isRelayAgentIpFromCfgEmpty(DhcpServerInfo serverInfo, DeviceId receivedFromDevice) {
+        if (!serverInfo.getRelayAgentIp6(receivedFromDevice).isPresent()) {
+            log.warn("indirect connection: relayAgentIp NOT availale from config file! Use dynamic.");
+            return true;
+        }
+        return false;
+    }
+
+    private static Dhcp6Option getInterfaceIdIdOption(PacketContext context, Ethernet clientPacket) {
+        String inPortString = "-" + context.inPacket().receivedFrom().toString() + ":";
+        Dhcp6Option interfaceId = new Dhcp6Option();
+        interfaceId.setCode(DHCP6.OptionCode.INTERFACE_ID.value());
+        byte[] clientSoureMacBytes = clientPacket.getSourceMACAddress();
+        byte[] inPortStringBytes = inPortString.getBytes();
+        byte[] vlanIdBytes = new byte[2];
+        vlanIdBytes[0] = (byte) (clientPacket.getVlanID() & 0xff);
+        vlanIdBytes[1] = (byte) ((clientPacket.getVlanID() >> 8) & 0xff);
+        byte[] interfaceIdBytes = new byte[clientSoureMacBytes.length +
+                inPortStringBytes.length + vlanIdBytes.length];
+        log.debug("Length: interfaceIdBytes  {} clientSoureMacBytes {} inPortStringBytes {} vlan {}",
+                interfaceIdBytes.length, clientSoureMacBytes.length, inPortStringBytes.length,
+                vlanIdBytes.length);
+
+        System.arraycopy(clientSoureMacBytes, 0, interfaceIdBytes, 0, clientSoureMacBytes.length);
+        System.arraycopy(inPortStringBytes, 0, interfaceIdBytes, clientSoureMacBytes.length,
+                inPortStringBytes.length);
+        System.arraycopy(vlanIdBytes, 0, interfaceIdBytes,
+                clientSoureMacBytes.length + inPortStringBytes.length,
+                vlanIdBytes.length);
+        interfaceId.setData(interfaceIdBytes);
+        interfaceId.setLength((short) interfaceIdBytes.length);
+        log.debug("interfaceId write srcMac {} portString {}",
+                HexString.toHexString(clientSoureMacBytes, ":"), inPortString);
+        return interfaceId;
+    }
+
+    private static void addDhcp6OptionsFromClient(List<Dhcp6Option> options, byte[] dhcp6PacketByte,
+                                           PacketContext context, Ethernet clientPacket) {
+        Dhcp6Option relayMessage = new Dhcp6Option();
+        relayMessage.setCode(DHCP6.OptionCode.RELAY_MSG.value());
+        relayMessage.setLength((short) dhcp6PacketByte.length);
+        relayMessage.setData(dhcp6PacketByte);
+        options.add(relayMessage);
+        // create interfaceId option
+        Dhcp6Option interfaceId = getInterfaceIdIdOption(context, clientPacket);
+        options.add(interfaceId);
+    }
+
+    /**
+     * build the DHCP6 solicit/request packet with gatewayip.
+     *
+     * @param context packet context
+     * @param clientPacket client ethernet packet
+     * @param clientInterfaces set of client side interfaces
+     * @param serverInfo target server which a packet is generated for
+     * @param serverInterface target server interface
+     * @return ethernet packet with dhcp6 packet info
+     */
+    public static Ethernet buildDhcp6PacketFromClient(PacketContext context, Ethernet clientPacket,
+                                               Set<Interface> clientInterfaces, DhcpServerInfo serverInfo,
+                                               Interface serverInterface) {
+        ConnectPoint receivedFrom = context.inPacket().receivedFrom();
+        DeviceId receivedFromDevice = receivedFrom.deviceId();
+
+        Ip6Address relayAgentIp = getRelayAgentIPv6Address(clientInterfaces);
+        MacAddress relayAgentMac = clientInterfaces.iterator().next().mac();
+        if (relayAgentIp == null || relayAgentMac == null) {
+            log.warn("Missing DHCP relay agent interface Ipv6 addr config for "
+                            + "packet from client on port: {}. Aborting packet processing",
+                    clientInterfaces.iterator().next().connectPoint());
+            return null;
+        }
+        IPv6 clientIpv6 = (IPv6) clientPacket.getPayload();
+        UDP clientUdp = (UDP) clientIpv6.getPayload();
+        DHCP6 clientDhcp6 = (DHCP6) clientUdp.getPayload();
+        boolean directConnFlag = directlyConnected(clientDhcp6);
+
+        Ip6Address serverIpFacing = getFirstIpFromInterface(serverInterface);
+        if (serverIpFacing == null || serverInterface.mac() == null) {
+            log.warn("No IP v6 address for server Interface {}", serverInterface);
+            return null;
+        }
+
+        Ethernet etherReply = clientPacket.duplicate();
+        etherReply.setSourceMACAddress(serverInterface.mac());
+
+        // set default info and replace with indirect if available later on.
+        if (serverInfo.getDhcpConnectMac().isPresent()) {
+            etherReply.setDestinationMACAddress(serverInfo.getDhcpConnectMac().get());
+        }
+        if (serverInfo.getDhcpConnectVlan().isPresent()) {
+            etherReply.setVlanID(serverInfo.getDhcpConnectVlan().get().toShort());
+        }
+        IPv6 ipv6Packet = (IPv6) etherReply.getPayload();
+        byte[] peerAddress = clientIpv6.getSourceAddress();
+        ipv6Packet.setSourceAddress(serverIpFacing.toOctets());
+        ipv6Packet.setDestinationAddress(serverInfo.getDhcpServerIp6().get().toOctets());
+        UDP udpPacket = (UDP) ipv6Packet.getPayload();
+        udpPacket.setSourcePort(UDP.DHCP_V6_SERVER_PORT);
+        DHCP6 dhcp6Packet = (DHCP6) udpPacket.getPayload();
+        byte[] dhcp6PacketByte = dhcp6Packet.serialize();
+
+        DHCP6 dhcp6Relay = new DHCP6();
+
+        dhcp6Relay.setMsgType(DHCP6.MsgType.RELAY_FORW.value());
+
+        if (directConnFlag) {
+            dhcp6Relay.setLinkAddress(relayAgentIp.toOctets());
+        } else {
+            if (isServerIpEmpty(serverInfo)) {
+                log.warn("indirect DhcpServerIp empty... use default server ");
+            } else {
+                // Indirect case, replace destination to indirect dhcp server if exist
+                // Check if mac is obtained for valid server ip
+                if (isConnectMacEmpty(serverInfo, clientInterfaces)) {
+                    log.warn("indirect Dhcp ConnectMac empty ...");
+                    return null;
+                }
+                etherReply.setDestinationMACAddress(serverInfo.getDhcpConnectMac().get());
+                etherReply.setVlanID(serverInfo.getDhcpConnectVlan().get().toShort());
+                ipv6Packet.setDestinationAddress(serverInfo.getDhcpServerIp6().get().toOctets());
+            }
+            if (isRelayAgentIpFromCfgEmpty(serverInfo, receivedFromDevice)) {
+                log.debug("indirect connection: relayAgentIp NOT availale from config file! Use dynamic. {}",
+                        HexString.toHexString(relayAgentIp.toOctets(), ":"));
+                serverIpFacing = relayAgentIp;
+            } else {
+                serverIpFacing = serverInfo.getRelayAgentIp6(receivedFromDevice).get();
+            }
+            log.debug("Source IP address set as relay agent IP with value: {}", serverIpFacing);
+            dhcp6Relay.setLinkAddress(serverIpFacing.toOctets());
+            ipv6Packet.setSourceAddress(serverIpFacing.toOctets());
+        }
+        // peer address: address of the client or relay agent from which the message to be relayed was received.
+        dhcp6Relay.setPeerAddress(peerAddress);
+        // directly connected case, hop count is zero; otherwise, hop count + 1
+        if (directConnFlag) {
+            dhcp6Relay.setHopCount((byte) 0);
+        } else {
+            dhcp6Relay.setHopCount((byte) (dhcp6Packet.getHopCount() + 1));
+        }
+
+        List<Dhcp6Option> options = new ArrayList<>();
+        addDhcp6OptionsFromClient(options, dhcp6PacketByte, context, clientPacket);
+        dhcp6Relay.setOptions(options);
+        udpPacket.setPayload(dhcp6Relay);
+        udpPacket.resetChecksum();
+        ipv6Packet.setPayload(udpPacket);
+        ipv6Packet.setHopLimit((byte) 64);
+        etherReply.setPayload(ipv6Packet);
+
+        return etherReply;
+    }
+
+    /**
+     * build the DHCP6 solicit/request packet with gatewayip.
+     *
+     * @param directConnFlag flag indicating if packet is from direct client or not
+     * @param serverInfo server to check its connect point
+     * @return boolean true if serverInfo is found; false otherwise
+     */
+    public static boolean checkDhcpServerConnPt(boolean directConnFlag,
+                                          DhcpServerInfo serverInfo) {
+        if (serverInfo.getDhcpServerConnectPoint() == null) {
+            log.warn("DHCP6 server connect point for {} connPt {}",
+                    directConnFlag ? "direct" : "indirect", serverInfo.getDhcpServerConnectPoint());
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * extract from dhcp6 packet ClientIdOption.
+     *
+     * @param directConnFlag directly connected host
+     * @param dhcp6Payload the dhcp6 payload
+     * @return Dhcp6ClientIdOption clientIdOption, or null if not exists.
+     */
+    static Dhcp6ClientIdOption extractClientId(Boolean directConnFlag, DHCP6 dhcp6Payload) {
+        Dhcp6ClientIdOption clientIdOption;
+
+        if (directConnFlag) {
+            clientIdOption = dhcp6Payload.getOptions()
+                    .stream()
+                    .filter(opt -> opt instanceof Dhcp6ClientIdOption)
+                    .map(opt -> (Dhcp6ClientIdOption) opt)
+                    .findFirst()
+                    .orElse(null);
+        } else {
+            DHCP6 leafDhcp = Dhcp6HandlerUtil.getDhcp6Leaf(dhcp6Payload);
+            clientIdOption = leafDhcp.getOptions()
+                    .stream()
+                    .filter(opt -> opt instanceof Dhcp6ClientIdOption)
+                    .map(opt -> (Dhcp6ClientIdOption) opt)
+                    .findFirst()
+                    .orElse(null);
+        }
+
+        return clientIdOption;
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/DhcpRelayManager.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/DhcpRelayManager.java
new file mode 100644
index 0000000..f2ca330
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/DhcpRelayManager.java
@@ -0,0 +1,677 @@
+/*
+ * Copyright 2016-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 java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Modified;
+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.ARP;
+import org.onlab.packet.DHCP;
+import org.onlab.packet.DHCP6;
+import org.onlab.packet.IPacket;
+import org.onlab.packet.IPv6;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.UDP;
+import org.onlab.packet.VlanId;
+import org.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.dhcprelay.api.DhcpHandler;
+import org.onosproject.dhcprelay.api.DhcpRelayService;
+import org.onosproject.dhcprelay.api.DhcpServerInfo;
+import org.onosproject.dhcprelay.config.DefaultDhcpRelayConfig;
+import org.onosproject.dhcprelay.config.DhcpServerConfig;
+import org.onosproject.dhcprelay.config.EnableDhcpFpmConfig;
+import org.onosproject.dhcprelay.config.IndirectDhcpRelayConfig;
+import org.onosproject.dhcprelay.config.IgnoreDhcpConfig;
+import org.onosproject.dhcprelay.store.DhcpRecord;
+import org.onosproject.dhcprelay.store.DhcpRelayStore;
+import org.onosproject.dhcprelay.store.DhcpFpmPrefixStore;
+import org.onosproject.routing.fpm.api.FpmRecord;
+import org.onosproject.net.Device;
+import org.onosproject.net.Host;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.intf.InterfaceService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.HostId;
+import org.onosproject.net.config.ConfigFactory;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.config.NetworkConfigListener;
+import org.onosproject.net.config.NetworkConfigRegistry;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.packet.PacketPriority;
+import org.onosproject.net.packet.PacketProcessor;
+import org.onosproject.net.packet.PacketService;
+
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import static org.onlab.util.Tools.groupedThreads;
+
+
+import com.google.common.collect.ImmutableSet;
+
+import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
+
+/**
+ * DHCP Relay Agent Application Component.
+ */
+@Component(immediate = true)
+@Service
+public class DhcpRelayManager implements DhcpRelayService {
+    public static final String DHCP_RELAY_APP = "org.onosproject.dhcprelay";
+    public static final String ROUTE_STORE_IMPL =
+            "org.onosproject.routeservice.store.RouteStoreImpl";
+
+    private static final TrafficSelector ARP_SELECTOR = DefaultTrafficSelector.builder()
+            .matchEthType(Ethernet.TYPE_ARP)
+            .build();
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private final InternalConfigListener cfgListener = new InternalConfigListener();
+
+    private final Set<ConfigFactory> factories = ImmutableSet.of(
+            new ConfigFactory<ApplicationId, DefaultDhcpRelayConfig>(APP_SUBJECT_FACTORY,
+                    DefaultDhcpRelayConfig.class,
+                    DefaultDhcpRelayConfig.KEY,
+                    true) {
+                @Override
+                public DefaultDhcpRelayConfig createConfig() {
+                    return new DefaultDhcpRelayConfig();
+                }
+            },
+            new ConfigFactory<ApplicationId, IndirectDhcpRelayConfig>(APP_SUBJECT_FACTORY,
+                    IndirectDhcpRelayConfig.class,
+                    IndirectDhcpRelayConfig.KEY,
+                    true) {
+                @Override
+                public IndirectDhcpRelayConfig createConfig() {
+                    return new IndirectDhcpRelayConfig();
+                }
+            },
+            new ConfigFactory<ApplicationId, IgnoreDhcpConfig>(APP_SUBJECT_FACTORY,
+                    IgnoreDhcpConfig.class,
+                    IgnoreDhcpConfig.KEY,
+                    true) {
+                @Override
+                public IgnoreDhcpConfig createConfig() {
+                    return new IgnoreDhcpConfig();
+                }
+            },
+            new ConfigFactory<ApplicationId, EnableDhcpFpmConfig>(APP_SUBJECT_FACTORY,
+                    EnableDhcpFpmConfig.class,
+                    EnableDhcpFpmConfig.KEY,
+                    false) {
+                @Override
+                public EnableDhcpFpmConfig createConfig() {
+                    return new EnableDhcpFpmConfig();
+                }
+            }
+    );
+
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected NetworkConfigRegistry cfgService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected PacketService packetService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostService hostService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected InterfaceService interfaceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DhcpRelayStore dhcpRelayStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ComponentConfigService compCfgService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DhcpFpmPrefixStore dhcpFpmPrefixStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY,
+            target = "(version=4)")
+    protected DhcpHandler v4Handler;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY,
+            target = "(version=6)")
+    protected DhcpHandler v6Handler;
+
+    @Property(name = "arpEnabled", boolValue = true,
+            label = "Enable Address resolution protocol")
+    protected boolean arpEnabled = true;
+
+    @Property(name = "dhcpPollInterval", intValue = 24 * 3600,
+            label = "dhcp relay poll interval")
+    protected int dhcpPollInterval = 24 * 3600;
+
+    @Property(name = "dhcpFpmEnabled", boolValue = false,
+            label = "Enable DhcpRelay Fpm")
+    protected boolean dhcpFpmEnabled = false;
+
+    private ScheduledExecutorService timerExecutor;
+
+    protected DeviceListener deviceListener = new InternalDeviceListener();
+    private DhcpRelayPacketProcessor dhcpRelayPacketProcessor = new DhcpRelayPacketProcessor();
+    private ApplicationId appId;
+
+    /**
+     *   One second timer.
+     */
+    class Dhcp6Timer implements Runnable {
+        @Override
+        public void run() {
+            v6Handler.timeTick();
+        }
+    };
+
+    @Activate
+    protected void activate(ComponentContext context) {
+        //start the dhcp relay agent
+        appId = coreService.registerApplication(DHCP_RELAY_APP);
+
+        cfgService.addListener(cfgListener);
+        factories.forEach(cfgService::registerConfigFactory);
+        //update the dhcp server configuration.
+        updateConfig();
+
+        //add the packet processor
+        packetService.addProcessor(dhcpRelayPacketProcessor, PacketProcessor.director(0));
+
+        timerExecutor = Executors.newScheduledThreadPool(1,
+                groupedThreads("dhcpRelay",
+                        "config-reloader-%d", log));
+        timerExecutor.scheduleAtFixedRate(new Dhcp6Timer(),
+                0,
+                dhcpPollInterval,
+                TimeUnit.SECONDS);
+
+        modified(context);
+
+        // Enable distribute route store
+        compCfgService.preSetProperty(ROUTE_STORE_IMPL,
+                               "distributed", Boolean.TRUE.toString());
+        compCfgService.registerProperties(getClass());
+
+        deviceService.addListener(deviceListener);
+
+
+
+        log.info("DHCP-RELAY Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        cfgService.removeListener(cfgListener);
+        factories.forEach(cfgService::unregisterConfigFactory);
+        packetService.removeProcessor(dhcpRelayPacketProcessor);
+        cancelArpPackets();
+        compCfgService.unregisterProperties(getClass(), false);
+        deviceService.removeListener(deviceListener);
+        timerExecutor.shutdown();
+
+        log.info("DHCP-RELAY Stopped");
+    }
+
+    @Modified
+    protected void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context.getProperties();
+        Boolean flag;
+
+        flag = Tools.isPropertyEnabled(properties, "arpEnabled");
+        if (flag != null) {
+            arpEnabled = flag;
+            log.info("Address resolution protocol is {}",
+                    arpEnabled ? "enabled" : "disabled");
+        }
+
+        if (arpEnabled) {
+            requestArpPackets();
+        } else {
+            cancelArpPackets();
+        }
+
+        int intervalVal = Tools.getIntegerProperty(properties, "dhcpPollInterval");
+        log.info("DhcpRelay poll interval new {} old {}", intervalVal, dhcpPollInterval);
+        if (intervalVal !=  dhcpPollInterval) {
+            timerExecutor.shutdown();
+            dhcpPollInterval = intervalVal;
+            timerExecutor = Executors.newScheduledThreadPool(1,
+                    groupedThreads("dhcpRelay",
+                            "config-reloader-%d", log));
+            timerExecutor.scheduleAtFixedRate(new Dhcp6Timer(),
+                        0,
+                        dhcpPollInterval > 1 ? dhcpPollInterval : 1,
+                        TimeUnit.SECONDS);
+            v6Handler.setDhcp6PollInterval(dhcpPollInterval);
+        }
+
+        flag = Tools.isPropertyEnabled(properties, "dhcpFpmEnabled");
+        if (flag != null) {
+            boolean oldValue = dhcpFpmEnabled;
+            dhcpFpmEnabled = flag;
+            log.info("DhcpRelay FPM is {}",
+                    dhcpFpmEnabled ? "enabled" : "disabled");
+
+            if (dhcpFpmEnabled && !oldValue) {
+                log.info("Dhcp Fpm is enabled.");
+                processDhcpFpmRoutes(true);
+            }
+            if (!dhcpFpmEnabled && oldValue) {
+                log.info("Dhcp Fpm is disabled.");
+                processDhcpFpmRoutes(false);
+            }
+            v6Handler.setDhcpFpmEnabled(dhcpFpmEnabled);
+        }
+    }
+
+    private static List<TrafficSelector> buildClientDhcpSelectors() {
+        return Streams.concat(Dhcp4HandlerImpl.DHCP_SELECTORS.stream(),
+                              Dhcp6HandlerImpl.DHCP_SELECTORS.stream())
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Updates DHCP relay app configuration.
+     */
+    private void updateConfig() {
+        DefaultDhcpRelayConfig defaultConfig =
+                cfgService.getConfig(appId, DefaultDhcpRelayConfig.class);
+        IndirectDhcpRelayConfig indirectConfig =
+                cfgService.getConfig(appId, IndirectDhcpRelayConfig.class);
+        IgnoreDhcpConfig ignoreDhcpConfig =
+                cfgService.getConfig(appId, IgnoreDhcpConfig.class);
+
+        if (defaultConfig != null) {
+            updateConfig(defaultConfig);
+        }
+        if (indirectConfig != null) {
+            updateConfig(indirectConfig);
+        }
+        if (ignoreDhcpConfig != null) {
+            updateConfig(ignoreDhcpConfig);
+        }
+    }
+
+    /**
+     * Updates DHCP relay app configuration with given configuration.
+     *
+     * @param config the configuration ot update
+     */
+    protected void updateConfig(Config config) {
+        if (config instanceof IndirectDhcpRelayConfig) {
+            IndirectDhcpRelayConfig indirectConfig = (IndirectDhcpRelayConfig) config;
+            v4Handler.setIndirectDhcpServerConfigs(indirectConfig.dhcpServerConfigs());
+            v6Handler.setIndirectDhcpServerConfigs(indirectConfig.dhcpServerConfigs());
+        } else if (config instanceof DefaultDhcpRelayConfig) {
+            DefaultDhcpRelayConfig defaultConfig = (DefaultDhcpRelayConfig) config;
+            v4Handler.setDefaultDhcpServerConfigs(defaultConfig.dhcpServerConfigs());
+            v6Handler.setDefaultDhcpServerConfigs(defaultConfig.dhcpServerConfigs());
+        }
+        if (config instanceof IgnoreDhcpConfig) {
+            v4Handler.updateIgnoreVlanConfig((IgnoreDhcpConfig) config);
+            v6Handler.updateIgnoreVlanConfig((IgnoreDhcpConfig) config);
+        }
+    }
+
+    protected void removeConfig(Config config) {
+        if (config instanceof IndirectDhcpRelayConfig) {
+            v4Handler.setIndirectDhcpServerConfigs(Collections.emptyList());
+            v6Handler.setIndirectDhcpServerConfigs(Collections.emptyList());
+        } else if (config instanceof DefaultDhcpRelayConfig) {
+            v4Handler.setDefaultDhcpServerConfigs(Collections.emptyList());
+            v6Handler.setDefaultDhcpServerConfigs(Collections.emptyList());
+        }
+        if (config instanceof IgnoreDhcpConfig) {
+            v4Handler.updateIgnoreVlanConfig(null);
+            v6Handler.updateIgnoreVlanConfig(null);
+        }
+    }
+
+    private void processDhcpFpmRoutes(Boolean add) {
+        // needs to restore/remove fpm
+    }
+
+    public boolean isDhcpFpmEnabled() {
+        return dhcpFpmEnabled;
+    }
+
+    /**
+     * Request ARP packet in via PacketService.
+     */
+    private void requestArpPackets() {
+        packetService.requestPackets(ARP_SELECTOR, PacketPriority.CONTROL, appId);
+    }
+
+    /**
+     * Cancel requested ARP packets in via packet service.
+     */
+    private void cancelArpPackets() {
+        packetService.cancelPackets(ARP_SELECTOR, PacketPriority.CONTROL, appId);
+    }
+
+    @Override
+    public Optional<DhcpRecord> getDhcpRecord(HostId hostId) {
+        return dhcpRelayStore.getDhcpRecord(hostId);
+    }
+
+    @Override
+    public Collection<DhcpRecord> getDhcpRecords() {
+        return dhcpRelayStore.getDhcpRecords();
+    }
+    @Override
+    public void updateDhcpRecord(HostId hostId, DhcpRecord dhcpRecord) {
+        dhcpRelayStore.updateDhcpRecord(hostId, dhcpRecord);
+    }
+    @Override
+    public Optional<MacAddress> getDhcpServerMacAddress() {
+        // TODO: depreated it
+        DefaultDhcpRelayConfig config = cfgService.getConfig(appId, DefaultDhcpRelayConfig.class);
+        DhcpServerConfig serverConfig = config.dhcpServerConfigs().get(0);
+        Ip4Address serverip = serverConfig.getDhcpServerIp4().get();
+        return hostService.getHostsByIp(serverip)
+                .stream()
+                .map(Host::mac)
+                .findFirst();
+    }
+
+    @Override
+    public List<DhcpServerInfo> getDefaultDhcpServerInfoList() {
+        return ImmutableList.<DhcpServerInfo>builder()
+                .addAll(v4Handler.getDefaultDhcpServerInfoList())
+                .addAll(v6Handler.getDefaultDhcpServerInfoList())
+                .build();
+    }
+
+    @Override
+    public List<DhcpServerInfo> getIndirectDhcpServerInfoList() {
+        return ImmutableList.<DhcpServerInfo>builder()
+                .addAll(v4Handler.getIndirectDhcpServerInfoList())
+                .addAll(v6Handler.getIndirectDhcpServerInfoList())
+                .build();
+    }
+
+    /**
+     * Gets DHCP data from a packet.
+     *
+     * @param packet the packet
+     * @return the DHCP data; empty if it is not a DHCP packet
+     */
+    private Optional<DHCP> findDhcp(Ethernet packet) {
+        return Stream.of(packet)
+                .filter(Objects::nonNull)
+                .map(Ethernet::getPayload)
+                .filter(p -> p instanceof IPv4)
+                .map(IPacket::getPayload)
+                .filter(Objects::nonNull)
+                .filter(p -> p instanceof UDP)
+                .map(IPacket::getPayload)
+                .filter(Objects::nonNull)
+                .filter(p -> p instanceof DHCP)
+                .map(p -> (DHCP) p)
+                .findFirst();
+    }
+
+    /**
+     * Gets DHCPv6 data from a packet.
+     *
+     * @param packet the packet
+     * @return the DHCPv6 data; empty if it is not a DHCPv6 packet
+     */
+    private Optional<DHCP6> findDhcp6(Ethernet packet) {
+        return Stream.of(packet)
+                .filter(Objects::nonNull)
+                .map(Ethernet::getPayload)
+                .filter(p -> p instanceof IPv6)
+                .map(IPacket::getPayload)
+                .filter(Objects::nonNull)
+                .filter(p -> p instanceof UDP)
+                .map(IPacket::getPayload)
+                .filter(Objects::nonNull)
+                .filter(p -> p instanceof DHCP6)
+                .map(p -> (DHCP6) p)
+                .findFirst();
+    }
+
+
+    private class DhcpRelayPacketProcessor implements PacketProcessor {
+
+        @Override
+        public void process(PacketContext context) {
+            // process the packet and get the payload
+            Ethernet packet = context.inPacket().parsed();
+            if (packet == null) {
+                return;
+            }
+
+            findDhcp(packet).ifPresent(dhcpPayload -> {
+                v4Handler.processDhcpPacket(context, dhcpPayload);
+            });
+
+            findDhcp6(packet).ifPresent(dhcp6Payload -> {
+                v6Handler.processDhcpPacket(context, dhcp6Payload);
+            });
+
+            if (packet.getEtherType() == Ethernet.TYPE_ARP && arpEnabled) {
+                ARP arpPacket = (ARP) packet.getPayload();
+                VlanId vlanId = VlanId.vlanId(packet.getVlanID());
+                Set<Interface> interfaces = interfaceService.
+                        getInterfacesByPort(context.inPacket().receivedFrom());
+                //ignore the packets if dhcp server interface is not configured on onos.
+                if (interfaces.isEmpty()) {
+                    log.warn("server virtual interface not configured");
+                    return;
+                }
+                if ((arpPacket.getOpCode() != ARP.OP_REQUEST)) {
+                    // handle request only
+                    return;
+                }
+                MacAddress interfaceMac = interfaces.stream()
+                        .filter(iface -> iface.vlan().equals(vlanId))
+                        .map(Interface::mac)
+                        .filter(mac -> !mac.equals(MacAddress.NONE))
+                        .findFirst()
+                        .orElse(MacAddress.ONOS);
+                if (interfaceMac == null) {
+                    // can't find interface mac address
+                    return;
+                }
+                processArpPacket(context, packet, interfaceMac);
+            }
+        }
+
+        /**
+         * Processes the ARP Payload and initiates a reply to the client.
+         *
+         * @param context the packet context
+         * @param packet the ethernet payload
+         * @param replyMac mac address to be replied
+         */
+        private void processArpPacket(PacketContext context, Ethernet packet, MacAddress replyMac) {
+            ARP arpPacket = (ARP) packet.getPayload();
+            ARP arpReply = arpPacket.duplicate();
+            arpReply.setOpCode(ARP.OP_REPLY);
+
+            arpReply.setTargetProtocolAddress(arpPacket.getSenderProtocolAddress());
+            arpReply.setTargetHardwareAddress(arpPacket.getSenderHardwareAddress());
+            arpReply.setSenderProtocolAddress(arpPacket.getTargetProtocolAddress());
+            arpReply.setSenderHardwareAddress(replyMac.toBytes());
+
+            // Ethernet Frame.
+            Ethernet ethReply = new Ethernet();
+            ethReply.setSourceMACAddress(replyMac.toBytes());
+            ethReply.setDestinationMACAddress(packet.getSourceMAC());
+            ethReply.setEtherType(Ethernet.TYPE_ARP);
+            ethReply.setVlanID(packet.getVlanID());
+            ethReply.setPayload(arpReply);
+
+            ConnectPoint targetPort = context.inPacket().receivedFrom();
+            TrafficTreatment t = DefaultTrafficTreatment.builder()
+                    .setOutput(targetPort.port()).build();
+            OutboundPacket o = new DefaultOutboundPacket(
+                    targetPort.deviceId(), t, ByteBuffer.wrap(ethReply.serialize()));
+            if (log.isTraceEnabled()) {
+                log.trace("Relaying ARP packet {} to {}", packet, targetPort);
+            }
+            packetService.emit(o);
+        }
+    }
+
+    /**
+     * Listener for network config events.
+     */
+    private class InternalConfigListener implements NetworkConfigListener {
+        @Override
+        public void event(NetworkConfigEvent event) {
+            switch (event.type()) {
+                case CONFIG_UPDATED:
+                case CONFIG_ADDED:
+                    event.config().ifPresent(config -> {
+                        updateConfig(config);
+                        log.info("{} updated", config.getClass().getSimpleName());
+                    });
+                    break;
+                case CONFIG_REMOVED:
+                    event.prevConfig().ifPresent(config -> {
+                        removeConfig(config);
+                        log.info("{} removed", config.getClass().getSimpleName());
+                    });
+                    break;
+                case CONFIG_REGISTERED:
+                case CONFIG_UNREGISTERED:
+                    break;
+                default:
+                    log.warn("Unsupported event type {}", event.type());
+                    break;
+            }
+        }
+
+        @Override
+        public boolean isRelevant(NetworkConfigEvent event) {
+            if (event.configClass().equals(DefaultDhcpRelayConfig.class) ||
+                    event.configClass().equals(IndirectDhcpRelayConfig.class) ||
+                    event.configClass().equals(IgnoreDhcpConfig.class)) {
+                return true;
+            }
+            log.debug("Ignore irrelevant event class {}", event.configClass().getName());
+            return false;
+        }
+    }
+
+    private class InternalDeviceListener implements DeviceListener {
+
+        @Override
+        public void event(DeviceEvent event) {
+            Device device = event.subject();
+            switch (event.type()) {
+                case DEVICE_ADDED:
+                    updateIgnoreVlanConfigs();
+                    break;
+                case DEVICE_AVAILABILITY_CHANGED:
+                    deviceAvailabilityChanged(device);
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        private void deviceAvailabilityChanged(Device device) {
+            if (deviceService.isAvailable(device.id())) {
+                updateIgnoreVlanConfigs();
+            } else {
+                removeIgnoreVlanState();
+            }
+        }
+
+        private void updateIgnoreVlanConfigs() {
+            IgnoreDhcpConfig config = cfgService.getConfig(appId, IgnoreDhcpConfig.class);
+            v4Handler.updateIgnoreVlanConfig(config);
+            v6Handler.updateIgnoreVlanConfig(config);
+        }
+
+        private void removeIgnoreVlanState() {
+            IgnoreDhcpConfig config = cfgService.getConfig(appId, IgnoreDhcpConfig.class);
+            v4Handler.removeIgnoreVlanState(config);
+            v6Handler.removeIgnoreVlanState(config);
+        }
+    }
+
+
+
+    public Optional<FpmRecord> getFpmRecord(IpPrefix prefix) {
+        return dhcpFpmPrefixStore.getFpmRecord(prefix);
+    }
+
+    public Collection<FpmRecord> getFpmRecords() {
+        return dhcpFpmPrefixStore.getFpmRecords();
+    }
+
+    @Override
+    public void addFpmRecord(IpPrefix prefix, FpmRecord fpmRecord) {
+        dhcpFpmPrefixStore.addFpmRecord(prefix, fpmRecord);
+    }
+
+    @Override
+    public Optional<FpmRecord> removeFpmRecord(IpPrefix prefix) {
+        return dhcpFpmPrefixStore.removeFpmRecord(prefix);
+    }
+
+
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/InternalPacket.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/InternalPacket.java
new file mode 100644
index 0000000..70d4ae2
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/InternalPacket.java
@@ -0,0 +1,52 @@
+/*
+ * 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 org.onlab.packet.Ethernet;
+import org.onosproject.net.ConnectPoint;
+
+/**
+ * Container for Ethernet packet and destination port.
+ */
+final class InternalPacket {
+    private Ethernet packet;
+    private ConnectPoint destLocation;
+
+    private InternalPacket(Ethernet newPacket, ConnectPoint newLocation) {
+        packet = newPacket;
+        destLocation = newLocation;
+    }
+
+    public Ethernet getPacket() {
+        return packet;
+    }
+
+    public ConnectPoint getDestLocation() {
+        return destLocation;
+    }
+
+    /**
+     * Build {@link InternalPacket} object instance.
+     *
+     * @param newPacket {@link Ethernet} packet to be sent
+     * @param newLocation {@link ConnectPoint} packet destination
+     * @return new instance of {@link InternalPacket} class
+     */
+    public static InternalPacket internalPacket(Ethernet newPacket, ConnectPoint newLocation) {
+        return new InternalPacket(newPacket, newLocation);
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/api/DhcpHandler.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/api/DhcpHandler.java
new file mode 100644
index 0000000..3de3ced
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/api/DhcpHandler.java
@@ -0,0 +1,199 @@
+/*
+ * 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.api;
+
+import org.onlab.packet.BasePacket;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.dhcprelay.config.DhcpServerConfig;
+import org.onosproject.dhcprelay.config.IgnoreDhcpConfig;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.packet.PacketContext;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * DHCP relay handler.
+ */
+public interface DhcpHandler {
+    /**
+     * Process the DHCP packet before sending to server or client.
+     *
+     * @param context the packet context
+     * @param dhcpPayload the DHCP payload
+     */
+    void processDhcpPacket(PacketContext context, BasePacket dhcpPayload);
+
+    /**
+     * Gets DHCP server IP.
+     *
+     * @return IP address of DHCP server; empty value if not exist
+     * @deprecated 1.12 get the address from config service
+     */
+    @Deprecated
+    default Optional<IpAddress> getDhcpServerIp() {
+        throw new UnsupportedOperationException("Method deprecated");
+    }
+
+    /**
+     * Gets DHCP gateway IP.
+     *
+     * @return IP address of DHCP gateway; empty value if not exist
+     * @deprecated 1.12 get the address from config service
+     */
+    @Deprecated
+    default Optional<IpAddress> getDhcpGatewayIp() {
+        throw new UnsupportedOperationException("Method deprecated");
+    }
+
+    /**
+     * Gets DHCP connect Mac address.
+     *
+     * @return the connect Mac address of server or gateway
+     * @deprecated 1.12 get host mac from host service
+     */
+    @Deprecated
+    default Optional<MacAddress> getDhcpConnectMac() {
+        throw new UnsupportedOperationException("Method deprecated");
+    }
+
+    /**
+     * Sets DHCP gateway IP.
+     *
+     * @param dhcpGatewayIp the DHCP gateway IP
+     * @deprecated 1.12 use setDefaultDhcpServerConfigs or setindirectDhcpServerConfigs
+     */
+    @Deprecated
+    default void setDhcpGatewayIp(IpAddress dhcpGatewayIp) {
+        throw new UnsupportedOperationException("Method deprecated");
+    }
+
+    /**
+     * Sets DHCP connect vlan.
+     *
+     * @param dhcpConnectVlan the DHCP connect vlan
+     * @deprecated 1.12 use setDefaultDhcpServerConfigs or setindirectDhcpServerConfigs
+     */
+    @Deprecated
+    default void setDhcpConnectVlan(VlanId dhcpConnectVlan) {
+        throw new UnsupportedOperationException("Method deprecated");
+    }
+
+    /**
+     * Sets DHCP connect Mac address.
+     *
+     * @param dhcpConnectMac the connect Mac address
+     * @deprecated 1.12 use setDefaultDhcpServerConfigs or setindirectDhcpServerConfigs
+     */
+    @Deprecated
+    default void setDhcpConnectMac(MacAddress dhcpConnectMac) {
+        throw new UnsupportedOperationException("Method deprecated");
+    }
+
+    /**
+     * Sets DHCP server connect point.
+     *
+     * @param dhcpServerConnectPoint the server connect point
+     * @deprecated 1.12 use setDefaultDhcpServerConfigs or setindirectDhcpServerConfigs
+     */
+    @Deprecated
+    default void setDhcpServerConnectPoint(ConnectPoint dhcpServerConnectPoint) {
+        throw new UnsupportedOperationException("Method deprecated");
+    }
+
+    /**
+     * Sets DHCP server IP.
+     *
+     * @param dhcpServerIp the DHCP server IP
+     * @deprecated 1.12 use setDefaultDhcpServerConfigs or setindirectDhcpServerConfigs
+     */
+    @Deprecated
+    default void setDhcpServerIp(IpAddress dhcpServerIp) {
+        throw new UnsupportedOperationException("Method deprecated");
+    }
+
+    /**
+     * Gets list of default DHCP server information.
+     *
+     * @return list of default DHCP server information
+     */
+    default List<DhcpServerInfo> getDefaultDhcpServerInfoList() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * Gets list of indirect DHCP server information.
+     *
+     * @return list of indirect DHCP server information
+     */
+    default List<DhcpServerInfo> getIndirectDhcpServerInfoList() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * Sets DHCP server config for default case.
+     *
+     * @param configs the config
+     */
+    void setDefaultDhcpServerConfigs(Collection<DhcpServerConfig> configs);
+
+    /**
+     * Sets DHCP server config for indirect case.
+     *
+     * @param configs the config
+     */
+    void setIndirectDhcpServerConfigs(Collection<DhcpServerConfig> configs);
+
+    /**
+     * Push IgnoreDhcpConfig to the handler.
+     *
+     * @param config the config
+     */
+    void updateIgnoreVlanConfig(IgnoreDhcpConfig config);
+
+    /**
+     * Remove internal state for IgnoreDhcp.
+     *
+     * @param config the config
+     */
+    void removeIgnoreVlanState(IgnoreDhcpConfig config);
+
+    /**
+     * Hander for Dhcp expiration poll timer.
+     *
+     */
+    default void timeTick() { }
+
+    /**
+     * Update Dhcp expiration poll timer value.
+     *
+     * @param val the timer interval value
+     */
+    default void setDhcp6PollInterval(int val) { }
+
+    /**
+     * Sets DHCP FPM Enable state.
+     *
+     * @param dhcpFpmFlag flag indicating dhcpFpmEnable state
+     */
+    default void setDhcpFpmEnabled(Boolean dhcpFpmFlag) { }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/api/DhcpRelayService.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/api/DhcpRelayService.java
new file mode 100644
index 0000000..5eb8d9e
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/api/DhcpRelayService.java
@@ -0,0 +1,114 @@
+/*
+ * 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.api;
+
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onosproject.dhcprelay.store.DhcpRecord;
+import org.onosproject.routing.fpm.api.FpmRecord;
+import org.onosproject.net.HostId;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+public interface DhcpRelayService {
+    /**
+     * Gets DHCP record for specific host id (mac + vlan).
+     *
+     * @param hostId the id of host
+     * @return the DHCP record of the host
+     */
+    Optional<DhcpRecord> getDhcpRecord(HostId hostId);
+
+    /**
+     * Gets all DHCP records from store.
+     *
+     * @return all DHCP records from store
+     */
+    Collection<DhcpRecord> getDhcpRecords();
+
+    /**
+     * Updates DHCP record for specific host id (mac + vlan).
+     *
+     * @param hostId the id of host
+     * @param dhcpRecord the DHCP record of the host
+     */
+    void updateDhcpRecord(HostId hostId, DhcpRecord dhcpRecord);
+
+    /**
+     * Gets mac address of DHCP server.
+     *
+     * @return the mac address of DHCP server; empty if not exist
+     * @deprecated 1.12, use get DHCP server configs method
+     */
+    @Deprecated
+    Optional<MacAddress> getDhcpServerMacAddress();
+
+    /**
+     * Gets list of default DHCP server information.
+     *
+     * @return list of default DHCP server information
+     */
+    List<DhcpServerInfo> getDefaultDhcpServerInfoList();
+
+    /**
+     * Gets list of indirect DHCP server information.
+     *
+     * @return list of indirect DHCP server information
+     */
+    List<DhcpServerInfo> getIndirectDhcpServerInfoList();
+
+    /**
+     * Add DHCP FPM record to store.
+     *
+     * @param prefix the prefix
+     * @param fpmRecord the fpmRecord
+     */
+    void addFpmRecord(IpPrefix prefix, FpmRecord fpmRecord);
+
+    /**
+     * Delete DHCP FPM record from store.
+     *
+     * @param prefix the prefix
+     * @return DHCP record from store; empty value if it does not exist.
+     */
+    Optional<FpmRecord> removeFpmRecord(IpPrefix prefix);
+
+    /**
+     * Gets PD route record for specific prefix.
+     *
+     * @param prefix PD prefix
+     * @return the PD route record from store
+     */
+    Optional<FpmRecord> getFpmRecord(IpPrefix prefix);
+
+    /**
+     * Gets all PD route records from store.
+     *
+     * @return all PD records from store
+     */
+    Collection<FpmRecord> getFpmRecords();
+
+    /**
+     * Determine if DHCP FPM feature is enabled or not.
+     *
+     * @return boolean value
+     */
+    public boolean isDhcpFpmEnabled();
+
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/api/DhcpServerInfo.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/api/DhcpServerInfo.java
new file mode 100644
index 0000000..74328ec
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/api/DhcpServerInfo.java
@@ -0,0 +1,154 @@
+/*
+ * 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.api;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import com.google.common.collect.Maps;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.dhcprelay.config.DhcpServerConfig;
+
+import java.util.Optional;
+
+/**
+ * class contains DHCP server information.
+ */
+public class DhcpServerInfo extends DhcpServerConfig {
+    public enum Version {
+        DHCP_V4,
+        DHCP_V6
+    }
+    private MacAddress dhcpConnectMac;
+    private VlanId dhcpConnectVlan;
+    private Version version;
+
+    /**
+     * Creates DHCP server information from config.
+     *
+     * @param config DHCP server config
+     * @param version DHCP version for the server
+     */
+    public DhcpServerInfo(DhcpServerConfig config, Version version) {
+        this.relayAgentIps = Maps.newHashMap(config.getRelayAgentIps());
+        this.connectPoint = config.getDhcpServerConnectPoint().orElse(null);
+        this.version = version;
+
+        switch (version) {
+            case DHCP_V4:
+                this.serverIp4Addr = config.getDhcpServerIp4().orElse(null);
+                this.gatewayIp4Addr = config.getDhcpGatewayIp4().orElse(null);
+                break;
+            case DHCP_V6:
+                this.serverIp6Addr = config.getDhcpServerIp6().orElse(null);
+                this.gatewayIp6Addr = config.getDhcpGatewayIp6().orElse(null);
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Sets DHCP server or gateway mac address.
+     *
+     * @param dhcpConnectMac the mac address
+     */
+    public void setDhcpConnectMac(MacAddress dhcpConnectMac) {
+        this.dhcpConnectMac = dhcpConnectMac;
+    }
+
+    /**
+     * Sets DHCP server or gateway vlan id.
+     *
+     * @param dhcpConnectVlan the vlan id
+     */
+    public void setDhcpConnectVlan(VlanId dhcpConnectVlan) {
+        this.dhcpConnectVlan = dhcpConnectVlan;
+    }
+
+    /**
+     * Gets DHCP server or gateway mac address.
+     *
+     * @return the mac address
+     */
+    public Optional<MacAddress> getDhcpConnectMac() {
+        return Optional.ofNullable(dhcpConnectMac);
+    }
+
+    /**
+     * Gets DHCP server or gateway vlan id.
+     *
+     * @return the vlan id.
+     */
+    public Optional<VlanId> getDhcpConnectVlan() {
+        return Optional.ofNullable(dhcpConnectVlan);
+    }
+
+    /**
+     * Get DHCP version of the DHCP server.
+     *
+     * @return the version; can be DHCP_V4 or DHCP_V6
+     */
+    public Version getVersion() {
+        return version;
+    }
+
+    @Override
+    public String toString() {
+        MoreObjects.ToStringHelper toStringHelper = MoreObjects.toStringHelper(this);
+        toStringHelper
+                .add("dhcpConnectMac", dhcpConnectMac)
+                .add("dhcpConnectVlan", dhcpConnectVlan)
+                .add("connectPoint", connectPoint)
+                .add("version", version);
+        switch (version) {
+            case DHCP_V4:
+                toStringHelper
+                        .add("serverIp4Addr", serverIp4Addr)
+                        .add("gatewayIp4Addr", gatewayIp4Addr);
+                break;
+            case DHCP_V6:
+                toStringHelper
+                        .add("serverIp6Addr", serverIp6Addr)
+                        .add("gatewayIp6Addr", gatewayIp6Addr);
+                break;
+            default:
+                break;
+        }
+        return toStringHelper.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof DhcpServerInfo)) {
+            return false;
+        }
+        DhcpServerInfo that = (DhcpServerInfo) o;
+        return super.equals(o) &&
+                Objects.equal(dhcpConnectMac, that.dhcpConnectMac) &&
+                Objects.equal(dhcpConnectVlan, that.dhcpConnectVlan) &&
+                version == that.version;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(super.hashCode(), dhcpConnectMac, dhcpConnectVlan, version);
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/api/package-info.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/api/package-info.java
new file mode 100644
index 0000000..fa239e8
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/api/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ *
+ */
+
+/**
+ * APIs for DHCP relay application.
+ */
+package org.onosproject.dhcprelay.api;
\ No newline at end of file
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpFpmAddCommand.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpFpmAddCommand.java
new file mode 100644
index 0000000..fd886b9
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpFpmAddCommand.java
@@ -0,0 +1,56 @@
+/*
+ * 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.cli;
+
+//import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.apache.karaf.shell.commands.Argument;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.IpAddress;
+import org.onosproject.routing.fpm.api.FpmRecord;
+import org.onosproject.dhcprelay.api.DhcpRelayService;
+
+/**
+ * Prints Dhcp FPM Routes information.
+ */
+@Command(scope = "onos", name = "dhcp-fpm-add",
+         description = "Add DHCP FPM prefix in dhcp-fpm-store.")
+public class DhcpFpmAddCommand extends AbstractShellCommand {
+
+    private static final DhcpRelayService DHCP_RELAY_SERVICE = get(DhcpRelayService.class);
+
+    @Argument(index = 0, name = "prefix",
+            description = "prefix",
+            required = true, multiValued = false)
+    String prefixString = null;
+
+    @Argument(index = 1, name = "next hop",
+            description = "next hop",
+            required = true, multiValued = false)
+    String nextHopString = null;
+
+    @Override
+    protected void execute() {
+
+        IpPrefix prefix = IpPrefix.valueOf(prefixString);
+        IpAddress nextHop = IpAddress.valueOf(nextHopString);
+        FpmRecord record = new FpmRecord(prefix, nextHop, FpmRecord.Type.DHCP_RELAY);
+
+        DHCP_RELAY_SERVICE.addFpmRecord(prefix, record);
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpFpmDeleteCommand.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpFpmDeleteCommand.java
new file mode 100644
index 0000000..244bcf8
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpFpmDeleteCommand.java
@@ -0,0 +1,45 @@
+/*
+ * 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.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.apache.karaf.shell.commands.Argument;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.dhcprelay.api.DhcpRelayService;
+
+/**
+ * Prints Dhcp FPM Routes information.
+ */
+@Command(scope = "onos", name = "dhcp-fpm-delete",
+         description = "delete DHCP FPM prefix in dhcp-fpm-store")
+public class DhcpFpmDeleteCommand extends AbstractShellCommand {
+
+    private static final DhcpRelayService DHCP_RELAY_SERVICE = get(DhcpRelayService.class);
+
+    @Argument(index = 0, name = "prefix",
+            description = "prefix",
+            required = true, multiValued = false)
+    String prefixString = null;
+
+    @Override
+    protected void execute() {
+        IpPrefix prefix = IpPrefix.valueOf(prefixString);
+
+        DHCP_RELAY_SERVICE.removeFpmRecord(prefix);
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpFpmRoutesCommand.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpFpmRoutesCommand.java
new file mode 100644
index 0000000..9aacac3
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpFpmRoutesCommand.java
@@ -0,0 +1,56 @@
+/*
+ * 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.cli;
+
+//import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+
+import org.onosproject.dhcprelay.api.DhcpRelayService;
+import org.onosproject.routing.fpm.api.FpmRecord;
+
+import java.util.Collection;
+
+/**
+ * Prints Dhcp FPM Routes information.
+ */
+@Command(scope = "onos", name = "dhcp-fpm-routes",
+         description = "DHCP FPM routes cli.")
+public class DhcpFpmRoutesCommand extends AbstractShellCommand {
+    private static final String NO_RECORDS = "No DHCP FPM Route record found";
+    private static final String HEADER = "DHCP FPM Routes records :";
+    private static final String ROUTE = "prefix=%s, next-hop=%s";
+
+
+    private static final DhcpRelayService DHCP_RELAY_SERVICE = get(DhcpRelayService.class);
+
+    @Override
+    protected void execute() {
+
+            print("Dhcp Fpm Feature is %s !", DHCP_RELAY_SERVICE.isDhcpFpmEnabled() ? "enabled" : "disabled");
+            print("\n");
+            Collection<FpmRecord> records = DHCP_RELAY_SERVICE.getFpmRecords();
+            if (records.isEmpty()) {
+                print(NO_RECORDS);
+                return;
+            }
+            print(HEADER);
+            records.forEach(record -> print(ROUTE,
+                    record.ipPrefix(),
+                    record.nextHop()));
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpRelayAggCountersCommand.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpRelayAggCountersCommand.java
new file mode 100644
index 0000000..a91a53a
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpRelayAggCountersCommand.java
@@ -0,0 +1,82 @@
+/*
+ * 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.cli;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.dhcprelay.api.DhcpRelayService;
+import org.onosproject.dhcprelay.store.DhcpRelayCounters;
+import org.onosproject.dhcprelay.store.DhcpRelayCountersStore;
+
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Prints Dhcp FPM Routes information.
+ */
+@Command(scope = "onos", name = "dhcp-relay-agg-counters",
+         description = "DHCP Relay Aggregate Counters cli.")
+public class DhcpRelayAggCountersCommand extends AbstractShellCommand {
+    @Argument(index = 0, name = "reset",
+            description = "reset counters or not",
+            required = false, multiValued = false)
+    String reset = null;
+
+    private static final String HEADER = "DHCP Relay Aggregate Counters :";
+    private static final String GCOUNT = "global";
+    private static final DhcpRelayService DHCP_RELAY_SERVICE = get(DhcpRelayService.class);
+
+    @Override
+    protected void execute() {
+        boolean toResetFlag;
+
+        if (reset != null) {
+            if (reset.equals("reset") || reset.equals("[reset]")) {
+                toResetFlag = true;
+            } else {
+                print("Last parameter is [reset]");
+                return;
+            }
+        } else {
+            toResetFlag = false;
+        }
+
+        print(HEADER);
+
+        DhcpRelayCountersStore counterStore = AbstractShellCommand.get(DhcpRelayCountersStore.class);
+
+        Optional<DhcpRelayCounters> perClassCounters = counterStore.getCounters(GCOUNT);
+
+        if (perClassCounters.isPresent()) {
+            Map<String, Integer> counters = perClassCounters.get().getCounters();
+            if (counters.size() > 0) {
+                counters.forEach((name, value) -> {
+                    print("%-30s  ............................  %-4d packets", name, value);
+                });
+            } else {
+                print("No counter for {}", GCOUNT);
+            }
+
+            if (toResetFlag) {
+                counterStore.resetCounters(GCOUNT);
+
+            }
+        }
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpRelayCommand.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpRelayCommand.java
new file mode 100644
index 0000000..90816b0
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpRelayCommand.java
@@ -0,0 +1,277 @@
+/*
+ * 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.cli;
+
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onlab.util.Tools;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.dhcprelay.store.DhcpRelayCounters;
+import org.onosproject.dhcprelay.api.DhcpServerInfo;
+import org.onosproject.dhcprelay.api.DhcpRelayService;
+import org.onosproject.dhcprelay.store.DhcpRecord;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.host.HostService;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.Map;
+
+
+/**
+ * Prints DHCP server and DHCP relay status.
+ */
+@Command(scope = "onos", name = "dhcp-relay", description = "DHCP relay app cli.")
+public class DhcpRelayCommand extends AbstractShellCommand {
+    @Argument(index = 0, name = "counter",
+            description = "shows counter values",
+            required = false, multiValued = false)
+    String counter = null;
+
+    @Argument(index = 1, name = "reset",
+            description = "reset counters or not",
+            required = false, multiValued = false)
+    String reset = null;
+
+
+
+    private static final String CONUTER_HEADER = "DHCP Relay Counters :";
+    private static final String COUNTER_HOST = "Counters for id=%s/%s, locations=%s%s";
+
+
+    private static final String HEADER = "DHCP relay records ([D]: Directly connected):";
+    private static final String NO_RECORDS = "No DHCP relay record found";
+    private static final String HOST = "id=%s/%s, locations=%s%s, last-seen=%s, IPv4=%s, IPv6=%s";
+    private static final String DHCP_SERVER_GW = "DHCP Server: %s, %s via %s (Mac: %s)";
+    private static final String DHCP_SERVER = "DHCP Server: %s, %s (Mac: %s)";
+    private static final String MISSING_SERVER_CFG = "DHCP Server info not available";
+    private static final String DIRECTLY = "[D]";
+    private static final String EMPTY = "";
+    private static final String NA = "N/A";
+    private static final String STATUS_FMT = "[%s, %s]";
+    private static final String STATUS_FMT_NH = "[%s via %s, %s]";
+    private static final String STATUS_FMT_V6 = "[%s %d, %d ms %s %d %d ms, %s]";
+    private static final String STATUS_FMT_V6_NH = "[%s %d %d ms, %s %d %d ms via %s, %s]";
+    private static final String DEFAULT_SERVERS = "Default DHCP servers:";
+    private static final String INDIRECT_SERVERS = "Indirect DHCP servers:";
+
+    private static final DhcpRelayService DHCP_RELAY_SERVICE = get(DhcpRelayService.class);
+    private static final HostService HOST_SERVICE = get(HostService.class);
+
+
+    @Override
+    protected void execute() {
+        List<DhcpServerInfo> defaultDhcpServerInfoList = DHCP_RELAY_SERVICE.getDefaultDhcpServerInfoList();
+        List<DhcpServerInfo> indirectDhcpServerInfoList = DHCP_RELAY_SERVICE.getIndirectDhcpServerInfoList();
+
+        if (defaultDhcpServerInfoList.isEmpty() && indirectDhcpServerInfoList.isEmpty()) {
+            print(MISSING_SERVER_CFG);
+            return;
+        }
+
+        if (!defaultDhcpServerInfoList.isEmpty()) {
+            print(DEFAULT_SERVERS);
+            listServers(defaultDhcpServerInfoList);
+        }
+        if (!indirectDhcpServerInfoList.isEmpty()) {
+            print(INDIRECT_SERVERS);
+            listServers(indirectDhcpServerInfoList);
+        }
+
+        // DHCP records
+        Collection<DhcpRecord> records = DHCP_RELAY_SERVICE.getDhcpRecords();
+        if (records.isEmpty()) {
+            print(NO_RECORDS);
+            return;
+        }
+
+        // Handle display of counters
+        boolean toResetFlag;
+
+        if (counter != null) {
+            if (counter.equals("counter") || counter.equals("[counter]")) {
+                print(CONUTER_HEADER);
+            } else {
+                print("first parameter is [counter]");
+                return;
+            }
+            if (reset != null) {
+                if (reset.equals("reset") || reset.equals("[reset]")) {
+                    toResetFlag = true;
+                } else {
+                    print("Last parameter is [reset]");
+                    return;
+                }
+            } else {
+                toResetFlag = false;
+            }
+
+            records.forEach(record -> {
+                print(COUNTER_HOST, record.macAddress(),
+                        record.vlanId(),
+                        record.locations(),
+                        record.directlyConnected() ? DIRECTLY : EMPTY);
+                DhcpRelayCounters v6Counters = record.getV6Counters();
+                Map<String, Integer> countersMap = v6Counters.getCounters();
+                countersMap.forEach((name, value) -> {
+                    print("%-30s  ............................  %-4d packets", name, value);
+                });
+                if (toResetFlag) {
+                    v6Counters.resetCounters();
+                    record.updateLastSeen();
+                    DHCP_RELAY_SERVICE.updateDhcpRecord(HostId.hostId(record.macAddress(), record.vlanId()), record);
+                }
+            });
+
+
+            return;
+        }
+
+
+        // Handle display of records
+
+        print(HEADER);
+        records.forEach(record -> print(HOST,
+                                        record.macAddress(),
+                                        record.vlanId(),
+                                        record.locations(),
+                                        record.directlyConnected() ? DIRECTLY : EMPTY,
+                                        Tools.timeAgo(record.lastSeen()),
+                                        ip4State(record),
+                                        ip6State(record)));
+    }
+
+    private void listServers(List<DhcpServerInfo> dhcpServerInfoList) {
+        dhcpServerInfoList.forEach(dhcpServerInfo -> {
+            String connectPoint = dhcpServerInfo.getDhcpServerConnectPoint()
+                    .map(Object::toString).orElse(NA);
+            String serverMac = dhcpServerInfo.getDhcpConnectMac()
+                    .map(Object::toString).orElse(NA);
+            String gatewayAddress;
+            String serverIp;
+
+            switch (dhcpServerInfo.getVersion()) {
+                case DHCP_V4:
+                    gatewayAddress = dhcpServerInfo.getDhcpGatewayIp4()
+                            .map(Object::toString).orElse(null);
+                    serverIp = dhcpServerInfo.getDhcpServerIp4()
+                            .map(Object::toString).orElse(NA);
+                    break;
+                case DHCP_V6:
+                    gatewayAddress = dhcpServerInfo.getDhcpGatewayIp6()
+                            .map(Object::toString).orElse(null);
+                    serverIp = dhcpServerInfo.getDhcpServerIp6()
+                            .map(Object::toString).orElse(NA);
+                    break;
+                default:
+                    return;
+            }
+            if (gatewayAddress != null) {
+                print(DHCP_SERVER_GW, connectPoint, serverIp, gatewayAddress, serverMac);
+            } else {
+                print(DHCP_SERVER, connectPoint, serverIp, serverMac);
+            }
+        });
+    }
+
+    private String ip4State(DhcpRecord record) {
+        String nextHopIp = findNextHopIp(IpAddress::isIp4,
+                                         record.nextHop().orElse(null),
+                                         record.vlanId());
+        return ipState(record.ip4Address().map(Object::toString).orElse(NA),
+                       record.ip4Status().map(Object::toString).orElse(NA),
+                       record.directlyConnected(),
+                       nextHopIp);
+    }
+
+    private String ip6State(DhcpRecord record) {
+        String nextHopIp = findNextHopIp6(IpAddress::isIp6,
+                                         record.nextHop().orElse(null),
+                                         record.vlanId());
+
+        if (record.directlyConnected()) {
+            return String.format(STATUS_FMT_V6,
+                    record.ip6Address().map(Object::toString).orElse(NA),
+                    record.addrPrefTime(),
+                    record.getLastIp6Update(),
+                    record.pdPrefix().map(Object::toString).orElse(NA),
+                    record.pdPrefTime(),
+                    record.getLastPdUpdate(),
+                    record.ip6Status().map(Object::toString).orElse(NA));
+        } else {
+            return String.format(STATUS_FMT_V6_NH,
+                    record.ip6Address().map(Object::toString).orElse(NA),
+                    record.addrPrefTime(),
+                    record.getLastIp6Update(),
+                    record.pdPrefix().map(Object::toString).orElse(NA),
+                    record.pdPrefTime(),
+                    record.getLastPdUpdate(),
+                    nextHopIp,
+                    record.ip6Status().map(Object::toString).orElse(NA));
+        }
+    }
+
+    private String ipState(String ipAddress, String status,
+                           boolean directlyConnected,
+                           String nextHopIp) {
+        if (directlyConnected) {
+            return String.format(STATUS_FMT, ipAddress, status);
+        } else {
+            return String.format(STATUS_FMT_NH, ipAddress, nextHopIp, status);
+        }
+    }
+
+    private String findNextHopIp(Predicate<IpAddress> ipFilter, MacAddress nextHopMac, VlanId vlanId) {
+        if (ipFilter == null || nextHopMac == null || vlanId == null) {
+            return NA;
+        }
+
+        Host host = HOST_SERVICE.getHost(HostId.hostId(nextHopMac, vlanId));
+        if (host == null) {
+            return NA;
+        }
+        return host.ipAddresses().stream()
+                .filter(ipFilter)
+                .filter(ip -> !ip.isLinkLocal())
+                .map(Object::toString)
+                .findFirst()
+                .orElse(NA);
+    }
+
+    private String findNextHopIp6(Predicate<IpAddress> ipFilter, MacAddress nextHopMac, VlanId vlanId) {
+        if (ipFilter == null || nextHopMac == null || vlanId == null) {
+            return NA;
+        }
+
+        Host host = HOST_SERVICE.getHost(HostId.hostId(nextHopMac, vlanId));
+        if (host == null) {
+            return NA;
+        }
+        return host.ipAddresses().stream()
+                .filter(ipFilter)
+                .filter(ip -> ip.isLinkLocal())
+                .map(Object::toString)
+                .findFirst()
+                .orElse(NA);
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpRelayCounterCompleter.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpRelayCounterCompleter.java
new file mode 100644
index 0000000..8b024a5
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpRelayCounterCompleter.java
@@ -0,0 +1,39 @@
+/*
+ * 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.cli;
+
+import org.apache.karaf.shell.console.Completer;
+import org.apache.karaf.shell.console.completer.StringsCompleter;
+
+import java.util.List;
+import java.util.SortedSet;
+
+/**
+ * Dhcp Relay counter completer.
+ */
+public class DhcpRelayCounterCompleter implements Completer {
+
+    @Override
+    public int complete(String buffer, int cursor, List<String> candidates) {
+        // Delegate string completer
+        StringsCompleter delegate = new StringsCompleter();
+        SortedSet<String> strings = delegate.getStrings();
+        strings.add("counter");
+
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(buffer, cursor, candidates);
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpRelayResetCompleter.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpRelayResetCompleter.java
new file mode 100644
index 0000000..3eb4404
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/DhcpRelayResetCompleter.java
@@ -0,0 +1,39 @@
+/*
+ * 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.cli;
+
+import org.apache.karaf.shell.console.Completer;
+import org.apache.karaf.shell.console.completer.StringsCompleter;
+
+import java.util.List;
+import java.util.SortedSet;
+
+/**
+ * Dhcp Relay reset completer.
+ */
+public class DhcpRelayResetCompleter implements Completer {
+
+    @Override
+    public int complete(String buffer, int cursor, List<String> candidates) {
+        // Delegate string completer
+        StringsCompleter delegate = new StringsCompleter();
+        SortedSet<String> strings = delegate.getStrings();
+        strings.add("reset");
+
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(buffer, cursor, candidates);
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/package-info.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/package-info.java
new file mode 100644
index 0000000..2a983fd
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/cli/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * Command line interface for DHCP relay application.
+ */
+package org.onosproject.dhcprelay.cli;
\ No newline at end of file
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/DefaultDhcpRelayConfig.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/DefaultDhcpRelayConfig.java
new file mode 100644
index 0000000..daf97bf
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/DefaultDhcpRelayConfig.java
@@ -0,0 +1,45 @@
+/*
+ * 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.config;
+
+import com.google.common.collect.Lists;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.config.Config;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * DHCP Relay Config class for default use case (directly connected hosts).
+ */
+public class DefaultDhcpRelayConfig extends Config<ApplicationId> {
+    public static final String KEY = "default";
+
+    @Override
+    public boolean isValid() {
+        // check if all configs are valid
+        AtomicBoolean valid = new AtomicBoolean(true);
+        array.forEach(config -> valid.compareAndSet(true, DhcpServerConfig.isValid(config)));
+        return valid.get();
+    }
+
+    public List<DhcpServerConfig> dhcpServerConfigs() {
+        List<DhcpServerConfig> configs = Lists.newArrayList();
+        array.forEach(node -> configs.add(new DhcpServerConfig(node)));
+        return configs;
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/DhcpServerConfig.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/DhcpServerConfig.java
new file mode 100644
index 0000000..d60c6b0
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/DhcpServerConfig.java
@@ -0,0 +1,276 @@
+/*
+ * 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.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.google.common.base.Objects;
+import com.google.common.collect.Maps;
+import org.apache.commons.lang3.tuple.Pair;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpAddress;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.slf4j.Logger;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * DHCP server configuration.
+ */
+public class DhcpServerConfig {
+    private final Logger log = getLogger(getClass());
+
+    private static final String DHCP_CONNECT_POINT = "dhcpServerConnectPoint";
+    private static final String DHCP_SERVER_IP = "serverIps";
+    private static final String DHCP_GATEWAY_IP = "gatewayIps";
+    private static final String RELAY_AGENT_IP = "relayAgentIps";
+    private static final String IPV4 = "ipv4";
+    private static final String IPV6 = "ipv6";
+
+    protected ConnectPoint connectPoint;
+    protected Ip4Address serverIp4Addr;
+    protected Ip4Address gatewayIp4Addr;
+    protected Ip6Address serverIp6Addr;
+    protected Ip6Address gatewayIp6Addr;
+    protected Map<DeviceId, Pair<Ip4Address, Ip6Address>> relayAgentIps = Maps.newHashMap();
+
+    protected DhcpServerConfig() {
+        // empty config not allowed here
+    }
+
+    public DhcpServerConfig(JsonNode config) {
+        if (!config.has(DHCP_CONNECT_POINT)) {
+            // connect point doesn't exist
+            throw new IllegalArgumentException("Missing " + DHCP_CONNECT_POINT);
+        }
+        connectPoint = ConnectPoint.deviceConnectPoint(config.path(DHCP_CONNECT_POINT).asText());
+
+        if (!config.has(DHCP_SERVER_IP)) {
+            // server ip doesn't exist
+            throw new IllegalArgumentException("Missing " + DHCP_SERVER_IP);
+        }
+        ArrayNode serverIps = (ArrayNode) config.path(DHCP_SERVER_IP);
+        serverIps.forEach(node -> {
+            if (node.isTextual()) {
+                IpAddress ip = IpAddress.valueOf(node.asText());
+                if (ip.isIp4() && serverIp4Addr == null) {
+                  try {
+                      serverIp4Addr = ip.getIp4Address();
+                  } catch (IllegalArgumentException iae) {
+                      log.warn("Invalid IPv4 address {} found in DHCP server config. Ignored.", ip.toString());
+                  }
+                }
+                if (ip.isIp6() && serverIp6Addr == null) {
+                  try {
+                    serverIp6Addr = ip.getIp6Address();
+                  } catch (IllegalArgumentException iae) {
+                      log.warn("Invalid IPv6 address {} found in DHCP server config. Ignored.", ip.toString());
+                  }
+                }
+            }
+        });
+
+        if (config.has(DHCP_GATEWAY_IP)) {
+            ArrayNode gatewayIps = (ArrayNode) config.path(DHCP_GATEWAY_IP);
+            gatewayIps.forEach(node -> {
+                if (node.isTextual()) {
+                    IpAddress ip = IpAddress.valueOf(node.asText());
+                    if (ip.isIp4() && gatewayIp4Addr == null) {
+                      try {
+                          gatewayIp4Addr = ip.getIp4Address();
+                      } catch (IllegalArgumentException iae) {
+                          log.warn("Invalid IPv4 address {} found in DHCP gateway config. Ignored.", ip.toString());
+                      }
+                    }
+                    if (ip.isIp6() && gatewayIp6Addr == null) {
+                      try {
+                          gatewayIp6Addr = ip.getIp6Address();
+                      } catch (IllegalArgumentException iae) {
+                          log.warn("Invalid IPv6 address {} found in DHCP gateway config. Ignored.", ip.toString());
+                      }
+                    }
+                }
+            });
+        }
+        if (config.has(RELAY_AGENT_IP)) {
+            JsonNode relayAgentIpsNode = config.path(RELAY_AGENT_IP);
+            relayAgentIpsNode.fields().forEachRemaining(e -> {
+                DeviceId deviceId = DeviceId.deviceId(e.getKey());
+                JsonNode ips = e.getValue();
+                Ip4Address ipv4 = null;
+                Ip6Address ipv6 = null;
+                if (ips.has(IPV4)) {
+                    String ipv4Str = ips.get(IPV4).asText();
+                    try {
+                        ipv4 = Ip4Address.valueOf(ipv4Str);
+                    } catch (IllegalArgumentException iae) {
+                        log.warn("Invalid IPv4 address {} found in DHCP relay config. Ignored.", ipv4Str);
+                    }
+                }
+                if (ips.has(IPV6)) {
+                    String ipv6Str = ips.get(IPV6).asText();
+                    try {
+                        ipv6 = Ip6Address.valueOf(ipv6Str);
+                    } catch (IllegalArgumentException iae) {
+                        log.warn("Invalid IPv6 address {} found in DHCP relay config. Ignored.", ipv6Str);
+                    }
+                }
+                relayAgentIps.put(deviceId, Pair.of(ipv4, ipv6));
+            });
+        }
+
+        checkNotNull(connectPoint, "Connect point of DHCP server can't be null");
+        checkState(serverIp4Addr != null || serverIp6Addr != null,
+                   "Should exist at least one server IP for DHCPv4 or DHCPv6");
+
+    }
+
+    /**
+     * Verify a json config is a valid DHCP server config.
+     *
+     * @param jsonConfig the json config
+     * @return true if valid; false otherwise
+     */
+    public static boolean isValid(JsonNode jsonConfig) {
+        return jsonConfig.has(DHCP_CONNECT_POINT) && jsonConfig.has(DHCP_SERVER_IP);
+    }
+
+    /**
+     * Returns the dhcp server connect point.
+     *
+     * @return dhcp server connect point
+     */
+    public Optional<ConnectPoint> getDhcpServerConnectPoint() {
+        return Optional.ofNullable(connectPoint);
+    }
+
+    /**
+     * Returns the IPv4 address of DHCP server.
+     *
+     * @return IPv4 address of server; empty value if not set
+     */
+    public Optional<Ip4Address> getDhcpServerIp4() {
+        return Optional.ofNullable(serverIp4Addr);
+    }
+
+    /**
+     * Returns the optional IPv4 address of dhcp gateway, if configured.
+     * This option is typically used if the dhcp server is not directly attached
+     * to a switch; For example, the dhcp server may be reached via an external
+     * gateway connected to the dhcpserverConnectPoint.
+     *
+     * @return IPv4 address of gateway; empty value if not set
+     */
+    public Optional<Ip4Address> getDhcpGatewayIp4() {
+        return Optional.ofNullable(gatewayIp4Addr);
+    }
+
+    /**
+     * Returns the IPv6 address of DHCP server.
+     *
+     * @return IPv6 address of server ; empty value if not set
+     */
+    public Optional<Ip6Address> getDhcpServerIp6() {
+        return Optional.ofNullable(serverIp6Addr);
+    }
+
+    /**
+     * Returns the optional IPv6 address of dhcp gateway, if configured.
+     * This option is typically used if the dhcp server is not directly attached
+     * to a switch; For example, the dhcp server may be reached via an external
+     * gateway connected to the dhcpserverConnectPoint.
+     *
+     * @return IPv6 address of gateway; empty value if not set
+     */
+    public Optional<Ip6Address> getDhcpGatewayIp6() {
+        return Optional.ofNullable(gatewayIp6Addr);
+    }
+
+    /**
+     * Returns the optional IPv4 address for relay agent for given device,
+     * if configured.
+     * This option is used if we want to replace the giaddr field in DHCPv4
+     * payload.
+     *
+     * @param deviceId the device
+     * @return the giaddr; empty value if not set
+     */
+    public Optional<Ip4Address> getRelayAgentIp4(DeviceId deviceId) {
+        Pair<Ip4Address, Ip6Address> relayAgentIp = relayAgentIps.get(deviceId);
+        if (relayAgentIp == null) {
+            return Optional.empty();
+        }
+        return Optional.ofNullable(relayAgentIp.getLeft());
+    }
+
+    /**
+     * Returns the optional IPv6 address for relay agent for given device,
+     * if configured.
+     * This option is used if we want to replace the link-address field in DHCPv6
+     * payload.
+     *
+     * @param deviceId the device
+     * @return the link-addr; empty value if not set
+     */
+    public Optional<Ip6Address> getRelayAgentIp6(DeviceId deviceId) {
+        Pair<Ip4Address, Ip6Address> relayAgentIp = relayAgentIps.get(deviceId);
+        if (relayAgentIp == null) {
+            return Optional.empty();
+        }
+        return Optional.ofNullable(relayAgentIp.getRight());
+    }
+
+    /**
+     * Gets all relay agent ips and device mapping.
+     *
+     * @return the mapping
+     */
+    public Map<DeviceId, Pair<Ip4Address, Ip6Address>> getRelayAgentIps() {
+        return relayAgentIps;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        DhcpServerConfig that = (DhcpServerConfig) o;
+        return Objects.equal(connectPoint, that.connectPoint) &&
+                Objects.equal(serverIp4Addr, that.serverIp4Addr) &&
+                Objects.equal(gatewayIp4Addr, that.gatewayIp4Addr) &&
+                Objects.equal(serverIp6Addr, that.serverIp6Addr) &&
+                Objects.equal(gatewayIp6Addr, that.gatewayIp6Addr) &&
+                Objects.equal(relayAgentIps, that.relayAgentIps);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(connectPoint, serverIp4Addr, gatewayIp4Addr,
+                                serverIp6Addr, gatewayIp6Addr, relayAgentIps);
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/EnableDhcpFpmConfig.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/EnableDhcpFpmConfig.java
new file mode 100644
index 0000000..6eb9cf5
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/EnableDhcpFpmConfig.java
@@ -0,0 +1,46 @@
+/*
+ * 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.config;
+
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.config.Config;
+
+
+/**
+ * Dhcp Fpm Config.
+ */
+public class EnableDhcpFpmConfig extends Config<ApplicationId> {
+    public static final String KEY = "dhcpFpm";
+    private static final String DHCP_FPM_ENABLE = "enabled";
+
+    @Override
+    public boolean isValid() {
+      if (!hasFields(DHCP_FPM_ENABLE)) {
+          return false;
+      }
+      return isBoolean(DHCP_FPM_ENABLE, FieldPresence.OPTIONAL);
+    }
+
+    /**
+     * Returns whether Dhcp Fpm is enabled.
+     *
+     * @return true if enabled, otherwise false
+     */
+    public boolean getDhcpFpmEnable() {
+        return object.path(DHCP_FPM_ENABLE).asBoolean(false);
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/IgnoreDhcpConfig.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/IgnoreDhcpConfig.java
new file mode 100644
index 0000000..b4b8adb
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/IgnoreDhcpConfig.java
@@ -0,0 +1,56 @@
+/*
+ * 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.config;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import org.onlab.packet.VlanId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.config.Config;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class IgnoreDhcpConfig extends Config<ApplicationId> {
+    public static final String KEY = "ignoreDhcp";
+    private static final String DEVICE_ID = "deviceId";
+    private static final String VLAN_ID = "vlan";
+
+    @Override
+    public boolean isValid() {
+        AtomicBoolean valid = new AtomicBoolean(true);
+        if (array == null) {
+            return false;
+        }
+        array.forEach(node -> {
+            valid.compareAndSet(true, node.has(DEVICE_ID) && node.has(VLAN_ID));
+        });
+        return valid.get();
+    }
+
+    public Multimap<DeviceId, VlanId> ignoredVlans() {
+        Multimap<DeviceId, VlanId> ignored = ArrayListMultimap.create();
+
+        array.forEach(node -> {
+            DeviceId deviceId = DeviceId.deviceId(node.get(DEVICE_ID).asText());
+            VlanId vlanId = VlanId.vlanId((short) node.get(VLAN_ID).asInt());
+            ignored.put(deviceId, vlanId);
+        });
+
+        return ignored;
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/IndirectDhcpRelayConfig.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/IndirectDhcpRelayConfig.java
new file mode 100644
index 0000000..02e76b2
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/IndirectDhcpRelayConfig.java
@@ -0,0 +1,25 @@
+/*
+ * 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.config;
+
+/**
+ * DHCP Relay Config class for indirect use case (indirectly connected hosts).
+ */
+public class IndirectDhcpRelayConfig extends DefaultDhcpRelayConfig {
+    public static final String KEY = "indirect";
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/package-info.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/package-info.java
new file mode 100644
index 0000000..b9ca580
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ *
+ */
+
+/**
+ * Configuration utility for DHCP relay app.
+ */
+package org.onosproject.dhcprelay.config;
\ No newline at end of file
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/package-info.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/package-info.java
new file mode 100644
index 0000000..20d1a7a
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-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.
+ */
+
+/**
+ *  DHCP-RELAY application.
+ */
+package org.onosproject.dhcprelay;
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpFpmPrefixStore.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpFpmPrefixStore.java
new file mode 100644
index 0000000..694b048
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpFpmPrefixStore.java
@@ -0,0 +1,51 @@
+/*
+ * 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.store;
+import org.onosproject.routing.fpm.api.FpmRecord;
+import org.onosproject.routing.fpm.api.FpmPrefixStore;
+
+
+
+import org.onlab.packet.IpPrefix;
+import java.util.Optional;
+
+
+
+/**
+ * Interface to store DhcpFpm records.
+ */
+
+public interface DhcpFpmPrefixStore extends FpmPrefixStore {
+
+
+    /**
+     * Add a dhcp fpm record.
+     *
+     * @param prefix the route prefix in the advertisement
+     * @param fpmRecord the route for fpm
+     **/
+    public void addFpmRecord(IpPrefix prefix, FpmRecord fpmRecord);
+
+    /**
+     * Remove a dhcp fpm entry
+     *  and return the removed record; return empty value if not exists.
+     *
+     * @param prefix the route prefix in the advertisement
+     * @return none
+     **/
+    public Optional<FpmRecord> removeFpmRecord(IpPrefix prefix);
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpFpmRecord.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpFpmRecord.java
new file mode 100644
index 0000000..81eadb7
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpFpmRecord.java
@@ -0,0 +1,85 @@
+/*
+ * 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.store;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import java.util.Objects;
+import com.google.common.base.MoreObjects;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A class to define a Dhcp Fpm record.
+ */
+public class DhcpFpmRecord {
+
+    private IpPrefix prefix;
+    private IpAddress nextHop;
+
+    public DhcpFpmRecord(IpPrefix prefix, IpAddress nextHop) {
+        checkNotNull(prefix, "prefix cannot be null");
+        checkNotNull(nextHop, "ipAddress cannot be null");
+
+        this.prefix = prefix;
+        this.nextHop = nextHop;
+    }
+
+    /**
+     * Gets IP prefix of record.
+     *
+     * @return the IP prefix
+     */
+    public IpPrefix ipPrefix() {
+        return prefix;
+    }
+
+    /**
+     * Gets IP address of record.
+     *
+     * @return the IP address
+     */
+    public IpAddress nextHop() {
+        return nextHop;
+    }
+
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(prefix, nextHop);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof DhcpFpmRecord)) {
+            return false;
+        }
+        DhcpFpmRecord that = (DhcpFpmRecord) obj;
+        return Objects.equals(prefix, that.prefix) &&
+                Objects.equals(nextHop, that.nextHop);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("prefix", prefix)
+                .add("ipAddress", nextHop)
+                .toString();
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRecord.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRecord.java
new file mode 100644
index 0000000..f0beff9
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRecord.java
@@ -0,0 +1,483 @@
+/*
+ * 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.store;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Sets;
+import org.onlab.packet.DHCP;
+import org.onlab.packet.DHCP6;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A class to record DHCP from DHCP relay application.
+ */
+public class DhcpRecord {
+    private final Set<HostLocation> locations;
+    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;
+
+    private Ip6Address ip6Address;
+    private IpPrefix pdPrefix;
+    private DHCP6.MsgType ip6Status;
+
+    private long lastSeen;
+    private long lastIp6Update;
+    private long lastPdUpdate;
+
+    private boolean directlyConnected;
+    private long addrPrefTime;
+    private long pdPrefTime;
+    private DhcpRelayCounters v6Counters;
+
+
+    /**
+     * Creates a DHCP record for a host (mac + vlan).
+     *
+     * @param hostId the host id for the host
+     */
+    public DhcpRecord(HostId hostId) {
+        checkNotNull(hostId, "Host id can't be null");
+
+        this.locations = Sets.newHashSet();
+        this.macAddress = hostId.mac();
+        this.vlanId = hostId.vlanId();
+        this.lastSeen = System.currentTimeMillis();
+        this.directlyConnected = false;
+        this.v6Counters = new DhcpRelayCounters();
+    }
+
+    /**
+     * Gets host locations.
+     *
+     * @return the locations of host
+     */
+    public Set<HostLocation> locations() {
+        return locations;
+    }
+
+    /**
+     * Adds a location to record.
+     *
+     * @param location the location
+     * @return the DHCP record
+     */
+    public DhcpRecord addLocation(HostLocation location) {
+        if (locations.contains(location)) {
+            locations.remove(location);
+        }
+        locations.add(location);
+        return this;
+    }
+
+    /**
+     * Removes a location from record.
+     *
+     * @param location the location
+     * @return the DHCP record
+     */
+    public DhcpRecord removeLocation(HostLocation location) {
+        locations.remove(location);
+        return this;
+    }
+
+    /**
+     * Gets host mac address of this record.
+     *
+     * @return the host mac address
+     */
+    public MacAddress macAddress() {
+        return macAddress;
+    }
+
+    /**
+     * Gets host vlan id of this record.
+     *
+     * @return the host id.
+     */
+    public VlanId vlanId() {
+        return vlanId;
+    }
+
+    /**
+     * Gets IPv4 address which assigned to the host.
+     *
+     * @return the IP address assigned to the host
+     */
+    public Optional<Ip4Address> ip4Address() {
+        return Optional.ofNullable(ip4Address);
+    }
+
+    /**
+     * Sets IPv4 address.
+     *
+     * @param ip4Address the IPv4 address
+     * @return the DHCP record
+     */
+    public DhcpRecord ip4Address(Ip4Address ip4Address) {
+        this.ip4Address = ip4Address;
+        return this;
+    }
+
+    /**
+     * Gets IPv6 address which assigned to the host.
+     *
+     * @return the IP address assigned to the host
+     */
+    public Optional<Ip6Address> ip6Address() {
+        return Optional.ofNullable(ip6Address);
+    }
+
+    /**
+     * Sets IPv6 address.
+     *
+     * @param ip6Address the IPv6 address
+     * @return the DHCP record
+     */
+    public DhcpRecord ip6Address(Ip6Address ip6Address) {
+        this.ip6Address = ip6Address;
+        return this;
+    }
+
+    /**
+     * Gets IPv6 PD address which assigned to the host.
+     *
+     * @return the PD IP address assigned to the host
+     */
+    public Optional<IpPrefix> pdPrefix() {
+        return Optional.ofNullable(pdPrefix);
+    }
+
+    /**
+     * Sets IPv6 PD address.
+     *
+     * @param pdPrefix the IPv6 PD address
+     * @return the DHCP record
+     */
+    public DhcpRecord pdPrefix(IpPrefix pdPrefix) {
+        this.pdPrefix = pdPrefix;
+        return this;
+    }
+
+    /**
+     * Gets the latest time this record updated.
+     *
+     * @return the last time host send or receive DHCP packet
+     */
+    public long lastSeen() {
+        return lastSeen;
+    }
+
+    /**
+     * Updates the update time of this record.
+     *
+     * @return the DHCP record
+     */
+    public DhcpRecord updateLastSeen() {
+        lastSeen = System.currentTimeMillis();
+        return this;
+    }
+
+    /**
+     * Gets the latest time this record updated with ip6 Address.
+     *
+     * @return the last time received DHCP packet provide ip6 Address
+     */
+    public long getLastIp6Update() {
+        return lastIp6Update;
+    }
+
+    /**
+     * Updates the update time of this record is given ip6 Address.
+     *
+     * @return the DHCP record
+     */
+    public DhcpRecord updateLastIp6Update() {
+        lastIp6Update = System.currentTimeMillis();
+        return this;
+    }
+
+    /**
+     * Gets the latest time this record updated with pd Prefix.
+     *
+     * @return the last time received DHCP packet provide pd Prefix
+     */
+    public long getLastPdUpdate() {
+        return lastPdUpdate;
+    }
+
+    /**
+     * Updates the update time of this record is given pd Prefix.
+     *
+     * @return the DHCP record
+     */
+    public DhcpRecord updateLastPdUpdate() {
+        lastPdUpdate = System.currentTimeMillis();
+        return this;
+    }
+
+    /**
+     * Gets the IP Address preferred time for this record.
+     *
+     * @return the preferred lease time for this ip address
+     */
+    public long addrPrefTime() {
+        return addrPrefTime;
+    }
+
+    /**
+     * Updates the IP Address preferred time of this record.
+     *
+     * @param prefTime preferred liftme
+     * @return the DHCP record
+     */
+    public DhcpRecord updateAddrPrefTime(long prefTime) {
+        addrPrefTime = prefTime;
+        return this;
+    }
+
+    /**
+     * Gets the PD Prefix preferred time for this record.
+     *
+     * @return the preferred lease time for this PD prefix
+     */
+    public long pdPrefTime() {
+        return pdPrefTime;
+    }
+
+    /**
+     * Updates the PD Prefix preferred time of this record.
+     *
+     * @param prefTime preferred liftme
+     * @return the DHCP record
+     */
+    public DhcpRecord updatePdPrefTime(long prefTime) {
+        pdPrefTime = prefTime;
+        return this;
+    }
+
+    /**
+     * Indicated that the host is direct connected to the network or not.
+     *
+     * @return true if the host is directly connected to the network; false otherwise
+     */
+    public boolean directlyConnected() {
+        return directlyConnected;
+    }
+
+    /**
+     * Sets the flag which indicated that the host is directly connected to the
+     * network.
+     *
+     * @param directlyConnected the flag to set
+     * @return the DHCP record
+     */
+    public DhcpRecord setDirectlyConnected(boolean directlyConnected) {
+        this.directlyConnected = directlyConnected;
+        return this;
+    }
+
+    /**
+     * Gets the DHCPv4 status of this record.
+     *
+     * @return the DHCPv4 status; empty if not exists
+     */
+    public Optional<DHCP.MsgType> ip4Status() {
+        return Optional.ofNullable(ip4Status);
+    }
+
+    /**
+     * Sets status of DHCPv4.
+     *
+     * @param ip4Status the status
+     * @return the DHCP record
+     */
+    public DhcpRecord ip4Status(DHCP.MsgType ip4Status) {
+        this.ip4Status = ip4Status;
+        return this;
+    }
+
+    /**
+     * Gets the DHCPv6 status of this record.
+     *
+     * @return the DHCPv6 status; empty if not exists
+     */
+    public Optional<DHCP6.MsgType> ip6Status() {
+        return Optional.ofNullable(ip6Status);
+    }
+
+    /**
+     * Sets status of DHCPv6.
+     *
+     * @param ip6Status the DHCPv6 status
+     * @return the DHCP record
+     */
+    public DhcpRecord ip6Status(DHCP6.MsgType ip6Status) {
+        this.ip6Status = ip6Status;
+        return this;
+    }
+
+    /**
+     * Gets nextHop mac address.
+     *
+     * @return the IPv4 nextHop mac address; empty if not exists
+     */
+    public Optional<MacAddress> nextHop() {
+        return Optional.ofNullable(nextHop);
+    }
+
+    /**
+     * Sets nextHop mac address.
+     *
+     * @param nextHop the IPv4 nextHop mac address
+     * @return the DHCP record
+     */
+    public DhcpRecord nextHop(MacAddress nextHop) {
+        this.nextHop = nextHop;
+        return this;
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * Gets dhcp relay counters.
+     *
+     * @return the counter object
+     */
+    public DhcpRelayCounters getV6Counters() {
+        return v6Counters;
+    }
+
+    /**
+     * Clone this DHCP record.
+     *
+     * @return the DHCP record which cloned
+     */
+    public DhcpRecord clone() {
+        DhcpRecord newRecord = new DhcpRecord(HostId.hostId(macAddress, vlanId));
+        locations.forEach(newRecord::addLocation);
+        newRecord.directlyConnected = directlyConnected;
+        newRecord.nextHop = nextHop;
+        newRecord.nextHopTemp = nextHopTemp;
+        newRecord.ip4Address = ip4Address;
+        newRecord.ip4Status = ip4Status;
+        newRecord.ip6Address = ip6Address;
+        newRecord.pdPrefix = pdPrefix;
+        newRecord.ip6Status = ip6Status;
+        newRecord.lastSeen = lastSeen;
+        newRecord.lastIp6Update = lastIp6Update;
+        newRecord.lastPdUpdate = lastPdUpdate;
+        newRecord.addrPrefTime = addrPrefTime;
+        newRecord.pdPrefTime = pdPrefTime;
+        newRecord.v6Counters = v6Counters;
+        return newRecord;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(locations, macAddress, vlanId, ip4Address, ip4Status,
+                nextHop, nextHopTemp, ip6Address, pdPrefix, ip6Status, lastSeen,
+                lastIp6Update, lastPdUpdate, addrPrefTime, pdPrefTime, v6Counters);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof DhcpRecord)) {
+            return false;
+        }
+        DhcpRecord that = (DhcpRecord) obj;
+        return Objects.equals(locations, that.locations) &&
+                Objects.equals(macAddress, that.macAddress) &&
+                Objects.equals(vlanId, that.vlanId) &&
+                Objects.equals(ip4Address, that.ip4Address) &&
+                Objects.equals(ip4Status, that.ip4Status) &&
+                Objects.equals(nextHop, that.nextHop) &&
+                Objects.equals(nextHopTemp, that.nextHopTemp) &&
+                Objects.equals(ip6Address, that.ip6Address) &&
+                Objects.equals(pdPrefix, that.pdPrefix) &&
+                Objects.equals(ip6Status, that.ip6Status) &&
+                Objects.equals(lastSeen, that.lastSeen) &&
+                Objects.equals(lastIp6Update, that.lastIp6Update) &&
+                Objects.equals(lastPdUpdate, that.lastPdUpdate) &&
+                Objects.equals(directlyConnected, that.directlyConnected) &&
+                Objects.equals(addrPrefTime, that.addrPrefTime) &&
+                Objects.equals(pdPrefTime, that.pdPrefTime) &&
+                Objects.equals(v6Counters, that.v6Counters);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("locations", locations)
+                .add("macAddress", macAddress)
+                .add("vlanId", vlanId)
+                .add("ip4Address", ip4Address)
+                .add("ip4State", ip4Status)
+                .add("nextHop", nextHop)
+                .add("nextHopTemp", nextHopTemp)
+                .add("ip6Address", ip6Address)
+                .add("pdPrefix", pdPrefix)
+                .add("ip6State", ip6Status)
+                .add("lastSeen", lastSeen)
+                .add("lastIp6Update", lastIp6Update)
+                .add("lastPdUpdate", lastPdUpdate)
+                .add("directlyConnected", directlyConnected)
+                .add("addPrefTime", addrPrefTime)
+                .add("pdPrefTime", pdPrefTime)
+                .add("v6Counters", v6Counters)
+                .toString();
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRelayCounters.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRelayCounters.java
new file mode 100644
index 0000000..2a69c21
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRelayCounters.java
@@ -0,0 +1,110 @@
+/*
+ * 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.store;
+
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+public class DhcpRelayCounters {
+    // common counters
+
+    // IpV6 specific counters
+    public static final String SOLICIT = "SOLICIT";
+    public static final String ADVERTISE = "ADVERTISE";
+    public static final String REQUEST = "REQUEST";
+    public static final String CONFIRM = "CONFIRM";
+    public static final String RENEW = "RENEW";
+    public static final String REBIND = "REBIND";
+    public static final String REPLY = "REPLY";
+    public static final String RELEASE = "RELEASE";
+    public static final String DECLINE = "DECLINE";
+    public static final String RECONFIGURE = "RECONFIGURE";
+    public static final String INFORMATION_REQUEST = "INFORMATION_REQUEST";
+    public static final String RELAY_FORW = "RELAY_FORW";
+    public static final String RELAY_REPL = "RELAY_REPL";
+
+    public static final String NO_LINKLOCAL_GW = "No link-local in Gateway";
+    public static final String NO_LINKLOCAL_FAIL = "No link-local in CLIENT_ID";
+    public static final String NO_CLIENTID_FAIL = "No CLIENT_ID Found";
+    public static final String SVR_CFG_FAIL = "Server Config Error";
+    public static final String OPTION_MISSING_FAIL = "Expected Option missing";
+    public static final String NO_MATCHING_INTF = "No matching Inteface";
+    public static final String NO_CLIENT_INTF_MAC = "No client interface mac";
+    public static final String NO_SERVER_INFO = "No Server info found";
+    public static final String NO_SERVER_IP6ADDR = "No Server ip6 addr found";
+
+    public static final String INVALID_PACKET = "Invalid Packet";
+
+    public static final Set<String> SUPPORTED_COUNTERS =
+            ImmutableSet.of(SOLICIT, ADVERTISE, REQUEST, CONFIRM, RENEW,
+                    REBIND, REPLY, RELEASE, DECLINE, RECONFIGURE,
+                    INFORMATION_REQUEST, RELAY_FORW, RELAY_REPL,
+                    NO_LINKLOCAL_GW, NO_LINKLOCAL_FAIL, NO_CLIENTID_FAIL, SVR_CFG_FAIL, OPTION_MISSING_FAIL,
+                    NO_MATCHING_INTF, NO_CLIENT_INTF_MAC, NO_SERVER_INFO, NO_SERVER_IP6ADDR,
+                    INVALID_PACKET);
+
+    // TODO Use AtomicInteger for the counters
+    private Map<String, Integer> countersMap = new ConcurrentHashMap<>();
+    public long lastUpdate;
+
+    public void resetCounters() {
+        countersMap.forEach((name, value) -> {
+            countersMap.put(name, 0);
+        });
+    }
+    public boolean incrementCounter(String name) {
+        boolean counterValid = false;
+        if (SUPPORTED_COUNTERS.contains(name)) {
+            Integer counter = countersMap.get(name);
+            if (counter != null) {
+                counter = counter + 1;
+                countersMap.put(name, counter);
+            } else {
+                // this is the first time
+                countersMap.put(name, 1);
+            }
+            lastUpdate = System.currentTimeMillis();
+            counterValid = true;
+        }
+        return counterValid;
+    }
+    public Map<String, Integer> getCounters() {
+        return countersMap;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(countersMap, lastUpdate);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof DhcpRelayCounters)) {
+            return false;
+        }
+        DhcpRelayCounters that = (DhcpRelayCounters) obj;
+        return Objects.equals(countersMap, that.countersMap) &&
+                Objects.equals(lastUpdate, that.lastUpdate);
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRelayCountersStore.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRelayCountersStore.java
new file mode 100644
index 0000000..3534871
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRelayCountersStore.java
@@ -0,0 +1,65 @@
+/*
+ * 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.store;
+
+
+import java.util.Optional;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Stores DHCP Relay Counters records.
+ */
+public interface DhcpRelayCountersStore {
+
+    /**
+     * Creates or updates DHCP record for specific host id (mac + vlan).
+     *
+     * @param counterClass class of counters (direct, indirect, global)
+     * @param counterName name of counter
+     */
+    void incrementCounter(String counterClass, String counterName);
+
+    /**
+     * Gets the DHCP counter record for a given counter class.
+     *
+     * @param counterClass the class of counters (direct, indirect, global)
+     * @return the DHCP counter record for a given counter class; empty if record not exists
+     */
+    Optional<DhcpRelayCounters> getCounters(String counterClass);
+
+    /**
+     * Gets all classes of DHCP counters record from store.
+     *
+     * @return all classes of DHCP counters records from store
+     */
+    Set<Map.Entry<String, DhcpRelayCounters>> getAllCounters();
+
+    /**
+     * Resets counter value for a given counter class.
+     *
+     * @param counterClass the class of counters (direct, indirect, global)
+     */
+    void resetCounters(String counterClass);
+
+    /**
+     * Resets counter value for a all counter classes.
+     *
+     */
+    void resetAllCounters();
+
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRelayStore.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRelayStore.java
new file mode 100644
index 0000000..e26d99e
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRelayStore.java
@@ -0,0 +1,61 @@
+/*
+ * 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.store;
+
+import org.onosproject.net.HostId;
+import org.onosproject.store.Store;
+import org.onosproject.store.StoreDelegate;
+
+import java.util.Collection;
+import java.util.Optional;
+
+/**
+ * Stores DHCP records which relay-ed by DHCP relay application.
+ */
+public interface DhcpRelayStore extends Store<DhcpRelayStoreEvent, StoreDelegate<DhcpRelayStoreEvent>> {
+
+    /**
+     * Creates or updates DHCP record for specific host id (mac + vlan).
+     *
+     * @param hostId the id of host
+     * @param dhcpRecord the DHCP record to update
+     */
+    void updateDhcpRecord(HostId hostId, DhcpRecord dhcpRecord);
+
+    /**
+     * Gets DHCP record for specific host id (mac + vlan).
+     *
+     * @param hostId the id of host
+     * @return the DHCP record of the host; empty if record not exists
+     */
+    Optional<DhcpRecord> getDhcpRecord(HostId hostId);
+
+    /**
+     * Gets all DHCP records from store.
+     *
+     * @return all DHCP records from store
+     */
+    Collection<DhcpRecord> getDhcpRecords();
+
+    /**
+     * Removes record for specific host id (mac + vlan).
+     *
+     * @param hostId the id of host
+     * @return the DHCP record of the host; empty if record not exists
+     */
+    Optional<DhcpRecord> removeDhcpRecord(HostId hostId);
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRelayStoreEvent.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRelayStoreEvent.java
new file mode 100644
index 0000000..03f8b6a
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DhcpRelayStoreEvent.java
@@ -0,0 +1,51 @@
+/*
+ * 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.store;
+
+
+import org.onosproject.event.AbstractEvent;
+
+/**
+ * Event class for DHCP relay store.
+ */
+public class DhcpRelayStoreEvent extends AbstractEvent<DhcpRelayStoreEvent.Type, DhcpRecord> {
+
+    /**
+     * Types of the event.
+     */
+    public enum Type {
+        /**
+         * A DHCP record has been created or updated.
+         */
+        UPDATED,
+
+        /**
+         * A DHCP record has been removed.
+         */
+        REMOVED
+    }
+
+    /**
+     * Creates a DHCP relay store event by given information.
+     *
+     * @param type the type of event
+     * @param subject the DHCP record of this event
+     */
+    public DhcpRelayStoreEvent(Type type, DhcpRecord subject) {
+        super(type, subject);
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DistributedDhcpRelayCountersStore.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DistributedDhcpRelayCountersStore.java
new file mode 100644
index 0000000..4b7b26c
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DistributedDhcpRelayCountersStore.java
@@ -0,0 +1,130 @@
+/*
+ * 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.store;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+
+import org.onlab.util.KryoNamespace;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.onosproject.store.serializers.KryoNamespaces;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import org.onosproject.store.service.Versioned;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+@Component(immediate = true)
+@Service
+public class DistributedDhcpRelayCountersStore implements DhcpRelayCountersStore {
+    private static final KryoNamespace.Builder APP_KYRO = KryoNamespace.newBuilder()
+            .register(KryoNamespaces.API)
+            .register(DhcpRelayCounters.class);
+
+    private Logger log = LoggerFactory.getLogger(getClass());
+    private ConsistentMap<String, DhcpRelayCounters> counters;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected StorageService storageService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+
+    @Activate
+    protected void activated() {
+        ApplicationId appId = coreService.getAppId("org.onosproject.Dhcp6HandlerImpl");
+        counters = storageService.<String, DhcpRelayCounters>consistentMapBuilder()
+                .withSerializer(Serializer.using(APP_KYRO.build()))
+                .withName("Dhcp-Relay-Counters")
+                .withApplicationId(appId)
+                .withPurgeOnUninstall()
+                .build();
+    }
+
+    @Deactivate
+    protected void deactivated() {
+        counters.destroy().join();
+    }
+    @Override
+    public void incrementCounter(String coutnerClass, String counterName) {
+        DhcpRelayCounters countersRecord;
+
+        Versioned<DhcpRelayCounters> vCounters = counters.get(coutnerClass);
+        if (vCounters == null) {
+            countersRecord = new DhcpRelayCounters();
+        } else {
+            countersRecord = vCounters.value();
+        }
+        countersRecord.incrementCounter(counterName);
+        counters.put(coutnerClass, countersRecord);
+    }
+
+    @Override
+    public Set<Map.Entry<String, DhcpRelayCounters>> getAllCounters() {
+        final Set<Map.Entry<String, DhcpRelayCounters>> result =
+                new HashSet<Map.Entry<String, DhcpRelayCounters>>();
+        Set<Map.Entry<String, Versioned<DhcpRelayCounters>>> tmpCounters = counters.entrySet();
+        tmpCounters.forEach(entry -> {
+            String key = entry.getKey();
+            DhcpRelayCounters value = entry.getValue().value();
+            ConcurrentHashMap<String, DhcpRelayCounters> newMap = new ConcurrentHashMap();
+            newMap.put(key, value);
+
+            for (Map.Entry m: newMap.entrySet()) {
+                result.add(m);
+            }
+        });
+        return  result;
+    }
+    @Override
+    public Optional<DhcpRelayCounters> getCounters(String counterClass) {
+        DhcpRelayCounters countersRecord;
+        checkNotNull(counterClass, "counter class can't be null");
+        Versioned<DhcpRelayCounters> vCounters = counters.get(counterClass);
+        if (vCounters == null) {
+            return Optional.empty();
+        }
+        return Optional.of(vCounters.value());
+    }
+    @Override
+    public void resetAllCounters() {
+        counters.clear();
+    }
+
+    @Override
+    public void resetCounters(String counterClass) {
+        checkNotNull(counterClass, "counter class can't be null");
+        DhcpRelayCounters countersRecord = counters.get(counterClass).value();
+        countersRecord.resetCounters();
+        counters.put(counterClass, countersRecord);
+    }
+
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DistributedDhcpRelayStore.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DistributedDhcpRelayStore.java
new file mode 100644
index 0000000..884d9cc
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DistributedDhcpRelayStore.java
@@ -0,0 +1,152 @@
+/*
+ * 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.store;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+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.DHCP;
+import org.onlab.packet.DHCP6;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.net.HostId;
+import org.onosproject.store.StoreDelegate;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.onosproject.store.service.EventuallyConsistentMapEvent;
+import org.onosproject.store.service.EventuallyConsistentMapListener;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.WallClockTimestamp;
+import org.slf4j.Logger;
+
+import java.util.Collection;
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Distributed DHCP relay store.
+ */
+@Component(immediate = true)
+@Service
+public class DistributedDhcpRelayStore implements DhcpRelayStore {
+    private static final KryoNamespace APP_KRYO = KryoNamespace.newBuilder()
+            .register(KryoNamespaces.API)
+            .register(DhcpRecord.class)
+            .register(DHCP.MsgType.class)
+            .register(DHCP6.MsgType.class)
+            .register(DhcpRelayCounters.class)
+            .build();
+
+    private Logger log = getLogger(getClass());
+    private StoreDelegate<DhcpRelayStoreEvent> delegate;
+    private EventuallyConsistentMap<HostId, DhcpRecord> dhcpRecords;
+    private EventuallyConsistentMapListener<HostId, DhcpRecord> listener;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected StorageService storageService;
+
+    @Activate
+    protected void activated() {
+        dhcpRecords = storageService.<HostId, DhcpRecord>eventuallyConsistentMapBuilder()
+                .withName("DHCP-Relay-Records")
+                .withTimestampProvider((hostId, record) -> {
+                    if (record != null) {
+                        return new WallClockTimestamp(record.lastSeen());
+                    } else {
+                        return new WallClockTimestamp();
+                    }
+                })
+                .withSerializer(APP_KRYO)
+                .build();
+        listener = new InternalMapListener();
+        dhcpRecords.addListener(listener);
+    }
+
+    @Deactivate
+    protected void deactivated() {
+        dhcpRecords.removeListener(listener);
+        dhcpRecords.destroy().join();
+    }
+
+    @Override
+    public void setDelegate(StoreDelegate<DhcpRelayStoreEvent> delegate) {
+        checkNotNull(delegate, "Delegate can't be null");
+        this.delegate = delegate;
+    }
+
+    @Override
+    public void unsetDelegate(StoreDelegate<DhcpRelayStoreEvent> delegate) {
+        this.delegate = null;
+    }
+
+    @Override
+    public boolean hasDelegate() {
+        return delegate != null;
+    }
+
+    @Override
+    public void updateDhcpRecord(HostId hostId, DhcpRecord dhcpRecord) {
+        checkNotNull(hostId, "Host id can't be null");
+        checkNotNull(dhcpRecord, "DHCP record can't be null");
+        dhcpRecords.put(hostId, dhcpRecord);
+    }
+
+    @Override
+    public Optional<DhcpRecord> getDhcpRecord(HostId hostId) {
+        checkNotNull(hostId, "Host id can't be null");
+        return Optional.ofNullable(dhcpRecords.get(hostId));
+    }
+
+    @Override
+    public Collection<DhcpRecord> getDhcpRecords() {
+        return dhcpRecords.values();
+    }
+
+    @Override
+    public Optional<DhcpRecord> removeDhcpRecord(HostId hostId) {
+        checkNotNull(hostId, "Host id can't be null");
+        return Optional.ofNullable(dhcpRecords.remove(hostId));
+    }
+
+    /**
+     * Internal map listener for DHCP records map.
+     */
+    private class InternalMapListener implements EventuallyConsistentMapListener<HostId, DhcpRecord> {
+        @Override
+        public void event(EventuallyConsistentMapEvent<HostId, DhcpRecord> event) {
+            DhcpRelayStoreEvent.Type eventType;
+            switch (event.type()) {
+                case PUT:
+                    eventType = DhcpRelayStoreEvent.Type.UPDATED;
+                    break;
+                case REMOVE:
+                    eventType = DhcpRelayStoreEvent.Type.REMOVED;
+                    break;
+                default:
+                    log.warn("Unknown event type {}", event.type());
+                    return;
+            }
+            if (delegate != null) {
+                delegate.notify(new DhcpRelayStoreEvent(eventType, event.value()));
+            }
+        }
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DistributedFpmPrefixStore.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DistributedFpmPrefixStore.java
new file mode 100644
index 0000000..bc95d0b
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/DistributedFpmPrefixStore.java
@@ -0,0 +1,167 @@
+/*
+ * 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.store;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.felix.scr.annotations.Property;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.store.StoreDelegate;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.onosproject.store.service.EventuallyConsistentMapEvent;
+import org.onosproject.store.service.EventuallyConsistentMapListener;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.WallClockTimestamp;
+import org.onosproject.routing.fpm.api.FpmRecord;
+import org.onosproject.routing.fpm.api.FpmPrefixStoreEvent;
+
+
+
+
+import org.slf4j.Logger;
+
+import java.util.Collection;
+import java.util.Optional;
+
+
+
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Persistent Fpm Prefix Store with Listener.
+ */
+@Component(immediate = true)
+@Property(name = "fpm_type", value = "DHCP")
+@Service
+public class DistributedFpmPrefixStore implements DhcpFpmPrefixStore {
+
+    private static final KryoNamespace APP_KRYO = KryoNamespace.newBuilder()
+            .register(KryoNamespaces.API)
+            .register(FpmRecord.class)
+            .register(FpmRecord.Type.class)
+            .build();
+
+    private Logger log = getLogger(getClass());
+    private StoreDelegate<FpmPrefixStoreEvent> delegate;
+    private EventuallyConsistentMap<IpPrefix, FpmRecord> dhcpFpmRecords;
+    private EventuallyConsistentMapListener<IpPrefix, FpmRecord> listener;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected StorageService storageService;
+
+    @Activate
+    protected void activated() {
+        dhcpFpmRecords = storageService.<IpPrefix, FpmRecord>eventuallyConsistentMapBuilder()
+                .withName("DHCP-FPM-Records")
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .withSerializer(APP_KRYO)
+                .withPersistence()
+                .build();
+        listener = new InternalMapListener();
+        dhcpFpmRecords.addListener(listener);
+    }
+
+    @Deactivate
+    protected void deactivated() {
+        dhcpFpmRecords.removeListener(listener);
+        dhcpFpmRecords.destroy().join();
+    }
+
+    @Override
+    public void setDelegate(StoreDelegate<FpmPrefixStoreEvent> delegate) {
+        checkNotNull(delegate, "Delegate can't be null");
+        this.delegate = delegate;
+    }
+
+    @Override
+    public void unsetDelegate(StoreDelegate<FpmPrefixStoreEvent> delegate) {
+        this.delegate = null;
+    }
+
+    @Override
+    public boolean hasDelegate() {
+        return delegate != null;
+    }
+
+    @Override
+    public Optional<FpmRecord> getFpmRecord(IpPrefix prefix) {
+        checkNotNull(prefix, "Prefix can't be null");
+        return Optional.ofNullable(dhcpFpmRecords.get(prefix));
+    }
+
+    @Override
+    public Collection<FpmRecord> getFpmRecords() {
+        return dhcpFpmRecords.values();
+    }
+
+    /**
+     * Add a dhcp fpm entry.
+     *
+     * @param prefix the route prefix in the advertisement
+     * @param fpmRecord the route for fpm
+     **/
+    @Override
+    public void addFpmRecord(IpPrefix prefix, FpmRecord fpmRecord) {
+        checkNotNull(prefix, "Prefix can't be null");
+        checkNotNull(fpmRecord, "Fpm record can't be null");
+        dhcpFpmRecords.put(prefix, fpmRecord);
+    }
+
+    /**
+     * Remove a dhcp fpm entry.
+     *
+     * @param prefix the route prefix in the advertisement
+     * @return none
+     **/
+    @Override
+    public Optional<FpmRecord> removeFpmRecord(IpPrefix prefix) {
+        checkNotNull(prefix, "Prefix can't be null");
+        return Optional.ofNullable(dhcpFpmRecords.remove(prefix));
+    }
+
+    /**
+     * Internal map listener for Fpm records map.
+     */
+    private class InternalMapListener implements EventuallyConsistentMapListener<IpPrefix, FpmRecord> {
+        @Override
+        public void event(EventuallyConsistentMapEvent<IpPrefix, FpmRecord> event) {
+            FpmPrefixStoreEvent.Type eventType;
+            switch (event.type()) {
+                case PUT:
+                    eventType = FpmPrefixStoreEvent.Type.ADD;
+                    break;
+                case REMOVE:
+                    eventType = FpmPrefixStoreEvent.Type.REMOVE;
+                    break;
+                default:
+                    log.warn("Unknown event type {}", event.type());
+                    return;
+            }
+            if (delegate != null) {
+                delegate.notify(new FpmPrefixStoreEvent(eventType, event.value()));
+            }
+        }
+    }
+}
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/package-info.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/package-info.java
new file mode 100644
index 0000000..105854f
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/store/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * Store of DHCP relay application.
+ */
+package org.onosproject.dhcprelay.store;
\ No newline at end of file
diff --git a/apps/dhcprelay/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/dhcprelay/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
new file mode 100644
index 0000000..c0a4b6f
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,44 @@
+<!--
+  ~ Copyright 2014-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.
+  -->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
+
+    <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0">
+        <command>
+            <action class="org.onosproject.dhcprelay.cli.DhcpRelayCommand"/>
+            <completers>
+                <ref component-id="dhcpRelayCounterCompleter"/>
+                <ref component-id="dhcpRelayResetCompleter"/>
+            </completers>
+        </command>
+        <command>
+            <action class="org.onosproject.dhcprelay.cli.DhcpFpmRoutesCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.dhcprelay.cli.DhcpFpmAddCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.dhcprelay.cli.DhcpFpmDeleteCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.dhcprelay.cli.DhcpRelayAggCountersCommand"/>
+            <completers>
+                <ref component-id="dhcpRelayResetCompleter"/>
+            </completers>
+        </command>
+    </command-bundle>
+    <bean id="dhcpRelayCounterCompleter" class="org.onosproject.dhcprelay.cli.DhcpRelayCounterCompleter"/>
+    <bean id="dhcpRelayResetCompleter" class="org.onosproject.dhcprelay.cli.DhcpRelayResetCompleter"/>
+</blueprint>
diff --git a/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/DhcpRelayManagerTest.java b/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/DhcpRelayManagerTest.java
new file mode 100644
index 0000000..9d86606
--- /dev/null
+++ b/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/DhcpRelayManagerTest.java
@@ -0,0 +1,1573 @@
+/*
+ * 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.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
+import com.google.common.io.Resources;
+import org.apache.commons.io.Charsets;
+import org.apache.commons.lang3.tuple.Pair;
+import org.easymock.Capture;
+import org.easymock.CaptureType;
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.ARP;
+import org.onlab.packet.DHCP;
+import org.onlab.packet.DeserializationException;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpPrefix;
+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.onlab.packet.dhcp.Dhcp6InterfaceIdOption;
+import org.onlab.packet.dhcp.Dhcp6RelayOption;
+import org.onlab.packet.dhcp.Dhcp6IaNaOption;
+import org.onlab.packet.dhcp.Dhcp6IaPdOption;
+import org.onlab.packet.dhcp.Dhcp6IaAddressOption;
+import org.onlab.packet.dhcp.Dhcp6IaPrefixOption;
+import org.onlab.packet.dhcp.Dhcp6Option;
+import org.onlab.packet.dhcp.Dhcp6ClientIdOption;
+import org.onlab.packet.dhcp.Dhcp6Duid;
+import org.onosproject.dhcprelay.store.DhcpRelayStore;
+import org.onosproject.dhcprelay.store.DhcpRecord;
+import org.onosproject.dhcprelay.store.DhcpRelayStoreEvent;
+import org.onosproject.dhcprelay.store.DhcpRelayCounters;
+import org.onosproject.dhcprelay.store.DhcpRelayCountersStore;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.TestApplicationId;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.dhcprelay.config.DefaultDhcpRelayConfig;
+import org.onosproject.dhcprelay.config.DhcpServerConfig;
+import org.onosproject.dhcprelay.config.IgnoreDhcpConfig;
+import org.onosproject.dhcprelay.config.IndirectDhcpRelayConfig;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.behaviour.Pipeliner;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.Objective;
+import org.onosproject.net.host.HostProviderService;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.intf.InterfaceServiceAdapter;
+import org.onosproject.net.packet.PacketPriority;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteStoreAdapter;
+import org.onosproject.net.DefaultHost;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.config.NetworkConfigRegistry;
+import org.onosproject.net.host.HostDescription;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.host.InterfaceIpAddress;
+import org.onosproject.net.packet.DefaultInboundPacket;
+import org.onosproject.net.packet.InboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.packet.PacketContextAdapter;
+import org.onosproject.net.packet.PacketProcessor;
+import org.onosproject.net.packet.PacketServiceAdapter;
+import org.onosproject.store.StoreDelegate;
+
+
+
+
+import org.osgi.service.component.ComponentContext;
+import org.onlab.packet.DHCP6;
+import org.onlab.packet.IPv6;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.easymock.EasyMock.*;
+import static org.junit.Assert.*;
+import static org.onosproject.dhcprelay.DhcpRelayManager.DHCP_RELAY_APP;
+
+public class DhcpRelayManagerTest {
+    private static final short VLAN_LEN = 2;
+    private static final short SEPARATOR_LEN = 1;
+    private static final String CONFIG_FILE_PATH = "dhcp-relay.json";
+    private static final DeviceId DEV_1_ID = DeviceId.deviceId("of:0000000000000001");
+    private static final DeviceId DEV_2_ID = DeviceId.deviceId("of:0000000000000002");
+    // Ip address for interfaces
+    private static final InterfaceIpAddress INTERFACE_IP = InterfaceIpAddress.valueOf("10.0.3.254/32");
+    private static final InterfaceIpAddress INTERFACE_IP_V6 = InterfaceIpAddress.valueOf("2001:db8:1::254/128");
+    private static final List<InterfaceIpAddress> INTERFACE_IPS = ImmutableList.of(INTERFACE_IP, INTERFACE_IP_V6);
+
+    // DHCP client (will send without option 82)
+    private static final Ip4Address IP_FOR_CLIENT = Ip4Address.valueOf("10.0.0.1");
+    private static final Ip6Address IP_FOR_CLIENT_V6 = Ip6Address.valueOf("2001:db8:1::110");
+    private static final IpPrefix PREFIX_FOR_CLIENT_V6 = IpPrefix.valueOf("2001:db8:10::/56");
+    private static final IpPrefix PREFIX_FOR_ZERO = IpPrefix.valueOf("::/0");
+    private static final MacAddress CLIENT_MAC = MacAddress.valueOf("00:00:00:00:00:01");
+    private static final VlanId CLIENT_VLAN = VlanId.vlanId("100");
+    private static final ConnectPoint CLIENT_CP = ConnectPoint.deviceConnectPoint("of:0000000000000001/1");
+    private static final MacAddress CLIENT_IFACE_MAC = MacAddress.valueOf("00:00:00:00:11:01");
+    private static final HostLocation CLIENT_LOCATION = new HostLocation(CLIENT_CP, 0);
+    private static final HostId CLIENT_HOST_ID = HostId.hostId(CLIENT_MAC, CLIENT_VLAN);
+    private static final Ip6Address CLIENT_LL_IP_V6 = Ip6Address.valueOf("fe80::200:00ff:fe00:0001");
+    private static final Host EXISTS_HOST = new DefaultHost(Dhcp4HandlerImpl.PROVIDER_ID,
+                                                            CLIENT_HOST_ID, CLIENT_MAC, CLIENT_VLAN,
+                                                            CLIENT_LOCATION, ImmutableSet.of(CLIENT_LL_IP_V6));
+    private static final Interface CLIENT_INTERFACE = createInterface("C1",
+                                                                      CLIENT_CP,
+                                                                      INTERFACE_IPS,
+                                                                      CLIENT_IFACE_MAC,
+                                                                      CLIENT_VLAN,
+                                                                      null);
+
+
+
+    // Dual homing test
+    private static final ConnectPoint CLIENT_DH_CP = ConnectPoint.deviceConnectPoint("of:0000000000000001/3");
+    private static final HostLocation CLIENT_DH_LOCATION = new HostLocation(CLIENT_DH_CP, 0);
+    private static final Interface CLIENT_DH_INTERFACE = createInterface("C1-DH",
+                                                                         CLIENT_DH_CP,
+                                                                         INTERFACE_IPS,
+                                                                         CLIENT_IFACE_MAC,
+                                                                         CLIENT_VLAN,
+                                                                         null);
+
+
+    // DHCP client 2 (will send with option 82, so the vlan should equals to vlan from server)
+    private static final MacAddress CLIENT2_MAC = MacAddress.valueOf("00:00:00:00:00:01");
+    private static final VlanId CLIENT2_VLAN = VlanId.NONE;
+    private static final VlanId CLIENT2_VLAN_NATIVE = VlanId.vlanId("20");
+    private static final ConnectPoint CLIENT2_CP = ConnectPoint.deviceConnectPoint("of:0000000000000001/2");
+    private static final MacAddress CLIENT2_IFACE_MAC = MacAddress.valueOf("00:00:00:00:11:01");
+    private static final Interface CLIENT2_INTERFACE = createInterface("C2",
+                                                                       CLIENT2_CP,
+                                                                       INTERFACE_IPS,
+                                                                       CLIENT2_IFACE_MAC,
+                                                                       CLIENT2_VLAN,
+                                                                       CLIENT2_VLAN_NATIVE);
+    private static final VlanId CLIENT_BOGUS_VLAN = VlanId.vlanId("108");
+
+    // Outer relay information
+    private static final Ip4Address OUTER_RELAY_IP = Ip4Address.valueOf("10.0.6.253");
+    private static final Ip6Address OUTER_RELAY_IP_V6 = Ip6Address.valueOf("2001:db8:1::4");
+    private static final Ip6Address OUTER_RELAY_LL_IP_V6 = Ip6Address.valueOf("fe80::200:0102:0304:0501");
+    private static final Set<IpAddress> OUTER_RELAY_IPS = ImmutableSet.of(OUTER_RELAY_IP,
+            OUTER_RELAY_IP_V6,
+            OUTER_RELAY_LL_IP_V6);
+    private static final MacAddress OUTER_RELAY_MAC = MacAddress.valueOf("00:01:02:03:04:05");
+    private static final VlanId OUTER_RELAY_VLAN = VlanId.NONE;
+    private static final ConnectPoint OUTER_RELAY_CP = ConnectPoint.deviceConnectPoint("of:0000000000000001/2");
+    private static final HostLocation OUTER_REPLAY_HL = new HostLocation(OUTER_RELAY_CP, 0);
+    private static final HostId OUTER_RELAY_HOST_ID = HostId.hostId(OUTER_RELAY_MAC, OUTER_RELAY_VLAN);
+    private static final Host OUTER_RELAY_HOST = new DefaultHost(Dhcp4HandlerImpl.PROVIDER_ID,
+                                                                 OUTER_RELAY_HOST_ID,
+                                                                 OUTER_RELAY_MAC,
+                                                                 OUTER_RELAY_VLAN,
+                                                                 OUTER_REPLAY_HL,
+                                                                 OUTER_RELAY_IPS);
+
+    // DHCP Server
+    private static final MacAddress SERVER_MAC = MacAddress.valueOf("00:00:00:00:00:01");
+    private static final VlanId SERVER_VLAN = VlanId.NONE;
+    private static final VlanId SERVER_VLAN_NATIVE = VlanId.vlanId("10");
+    private static final ConnectPoint SERVER_CONNECT_POINT =
+            ConnectPoint.deviceConnectPoint("of:0000000000000001/5");
+    private static final HostLocation SERVER_LOCATION =
+            new HostLocation(SERVER_CONNECT_POINT, 0);
+    private static final Ip4Address GATEWAY_IP = Ip4Address.valueOf("10.0.5.253");
+    private static final Ip6Address GATEWAY_IP_V6 = Ip6Address.valueOf("2000::105:253");
+    private static final Ip4Address SERVER_IP = Ip4Address.valueOf("10.0.3.253");
+    private static final Ip6Address SERVER_IP_V6 = Ip6Address.valueOf("2000::103:253");
+    private static final Ip6Address SERVER_IP_V6_MCAST = Ip6Address.valueOf("ff02::1:2");
+    private static final Set<IpAddress> DHCP_SERVER_IPS = ImmutableSet.of(SERVER_IP, SERVER_IP_V6);
+    private static final HostId SERVER_HOST_ID = HostId.hostId(SERVER_MAC, SERVER_VLAN);
+    private static final Host SERVER_HOST = new DefaultHost(Dhcp4HandlerImpl.PROVIDER_ID,
+                                                            SERVER_HOST_ID,
+                                                            SERVER_MAC,
+                                                            SERVER_VLAN,
+                                                            SERVER_LOCATION,
+                                                            DHCP_SERVER_IPS);
+    private static final MacAddress SERVER_IFACE_MAC = MacAddress.valueOf("00:00:00:00:00:01");
+    private static final Interface SERVER_INTERFACE = createInterface("SERVER",
+                                                                      SERVER_CONNECT_POINT,
+                                                                      INTERFACE_IPS,
+                                                                      SERVER_IFACE_MAC,
+                                                                      SERVER_VLAN,
+                                                                      SERVER_VLAN_NATIVE);
+
+    // Relay agent config
+    private static final Ip4Address RELAY_AGENT_IP = Ip4Address.valueOf("10.0.4.254");
+
+    private static final List<TrafficSelector> DHCP_SELECTORS = buildClientDhcpSelectors();
+
+    // Components
+    private static final ApplicationId APP_ID = TestApplicationId.create(DhcpRelayManager.DHCP_RELAY_APP);
+    private static final DefaultDhcpRelayConfig CONFIG = new MockDefaultDhcpRelayConfig();
+    private static final IndirectDhcpRelayConfig CONFIG_INDIRECT = new MockIndirectDhcpRelayConfig();
+    private static final Set<Interface> INTERFACES = ImmutableSet.of(
+            CLIENT_INTERFACE,
+            CLIENT2_INTERFACE,
+            SERVER_INTERFACE,
+            CLIENT_DH_INTERFACE
+    );
+    private static final String NON_ONOS_CID = "Non-ONOS circuit ID";
+    private static final VlanId IGNORED_VLAN = VlanId.vlanId("100");
+    private static final int IGNORE_CONTROL_PRIORITY = PacketPriority.CONTROL.priorityValue() + 1000;
+
+    private DhcpRelayManager manager;
+    private MockPacketService packetService;
+    private MockRouteStore mockRouteStore;
+    private MockDhcpRelayStore mockDhcpRelayStore;
+    private MockDhcpRelayCountersStore mockDhcpRelayCountersStore;
+    private HostProviderService mockHostProviderService;
+    private FlowObjectiveService flowObjectiveService;
+    private DeviceService deviceService;
+    private Dhcp4HandlerImpl v4Handler;
+    private Dhcp6HandlerImpl v6Handler;
+
+    private static Interface createInterface(String name, ConnectPoint connectPoint,
+                                             List<InterfaceIpAddress> interfaceIps,
+                                             MacAddress macAddress,
+                                             VlanId vlanId,
+                                             VlanId vlanNative) {
+
+        if (vlanId.equals(VlanId.NONE)) {
+            return new Interface(name, connectPoint, interfaceIps, macAddress, vlanId,
+                                 null, null, vlanNative);
+        } else {
+            return new Interface(name, connectPoint, interfaceIps, macAddress, vlanId,
+                                 null, ImmutableSet.of(vlanId), null);
+        }
+    }
+
+    @Before
+    public void setup() {
+        manager = new DhcpRelayManager();
+        manager.cfgService = createNiceMock(NetworkConfigRegistry.class);
+
+        expect(manager.cfgService.getConfig(APP_ID, DefaultDhcpRelayConfig.class))
+                .andReturn(CONFIG)
+                .anyTimes();
+
+        expect(manager.cfgService.getConfig(APP_ID, IndirectDhcpRelayConfig.class))
+                .andReturn(CONFIG_INDIRECT)
+                .anyTimes();
+
+        manager.coreService = createNiceMock(CoreService.class);
+        expect(manager.coreService.registerApplication(anyString()))
+                .andReturn(APP_ID).anyTimes();
+
+        manager.hostService = createNiceMock(HostService.class);
+
+        expect(manager.hostService.getHostsByIp(OUTER_RELAY_IP_V6))
+                .andReturn(ImmutableSet.of(OUTER_RELAY_HOST)).anyTimes();
+        expect(manager.hostService.getHostsByIp(SERVER_IP))
+                .andReturn(ImmutableSet.of(SERVER_HOST)).anyTimes();
+        expect(manager.hostService.getHostsByIp(SERVER_IP_V6))
+                .andReturn(ImmutableSet.of(SERVER_HOST)).anyTimes();
+        expect(manager.hostService.getHostsByIp(GATEWAY_IP))
+                .andReturn(ImmutableSet.of(SERVER_HOST)).anyTimes();
+        expect(manager.hostService.getHostsByIp(GATEWAY_IP_V6))
+                .andReturn(ImmutableSet.of(SERVER_HOST)).anyTimes();
+        expect(manager.hostService.getHostsByIp(CLIENT_LL_IP_V6))
+                .andReturn(ImmutableSet.of(EXISTS_HOST)).anyTimes();
+
+        expect(manager.hostService.getHost(OUTER_RELAY_HOST_ID)).andReturn(OUTER_RELAY_HOST).anyTimes();
+
+        packetService = new MockPacketService();
+        manager.packetService = packetService;
+        manager.compCfgService = createNiceMock(ComponentConfigService.class);
+        deviceService = createNiceMock(DeviceService.class);
+
+        Device device = createNiceMock(Device.class);
+        expect(device.is(Pipeliner.class)).andReturn(true).anyTimes();
+
+        expect(deviceService.getDevice(DEV_1_ID)).andReturn(device).anyTimes();
+        expect(deviceService.getDevice(DEV_2_ID)).andReturn(device).anyTimes();
+        replay(deviceService, device);
+
+        mockRouteStore = new MockRouteStore();
+        mockDhcpRelayStore = new MockDhcpRelayStore();
+        mockDhcpRelayCountersStore = new MockDhcpRelayCountersStore();
+
+        manager.dhcpRelayStore = mockDhcpRelayStore;
+
+        manager.deviceService = deviceService;
+
+        manager.interfaceService = new MockInterfaceService();
+        flowObjectiveService = EasyMock.niceMock(FlowObjectiveService.class);
+        mockHostProviderService = createNiceMock(HostProviderService.class);
+        v4Handler = new Dhcp4HandlerImpl();
+        v4Handler.providerService = mockHostProviderService;
+        v4Handler.dhcpRelayStore = mockDhcpRelayStore;
+        v4Handler.hostService = manager.hostService;
+        v4Handler.interfaceService = manager.interfaceService;
+        v4Handler.packetService = manager.packetService;
+        v4Handler.routeStore = mockRouteStore;
+        v4Handler.coreService = createNiceMock(CoreService.class);
+        v4Handler.flowObjectiveService = flowObjectiveService;
+        v4Handler.appId = TestApplicationId.create(Dhcp4HandlerImpl.DHCP_V4_RELAY_APP);
+        v4Handler.deviceService = deviceService;
+        manager.v4Handler = v4Handler;
+
+        v6Handler = new Dhcp6HandlerImpl();
+        v6Handler.dhcpRelayStore = mockDhcpRelayStore;
+        v6Handler.dhcpRelayCountersStore = mockDhcpRelayCountersStore;
+        v6Handler.hostService = manager.hostService;
+        v6Handler.interfaceService = manager.interfaceService;
+        v6Handler.packetService = manager.packetService;
+        v6Handler.routeStore = mockRouteStore;
+        v6Handler.providerService = mockHostProviderService;
+        v6Handler.coreService = createNiceMock(CoreService.class);
+        v6Handler.flowObjectiveService = flowObjectiveService;
+        v6Handler.appId = TestApplicationId.create(Dhcp6HandlerImpl.DHCP_V6_RELAY_APP);
+        v6Handler.deviceService = deviceService;
+        manager.v6Handler = v6Handler;
+
+        // properties
+        Dictionary<String, Object> dictionary = createNiceMock(Dictionary.class);
+        expect(dictionary.get("arpEnabled")).andReturn(true).anyTimes();
+        expect(dictionary.get("dhcpPollInterval")).andReturn(120).anyTimes();
+        ComponentContext context = createNiceMock(ComponentContext.class);
+        expect(context.getProperties()).andReturn(dictionary).anyTimes();
+
+        replay(manager.cfgService, manager.coreService, manager.hostService,
+               manager.compCfgService, dictionary, context);
+        manager.activate(context);
+    }
+
+    @After
+    public void tearDown() {
+        manager.deactivate();
+    }
+
+    /**
+     * Relay a DHCP packet without option 82.
+     * Should add new host to host store after dhcp ack.
+     */
+    @Test
+    public void relayDhcpWithoutAgentInfo() {
+        replay(mockHostProviderService);
+        // send request
+        packetService.processPacket(new TestDhcpRequestPacketContext(CLIENT_MAC,
+                                                                     CLIENT_VLAN,
+                                                                     CLIENT_CP,
+                                                                     INTERFACE_IP.ipAddress().getIp4Address(),
+                                                                     false));
+        // won't trigger the host provider service
+        verify(mockHostProviderService);
+        reset(mockHostProviderService);
+
+        assertEquals(0, mockRouteStore.routes.size());
+
+        HostId expectHostId = HostId.hostId(CLIENT_MAC, CLIENT_VLAN);
+        Capture<HostDescription> capturedHostDesc = newCapture();
+        mockHostProviderService.hostDetected(eq(expectHostId), capture(capturedHostDesc), eq(false));
+        replay(mockHostProviderService);
+        // send ack
+        packetService.processPacket(new TestDhcpAckPacketContext(CLIENT_CP, CLIENT_MAC,
+                                                                 CLIENT_VLAN, INTERFACE_IP.ipAddress().getIp4Address(),
+                                                                 false));
+        verify(mockHostProviderService);
+        assertEquals(0, mockRouteStore.routes.size());
+
+        HostDescription host = capturedHostDesc.getValue();
+        assertEquals(false, host.configured());
+        assertEquals(CLIENT_CP.deviceId(), host.location().elementId());
+        assertEquals(CLIENT_CP.port(), host.location().port());
+        assertEquals(1, host.ipAddress().size());
+        assertEquals(IP_FOR_CLIENT, host.ipAddress().iterator().next());
+    }
+
+    /**
+     * Relay a DHCP packet with option 82 (Indirectly connected host).
+     */
+    @Test
+    public void relayDhcpWithAgentInfo() {
+        replay(mockHostProviderService);
+        // Assume outer dhcp relay agent exists in store already
+        // send request
+        packetService.processPacket(new TestDhcpRequestPacketContext(CLIENT2_MAC,
+                                                                     CLIENT2_VLAN,
+                                                                     CLIENT2_CP,
+                                                                     INTERFACE_IP.ipAddress().getIp4Address(),
+                                                                     true));
+        // No routes
+        assertEquals(0, mockRouteStore.routes.size());
+        // send ack
+        packetService.processPacket(new TestDhcpAckPacketContext(CLIENT2_CP,
+                                                                 CLIENT2_MAC,
+                                                                 CLIENT2_VLAN,
+                                                                 INTERFACE_IP.ipAddress().getIp4Address(),
+                                                                 true));
+
+        // won't trigger the host provider service
+        verify(mockHostProviderService);
+        reset(mockHostProviderService);
+
+        assertEquals(1, mockRouteStore.routes.size());
+
+        Route route = mockRouteStore.routes.get(0);
+        assertEquals(OUTER_RELAY_IP, route.nextHop());
+        assertEquals(IP_FOR_CLIENT.toIpPrefix(), route.prefix());
+        assertEquals(Route.Source.DHCP, route.source());
+    }
+
+    @Test
+    public void testWithRelayAgentConfig() throws DeserializationException {
+        manager.v4Handler
+                .setDefaultDhcpServerConfigs(ImmutableList.of(new MockDhcpServerConfig(RELAY_AGENT_IP)));
+        manager.v4Handler
+                .setIndirectDhcpServerConfigs(ImmutableList.of(new MockDhcpServerConfig(RELAY_AGENT_IP)));
+        packetService.processPacket(new TestDhcpRequestPacketContext(CLIENT2_MAC,
+                                                                     CLIENT2_VLAN,
+                                                                     CLIENT2_CP,
+                                                                     INTERFACE_IP.ipAddress().getIp4Address(),
+                                                                     true));
+        OutboundPacket outPacket = packetService.emittedPacket;
+        byte[] outData = outPacket.data().array();
+        Ethernet eth = Ethernet.deserializer().deserialize(outData, 0, outData.length);
+        IPv4 ip = (IPv4) eth.getPayload();
+        UDP udp = (UDP) ip.getPayload();
+        DHCP dhcp = (DHCP) udp.getPayload();
+        assertEquals(RELAY_AGENT_IP.toInt(), dhcp.getGatewayIPAddress());
+    }
+
+    @Test
+    public void testArpRequest() throws Exception {
+        packetService.processPacket(new TestArpRequestPacketContext(CLIENT_INTERFACE));
+        OutboundPacket outboundPacket = packetService.emittedPacket;
+        byte[] outPacketData = outboundPacket.data().array();
+        Ethernet eth = Ethernet.deserializer().deserialize(outPacketData, 0, outPacketData.length);
+
+        assertEquals(eth.getEtherType(), Ethernet.TYPE_ARP);
+        ARP arp = (ARP) eth.getPayload();
+        assertArrayEquals(arp.getSenderHardwareAddress(), CLIENT_INTERFACE.mac().toBytes());
+    }
+
+    /**
+     * Ignores specific vlans from specific devices if config.
+     *
+     * @throws Exception the exception from this test
+     */
+    @Test
+    public void testIgnoreVlan() throws Exception {
+        ObjectMapper om = new ObjectMapper();
+        JsonNode json = om.readTree(Resources.getResource(CONFIG_FILE_PATH));
+        IgnoreDhcpConfig config = new IgnoreDhcpConfig();
+        json = json.path("apps").path(DHCP_RELAY_APP).path(IgnoreDhcpConfig.KEY);
+        config.init(APP_ID, IgnoreDhcpConfig.KEY, json, om, null);
+
+        Capture<Objective> capturedFromDev1 = newCapture(CaptureType.ALL);
+        flowObjectiveService.apply(eq(DEV_1_ID), capture(capturedFromDev1));
+        expectLastCall().times(DHCP_SELECTORS.size());
+        Capture<Objective> capturedFromDev2 = newCapture(CaptureType.ALL);
+        flowObjectiveService.apply(eq(DEV_2_ID), capture(capturedFromDev2));
+        expectLastCall().times(DHCP_SELECTORS.size());
+        replay(flowObjectiveService);
+        manager.updateConfig(config);
+        verify(flowObjectiveService);
+
+        List<Objective> objectivesFromDev1 = capturedFromDev1.getValues();
+        List<Objective> objectivesFromDev2 = capturedFromDev2.getValues();
+
+        assertTrue(objectivesFromDev1.containsAll(objectivesFromDev2));
+        assertTrue(objectivesFromDev2.containsAll(objectivesFromDev1));
+
+        for (int index = 0; index < objectivesFromDev1.size(); index++) {
+            TrafficSelector selector =
+                    DefaultTrafficSelector.builder(DHCP_SELECTORS.get(index))
+                    .matchVlanId(IGNORED_VLAN)
+                    .build();
+            ForwardingObjective fwd = (ForwardingObjective) objectivesFromDev1.get(index);
+            assertEquals(selector, fwd.selector());
+            assertEquals(DefaultTrafficTreatment.emptyTreatment(), fwd.treatment());
+            assertEquals(IGNORE_CONTROL_PRIORITY, fwd.priority());
+            assertEquals(ForwardingObjective.Flag.VERSATILE, fwd.flag());
+            assertEquals(Objective.Operation.ADD, fwd.op());
+            fwd.context().ifPresent(ctx -> {
+                ctx.onSuccess(fwd);
+            });
+        }
+        objectivesFromDev2.forEach(obj -> obj.context().ifPresent(ctx -> ctx.onSuccess(obj)));
+        assertEquals(2, v4Handler.ignoredVlans.size());
+        assertEquals(2, v6Handler.ignoredVlans.size());
+    }
+
+    /**
+     * "IgnoreVlan" policy should be removed when the config removed.
+     */
+    @Test
+    public void testRemoveIgnoreVlan() {
+        v4Handler.ignoredVlans.put(DEV_1_ID, IGNORED_VLAN);
+        v4Handler.ignoredVlans.put(DEV_2_ID, IGNORED_VLAN);
+        v6Handler.ignoredVlans.put(DEV_1_ID, IGNORED_VLAN);
+        v6Handler.ignoredVlans.put(DEV_2_ID, IGNORED_VLAN);
+        IgnoreDhcpConfig config = new IgnoreDhcpConfig();
+
+        Capture<Objective> capturedFromDev1 = newCapture(CaptureType.ALL);
+        flowObjectiveService.apply(eq(DEV_1_ID), capture(capturedFromDev1));
+        expectLastCall().times(DHCP_SELECTORS.size());
+        Capture<Objective> capturedFromDev2 = newCapture(CaptureType.ALL);
+        flowObjectiveService.apply(eq(DEV_2_ID), capture(capturedFromDev2));
+        expectLastCall().times(DHCP_SELECTORS.size());
+        replay(flowObjectiveService);
+        manager.removeConfig(config);
+        verify(flowObjectiveService);
+
+        List<Objective> objectivesFromDev1 = capturedFromDev1.getValues();
+        List<Objective> objectivesFromDev2 = capturedFromDev2.getValues();
+
+        assertTrue(objectivesFromDev1.containsAll(objectivesFromDev2));
+        assertTrue(objectivesFromDev2.containsAll(objectivesFromDev1));
+
+        for (int index = 0; index < objectivesFromDev1.size(); index++) {
+            TrafficSelector selector =
+                    DefaultTrafficSelector.builder(DHCP_SELECTORS.get(index))
+                    .matchVlanId(IGNORED_VLAN)
+                    .build();
+            ForwardingObjective fwd = (ForwardingObjective) objectivesFromDev1.get(index);
+            assertEquals(selector, fwd.selector());
+            assertEquals(DefaultTrafficTreatment.emptyTreatment(), fwd.treatment());
+            assertEquals(IGNORE_CONTROL_PRIORITY, fwd.priority());
+            assertEquals(ForwardingObjective.Flag.VERSATILE, fwd.flag());
+            assertEquals(Objective.Operation.REMOVE, fwd.op());
+            fwd.context().ifPresent(ctx -> {
+                ctx.onSuccess(fwd);
+            });
+        }
+        objectivesFromDev2.forEach(obj -> obj.context().ifPresent(ctx -> ctx.onSuccess(obj)));
+        assertEquals(0, v4Handler.ignoredVlans.size());
+        assertEquals(0, v6Handler.ignoredVlans.size());
+    }
+
+    /**
+     * Should ignore ignore rules installation when device not available.
+     */
+    @Test
+    public void testIgnoreUnknownDevice() throws IOException {
+        reset(manager.deviceService);
+        Device device = createNiceMock(Device.class);
+        expect(device.is(Pipeliner.class)).andReturn(true).anyTimes();
+
+        expect(manager.deviceService.getDevice(DEV_1_ID)).andReturn(device).anyTimes();
+        expect(manager.deviceService.getDevice(DEV_2_ID)).andReturn(null).anyTimes();
+
+        ObjectMapper om = new ObjectMapper();
+        JsonNode json = om.readTree(Resources.getResource(CONFIG_FILE_PATH));
+        IgnoreDhcpConfig config = new IgnoreDhcpConfig();
+        json = json.path("apps").path(DHCP_RELAY_APP).path(IgnoreDhcpConfig.KEY);
+        config.init(APP_ID, IgnoreDhcpConfig.KEY, json, om, null);
+
+        Capture<Objective> capturedFromDev1 = newCapture(CaptureType.ALL);
+        flowObjectiveService.apply(eq(DEV_1_ID), capture(capturedFromDev1));
+        expectLastCall().times(DHCP_SELECTORS.size());
+        replay(flowObjectiveService, manager.deviceService, device);
+
+        manager.updateConfig(config);
+        capturedFromDev1.getValues().forEach(obj -> obj.context().ifPresent(ctx -> ctx.onSuccess(obj)));
+
+        assertEquals(1, v4Handler.ignoredVlans.size());
+        assertEquals(1, v6Handler.ignoredVlans.size());
+    }
+
+    /**
+     * Should try install ignore rules when device comes up.
+     */
+    @Test
+    public void testInstallIgnoreRuleWhenDeviceComesUp() throws IOException {
+        ObjectMapper om = new ObjectMapper();
+        JsonNode json = om.readTree(Resources.getResource(CONFIG_FILE_PATH));
+        IgnoreDhcpConfig config = new IgnoreDhcpConfig();
+        json = json.path("apps").path(DHCP_RELAY_APP).path(IgnoreDhcpConfig.KEY);
+        config.init(APP_ID, IgnoreDhcpConfig.KEY, json, om, null);
+
+        reset(manager.cfgService, flowObjectiveService, manager.deviceService);
+        expect(manager.cfgService.getConfig(APP_ID, IgnoreDhcpConfig.class))
+                .andReturn(config).anyTimes();
+
+        Device device = createNiceMock(Device.class);
+        expect(device.is(Pipeliner.class)).andReturn(true).anyTimes();
+        expect(device.id()).andReturn(DEV_1_ID).anyTimes();
+        expect(manager.deviceService.getDevice(DEV_1_ID)).andReturn(device).anyTimes();
+        DeviceEvent event = new DeviceEvent(DeviceEvent.Type.DEVICE_ADDED, device);
+        Capture<Objective> capturedFromDev1 = newCapture(CaptureType.ALL);
+        flowObjectiveService.apply(eq(DEV_1_ID), capture(capturedFromDev1));
+        expectLastCall().times(DHCP_SELECTORS.size());
+        replay(manager.cfgService, flowObjectiveService, manager.deviceService, device);
+        manager.deviceListener.event(event);
+        capturedFromDev1.getValues().forEach(obj -> obj.context().ifPresent(ctx -> ctx.onSuccess(obj)));
+        assertEquals(1, v4Handler.ignoredVlans.size());
+        assertEquals(1, v6Handler.ignoredVlans.size());
+    }
+
+    /**
+     * Relay a DHCP6 packet without relay option
+     * Note: Should add new host to host store after dhcp ack.
+     */
+    @Test
+    public void relayDhcp6WithoutAgentInfo() {
+        replay(mockHostProviderService);
+        // send request
+        packetService.processPacket(new TestDhcp6RequestPacketContext(DHCP6.MsgType.REQUEST.value(),
+                                                                     CLIENT_MAC,
+                                                                     CLIENT_VLAN,
+                                                                     CLIENT_CP,
+                                                                     INTERFACE_IP_V6.ipAddress().getIp6Address(),
+                                                                     0));
+
+        verify(mockHostProviderService);
+        reset(mockHostProviderService);
+        assertEquals(0, mockRouteStore.routes.size());
+
+        Capture<HostDescription> capturedHostDesc = newCapture();
+        mockHostProviderService.hostDetected(eq(HostId.hostId(CLIENT_MAC, CLIENT_VLAN)),
+                                             capture(capturedHostDesc), eq(false));
+        replay(mockHostProviderService);
+        // send reply
+        packetService.processPacket(new TestDhcp6ReplyPacketContext(DHCP6.MsgType.REPLY.value(),
+                                                                    CLIENT_CP, CLIENT_MAC,
+                                                                    CLIENT_VLAN,
+                                                                    INTERFACE_IP_V6.ipAddress().getIp6Address(),
+                                                                    0, false, CLIENT_VLAN));
+        verify(mockHostProviderService);
+        assertEquals(0, mockRouteStore.routes.size());
+
+        HostDescription host = capturedHostDesc.getValue();
+        assertEquals(CLIENT_VLAN, host.vlan());
+        assertEquals(CLIENT_CP.deviceId(), host.location().elementId());
+        assertEquals(CLIENT_CP.port(), host.location().port());
+        assertEquals(1, host.ipAddress().size());
+        assertEquals(IP_FOR_CLIENT_V6, host.ipAddress().iterator().next());
+
+        // send release
+        packetService.processPacket(new TestDhcp6RequestPacketContext(DHCP6.MsgType.RELEASE.value(),
+                CLIENT_MAC,
+                CLIENT_VLAN,
+                CLIENT_CP,
+                INTERFACE_IP_V6.ipAddress().getIp6Address(),
+                0));
+
+        assertEquals(null, manager.hostService.getHost(HostId.hostId(CLIENT_MAC, CLIENT_VLAN)));
+    }
+
+    /**
+     * Relay a DHCP6 packet with Relay Message opion (Indirectly connected host).
+     */
+    @Test
+    public void relayDhcp6WithAgentInfo() {
+        replay(mockHostProviderService);
+        // Assume outer dhcp6 relay agent exists in store already
+        // send request
+        packetService.processPacket(new TestDhcp6RequestPacketContext(DHCP6.MsgType.REQUEST.value(),
+                CLIENT2_MAC,
+                CLIENT2_VLAN,
+                CLIENT2_CP,
+                OUTER_RELAY_IP_V6,
+                1));
+
+        assertEquals(0, mockRouteStore.routes.size());
+
+        // send reply
+        packetService.processPacket(new TestDhcp6ReplyPacketContext(DHCP6.MsgType.REPLY.value(), CLIENT2_CP,
+                CLIENT2_MAC,
+                CLIENT2_VLAN,
+                OUTER_RELAY_IP_V6,
+                1, false, CLIENT2_VLAN));
+
+        // won't trigger the host provider service
+        verify(mockHostProviderService);
+        reset(mockHostProviderService);
+        assertEquals(2, mockRouteStore.routes.size()); // ipAddress and prefix
+
+        Route aRoute = mockRouteStore.routes.stream()
+                             .filter(rt -> rt.prefix().contains(IP_FOR_CLIENT_V6))
+                             .findFirst()
+                             .orElse(null);
+        assertNotEquals(null, aRoute);
+
+        aRoute = mockRouteStore.routes.stream()
+                .filter(rt -> rt.prefix().contains(PREFIX_FOR_CLIENT_V6))
+                .findFirst()
+                .orElse(null);
+        assertNotEquals(null, aRoute);
+
+        // send release msg
+        packetService.processPacket(new TestDhcp6RequestPacketContext(DHCP6.MsgType.RELEASE.value(),
+                CLIENT2_MAC,
+                CLIENT2_VLAN,
+                CLIENT2_CP,
+                OUTER_RELAY_IP_V6,
+                1));
+
+        aRoute = mockRouteStore.routes.stream()
+                .filter(rt -> rt.prefix().contains(IP_FOR_CLIENT_V6))
+                .findFirst()
+                .orElse(null);
+        assertEquals(null, aRoute);
+
+        aRoute = mockRouteStore.routes.stream()
+                .filter(rt -> rt.prefix().contains(PREFIX_FOR_CLIENT_V6))
+                .findFirst()
+                .orElse(null);
+        assertEquals(null, aRoute);
+
+        assertEquals(0, mockRouteStore.routes.size());
+
+    }
+
+    /**
+     * Relay a DHCP6 packet with Relay Message opion (Indirectly connected host) and server responded
+     * with vlan differnt from client interface vlan.
+     */
+    @Test
+    public void relayDhcp6WithAgentInfoWrongVlan() {
+        replay(mockHostProviderService);
+        // Assume outer dhcp6 relay agent exists in store already
+        // send request
+        packetService.processPacket(new TestDhcp6RequestPacketContext(DHCP6.MsgType.REQUEST.value(),
+                CLIENT2_MAC,
+                CLIENT2_VLAN,
+                CLIENT2_CP,
+                INTERFACE_IP_V6.ipAddress().getIp6Address(),
+                1));
+
+        assertEquals(0, mockRouteStore.routes.size());
+
+        // send reply
+        packetService.processPacket(new TestDhcp6ReplyPacketContext(DHCP6.MsgType.REPLY.value(),
+                CLIENT2_CP,
+                CLIENT2_MAC,
+                CLIENT2_VLAN,
+                INTERFACE_IP_V6.ipAddress().getIp6Address(),
+                1, true,
+                CLIENT_BOGUS_VLAN // mismatch
+        ));
+
+        // won't trigger the host provider service
+        verify(mockHostProviderService);
+        reset(mockHostProviderService);
+        assertEquals(0, mockRouteStore.routes.size()); // ipAddress and prefix
+
+    }
+
+
+    @Test
+    public void testDhcp4DualHome() {
+        PacketContext packetContext =
+                new TestDhcpAckPacketContext(CLIENT_DH_CP, CLIENT_MAC, CLIENT_VLAN,
+                                             INTERFACE_IP.ipAddress().getIp4Address(),
+                                             false);
+        reset(manager.hostService);
+        expect(manager.hostService.getHost(CLIENT_HOST_ID)).andReturn(EXISTS_HOST).anyTimes();
+        Capture<HostDescription> capturedHostDesc = newCapture();
+        mockHostProviderService.hostDetected(eq(CLIENT_HOST_ID), capture(capturedHostDesc), eq(false));
+        replay(mockHostProviderService, manager.hostService);
+        packetService.processPacket(packetContext);
+        verify(mockHostProviderService);
+
+        HostDescription hostDesc = capturedHostDesc.getValue();
+        Set<HostLocation> hostLocations = hostDesc.locations();
+        assertEquals(2, hostLocations.size());
+        assertTrue(hostLocations.contains(CLIENT_LOCATION));
+        assertTrue(hostLocations.contains(CLIENT_DH_LOCATION));
+    }
+
+    @Test
+    public void testDhcp6DualHome() {
+        PacketContext packetContext =
+                new TestDhcp6ReplyPacketContext(DHCP6.MsgType.REPLY.value(),
+                                                CLIENT_DH_CP, CLIENT_MAC, CLIENT_VLAN,
+                                                INTERFACE_IP_V6.ipAddress().getIp6Address(),
+                                                0, false, CLIENT_VLAN);
+        reset(manager.hostService);
+        expect(manager.hostService.getHostsByIp(CLIENT_LL_IP_V6)).andReturn(ImmutableSet.of(EXISTS_HOST)).anyTimes();
+
+        // FIXME: currently DHCPv6 has a bug, we can't get correct vlan of client......
+        // XXX: The vlan relied from DHCP6 handler might be wrong, do hack here
+        HostId hostId = HostId.hostId(CLIENT_MAC, VlanId.NONE);
+        expect(manager.hostService.getHost(hostId)).andReturn(EXISTS_HOST).anyTimes();
+
+        // XXX: sometimes this will work, sometimes not
+         expect(manager.hostService.getHost(CLIENT_HOST_ID)).andReturn(EXISTS_HOST).anyTimes();
+
+        Capture<HostDescription> capturedHostDesc = newCapture();
+
+        // XXX: also a hack here
+        mockHostProviderService.hostDetected(eq(hostId), capture(capturedHostDesc), eq(false));
+        expectLastCall().anyTimes();
+
+        mockHostProviderService.hostDetected(eq(CLIENT_HOST_ID), capture(capturedHostDesc), eq(false));
+        expectLastCall().anyTimes();
+        replay(mockHostProviderService, manager.hostService);
+        packetService.processPacket(packetContext);
+        verify(mockHostProviderService);
+
+        HostDescription hostDesc = capturedHostDesc.getValue();
+        Set<HostLocation> hostLocations = hostDesc.locations();
+        assertEquals(2, hostLocations.size());
+        assertTrue(hostLocations.contains(CLIENT_LOCATION));
+        assertTrue(hostLocations.contains(CLIENT_DH_LOCATION));
+    }
+
+    private static class MockDefaultDhcpRelayConfig extends DefaultDhcpRelayConfig {
+        @Override
+        public boolean isValid() {
+            return true;
+        }
+
+        @Override
+        public List<DhcpServerConfig> dhcpServerConfigs() {
+            return ImmutableList.of(new MockDhcpServerConfig(null));
+        }
+    }
+
+    private static class MockIndirectDhcpRelayConfig extends IndirectDhcpRelayConfig {
+        @Override
+        public boolean isValid() {
+            return true;
+        }
+
+        @Override
+        public List<DhcpServerConfig> dhcpServerConfigs() {
+            return ImmutableList.of(new MockDhcpServerConfig(null));
+        }
+    }
+
+    private static class MockDhcpServerConfig extends DhcpServerConfig {
+        Ip4Address relayAgentIp;
+
+        /**
+         * Create mocked version DHCP server config.
+         *
+         * @param relayAgentIp the relay agent Ip config; null if we don't need it
+         */
+        public MockDhcpServerConfig(Ip4Address relayAgentIp) {
+            this.relayAgentIp = relayAgentIp;
+            this.relayAgentIps.put(DEV_1_ID, Pair.of(relayAgentIp, null));
+            this.relayAgentIps.put(DEV_2_ID, Pair.of(relayAgentIp, null));
+        }
+
+        @Override
+        public Optional<Ip4Address> getRelayAgentIp4(DeviceId deviceId) {
+            return Optional.ofNullable(this.relayAgentIps.get(deviceId).getLeft());
+        }
+
+        @Override
+        public Optional<ConnectPoint> getDhcpServerConnectPoint() {
+            return Optional.of(SERVER_CONNECT_POINT);
+        }
+
+        @Override
+        public Optional<Ip4Address> getDhcpServerIp4() {
+            return Optional.of(SERVER_IP);
+        }
+
+        @Override
+        public Optional<Ip4Address> getDhcpGatewayIp4() {
+            return Optional.of(GATEWAY_IP);
+        }
+
+        @Override
+        public Optional<Ip6Address> getDhcpServerIp6() {
+            return Optional.of(SERVER_IP_V6);
+        }
+
+        @Override
+        public Optional<Ip6Address> getDhcpGatewayIp6() {
+            return Optional.of(GATEWAY_IP_V6);
+        }
+    }
+
+    private class MockRouteStore extends RouteStoreAdapter {
+        private List<Route> routes = Lists.newArrayList();
+
+        @Override
+        public void updateRoute(Route route) {
+            routes.add(route);
+        }
+
+        @Override
+        public void removeRoute(Route route) {
+            routes.remove(route);
+        }
+
+        public void replaceRoute(Route route) {
+            routes.remove(route);
+            routes.add(route);
+        }
+    }
+
+    private class MockInterfaceService extends InterfaceServiceAdapter {
+
+        @Override
+        public Set<Interface> getInterfaces() {
+            return INTERFACES;
+        }
+
+        @Override
+        public Set<Interface> getInterfacesByIp(IpAddress ip) {
+            return INTERFACES.stream()
+                    .filter(iface -> {
+                        return iface.ipAddressesList().stream()
+                                .anyMatch(ifaceIp -> ifaceIp.ipAddress().equals(ip));
+                    })
+                    .collect(Collectors.toSet());
+        }
+
+        @Override
+        public Set<Interface> getInterfacesByPort(ConnectPoint port) {
+            return INTERFACES.stream()
+                    .filter(iface -> iface.connectPoint().equals(port))
+                    .collect(Collectors.toSet());
+        }
+    }
+
+    private class MockDhcpRelayStore implements DhcpRelayStore {
+        StoreDelegate<DhcpRelayStoreEvent> delegate;
+        private Map<HostId, DhcpRecord> records = Maps.newHashMap();
+
+        @Override
+        public void updateDhcpRecord(HostId hostId, DhcpRecord dhcpRecord) {
+            records.put(hostId, dhcpRecord);
+            DhcpRelayStoreEvent event = new DhcpRelayStoreEvent(DhcpRelayStoreEvent.Type.UPDATED,
+                                                                dhcpRecord);
+            if (delegate != null) {
+                delegate.notify(event);
+            }
+        }
+
+        @Override
+        public Optional<DhcpRecord> getDhcpRecord(HostId hostId) {
+            return Optional.ofNullable(records.get(hostId));
+        }
+
+        @Override
+        public Collection<DhcpRecord> getDhcpRecords() {
+            return records.values();
+        }
+
+        @Override
+        public Optional<DhcpRecord> removeDhcpRecord(HostId hostId) {
+            DhcpRecord dhcpRecord = records.remove(hostId);
+            if (dhcpRecord != null) {
+                DhcpRelayStoreEvent event = new DhcpRelayStoreEvent(DhcpRelayStoreEvent.Type.REMOVED,
+                                                                    dhcpRecord);
+                if (delegate != null) {
+                    delegate.notify(event);
+                }
+            }
+            return Optional.ofNullable(dhcpRecord);
+        }
+
+        @Override
+        public void setDelegate(StoreDelegate<DhcpRelayStoreEvent> delegate) {
+            this.delegate = delegate;
+        }
+
+        @Override
+        public void unsetDelegate(StoreDelegate<DhcpRelayStoreEvent> delegate) {
+            this.delegate = null;
+        }
+
+        @Override
+        public boolean hasDelegate() {
+            return this.delegate != null;
+        }
+    }
+
+    private class MockDhcpRelayCountersStore implements DhcpRelayCountersStore {
+        private Map<String, DhcpRelayCounters> counters = Maps.newHashMap();
+
+        public void incrementCounter(String coutnerClass, String counterName) {
+            DhcpRelayCounters countersRecord;
+
+            DhcpRelayCounters classCounters = counters.get(coutnerClass);
+            if (classCounters == null) {
+                classCounters = new DhcpRelayCounters();
+            }
+            classCounters.incrementCounter(counterName);
+            counters.put(coutnerClass, classCounters);
+        }
+
+        @Override
+        public Set<Map.Entry<String, DhcpRelayCounters>> getAllCounters() {
+            return counters.entrySet();
+        }
+
+        @Override
+        public Optional<DhcpRelayCounters> getCounters(String counterClass) {
+            DhcpRelayCounters classCounters = counters.get(counterClass);
+            if (classCounters == null) {
+                return Optional.empty();
+            }
+            return Optional.of(classCounters);
+        }
+
+        @Override
+        public void resetAllCounters() {
+            counters.clear();
+        }
+
+        @Override
+        public void resetCounters(String counterClass) {
+            DhcpRelayCounters classCounters = counters.get(counterClass);
+            classCounters.resetCounters();
+            counters.put(counterClass, classCounters);
+        }
+    }
+
+
+    private class MockPacketService extends PacketServiceAdapter {
+        Set<PacketProcessor> packetProcessors = Sets.newHashSet();
+        OutboundPacket emittedPacket;
+
+        @Override
+        public void addProcessor(PacketProcessor processor, int priority) {
+            packetProcessors.add(processor);
+        }
+
+        public void processPacket(PacketContext packetContext) {
+            packetProcessors.forEach(p -> p.process(packetContext));
+        }
+
+        @Override
+        public void emit(OutboundPacket packet) {
+            this.emittedPacket = packet;
+        }
+    }
+
+
+
+    /**
+     * Generates DHCP REQUEST packet.
+     */
+    private class TestDhcpRequestPacketContext extends PacketContextAdapter {
+
+
+        private InboundPacket inPacket;
+
+        public TestDhcpRequestPacketContext(MacAddress clientMac, VlanId vlanId,
+                                            ConnectPoint clientCp,
+                                            Ip4Address clientGwAddr,
+                                            boolean withNonOnosRelayInfo) {
+            super(0, null, null, false);
+            byte[] dhcpMsgType = new byte[1];
+            dhcpMsgType[0] = (byte) DHCP.MsgType.DHCPREQUEST.getValue();
+
+            DhcpOption dhcpOption = new DhcpOption();
+            dhcpOption.setCode(DHCP.DHCPOptionCode.OptionCode_MessageType.getValue());
+            dhcpOption.setData(dhcpMsgType);
+            dhcpOption.setLength((byte) 1);
+            DhcpOption endOption = new DhcpOption();
+            endOption.setCode(DHCP.DHCPOptionCode.OptionCode_END.getValue());
+
+            DHCP dhcp = new DHCP();
+            dhcp.setHardwareType(DHCP.HWTYPE_ETHERNET);
+            dhcp.setHardwareAddressLength((byte) 6);
+            dhcp.setClientHardwareAddress(clientMac.toBytes());
+            if (withNonOnosRelayInfo) {
+                DhcpRelayAgentOption relayOption = new DhcpRelayAgentOption();
+                DhcpOption circuitIdOption = new DhcpOption();
+                CircuitId circuitId = new CircuitId("Custom option", VlanId.NONE);
+                byte[] cid = circuitId.serialize();
+                circuitIdOption.setCode(DhcpRelayAgentOption.RelayAgentInfoOptions.CIRCUIT_ID.getValue());
+                circuitIdOption.setLength((byte) cid.length);
+                circuitIdOption.setData(cid);
+                relayOption.setCode(DHCP.DHCPOptionCode.OptionCode_CircuitID.getValue());
+                relayOption.addSubOption(circuitIdOption);
+                dhcp.setOptions(ImmutableList.of(dhcpOption, relayOption, endOption));
+                dhcp.setGatewayIPAddress(OUTER_RELAY_IP.getIp4Address().toInt());
+            } else {
+                dhcp.setOptions(ImmutableList.of(dhcpOption, endOption));
+            }
+
+
+            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(SERVER_IP.toInt());
+            ipv4.setSourceAddress(clientGwAddr.toInt());
+
+            Ethernet eth = new Ethernet();
+            if (withNonOnosRelayInfo) {
+                eth.setEtherType(Ethernet.TYPE_IPV4)
+                        .setVlanID(vlanId.toShort())
+                        .setSourceMACAddress(OUTER_RELAY_MAC)
+                        .setDestinationMACAddress(MacAddress.BROADCAST)
+                        .setPayload(ipv4);
+            } else {
+                eth.setEtherType(Ethernet.TYPE_IPV4)
+                        .setVlanID(vlanId.toShort())
+                        .setSourceMACAddress(clientMac)
+                        .setDestinationMACAddress(MacAddress.BROADCAST)
+                        .setPayload(ipv4);
+            }
+
+            this.inPacket = new DefaultInboundPacket(clientCp, eth,
+                                                     ByteBuffer.wrap(eth.serialize()));
+        }
+
+        @Override
+        public InboundPacket inPacket() {
+            return this.inPacket;
+        }
+    }
+
+    /**
+     * Generates DHCP ACK packet.
+     */
+    private class TestDhcpAckPacketContext extends PacketContextAdapter {
+        private InboundPacket inPacket;
+
+        public TestDhcpAckPacketContext(ConnectPoint clientCp, MacAddress clientMac,
+                                        VlanId clientVlan, Ip4Address clientGwAddr,
+                                        boolean withNonOnosRelayInfo) {
+            super(0, null, null, false);
+
+            byte[] dhcpMsgType = new byte[1];
+            dhcpMsgType[0] = (byte) DHCP.MsgType.DHCPACK.getValue();
+
+            DhcpOption dhcpOption = new DhcpOption();
+            dhcpOption.setCode(DHCP.DHCPOptionCode.OptionCode_MessageType.getValue());
+            dhcpOption.setData(dhcpMsgType);
+            dhcpOption.setLength((byte) 1);
+
+            DhcpOption endOption = new DhcpOption();
+            endOption.setCode(DHCP.DHCPOptionCode.OptionCode_END.getValue());
+
+            DHCP dhcp = new DHCP();
+            if (withNonOnosRelayInfo) {
+                DhcpRelayAgentOption relayOption = new DhcpRelayAgentOption();
+                DhcpOption circuitIdOption = new DhcpOption();
+                String circuitId = NON_ONOS_CID;
+                byte[] cid = circuitId.getBytes(Charsets.US_ASCII);
+                circuitIdOption.setCode(DhcpRelayAgentOption.RelayAgentInfoOptions.CIRCUIT_ID.getValue());
+                circuitIdOption.setLength((byte) cid.length);
+                circuitIdOption.setData(cid);
+                relayOption.setCode(DHCP.DHCPOptionCode.OptionCode_CircuitID.getValue());
+                relayOption.addSubOption(circuitIdOption);
+                dhcp.setOptions(ImmutableList.of(dhcpOption, relayOption, endOption));
+                dhcp.setGatewayIPAddress(OUTER_RELAY_IP.getIp4Address().toInt());
+            } else {
+                CircuitId cid = new CircuitId(clientCp.toString(), clientVlan);
+                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, endOption));
+                dhcp.setGatewayIPAddress(clientGwAddr.toInt());
+            }
+            dhcp.setHardwareType(DHCP.HWTYPE_ETHERNET);
+            dhcp.setHardwareAddressLength((byte) 6);
+            dhcp.setClientHardwareAddress(clientMac.toBytes());
+            dhcp.setYourIPAddress(IP_FOR_CLIENT.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_FOR_CLIENT.toString());
+            ipv4.setSourceAddress(SERVER_IP.toString());
+            Ethernet eth = new Ethernet();
+            if (withNonOnosRelayInfo) {
+                eth.setEtherType(Ethernet.TYPE_IPV4)
+                        .setVlanID(SERVER_VLAN.toShort())
+                        .setSourceMACAddress(SERVER_MAC)
+                        .setDestinationMACAddress(OUTER_RELAY_MAC)
+                        .setPayload(ipv4);
+            } else {
+                eth.setEtherType(Ethernet.TYPE_IPV4)
+                        .setVlanID(SERVER_VLAN.toShort())
+                        .setSourceMACAddress(SERVER_MAC)
+                        .setDestinationMACAddress(CLIENT_MAC)
+                        .setPayload(ipv4);
+            }
+
+            this.inPacket = new DefaultInboundPacket(SERVER_CONNECT_POINT, eth,
+                                                     ByteBuffer.wrap(eth.serialize()));
+
+        }
+
+        @Override
+        public InboundPacket inPacket() {
+            return this.inPacket;
+        }
+    }
+
+    private class TestArpRequestPacketContext extends PacketContextAdapter {
+        private InboundPacket inPacket;
+
+        public TestArpRequestPacketContext(Interface fromInterface) {
+            super(0, null, null, false);
+            ARP arp = new ARP();
+            arp.setOpCode(ARP.OP_REQUEST);
+
+            IpAddress targetIp = fromInterface.ipAddressesList().get(0).ipAddress();
+            arp.setTargetProtocolAddress(targetIp.toOctets());
+            arp.setTargetHardwareAddress(MacAddress.BROADCAST.toBytes());
+            arp.setSenderHardwareAddress(MacAddress.NONE.toBytes());
+            arp.setSenderProtocolAddress(Ip4Address.valueOf(0).toOctets());
+            arp.setHardwareAddressLength((byte) MacAddress.MAC_ADDRESS_LENGTH);
+            Ethernet eth = new Ethernet();
+            eth.setEtherType(Ethernet.TYPE_ARP);
+            eth.setSourceMACAddress(MacAddress.NONE);
+            eth.setDestinationMACAddress(MacAddress.BROADCAST);
+            eth.setVlanID(fromInterface.vlan().toShort());
+            eth.setPayload(arp);
+
+            this.inPacket = new DefaultInboundPacket(fromInterface.connectPoint(), eth,
+                                                     ByteBuffer.wrap(eth.serialize()));
+        }
+
+        @Override
+        public InboundPacket inPacket() {
+            return this.inPacket;
+        }
+    }
+
+    /**
+     * Generates DHCP6 REQUEST packet.
+     */
+    private void buildDhcp6Packet(DHCP6 dhcp6, byte msgType, Ip6Address ip6Addr, IpPrefix prefix) {
+
+        // build address option
+        Dhcp6IaAddressOption iaAddressOption = new Dhcp6IaAddressOption();
+        iaAddressOption.setCode(DHCP6.OptionCode.IAADDR.value());
+        iaAddressOption.setIp6Address(ip6Addr);
+        iaAddressOption.setPreferredLifetime(3600);
+        iaAddressOption.setValidLifetime(1200);
+        iaAddressOption.setLength((short) Dhcp6IaAddressOption.DEFAULT_LEN);
+
+        Dhcp6ClientIdOption clientIdOption = new Dhcp6ClientIdOption();
+        Dhcp6Duid dhcp6Duip = new Dhcp6Duid();
+        dhcp6Duip.setDuidType(Dhcp6Duid.DuidType.DUID_LLT);
+        dhcp6Duip.setHardwareType((short) 0x01);   // Ethernet
+        dhcp6Duip.setDuidTime(1234);
+        dhcp6Duip.setLinkLayerAddress(CLIENT_MAC.toBytes());
+        clientIdOption.setDuid(dhcp6Duip);
+
+        Dhcp6IaNaOption iaNaOption = new Dhcp6IaNaOption();
+        iaNaOption.setCode(DHCP6.OptionCode.IA_NA.value());
+        iaNaOption.setIaId(0);
+        iaNaOption.setT1(302400);
+        iaNaOption.setT2(483840);
+        List<Dhcp6Option> iaNaSubOptions = new ArrayList<Dhcp6Option>();
+        iaNaSubOptions.add(iaAddressOption);
+        iaNaOption.setOptions(iaNaSubOptions);
+        iaNaOption.setLength((short) (Dhcp6IaNaOption.DEFAULT_LEN + iaAddressOption.getLength()));
+
+        // build prefix option
+        Dhcp6IaPrefixOption iaPrefixOption = new Dhcp6IaPrefixOption();
+        iaPrefixOption.setCode(DHCP6.OptionCode.IAPREFIX.value());
+        iaPrefixOption.setIp6Prefix(prefix.address().getIp6Address());
+        iaPrefixOption.setPrefixLength((byte) prefix.prefixLength());
+        iaPrefixOption.setPreferredLifetime(3601);
+        iaPrefixOption.setValidLifetime(1201);
+        iaPrefixOption.setLength((short) Dhcp6IaPrefixOption.DEFAULT_LEN);
+
+        Dhcp6IaPdOption iaPdOption = new Dhcp6IaPdOption();
+        iaPdOption.setCode(DHCP6.OptionCode.IA_PD.value());
+        iaPdOption.setIaId(0);
+        iaPdOption.setT1(302401);
+        iaPdOption.setT2(483841);
+        List<Dhcp6Option> iaPdSubOptions = new ArrayList<Dhcp6Option>();
+        iaPdSubOptions.add(iaPrefixOption);
+        iaPdOption.setOptions(iaPdSubOptions);
+        iaPdOption.setLength((short) (Dhcp6IaPdOption.DEFAULT_LEN + iaPrefixOption.getLength()));
+
+        dhcp6.setMsgType(msgType);
+        List<Dhcp6Option> dhcp6Options = new ArrayList<Dhcp6Option>();
+        dhcp6Options.add(iaNaOption);
+        dhcp6Options.add(clientIdOption);
+        dhcp6Options.add(iaPdOption);
+        dhcp6.setOptions(dhcp6Options);
+
+    }
+
+    private void buildRelayMsg(DHCP6 dhcp6Relay, byte msgType, Ip6Address linkAddr,
+                               Ip6Address peerAddr, byte hop, byte[] interfaceIdBytes,
+                               DHCP6 dhcp6Payload) {
+
+        dhcp6Relay.setMsgType(msgType);
+
+        dhcp6Relay.setLinkAddress(linkAddr.toOctets());
+        dhcp6Relay.setPeerAddress(peerAddr.toOctets());
+        dhcp6Relay.setHopCount(hop);
+        List<Dhcp6Option> options = new ArrayList<Dhcp6Option>();
+
+        // interfaceId  option
+        Dhcp6Option interfaceId = new Dhcp6Option();
+        interfaceId.setCode(DHCP6.OptionCode.INTERFACE_ID.value());
+
+
+        interfaceId.setData(interfaceIdBytes);
+        interfaceId.setLength((short) interfaceIdBytes.length);
+        Dhcp6InterfaceIdOption interfaceIdOption = new Dhcp6InterfaceIdOption(interfaceId);
+        byte[] optionData = interfaceIdOption.getData();
+        ByteBuffer bb = ByteBuffer.wrap(interfaceIdBytes);
+
+        byte[] macAddr = new byte[MacAddress.MAC_ADDRESS_LENGTH];
+        byte[] port =  new byte[optionData.length - MacAddress.MAC_ADDRESS_LENGTH -
+                                VLAN_LEN - SEPARATOR_LEN * 2];
+        short vlan;
+        bb.get(macAddr);
+        bb.get();  // separator
+        bb.get(port);
+        bb.get();  // separator
+        vlan = bb.getShort();
+        interfaceIdOption.setMacAddress(MacAddress.valueOf(macAddr));
+        interfaceIdOption.setInPort(port);
+        interfaceIdOption.setVlanId(vlan);
+
+        options.add(interfaceIdOption);
+
+        // relay message option
+        Dhcp6Option relayMsgOption = new Dhcp6Option();
+        relayMsgOption.setCode(DHCP6.OptionCode.RELAY_MSG.value());
+        byte[] dhcp6PayloadByte = dhcp6Payload.serialize();
+        relayMsgOption.setLength((short) dhcp6PayloadByte.length);
+        relayMsgOption.setPayload(dhcp6Payload);
+        Dhcp6RelayOption relayOpt = new Dhcp6RelayOption(relayMsgOption);
+
+        options.add(relayOpt);
+
+        dhcp6Relay.setOptions(options);
+    }
+    private byte[] buildInterfaceId(MacAddress clientMac, short vlanId, ConnectPoint clientCp) {
+        String inPortString = "-" + clientCp.toString() + ":";
+        byte[] clientSoureMacBytes = clientMac.toBytes();
+        byte[] inPortStringBytes = inPortString.getBytes();
+        byte[] vlanIdBytes = new byte[2];
+        vlanIdBytes[0] = (byte) ((vlanId >> 8) & 0xff);  // high-order byte first
+        vlanIdBytes[1] = (byte) (vlanId & 0xff);
+        byte[] interfaceIdBytes = new byte[clientSoureMacBytes.length +  inPortStringBytes.length + vlanIdBytes.length];
+
+        System.arraycopy(clientSoureMacBytes, 0, interfaceIdBytes, 0, clientSoureMacBytes.length);
+        System.arraycopy(inPortStringBytes, 0, interfaceIdBytes, clientSoureMacBytes.length, inPortStringBytes.length);
+        System.arraycopy(vlanIdBytes, 0, interfaceIdBytes, clientSoureMacBytes.length + inPortStringBytes.length,
+                vlanIdBytes.length);
+
+        return interfaceIdBytes;
+    }
+
+    private static List<TrafficSelector> buildClientDhcpSelectors() {
+        return Streams.concat(Dhcp4HandlerImpl.DHCP_SELECTORS.stream(),
+                              Dhcp6HandlerImpl.DHCP_SELECTORS.stream())
+                .collect(Collectors.toList());
+    }
+
+    private class TestDhcp6RequestPacketContext extends PacketContextAdapter {
+
+
+        private InboundPacket inPacket;
+
+
+        public TestDhcp6RequestPacketContext(byte msgType, MacAddress clientMac, VlanId vlanId,
+                                            ConnectPoint clientCp,
+                                            Ip6Address clientGwAddr,
+                                            int relayLevel) {
+            super(0, null, null, false);
+
+            DHCP6 dhcp6 = new DHCP6();
+            if (relayLevel > 0) {
+                DHCP6 dhcp6Payload = new DHCP6();
+                buildDhcp6Packet(dhcp6Payload, msgType,
+                                 IP_FOR_CLIENT_V6,
+                        msgType == DHCP6.MsgType.REQUEST.value() ? PREFIX_FOR_ZERO : PREFIX_FOR_CLIENT_V6);
+                DHCP6 dhcp6Parent = null;
+                DHCP6 dhcp6Child = dhcp6Payload;
+                for (int i = 0; i < relayLevel; i++) {
+                    dhcp6Parent = new DHCP6();
+                    byte[] interfaceId = buildInterfaceId(clientMac, vlanId.toShort(), clientCp);
+                    buildRelayMsg(dhcp6Parent, DHCP6.MsgType.RELAY_FORW.value(),
+                            INTERFACE_IP_V6.ipAddress().getIp6Address(),
+                            OUTER_RELAY_IP_V6,
+                            (byte) (relayLevel - 1), interfaceId,
+                            dhcp6Child);
+                    dhcp6Child = dhcp6Parent;
+                }
+                if (dhcp6Parent != null) {
+                    dhcp6 = dhcp6Parent;
+                }
+            } else {
+                buildDhcp6Packet(dhcp6, msgType,
+                                        IP_FOR_CLIENT_V6,
+                        msgType == DHCP6.MsgType.REQUEST.value() ? PREFIX_FOR_ZERO : PREFIX_FOR_CLIENT_V6);
+            }
+
+            UDP udp = new UDP();
+            udp.setPayload(dhcp6);
+            if (relayLevel > 0) {
+                udp.setSourcePort(UDP.DHCP_V6_SERVER_PORT);
+            } else {
+                udp.setSourcePort(UDP.DHCP_V6_CLIENT_PORT);
+            }
+            udp.setDestinationPort(UDP.DHCP_V6_SERVER_PORT);
+
+            IPv6 ipv6 = new IPv6();
+            ipv6.setPayload(udp);
+            ipv6.setNextHeader(IPv6.PROTOCOL_UDP);
+            ipv6.setDestinationAddress(SERVER_IP_V6_MCAST.toOctets());
+            ipv6.setSourceAddress(clientGwAddr.toOctets());
+
+            Ethernet eth = new Ethernet();
+            if (relayLevel > 0) {
+                eth.setEtherType(Ethernet.TYPE_IPV6)
+                        .setVlanID(OUTER_RELAY_VLAN.toShort())
+                        .setSourceMACAddress(OUTER_RELAY_MAC)
+                        .setDestinationMACAddress(MacAddress.valueOf("33:33:00:01:00:02"))
+                        .setPayload(ipv6);
+            } else {
+                eth.setEtherType(Ethernet.TYPE_IPV6)
+                        .setVlanID(vlanId.toShort())
+                        .setSourceMACAddress(clientMac)
+                        .setDestinationMACAddress(MacAddress.valueOf("33:33:00:01:00:02"))
+                        .setPayload(ipv6);
+            }
+            this.inPacket = new DefaultInboundPacket(clientCp, eth,
+                                                     ByteBuffer.wrap(eth.serialize()));
+        }
+
+        @Override
+        public InboundPacket inPacket() {
+            return this.inPacket;
+        }
+    }
+
+    /**
+     * Generates DHCP6 REPLY  packet.
+     */
+
+    private class TestDhcp6ReplyPacketContext extends PacketContextAdapter {
+        private InboundPacket inPacket;
+
+        public TestDhcp6ReplyPacketContext(byte msgType, ConnectPoint clientCp, MacAddress clientMac,
+                                        VlanId clientVlan, Ip6Address clientGwAddr,
+                                        int relayLevel, boolean overWriteFlag, VlanId overWriteVlan) {
+            super(0, null, null, false);
+
+
+            DHCP6 dhcp6Payload = new DHCP6();
+            buildDhcp6Packet(dhcp6Payload, msgType,
+                    IP_FOR_CLIENT_V6,
+                    PREFIX_FOR_CLIENT_V6);
+            byte[] interfaceId = null;
+            if (relayLevel > 0) {
+                interfaceId = buildInterfaceId(OUTER_RELAY_MAC,
+                                               overWriteFlag ? overWriteVlan.toShort() : OUTER_RELAY_VLAN.toShort(),
+                                                OUTER_RELAY_CP);
+            } else {
+                interfaceId = buildInterfaceId(clientMac, clientVlan.toShort(), clientCp);
+            }
+            DHCP6 dhcp6 = new DHCP6();
+            buildRelayMsg(dhcp6, DHCP6.MsgType.RELAY_REPL.value(),
+                          INTERFACE_IP_V6.ipAddress().getIp6Address(),
+                          CLIENT_LL_IP_V6,
+                          (byte) 0, interfaceId,
+                          dhcp6Payload);
+
+            DHCP6 dhcp6Parent = null;
+            DHCP6 dhcp6Child = dhcp6;
+            for (int i = 0; i < relayLevel; i++) {   // relayLevel 0 : no relay
+                dhcp6Parent = new DHCP6();
+
+                buildRelayMsg(dhcp6Parent, DHCP6.MsgType.RELAY_REPL.value(),
+                        INTERFACE_IP_V6.ipAddress().getIp6Address(),
+                        OUTER_RELAY_IP_V6,
+                        (byte) relayLevel, interfaceId,
+                        dhcp6Child);
+
+                dhcp6Child = dhcp6Parent;
+            }
+            if (dhcp6Parent != null) {
+                dhcp6 = dhcp6Parent;
+            }
+
+
+            UDP udp = new UDP();
+            udp.setPayload(dhcp6);
+            udp.setSourcePort(UDP.DHCP_V6_SERVER_PORT);
+            udp.setDestinationPort(UDP.DHCP_V6_CLIENT_PORT);
+            IPv6 ipv6 = new IPv6();
+            ipv6.setPayload(udp);
+            ipv6.setNextHeader(IPv6.PROTOCOL_UDP);
+            ipv6.setDestinationAddress(IP_FOR_CLIENT_V6.toOctets());
+            ipv6.setSourceAddress(SERVER_IP_V6.toOctets());
+            Ethernet eth = new Ethernet();
+            if (relayLevel > 0) {
+                eth.setEtherType(Ethernet.TYPE_IPV6)
+                        .setVlanID(SERVER_VLAN.toShort())
+                        .setSourceMACAddress(SERVER_MAC)
+                        .setDestinationMACAddress(OUTER_RELAY_MAC)
+                        .setPayload(ipv6);
+            } else {
+                eth.setEtherType(Ethernet.TYPE_IPV6)
+                        .setVlanID(SERVER_VLAN.toShort())
+                        .setSourceMACAddress(SERVER_MAC)
+                        .setDestinationMACAddress(CLIENT_MAC)
+                        .setPayload(ipv6);
+            }
+
+            this.inPacket = new DefaultInboundPacket(SERVER_CONNECT_POINT, eth,
+                                                     ByteBuffer.wrap(eth.serialize()));
+
+        }
+
+        @Override
+        public InboundPacket inPacket() {
+            return this.inPacket;
+        }
+    }
+
+}
diff --git a/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/config/DhcpRelayConfigTest.java b/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/config/DhcpRelayConfigTest.java
new file mode 100644
index 0000000..c743247
--- /dev/null
+++ b/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/config/DhcpRelayConfigTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.io.Resources;
+import org.junit.Test;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onosproject.TestApplicationId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.ConnectPoint;
+
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+import static org.onosproject.dhcprelay.DhcpRelayManager.DHCP_RELAY_APP;
+
+/**
+ * Tests for DHCP relay app configuration.
+ */
+public class DhcpRelayConfigTest {
+    private static final String CONFIG_FILE_PATH = "dhcp-relay.json";
+    private static final String INVALID_CONFIG_FILE_PATH = "invalid-dhcp-relay.json";
+    private static final ApplicationId APP_ID = new TestApplicationId("DhcpRelayTest");
+    private static final ConnectPoint DEFAULT_CONNECT_POINT = ConnectPoint.deviceConnectPoint("of:0000000000000002/2");
+    private static final Ip4Address DEFAULT_SERVER_IP = Ip4Address.valueOf("172.168.10.2");
+    private static final Ip4Address DEFAULT_GATEWAY_IP = Ip4Address.valueOf("192.168.10.254");
+    private static final Ip6Address DEFAULT_SERVER_IP_V6 = Ip6Address.valueOf("2000::200:1");
+    private static final Ip6Address DEFAULT_GATEWAY_IP_V6 = Ip6Address.valueOf("1000::100:1");
+    private static final ConnectPoint INDIRECT_CONNECT_POINT = ConnectPoint.deviceConnectPoint("of:0000000000000002/3");
+    private static final Ip4Address INDIRECT_SERVER_IP = Ip4Address.valueOf("172.168.10.3");
+
+    @Test
+    public void testDefaultConfig() throws IOException {
+        ObjectMapper om = new ObjectMapper();
+        JsonNode json = om.readTree(Resources.getResource(CONFIG_FILE_PATH));
+        DefaultDhcpRelayConfig config = new DefaultDhcpRelayConfig();
+        json = json.path("apps").path(DHCP_RELAY_APP).path(DefaultDhcpRelayConfig.KEY);
+        config.init(APP_ID, DefaultDhcpRelayConfig.KEY, json, om, null);
+
+        assertEquals(1, config.dhcpServerConfigs().size());
+        DhcpServerConfig serverConfig = config.dhcpServerConfigs().get(0);
+        assertEquals(DEFAULT_CONNECT_POINT, serverConfig.getDhcpServerConnectPoint().orElse(null));
+        assertEquals(DEFAULT_SERVER_IP, serverConfig.getDhcpServerIp4().orElse(null));
+        assertEquals(DEFAULT_GATEWAY_IP, serverConfig.getDhcpGatewayIp4().orElse(null));
+        assertEquals(DEFAULT_SERVER_IP_V6, serverConfig.getDhcpServerIp6().orElse(null));
+        assertEquals(DEFAULT_GATEWAY_IP_V6, serverConfig.getDhcpGatewayIp6().orElse(null));
+    }
+
+    @Test
+    public void testIndirectConfig() throws IOException {
+        ObjectMapper om = new ObjectMapper();
+        JsonNode json = om.readTree(Resources.getResource(CONFIG_FILE_PATH));
+        IndirectDhcpRelayConfig config = new IndirectDhcpRelayConfig();
+        json = json.path("apps").path(DHCP_RELAY_APP).path(IndirectDhcpRelayConfig.KEY);
+        config.init(APP_ID, IndirectDhcpRelayConfig.KEY, json, om, null);
+
+        assertEquals(1, config.dhcpServerConfigs().size());
+        DhcpServerConfig serverConfig = config.dhcpServerConfigs().get(0);
+        assertEquals(INDIRECT_CONNECT_POINT, serverConfig.getDhcpServerConnectPoint().orElse(null));
+        assertEquals(INDIRECT_SERVER_IP, serverConfig.getDhcpServerIp4().orElse(null));
+        assertNull(serverConfig.getDhcpGatewayIp4().orElse(null));
+        assertNull(serverConfig.getDhcpServerIp6().orElse(null));
+        assertNull(serverConfig.getDhcpGatewayIp6().orElse(null));
+    }
+
+    @Test
+    public void testInvalidConfig() throws IOException {
+        ObjectMapper om = new ObjectMapper();
+        JsonNode json = om.readTree(Resources.getResource(INVALID_CONFIG_FILE_PATH));
+        DefaultDhcpRelayConfig config = new DefaultDhcpRelayConfig();
+        json = json.path("apps").path(DHCP_RELAY_APP).path(DefaultDhcpRelayConfig.KEY);
+        config.init(APP_ID, DefaultDhcpRelayConfig.KEY, json, om, null);
+        assertFalse(config.isValid());
+    }
+}
diff --git a/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/config/IgnoreDhcpConfigTest.java b/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/config/IgnoreDhcpConfigTest.java
new file mode 100644
index 0000000..3712c06
--- /dev/null
+++ b/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/config/IgnoreDhcpConfigTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.io.Resources;
+import org.junit.Test;
+import org.onlab.packet.VlanId;
+import org.onosproject.TestApplicationId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.DeviceId;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.onosproject.dhcprelay.DhcpRelayManager.DHCP_RELAY_APP;
+
+public class IgnoreDhcpConfigTest {
+    private static final String CONFIG_FILE_PATH = "dhcp-relay.json";
+    private static final ApplicationId APP_ID = new TestApplicationId("DhcpRelayTest");
+    private static final DeviceId DEV_1_ID = DeviceId.deviceId("of:0000000000000001");
+    private static final DeviceId DEV_2_ID = DeviceId.deviceId("of:0000000000000002");
+    private static final VlanId IGNORED_VLAN = VlanId.vlanId("100");
+    @Test
+    public void testIgnoredDhcpConfig() throws IOException {
+        ObjectMapper om = new ObjectMapper();
+        JsonNode json = om.readTree(Resources.getResource(CONFIG_FILE_PATH));
+        IgnoreDhcpConfig config = new IgnoreDhcpConfig();
+        json = json.path("apps").path(DHCP_RELAY_APP).path(IgnoreDhcpConfig.KEY);
+        config.init(APP_ID, IgnoreDhcpConfig.KEY, json, om, null);
+
+        assertEquals(2, config.ignoredVlans().size());
+        Collection<VlanId> vlanForDev1 = config.ignoredVlans().get(DEV_1_ID);
+        Collection<VlanId> vlanForDev2 = config.ignoredVlans().get(DEV_2_ID);
+
+        assertEquals(1, vlanForDev1.size());
+        assertEquals(1, vlanForDev2.size());
+
+        assertTrue(vlanForDev1.contains(IGNORED_VLAN));
+        assertTrue(vlanForDev2.contains(IGNORED_VLAN));
+    }
+}
diff --git a/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/store/DhcpRecordTest.java b/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/store/DhcpRecordTest.java
new file mode 100644
index 0000000..7aaf65b
--- /dev/null
+++ b/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/store/DhcpRecordTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.store;
+
+import org.junit.Test;
+import org.onlab.junit.TestUtils;
+import org.onlab.packet.DHCP;
+import org.onlab.packet.DHCP6;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+
+import java.util.Optional;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Unit test for DHCP record.
+ */
+public class DhcpRecordTest {
+    private static final MacAddress MAC = MacAddress.valueOf("1a:1a:1a:1a:1a:1a");
+    private static final VlanId VLAN = VlanId.vlanId("100");
+    private static final HostId HOST_ID = HostId.hostId(MAC, VLAN);
+    private static final HostLocation HL1 =
+            new HostLocation(ConnectPoint.deviceConnectPoint("of:0000000000000001/1"), 0);
+    private static final HostLocation HL2 =
+            new HostLocation(ConnectPoint.deviceConnectPoint("of:0000000000000001/2"), 0);
+    private static final Ip4Address IP4ADDR = Ip4Address.valueOf("10.0.2.1");
+    private static final MacAddress GW_MAC = MacAddress.valueOf("00:00:00:00:04:01");
+    private static final Ip6Address IP6ADDR = Ip6Address.valueOf("2001::1");
+
+    /**
+     * Test creating a DHCP relay record.
+     */
+    @Test
+    public void testCreateRecord() {
+        DhcpRecord record = new DhcpRecord(HOST_ID)
+                .addLocation(HL1)
+                .addLocation(HL2)
+                .ip4Address(IP4ADDR)
+                .nextHop(GW_MAC)
+                .ip4Status(DHCP.MsgType.DHCPACK)
+                .ip6Address(IP6ADDR)
+                .ip6Status(DHCP6.MsgType.REPLY)
+                .setDirectlyConnected(true);
+
+        assertThat(record.locations().size(), is(2));
+        assertThat(record.locations(), containsInAnyOrder(HL1, HL2));
+        assertThat(record.ip4Address(), is(Optional.of(IP4ADDR)));
+        assertThat(record.nextHop(), is(Optional.of(GW_MAC)));
+        assertThat(record.ip4Status(), is(Optional.of(DHCP.MsgType.DHCPACK)));
+        assertThat(record.ip6Address(), is(Optional.of(IP6ADDR)));
+        assertThat(record.ip6Status(), is(Optional.of(DHCP6.MsgType.REPLY)));
+        assertThat(record.directlyConnected(), is(true));
+
+        DhcpRecord record2 = new DhcpRecord(HOST_ID)
+                .nextHop(GW_MAC)
+                .addLocation(HL2)
+                .ip6Address(IP6ADDR)
+                .addLocation(HL1)
+                .ip6Status(DHCP6.MsgType.REPLY)
+                .ip4Address(IP4ADDR)
+                .ip4Status(DHCP.MsgType.DHCPACK)
+                .setDirectlyConnected(true);
+
+        TestUtils.setField(record, "lastSeen", 0);
+        TestUtils.setField(record2, "lastSeen", 0);
+        TestUtils.setField(record, "addrPrefTime", 0);
+        TestUtils.setField(record2, "addrPrefTime", 0);
+        TestUtils.setField(record, "pdPrefTime", 0);
+        TestUtils.setField(record2, "pdPrefTime", 0);
+        TestUtils.setField(record, "v6Counter", null);
+        TestUtils.setField(record2, "v6Counter", null);
+
+        assertThat(record, equalTo(record2));
+        assertThat(record.hashCode(), equalTo(record2.hashCode()));
+    }
+
+    /**
+     * Test clone a DHCP record.
+     */
+    @Test
+    public void testCloneRecord() {
+        DhcpRecord record = new DhcpRecord(HOST_ID)
+                .addLocation(HL1)
+                .addLocation(HL2)
+                .ip4Address(IP4ADDR)
+                .nextHop(GW_MAC)
+                .ip4Status(DHCP.MsgType.DHCPACK)
+                .ip6Address(IP6ADDR)
+                .ip6Status(DHCP6.MsgType.REPLY)
+                .setDirectlyConnected(true);
+        DhcpRecord clonedRecord = record.clone();
+        assertEquals(record, clonedRecord);
+    }
+}
diff --git a/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/store/DistributedDhcpRelayStoreTest.java b/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/store/DistributedDhcpRelayStoreTest.java
new file mode 100644
index 0000000..81491d0
--- /dev/null
+++ b/apps/dhcprelay/app/src/test/java/org/onosproject/dhcprelay/store/DistributedDhcpRelayStoreTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.store;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.store.service.TestStorageService;
+
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.onlab.packet.DHCP.MsgType.DHCPREQUEST;
+
+public class DistributedDhcpRelayStoreTest {
+    private static final ConnectPoint CP = ConnectPoint.deviceConnectPoint("of:1/1");
+    private static final MacAddress MAC = MacAddress.valueOf("00:00:00:00:00:01");
+    private static final VlanId VLAN_ID = VlanId.vlanId("100");
+    private static final HostId HOST_ID = HostId.hostId(MAC, VLAN_ID);
+    private static final Ip4Address IP = Ip4Address.valueOf("192.168.1.10");
+    private static final MacAddress GW_MAC = MacAddress.valueOf("00:00:00:00:01:01");
+    private DistributedDhcpRelayStore store;
+
+    @Before
+    public void setup() {
+        store = new DistributedDhcpRelayStore();
+        store.storageService = new TestStorageService();
+        store.activated();
+    }
+
+    @After
+    public void teerDown() {
+        store.deactivated();
+    }
+
+    /**
+     * Puts and removes a record, should received UPDATED and REMOVED event.
+     */
+    @Test
+    public void testPutAndRemoveRecord() {
+        // dhcp request, no IP
+        HostId hostId = HostId.hostId(MAC, VLAN_ID);
+        DhcpRecord record = new DhcpRecord(hostId);
+        record.addLocation(new HostLocation(CP, System.currentTimeMillis()));
+        record.setDirectlyConnected(true);
+        record.nextHop(GW_MAC);
+        record.ip4Status(DHCPREQUEST);
+
+        CompletableFuture<DhcpRelayStoreEvent> recordComplete = new CompletableFuture<>();
+        store.setDelegate(recordComplete::complete);
+        store.updateDhcpRecord(HOST_ID, record);
+        DhcpRelayStoreEvent event = recordComplete.join();
+        assertEquals(record, event.subject());
+        assertEquals(DhcpRelayStoreEvent.Type.UPDATED, event.type());
+        DhcpRecord recordInStore = store.getDhcpRecord(HOST_ID).orElse(null);
+        assertNotNull(recordInStore);
+        assertEquals(record, recordInStore);
+        Collection<DhcpRecord> recordsInStore = store.getDhcpRecords();
+        assertEquals(1, recordsInStore.size());
+        assertEquals(record, recordsInStore.iterator().next());
+
+        // dhcp request, with IP
+        record = new DhcpRecord(hostId);
+        record.addLocation(new HostLocation(CP, System.currentTimeMillis()));
+        record.setDirectlyConnected(true);
+        record.ip4Address(IP);
+        record.nextHop(GW_MAC);
+        record.ip4Status(DHCPREQUEST);
+
+        recordComplete = new CompletableFuture<>();
+        store.setDelegate(recordComplete::complete);
+        store.updateDhcpRecord(HOST_ID, record);
+        event = recordComplete.join();
+        DhcpRecord subject = event.subject();
+        assertEquals(record.locations(), subject.locations());
+        assertEquals(record.vlanId(), subject.vlanId());
+        assertEquals(record.macAddress(), subject.macAddress());
+        assertEquals(record.ip4Address(), subject.ip4Address());
+        assertEquals(record.nextHop(), subject.nextHop());
+        assertEquals(record.ip4Status(), subject.ip4Status());
+        assertEquals(record.ip6Address(), subject.ip6Address());
+        assertEquals(record.ip6Status(), subject.ip6Status());
+        assertEquals(record.directlyConnected(), subject.directlyConnected());
+
+        assertEquals(DhcpRelayStoreEvent.Type.UPDATED, event.type());
+        recordInStore = store.getDhcpRecord(HOST_ID).orElse(null);
+        assertNotNull(recordInStore);
+        assertEquals(record.locations(), recordInStore.locations());
+        assertEquals(record.vlanId(), recordInStore.vlanId());
+        assertEquals(record.macAddress(), recordInStore.macAddress());
+        assertEquals(record.ip4Address(), recordInStore.ip4Address());
+        assertEquals(record.nextHop(), recordInStore.nextHop());
+        assertEquals(record.ip4Status(), recordInStore.ip4Status());
+        assertEquals(record.ip6Address(), recordInStore.ip6Address());
+        assertEquals(record.ip6Status(), recordInStore.ip6Status());
+        assertEquals(record.directlyConnected(), recordInStore.directlyConnected());
+        recordsInStore = store.getDhcpRecords();
+        assertEquals(1, recordsInStore.size());
+
+        // removes record
+        recordComplete = new CompletableFuture<>();
+        store.setDelegate(recordComplete::complete);
+        DhcpRecord removedRecord = store.removeDhcpRecord(HOST_ID).orElse(null);
+        assertEquals(record.locations(), removedRecord.locations());
+        assertEquals(record.vlanId(), removedRecord.vlanId());
+        assertEquals(record.macAddress(), removedRecord.macAddress());
+        assertEquals(record.ip4Address(), removedRecord.ip4Address());
+        assertEquals(record.nextHop(), removedRecord.nextHop());
+        assertEquals(record.ip4Status(), removedRecord.ip4Status());
+        assertEquals(record.ip6Address(), removedRecord.ip6Address());
+        assertEquals(record.ip6Status(), removedRecord.ip6Status());
+        assertEquals(record.directlyConnected(), removedRecord.directlyConnected());
+        event = recordComplete.join();
+        assertEquals(record, event.subject());
+        assertEquals(DhcpRelayStoreEvent.Type.REMOVED, event.type());
+        recordInStore = store.getDhcpRecord(HOST_ID).orElse(null);
+        assertNull(recordInStore);
+        recordsInStore = store.getDhcpRecords();
+        assertEquals(0, recordsInStore.size());
+    }
+}
diff --git a/apps/dhcprelay/app/src/test/resources/dhcp-relay.json b/apps/dhcprelay/app/src/test/resources/dhcp-relay.json
new file mode 100644
index 0000000..fe5da9c
--- /dev/null
+++ b/apps/dhcprelay/app/src/test/resources/dhcp-relay.json
@@ -0,0 +1,39 @@
+{
+  "apps": {
+    "org.onosproject.dhcprelay" : {
+      "default": [
+        {
+          "dhcpServerConnectPoint": "of:0000000000000002/2",
+          "serverIps": ["172.168.10.2", "2000::200:1"],
+          "gatewayIps": ["192.168.10.254", "1000::100:1"],
+          "relayAgentIps": {
+            "of:0000000000000001": {
+              "ipv4": "10.0.0.10",
+              "ipv6": "2000::10"
+            },
+            "of:0000000000000002": {
+              "ipv4": "10.0.1.10",
+              "ipv6": "2000::1:10"
+            }
+          }
+        }
+      ],
+      "indirect": [
+        {
+          "dhcpServerConnectPoint": "of:0000000000000002/3",
+          "serverIps": ["172.168.10.3"],
+          "relayAgentIps": {
+            "of:0000000000000001": {
+              "ipv4": "10.0.0.10",
+              "ipv6": "2000::10"
+            }
+          }
+        }
+      ],
+      "ignoreDhcp": [
+        {"deviceId": "of:0000000000000001", "vlan": 100},
+        {"deviceId": "of:0000000000000002", "vlan": 100}
+      ]
+    }
+  }
+}
diff --git a/apps/dhcprelay/app/src/test/resources/invalid-dhcp-relay.json b/apps/dhcprelay/app/src/test/resources/invalid-dhcp-relay.json
new file mode 100644
index 0000000..2c67eaa
--- /dev/null
+++ b/apps/dhcprelay/app/src/test/resources/invalid-dhcp-relay.json
@@ -0,0 +1,12 @@
+{
+  "apps": {
+    "org.onosproject.dhcprelay" : {
+      "default": [
+        {
+          "dhcpServerConnectPoint": "of:0000000000000002/2",
+          "gatewayIps": ["192.168.10.254", "1000::100:1"]
+        }
+      ]
+    }
+  }
+}