[CORD-3101]Implement OVS OFDPA driver for double-vlan termination

Change-Id: If627ee1b2d6931d1428e5a35eec802cca28c2324
diff --git a/drivers/default/src/main/java/org/onosproject/driver/extensions/OvsOfdpaExtensionSelectorInterpreter.java b/drivers/default/src/main/java/org/onosproject/driver/extensions/OvsOfdpaExtensionSelectorInterpreter.java
new file mode 100644
index 0000000..261c8ba
--- /dev/null
+++ b/drivers/default/src/main/java/org/onosproject/driver/extensions/OvsOfdpaExtensionSelectorInterpreter.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.driver.extensions;
+
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.behaviour.ExtensionSelectorResolver;
+import org.onosproject.net.driver.AbstractHandlerBehaviour;
+import org.onosproject.net.flow.criteria.ExtensionSelector;
+import org.onosproject.net.flow.criteria.ExtensionSelectorType;
+import org.onosproject.net.flow.criteria.ExtensionSelectorType.ExtensionSelectorTypes;
+import org.onosproject.openflow.controller.ExtensionSelectorInterpreter;
+import org.projectfloodlight.openflow.protocol.OFFactory;
+import org.projectfloodlight.openflow.protocol.match.MatchField;
+import org.projectfloodlight.openflow.protocol.oxm.OFOxm;
+import org.projectfloodlight.openflow.protocol.oxm.OFOxmOfdpaActsetOutput;
+import org.projectfloodlight.openflow.types.U32;
+
+/**
+ * Interpreter for OFDPA OpenFlow selector extensions for OVS.
+ */
+public class OvsOfdpaExtensionSelectorInterpreter extends AbstractHandlerBehaviour
+        implements ExtensionSelectorInterpreter, ExtensionSelectorResolver {
+
+    @Override
+    public boolean supported(ExtensionSelectorType extensionSelectorType) {
+        if (extensionSelectorType.equals(
+                ExtensionSelectorTypes.OFDPA_MATCH_ACTSET_OUTPUT.type())) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public OFOxm<?> mapSelector(OFFactory factory, ExtensionSelector extensionSelector) {
+        ExtensionSelectorType type = extensionSelector.type();
+        if (type.equals(ExtensionSelectorTypes.OFDPA_MATCH_ACTSET_OUTPUT.type())) {
+            PortNumber port = ((OfdpaMatchActsetOutput) extensionSelector).port();
+            return factory.oxms().ofdpaActsetOutput(U32.of(port.toLong()));
+        }
+        throw new UnsupportedOperationException(
+                "Unexpected ExtensionSelector: " + extensionSelector.toString());
+    }
+
+    @Override
+    public ExtensionSelector mapOxm(OFOxm<?> oxm) {
+        if (oxm.getMatchField().equals(MatchField.OFDPA_ACTSET_OUTPUT)) {
+            U32 portNumberU32 = ((OFOxmOfdpaActsetOutput) oxm).getValue();
+            PortNumber portNumber = PortNumber.portNumber(portNumberU32.getValue());
+            return new OfdpaMatchActsetOutput(portNumber);
+        }
+        throw new UnsupportedOperationException(
+                "Unexpected OXM: " + oxm.toString());
+    }
+
+    @Override
+    public ExtensionSelector getExtensionSelector(ExtensionSelectorType type) {
+        if (type.equals(ExtensionSelectorTypes.OFDPA_MATCH_ACTSET_OUTPUT.type())) {
+            return new OfdpaMatchActsetOutput();
+        }
+        throw new UnsupportedOperationException(
+                "Driver does not support extension type " + type.toString());
+    }
+}
diff --git a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/CpqdOfdpa2GroupHandler.java b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/CpqdOfdpa2GroupHandler.java
index 078e120..9b1d283 100644
--- a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/CpqdOfdpa2GroupHandler.java
+++ b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/CpqdOfdpa2GroupHandler.java
@@ -56,6 +56,9 @@
     protected GroupInfo createL2L3Chain(TrafficTreatment treatment, int nextId,
                                         ApplicationId appId, boolean mpls,
                                         TrafficSelector meta) {
+        if (createUnfiltered(treatment, meta)) {
+            return createUnfilteredL2L3Chain(treatment, nextId, appId);
+        }
         // for the l2interface group, get vlan and port info
         // for the outer group, get the src/dst mac, and vlan info
         TrafficTreatment.Builder outerTtb = DefaultTrafficTreatment.builder();
@@ -281,4 +284,105 @@
         }
         super.processHashedNextObjective(nextObjective);
     }
+
+    /**
+     * Internal implementation of createL2L3Chain to handle double-tagged vlan.
+     * L3UG Group carries dummyVlanId and output port information in its groupId,
+     * and does not set vlan.
+     * L2UG Group only has OUTPUT instruction.
+     *
+     * @param treatment that needs to be broken up to create the group chain
+     * @param nextId of the next objective that needs this group chain
+     * @param appId of the application that sent this next objective
+     * @return GroupInfo containing the GroupDescription of the
+     *         L2 Unfiltered Interface group(inner) and the GroupDescription of the (outer)
+     *         L3Unicast group. May return null if there is an error in processing the chain.
+     */
+    private GroupInfo createUnfilteredL2L3Chain(TrafficTreatment treatment, int nextId,
+                                                ApplicationId appId) {
+        // for the l2 unfiltered interface group, get port info
+        // for the l3 unicast group, get the src/dst mac, and vlan info
+        TrafficTreatment.Builder outerTtb = DefaultTrafficTreatment.builder();
+        TrafficTreatment.Builder innerTtb = DefaultTrafficTreatment.builder();
+        VlanId vlanId = VlanId.NONE;
+        long portNum = 0;
+        MacAddress srcMac;
+        MacAddress dstMac;
+        for (Instruction ins : treatment.allInstructions()) {
+            if (ins.type() == Instruction.Type.L2MODIFICATION) {
+                L2ModificationInstruction l2ins = (L2ModificationInstruction) ins;
+                switch (l2ins.subtype()) {
+                    case ETH_DST:
+                        dstMac = ((L2ModificationInstruction.ModEtherInstruction) l2ins).mac();
+                        outerTtb.setEthDst(dstMac);
+                        break;
+                    case ETH_SRC:
+                        srcMac = ((L2ModificationInstruction.ModEtherInstruction) l2ins).mac();
+                        outerTtb.setEthSrc(srcMac);
+                        break;
+                    case VLAN_ID:
+                        vlanId = ((L2ModificationInstruction.ModVlanIdInstruction) l2ins).vlanId();
+                        break;
+                    default:
+                        break;
+                }
+            } else if (ins.type() == Instruction.Type.OUTPUT) {
+                portNum = ((Instructions.OutputInstruction) ins).port().toLong();
+                innerTtb.add(ins);
+            } else {
+                log.debug("Driver does not handle this type of TrafficTreatment"
+                                  + " instruction in l2l3chain:  {} - {}", ins.type(),
+                          ins);
+            }
+        }
+
+        // assemble information for ofdpa l2 unfiltered interface group
+        int l2groupId = l2UnfilteredGroupId(portNum);
+        // a globally unique groupkey that is different for ports in the same device,
+        // but different for the same portnumber on different devices. Also different
+        // for the various group-types created out of the same next objective.
+        int l2gk = l2UnfilteredGroupKey(deviceId, portNum);
+        final GroupKey l2groupkey = new DefaultGroupKey(Ofdpa2Pipeline.appKryo.serialize(l2gk));
+
+        // assemble information for outer group (L3Unicast)
+        GroupDescription outerGrpDesc;
+        int l3groupId = doubleVlanL3UnicastGroupId(vlanId, portNum);
+        final GroupKey l3groupkey = new DefaultGroupKey(
+                Ofdpa3Pipeline.appKryo.serialize(doubleVlanL3UnicastGroupKey(deviceId, vlanId, portNum)));
+        outerTtb.group(new GroupId(l2groupId));
+        // create the l3unicast group description to wait for the
+        // l2 unfiltered interface group to be processed
+        GroupBucket l3unicastGroupBucket =
+                DefaultGroupBucket.createIndirectGroupBucket(outerTtb.build());
+        outerGrpDesc = new DefaultGroupDescription(
+                deviceId,
+                GroupDescription.Type.INDIRECT,
+                new GroupBuckets(Collections.singletonList(l3unicastGroupBucket)),
+                l3groupkey,
+                l3groupId,
+                appId);
+        log.debug("Trying L3Unicast: device:{} gid:{} gkey:{} nextid:{}",
+                  deviceId, Integer.toHexString(l3groupId),
+                  l3groupkey, nextId);
+
+        // store l2groupkey with the groupChainElem for the outer-group that depends on it
+        OfdpaGroupHandlerUtility.GroupChainElem gce = new OfdpaGroupHandlerUtility.GroupChainElem(
+                outerGrpDesc, 1, false, deviceId);
+        updatePendingGroups(l2groupkey, gce);
+
+        // create group description for the inner l2 unfiltered interface group
+        GroupBucket l2InterfaceGroupBucket =
+                DefaultGroupBucket.createIndirectGroupBucket(innerTtb.build());
+        GroupDescription l2groupDescription =
+                new DefaultGroupDescription(deviceId,
+                                            GroupDescription.Type.INDIRECT,
+                                            new GroupBuckets(Collections.singletonList(l2InterfaceGroupBucket)),
+                                            l2groupkey,
+                                            l2groupId,
+                                            appId);
+        log.debug("Trying L2Unfiltered: device:{} gid:{} gkey:{} nextId:{}",
+                  deviceId, Integer.toHexString(l2groupId), l2groupkey, nextId);
+        return new OfdpaGroupHandlerUtility.GroupInfo(l2groupDescription, outerGrpDesc);
+    }
+
 }
diff --git a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/CpqdOfdpa2Pipeline.java b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/CpqdOfdpa2Pipeline.java
index 72b2e8a..898ec44 100644
--- a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/CpqdOfdpa2Pipeline.java
+++ b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/CpqdOfdpa2Pipeline.java
@@ -25,7 +25,10 @@
 import org.onlab.packet.VlanId;
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.GroupId;
+import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DeviceId;
+import org.onosproject.driver.extensions.OfdpaMatchActsetOutput;
+import org.onosproject.net.Host;
 import org.onosproject.net.Port;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.behaviour.NextGroup;
@@ -50,9 +53,12 @@
 import org.onosproject.net.flow.criteria.PortCriterion;
 import org.onosproject.net.flow.criteria.VlanIdCriterion;
 import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions;
 import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
 import org.onosproject.net.flow.instructions.Instructions.NoActionInstruction;
+import org.onosproject.net.flow.instructions.L2ModificationInstruction;
 import org.onosproject.net.flow.instructions.L3ModificationInstruction;
+import org.onosproject.net.flowobjective.FilteringObjective;
 import org.onosproject.net.flowobjective.ForwardingObjective;
 import org.onosproject.net.flowobjective.ObjectiveError;
 import org.onosproject.net.group.DefaultGroupBucket;
@@ -63,6 +69,7 @@
 import org.onosproject.net.group.GroupBuckets;
 import org.onosproject.net.group.GroupDescription;
 import org.onosproject.net.group.GroupKey;
+import org.onosproject.net.host.HostService;
 import org.onosproject.net.packet.PacketPriority;
 import org.slf4j.Logger;
 
@@ -70,6 +77,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Deque;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Queue;
@@ -78,12 +86,15 @@
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.Optional;
 
 import static org.onlab.packet.IPv6.PROTOCOL_ICMP6;
 import static org.onlab.packet.MacAddress.BROADCAST;
 import static org.onlab.packet.MacAddress.NONE;
 import static org.onlab.util.Tools.groupedThreads;
 import static org.onosproject.driver.pipeline.ofdpa.OfdpaGroupHandlerUtility.*;
+import static org.onosproject.net.flow.criteria.Criterion.Type.VLAN_VID;
+import static org.onosproject.net.flow.instructions.Instruction.Type.L2MODIFICATION;
 import static org.slf4j.LoggerFactory.getLogger;
 
 
@@ -99,6 +110,8 @@
 
     private final Logger log = getLogger(getClass());
 
+    private static final int EGRESS_VLAN_FLOW_TABLE_IN_INGRESS = 31;
+    private static final int UNICAST_ROUTING_TABLE_1 = 32;
     /**
      * Table that determines whether VLAN is popped before punting to controller.
      * <p>
@@ -193,6 +206,458 @@
             super.init(deviceId, context);
         }
     }
+    protected void processFilter(FilteringObjective filteringObjective,
+                                 boolean install,
+                                 ApplicationId applicationId) {
+        if (isDoubleTagged(filteringObjective)) {
+            processDoubleTaggedFilter(filteringObjective, install, applicationId);
+        } else {
+            // If it is not a double-tagged filter, we fall back
+            // to the OFDPA 2.0 pipeline.
+            super.processFilter(filteringObjective, install, applicationId);
+        }
+    }
+
+    /**
+     * Determines if the filtering objective will be used for double-tagged packets.
+     *
+     * @param fob Filtering objective
+     * @return True if the objective was created for double-tagged packets, false otherwise.
+     */
+    private boolean isDoubleTagged(FilteringObjective fob) {
+        return fob.meta() != null &&
+                fob.meta().allInstructions().stream().anyMatch(inst -> inst.type() == L2MODIFICATION
+                        && ((L2ModificationInstruction) inst).subtype() ==
+                        L2ModificationInstruction.L2SubType.VLAN_POP) &&
+                fob.conditions().stream().filter(criterion -> criterion.type() == VLAN_VID).count() == 2;
+    }
+
+    /**
+     * Determines if the forwarding objective will be used for double-tagged packets.
+     *
+     * @param fwd Forwarding objective
+     * @return True if the objective was created for double-tagged packets, false otherwise.
+     */
+    private boolean isDoubleTagged(ForwardingObjective fwd) {
+        if (fwd.nextId() != null) {
+            NextGroup next = getGroupForNextObjective(fwd.nextId());
+            if (next != null) {
+                List<Deque<GroupKey>> gkeys = appKryo.deserialize(next.data());
+                // we only need the top level group's key
+                Group group = groupService.getGroup(deviceId, gkeys.get(0).peekFirst());
+                if (group != null) {
+                    int groupId = group.id().id();
+                    if (((groupId & ~TYPE_MASK) == L3_UNICAST_TYPE) &&
+                            ((groupId & TYPE_L3UG_DOUBLE_VLAN_MASK) == TYPE_L3UG_DOUBLE_VLAN_MASK)) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Configure filtering rules of outer and inner VLAN IDs, and a MAC address.
+     * Filtering happens in three tables (VLAN_TABLE, VLAN_1_TABLE, TMAC_TABLE).
+     *
+     * @param filteringObjective the filtering objective
+     * @param install            true to add, false to remove
+     * @param applicationId      for application programming this filter
+     */
+    private void processDoubleTaggedFilter(FilteringObjective filteringObjective,
+                                           boolean install,
+                                           ApplicationId applicationId) {
+        PortCriterion portCriterion = null;
+        EthCriterion ethCriterion = null;
+        VlanIdCriterion innervidCriterion = null;
+        VlanIdCriterion outerVidCriterion = null;
+        boolean popVlan = false;
+        TrafficTreatment meta = filteringObjective.meta();
+        if (!filteringObjective.key().equals(Criteria.dummy()) &&
+                filteringObjective.key().type() == Criterion.Type.IN_PORT) {
+            portCriterion = (PortCriterion) filteringObjective.key();
+        }
+        if (portCriterion == null) {
+            log.warn("No IN_PORT defined in filtering objective from app: {}" +
+                             "Failed to program VLAN tables.", applicationId);
+            return;
+        } else {
+            log.debug("Received filtering objective for dev/port: {}/{}", deviceId,
+                      portCriterion.port());
+        }
+
+        // meta should have only one instruction, popVlan.
+        if (meta != null && meta.allInstructions().size() == 1) {
+            L2ModificationInstruction l2Inst = (L2ModificationInstruction) meta.allInstructions().get(0);
+            if (l2Inst.subtype().equals(L2ModificationInstruction.L2SubType.VLAN_POP)) {
+                popVlan = true;
+            } else {
+                log.warn("Filtering objective can have only VLAN_POP instruction.");
+                return;
+            }
+        } else {
+            log.warn("Filtering objective should have one instruction.");
+            return;
+        }
+
+        FlowRuleOperations.Builder ops = FlowRuleOperations.builder();
+        for (Criterion criterion : filteringObjective.conditions()) {
+            switch (criterion.type()) {
+                case ETH_DST:
+                case ETH_DST_MASKED:
+                    ethCriterion = (EthCriterion) criterion;
+                    break;
+                case VLAN_VID:
+                    if (innervidCriterion == null) {
+                        innervidCriterion = (VlanIdCriterion) criterion;
+                    } else {
+                        outerVidCriterion = innervidCriterion;
+                        innervidCriterion = (VlanIdCriterion) criterion;
+                    }
+                    break;
+                default:
+                    log.warn("Unsupported filter {}", criterion);
+                    fail(filteringObjective, ObjectiveError.UNSUPPORTED);
+                    return;
+            }
+        }
+
+        if (innervidCriterion == null || outerVidCriterion == null) {
+            log.warn("filtering objective should have two vidCriterion.");
+            return;
+        }
+
+        if (ethCriterion == null || ethCriterion.mac().equals(NONE)) {
+            // NOTE: it is possible that a filtering objective only has vidCriterion
+            log.warn("filtering objective missing dstMac, cannot program TMAC table");
+            return;
+        } else {
+            MacAddress unicastMac = readEthDstFromTreatment(filteringObjective.meta());
+            List<List<FlowRule>> allStages = processEthDstFilter(portCriterion, ethCriterion, innervidCriterion,
+                                                                 innervidCriterion.vlanId(), unicastMac,
+                                                                 applicationId);
+            for (List<FlowRule> flowRules : allStages) {
+                log.trace("Starting a new flow rule stage for TMAC table flow");
+                ops.newStage();
+
+                for (FlowRule flowRule : flowRules) {
+                    log.trace("{} flow rules in TMAC table: {} for dev: {}",
+                              (install) ? "adding" : "removing", flowRules, deviceId);
+                    if (install) {
+                        ops = ops.add(flowRule);
+                    } else {
+                        // NOTE: Only remove TMAC flow when there is no more enabled port within the
+                        // same VLAN on this device if TMAC doesn't support matching on in_port.
+                        if (matchInPortTmacTable()
+                                || (filteringObjective.meta() != null
+                                && filteringObjective.meta().clearedDeferred())) {
+                            ops = ops.remove(flowRule);
+                        } else {
+                            log.debug("Abort TMAC flow removal on {}. Some other ports still share this TMAC flow");
+                        }
+                    }
+                }
+            }
+        }
+
+        List<FlowRule> rules;
+        rules = processDoubleVlanIdFilter(portCriterion, innervidCriterion,
+                                          outerVidCriterion, popVlan, applicationId);
+        for (FlowRule flowRule : rules) {
+            log.trace("{} flow rule in VLAN table: {} for dev: {}",
+                      (install) ? "adding" : "removing", flowRule, deviceId);
+            ops = install ? ops.add(flowRule) : ops.remove(flowRule);
+        }
+
+        // apply filtering flow rules
+        flowRuleService.apply(ops.build(new FlowRuleOperationsContext() {
+            @Override
+            public void onSuccess(FlowRuleOperations ops) {
+                log.debug("Applied {} filtering rules in device {}",
+                          ops.stages().get(0).size(), deviceId);
+                pass(filteringObjective);
+            }
+
+            @Override
+            public void onError(FlowRuleOperations ops) {
+                log.info("Failed to apply all filtering rules in dev {}", deviceId);
+                fail(filteringObjective, ObjectiveError.FLOWINSTALLATIONFAILED);
+            }
+        }));
+
+    }
+    /**
+     * Internal implementation of processDoubleVlanIdFilter.
+     *
+     * @param portCriterion       port on device for which this filter is programmed
+     * @param innerVidCriterion   inner vlan
+     * @param outerVidCriterion   outer vlan
+     * @param popVlan             true if outer vlan header needs to be removed
+     * @param applicationId       for application programming this filter
+     * @return flow rules for port-vlan filters
+     */
+    private List<FlowRule> processDoubleVlanIdFilter(PortCriterion portCriterion,
+                                                     VlanIdCriterion innerVidCriterion,
+                                                     VlanIdCriterion outerVidCriterion,
+                                                     boolean popVlan,
+                                                     ApplicationId applicationId) {
+        List<FlowRule> rules = new ArrayList<>();
+        TrafficSelector.Builder outerSelector = DefaultTrafficSelector.builder();
+        TrafficTreatment.Builder outerTreatment = DefaultTrafficTreatment.builder();
+        TrafficSelector.Builder innerSelector = DefaultTrafficSelector.builder();
+        TrafficTreatment.Builder innerTreatment = DefaultTrafficTreatment.builder();
+
+        VlanId outerVlanId = outerVidCriterion.vlanId();
+        VlanId innerVlanId = innerVidCriterion.vlanId();
+        PortNumber portNumber = portCriterion.port();
+        // Check arguments
+        if (PortNumber.ALL.equals(portNumber)
+                || outerVlanId.equals(VlanId.NONE)
+                || innerVlanId.equals(VlanId.NONE)) {
+            log.warn("Incomplete Filtering Objective. " +
+                             "VLAN Table cannot be programmed for {}", deviceId);
+            return ImmutableList.of();
+        } else {
+            outerSelector.matchInPort(portNumber);
+            innerSelector.matchInPort(portNumber);
+            outerTreatment.transition(VLAN_1_TABLE);
+            innerTreatment.transition(TMAC_TABLE);
+            outerTreatment.writeMetadata(outerVlanId.toShort(), 0xFFF);
+
+            outerSelector.matchVlanId(outerVlanId);
+            innerSelector.matchVlanId(innerVlanId);
+            //force recompilation
+            //FIXME might be issue due tu /fff mask
+            innerSelector.matchMetadata(outerVlanId.toShort());
+
+            if (popVlan) {
+                outerTreatment.popVlan();
+            }
+        }
+
+        // NOTE: for double-tagged packets, restore original outer vlan
+        // before sending it to the controller.
+        if (supportPuntGroup()) {
+            GroupKey groupKey = popVlanPuntGroupKey();
+            Group group = groupService.getGroup(deviceId, groupKey);
+            if (group != null) {
+                // push outer vlan and send to controller
+                TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder()
+                        .matchInPort(portNumber)
+                        .matchVlanId(innerVlanId);
+                Host host = handler().get(HostService.class).getConnectedHosts(ConnectPoint.
+                        deviceConnectPoint(deviceId + "/" + portNumber.toLong())).stream().filter(h ->
+                        h.vlan().equals(outerVlanId)).findFirst().orElse(null);
+                EthType vlanType = EthType.EtherType.VLAN.ethType();
+                if (host != null) {
+                    vlanType = host.tpid();
+                }
+                TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder()
+                        .pushVlan(vlanType).setVlanId(outerVlanId).punt();
+
+                rules.add(DefaultFlowRule.builder()
+                        .forDevice(deviceId)
+                        .withSelector(sbuilder.build())
+                        .withTreatment(tbuilder.build())
+                        .withPriority(PacketPriority.CONTROL.priorityValue())
+                        .fromApp(driverId)
+                        .makePermanent()
+                        .forTable(PUNT_TABLE).build());
+            } else {
+                log.info("popVlanPuntGroup not found in dev:{}", deviceId);
+                return Collections.emptyList();
+            }
+        }
+        FlowRule outerRule = DefaultFlowRule.builder()
+                .forDevice(deviceId)
+                .withSelector(outerSelector.build())
+                .withTreatment(outerTreatment.build())
+                .withPriority(DEFAULT_PRIORITY)
+                .fromApp(applicationId)
+                .makePermanent()
+                .forTable(VLAN_TABLE)
+                .build();
+        FlowRule innerRule = DefaultFlowRule.builder()
+                .forDevice(deviceId)
+                .withSelector(innerSelector.build())
+                .withTreatment(innerTreatment.build())
+                .withPriority(DEFAULT_PRIORITY)
+                .fromApp(applicationId)
+                .makePermanent()
+                .forTable(VLAN_1_TABLE)
+                .build();
+        rules.add(outerRule);
+        rules.add(innerRule);
+
+        return rules;
+    }
+
+    /**
+     * In the OF-DPA 2.0 pipeline, egress forwarding objectives go to the
+     * egress tables.
+     * @param fwd  the forwarding objective of type 'egress'
+     * @return     a collection of flow rules to be sent to the switch. An empty
+     *             collection may be returned if there is a problem in processing
+     *             the flow rule
+     */
+    @Override
+    protected Collection<FlowRule> processEgress(ForwardingObjective fwd) {
+        log.debug("Processing egress forwarding objective:{} in dev:{}",
+                  fwd, deviceId);
+
+        List<FlowRule> rules = new ArrayList<>();
+
+        // Build selector
+        TrafficSelector.Builder sb = DefaultTrafficSelector.builder();
+        VlanIdCriterion vlanIdCriterion = (VlanIdCriterion) fwd.selector().getCriterion(Criterion.Type.VLAN_VID);
+        if (vlanIdCriterion == null) {
+            log.error("Egress forwarding objective:{} must include vlanId", fwd.id());
+            fail(fwd, ObjectiveError.BADPARAMS);
+            return rules;
+        }
+
+        Optional<Instruction> outInstr = fwd.treatment().allInstructions().stream()
+                .filter(instruction -> instruction instanceof Instructions.OutputInstruction).findFirst();
+        if (!outInstr.isPresent()) {
+            log.error("Egress forwarding objective:{} must include output port", fwd.id());
+            fail(fwd, ObjectiveError.BADPARAMS);
+            return rules;
+        }
+
+        PortNumber portNumber = ((Instructions.OutputInstruction) outInstr.get()).port();
+
+        sb.matchVlanId(vlanIdCriterion.vlanId());
+        OfdpaMatchActsetOutput actsetOutput = new OfdpaMatchActsetOutput(portNumber);
+        sb.extension(actsetOutput, deviceId);
+
+        // Build a flow rule for Egress VLAN Flow table
+        TrafficTreatment.Builder tb = DefaultTrafficTreatment.builder();
+        tb.transition(UNICAST_ROUTING_TABLE_1);
+        if (fwd.treatment() != null) {
+            for (Instruction instr : fwd.treatment().allInstructions()) {
+                if (instr instanceof L2ModificationInstruction &&
+                        ((L2ModificationInstruction) instr).subtype() ==
+                                L2ModificationInstruction.L2SubType.VLAN_ID) {
+                    tb.immediate().add(instr);
+                }
+                if (instr instanceof L2ModificationInstruction &&
+                        ((L2ModificationInstruction) instr).subtype() ==
+                                L2ModificationInstruction.L2SubType.VLAN_PUSH) {
+                    EthType ethType = ((L2ModificationInstruction.ModVlanHeaderInstruction) instr).ethernetType();
+                    tb.immediate().pushVlan(ethType);
+                }
+            }
+        }
+
+        FlowRule.Builder ruleBuilder = DefaultFlowRule.builder()
+                .fromApp(fwd.appId())
+                .withPriority(fwd.priority())
+                .forDevice(deviceId)
+                .withSelector(sb.build())
+                .withTreatment(tb.build())
+                .makePermanent()
+                .forTable(EGRESS_VLAN_FLOW_TABLE_IN_INGRESS);
+        rules.add(ruleBuilder.build());
+        return rules;
+    }
+
+    /**
+     * Handles forwarding rules to the IP Unicast Routing.
+     *
+     * @param fwd the forwarding objective
+     * @return A collection of flow rules, or an empty set
+     */
+    protected Collection<FlowRule> processDoubleTaggedFwd(ForwardingObjective fwd) {
+        // inner for UNICAST_ROUTING_TABLE_1, outer for UNICAST_ROUTING_TABLE
+        TrafficSelector selector = fwd.selector();
+        TrafficSelector.Builder sBuilder = DefaultTrafficSelector.builder();
+        TrafficTreatment.Builder innerTtb = DefaultTrafficTreatment.builder();
+        TrafficTreatment.Builder outerTtb = DefaultTrafficTreatment.builder();
+
+        EthTypeCriterion ethType =
+                (EthTypeCriterion) selector.getCriterion(Criterion.Type.ETH_TYPE);
+
+        if (ethType.ethType().toShort() == Ethernet.TYPE_IPV4) {
+            sBuilder.matchEthType(Ethernet.TYPE_IPV4);
+            sBuilder.matchVlanId(VlanId.ANY);
+            IpPrefix ipv4Dst = ((IPCriterion) selector.getCriterion(Criterion.Type.IPV4_DST)).ip();
+            if (!ipv4Dst.isMulticast() && ipv4Dst.prefixLength() == 32) {
+                sBuilder.matchIPDst(ipv4Dst);
+                if (fwd.nextId() != null) {
+                    NextGroup next = getGroupForNextObjective(fwd.nextId());
+                    if (next != null) {
+                        List<Deque<GroupKey>> gkeys = appKryo.deserialize(next.data());
+                        // we only need the top level group's key to point the flow to it
+                        Group group = groupService.getGroup(deviceId, gkeys.get(0).peekFirst());
+                        if (group == null) {
+                            log.warn("Group with key:{} for next-id:{} not found in dev:{}",
+                                     gkeys.get(0).peekFirst(), fwd.nextId(), deviceId);
+                            fail(fwd, ObjectiveError.GROUPMISSING);
+                            return Collections.emptySet();
+                        }
+                        outerTtb.immediate().setVlanId(extractDummyVlanIdFromGroupId(group.id().id()));
+                        //ACTSET_OUTPUT in OVS will match output action in write_action() set.
+                        outerTtb.deferred().setOutput(extractOutputPortFromGroupId(group.id().id()));
+                        outerTtb.transition(EGRESS_VLAN_FLOW_TABLE_IN_INGRESS);
+                        innerTtb.deferred().group(group.id());
+                        innerTtb.transition(ACL_TABLE);
+
+                        FlowRule.Builder innerRuleBuilder = DefaultFlowRule.builder()
+                                .fromApp(fwd.appId())
+                                .withPriority(fwd.priority())
+                                .forDevice(deviceId)
+                                .withSelector(sBuilder.build())
+                                .withTreatment(innerTtb.build())
+                                .forTable(UNICAST_ROUTING_TABLE_1);
+                        if (fwd.permanent()) {
+                            innerRuleBuilder.makePermanent();
+                        } else {
+                            innerRuleBuilder.makeTemporary(fwd.timeout());
+                        }
+                        Collection<FlowRule> flowRuleCollection = new HashSet<>();
+                        flowRuleCollection.add(innerRuleBuilder.build());
+
+                        FlowRule.Builder outerRuleBuilder = DefaultFlowRule.builder()
+                                .fromApp(fwd.appId())
+                                .withPriority(fwd.priority())
+                                .forDevice(deviceId)
+                                .withSelector(sBuilder.build())
+                                .withTreatment(outerTtb.build())
+                                .forTable(UNICAST_ROUTING_TABLE);
+                        if (fwd.permanent()) {
+                            outerRuleBuilder.makePermanent();
+                        } else {
+                            outerRuleBuilder.makeTemporary(fwd.timeout());
+                        }
+                        flowRuleCollection.add(innerRuleBuilder.build());
+                        flowRuleCollection.add(outerRuleBuilder.build());
+                        return flowRuleCollection;
+                    } else {
+                        log.warn("Cannot find group for nextId:{} in dev:{}. Aborting fwd:{}",
+                                 fwd.nextId(), deviceId, fwd.id());
+                        fail(fwd, ObjectiveError.FLOWINSTALLATIONFAILED);
+                        return Collections.emptySet();
+                    }
+                } else {
+                    log.warn("NextId is not specified in fwd:{}", fwd.id());
+                    fail(fwd, ObjectiveError.FLOWINSTALLATIONFAILED);
+                    return Collections.emptySet();
+                }
+            }
+        }
+        return Collections.emptySet();
+    }
+
+    private static VlanId extractDummyVlanIdFromGroupId(int groupId) {
+        short vlanId = (short) ((groupId & 0x7FF8000) >> 15);
+        return VlanId.vlanId(vlanId);
+    }
+
+    private static PortNumber extractOutputPortFromGroupId(int groupId) {
+        return PortNumber.portNumber(groupId & 0x7FFF);
+    }
+
     /*
      * Cpqd emulation does not require the non OF-standard rules for
      * matching untagged packets that ofdpa uses.
@@ -528,6 +993,9 @@
      */
     @Override
     protected Collection<FlowRule> processEthTypeSpecific(ForwardingObjective fwd) {
+        if (isDoubleTagged(fwd)) {
+            return processDoubleTaggedFwd(fwd);
+        }
         TrafficSelector selector = fwd.selector();
         EthTypeCriterion ethType =
                 (EthTypeCriterion) selector.getCriterion(Criterion.Type.ETH_TYPE);
@@ -682,7 +1150,7 @@
         EthCriterion ethCriterion = (EthCriterion) selector
                 .getCriterion(Criterion.Type.ETH_DST);
         VlanIdCriterion vlanIdCriterion = (VlanIdCriterion) selector
-                .getCriterion(Criterion.Type.VLAN_VID);
+                .getCriterion(VLAN_VID);
 
         if (vlanIdCriterion == null) {
             log.warn("Forwarding objective for bridging requires vlan. Not "
@@ -841,9 +1309,12 @@
     protected void initializePipeline() {
         initTableMiss(PORT_TABLE, VLAN_TABLE, null);
         initTableMiss(VLAN_TABLE, ACL_TABLE, null);
+        initTableMiss(VLAN_1_TABLE, ACL_TABLE, null);
         initTableMiss(TMAC_TABLE, BRIDGING_TABLE, null);
         initTableMiss(UNICAST_ROUTING_TABLE, ACL_TABLE, null);
         initTableMiss(MULTICAST_ROUTING_TABLE, ACL_TABLE, null);
+        initTableMiss(EGRESS_VLAN_FLOW_TABLE_IN_INGRESS, ACL_TABLE, null);
+        initTableMiss(UNICAST_ROUTING_TABLE_1, ACL_TABLE, null);
         initTableMiss(MPLS_TABLE_0, MPLS_TABLE_1, null);
         initTableMiss(MPLS_TABLE_1, ACL_TABLE, null);
         initTableMiss(BRIDGING_TABLE, ACL_TABLE, null);
diff --git a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/OfdpaGroupHandlerUtility.java b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/OfdpaGroupHandlerUtility.java
index 002df22..31b92a8 100644
--- a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/OfdpaGroupHandlerUtility.java
+++ b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/OfdpaGroupHandlerUtility.java
@@ -27,6 +27,8 @@
 import org.onosproject.net.flow.DefaultTrafficTreatment;
 import org.onosproject.net.flow.TrafficSelector;
 import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criterion;
+import org.onosproject.net.flow.criteria.VlanIdCriterion;
 import org.onosproject.net.flow.instructions.Instruction;
 import org.onosproject.net.flow.instructions.Instructions;
 import org.onosproject.net.flow.instructions.L2ModificationInstruction;
@@ -60,6 +62,7 @@
      * OFDPA requires group-id's to have a certain form.
      * L2 Interface Groups have <4bits-0><12bits-vlanId><16bits-portId>
      * L3 Unicast Groups have <4bits-2><28bits-index>
+     * L3 Unicast Groups for double-vlan have <4bits-2><1bit-1><12bits-vlanId><15bits-portId>
      * MPLS Interface Groups have <4bits-9><4bits:0><24bits-index>
      * L3 ECMP Groups have <4bits-7><28bits-index>
      * L2 Flood Groups have <4bits-4><12bits-vlanId><16bits-index>
@@ -78,6 +81,7 @@
     static final int TYPE_MASK = 0x0fffffff;
     static final int SUBTYPE_MASK = 0x00ffffff;
     static final int TYPE_VLAN_MASK = 0x0000ffff;
+    static final int TYPE_L3UG_DOUBLE_VLAN_MASK = 0x08000000;
 
     static final int THREE_BIT_MASK = 0x0fff;
     static final int FOUR_BIT_MASK = 0xffff;
@@ -710,4 +714,52 @@
             }
         }
     }
+
+    /**
+     * Helper method to decide whether L2 Interface group or L2 Unfiltered group needs to be created.
+     * L2 Unfiltered group will be created if meta has VlanIdCriterion with VlanId.ANY, and
+     * treatment has set Vlan ID action.
+     *
+     * @param treatment  treatment passed in by the application as part of the nextObjective
+     * @param meta       metadata passed in by the application as part of the nextObjective
+     * @return true if L2 Unfiltered group needs to be created, false otherwise.
+     */
+    public static boolean createUnfiltered(TrafficTreatment treatment, TrafficSelector meta) {
+        if (meta == null || treatment == null) {
+            return false;
+        }
+        VlanIdCriterion vlanIdCriterion = (VlanIdCriterion) meta.getCriterion(Criterion.Type.VLAN_VID);
+        if (vlanIdCriterion == null || !vlanIdCriterion.vlanId().equals(VlanId.ANY)) {
+            return false;
+        }
+
+        return treatment.allInstructions().stream()
+                .filter(i -> (i.type() == Instruction.Type.L2MODIFICATION
+                        && ((L2ModificationInstruction) i).subtype() == L2ModificationInstruction.L2SubType.VLAN_ID))
+                .count() == 1;
+    }
+
+    /**
+     * Returns a hash as the L3 Unicast Group Key.
+     *
+     * Keep the lower 6-bit for port since port number usually smaller than 64.
+     * Hash other information into remaining 28 bits.
+     *
+     * @param deviceId Device ID
+     * @param vlanId vlan ID
+     * @param portNumber Port number
+     * @return L3 unicast group key
+     */
+    public static int doubleVlanL3UnicastGroupKey(DeviceId deviceId, VlanId vlanId, long portNumber) {
+        int portLowerBits = (int) portNumber & PORT_LOWER_BITS_MASK;
+        long portHigherBits = portNumber & PORT_HIGHER_BITS_MASK;
+        int hash = Objects.hash(deviceId, portHigherBits, vlanId);
+        return  L3_UNICAST_TYPE | (TYPE_MASK & hash << 6) | portLowerBits;
+    }
+
+    public static int doubleVlanL3UnicastGroupId(VlanId vlanId, long portNum) {
+        // <4bits-2><1bit-1><12bits-vlanId><15bits-portId>
+        return L3_UNICAST_TYPE | 1 << 27 | (vlanId.toShort() << 15) | (int) (portNum & 0x7FFF);
+    }
+
 }
diff --git a/drivers/default/src/main/resources/onos-drivers.xml b/drivers/default/src/main/resources/onos-drivers.xml
index 5b81f09..e0c7693 100644
--- a/drivers/default/src/main/resources/onos-drivers.xml
+++ b/drivers/default/src/main/resources/onos-drivers.xml
@@ -159,6 +159,8 @@
             hwVersion="OFDPA OVS" swVersion="OFDPA OVS">
         <behaviour api="org.onosproject.net.behaviour.Pipeliner"
                    impl="org.onosproject.driver.pipeline.ofdpa.OvsOfdpa2Pipeline"/>
+        <behaviour api="org.onosproject.openflow.controller.ExtensionSelectorInterpreter"
+                   impl="org.onosproject.driver.extensions.OvsOfdpaExtensionSelectorInterpreter" />
     </driver>
 
     <driver name="celestica" extends="default"