[ONOS-7624] Implement egress pipeline programming

- Implemented logic to handle double-tagged host in segmentrouting application.
- Added 'DummyVlanId' to segmentrouting application to keep track of dummy vlan for L3L2Unfiltered group chain and egress tables.
- Implemented L2Unfiltered group and Egress pipeline programming support in OFDPA pipeline.
- Added EGRESS flag to the forwardingObjective to program Egress tables.
- Fixed bugs when handling double-tagged ARP request, to get correct vlan id and reply with double-tagged packet.
- Fixed bugs in BasicHostConfig, to set the value of 'outerTpid' to 0x8100 if it is not specified.
- Fixed build(ARP/ICMP/ICMP6)reply to build double-tagged reply if corresponding request is double-tagged.

Change-Id: I1fdc30b55827c3f73fad9e854bcaa5fb23f7bcd0
diff --git a/app/src/main/java/org/onosproject/segmentrouting/DefaultRoutingHandler.java b/app/src/main/java/org/onosproject/segmentrouting/DefaultRoutingHandler.java
index 31936e2..eb0af67 100644
--- a/app/src/main/java/org/onosproject/segmentrouting/DefaultRoutingHandler.java
+++ b/app/src/main/java/org/onosproject/segmentrouting/DefaultRoutingHandler.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 
+import org.onlab.packet.EthType;
 import org.onlab.packet.Ip4Address;
 import org.onlab.packet.Ip6Address;
 import org.onlab.packet.IpPrefix;
@@ -38,6 +39,7 @@
 import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
 import org.onosproject.segmentrouting.config.DeviceConfiguration;
 import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
+import org.onosproject.segmentrouting.storekey.DummyVlanIdStoreKey;
 import org.onosproject.store.serializers.KryoNamespaces;
 import org.onosproject.store.service.Serializer;
 import org.slf4j.Logger;
@@ -1179,6 +1181,81 @@
     }
 
     /**
+     * Populates IP rules for a route when the next hop is double-tagged.
+     *
+     * @param deviceId  device ID that next hop attaches to
+     * @param prefix    IP prefix of the route
+     * @param hostMac   MAC address of the next hop
+     * @param innerVlan Inner Vlan ID of the next hop
+     * @param outerVlan Outer Vlan ID of the next hop
+     * @param outerTpid Outer TPID of the next hop
+     * @param outPort   port that the next hop attaches to
+     */
+    void populateDoubleTaggedRoute(DeviceId deviceId, IpPrefix prefix, MacAddress hostMac, VlanId innerVlan,
+                                   VlanId outerVlan, EthType outerTpid, PortNumber outPort) {
+        if (srManager.mastershipService.isLocalMaster(deviceId)) {
+            VlanId dummyVlan = srManager.allocateDummyVlanId(
+                    new ConnectPoint(deviceId, outPort), prefix.address());
+            if (!dummyVlan.equals(VlanId.NONE)) {
+                srManager.routingRulePopulator.populateDoubleTaggedRoute(
+                        deviceId, prefix, hostMac, dummyVlan, innerVlan, outerVlan, outerTpid, outPort);
+                srManager.routingRulePopulator.processDoubleTaggedFilter(
+                        deviceId, outPort, outerVlan, innerVlan, true);
+            } else {
+                log.error("Failed to allocate dummy VLAN ID for host {} at {}/{}",
+                          prefix.address(), deviceId, outPort);
+            }
+        }
+    }
+
+    /**
+     * Revokes IP rules for a route when the next hop is double-tagged.
+     *
+     * @param deviceId  device ID that next hop attaches to
+     * @param prefix    IP prefix of the route
+     * @param hostMac   MAC address of the next hop
+     * @param innerVlan Inner Vlan ID of the next hop
+     * @param outerVlan Outer Vlan ID of the next hop
+     * @param outerTpid Outer TPID of the next hop
+     * @param outPort   port that the next hop attaches to
+     */
+    void revokeDoubleTaggedRoute(DeviceId deviceId, IpPrefix prefix, MacAddress hostMac, VlanId innerVlan,
+                                 VlanId outerVlan, EthType outerTpid, PortNumber outPort) {
+        // Revoke route either if this node have the mastership (when device is available) or
+        // if this node is the leader (even when device is unavailable)
+        if (!srManager.mastershipService.isLocalMaster(deviceId)) {
+            if (srManager.deviceService.isAvailable(deviceId)) {
+                // Master node will revoke specified rule.
+                log.debug("This node is not a master for {}, stop revoking route.", deviceId);
+                return;
+            }
+
+            // isLocalMaster will return false when the device is unavailable.
+            // Verify if this node is the leader in that case.
+            NodeId leader = srManager.leadershipService.runForLeadership(
+                    deviceId.toString()).leaderNodeId();
+            if (!srManager.clusterService.getLocalNode().id().equals(leader)) {
+                // Leader node will revoke specified rule.
+                log.debug("This node is not a master for {}, stop revoking route.", deviceId);
+                return;
+            }
+        }
+
+        VlanId dummyVlan = srManager.dummyVlanIdStore().get(new DummyVlanIdStoreKey(
+                new ConnectPoint(deviceId, outPort), prefix.address()));
+        if (dummyVlan == null) {
+            log.error("Failed to get dummyVlanId for host {} at {}/{}.",
+                      prefix.address(), deviceId, outPort);
+        } else {
+            srManager.routingRulePopulator.revokeDoubleTaggedRoute(
+                    deviceId, prefix, hostMac, dummyVlan, innerVlan, outerVlan, outerTpid, outPort);
+            srManager.routingRulePopulator.processDoubleTaggedFilter(
+                    deviceId, outPort, outerVlan, innerVlan, false);
+        }
+    }
+
+
+    /**
      * Remove ECMP graph entry for the given device. Typically called when
      * device is no longer available.
      *
diff --git a/app/src/main/java/org/onosproject/segmentrouting/HostHandler.java b/app/src/main/java/org/onosproject/segmentrouting/HostHandler.java
index 5fdbf69..e14fdc2 100644
--- a/app/src/main/java/org/onosproject/segmentrouting/HostHandler.java
+++ b/app/src/main/java/org/onosproject/segmentrouting/HostHandler.java
@@ -16,6 +16,7 @@
 
 package org.onosproject.segmentrouting;
 
+import org.onlab.packet.EthType;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.IpPrefix;
 import org.onlab.packet.MacAddress;
@@ -94,10 +95,17 @@
         Set<IpAddress> ips = host.ipAddresses();
         log.info("Host {}/{} is added at {}", hostMac, hostVlanId, locations);
 
-        processBridgingRule(location.deviceId(), location.port(), hostMac, hostVlanId, false);
-        ips.forEach(ip ->
+        if (isDoubleTaggedHost(host)) {
+            ips.forEach(ip ->
+                processDoubleTaggedRoutingRule(location.deviceId(), location.port(), hostMac,
+                                               host.innerVlan(), hostVlanId, host.tpid(), ip, false)
+            );
+        } else {
+            processBridgingRule(location.deviceId(), location.port(), hostMac, hostVlanId, false);
+            ips.forEach(ip ->
                 processRoutingRule(location.deviceId(), location.port(), hostMac, hostVlanId, ip, false)
-        );
+            );
+        }
 
         // Use the pair link temporarily before the second location of a dual-homed host shows up.
         // This do not affect single-homed hosts since the flow will be blocked in
@@ -133,10 +141,17 @@
         log.info("Host {}/{} is removed from {}", hostMac, hostVlanId, locations);
 
         locations.forEach(location -> {
-            processBridgingRule(location.deviceId(), location.port(), hostMac, hostVlanId, true);
-            ips.forEach(ip ->
+            if (isDoubleTaggedHost(host)) {
+                ips.forEach(ip ->
+                    processDoubleTaggedRoutingRule(location.deviceId(), location.port(), hostMac,
+                                                   host.innerVlan(), hostVlanId, host.tpid(), ip, true)
+                );
+            } else {
+                processBridgingRule(location.deviceId(), location.port(), hostMac, hostVlanId, true);
+                ips.forEach(ip ->
                     processRoutingRule(location.deviceId(), location.port(), hostMac, hostVlanId, ip, true)
-            );
+                );
+            }
 
             // Also remove redirection flows on the pair device if exists.
             Optional<DeviceId> pairDeviceId = srManager.getPairDeviceId(location.deviceId());
@@ -155,18 +170,20 @@
     }
 
     void processHostMovedEvent(HostEvent event) {
-        MacAddress hostMac = event.subject().mac();
-        VlanId hostVlanId = event.subject().vlan();
         Set<HostLocation> prevLocations = event.prevSubject().locations();
         Set<IpAddress> prevIps = event.prevSubject().ipAddresses();
         Set<HostLocation> newLocations = event.subject().locations();
         Set<IpAddress> newIps = event.subject().ipAddresses();
 
-        processHostMoved(hostMac, hostVlanId, prevLocations, prevIps, newLocations, newIps);
+        processHostMoved(event.subject(), prevLocations, prevIps, newLocations, newIps);
     }
 
-    private void processHostMoved(MacAddress hostMac, VlanId hostVlanId, Set<HostLocation> prevLocations,
-                                  Set<IpAddress> prevIps, Set<HostLocation> newLocations, Set<IpAddress> newIps) {
+    private void processHostMoved(Host host, Set<HostLocation> prevLocations, Set<IpAddress> prevIps,
+                                  Set<HostLocation> newLocations, Set<IpAddress> newIps) {
+        MacAddress hostMac = host.mac();
+        VlanId hostVlanId = host.vlan();
+        EthType hostTpid = host.tpid();
+        boolean doubleTaggedHost = isDoubleTaggedHost(host);
         log.info("Host {}/{} is moved from {} to {}", hostMac, hostVlanId, prevLocations, newLocations);
         Set<DeviceId> newDeviceIds = newLocations.stream().map(HostLocation::deviceId)
                 .collect(Collectors.toSet());
@@ -174,10 +191,15 @@
         // For each old location
         Sets.difference(prevLocations, newLocations).forEach(prevLocation -> {
             // Remove routing rules for old IPs
-            Sets.difference(prevIps, newIps).forEach(ip ->
+            Sets.difference(prevIps, newIps).forEach(ip -> {
+                if (doubleTaggedHost) {
+                    processDoubleTaggedRoutingRule(prevLocation.deviceId(), prevLocation.port(),
+                                                   hostMac, host.innerVlan(), hostVlanId, hostTpid, ip, true);
+                } else {
                     processRoutingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId,
-                            ip, true)
-            );
+                                       ip, true);
+                }
+            });
 
             // Redirect the flows to pair link if configured
             // Note: Do not continue removing any rule
@@ -200,10 +222,15 @@
             // Otherwise, do not remove and let the adding part update the old flow
             if (!newDeviceIds.contains(prevLocation.deviceId())) {
                 processBridgingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId, true);
-                Sets.intersection(prevIps, newIps).forEach(ip ->
-                        processRoutingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId,
-                                ip, true)
-                );
+                Sets.intersection(prevIps, newIps).forEach(ip -> {
+                    if (doubleTaggedHost) {
+                        processDoubleTaggedRoutingRule(prevLocation.deviceId(), prevLocation.port(),
+                                                       hostMac, host.innerVlan(), hostVlanId, hostTpid, ip, true);
+                    } else {
+                        processRoutingRule(prevLocation.deviceId(), prevLocation.port(),
+                                           hostMac, hostVlanId, ip, true);
+                    }
+                });
             }
 
             // Remove bridging rules if new interface vlan is different from old interface vlan
@@ -225,8 +252,13 @@
             Sets.intersection(prevIps, newIps).forEach(ip -> {
                 if (newLocations.stream().noneMatch(newLocation ->
                         srManager.deviceConfiguration.inSameSubnet(newLocation, ip))) {
-                    processRoutingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId,
-                            ip, true);
+                    if (doubleTaggedHost) {
+                        processDoubleTaggedRoutingRule(prevLocation.deviceId(), prevLocation.port(),
+                                                       hostMac, host.innerVlan(), hostVlanId, hostTpid, ip, true);
+                    } else {
+                        processRoutingRule(prevLocation.deviceId(), prevLocation.port(),
+                                           hostMac, hostVlanId, ip, true);
+                    }
                 }
             });
         });
@@ -234,23 +266,38 @@
         // For each new location, add all new IPs.
         Sets.difference(newLocations, prevLocations).forEach(newLocation -> {
             processBridgingRule(newLocation.deviceId(), newLocation.port(), hostMac, hostVlanId, false);
-            newIps.forEach(ip ->
+            newIps.forEach(ip -> {
+                if (doubleTaggedHost) {
+                    processDoubleTaggedRoutingRule(newLocation.deviceId(), newLocation.port(),
+                                                   hostMac, host.innerVlan(), hostVlanId, hostTpid, ip, false);
+                } else {
                     processRoutingRule(newLocation.deviceId(), newLocation.port(), hostMac, hostVlanId,
-                            ip, false)
-            );
+                                       ip, false);
+                }
+            });
         });
 
         // For each unchanged location, add new IPs and remove old IPs.
         Sets.intersection(newLocations, prevLocations).forEach(unchangedLocation -> {
-            Sets.difference(prevIps, newIps).forEach(ip ->
-                    processRoutingRule(unchangedLocation.deviceId(), unchangedLocation.port(), hostMac,
-                            hostVlanId, ip, true)
-            );
+            Sets.difference(prevIps, newIps).forEach(ip -> {
+                 if (doubleTaggedHost) {
+                     processDoubleTaggedRoutingRule(unchangedLocation.deviceId(), unchangedLocation.port(),
+                                                    hostMac, host.innerVlan(), hostVlanId, hostTpid, ip, true);
+                 } else {
+                     processRoutingRule(unchangedLocation.deviceId(), unchangedLocation.port(),
+                                        hostMac, hostVlanId, ip, true);
+                 }
+            });
 
-            Sets.difference(newIps, prevIps).forEach(ip ->
-                    processRoutingRule(unchangedLocation.deviceId(), unchangedLocation.port(), hostMac,
-                        hostVlanId, ip, false)
-            );
+            Sets.difference(newIps, prevIps).forEach(ip -> {
+                if (doubleTaggedHost) {
+                    processDoubleTaggedRoutingRule(unchangedLocation.deviceId(), unchangedLocation.port(),
+                                                   hostMac, host.innerVlan(), hostVlanId, hostTpid, ip, false);
+                } else {
+                    processRoutingRule(unchangedLocation.deviceId(), unchangedLocation.port(),
+                                       hostMac, hostVlanId, ip, false);
+                }
+            });
         });
 
         // ensure dual-homed host locations have viable uplinks
@@ -267,18 +314,31 @@
         Host host = event.subject();
         MacAddress hostMac = host.mac();
         VlanId hostVlanId = host.vlan();
+        EthType hostTpid = host.tpid();
         Set<HostLocation> locations = host.locations();
         Set<IpAddress> prevIps = event.prevSubject().ipAddresses();
         Set<IpAddress> newIps = host.ipAddresses();
         log.info("Host {}/{} is updated", hostMac, hostVlanId);
 
         locations.forEach(location -> {
-            Sets.difference(prevIps, newIps).forEach(ip ->
-                    processRoutingRule(location.deviceId(), location.port(), hostMac, hostVlanId, ip, true)
-            );
-            Sets.difference(newIps, prevIps).forEach(ip ->
-                    processRoutingRule(location.deviceId(), location.port(), hostMac, hostVlanId, ip, false)
-            );
+            Sets.difference(prevIps, newIps).forEach(ip -> {
+                if (isDoubleTaggedHost(host)) {
+                    processDoubleTaggedRoutingRule(location.deviceId(), location.port(), hostMac,
+                                                   host.innerVlan(), hostVlanId, hostTpid, ip, true);
+                } else {
+                    processRoutingRule(location.deviceId(), location.port(), hostMac,
+                                       hostVlanId, ip, true);
+                }
+            });
+            Sets.difference(newIps, prevIps).forEach(ip -> {
+                if (isDoubleTaggedHost(host)) {
+                    processDoubleTaggedRoutingRule(location.deviceId(), location.port(), hostMac,
+                                                   host.innerVlan(), hostVlanId, hostTpid, ip, false);
+                } else {
+                    processRoutingRule(location.deviceId(), location.port(), hostMac,
+                                       hostVlanId, ip, false);
+                }
+            });
         });
 
         // Use the pair link temporarily before the second location of a dual-homed host shows up.
@@ -374,8 +434,8 @@
     }
 
     /**
-     * Populate or revoke a bridging rule on given deviceId that matches given mac, given vlan and
-     * output to given port.
+     * Populates or revokes a bridging rule on given deviceId that matches given mac,
+     * given vlan and output to given port.
      *
      * @param deviceId device ID
      * @param port port
@@ -422,7 +482,39 @@
         }
     }
 
-
+    /**
+     * Populate or revoke a routing rule and egress rules on given deviceId that matches given IP,
+     * to set destination mac to given mac, set inner vlan and outer vlan to given vlans,
+     * set outer TPID, and output to given port.
+     *
+     * @param deviceId  device ID
+     * @param port      port
+     * @param mac       mac address
+     * @param innerVlan inner VLAN ID
+     * @param outerVlan outer VLAN ID
+     * @param outerTpid outer TPID
+     * @param ip        IP address
+     * @param revoke    true to revoke the rule; false to populate
+     */
+    private void processDoubleTaggedRoutingRule(DeviceId deviceId, PortNumber port,
+                                                MacAddress mac, VlanId innerVlan,
+                                                VlanId outerVlan, EthType outerTpid,
+                                                IpAddress ip, boolean revoke) {
+        ConnectPoint location = new ConnectPoint(deviceId, port);
+        if (!srManager.deviceConfiguration.inSameSubnet(location, ip)) {
+            log.info("{} is not included in the subnet config of {}/{}. Ignored.", ip, deviceId, port);
+            return;
+        }
+        log.info("{} routing rule for double-tagged host {} at {}",
+                 revoke ? "Revoking" : "Populating", ip, location);
+        if (revoke) {
+            srManager.defaultRoutingHandler.revokeDoubleTaggedRoute(
+                    deviceId, ip.toIpPrefix(), mac, innerVlan, outerVlan, outerTpid, port);
+        } else {
+            srManager.defaultRoutingHandler.populateDoubleTaggedRoute(
+                    deviceId, ip.toIpPrefix(), mac, innerVlan, outerVlan, outerTpid, port);
+        }
+    }
 
     /**
      * Update forwarding objective for unicast bridging and unicast routing.
@@ -510,4 +602,15 @@
                         .forEach(loc -> dualHomedLocations.add(loc.port())));
         return dualHomedLocations;
     }
+
+    /**
+     * Checks if the given host is double-tagged or not.
+     *
+     * @param host host to check
+     * @return true if it is double-tagged, false otherwise
+     */
+    private boolean isDoubleTaggedHost(Host host) {
+        return !host.innerVlan().equals(VlanId.NONE);
+    }
+
 }
diff --git a/app/src/main/java/org/onosproject/segmentrouting/RoutingRulePopulator.java b/app/src/main/java/org/onosproject/segmentrouting/RoutingRulePopulator.java
index 06b650d..d4a5b65 100644
--- a/app/src/main/java/org/onosproject/segmentrouting/RoutingRulePopulator.java
+++ b/app/src/main/java/org/onosproject/segmentrouting/RoutingRulePopulator.java
@@ -52,6 +52,7 @@
 import org.onosproject.net.flowobjective.ForwardingObjective;
 import org.onosproject.net.flowobjective.ForwardingObjective.Builder;
 import org.onosproject.net.flowobjective.ForwardingObjective.Flag;
+import org.onosproject.segmentrouting.storekey.DummyVlanIdStoreKey;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -76,6 +77,7 @@
 import static org.onlab.packet.ICMP6.ROUTER_SOLICITATION;
 import static org.onlab.packet.IPv6.PROTOCOL_ICMP6;
 import static org.onosproject.segmentrouting.SegmentRoutingManager.INTERNAL_VLAN;
+import static org.onosproject.segmentrouting.SegmentRoutingService.DEFAULT_PRIORITY;
 
 /**
  * Populator of segment routing flow rules.
@@ -418,9 +420,18 @@
                 return null;
             }
         } else {
-            log.warn("Tagged nexthop {}/{} is not allowed on {} without VLAN listed"
-                    + " in tagged vlan", hostMac, hostVlanId, connectPoint);
-            return null;
+            // Internally-assigned dummy VLAN id will be given as hostVlanId
+            // when destination is double-tagged.
+            VlanId vlanId = srManager.dummyVlanIdStore().get(
+                    new DummyVlanIdStoreKey(connectPoint, prefix.address()));
+            if (vlanId != null && vlanId.equals(hostVlanId)) {
+                tbuilder.setVlanId(hostVlanId);
+                mbuilder.matchVlanId(VlanId.ANY);
+            } else {
+                log.warn("Tagged nexthop {}/{} is not allowed on {} without VLAN listed"
+                                 + " in tagged vlan", hostMac, hostVlanId, connectPoint);
+                return null;
+            }
         }
         // if the objective is to revoke an existing rule, and for some reason
         // the next-objective does not exist, then a new one should not be created
@@ -940,6 +951,7 @@
         Set<VlanId> taggedVlans = srManager.getTaggedVlanId(connectPoint);
         VlanId nativeVlan = srManager.getNativeVlanId(connectPoint);
 
+        // Do not configure filter for edge ports where double-tagged hosts are connected.
         if (taggedVlans.size() != 0) {
             // Filter for tagged vlans
             if (!srManager.getTaggedVlanId(connectPoint).stream().allMatch(taggedVlanId ->
@@ -957,8 +969,8 @@
             if (!processSinglePortFiltersInternal(deviceId, portnum, true, untaggedVlan, install)) {
                 return false;
             }
-        } else {
-            // Unconfigured port, use INTERNAL_VLAN
+        } else if (srManager.linkService.getLinks(connectPoint).size() == 0) {
+            // Filter for unconfigured upstream port, using INTERNAL_VLAN
             if (!processSinglePortFiltersInternal(deviceId, portnum, true, INTERNAL_VLAN, install)) {
                 return false;
             }
@@ -1042,6 +1054,70 @@
     }
 
     /**
+     * Creates or removes filtering objectives for a double-tagged host on a port.
+     *
+     * @param deviceId device identifier
+     * @param portNum  port identifier for port to be filtered
+     * @param outerVlan outer VLAN ID
+     * @param innerVlan inner VLAN ID
+     * @param install true to install the filtering objective, false to remove
+     */
+    void processDoubleTaggedFilter(DeviceId deviceId, PortNumber portNum, VlanId outerVlan,
+                                   VlanId innerVlan, boolean install) {
+        FilteringObjective.Builder fob = buildDoubleTaggedFilteringObj(deviceId, portNum, outerVlan, innerVlan);
+        if (fob == null) {
+            // error encountered during build
+            return;
+        }
+        log.debug("{} double-tagged filtering objectives for dev/port: {}/{}",
+                  install ? "Installing" : "Removing", deviceId, portNum);
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Filter for {}/{} {}", deviceId, portNum,
+                                         install ? "installed" : "removed"),
+                (objective, error) -> log.warn("Failed to {} filter for {}/{}: {}",
+                                               install ? "install" : "remove", deviceId, portNum, error));
+        if (install) {
+            srManager.flowObjectiveService.filter(deviceId, fob.add(context));
+        } else {
+            srManager.flowObjectiveService.filter(deviceId, fob.remove(context));
+        }
+    }
+
+    private FilteringObjective.Builder buildDoubleTaggedFilteringObj(DeviceId deviceId, PortNumber portNum,
+                                                                     VlanId outerVlan, VlanId innerVlan) {
+        MacAddress deviceMac;
+        try {
+            deviceMac = config.getDeviceMac(deviceId);
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Processing DoubleTaggedFilters aborted");
+            return null;
+        }
+        FilteringObjective.Builder fob = DefaultFilteringObjective.builder();
+        // Outer vlan id match should be appeared before inner vlan id match.
+        fob.withKey(Criteria.matchInPort(portNum))
+                .addCondition(Criteria.matchEthDst(deviceMac))
+                .addCondition(Criteria.matchVlanId(outerVlan))
+                .addCondition(Criteria.matchVlanId(innerVlan))
+                .withPriority(SegmentRoutingService.DEFAULT_PRIORITY);
+
+        TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+        // Pop outer vlan
+        tBuilder.popVlan();
+
+        // NOTE: Some switch hardware share the same filtering flow among different ports.
+        //       We use this metadata to let the driver know that there is no more enabled port
+        //       within the same VLAN on this device.
+        if (noMoreEnabledPort(deviceId, outerVlan)) {
+            tBuilder.wipeDeferred();
+        }
+
+        fob.withMeta(tBuilder.build());
+
+        fob.permit().fromApp(srManager.appId);
+        return fob;
+    }
+
+    /**
      * Creates a forwarding objective to punt all IP packets, destined to the
      * router's port IP addresses, to the controller. Note that the input
      * port should not be matched on, as these packets can come from any input.
@@ -1447,4 +1523,130 @@
             (srManager.getInternalVlanId(cp) != null && srManager.getInternalVlanId(cp).equals(vlanId))
         );
     }
+
+    /**
+     * Returns a forwarding objective builder for egress forwarding rules.
+     * <p>
+     * The forwarding objective installs flow rules to egress pipeline to push
+     * two vlan headers with given inner, outer vlan ids and outer tpid.
+     *
+     * @param portNumber port where the next hop attaches to
+     * @param dummyVlanId vlan ID of the packet to match
+     * @param innerVlan inner vlan ID of the next hop
+     * @param outerVlan outer vlan ID of the next hop
+     * @param outerTpid outer TPID of the next hop
+     * @return forwarding objective builder
+     */
+    private ForwardingObjective.Builder egressFwdObjBuilder(PortNumber portNumber, VlanId dummyVlanId,
+                                                            VlanId innerVlan, VlanId outerVlan, EthType outerTpid) {
+        TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+        sbuilder.matchVlanId(dummyVlanId);
+        TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+        tbuilder.setOutput(portNumber).setVlanId(innerVlan);
+
+        if (outerTpid.equals(EthType.EtherType.QINQ.ethType())) {
+            tbuilder.pushVlan(outerTpid);
+        } else {
+            tbuilder.pushVlan();
+        }
+
+        tbuilder.setVlanId(outerVlan);
+        return DefaultForwardingObjective.builder()
+                .withSelector(sbuilder.build())
+                .withTreatment(tbuilder.build())
+                .fromApp(srManager.appId)
+                .makePermanent()
+                .withPriority(DEFAULT_PRIORITY)
+                .withFlag(ForwardingObjective.Flag.EGRESS);
+    }
+
+    /**
+     * Populates IP rules for a route that has double-tagged next hop.
+     *
+     * @param deviceId device ID of the device that next hop attaches to
+     * @param prefix IP prefix of the route
+     * @param hostMac MAC address of the next hop
+     * @param dummyVlan Dummy Vlan ID allocated for this route
+     * @param innerVlan inner Vlan ID of the next hop
+     * @param outerVlan outer Vlan ID of the next hop
+     * @param outerTpid outer TPID of the next hop
+     * @param outPort port where the next hop attaches to
+     */
+    void populateDoubleTaggedRoute(DeviceId deviceId, IpPrefix prefix, MacAddress hostMac, VlanId dummyVlan,
+                                   VlanId innerVlan, VlanId outerVlan, EthType outerTpid, PortNumber outPort) {
+        ForwardingObjective.Builder fwdBuilder;
+        log.debug("Populate direct routing entry for double-tagged host route {} at {}:{}",
+                  prefix, deviceId, outPort);
+
+        ForwardingObjective.Builder egressFwdBuilder = egressFwdObjBuilder(
+                outPort, dummyVlan, innerVlan, outerVlan, outerTpid);
+        DefaultObjectiveContext egressFwdContext = new DefaultObjectiveContext(
+                objective -> log.debug("Egress rule for IP {} is populated", prefix.address()),
+                (objective, error) -> {
+                    log.warn("Failed to populate egress rule for IP {}: {}", prefix.address(), error);
+                    srManager.dummyVlanIdStore().remove(new DummyVlanIdStoreKey(
+                            new ConnectPoint(deviceId, outPort), prefix.address()
+                    ));
+                });
+        try {
+            fwdBuilder = routingFwdObjBuilder(deviceId, prefix, hostMac, dummyVlan, outPort, false);
+        } catch (DeviceConfigNotFoundException e) {
+            log.error(e.getMessage() + " Aborting populateDoubleTaggedRoute");
+            return;
+        }
+        if (fwdBuilder == null) {
+            log.error("Aborting double-tagged host routing table entry due to error for dev:{} route:{}",
+                     deviceId, prefix);
+            return;
+        }
+
+        // Egress forwarding objective should be installed after the nextObjective for the output port is installed.
+        // Installation of routingFwdObj will ensure the installation of the nextObjective.
+        int nextId = fwdBuilder.add().nextId();
+        DefaultObjectiveContext context = new DefaultObjectiveContext(objective -> {
+            log.debug("Direct routing rule for double-tagged host route {} populated. nextId={}", prefix, nextId);
+            srManager.flowObjectiveService.forward(deviceId, egressFwdBuilder.add(egressFwdContext));
+        }, (objective, error) ->
+            log.warn("Failed to populate direct routing rule for double-tagged host route {}: {}", prefix, error)
+        );
+        srManager.flowObjectiveService.forward(deviceId, fwdBuilder.add(context));
+        rulePopulationCounter.incrementAndGet();
+    }
+
+    /**
+     * Revokes IP rules for a route that has double-tagged next hop.
+     *
+     * @param deviceId device ID of the device that next hop attaches to
+     * @param prefix IP prefix of the route
+     * @param hostMac MAC address of the next hop
+     * @param hostVlan Vlan ID of the next hop
+     * @param innerVlan inner Vlan ID of the next hop
+     * @param outerVlan outer Vlan ID of the next hop
+     * @param outerTpid outer TPID of the next hop
+     * @param outPort port where the next hop attaches to
+     */
+    void revokeDoubleTaggedRoute(DeviceId deviceId, IpPrefix prefix, MacAddress hostMac,
+                                 VlanId hostVlan, VlanId innerVlan, VlanId outerVlan,
+                                 EthType outerTpid, PortNumber outPort) {
+        revokeRoute(deviceId, prefix, hostMac, hostVlan, outPort);
+
+        DummyVlanIdStoreKey key = new DummyVlanIdStoreKey(
+                new ConnectPoint(deviceId, outPort), prefix.address());
+        VlanId dummyVlanId = srManager.dummyVlanIdStore().get(key);
+        if (dummyVlanId == null) {
+            log.warn("Failed to retrieve dummy VLAN ID for {}/{} and {}",
+                     deviceId, outPort, prefix.address());
+            return;
+        }
+        ForwardingObjective.Builder fob = egressFwdObjBuilder(
+                outPort, dummyVlanId, innerVlan, outerVlan, outerTpid);
+        DefaultObjectiveContext context = new DefaultObjectiveContext(objective -> {
+            log.debug("Egress rule for IP {} revoked", prefix.address());
+            srManager.dummyVlanIdStore().remove(key);
+        }, (objective, error) -> {
+            log.warn("Failed to revoke egress rule for IP {}: {}", prefix.address(), error);
+        });
+        srManager.flowObjectiveService.forward(deviceId, fob.remove(context));
+    }
+
 }
diff --git a/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java b/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
index 0322201..388d213 100644
--- a/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
+++ b/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
@@ -41,6 +41,7 @@
 import org.onosproject.cluster.ClusterEvent;
 import org.onosproject.cluster.ClusterEventListener;
 import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.LeadershipService;
 import org.onosproject.cluster.NodeId;
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.CoreService;
@@ -118,6 +119,7 @@
 import org.onosproject.segmentrouting.pwaas.L2TunnelDescription;
 
 import org.onosproject.segmentrouting.storekey.DestinationSetNextObjectiveStoreKey;
+import org.onosproject.segmentrouting.storekey.DummyVlanIdStoreKey;
 import org.onosproject.segmentrouting.storekey.McastStoreKey;
 import org.onosproject.segmentrouting.storekey.PortNextObjectiveStoreKey;
 import org.onosproject.segmentrouting.storekey.VlanNextObjectiveStoreKey;
@@ -148,6 +150,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 import static com.google.common.base.Preconditions.checkState;
 import static org.onlab.packet.Ethernet.TYPE_ARP;
@@ -222,6 +225,9 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     public WorkPartitionService workPartitionService;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public LeadershipService leadershipService;
+
     @Property(name = "activeProbing", boolValue = true,
             label = "Enable active probing to discover dual-homed hosts.")
     boolean activeProbing = true;
@@ -287,6 +293,13 @@
     private EventuallyConsistentMap<PortNextObjectiveStoreKey, Integer>
             portNextObjStore = null;
 
+    /**
+     * Per port dummy VLAN ID store with (connect point + ip address) as key.
+     * Used to keep track on dummy VLAN ID allocation.
+     */
+    private EventuallyConsistentMap<DummyVlanIdStoreKey, VlanId>
+            dummyVlanIdStore = null;
+
     private EventuallyConsistentMap<String, Tunnel> tunnelStore = null;
     private EventuallyConsistentMap<String, Policy> policyStore = null;
 
@@ -342,6 +355,12 @@
      */
     public static final VlanId INTERNAL_VLAN = VlanId.vlanId((short) 4094);
 
+    /**
+     * Minumum and maximum value of dummy VLAN ID to be allocated.
+     */
+    public static final int MIN_DUMMY_VLAN_ID = 2;
+    public static final int MAX_DUMMY_VLAN_ID = 4093;
+
     @Activate
     protected void activate(ComponentContext context) {
         appId = coreService.registerApplication(APP_NAME);
@@ -379,6 +398,14 @@
                 .withTimestampProvider((k, v) -> new WallClockTimestamp())
                 .build();
 
+        EventuallyConsistentMapBuilder<DummyVlanIdStoreKey, VlanId>
+                dummyVlanIdMapBuilder = storageService.eventuallyConsistentMapBuilder();
+        dummyVlanIdStore = dummyVlanIdMapBuilder
+                .withName("dummyvlanidstore")
+                .withSerializer(createSerializer())
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .build();
+
         EventuallyConsistentMapBuilder<String, Tunnel> tunnelMapBuilder =
                 storageService.eventuallyConsistentMapBuilder();
         tunnelStore = tunnelMapBuilder
@@ -484,7 +511,8 @@
                           L2Tunnel.class,
                           L2TunnelPolicy.class,
                           DefaultL2Tunnel.class,
-                          DefaultL2TunnelPolicy.class
+                          DefaultL2TunnelPolicy.class,
+                          DummyVlanIdStoreKey.class
                 );
     }
 
@@ -524,6 +552,7 @@
         dsNextObjStore.destroy();
         vlanNextObjStore.destroy();
         portNextObjStore.destroy();
+        dummyVlanIdStore.destroy();
         tunnelStore.destroy();
         policyStore.destroy();
 
@@ -774,6 +803,16 @@
     }
 
     /**
+     * Per port dummy VLAN ID store with (connect point + ip address) as key.
+     * Used to keep track on dummy VLAN ID allocation.
+     *
+     * @return dummy vlan id store.
+     */
+    public EventuallyConsistentMap<DummyVlanIdStoreKey, VlanId> dummyVlanIdStore() {
+        return dummyVlanIdStore;
+    }
+
+    /**
      * Returns the MPLS-ECMP configuration which indicates whether ECMP on
      * labeled packets should be programmed or not.
      *
@@ -986,6 +1025,35 @@
         return defaultRoutingHandler;
     }
 
+    /**
+     * Returns new dummy VLAN ID.
+     * Dummy VLAN ID should be unique in each connect point.
+     *
+     * @param cp connect point
+     * @param ipAddress IP address
+     * @return new dummy VLAN ID. Returns VlanId.NONE if no VLAN ID is available.
+     */
+    public synchronized VlanId allocateDummyVlanId(ConnectPoint cp, IpAddress ipAddress) {
+        Set<VlanId> usedVlanId = Sets.union(getVlanPortMap(cp.deviceId()).keySet(),
+                                            dummyVlanIdStore.entrySet().stream().filter(entry ->
+                                                (entry.getKey()).connectPoint().equals(cp))
+                                            .map(Map.Entry::getValue)
+                                            .collect(Collectors.toSet()));
+
+        VlanId dummyVlanId = IntStream.range(MIN_DUMMY_VLAN_ID, MAX_DUMMY_VLAN_ID).mapToObj(
+                i -> VlanId.vlanId((short) (i & 0xFFFF))
+        ).filter(vlanId -> !usedVlanId.contains(vlanId)).findFirst().orElse(VlanId.NONE);
+
+        if (!dummyVlanId.equals(VlanId.NONE)) {
+            this.dummyVlanIdStore.put(new DummyVlanIdStoreKey(cp, ipAddress), dummyVlanId);
+            log.debug("Dummy VLAN ID {} is allocated to {}, {}", dummyVlanId, cp, ipAddress);
+        } else {
+            log.error("Failed to allocate dummy VLAN ID for {}, {}", cp, ipAddress);
+        }
+        return dummyVlanId;
+    }
+
+
     private class InternalPacketProcessor implements PacketProcessor {
         @Override
         public void process(PacketContext context) {
diff --git a/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourHandler.java b/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourHandler.java
index f03e0be..5752bfa 100644
--- a/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourHandler.java
+++ b/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourHandler.java
@@ -19,6 +19,7 @@
 import org.onlab.packet.Ethernet;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
 import org.onosproject.net.neighbour.NeighbourMessageContext;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DeviceId;
@@ -113,7 +114,9 @@
      * @param hostService the host service
      */
     protected void sendResponse(NeighbourMessageContext pkt, MacAddress targetMac, HostService hostService) {
-        HostId dstId = HostId.hostId(pkt.srcMac(), pkt.vlan());
+        short vlanId = pkt.packet().getQinQVID();
+        HostId dstId = HostId.hostId(pkt.srcMac(), vlanId == Ethernet.VLAN_UNTAGGED
+                ? pkt.vlan() : VlanId.vlanId(vlanId));
         Host dst = hostService.getHost(dstId);
         if (dst == null) {
             log.warn("Cannot send {} response to host {} - does not exist in the store",
diff --git a/app/src/main/java/org/onosproject/segmentrouting/storekey/DummyVlanIdStoreKey.java b/app/src/main/java/org/onosproject/segmentrouting/storekey/DummyVlanIdStoreKey.java
new file mode 100644
index 0000000..65e7ca3
--- /dev/null
+++ b/app/src/main/java/org/onosproject/segmentrouting/storekey/DummyVlanIdStoreKey.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.segmentrouting.storekey;
+
+import org.onlab.packet.IpAddress;
+import org.onosproject.net.ConnectPoint;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Key of VLAN ID to DummyVlanIdStore.
+ */
+public class DummyVlanIdStoreKey {
+    private final ConnectPoint connectPoint;
+    private final IpAddress ipAddress;
+
+    /**
+     * Construct the key of dummy vlan id key store.
+     *
+     * @param connectPoint connect point that this vlan id is associated
+     * @param ipAddress IP address that this vlan id is associated
+     */
+    public DummyVlanIdStoreKey(ConnectPoint connectPoint, IpAddress ipAddress) {
+        this.connectPoint = connectPoint;
+        this.ipAddress = ipAddress;
+    }
+
+    /**
+     * Returns the connect point in the key.
+     *
+     * @return connect point
+     */
+    public ConnectPoint connectPoint() {
+        return connectPoint;
+    }
+
+    /**
+     * Returns the IP address in the key.
+     *
+     * @return IP address
+     */
+    public IpAddress ipAddress() {
+        return ipAddress;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof DummyVlanIdStoreKey)) {
+            return false;
+        }
+        DummyVlanIdStoreKey that = (DummyVlanIdStoreKey) o;
+        return (Objects.equals(this.connectPoint, that.connectPoint) &&
+                Objects.equals(this.ipAddress, that.ipAddress));
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(connectPoint, ipAddress);
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(getClass())
+                .add("connectPoint", connectPoint)
+                .add("ipAddress", ipAddress)
+                .toString();
+    }
+}