Distributed Virtual Router Support

A distributed virtual router works by attmpeting to switch a packet, but on failing to find a local consumer (i.e. the packet is destined to a locally attached host) then the packet is sent unmodified 'upstream' to where the rest of the 'distributed' router is present. When L3 switching a packet this means the L2 header must not be modifed. This patch adds a 'l2-bridge' object to the L3 FIB which re-injects packets from the L3 path back into the L2 path - use with extreme caution.

Change-Id: I069724eb45956647d7980cbe40a80a788ee6ee82
Signed-off-by: Neale Ranns <nranns@cisco.com>
diff --git a/test/test_dvr.py b/test/test_dvr.py
new file mode 100644
index 0000000..27522a5
--- /dev/null
+++ b/test/test_dvr.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python
+import random
+import socket
+import unittest
+
+from framework import VppTestCase, VppTestRunner
+from vpp_sub_interface import VppSubInterface, VppDot1QSubint
+from vpp_ip_route import VppIpRoute, VppRoutePath, DpoProto
+from vpp_papi_provider import L2_VTR_OP
+
+from scapy.packet import Raw
+from scapy.layers.l2 import Ether, Dot1Q, ARP
+from scapy.layers.inet import IP, UDP
+from util import ppp
+
+
+class TestDVR(VppTestCase):
+    """ IPv4 Load-Balancing """
+
+    def setUp(self):
+        super(TestDVR, self).setUp()
+
+        self.create_pg_interfaces(range(4))
+        self.create_loopback_interfaces(range(1))
+
+        for i in self.pg_interfaces:
+            i.admin_up()
+
+        self.loop0.config_ip4()
+
+    def tearDown(self):
+        for i in self.pg_interfaces:
+            i.admin_down()
+        self.loop0.unconfig_ip4()
+
+        super(TestDVR, self).tearDown()
+
+    def test_dvr(self):
+        """ Distributed Virtual Router """
+
+        #
+        # A packet destined to an IP address that is L2 bridged via
+        # a non-tag interface
+        #
+        ip_non_tag_bridged = "10.10.10.10"
+        ip_tag_bridged = "10.10.10.11"
+        any_src_addr = "1.1.1.1"
+
+        pkt_no_tag = (Ether(src=self.pg0.remote_mac,
+                            dst=self.loop0.local_mac) /
+                      IP(src=any_src_addr,
+                         dst=ip_non_tag_bridged) /
+                      UDP(sport=1234, dport=1234) /
+                      Raw('\xa5' * 100))
+        pkt_tag = (Ether(src=self.pg0.remote_mac,
+                         dst=self.loop0.local_mac) /
+                   IP(src=any_src_addr,
+                      dst=ip_tag_bridged) /
+                   UDP(sport=1234, dport=1234) /
+                   Raw('\xa5' * 100))
+
+        #
+        # Two sub-interfaces so we can test VLAN tag push/pop
+        #
+        sub_if_on_pg2 = VppDot1QSubint(self, self.pg2, 92)
+        sub_if_on_pg3 = VppDot1QSubint(self, self.pg3, 93)
+        sub_if_on_pg2.admin_up()
+        sub_if_on_pg3.admin_up()
+
+        #
+        # Put all the interfaces into a new bridge domain
+        #
+        self.vapi.sw_interface_set_l2_bridge(self.pg0.sw_if_index, 1)
+        self.vapi.sw_interface_set_l2_bridge(self.pg1.sw_if_index, 1)
+        self.vapi.sw_interface_set_l2_bridge(sub_if_on_pg2.sw_if_index, 1)
+        self.vapi.sw_interface_set_l2_bridge(sub_if_on_pg3.sw_if_index, 1)
+        self.vapi.sw_interface_set_l2_bridge(self.loop0.sw_if_index, 1, bvi=1)
+
+        self.vapi.sw_interface_set_l2_tag_rewrite(sub_if_on_pg2.sw_if_index,
+                                                  L2_VTR_OP.L2_POP_1,
+                                                  92)
+        self.vapi.sw_interface_set_l2_tag_rewrite(sub_if_on_pg3.sw_if_index,
+                                                  L2_VTR_OP.L2_POP_1,
+                                                  93)
+
+        self.logger.error(self.vapi.ppcli("show bridge-domain 1 detail"))
+
+        #
+        # Add routes to bridge the traffic via a tagged an nontagged interface
+        #
+        route_no_tag = VppIpRoute(
+            self, ip_non_tag_bridged, 32,
+            [VppRoutePath("0.0.0.0",
+                          self.pg1.sw_if_index,
+                          proto=DpoProto.DPO_PROTO_ETHERNET)])
+        route_no_tag.add_vpp_config()
+
+        #
+        # Inject the packet that arrives and leaves on a non-tagged interface
+        # Since it's 'bridged' expect that the MAC headed is unchanged.
+        #
+        self.pg0.add_stream(pkt_no_tag)
+
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+
+        rx = self.pg1.get_capture(1)
+
+        self.assertEqual(rx[0][Ether].dst, pkt_no_tag[Ether].dst)
+        self.assertEqual(rx[0][Ether].src, pkt_no_tag[Ether].src)
+
+        #
+        # Add routes to bridge the traffic via a tagged interface
+        #
+        route_no_tag = VppIpRoute(
+            self, ip_tag_bridged, 32,
+            [VppRoutePath("0.0.0.0",
+                          sub_if_on_pg3.sw_if_index,
+                          proto=DpoProto.DPO_PROTO_ETHERNET)])
+        route_no_tag.add_vpp_config()
+
+        #
+        # Inject the packet that arrives and leaves on a non-tagged interface
+        # Since it's 'bridged' expect that the MAC headed is unchanged.
+        #
+        self.pg0.add_stream(pkt_tag)
+
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+
+        rx = self.pg3.get_capture(1)
+
+        self.assertEqual(rx[0][Ether].dst, pkt_tag[Ether].dst)
+        self.assertEqual(rx[0][Ether].src, pkt_tag[Ether].src)
+        self.assertEqual(rx[0][Dot1Q].vlan, 93)
+
+        #
+        # Tag to tag
+        #
+        pkt_tag_to_tag = (Ether(src=self.pg2.remote_mac,
+                                dst=self.loop0.local_mac) /
+                          Dot1Q(vlan=92) /
+                          IP(src=any_src_addr,
+                             dst=ip_tag_bridged) /
+                          UDP(sport=1234, dport=1234) /
+                          Raw('\xa5' * 100))
+
+        self.pg2.add_stream(pkt_tag_to_tag)
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+        rx = self.pg3.get_capture(1)
+
+        self.assertEqual(rx[0][Ether].dst, pkt_tag_to_tag[Ether].dst)
+        self.assertEqual(rx[0][Ether].src, pkt_tag_to_tag[Ether].src)
+        self.assertEqual(rx[0][Dot1Q].vlan, 93)
+
+        #
+        # Tag to non-Tag
+        #
+        pkt_tag_to_non_tag = (Ether(src=self.pg2.remote_mac,
+                                    dst=self.loop0.local_mac) /
+                              Dot1Q(vlan=92) /
+                              IP(src=any_src_addr,
+                                 dst=ip_non_tag_bridged) /
+                              UDP(sport=1234, dport=1234) /
+                              Raw('\xa5' * 100))
+
+        self.pg2.add_stream(pkt_tag_to_non_tag)
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+        rx = self.pg1.get_capture(1)
+
+        self.assertEqual(rx[0][Ether].dst, pkt_tag_to_tag[Ether].dst)
+        self.assertEqual(rx[0][Ether].src, pkt_tag_to_tag[Ether].src)
+        self.assertFalse(rx[0].haslayer(Dot1Q))
+
+if __name__ == '__main__':
+    unittest.main(testRunner=VppTestRunner)
diff --git a/test/vpp_ip_route.py b/test/vpp_ip_route.py
index b799379..e670230 100644
--- a/test/vpp_ip_route.py
+++ b/test/vpp_ip_route.py
@@ -193,6 +193,8 @@
                     next_hop_via_label=path.nh_via_label,
                     next_hop_table_id=path.nh_table_id,
                     is_ipv6=self.is_ip6,
+                    is_l2_bridged=1
+                    if path.proto == DpoProto.DPO_PROTO_ETHERNET else 0,
                     is_resolve_host=path.is_resolve_host,
                     is_resolve_attached=path.is_resolve_attached,
                     is_multipath=1 if len(self.paths) > 1 else 0)
diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py
index b6759ec..f9f54da 100644
--- a/test/vpp_papi_provider.py
+++ b/test/vpp_papi_provider.py
@@ -714,6 +714,7 @@
             is_local=0,
             is_classify=0,
             is_multipath=0,
+            is_l2_bridged=0,
             not_last=0):
         """
 
@@ -754,6 +755,7 @@
              'is_multipath': is_multipath,
              'is_resolve_host': is_resolve_host,
              'is_resolve_attached': is_resolve_attached,
+             'is_l2_bridged': is_l2_bridged,
              'not_last': not_last,
              'next_hop_weight': next_hop_weight,
              'dst_address_length': dst_address_length,