VOL-3544 Enable the use of option82 for relaying packets to client without hostStore lookup.

Refactor the app's packet processor and some methods for more optimized packet handling,
for example avoid multiple lookups to the host store.

Change-Id: Id26cee017f14e2c838b9f42f20ebb3ec2ddc5efa
diff --git a/app/src/main/java/org/opencord/dhcpl2relay/impl/DhcpL2Relay.java b/app/src/main/java/org/opencord/dhcpl2relay/impl/DhcpL2Relay.java
index 8e11002..018ac69 100755
--- a/app/src/main/java/org/opencord/dhcpl2relay/impl/DhcpL2Relay.java
+++ b/app/src/main/java/org/opencord/dhcpl2relay/impl/DhcpL2Relay.java
@@ -15,11 +15,36 @@
  */
 package org.opencord.dhcpl2relay.impl;
 
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
+import static java.util.concurrent.Executors.newFixedThreadPool;
+import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_MessageType;
+import static org.onlab.packet.MacAddress.valueOf;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
+import static org.opencord.dhcpl2relay.impl.OsgiPropertyConstants.ENABLE_DHCP_BROADCAST_REPLIES;
+import static org.opencord.dhcpl2relay.impl.OsgiPropertyConstants.ENABLE_DHCP_BROADCAST_REPLIES_DEFAULT;
+import static org.opencord.dhcpl2relay.impl.OsgiPropertyConstants.OPTION_82;
+import static org.opencord.dhcpl2relay.impl.OsgiPropertyConstants.OPTION_82_DEFAULT;
+import static org.opencord.dhcpl2relay.impl.OsgiPropertyConstants.PACKET_PROCESSOR_THREADS;
+import static org.opencord.dhcpl2relay.impl.OsgiPropertyConstants.PACKET_PROCESSOR_THREADS_DEFAULT;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
 import org.apache.commons.io.HexDump;
 import org.onlab.packet.DHCP;
 import org.onlab.packet.Ethernet;
@@ -30,6 +55,7 @@
 import org.onlab.packet.UDP;
 import org.onlab.packet.VlanId;
 import org.onlab.packet.dhcp.DhcpOption;
+import org.onlab.packet.dhcp.DhcpRelayAgentOption;
 import org.onlab.util.KryoNamespace;
 import org.onlab.util.Tools;
 import org.onosproject.cfg.ComponentConfigService;
@@ -78,7 +104,7 @@
 import org.opencord.dhcpl2relay.DhcpL2RelayListener;
 import org.opencord.dhcpl2relay.DhcpL2RelayService;
 import org.opencord.dhcpl2relay.DhcpL2RelayStoreDelegate;
-import org.opencord.dhcpl2relay.impl.packet.DhcpOption82;
+import org.opencord.dhcpl2relay.impl.packet.DhcpOption82Data;
 import org.opencord.sadis.BaseInformationService;
 import org.opencord.sadis.SadisService;
 import org.opencord.sadis.SubscriberAndDeviceInformation;
@@ -93,30 +119,11 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.ByteArrayOutputStream;
-import java.nio.ByteBuffer;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Dictionary;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-
-import static java.util.concurrent.Executors.newFixedThreadPool;
-import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_MessageType;
-import static org.onlab.packet.MacAddress.valueOf;
-import static org.onlab.util.Tools.groupedThreads;
-import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
-import static org.opencord.dhcpl2relay.impl.OsgiPropertyConstants.*;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 
 /**
  * DHCP Relay Agent Application Component.
@@ -565,54 +572,12 @@
                                     appId, Optional.of(cp.deviceId()));
     }
 
-    private SubscriberAndDeviceInformation getDevice(PacketContext context) {
-        String serialNo = deviceService.getDevice(context.inPacket().
-                receivedFrom().deviceId()).serialNumber();
-
-        return subsService.get(serialNo);
-    }
-
-    private MacAddress relayAgentMacAddress(PacketContext context) {
-
-        SubscriberAndDeviceInformation device = this.getDevice(context);
-        if (device == null) {
-            log.warn("Device not found for {}", context.inPacket().
-                    receivedFrom());
-            return null;
-        }
-
-        return device.hardwareIdentifier();
-    }
-
-    private String nasPortId(PacketContext context) {
-        return nasPortId(context.inPacket().receivedFrom());
-    }
-
-    private String nasPortId(ConnectPoint cp) {
-        Port p = deviceService.getPort(cp);
-        return p.annotations().value(AnnotationKeys.PORT_NAME);
-    }
-
-    private SubscriberAndDeviceInformation getSubscriber(PacketContext context) {
-        return subsService.get(nasPortId(context));
-    }
-
-    private UniTagInformation getUnitagInformationFromPacketContext(PacketContext context,
-                                                                    SubscriberAndDeviceInformation sub) {
-        //If the ctag is defined in the tagList and dhcp is required, return the service info
-        List<UniTagInformation> tagList = sub.uniTagList();
-        for (UniTagInformation uniServiceInformation : tagList) {
-            if (uniServiceInformation.getPonCTag().toShort() == context.inPacket().parsed().getVlanID()) {
-                if (uniServiceInformation.getIsDhcpRequired()) {
-                    return uniServiceInformation;
-                }
-            }
-        }
-
-        return null;
-    }
-
+    /**
+     * Main packet-processing engine for dhcp l2 relay agent.
+     */
     private class DhcpRelayPacketProcessor implements PacketProcessor {
+        private static final String VLAN_KEYWORD = ":vlan";
+        private static final String PCP_KEYWORD = ":pcp";
 
         @Override
         public void process(PacketContext context) {
@@ -655,93 +620,6 @@
             }
         }
 
-        //forward the packet to ConnectPoint where the DHCP server is attached.
-        private void forwardPacket(Ethernet packet, PacketContext context) {
-            if (log.isTraceEnabled()) {
-                IPv4 ipv4Packet = (IPv4) packet.getPayload();
-                UDP udpPacket = (UDP) ipv4Packet.getPayload();
-                DHCP dhcpPayload = (DHCP) udpPacket.getPayload();
-                log.trace("Emitting packet to server: packet {}, with MAC {}",
-                          getDhcpPacketType(dhcpPayload),
-                          MacAddress.valueOf(dhcpPayload.getClientHardwareAddress()));
-            }
-            ConnectPoint toSendTo = null;
-            if (!useOltUplink) {
-                toSendTo = dhcpServerConnectPoint.get();
-            } else {
-                toSendTo = getUplinkConnectPointOfOlt(context.inPacket().
-                        receivedFrom().deviceId());
-            }
-
-            if (toSendTo != null) {
-                TrafficTreatment t = DefaultTrafficTreatment.builder()
-                        .setOutput(toSendTo.port()).build();
-                OutboundPacket o = new DefaultOutboundPacket(
-                        toSendTo.deviceId(), t,
-                        ByteBuffer.wrap(packet.serialize()));
-                if (log.isTraceEnabled()) {
-                    log.trace("Relaying packet to dhcp server at {} {}",
-                              toSendTo, packet);
-                }
-                packetService.emit(o);
-
-                SubscriberAndDeviceInformation entry = getSubscriberInfoFromClient(context);
-                updateDhcpRelayCountersStore(entry, DhcpL2RelayCounterNames.valueOf("PACKETS_TO_SERVER"));
-            } else {
-                log.error("No connect point to send msg to DHCP Server");
-            }
-        }
-
-        // get the type of the DHCP packet
-        private DHCP.MsgType getDhcpPacketType(DHCP dhcpPayload) {
-
-            for (DhcpOption option : dhcpPayload.getOptions()) {
-                if (option.getCode() == OptionCode_MessageType.getValue()) {
-                    byte[] data = option.getData();
-                    return DHCP.MsgType.getType(data[0]);
-                }
-            }
-            return null;
-        }
-
-        private void updateDhcpRelayCountersStore(SubscriberAndDeviceInformation entry,
-                                                  DhcpL2RelayCounterNames counterType) {
-            // Update global counter stats
-            dhcpL2RelayCounters.incrementCounter(DhcpL2RelayEvent.GLOBAL_COUNTER, counterType);
-            if (entry == null) {
-                log.warn("Counter not updated as subscriber info not found.");
-            } else {
-                // Update subscriber counter stats
-                dhcpL2RelayCounters.incrementCounter(entry.id(), counterType);
-            }
-        }
-
-        /*
-         * Get subscriber information based on it's context packet.
-         */
-        private SubscriberAndDeviceInformation getSubscriberInfoFromClient(PacketContext context) {
-            if (context != null) {
-                return getSubscriber(context);
-            }
-            return null;
-        }
-
-        /*
-         * Get subscriber information based on it's DHCP payload.
-         */
-        private SubscriberAndDeviceInformation getSubscriberInfoFromServer(DHCP dhcpPayload, PacketContext context) {
-            if (dhcpPayload != null) {
-                MacAddress descMac = valueOf(dhcpPayload.getClientHardwareAddress());
-                ConnectPoint subsCp = getConnectPointOfClient(descMac, context);
-
-                if (subsCp != null) {
-                    String portId = nasPortId(subsCp);
-                    return subsService.get(portId);
-                }
-            }
-            return null;
-        }
-
         // process the dhcp packet before relaying to server or client
         private void processDhcpPacket(PacketContext context, Ethernet packet,
                                        DHCP dhcpPayload) {
@@ -776,66 +654,63 @@
                     Ethernet ethernetPacketDiscover =
                             processDhcpPacketFromClient(context, packet);
                     if (ethernetPacketDiscover != null) {
-                        forwardPacket(ethernetPacketDiscover, context);
+                        relayPacketToServer(ethernetPacketDiscover, context);
                     }
-                    entry = getSubscriberInfoFromClient(context);
+                    entry = getSubscriber(context);
                     updateDhcpRelayCountersStore(entry, DhcpL2RelayCounterNames.valueOf("DHCPDISCOVER"));
                     break;
                 case DHCPOFFER:
-                    //reply to dhcp client.
-                    Ethernet ethernetPacketOffer =
+                    RelayToClientInfo r2cDataOffer =
                             processDhcpPacketFromServer(context, packet);
-                    if (ethernetPacketOffer != null) {
-                        sendReply(ethernetPacketOffer, dhcpPayload, context);
+                    if (r2cDataOffer != null) {
+                        relayPacketToClient(r2cDataOffer, clientMacAddress);
+                        entry = getSubscriber(r2cDataOffer.cp);
                     }
-                    entry = getSubscriberInfoFromServer(dhcpPayload, context);
                     updateDhcpRelayCountersStore(entry, DhcpL2RelayCounterNames.valueOf("DHCPOFFER"));
                     break;
                 case DHCPREQUEST:
                     Ethernet ethernetPacketRequest =
                             processDhcpPacketFromClient(context, packet);
                     if (ethernetPacketRequest != null) {
-                        forwardPacket(ethernetPacketRequest, context);
+                        relayPacketToServer(ethernetPacketRequest, context);
                     }
-                    entry = getSubscriberInfoFromClient(context);
+                    entry = getSubscriber(context);
                     updateDhcpRelayCountersStore(entry, DhcpL2RelayCounterNames.valueOf("DHCPREQUEST"));
                     break;
                 case DHCPACK:
-                    //reply to dhcp client.
-                    Ethernet ethernetPacketAck =
+                    RelayToClientInfo r2cDataAck =
                             processDhcpPacketFromServer(context, packet);
-                    if (ethernetPacketAck != null) {
-                        sendReply(ethernetPacketAck, dhcpPayload, context);
+                    if (r2cDataAck != null) {
+                        relayPacketToClient(r2cDataAck, clientMacAddress);
+                        entry = getSubscriber(r2cDataAck.cp);
                     }
-                    entry = getSubscriberInfoFromServer(dhcpPayload, context);
                     updateDhcpRelayCountersStore(entry, DhcpL2RelayCounterNames.valueOf("DHCPACK"));
                     break;
                 case DHCPDECLINE:
                     Ethernet ethernetPacketDecline =
                             processDhcpPacketFromClient(context, packet);
                     if (ethernetPacketDecline != null) {
-                        forwardPacket(ethernetPacketDecline, context);
+                        relayPacketToServer(ethernetPacketDecline, context);
                     }
-                    entry = getSubscriberInfoFromClient(context);
+                    entry = getSubscriber(context);
                     updateDhcpRelayCountersStore(entry, DhcpL2RelayCounterNames.valueOf("DHCPDECLINE"));
                     break;
                 case DHCPNAK:
-                    //reply to dhcp client.
-                    Ethernet ethernetPacketNak =
+                    RelayToClientInfo r2cDataNack =
                             processDhcpPacketFromServer(context, packet);
-                    if (ethernetPacketNak != null) {
-                        sendReply(ethernetPacketNak, dhcpPayload, context);
+                    if (r2cDataNack != null) {
+                        relayPacketToClient(r2cDataNack, clientMacAddress);
+                        entry = getSubscriber(r2cDataNack.cp);
                     }
-                    entry = getSubscriberInfoFromServer(dhcpPayload, context);
                     updateDhcpRelayCountersStore(entry, DhcpL2RelayCounterNames.valueOf("DHCPNACK"));
                     break;
                 case DHCPRELEASE:
                     Ethernet ethernetPacketRelease =
                             processDhcpPacketFromClient(context, packet);
                     if (ethernetPacketRelease != null) {
-                        forwardPacket(ethernetPacketRelease, context);
+                        relayPacketToServer(ethernetPacketRelease, context);
                     }
-                    entry = getSubscriberInfoFromClient(context);
+                    entry = getSubscriber(context);
                     updateDhcpRelayCountersStore(entry, DhcpL2RelayCounterNames.valueOf("DHCPRELEASE"));
                     break;
                 default:
@@ -843,6 +718,13 @@
             }
         }
 
+        /**
+         * Processes dhcp packets from clients.
+         *
+         * @param context the packet context
+         * @param ethernetPacket the dhcp packet from client
+         * @return the packet to relay to the server
+         */
         private Ethernet processDhcpPacketFromClient(PacketContext context,
                                                      Ethernet ethernetPacket) {
             if (log.isTraceEnabled()) {
@@ -884,19 +766,16 @@
                          inPort, clientVlan);
                 return null;
             }
-
-            DhcpAllocationInfo info = new DhcpAllocationInfo(
-                    inPort, dhcpPacket.getPacketType(), entry.circuitId(),
-                    clientMac, clientIp, clientVlan, entry.id());
-
-            String key = getUniqueUuidFromString(entry.id() + clientMac
-                                                 + clientVlan);
-            allocations.put(key, info);
-
-            post(new DhcpL2RelayEvent(DhcpL2RelayEvent.Type.UPDATED, info,
-                                      inPort));
+            DhcpOption82Data d82 = null;
             if (option82) {
-                DHCP dhcpPacketWithOption82 = addOption82(dhcpPacket, entry);
+                DHCP dhcpPacketWithOption82 = addOption82(dhcpPacket, entry,
+                                                          inPort, clientVlan,
+                                                          uniTagInformation
+                                                                  .getDsPonCTagPriority());
+                byte[] d82b = dhcpPacketWithOption82
+                        .getOption(DHCP.DHCPOptionCode.OptionCode_CircuitID)
+                        .getData();
+                d82 = new DhcpOption82Data(d82b);
                 udpPacket.setPayload(dhcpPacketWithOption82);
             }
 
@@ -914,14 +793,39 @@
             if (uniTagInformation.getUsPonSTagPriority() != -1) {
                 etherReply.setQinQPriorityCode((byte) uniTagInformation.getUsPonSTagPriority());
             }
-            log.info("Finished processing DHCP Packet of type {} from {} and relaying to dhcpServer",
-                     dhcpPacket.getPacketType(), entry.id());
+            if (uniTagInformation.getUsPonCTagPriority() != -1) {
+                etherReply.setPriorityCode((byte) uniTagInformation
+                        .getUsPonCTagPriority());
+            }
+
+            DhcpAllocationInfo info = new DhcpAllocationInfo(inPort,
+                                                             dhcpPacket.getPacketType(),
+                                                             (d82 == null)
+                                                                 ? entry.circuitId()
+                                                                 : d82.getAgentCircuitId(),
+                                                             clientMac, clientIp,
+                                                             clientVlan, entry.id());
+            String key = getUniqueUuidFromString(entry.id() + clientMac
+                    + clientVlan);
+            allocations.put(key, info);
+            post(new DhcpL2RelayEvent(DhcpL2RelayEvent.Type.UPDATED, info, inPort));
+            if (log.isTraceEnabled()) {
+                log.trace("Finished processing DHCP Packet of type {} from {} "
+                        + "... relaying to dhcpServer",
+                          dhcpPacket.getPacketType(), entry.id());
+            }
             return etherReply;
         }
 
-        //build the DHCP offer/ack with proper client port.
-        private Ethernet processDhcpPacketFromServer(PacketContext context,
-                                                     Ethernet ethernetPacket) {
+        /**
+         * Processes dhcp packets from the server.
+         *
+         * @param context the packet context
+         * @param ethernetPacket the dhcp packet
+         * @return returns information necessary for relaying packet to client
+         */
+        private RelayToClientInfo processDhcpPacketFromServer(PacketContext context,
+                                                              Ethernet ethernetPacket) {
             if (log.isTraceEnabled()) {
                 log.trace("DHCP Packet received from server at {} {}",
                           context.inPacket().receivedFrom(), ethernetPacket);
@@ -930,66 +834,349 @@
             Ethernet etherReply = (Ethernet) ethernetPacket.clone();
             IPv4 ipv4Packet = (IPv4) etherReply.getPayload();
             UDP udpPacket = (UDP) ipv4Packet.getPayload();
-            DHCP dhcpPayload = (DHCP) udpPacket.getPayload();
+            DHCP dhcpPacket = (DHCP) udpPacket.getPayload();
             VlanId innerVlan = VlanId.vlanId(ethernetPacket.getVlanID());
+            MacAddress dstMac = valueOf(dhcpPacket.getClientHardwareAddress());
 
-            MacAddress dstMac = valueOf(dhcpPayload.getClientHardwareAddress());
-            ConnectPoint subsCp = getConnectPointOfClient(dstMac, context);
-            // If we can't find the subscriber, can't process further
-            if (subsCp == null) {
-                log.warn("Couldn't find subscriber, service or host info for mac"
-                        + " address {} and vlan {} .. DHCP packet won't be delivered", dstMac, innerVlan);
-                return null;
-            }
-
-            SubscriberAndDeviceInformation entry = getSubscriberInfoFromServer(dhcpPayload, context);
-            if (entry != null) {
-                IpAddress ip = IpAddress.valueOf(dhcpPayload.getYourIPAddress());
-                // store DHCPAllocationInfo
-                DhcpAllocationInfo info = new DhcpAllocationInfo(subsCp,
-                     dhcpPayload.getPacketType(), entry.circuitId(), dstMac, ip,
-                     innerVlan, entry.id());
-                String key = getUniqueUuidFromString(entry.id()
-                                                     + info.macAddress() + innerVlan);
-                allocations.put(key, info);
-
-                post(new DhcpL2RelayEvent(DhcpL2RelayEvent.Type.UPDATED, info, subsCp));
-            }
-
-            UniTagInformation uniTagInformation = getUnitagInformationFromPacketContext(context, entry);
-            if (uniTagInformation == null) {
-                log.warn("Missing service information for connectPoint {} / cTag {}",
-                         context.inPacket().receivedFrom(), context.inPacket().parsed().getVlanID());
-                return null;
-            }
-
-            updateDhcpRelayCountersStore(entry, DhcpL2RelayCounterNames.valueOf("PACKETS_FROM_SERVER"));
-
-            // we leave the srcMac from the original packet
+            // we leave the srcMac from the original packet.
+            // TODO remove S-VLAN
             etherReply.setQinQVID(VlanId.NO_VID);
             etherReply.setQinQPriorityCode((byte) 0);
             etherReply.setDestinationMACAddress(dstMac);
-            etherReply.setVlanID(uniTagInformation.getPonCTag().toShort());
-            if (uniTagInformation.getUsPonCTagPriority() != -1) {
-                etherReply.setPriorityCode((byte) uniTagInformation.getUsPonCTagPriority());
-            }
 
+            // TODO deserialization of dhcp option82 leaves 'data' field null
+            // As a result we need to retrieve suboption data
+            RelayToClientInfo r2cData = null;
+            boolean usedOption82 = false;
             if (option82) {
-                udpPacket.setPayload(removeOption82(dhcpPayload));
-            } else {
-                udpPacket.setPayload(dhcpPayload);
+                // retrieve connectPoint and vlan from option82, if it is in expected format
+                DhcpOption opt = dhcpPacket
+                        .getOption(DHCP.DHCPOptionCode.OptionCode_CircuitID);
+                if (opt != null && opt instanceof DhcpRelayAgentOption) {
+                    DhcpRelayAgentOption d82 = (DhcpRelayAgentOption) opt;
+                    DhcpOption d82ckt = d82.getSubOption(DhcpOption82Data.CIRCUIT_ID_CODE);
+                    if (d82ckt.getData() != null) {
+                        r2cData = decodeCircuitId(new String(d82ckt.getData()));
+                    }
+                }
+                if (r2cData != null) {
+                    usedOption82 = true;
+                    etherReply.setVlanID(r2cData.cvid.toShort());
+                    if (r2cData.pcp != -1) {
+                        etherReply.setPriorityCode((byte) r2cData.pcp);
+                    }
+                }
             }
+            // always remove option82 if present
+            DHCP remDhcpPacket = removeOption82(dhcpPacket);
+            udpPacket.setPayload(remDhcpPacket);
+
             ipv4Packet.setPayload(udpPacket);
             etherReply.setPayload(ipv4Packet);
 
-            log.info("Finished processing packet.. relaying to client");
-            return etherReply;
+            if (!usedOption82) {
+                // option 82 data not present or not used, we need to
+                // lookup host store with client dstmac and vlan from context
+                r2cData = new RelayToClientInfo();
+                r2cData.cp = getConnectPointOfClient(dstMac, context);
+                if (r2cData.cp == null) {
+                    log.warn("Couldn't find subscriber, service or host info for mac"
+                            + " address {} and vlan {} .. DHCP packet can't be"
+                            + " delivered to client", dstMac, innerVlan);
+                    return null;
+                }
+            }
+
+            // always need the subscriber entry
+            SubscriberAndDeviceInformation entry = getSubscriber(r2cData.cp);
+            if (entry == null) {
+                log.warn("Couldn't find subscriber info for cp {}.. DHCP packet"
+                        + " can't be delivered to client mac {} and vlan {}",
+                         r2cData.cp, dstMac, innerVlan);
+                return null;
+            }
+
+            if (!usedOption82) {
+                UniTagInformation uniTagInformation =
+                        getUnitagInformationFromPacketContext(context, entry);
+                if (uniTagInformation == null) {
+                    log.warn("Missing service information for connectPoint {} "
+                            + " cTag {} .. DHCP packet can't be delivered to client",
+                             r2cData.cp, innerVlan);
+                    return null;
+                }
+                r2cData.cvid = uniTagInformation.getPonCTag();
+                r2cData.pcp = uniTagInformation.getDsPonCTagPriority();
+                r2cData.cktId = entry.circuitId();
+                etherReply.setVlanID(r2cData.cvid.toShort());
+                if (r2cData.pcp != -1) {
+                    etherReply.setPriorityCode((byte) r2cData.pcp);
+                }
+            }
+
+            // update stats and events
+            IpAddress ip = IpAddress.valueOf(dhcpPacket.getYourIPAddress());
+            DhcpAllocationInfo info =
+                    new DhcpAllocationInfo(r2cData.cp, dhcpPacket.getPacketType(),
+                                           r2cData.cktId, dstMac, ip, innerVlan,
+                                           entry.id());
+            String key = getUniqueUuidFromString(entry.id() + info.macAddress()
+                    + innerVlan);
+            allocations.put(key, info);
+            post(new DhcpL2RelayEvent(DhcpL2RelayEvent.Type.UPDATED, info,
+                                      r2cData.cp));
+            updateDhcpRelayCountersStore(entry, DhcpL2RelayCounterNames
+                    .valueOf("PACKETS_FROM_SERVER"));
+            if (log.isTraceEnabled()) {
+                log.trace("Finished processing packet.. relaying to client at {}",
+                     r2cData.cp);
+            }
+            r2cData.ethernetPkt = etherReply;
+            return r2cData;
         }
 
-        /*
-         * Get ConnectPoint of the Client based on it's MAC address
+        // forward the packet to ConnectPoint where the DHCP server is attached.
+        private void relayPacketToServer(Ethernet packet, PacketContext context) {
+            if (log.isTraceEnabled()) {
+                IPv4 ipv4Packet = (IPv4) packet.getPayload();
+                UDP udpPacket = (UDP) ipv4Packet.getPayload();
+                DHCP dhcpPayload = (DHCP) udpPacket.getPayload();
+                log.trace("Emitting packet to server: packet {}, with MAC {}",
+                          getDhcpPacketType(dhcpPayload),
+                          MacAddress.valueOf(dhcpPayload.getClientHardwareAddress()));
+            }
+            ConnectPoint toSendTo = null;
+            if (!useOltUplink) {
+                toSendTo = dhcpServerConnectPoint.get();
+            } else {
+                toSendTo = getUplinkConnectPointOfOlt(context.inPacket().receivedFrom()
+                        .deviceId());
+            }
+
+            if (toSendTo != null) {
+                TrafficTreatment t = DefaultTrafficTreatment.builder()
+                        .setOutput(toSendTo.port()).build();
+                OutboundPacket o = new DefaultOutboundPacket(toSendTo
+                        .deviceId(), t, ByteBuffer.wrap(packet.serialize()));
+                if (log.isTraceEnabled()) {
+                    log.trace("Relaying packet to dhcp server at {} {}", toSendTo,
+                              packet);
+                }
+                packetService.emit(o);
+
+                SubscriberAndDeviceInformation entry = getSubscriber(context);
+                updateDhcpRelayCountersStore(entry, DhcpL2RelayCounterNames
+                        .valueOf("PACKETS_TO_SERVER"));
+            } else {
+                log.error("No connect point to send msg to DHCP Server");
+            }
+        }
+
+        // send the response to the requester host (client)
+        private void relayPacketToClient(RelayToClientInfo r2cData,
+                                         MacAddress dstMac) {
+            ConnectPoint subCp = r2cData.cp;
+            Ethernet ethPacket = r2cData.ethernetPkt;
+            // Send packet out to requester if the host information is available
+            if (subCp != null) {
+                TrafficTreatment t = DefaultTrafficTreatment.builder()
+                        .setOutput(subCp.port()).build();
+                OutboundPacket o = new DefaultOutboundPacket(subCp.deviceId(),
+                                        t, ByteBuffer.wrap(ethPacket.serialize()));
+                if (log.isTraceEnabled()) {
+                    log.trace("Relaying packet to DHCP client at {} with "
+                        + "MacAddress {}, {} given {}", subCp, dstMac,
+                         ethPacket, r2cData);
+                }
+                packetService.emit(o);
+            } else {
+                log.error("Dropping DHCP Packet because unknown connectPoint for {}",
+                          dstMac);
+            }
+        }
+
+        /**
+         * Option 82 includes circuitId and remoteId data configured by an
+         * operator in sadis for a subscriber, and can be a string in any form
+         * relevant to the operator's dhcp-server. When circuitId is configured
+         * in sadis, the relay agent adds the option, but does not use the
+         * information for forwarding packets back to client.
+         * <p>
+         * If circuitId is not configured in sadis, this relay-agent adds
+         * circuitId information in the form
+         * "{@literal<}connectPoint>:vlan{@literal<}clientVlanId>:pcp{@literal<}downstreamPcp>"
+         * for example, "of:0000000000000001/32:vlan200:pcp7". When the packet
+         * is received back from the server with circuitId in this form, this
+         * relay agent will use this information to forward packets to the
+         * client.
+         *
+         * @param dhcpPacket the DHCP packet to transform
+         * @param entry sadis information for the subscriber
+         * @param cp the connectPoint to set if sadis entry has no circuitId
+         * @param clientVlan the vlan to set if sadis entry has no circuitId
+         * @param downstreamPbits the pbits to set if sadis entry has no
+         *            circuitId
+         * @return the modified dhcp packet with option82 added
          */
-        private ConnectPoint getConnectPointOfClient(MacAddress dstMac, PacketContext context) {
+        private DHCP addOption82(DHCP dhcpPacket, SubscriberAndDeviceInformation entry,
+                                 ConnectPoint cp, VlanId clientVlan,
+                                 int downstreamPbits) {
+            List<DhcpOption> options = Lists.newArrayList(dhcpPacket.getOptions());
+            DhcpOption82Data option82 = new DhcpOption82Data();
+            if (entry.circuitId() == null || entry.circuitId().isBlank()) {
+                option82.setAgentCircuitId(cp + VLAN_KEYWORD + clientVlan
+                        + PCP_KEYWORD
+                        + downstreamPbits);
+            } else {
+                option82.setAgentCircuitId(entry.circuitId());
+            }
+            option82.setAgentRemoteId(entry.remoteId());
+            if (log.isTraceEnabled()) {
+                log.trace("adding option82 {} ", option82);
+            }
+            DhcpOption option = new DhcpOption()
+                    .setCode(DHCP.DHCPOptionCode.OptionCode_CircuitID.getValue())
+                    .setData(option82.toByteArray())
+                    .setLength(option82.length());
+
+            options.add(options.size() - 1, option);
+            dhcpPacket.setOptions(options);
+
+            return dhcpPacket;
+        }
+
+        private DHCP removeOption82(DHCP dhcpPacket) {
+            List<DhcpOption> options = dhcpPacket.getOptions();
+            List<DhcpOption> newoptions = options.stream()
+                    .filter(option -> option
+                            .getCode() != DHCP.DHCPOptionCode.OptionCode_CircuitID
+                                    .getValue())
+                    .collect(Collectors.toList());
+
+            return dhcpPacket.setOptions(newoptions);
+        }
+
+        /**
+         * Returns the circuit Id values decoded from the option 82 data. Decoding
+         * is performed if and only if the circuit id format is in the form
+         * "{@literal<}connectPoint>:vlan{@literal<}clientVlanId>:pcp{@literal<}downstreamPcp>"
+         *
+         * @param cktId the circuitId string from option 82 data
+         * @return decoded circuit id data if it is in the expected format or
+         *         null
+         */
+        private RelayToClientInfo decodeCircuitId(String cktId) {
+            if (cktId.contains(VLAN_KEYWORD) && cktId.contains(PCP_KEYWORD)) {
+                ConnectPoint cp = ConnectPoint
+                        .fromString(cktId
+                                .substring(0, cktId.indexOf(VLAN_KEYWORD)));
+                VlanId cvid = VlanId
+                        .vlanId(cktId.substring(
+                                                cktId.indexOf(VLAN_KEYWORD)
+                                                        + VLAN_KEYWORD.length(),
+                                                cktId.indexOf(PCP_KEYWORD)));
+                int pcp = Integer
+                        .valueOf(cktId.substring(cktId.indexOf(PCP_KEYWORD)
+                                + PCP_KEYWORD.length()))
+                        .intValue();
+                log.debug("retrieved from option82-> cp={} cvlan={} down-pcp={}"
+                        + " for relaying to client ", cp, cvid, pcp);
+                return new RelayToClientInfo(cp, cvid, pcp, cktId);
+            } else {
+                log.debug("Option 82 circuitId {} is operator defined and will "
+                        + "not be used for forwarding", cktId);
+                return null;
+            }
+        }
+
+        private class RelayToClientInfo {
+            Ethernet ethernetPkt;
+            ConnectPoint cp;
+            VlanId cvid;
+            int pcp;
+            String cktId;
+
+            public RelayToClientInfo(ConnectPoint cp, VlanId cvid, int pcp,
+                                     String cktId) {
+                this.cp = cp;
+                this.cvid = cvid;
+                this.pcp = pcp;
+                this.cktId = cktId;
+            }
+
+            public RelayToClientInfo() {
+            }
+
+            @Override
+            public String toString() {
+                return "RelayToClientInfo: {connectPoint=" + cp + " clientVlan="
+                        + cvid + " clientPcp=" + pcp + " circuitId=" + cktId + "}";
+            }
+
+        }
+
+        // get the type of the DHCP packet
+        private DHCP.MsgType getDhcpPacketType(DHCP dhcpPayload) {
+            for (DhcpOption option : dhcpPayload.getOptions()) {
+                if (option.getCode() == OptionCode_MessageType.getValue()) {
+                    byte[] data = option.getData();
+                    return DHCP.MsgType.getType(data[0]);
+                }
+            }
+            return null;
+        }
+
+        private void updateDhcpRelayCountersStore(SubscriberAndDeviceInformation entry,
+                                                  DhcpL2RelayCounterNames counterType) {
+            // Update global counter stats
+            dhcpL2RelayCounters.incrementCounter(DhcpL2RelayEvent.GLOBAL_COUNTER,
+                                                 counterType);
+            if (entry == null) {
+                log.warn("Counter not updated as subscriber info not found.");
+            } else {
+                // Update subscriber counter stats
+                dhcpL2RelayCounters.incrementCounter(entry.id(), counterType);
+            }
+        }
+
+        /**
+         * Get subscriber information based on subscriber's connectPoint.
+         *
+         * @param subsCp the subscriber's connectPoint
+         * @return subscriber sadis info or null if not found
+         */
+        private SubscriberAndDeviceInformation getSubscriber(ConnectPoint subsCp) {
+            if (subsCp != null) {
+                String portName = getPortName(subsCp);
+                return subsService.get(portName);
+            }
+            return null;
+        }
+
+        /**
+         * Returns sadis info for subscriber based on incoming packet context.
+         * The packet context must refer to a packet coming from a subscriber
+         * port.
+         *
+         * @param context incoming packet context from subscriber port (UNI)
+         * @return sadis info for the subscriber or null
+         */
+        private SubscriberAndDeviceInformation getSubscriber(PacketContext context) {
+            String portName = getPortName(context.inPacket().receivedFrom());
+            return subsService.get(portName);
+        }
+
+        /**
+         * Returns ConnectPoint of the Client based on MAC address and C-VLAN.
+         * Verifies that returned connect point has service defined in sadis.
+         *
+         * @param dstMac client dstMac
+         * @param context context for incoming packet, parsed for C-vlan id
+         * @return connect point information for client or null if connect point
+         *         not found or service cannot be verified for client info
+         */
+        private ConnectPoint getConnectPointOfClient(MacAddress dstMac,
+                                                     PacketContext context) {
             Set<Host> hosts = hostService.getHostsByMac(dstMac);
             if (hosts == null || hosts.isEmpty()) {
                 log.warn("Cannot determine host for DHCP client: {}. Aborting "
@@ -1003,14 +1190,14 @@
                 ConnectPoint cp = new ConnectPoint(h.location().deviceId(),
                                                    h.location().port());
 
-                String portId = nasPortId(cp);
-                SubscriberAndDeviceInformation sub = subsService.get(portId);
+                SubscriberAndDeviceInformation sub = getSubscriber(cp);
                 if (sub == null) {
-                    log.warn("Subscriber info not found for {}", cp);
-                    return null;
+                    log.warn("Subscriber info not found for {} for host {}", cp, h);
+                    continue;
                 }
                 // check for cvlan in subscriber's uniTagInfo list
-                UniTagInformation uniTagInformation = getUnitagInformationFromPacketContext(context, sub);
+                UniTagInformation uniTagInformation =
+                        getUnitagInformationFromPacketContext(context, sub);
                 if (uniTagInformation != null) {
                     return cp;
                 }
@@ -1018,58 +1205,71 @@
             // no sadis config found for this connectPoint/vlan
             log.warn("Missing service information for dhcp packet received from"
                     + " {} with cTag {} .. cannot relay to client",
-                     context.inPacket().receivedFrom(), context.inPacket().parsed().getVlanID());
+                     context.inPacket().receivedFrom(),
+                     context.inPacket().parsed().getVlanID());
+            return null;
+        }
+
+        /**
+         * Returns the port-name for the given connectPoint port.
+         *
+         * @param cp the given connect point
+         * @return the port-name for the connect point port
+         */
+        private String getPortName(ConnectPoint cp) {
+            Port p = deviceService.getPort(cp);
+            return p.annotations().value(AnnotationKeys.PORT_NAME);
+        }
+
+        /**
+         * Return's uniTagInformation (service information) if incoming packet's
+         * client VLAN id matches the subscriber's service info, and dhcp is
+         * required for this service.
+         *
+         * @param context
+         * @param sub
+         * @return
+         */
+        private UniTagInformation getUnitagInformationFromPacketContext(PacketContext context,
+                                                                        SubscriberAndDeviceInformation sub) {
+            // If the ctag is defined in the tagList and dhcp is required,
+            // return the service info
+            List<UniTagInformation> tagList = sub.uniTagList();
+            for (UniTagInformation uniServiceInformation : tagList) {
+                if (uniServiceInformation.getPonCTag().toShort() == context.inPacket()
+                        .parsed().getVlanID()) {
+                    if (uniServiceInformation.getIsDhcpRequired()) {
+                        return uniServiceInformation;
+                    }
+                }
+            }
 
             return null;
         }
 
-        // send the response to the requester host (client)
-        private void sendReply(Ethernet ethPacket, DHCP dhcpPayload, PacketContext context) {
-            MacAddress descMac = valueOf(dhcpPayload.getClientHardwareAddress());
-            ConnectPoint subCp = getConnectPointOfClient(descMac, context);
-            // Send packet out to requester if the host information is available
-            if (subCp != null) {
-                TrafficTreatment t = DefaultTrafficTreatment.builder()
-                        .setOutput(subCp.port()).build();
-                OutboundPacket o = new DefaultOutboundPacket(
-                        subCp.deviceId(), t, ByteBuffer.wrap(ethPacket.serialize()));
-                if (log.isTraceEnabled()) {
-                    log.trace("Relaying packet to DHCP client at {} with MacAddress {}, {}", subCp,
-                              descMac, ethPacket);
-                }
-                packetService.emit(o);
-            } else {
-                log.error("Dropping DHCP Packet because can't find host for {}", descMac);
+
+        private MacAddress relayAgentMacAddress(PacketContext context) {
+            SubscriberAndDeviceInformation device = this.getDevice(context);
+            if (device == null) {
+                log.warn("Device not found for {}", context.inPacket().receivedFrom());
+                return null;
             }
+            return device.hardwareIdentifier();
         }
-    }
 
-    private DHCP addOption82(DHCP dhcpPacket, SubscriberAndDeviceInformation entry) {
-        log.trace("option82data {} ", entry);
+        /**
+         * Returns sadis information for device from which packet was received.
+         *
+         * @param context the packet context
+         * @return sadis information for device
+         */
+        private SubscriberAndDeviceInformation getDevice(PacketContext context) {
+            String serialNo = deviceService
+                    .getDevice(context.inPacket().receivedFrom().deviceId())
+                    .serialNumber();
+            return subsService.get(serialNo);
+        }
 
-        List<DhcpOption> options = Lists.newArrayList(dhcpPacket.getOptions());
-        DhcpOption82 option82 = new DhcpOption82();
-        option82.setAgentCircuitId(entry.circuitId());
-        option82.setAgentRemoteId(entry.remoteId());
-        DhcpOption option = new DhcpOption()
-                .setCode(DHCP.DHCPOptionCode.OptionCode_CircuitID.getValue())
-                .setData(option82.toByteArray())
-                .setLength(option82.length());
-
-        options.add(options.size() - 1, option);
-        dhcpPacket.setOptions(options);
-
-        return dhcpPacket;
-
-    }
-
-    private DHCP removeOption82(DHCP dhcpPacket) {
-        List<DhcpOption> options = dhcpPacket.getOptions();
-        List<DhcpOption> newoptions = options.stream()
-                .filter(option -> option.getCode() != DHCP.DHCPOptionCode.OptionCode_CircuitID.getValue())
-                .collect(Collectors.toList());
-
-        return dhcpPacket.setOptions(newoptions);
     }
 
     /**
@@ -1122,7 +1322,6 @@
         allocations.clear();
     }
 
-
     @Override
     public boolean removeAllocationsByConnectPoint(ConnectPoint cp) {
         boolean removed = false;
@@ -1136,7 +1335,6 @@
         return removed;
     }
 
-
     /**
      * Checks for mastership or falls back to leadership on deviceId.
      * If the node is not master and device is available
diff --git a/app/src/main/java/org/opencord/dhcpl2relay/impl/packet/DhcpOption82.java b/app/src/main/java/org/opencord/dhcpl2relay/impl/packet/DhcpOption82.java
deleted file mode 100644
index bad55f9..0000000
--- a/app/src/main/java/org/opencord/dhcpl2relay/impl/packet/DhcpOption82.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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.opencord.dhcpl2relay.impl.packet;
-
-import java.io.ByteArrayOutputStream;
-import java.nio.charset.StandardCharsets;
-
-/**
- * Represents the DHCP Option 82 information. Currently only supports
- * sub option 1 (agent-circuit-id) and 2 (agent=-relay-id).
- */
-public class DhcpOption82 {
-
-    private String agentCircuitId = null;
-    private String agentRemoteId = null;
-
-    public DhcpOption82() {
-
-    }
-
-    public void setAgentCircuitId(String value) {
-        this.agentCircuitId = value;
-    }
-
-    /**
-     *
-     * @return agentCircuitId
-     */
-    public String getAgentCircuitId() {
-        return this.agentCircuitId;
-    }
-
-    /**
-     * sets AgentRemoteId.
-     * @param value   Value to be set
-     */
-    public void setAgentRemoteId(String value) {
-        this.agentRemoteId = value;
-    }
-
-    /**
-     *
-     * @return agentRemoteId
-     */
-    public String getAgentRemoteId() {
-        return this.agentRemoteId;
-    }
-
-    /**
-     *
-     * @return length of option 82.
-     */
-    public byte length() {
-        int length = 0;
-
-        // +2 below for sub option ID and length of sub option
-        if (agentCircuitId != null) {
-            length += agentCircuitId.length() + 2;
-        }
-        if (agentRemoteId != null) {
-            length += agentRemoteId.length() + 2;
-        }
-        return (byte) length;
-    }
-
-    /**
-     * Returns the representation of the option 82 specification as a byte
-     * array.
-     * @return returns byte array
-     */
-    public byte[] toByteArray() {
-        ByteArrayOutputStream buf = new ByteArrayOutputStream();
-
-        // Add sub option if set
-        if (agentCircuitId != null) {
-            buf.write((byte) 1);
-            buf.write((byte) agentCircuitId.length());
-            byte[] bytes = agentCircuitId.getBytes(StandardCharsets.UTF_8);
-            buf.write(bytes, 0, bytes.length);
-        }
-
-        // Add sub option if set
-        if (agentRemoteId != null) {
-            buf.write((byte) 2);
-            buf.write((byte) agentRemoteId.length());
-            byte[] bytes = agentRemoteId.getBytes(StandardCharsets.UTF_8);
-            buf.write(bytes, 0, bytes.length);
-        }
-
-        return buf.toByteArray();
-    }
-
-}
diff --git a/app/src/main/java/org/opencord/dhcpl2relay/impl/packet/DhcpOption82Data.java b/app/src/main/java/org/opencord/dhcpl2relay/impl/packet/DhcpOption82Data.java
new file mode 100644
index 0000000..a2d031d
--- /dev/null
+++ b/app/src/main/java/org/opencord/dhcpl2relay/impl/packet/DhcpOption82Data.java
@@ -0,0 +1,157 @@
+/*
+ * 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.opencord.dhcpl2relay.impl.packet;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Represents the DHCP Option 82 sub-options. Currently only supports sub option
+ * 1 (agent-circuit-id) and 2 (agent-remote-id).
+ */
+public class DhcpOption82Data {
+
+    private String agentCircuitId = null;
+    private String agentRemoteId = null;
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    public static final byte CIRCUIT_ID_CODE = 1;
+    public static final byte REMOTE_ID_CODE = 2;
+
+    public DhcpOption82Data() {
+
+    }
+
+    /**
+     * Constructs a DhcpOption82Data object from the given byte array. The
+     * expectation is that the byte array starts with the first suboption (i.e
+     * it does not include the option-code and overall length of the option 82)
+     *
+     * @param b byte array representing the data portion of the dhcp option 82
+     */
+    public DhcpOption82Data(byte[] b) {
+        ByteBuffer bb = ByteBuffer.wrap(b, 0, b.length);
+        if (b.length < 3) {
+            log.warn("Malformed option82 sub-options {}", b);
+            return;
+        }
+        while (bb.hasRemaining() && bb.limit() - bb.position() > 2) {
+            byte subOptionCode = bb.get();
+            byte subOptionLen = bb.get();
+            byte[] subOptionData = new byte[subOptionLen];
+            try {
+                bb.get(subOptionData);
+            } catch (BufferUnderflowException e) {
+                log.warn("Malformed option82 sub-option {}", e.getMessage());
+                return;
+            }
+            if (subOptionCode == CIRCUIT_ID_CODE) {
+                agentCircuitId = new String(subOptionData);
+            } else if (subOptionCode == REMOTE_ID_CODE) {
+                agentRemoteId = new String(subOptionData);
+            } else {
+                log.debug("Unsupported subOption {} in DHCP option82 - {}",
+                          subOptionCode, new String(subOptionData));
+            }
+        }
+    }
+
+    public void setAgentCircuitId(String value) {
+        this.agentCircuitId = value;
+    }
+
+    public void setAgentCircuitId(byte[] subOptionData) {
+        agentCircuitId = new String(subOptionData);
+    }
+
+    /**
+     *
+     * @return agentCircuitId
+     */
+    public String getAgentCircuitId() {
+        return this.agentCircuitId;
+    }
+
+    /**
+     * sets AgentRemoteId.
+     * @param value   Value to be set
+     */
+    public void setAgentRemoteId(String value) {
+        this.agentRemoteId = value;
+    }
+
+    /**
+     *
+     * @return agentRemoteId
+     */
+    public String getAgentRemoteId() {
+        return this.agentRemoteId;
+    }
+
+    /**
+     *
+     * @return length of option 82.
+     */
+    public byte length() {
+        int length = 0;
+
+        // +2 below for sub option ID and length of sub option
+        if (agentCircuitId != null) {
+            length += agentCircuitId.length() + 2;
+        }
+        if (agentRemoteId != null) {
+            length += agentRemoteId.length() + 2;
+        }
+        return (byte) length;
+    }
+
+    /**
+     * Returns the representation of the option 82 specification as a byte
+     * array.
+     * @return returns byte array
+     */
+    public byte[] toByteArray() {
+        ByteArrayOutputStream buf = new ByteArrayOutputStream();
+
+        // Add sub option if set
+        if (agentCircuitId != null) {
+            buf.write(CIRCUIT_ID_CODE);
+            buf.write((byte) agentCircuitId.length());
+            byte[] bytes = agentCircuitId.getBytes(StandardCharsets.UTF_8);
+            buf.write(bytes, 0, bytes.length);
+        }
+
+        // Add sub option if set
+        if (agentRemoteId != null) {
+            buf.write(REMOTE_ID_CODE);
+            buf.write((byte) agentRemoteId.length());
+            byte[] bytes = agentRemoteId.getBytes(StandardCharsets.UTF_8);
+            buf.write(bytes, 0, bytes.length);
+        }
+
+        return buf.toByteArray();
+    }
+
+    @Override
+    public String toString() {
+        return "circuitId: " + agentCircuitId + " remoteId: " + agentRemoteId;
+    }
+
+}
diff --git a/app/src/test/java/org/opencord/dhcpl2relay/impl/DhcpL2RelayTest.java b/app/src/test/java/org/opencord/dhcpl2relay/impl/DhcpL2RelayTest.java
index c19b2eb..3e19917 100755
--- a/app/src/test/java/org/opencord/dhcpl2relay/impl/DhcpL2RelayTest.java
+++ b/app/src/test/java/org/opencord/dhcpl2relay/impl/DhcpL2RelayTest.java
@@ -16,19 +16,21 @@
 package org.opencord.dhcpl2relay.impl;
 
 import static org.easymock.EasyMock.createMock;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 import static org.slf4j.LoggerFactory.getLogger;
 
 import java.nio.ByteBuffer;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
-import com.google.common.util.concurrent.MoreExecutors;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.onlab.junit.TestUtils;
 import org.onlab.packet.DHCP;
+import org.onlab.packet.DeserializationException;
 import org.onlab.packet.Ethernet;
 import org.onlab.packet.IPv4;
 import org.onlab.packet.MacAddress;
@@ -45,10 +47,11 @@
 import org.opencord.dhcpl2relay.DhcpAllocationInfo;
 import org.opencord.dhcpl2relay.DhcpL2RelayEvent;
 import org.opencord.dhcpl2relay.DhcpL2RelayStoreDelegate;
-import org.opencord.dhcpl2relay.impl.packet.DhcpOption82;
+import org.opencord.dhcpl2relay.impl.packet.DhcpOption82Data;
 import org.slf4j.Logger;
 
 import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.MoreExecutors;
 
 public class DhcpL2RelayTest extends DhcpL2RelayTestBase {
 
@@ -101,9 +104,9 @@
         dhcpL2Relay.deactivate();
     }
 
-    private void checkAllocation(DHCP.MsgType messageType) {
+    private void checkAllocation(DHCP.MsgType messageType, String circuitId) {
         ConnectPoint clientCp = ConnectPoint.deviceConnectPoint(OLT_DEV_ID + "/"
-                                                                        + String.valueOf(CLIENT_PORT));
+                                                + String.valueOf(CLIENT_PORT));
         allocs = dhcpL2Relay.getAllocationInfo();
         assertEquals(1, allocs.size());
         allocs.forEach((k, v) -> {
@@ -111,6 +114,7 @@
             assertEquals(v.type(), messageType);
             assertEquals(v.macAddress(), CLIENT_MAC);
             assertEquals(v.location(), clientCp);
+            assertEquals(v.circuitId(), circuitId);
         });
     }
 
@@ -175,7 +179,8 @@
     }
 
     /**
-     * Tests the DHCP relay app by sending DHCP discovery Packet.
+     * Tests the DHCP relay app by sending DHCP discovery Packet. The circuitId
+     * and remote-Id for this client is operator defined in MockSadis.
      *
      * @throws Exception when an unhandled error occurs
      */
@@ -185,12 +190,79 @@
         dhcpL2Relay.clearAllocations();
         Ethernet discoverPacket = constructDhcpDiscoverPacket(CLIENT_MAC);
         ConnectPoint clientCp = ConnectPoint.deviceConnectPoint(OLT_DEV_ID + "/"
-                                                                        + String.valueOf(CLIENT_PORT));
-        sendPacket(discoverPacket, clientCp);
+                                    + String.valueOf(CLIENT_PORT));
+        // send a copy of the packet as the app code modifies the sent packet
+        sendPacket(discoverPacket.duplicate(), clientCp);
 
         Ethernet discoverRelayed = (Ethernet) getPacket();
         compareClientPackets(discoverPacket, discoverRelayed);
-        checkAllocation(DHCP.MsgType.DHCPDISCOVER);
+        checkAllocation(DHCP.MsgType.DHCPDISCOVER, CLIENT_CIRCUIT_ID);
+    }
+
+    /**
+     * Tests the addition of app-defined circuit id, when this client's
+     * MockSadis config for circiutId is empty. The remoteId is configured.
+     *
+     * @throws Exception when an unhandled error occurs
+     */
+    @Test
+    public void testDhcpDiscoverEmptyCircuitId() throws Exception {
+        dhcpL2Relay.clearAllocations();
+        MacAddress mac32 = MacAddress.valueOf("b4:96:91:0c:4f:e4");
+        VlanId vlan32a = VlanId.vlanId((short) 801); // defined in mockSadis
+        VlanId qinq32a = VlanId.vlanId((short) 111);
+        Ethernet discover32a = constructDhcpDiscoverPacket(mac32, vlan32a,
+                                                           (short) 0);
+        ConnectPoint client32 = ConnectPoint
+                .deviceConnectPoint("of:0000b86a974385f7/32");
+        sendPacket(discover32a.duplicate(), client32);
+        Ethernet discoverRelayed = (Ethernet) getPacket();
+        // empty circuitId in sadis for client32 should result in app defined
+        // circuitId
+        String expectedCircuitId = client32 + ":vlan" + vlan32a + ":pcp-1";
+        compareClientPackets(discover32a, discoverRelayed,
+                             qinq32a, vlan32a, CLIENT_C_PBIT,
+                             expectedCircuitId,
+                             CLIENT_REMOTE_ID);
+        allocs = dhcpL2Relay.getAllocationInfo();
+        allocs.forEach((k, v) -> {
+            log.info("Allocation {} : {}", k, v);
+            assertEquals(v.circuitId(), expectedCircuitId);
+        });
+    }
+
+    /**
+     * Tests the addition of app-defined circuit id, when this client's
+     * MockSadis config for circuitId and remoteId are null. In addition, it
+     * tests that the configured downstream-pcp is included in the circuitId.
+     *
+     * @throws Exception when an unhandled error occurs
+     */
+    @Test
+    public void testDhcpDiscoverNullIds() throws Exception {
+        dhcpL2Relay.clearAllocations();
+        MacAddress mac4112 = MacAddress.valueOf("b4:96:91:0c:4f:c9");
+        VlanId vlan4112 = VlanId.vlanId((short) 101);
+        VlanId qinq4112 = VlanId.vlanId((short) 222);
+        Ethernet discover4112 = constructDhcpDiscoverPacket(mac4112, vlan4112,
+                                                            (short) 0);
+        ConnectPoint client4112 = ConnectPoint
+                .deviceConnectPoint("of:0000b86a974385f7/4112");
+        sendPacket(discover4112.duplicate(), client4112);
+        Ethernet discoverRelayed = (Ethernet) getPacket();
+        // null circuitId in sadis for client32 should result in app defined
+        // circuitId. remoteId should not be there. Correct downstream pbit
+        // should be used
+        String expectedCircuitId = client4112 + ":vlan" + vlan4112 + ":pcp5";
+        compareClientPackets(discover4112, discoverRelayed,
+                             qinq4112, vlan4112, CLIENT_C_PBIT,
+                             expectedCircuitId,
+                             null);
+        allocs = dhcpL2Relay.getAllocationInfo();
+        allocs.forEach((k, v) -> {
+            log.info("Allocation {} : {}", k, v);
+            assertEquals(v.circuitId(), expectedCircuitId);
+        });
     }
 
     /**
@@ -203,67 +275,96 @@
         // Sending DHCP Request packet
         Ethernet requestPacket = constructDhcpRequestPacket(CLIENT_MAC);
         ConnectPoint clientCp = ConnectPoint.deviceConnectPoint(OLT_DEV_ID + "/"
-                                                                        + String.valueOf(CLIENT_PORT));
-        sendPacket(requestPacket, clientCp);
+                                                + String.valueOf(CLIENT_PORT));
+        sendPacket(requestPacket.duplicate(), clientCp);
 
         Ethernet requestRelayed = (Ethernet) getPacket();
         compareClientPackets(requestPacket, requestRelayed);
-        checkAllocation(DHCP.MsgType.DHCPREQUEST);
+        checkAllocation(DHCP.MsgType.DHCPREQUEST, CLIENT_CIRCUIT_ID);
     }
 
     /**
-     * Tests the DHCP relay app by sending DHCP Offer Packet.
+     * Tests the DHCP relay app by sending DHCP Offer Packet with app-defined
+     * circuit id. App should use the circuit id for forwarding.
      *
      * @throws Exception when an unhandled error occurs
      */
     @Test
     public void testDhcpOffer() throws InterruptedException {
         // Sending DHCP Offer packet
-        Ethernet offerPacket = constructDhcpOfferPacket(SERVER_MAC,
-                                                        CLIENT_MAC, DESTINATION_ADDRESS_IP, DHCP_CLIENT_IP_ADDRESS);
+        Ethernet offerPacket = constructDhcpOfferPacket(SERVER_MAC, CLIENT_MAC,
+                                                        DESTINATION_ADDRESS_IP,
+                                                        DHCP_CLIENT_IP_ADDRESS);
         sendPacket(offerPacket, ConnectPoint.deviceConnectPoint(OLT_DEV_ID + "/"
-                                                                        + String.valueOf(UPLINK_PORT)));
+                                         + String.valueOf(UPLINK_PORT)));
 
         Ethernet offerRelayed = (Ethernet) getPacket();
         compareServerPackets(offerPacket, offerRelayed);
-        checkAllocation(DHCP.MsgType.DHCPOFFER);
+        String expectedCircuitId = OLT_DEV_ID + "/" + CLIENT_PORT + ":vlan"
+                + CLIENT_C_TAG + ":pcp" + CLIENT_C_PBIT;
+        checkAllocation(DHCP.MsgType.DHCPOFFER, expectedCircuitId);
     }
 
     /**
-     * Tests the DHCP relay app by sending DHCP Ack Packet.
+     * Tests the DHCP relay app by sending DHCP Ack Packet with operator defined
+     * circuit id. App should ignore circuit Id and do a host lookup.
      *
      * @throws Exception when an unhandled error occurs
      */
     @Test
     public void testDhcpAck() throws InterruptedException {
 
-        Ethernet ackPacket = constructDhcpAckPacket(SERVER_MAC,
-                                                    CLIENT_MAC, DESTINATION_ADDRESS_IP, DHCP_CLIENT_IP_ADDRESS);
+        Ethernet ackPacket = constructDhcpAckPacket(SERVER_MAC, CLIENT_MAC,
+                                                    DESTINATION_ADDRESS_IP,
+                                                    DHCP_CLIENT_IP_ADDRESS);
 
         sendPacket(ackPacket, ConnectPoint.deviceConnectPoint(OLT_DEV_ID + "/"
-                                                                      + String.valueOf(UPLINK_PORT)));
+                                                + String.valueOf(UPLINK_PORT)));
 
         Ethernet ackRelayed = (Ethernet) getPacket();
         compareServerPackets(ackPacket, ackRelayed);
-        checkAllocation(DHCP.MsgType.DHCPACK);
+        checkAllocation(DHCP.MsgType.DHCPACK, CLIENT_CIRCUIT_ID);
     }
 
     /**
      * Tests the DHCP relay app by sending DHCP Nak Packet.
+     * Tests app-defined option82, but uses incorrect connectPoint - packet
+     * should still be forwarded to this connectPoint (ie without host lookup).
+     * Also pbit in circuitId is -1, which means original pbit should be retained
      *
      * @throws Exception when an unhandled error occurs
      */
     @Test
     public void testDhcpNak() throws InterruptedException {
+        VlanId fakeVlan = VlanId.vlanId((short) 50);
+        short fakePcp = (short) 4; // should be retained
+        VlanId expectedVlan = VlanId.vlanId((short) 111);
+        // relayed packet should have vlan 111 and retain pcp4 and be sent out
+        // of port32
+        ConnectPoint fakeCp = ConnectPoint.fromString("of:0000b86a974385f7/32");
+        String fakeCircuitId = fakeCp + ":vlan"
+                + expectedVlan + ":pcp-1";
+        Ethernet nakPacket = constructDhcpNakPacket(SERVER_MAC, CLIENT_MAC,
+                                                    DESTINATION_ADDRESS_IP,
+                                                    DHCP_CLIENT_IP_ADDRESS,
+                                                    fakeVlan,
+                                                    fakePcp);
 
-        Ethernet nakPacket = constructDhcpNakPacket(SERVER_MAC,
-                                                    CLIENT_MAC, DESTINATION_ADDRESS_IP, DHCP_CLIENT_IP_ADDRESS);
-
-        sendPacket(nakPacket, ConnectPoint.deviceConnectPoint(OLT_DEV_ID + "/" + 1));
+        sendPacket(nakPacket, ConnectPoint.deviceConnectPoint(OLT_DEV_ID + "/"
+                                                + String.valueOf(UPLINK_PORT)));
 
         Ethernet nakRelayed = (Ethernet) getPacket();
-        compareServerPackets(nakPacket, nakRelayed);
-        checkAllocation(DHCP.MsgType.DHCPNAK);
+        compareServerPackets(nakPacket, nakRelayed, expectedVlan, fakePcp);
+
+        allocs = dhcpL2Relay.getAllocationInfo();
+        assertEquals(1, allocs.size());
+        allocs.forEach((k, v) -> {
+            log.info("Allocation {} : {}", k, v);
+            assertEquals(v.type(), DHCP.MsgType.DHCPNAK);
+            assertEquals(v.macAddress(), CLIENT_MAC);
+            assertEquals(v.location(), fakeCp);
+            assertEquals(v.circuitId(), fakeCircuitId);
+        });
     }
 
     /**
@@ -276,11 +377,12 @@
 
         Ethernet declinePacket = constructDhcpDeclinePacket(CLIENT_MAC);
 
-        sendPacket(declinePacket, ConnectPoint.deviceConnectPoint(OLT_DEV_ID + "/" + 1));
+        sendPacket(declinePacket.duplicate(),
+                   ConnectPoint.deviceConnectPoint(OLT_DEV_ID + "/" + 1));
 
         Ethernet declineRelayed = (Ethernet) getPacket();
         compareClientPackets(declinePacket, declineRelayed);
-        checkAllocation(DHCP.MsgType.DHCPDECLINE);
+        checkAllocation(DHCP.MsgType.DHCPDECLINE, CLIENT_CIRCUIT_ID);
     }
 
     /**
@@ -352,18 +454,31 @@
     }
 
     public void compareClientPackets(Ethernet sent, Ethernet relayed) {
-        sent.setSourceMACAddress(OLT_MAC_ADDRESS);
-        sent.setQinQVID(CLIENT_S_TAG.toShort());
-        sent.setVlanID(CLIENT_C_TAG.toShort());
-        sent.setPriorityCode((byte) CLIENT_C_PBIT);
+        compareClientPackets(sent, relayed, CLIENT_S_TAG, CLIENT_C_TAG,
+                             CLIENT_C_PBIT, CLIENT_CIRCUIT_ID,
+                             CLIENT_REMOTE_ID);
+    }
+
+    public void compareClientPackets(Ethernet sent, Ethernet relayed,
+                                     VlanId expectedQinQ,
+                                     VlanId expectedVlan, short expectedPcp,
+                                     String expectedCircuitId,
+                                     String expectedRemoteId) {
+        // convert the sent packet to the expected relayed packet
+        sent.setSourceMACAddress(OLT_MAC_ADDRESS); // due to netconfig test in setup
+        sent.setQinQVID(expectedQinQ.toShort());
+        sent.setQinQTPID((short) 0x8100);
+        sent.setVlanID(expectedVlan.toShort());
+        sent.setPriorityCode((byte) expectedPcp);
 
         IPv4 ipv4Packet = (IPv4) sent.getPayload();
         UDP udpPacket = (UDP) ipv4Packet.getPayload();
         DHCP dhcpPacket = (DHCP) udpPacket.getPayload();
-
         List<DhcpOption> options = Lists.newArrayList(dhcpPacket.getOptions());
-        DhcpOption82 option82 = new DhcpOption82();
-        option82.setAgentCircuitId(CLIENT_CIRCUIT_ID);
+
+        DhcpOption82Data option82 = new DhcpOption82Data();
+        option82.setAgentCircuitId(expectedCircuitId);
+        option82.setAgentRemoteId(expectedRemoteId);
 
         DhcpOption option = new DhcpOption()
                 .setCode(DHCP.DHCPOptionCode.OptionCode_CircuitID.getValue())
@@ -372,17 +487,54 @@
 
         options.add(options.size() - 1, option);
         dhcpPacket.setOptions(options);
-        assertEquals(sent, relayed);
 
+        byte[] sb = sent.serialize();
+        Ethernet expectedPacket = null;
+        try {
+            expectedPacket = Ethernet.deserializer()
+                    .deserialize(sb, 0, sb.length);
+        } catch (DeserializationException e) {
+            log.error("exeption: {}", e.getMessage());
+            fail();
+        }
+        verifyDhcpOptions(expectedPacket, relayed);
+        assertEquals(expectedPacket, relayed);
+    }
+
+    public void verifyDhcpOptions(Ethernet expected, Ethernet relayed) {
+        DHCP de = ((DHCP) ((UDP) ((IPv4) expected.getPayload()).getPayload())
+                .getPayload());
+        DHCP dr = ((DHCP) ((UDP) ((IPv4) relayed.getPayload()).getPayload())
+                .getPayload());
+        List<DhcpOption> del = de.getOptions();
+        List<DhcpOption> der = dr.getOptions();
+        assertEquals(del.size(), der.size());
+        for (int i = 0; i < del.size(); i++) {
+            assertEquals(del.get(i), der.get(i));
+        }
     }
 
     public void compareServerPackets(Ethernet sent, Ethernet relayed) {
+        compareServerPackets(sent, relayed, CLIENT_C_TAG, CLIENT_C_PBIT);
+    }
 
+    public void compareServerPackets(Ethernet sent, Ethernet relayed,
+                                     VlanId expectedVlan, short expectedPcp) {
         try {
+            // modify sent packet to create expected packet
             sent.setDestinationMACAddress(CLIENT_MAC);
             sent.setQinQVID(NOT_PROVIDED);
             sent.setQinQPriorityCode((byte) NOT_PROVIDED);
-            sent.setVlanID(CLIENT_C_TAG.toShort());
+            sent.setVlanID(expectedVlan.toShort());
+            sent.setPriorityCode((byte) expectedPcp);
+            DHCP d = ((DHCP) ((UDP) ((IPv4) sent.getPayload()).getPayload())
+                    .getPayload());
+            List<DhcpOption> newOptions = d.getOptions().stream()
+                    .filter(option -> option
+                            .getCode() != DHCP.DHCPOptionCode.OptionCode_CircuitID
+                                    .getValue())
+                    .collect(Collectors.toList());
+            d.setOptions(newOptions);
 
             final ByteBuffer byteBuffer = ByteBuffer.wrap(sent.serialize());
             Ethernet expectedPacket = Ethernet.deserializer().deserialize(byteBuffer.array(),
diff --git a/app/src/test/java/org/opencord/dhcpl2relay/impl/DhcpL2RelayTestBase.java b/app/src/test/java/org/opencord/dhcpl2relay/impl/DhcpL2RelayTestBase.java
index 20ac9f5..df480a7 100755
--- a/app/src/test/java/org/opencord/dhcpl2relay/impl/DhcpL2RelayTestBase.java
+++ b/app/src/test/java/org/opencord/dhcpl2relay/impl/DhcpL2RelayTestBase.java
@@ -80,6 +80,7 @@
 import org.onosproject.net.packet.PacketProcessor;
 import org.onosproject.net.packet.PacketServiceAdapter;
 import org.onosproject.net.provider.ProviderId;
+import org.opencord.dhcpl2relay.impl.packet.DhcpOption82Data;
 import org.opencord.sadis.BandwidthProfileInformation;
 import org.opencord.sadis.BaseInformationService;
 import org.opencord.sadis.SadisService;
@@ -108,6 +109,9 @@
     static final String CLIENT_ID_1 = "SUBSCRIBER_ID_1";
     static final String CLIENT_NAS_PORT_ID = "PON 1/1";
     static final String CLIENT_CIRCUIT_ID = "CIR-PON 1/1";
+    static final String CLIENT32_CIRCUIT_ID = "";
+    static final String CLIENT4112_CIRCUIT_ID = null;
+    public static final String CLIENT_REMOTE_ID = "I am an RG";
     static final short NOT_PROVIDED = 0;
 
     static final MacAddress CLIENT_MAC = MacAddress.valueOf("00:00:00:00:00:01");
@@ -131,6 +135,7 @@
     static final DefaultAnnotations DEVICE_ANNOTATIONS = DefaultAnnotations.builder()
             .set(AnnotationKeys.PROTOCOL, SCHEME_NAME.toUpperCase()).build();
 
+
     List<BasePacket> savedPackets = new LinkedList<>();
     PacketProcessor packetProcessor;
 
@@ -329,10 +334,10 @@
                         CLIENT_S_TAG, CLIENT_NAS_PORT_ID, CLIENT_CIRCUIT_ID, null, null, -1);
         DhcpL2RelayTestBase.MockSubscriberAndDeviceInformation sub32 =
                 new DhcpL2RelayTestBase.MockSubscriberAndDeviceInformation("ALPHe3d1cea3-1", VlanId.vlanId((short) 801),
-                        VlanId.vlanId((short) 111), CLIENT_NAS_PORT_ID, CLIENT_CIRCUIT_ID, null, null, -1);
+                        VlanId.vlanId((short) 111), CLIENT_NAS_PORT_ID, CLIENT32_CIRCUIT_ID, null, null, -1);
         DhcpL2RelayTestBase.MockSubscriberAndDeviceInformation sub4112 =
                 new DhcpL2RelayTestBase.MockSubscriberAndDeviceInformation("ALPHe3d1ceb7-1", VlanId.vlanId((short) 101),
-                        VlanId.vlanId((short) 222), CLIENT_NAS_PORT_ID, CLIENT_CIRCUIT_ID, null, null, -1);
+                        VlanId.vlanId((short) 222), CLIENT_NAS_PORT_ID, CLIENT4112_CIRCUIT_ID, null, null, -1);
         @Override
         public SubscriberAndDeviceInformation get(String id) {
             if (id.equals(OLT_DEV_ID)) {
@@ -368,22 +373,29 @@
             this.setId(id);
             this.setIPAddress(ipAddress);
             this.setNasPortId(nasPortId);
-            this.setCircuitId(circuitId);
             this.setUplinkPort(uplinkPort);
+            this.setCircuitId(circuitId);
 
             List<UniTagInformation> uniTagInformationList = new ArrayList<>();
 
-            UniTagInformation uniTagInformation = new UniTagInformation.Builder()
+            UniTagInformation.Builder b = new UniTagInformation.Builder()
                     .setPonCTag(cTag)
                     .setPonSTag(sTag)
                     .setUsPonCTagPriority(CLIENT_C_PBIT)
-                    .setIsDhcpRequired(true)
-                    .build();
-            uniTagInformationList.add(uniTagInformation);
+                    .setIsDhcpRequired(true);
+
+            if (id.equals("ALPHe3d1ceb7-1")) {
+                // null remoteId, ds pbit is defined
+                b.setDsPonCTagPriority(5);
+            } else {
+                this.setRemoteId(CLIENT_REMOTE_ID);
+            }
+
+            uniTagInformationList.add(b.build());
 
             if (id.equals("ALPHe3d1cea3-1")) {
                 // a second service on the same UNI
-                uniTagInformation = new UniTagInformation.Builder()
+                UniTagInformation uniTagInformation = new UniTagInformation.Builder()
                         .setPonCTag(VlanId.vlanId(((short) (cTag.toShort() + 1))))
                         .setPonSTag(sTag)
                         .setUsPonCTagPriority(CLIENT_C_PBIT)
@@ -720,14 +732,16 @@
      */
     Ethernet constructDhcpDiscoverPacket(MacAddress clientMac) {
         Ethernet pkt = construcEthernetPacket(clientMac, MacAddress.BROADCAST,
-                "255.255.255.255", DHCP.OPCODE_REQUEST, clientMac,
-                Ip4Address.valueOf("0.0.0.0"));
+                                              "255.255.255.255",
+                                              DHCP.OPCODE_REQUEST, clientMac,
+                                              Ip4Address.valueOf("0.0.0.0"));
 
         IPv4 ipv4Packet = (IPv4) pkt.getPayload();
         UDP udpPacket = (UDP) ipv4Packet.getPayload();
         DHCP dhcpPacket = (DHCP) udpPacket.getPayload();
 
         dhcpPacket.setOptions(constructDhcpOptions(DHCP.MsgType.DHCPDISCOVER));
+        log.info("Sending discover packet {}", dhcpPacket.getOptions());
 
         return pkt;
     }
@@ -748,7 +762,6 @@
         DHCP dhcpPacket = (DHCP) udpPacket.getPayload();
 
         dhcpPacket.setOptions(constructDhcpOptions(DHCP.MsgType.DHCPDISCOVER));
-
         return pkt;
     }
 
@@ -786,7 +799,7 @@
         DHCP dhcpPacket = (DHCP) udpPacket.getPayload();
 
         dhcpPacket.setOptions(constructDhcpOptions(DHCP.MsgType.DHCPOFFER));
-
+        log.info("Sending offer packet {}", dhcpPacket.getOptions());
         return pkt;
     }
 
@@ -815,10 +828,13 @@
      * @return Ethernet packet
      */
     Ethernet constructDhcpNakPacket(MacAddress servMac, MacAddress clientMac,
-                                    String ipAddress, String dhcpClientIpAddress) {
+                                    String ipAddress, String dhcpClientIpAddress,
+                                    VlanId clientVlan, short clientPcp) {
 
-        Ethernet pkt = construcEthernetPacket(servMac, clientMac, ipAddress, DHCP.OPCODE_REPLY,
-                clientMac, Ip4Address.valueOf(dhcpClientIpAddress));
+        Ethernet pkt = constructEthernetPacket(servMac, clientMac, ipAddress,
+                                DHCP.OPCODE_REPLY, clientMac,
+                                Ip4Address.valueOf(dhcpClientIpAddress),
+                                clientVlan, clientPcp);
 
         IPv4 ipv4Packet = (IPv4) pkt.getPayload();
         UDP udpPacket = (UDP) ipv4Packet.getPayload();
@@ -876,6 +892,47 @@
         option.setData(optionData);
         optionList.add(option);
 
+        // Tests app defined Option82
+        if (packetType.equals(DHCP.MsgType.DHCPOFFER)) {
+            option = new DhcpOption();
+            option.setCode(DHCP.DHCPOptionCode.OptionCode_CircuitID.getValue());
+            DhcpOption82Data option82 = new DhcpOption82Data();
+            option82.setAgentCircuitId(OLT_DEV_ID + "/" + CLIENT_PORT + ":vlan"
+                    + CLIENT_C_TAG + ":pcp" + CLIENT_C_PBIT);
+            option82.setAgentRemoteId("bababababa");
+            option.setData(option82.toByteArray());
+            option.setLength(option82.length());
+            log.info("Added option82 {}", option);
+            optionList.add(option);
+        }
+        // Tests operator configured Option82, resulting in host lookup
+        if (packetType.equals(DHCP.MsgType.DHCPACK)) {
+            option = new DhcpOption();
+            option.setCode(DHCP.DHCPOptionCode.OptionCode_CircuitID.getValue());
+            DhcpOption82Data option82 = new DhcpOption82Data();
+            option82.setAgentCircuitId(CLIENT_CIRCUIT_ID);
+            option82.setAgentRemoteId(CLIENT_REMOTE_ID);
+            option.setData(option82.toByteArray());
+            option.setLength(option82.length());
+            log.info("Added option82 {}", option);
+            optionList.add(option);
+        }
+        // Tests app-defined option82, but uses incorrect connectPoint - packet
+        // should still be forwarded to this connectPoint (ie without host lookup).
+        // Also pbit in circuitId is -1, which means original pbit should be retained
+        // Finally remoteId is missing
+        if (packetType.equals(DHCP.MsgType.DHCPNAK)) {
+            option = new DhcpOption();
+            option.setCode(DHCP.DHCPOptionCode.OptionCode_CircuitID.getValue());
+            DhcpOption82Data option82 = new DhcpOption82Data();
+            option82.setAgentCircuitId("of:0000b86a974385f7/32" + ":vlan"
+                    + VlanId.vlanId((short) 111) + ":pcp-1");
+            option.setData(option82.toByteArray());
+            option.setLength(option82.length());
+            log.info("Added option82 {}", option);
+            optionList.add(option);
+        }
+
         // End Option.
         option = new DhcpOption();
         option.setCode(DHCP.DHCPOptionCode.OptionCode_END.getValue());