DHCPv6 Lease Query

Change-Id: I842deb7a09400504bec239fe49d2a627f00756c1
diff --git a/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerImpl.java b/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerImpl.java
index 09a118c..80d29fc 100644
--- a/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerImpl.java
+++ b/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerImpl.java
@@ -39,8 +39,11 @@
 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;
@@ -102,8 +105,6 @@
 import org.onosproject.net.flow.DefaultTrafficTreatment;
 import org.onosproject.net.flow.TrafficTreatment;
 import org.onosproject.dhcprelay.Dhcp6HandlerUtil.InternalPacket;
-
-
 import java.nio.ByteBuffer;
 import java.util.List;
 import java.util.Collection;
@@ -117,6 +118,7 @@
 import static org.onosproject.net.flowobjective.Objective.Operation.REMOVE;
 import java.util.concurrent.Semaphore;
 
+
 @Component
 @Service
 @Property(name = "version", value = "6")
@@ -139,9 +141,18 @@
             .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
+            SERVER_RELAY_SELECTOR,
+            LEASE_QUERY_RESPONSE_SELECTOR
     );
     private static Logger log = LoggerFactory.getLogger(Dhcp6HandlerImpl.class);
 
@@ -182,11 +193,8 @@
     protected ApplicationId appId;
     protected Multimap<DeviceId, VlanId> ignoredVlans = HashMultimap.create();
     private InternalHostListener hostListener = new InternalHostListener();
-
     private Boolean dhcpFpmEnabled = false;
-
     private Dhcp6HandlerUtil dhcp6HandlerUtil = new Dhcp6HandlerUtil();
-
     private List<DhcpServerInfo> defaultServerInfoList = Lists.newArrayList();
     private List<DhcpServerInfo> indirectServerInfoList = Lists.newArrayList();
     private class IpAddressInfo {
@@ -211,10 +219,12 @@
                             DHCP6.MsgType.RELEASE.value(),
                             DHCP6.MsgType.DECLINE.value(),
                             DHCP6.MsgType.CONFIRM.value(),
-                            DHCP6.MsgType.RELAY_FORW.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());
+            ImmutableSet.of(DHCP6.MsgType.RELAY_REPL.value(),
+                            DHCP6.MsgType.LEASEQUERY_REPLY.value());
 
     @Activate
     protected void activate() {
@@ -267,7 +277,6 @@
             }
             processIgnoreVlanRule(deviceId, vlanId, ADD);
         });
-
         ignoredVlans.forEach((deviceId, vlanId) -> {
             if (!config.ignoredVlans().get(deviceId).contains(vlanId)) {
                 // not contains in new config, remove it
@@ -287,6 +296,160 @@
         });
     }
 
+    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.STATIC, 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");
@@ -295,7 +458,8 @@
         Ethernet receivedPacket = context.inPacket().parsed();
 
         if (!configured()) {
-            log.warn("Missing DHCP6 relay server config. Abort packet processing dhcp6 payload {}", dhcp6Payload);
+            log.warn("Missing DHCP6 relay server config. " +
+                             "Abort packet processing dhcp6 payload {}", dhcp6Payload);
             return;
         }
         byte msgTypeVal = dhcp6Payload.getMsgType();
@@ -314,22 +478,47 @@
             return;
         }
 
-        if (MSG_TYPE_FROM_CLIENT.contains(msgTypeVal)) {
-
+        if (msgTypeVal == DHCP6.MsgType.LEASEQUERY.value()) {
             List<InternalPacket> ethernetClientPacket =
-                    processDhcp6PacketFromClient(context, receivedPacket, receivingInterfaces);
+                    processLQ6PacketFromClient(context, receivedPacket, receivingInterfaces, dhcp6Payload);
             for (InternalPacket internalPacket : ethernetClientPacket) {
                 forwardPacket(internalPacket);
             }
-        } else if (MSG_TYPE_FROM_SERVER.contains(msgTypeVal)) {
-            log.debug("calling processDhcp6PacketFromServer with RELAY_REPL {}", msgTypeVal);
+        } else if (msgTypeVal == DHCP6.MsgType.LEASEQUERY_REPLY.value()) {
+
+            IPv6 clientIpv6 = (IPv6) receivedPacket.getPayload();
+            UDP clientUdp = (UDP) clientIpv6.getPayload();
+            DHCP6 clientDhcp6 = (DHCP6) clientUdp.getPayload();
+            Interface serverInterface = dhcp6HandlerUtil.directlyConnected(clientDhcp6) ?
+                    getServerInterface() : getIndirectServerInterface();
             InternalPacket ethernetPacketReply =
-                    processDhcp6PacketFromServer(context, receivedPacket, receivingInterfaces);
+                    dhcp6HandlerUtil.processLQ6PacketFromServer(
+                            defaultServerInfoList, indirectServerInfoList,
+                            serverInterface, interfaceService,
+                            hostService,
+                            context, receivedPacket, receivingInterfaces);
             if (ethernetPacketReply != null) {
                 forwardPacket(ethernetPacketReply);
             }
+            handleLeaseQuery6ReplyMsg(context, dhcp6Payload);
         } else {
-            log.warn("Not so fast, packet type {} not supported yet", msgTypeVal);
+            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 {}", msgTypeVal);
+                InternalPacket ethernetPacketReply =
+                        processDhcp6PacketFromServer(context, receivedPacket, receivingInterfaces);
+                if (ethernetPacketReply != null) {
+                    forwardPacket(ethernetPacketReply);
+                }
+            } else {
+                log.warn("Not so fast, packet type {} not supported yet", msgTypeVal);
+            }
         }
     }
 
@@ -352,8 +541,6 @@
         // 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.
@@ -369,9 +556,6 @@
         } // if
     }
 
-
-
-
     /**
      * extract from dhcp6 packet client ipv6 address of given by dhcp server.
      *
@@ -422,6 +606,7 @@
         }
         return ipInfo;
     }
+
     /**
      * extract from dhcp6 packet Prefix prefix provided by dhcp server.
      *
@@ -500,6 +685,7 @@
 
         return clientIdOption;
     }
+
     /**
      * remove host or route and update dhcp relay record attributes.
      *
@@ -661,6 +847,7 @@
      * @param embeddedDhcp6 the dhcp6 payload within relay
      * @param srcMac client gw/host macAddress
      * @param clientInterface client interface
+     * @param dhcpServerIp6Address DHCP server IP
      */
     private void addHostOrRoute(boolean directConnFlag, ConnectPoint location, DHCP6 dhcp6Relay,
                                 DHCP6 embeddedDhcp6, MacAddress srcMac, Interface clientInterface) {
@@ -774,11 +961,11 @@
         if (leafMsgType == DHCP6.MsgType.REPLY.value()) {
             if (ipInfo != null) {
                 log.debug("IP6 address is being stored into dhcp-relay store.");
-                log.debug("IP6 address {}", HexString.toHexString(ipInfo.ip6Address.toOctets(), ":"));
+                log.debug("Client IP6 address {}", HexString.toHexString(ipInfo.ip6Address.toOctets(), ":"));
                 record.ip6Address(ipInfo.ip6Address);
                 record.updateAddrPrefTime(ipInfo.prefTime);
                 record.updateLastIp6Update();
-            } else {
+             } else {
                 log.debug("IP6 address is not returned from server. Maybe only PD is returned.");
             }
             if (pdInfo != null) {
@@ -811,6 +998,103 @@
         }
     }
 
+    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.STATIC, 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);
+
+        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 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 =
+                    new Dhcp6HandlerUtil().new 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.
      *
@@ -980,7 +1264,9 @@
             udpPacket.setDestinationPort(UDP.DHCP_V6_SERVER_PORT);
         }
         // add host or route
-        addHostOrRoute(directConnFlag, clientConnectionPoint, dhcp6Relay, embeddedDhcp6, clientMac, clientInterface);
+        addHostOrRoute(directConnFlag, clientConnectionPoint, dhcp6Relay, embeddedDhcp6,
+                       clientMac, clientInterface);
+
 
         udpPacket.setPayload(embeddedDhcp6);
         udpPacket.resetChecksum();
@@ -989,6 +1275,19 @@
         return new Dhcp6HandlerUtil().new InternalPacket(etherReply, clientConnectionPoint);
     }
 
+    // Returns the first v6 interface ip out of a set of interfaces or null.
+    // Checks all interfaces, and ignores v6 interface ips
+    private 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;
+    }
 
     @Override
     public void setDhcpFpmEnabled(Boolean enabled) {
@@ -1131,7 +1430,6 @@
             }
         }
     }
-
     /**
      * Handle host removed.
      * If the host is DHCP server or gateway, unset connect mac and vlan.
@@ -1181,6 +1479,65 @@
                 .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 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.
      *
@@ -1256,15 +1613,16 @@
         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) {
@@ -1277,6 +1635,16 @@
                                     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)
@@ -1287,6 +1655,16 @@
                                      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 =
@@ -1419,6 +1797,7 @@
             flowObjectiveService.apply(deviceId, fwd);
         });
     }
+
     /**
      * Find first ipaddress for a given Host info i.e.  mac and vlan.
      *
@@ -1481,6 +1860,7 @@
         }
         return foundServerInfo;
     }
+
     /**
      * Set the dhcp6 lease expiry poll interval value.
      *
diff --git a/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerUtil.java b/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerUtil.java
index 6e47a0d..17456f9 100644
--- a/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerUtil.java
+++ b/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/Dhcp6HandlerUtil.java
@@ -44,8 +44,11 @@
 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;
 
@@ -69,6 +72,127 @@
     }
 
     /**
+     *
+     * 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 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 || (!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());
+
+
+        // add host or route
+        //addHostOrRoute(directConnFlag, clientConnectionPoint, lq6Reply, embeddedDhcp6, clientMac, clientInterface);
+        // 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 new Dhcp6HandlerUtil().new InternalPacket(etherReply, clientConnectionPoint);
+    }
+    /**
      * Returns the first interface ip from interface.
      *
      * @param iface interface of one connect point
@@ -258,7 +382,14 @@
      * @return true if the host is directly connected to the network; false otherwise
      */
     public 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()) {
diff --git a/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/store/DhcpRecord.java b/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/store/DhcpRecord.java
index 44c6c3c..e4c2673 100644
--- a/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/store/DhcpRecord.java
+++ b/apps/dhcprelay/src/main/java/org/onosproject/dhcprelay/store/DhcpRecord.java
@@ -53,7 +53,6 @@
     private IpPrefix pdPrefix;
     private DHCP6.MsgType ip6Status;
 
-
     private long lastSeen;
     private long lastIp6Update;
     private long lastPdUpdate;
@@ -418,7 +417,6 @@
         newRecord.addrPrefTime = addrPrefTime;
         newRecord.pdPrefTime = pdPrefTime;
         newRecord.v6Counters = v6Counters;
-
         return newRecord;
     }
 
diff --git a/utils/misc/src/main/java/org/onlab/packet/DHCP6.java b/utils/misc/src/main/java/org/onlab/packet/DHCP6.java
index 8dbd1dc..50b11a1 100644
--- a/utils/misc/src/main/java/org/onlab/packet/DHCP6.java
+++ b/utils/misc/src/main/java/org/onlab/packet/DHCP6.java
@@ -19,11 +19,13 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
+import org.onlab.packet.dhcp.Dhcp6ClientDataOption;
 import org.onlab.packet.dhcp.Dhcp6ClientIdOption;
 import org.onlab.packet.dhcp.Dhcp6IaAddressOption;
 import org.onlab.packet.dhcp.Dhcp6IaNaOption;
 import org.onlab.packet.dhcp.Dhcp6IaTaOption;
 import org.onlab.packet.dhcp.Dhcp6IaPdOption;
+import org.onlab.packet.dhcp.Dhcp6LeaseQueryOption;
 import org.onlab.packet.dhcp.Dhcp6Option;
 import org.onlab.packet.dhcp.Dhcp6RelayOption;
 import org.onlab.packet.dhcp.Dhcp6InterfaceIdOption;
@@ -60,7 +62,12 @@
     // Relay message types
     public static final Set<Byte> RELAY_MSG_TYPES =
             ImmutableSet.of(MsgType.RELAY_FORW.value,
-                            MsgType.RELAY_REPL.value);
+                            MsgType.RELAY_REPL.value
+            );
+    public static final Set<Byte> LEASEQUERY_MSG_TYPES =
+            ImmutableSet.of(MsgType.LEASEQUERY.value,
+                            MsgType.LEASEQUERY_REPLY.value
+            );
 
     /**
      * DHCPv6 message type.
@@ -70,7 +77,8 @@
         CONFIRM((byte) 4), RENEW((byte) 5), REBIND((byte) 6),
         REPLY((byte) 7), RELEASE((byte) 8), DECLINE((byte) 9),
         RECONFIGURE((byte) 10), INFORMATION_REQUEST((byte) 11),
-        RELAY_FORW((byte) 12), RELAY_REPL((byte) 13);
+        RELAY_FORW((byte) 12), RELAY_REPL((byte) 13), LEASEQUERY((byte) 14),
+        LEASEQUERY_REPLY((byte) 15);
 
         protected byte value;
         MsgType(final byte value) {
@@ -107,6 +115,10 @@
                     return RELAY_FORW;
                 case 13:
                     return RELAY_REPL;
+                case 14:
+                    return LEASEQUERY;
+                case 15:
+                    return LEASEQUERY_REPLY;
                 default:
                     return null;
             }
@@ -155,7 +167,8 @@
         STATUS_CODE((short) 13), RAPID_COMMIT((short) 14), USER_CLASS((short) 15),
         VENDOR_CLASS((short) 16), VENDOR_OPTS((short) 17), INTERFACE_ID((short) 18),
         RECONF_MSG((short) 19), RECONF_ACCEPT((short) 20), IA_PD((short) 25), IAPREFIX((short) 26),
-        SUBSCRIBER_ID((short) 38);
+        SUBSCRIBER_ID((short) 38), OPTION_ERO((short) 43), LEASE_QUERY((short) 44),
+        CLIENT_DATA((short) 45),  CLIENT_LT((short) 48);
 
         protected short value;
         OptionCode(final short value) {
@@ -175,6 +188,8 @@
                             .put(OptionCode.CLIENTID.value, Dhcp6ClientIdOption.deserializer())
                             .put(OptionCode.IA_PD.value, Dhcp6IaPdOption.deserializer())
                             .put(OptionCode.INTERFACE_ID.value, Dhcp6InterfaceIdOption.deserializer())
+                            .put(OptionCode.LEASE_QUERY.value, Dhcp6LeaseQueryOption.deserializer())
+                            .put(OptionCode.CLIENT_DATA.value, Dhcp6ClientDataOption.deserializer())
                     .build();
 
     // general field
diff --git a/utils/misc/src/main/java/org/onlab/packet/dhcp/Dhcp6CLTOption.java b/utils/misc/src/main/java/org/onlab/packet/dhcp/Dhcp6CLTOption.java
new file mode 100644
index 0000000..c509ee3
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/dhcp/Dhcp6CLTOption.java
@@ -0,0 +1,141 @@
+/*
+ * 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.onlab.packet.dhcp;
+
+import org.onlab.packet.DHCP6;
+import org.onlab.packet.DeserializationException;
+import org.onlab.packet.Deserializer;
+
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+public final class Dhcp6CLTOption extends Dhcp6Option {
+    public static final int DEFAULT_LEN = 4;
+    private int clt; // client last transaction time
+
+    @Override
+    public short getCode() {
+        return DHCP6.OptionCode.CLIENT_LT.value();
+    }
+
+    @Override
+    public short getLength() {
+        return (short) (DEFAULT_LEN);
+    }
+
+    /**
+     * Gets Client Last Transaction Time.
+     *
+     * @return Client Last Transaction Time
+     */
+    public int getClt() {
+        return clt;
+    }
+
+    /**
+     * Sets Identity Association ID.
+     *
+     * @param clt the Client Last Transaction Time.
+     */
+    public void setClt(int clt) {
+        this.clt = clt;
+    }
+
+
+    /**
+     * Default constructor.
+     */
+    public Dhcp6CLTOption() {
+    }
+
+    /**
+     * Constructs a DHCPv6  Client Last Transaction Time option.
+     *
+     * @param dhcp6Option the DHCPv6 option
+     */
+    public Dhcp6CLTOption(Dhcp6Option dhcp6Option) {
+        super(dhcp6Option);
+    }
+
+    /**
+     * Gets deserializer.
+     *
+     * @return the deserializer
+     */
+    public static Deserializer<Dhcp6Option> deserializer() {
+        return (data, offset, length) -> {
+            Dhcp6Option dhcp6Option =
+                    Dhcp6Option.deserializer().deserialize(data, offset, length);
+            if (dhcp6Option.getLength() < DEFAULT_LEN) {
+                throw new DeserializationException("Invalid CLT option data");
+            }
+            Dhcp6CLTOption cltOption = new Dhcp6CLTOption(dhcp6Option);
+            byte[] optionData = cltOption.getData();
+            ByteBuffer bb = ByteBuffer.wrap(optionData);
+            cltOption.clt = bb.getInt();
+
+            return cltOption;
+        };
+    }
+
+    @Override
+    public byte[] serialize() {
+        int payloadLen = DEFAULT_LEN;
+        int len = Dhcp6Option.DEFAULT_LEN + payloadLen;
+        ByteBuffer bb = ByteBuffer.allocate(len);
+        bb.putShort(DHCP6.OptionCode.CLIENT_LT.value());
+        bb.putShort((short) payloadLen);
+        bb.putInt(clt);
+        return bb.array();
+    }
+
+
+    @Override
+    public String toString() {
+        return getToStringHelper()
+                .add("clt", clt)
+                .toString();
+    }
+
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), clt);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof Dhcp6CLTOption)) {
+            return false;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        final Dhcp6CLTOption other = (Dhcp6CLTOption) obj;
+
+        return Objects.equals(getCode(), other.getCode()) &&
+                Objects.equals(getLength(), other.getLength()) &&
+                Objects.equals(clt, other.clt);
+    }
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/dhcp/Dhcp6ClientDataOption.java b/utils/misc/src/main/java/org/onlab/packet/dhcp/Dhcp6ClientDataOption.java
new file mode 100644
index 0000000..01b4cc3
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/dhcp/Dhcp6ClientDataOption.java
@@ -0,0 +1,161 @@
+/*
+ * 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.onlab.packet.dhcp;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Lists;
+import org.onlab.packet.DHCP6;
+import org.onlab.packet.DeserializationException;
+import org.onlab.packet.Deserializer;
+import org.onlab.packet.Ip6Address;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * DHCPv6 Client Data Option.
+ */
+public final class Dhcp6ClientDataOption extends Dhcp6Option {
+    private List<Dhcp6Option> options;
+    private Ip6Address clientIaAddress;
+    public static final int DEFAULT_LEN = 1 + 16;
+
+    public Dhcp6ClientDataOption(Dhcp6Option dhcp6Option) {
+        super(dhcp6Option);
+    }
+
+    @Override
+    public short getCode() {
+        return DHCP6.OptionCode.CLIENT_DATA.value();
+    }
+
+    @Override
+    public short getLength() {
+        //return (short) (DEFAULT_LEN + options.stream()
+        //        .mapToInt(opt -> (int) opt.getLength() + Dhcp6Option.DEFAULT_LEN)
+        //        .sum());
+        return (short) payload.serialize().length;
+    }
+
+    @Override
+    public byte[] getData() {
+        return payload.serialize();
+    }
+
+    public List<Dhcp6Option> getOptions() {
+        return options;
+    }
+
+    public Ip6Address getIaAddress() {
+        return clientIaAddress;
+    }
+
+    public static Deserializer<Dhcp6Option> deserializer() {
+        return (data, offset, length) -> {
+            Dhcp6Option dhcp6Option = Dhcp6Option.deserializer().deserialize(data, offset, length);
+            Dhcp6ClientDataOption clientData = new Dhcp6ClientDataOption(dhcp6Option);
+
+            if (dhcp6Option.getLength() < DEFAULT_LEN) {
+                throw new DeserializationException("Invalid length of Client Id option");
+            }
+
+            byte[] optionData = clientData.getData();
+
+            clientData.options = Lists.newArrayList();
+
+            ByteBuffer bb = ByteBuffer.wrap(optionData);
+
+            while (bb.remaining() >= Dhcp6Option.DEFAULT_LEN) {
+                Dhcp6Option option;
+                ByteBuffer optByteBuffer = ByteBuffer.wrap(optionData,
+                                                           bb.position(),
+                                                           optionData.length - bb.position());
+                short code = optByteBuffer.getShort();
+                short len = optByteBuffer.getShort();
+                int optLen = UNSIGNED_SHORT_MASK & len;
+                byte[] subOptData = new byte[Dhcp6Option.DEFAULT_LEN + optLen];
+                bb.get(subOptData);
+
+                // TODO: put more sub-options?
+                if (code == DHCP6.OptionCode.IAADDR.value()) {
+                    option = Dhcp6IaAddressOption.deserializer()
+                            .deserialize(subOptData, 0, subOptData.length);
+                    clientData.clientIaAddress  = ((Dhcp6IaAddressOption) option).getIp6Address();
+                } else if (code == DHCP6.OptionCode.CLIENTID.value()) {
+                    option = Dhcp6ClientIdOption.deserializer()
+                            .deserialize(subOptData, 0, subOptData.length);
+                } else if (code == DHCP6.OptionCode.CLIENT_LT.value()) {
+                    option = Dhcp6CLTOption.deserializer()
+                            .deserialize(subOptData, 0, subOptData.length);
+                } else {
+                    option = Dhcp6Option.deserializer()
+                            .deserialize(subOptData, 0, subOptData.length);
+                }
+                clientData.options.add(option);
+            }
+            return clientData;
+        };
+    }
+
+    @Override
+    public byte[] serialize() {
+        ByteBuffer bb = ByteBuffer.allocate(this.getLength() + Dhcp6Option.DEFAULT_LEN);
+        bb.putShort(getCode());
+        bb.putShort(getLength());
+        bb.put(payload.serialize());
+        return bb.array();
+    }
+
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("code", getCode())
+                .add("length", getLength())
+                .add("clientAddr", getIaAddress())
+                .toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), clientIaAddress, options);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof Dhcp6ClientDataOption)) {
+            return false;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        final Dhcp6ClientDataOption other = (Dhcp6ClientDataOption) obj;
+
+        return Objects.equals(getCode(), other.getCode()) &&
+                Objects.equals(getLength(), other.getLength()) &&
+                Objects.equals(clientIaAddress, other.clientIaAddress) &&
+                Objects.equals(options, other.options);
+    }
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/dhcp/Dhcp6LeaseQueryOption.java b/utils/misc/src/main/java/org/onlab/packet/dhcp/Dhcp6LeaseQueryOption.java
new file mode 100644
index 0000000..5bcc8ba
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/dhcp/Dhcp6LeaseQueryOption.java
@@ -0,0 +1,153 @@
+/*
+ * 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.onlab.packet.dhcp;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Lists;
+import org.onlab.packet.DHCP6;
+import org.onlab.packet.Deserializer;
+import org.onlab.packet.Ip6Address;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * DHCPv6 Lease Query Option.
+ */
+public final class Dhcp6LeaseQueryOption extends Dhcp6Option {
+
+    public static final int DEFAULT_LEN = 1 + 16;
+    //public short QueryType;
+    public Ip6Address linkAddress;
+    private List<Dhcp6Option> options;
+
+    public Dhcp6LeaseQueryOption(Dhcp6Option dhcp6Option) {
+        super(dhcp6Option);
+    }
+
+    @Override
+    public short getCode() {
+        return DHCP6.OptionCode.LEASE_QUERY.value();
+    }
+
+    @Override
+    public short getLength() {
+        //return (short) payload.serialize().length;
+        return (short) (DEFAULT_LEN + options.stream()
+                .mapToInt(opt -> (int) opt.getLength() + Dhcp6Option.DEFAULT_LEN)
+                .sum());
+    }
+
+    @Override
+    public byte[] getData() {
+        return payload.serialize();
+    }
+
+
+    public static Deserializer<Dhcp6Option> deserializer() {
+        return (data, offset, length) -> {
+            Dhcp6Option dhcp6Option = Dhcp6Option.deserializer().deserialize(data, offset, length);
+            Dhcp6LeaseQueryOption lQ6Option = new Dhcp6LeaseQueryOption(dhcp6Option);
+
+            byte[] optionData = lQ6Option.getData();
+            if (optionData.length >= 61) { // 61 is LQ option length + 4 header
+                ByteBuffer bb = ByteBuffer.wrap(optionData);
+                // fetch the Query type - just pop the byte from the byte buffer for subsequent parsing...
+                bb.get();
+                byte[] ipv6Addr = new byte[16];
+                bb.get(ipv6Addr);
+                lQ6Option.linkAddress = Ip6Address.valueOf(ipv6Addr);
+                //int optionsLen = dhcp6Option.getLength() - 1 - 16; // query type (1) + link address (16)
+
+                lQ6Option.options = Lists.newArrayList();
+
+                while (bb.remaining() >= Dhcp6Option.DEFAULT_LEN) {
+                    Dhcp6Option option;
+                    ByteBuffer optByteBuffer = ByteBuffer.wrap(optionData,
+                                                               bb.position(),
+                                                               optionData.length - bb.position());
+                    short code = optByteBuffer.getShort();
+                    short len = optByteBuffer.getShort();
+                    int optLen = UNSIGNED_SHORT_MASK & len;
+                    byte[] subOptData = new byte[Dhcp6Option.DEFAULT_LEN + optLen];
+                    bb.get(subOptData);
+
+                    // TODO: put more sub-options?
+                    if (code == DHCP6.OptionCode.IAADDR.value()) {
+                        option = Dhcp6IaAddressOption.deserializer()
+                                .deserialize(subOptData, 0, subOptData.length);
+                    } else if (code == DHCP6.OptionCode.ORO.value()) {
+                        option = Dhcp6Option.deserializer()
+                                    .deserialize(subOptData, 0, subOptData.length);
+                    } else {
+                        option = Dhcp6Option.deserializer()
+                                .deserialize(subOptData, 0, subOptData.length);
+                    }
+                    lQ6Option.options.add(option);
+                }
+            }
+            return lQ6Option;
+        };
+    }
+
+    @Override
+    public byte[] serialize() {
+        ByteBuffer bb = ByteBuffer.allocate(this.getLength() + Dhcp6Option.DEFAULT_LEN);
+        bb.putShort(getCode());
+        bb.putShort(getLength());
+        bb.put(payload.serialize());
+        return bb.array();
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("code", getCode())
+                .add("length", getLength())
+                .toString();
+    }
+
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), linkAddress, options);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof Dhcp6LeaseQueryOption)) {
+            return false;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        final Dhcp6LeaseQueryOption other = (Dhcp6LeaseQueryOption) obj;
+
+        return Objects.equals(getCode(), other.getCode()) &&
+                Objects.equals(getLength(), other.getLength()) &&
+                Objects.equals(linkAddress, other.linkAddress) &&
+                Objects.equals(options, other.options);
+    }
+}