GBP: Endpoints with VLAN tags and birdges that don't learn
[vpp.git] / test / test_gbp.py
index 427b14d..92480ce 100644 (file)
 #!/usr/bin/env python
 
 import unittest
-import socket
-import struct
 
 from framework import VppTestCase, VppTestRunner
 from vpp_object import VppObject
+from vpp_neighbor import VppNeighbor
+from vpp_ip_route import VppIpRoute, VppRoutePath, VppIpTable, \
+    VppIpInterfaceAddress, VppIpInterfaceBind, find_route
+from vpp_l2 import VppBridgeDomain, VppBridgeDomainPort, \
+    VppBridgeDomainArpEntry, VppL2FibEntry, find_bridge_domain_port
+from vpp_vxlan_gbp_tunnel import *
+from vpp_sub_interface import VppDot1QSubint
+
+from vpp_ip import *
+from vpp_mac import *
+from vpp_papi_provider import L2_PORT_TYPE
+from vpp_papi import VppEnum
 
 from scapy.packet import Raw
-from scapy.layers.l2 import Ether
+from scapy.layers.l2 import Ether, ARP, Dot1Q
 from scapy.layers.inet import IP, UDP
-from scapy.layers.inet6 import IPv6
+from scapy.layers.inet6 import IPv6, ICMPv6ND_NS,  ICMPv6NDOptSrcLLAddr, \
+    ICMPv6ND_NA
+from scapy.utils6 import in6_getnsma, in6_getnsmac
+from scapy.layers.vxlan import VXLAN
 
 from socket import AF_INET, AF_INET6
-from scapy.utils import inet_pton
+from scapy.utils import inet_pton, inet_ntop
+from util import mactobinary
+from vpp_papi_provider import L2_VTR_OP
+
+
+def find_gbp_endpoint(test, sw_if_index=None, ip=None, mac=None):
+    if ip:
+        vip = VppIpAddress(ip)
+    if mac:
+        vmac = VppMacAddress(mac)
+
+    eps = test.vapi.gbp_endpoint_dump()
+
+    for ep in eps:
+        if sw_if_index:
+            if ep.endpoint.sw_if_index != sw_if_index:
+                continue
+        if ip:
+            for eip in ep.endpoint.ips:
+                if vip == eip:
+                    return True
+        if mac:
+            if vmac == ep.endpoint.mac:
+                return True
+    return False
+
+
+def find_gbp_vxlan(test, vni):
+    ts = test.vapi.gbp_vxlan_tunnel_dump()
+    for t in ts:
+        if t.tunnel.vni == vni:
+            return True
+    return False
 
 
 class VppGbpEndpoint(VppObject):
     """
-    GDB Endpoint
+    GBP Endpoint
     """
 
-    def __init__(self, test, sw_if_index, addr, epg, is_ip6=0):
+    @property
+    def bin_mac(self):
+        return self.vmac.bytes
+
+    @property
+    def mac(self):
+        return self.vmac.address
+
+    @property
+    def mac(self):
+        return self.itf.remote_mac
+
+    @property
+    def ip4(self):
+        return self._ip4
+
+    @property
+    def fip4(self):
+        return self._fip4
+
+    @property
+    def ip6(self):
+        return self._ip6
+
+    @property
+    def fip6(self):
+        return self._fip6
+
+    @property
+    def ips(self):
+        return [self.ip4, self.ip6]
+
+    @property
+    def fips(self):
+        return [self.fip4, self.fip6]
+
+    def __init__(self, test, itf, epg, recirc, ip4, fip4, ip6, fip6,
+                 flags=0,
+                 tun_src="0.0.0.0",
+                 tun_dst="0.0.0.0",
+                 mac=True):
         self._test = test
-        self.sw_if_index = sw_if_index
+        self.itf = itf
         self.epg = epg
-        self.addr_p = addr
-        self.is_ip6 = is_ip6
-        if is_ip6:
-            self.addr = inet_pton(AF_INET6, addr)
+        self.recirc = recirc
+
+        self._ip4 = VppIpAddress(ip4)
+        self._fip4 = VppIpAddress(fip4)
+        self._ip6 = VppIpAddress(ip6)
+        self._fip6 = VppIpAddress(fip6)
+
+        if mac:
+            self.vmac = VppMacAddress(self.itf.remote_mac)
         else:
-            self.addr = inet_pton(AF_INET, addr)
+            self.vmac = VppMacAddress("00:00:00:00:00:00")
+
+        self.flags = flags
+        self.tun_src = VppIpAddress(tun_src)
+        self.tun_dst = VppIpAddress(tun_dst)
+
+    def add_vpp_config(self):
+        res = self._test.vapi.gbp_endpoint_add(
+            self.itf.sw_if_index,
+            [self.ip4.encode(), self.ip6.encode()],
+            self.vmac.encode(),
+            self.epg.epg,
+            self.flags,
+            self.tun_src.encode(),
+            self.tun_dst.encode())
+        self.handle = res.handle
+        self._test.registry.register(self, self._test.logger)
+
+    def remove_vpp_config(self):
+        self._test.vapi.gbp_endpoint_del(self.handle)
+
+    def __str__(self):
+        return self.object_id()
+
+    def object_id(self):
+        return "gbp-endpoint:[%d==%d:%s:%d]" % (self.handle,
+                                                self.itf.sw_if_index,
+                                                self.ip4.address,
+                                                self.epg.epg)
+
+    def query_vpp_config(self):
+        return find_gbp_endpoint(self._test,
+                                 self.itf.sw_if_index,
+                                 self.ip4.address)
+
+
+class VppGbpRecirc(VppObject):
+    """
+    GBP Recirculation Interface
+    """
+
+    def __init__(self, test, epg, recirc, is_ext=False):
+        self._test = test
+        self.recirc = recirc
+        self.epg = epg
+        self.is_ext = is_ext
 
     def add_vpp_config(self):
-        self._test.vapi.gbp_endpoint_add_del(
+        self._test.vapi.gbp_recirc_add_del(
             1,
-            self.sw_if_index,
-            self.addr,
-            self.is_ip6,
-            self.epg)
+            self.recirc.sw_if_index,
+            self.epg.epg,
+            self.is_ext)
+        self._test.registry.register(self, self._test.logger)
+
+    def remove_vpp_config(self):
+        self._test.vapi.gbp_recirc_add_del(
+            0,
+            self.recirc.sw_if_index,
+            self.epg.epg,
+            self.is_ext)
+
+    def __str__(self):
+        return self.object_id()
+
+    def object_id(self):
+        return "gbp-recirc:[%d]" % (self.recirc.sw_if_index)
+
+    def query_vpp_config(self):
+        rs = self._test.vapi.gbp_recirc_dump()
+        for r in rs:
+            if r.recirc.sw_if_index == self.recirc.sw_if_index:
+                return True
+        return False
+
+
+class VppGbpSubnet(VppObject):
+    """
+    GBP Subnet
+    """
+    def __init__(self, test, rd, address, address_len,
+                 type, sw_if_index=None, epg=None):
+        self._test = test
+        self.rd_id = rd.rd_id
+        self.prefix = VppIpPrefix(address, address_len)
+        self.type = type
+        self.sw_if_index = sw_if_index
+        self.epg = epg
+
+    def add_vpp_config(self):
+        self._test.vapi.gbp_subnet_add_del(
+            1,
+            self.rd_id,
+            self.prefix.encode(),
+            self.type,
+            sw_if_index=self.sw_if_index if self.sw_if_index else 0xffffffff,
+            epg_id=self.epg if self.epg else 0xffff)
         self._test.registry.register(self, self._test.logger)
 
     def remove_vpp_config(self):
-        self._test.vapi.gbp_endpoint_add_del(
+        self._test.vapi.gbp_subnet_add_del(
             0,
-            self.sw_if_index,
-            self.addr,
-            self.is_ip6,
+            self.rd_id,
+            self.prefix.encode(),
+            self.type)
+
+    def __str__(self):
+        return self.object_id()
+
+    def object_id(self):
+        return "gbp-subnet:[%d-%s]" % (self.rd_id, self.prefix)
+
+    def query_vpp_config(self):
+        ss = self._test.vapi.gbp_subnet_dump()
+        for s in ss:
+            if s.subnet.rd_id == self.rd_id and \
+               s.subnet.type == self.type and \
+               s.subnet.prefix == self.prefix:
+                return True
+        return False
+
+
+class VppGbpEndpointGroup(VppObject):
+    """
+    GBP Endpoint Group
+    """
+
+    def __init__(self, test, epg, rd, bd, uplink,
+                 bvi, bvi_ip4, bvi_ip6=None):
+        self._test = test
+        self.uplink = uplink
+        self.bvi = bvi
+        self.bvi_ip4 = VppIpAddress(bvi_ip4)
+        self.bvi_ip6 = VppIpAddress(bvi_ip6)
+        self.epg = epg
+        self.bd = bd
+        self.rd = rd
+
+    def add_vpp_config(self):
+        self._test.vapi.gbp_endpoint_group_add(
+            self.epg,
+            self.bd.bd.bd_id,
+            self.rd.rd_id,
+            self.uplink.sw_if_index if self.uplink else INDEX_INVALID)
+        self._test.registry.register(self, self._test.logger)
+
+    def remove_vpp_config(self):
+        self._test.vapi.gbp_endpoint_group_del(
             self.epg)
 
     def __str__(self):
         return self.object_id()
 
     def object_id(self):
-        return "gbp-endpoint;[%d:%s:%d]" % (self.sw_if_index,
-                                            self.addr_p,
-                                            self.epg)
+        return "gbp-endpoint-group:[%d]" % (self.epg)
 
     def query_vpp_config(self):
-        eps = self._test.vapi.gbp_endpoint_dump()
-        for ep in eps:
-            if ep.endpoint.address == self.addr \
-               and ep.endpoint.sw_if_index == self.sw_if_index:
+        epgs = self._test.vapi.gbp_endpoint_group_dump()
+        for epg in epgs:
+            if epg.epg.epg_id == self.epg:
+                return True
+        return False
+
+
+class VppGbpBridgeDomain(VppObject):
+    """
+    GBP Bridge Domain
+    """
+
+    def __init__(self, test, bd, bvi, uu_flood=None, learn=True):
+        self._test = test
+        self.bvi = bvi
+        self.uu_flood = uu_flood
+        self.bd = bd
+
+        e = VppEnum.vl_api_gbp_bridge_domain_flags_t
+        if (learn):
+            self.learn = e.GBP_BD_API_FLAG_NONE
+        else:
+            self.learn = e.GBP_BD_API_FLAG_DO_NOT_LEARN
+
+    def add_vpp_config(self):
+        self._test.vapi.gbp_bridge_domain_add(
+            self.bd.bd_id,
+            self.learn,
+            self.bvi.sw_if_index,
+            self.uu_flood.sw_if_index if self.uu_flood else INDEX_INVALID)
+        self._test.registry.register(self, self._test.logger)
+
+    def remove_vpp_config(self):
+        self._test.vapi.gbp_bridge_domain_del(self.bd.bd_id)
+
+    def __str__(self):
+        return self.object_id()
+
+    def object_id(self):
+        return "gbp-bridge-domain:[%d]" % (self.bd.bd_id)
+
+    def query_vpp_config(self):
+        bds = self._test.vapi.gbp_bridge_domain_dump()
+        for bd in bds:
+            if bd.bd.bd_id == self.bd.bd_id:
+                return True
+        return False
+
+
+class VppGbpRouteDomain(VppObject):
+    """
+    GBP Route Domain
+    """
+
+    def __init__(self, test, rd_id, t4, t6, ip4_uu=None, ip6_uu=None):
+        self._test = test
+        self.rd_id = rd_id
+        self.t4 = t4
+        self.t6 = t6
+        self.ip4_uu = ip4_uu
+        self.ip6_uu = ip6_uu
+
+    def add_vpp_config(self):
+        self._test.vapi.gbp_route_domain_add(
+            self.rd_id,
+            self.t4.table_id,
+            self.t6.table_id,
+            self.ip4_uu.sw_if_index if self.ip4_uu else INDEX_INVALID,
+            self.ip6_uu.sw_if_index if self.ip6_uu else INDEX_INVALID)
+        self._test.registry.register(self, self._test.logger)
+
+    def remove_vpp_config(self):
+        self._test.vapi.gbp_route_domain_del(self.rd_id)
+
+    def __str__(self):
+        return self.object_id()
+
+    def object_id(self):
+        return "gbp-route-domain:[%d]" % (self.rd_id)
+
+    def query_vpp_config(self):
+        rds = self._test.vapi.gbp_route_domain_dump()
+        for rd in rds:
+            if rd.rd.rd_id == self.rd_id:
                 return True
         return False
 
 
 class VppGbpContract(VppObject):
     """
-    GDB Contract
+    GBP Contract
     """
 
     def __init__(self, test, src_epg, dst_epg, acl_index):
@@ -96,15 +395,103 @@ class VppGbpContract(VppObject):
         return self.object_id()
 
     def object_id(self):
-        return "gbp-contract;[%d:%s:%d]" % (self.src_epg,
+        return "gbp-contract:[%d:%s:%d]" % (self.src_epg,
                                             self.dst_epg,
                                             self.acl_index)
 
     def query_vpp_config(self):
-        eps = self._test.vapi.gbp_contract_dump()
-        for ep in eps:
-            if ep.contract.src_epg == self.src_epg \
-               and ep.contract.dst_epg == self.dst_epg:
+        cs = self._test.vapi.gbp_contract_dump()
+        for c in cs:
+            if c.contract.src_epg == self.src_epg \
+               and c.contract.dst_epg == self.dst_epg:
+                return True
+        return False
+
+
+class VppGbpVxlanTunnel(VppInterface):
+    """
+    GBP VXLAN tunnel
+    """
+
+    def __init__(self, test, vni, bd_rd_id, mode):
+        super(VppGbpVxlanTunnel, self).__init__(test)
+        self._test = test
+        self.vni = vni
+        self.bd_rd_id = bd_rd_id
+        self.mode = mode
+
+    def add_vpp_config(self):
+        r = self._test.vapi.gbp_vxlan_tunnel_add(
+            self.vni,
+            self.bd_rd_id,
+            self.mode)
+        self.set_sw_if_index(r.sw_if_index)
+        self._test.registry.register(self, self._test.logger)
+
+    def remove_vpp_config(self):
+        self._test.vapi.gbp_vxlan_tunnel_del(self.vni)
+
+    def __str__(self):
+        return self.object_id()
+
+    def object_id(self):
+        return "gbp-vxlan:%d" % (self.vni)
+
+    def query_vpp_config(self):
+        return find_gbp_vxlan(self._test, self.vni)
+
+
+class VppGbpAcl(VppObject):
+    """
+    GBP Acl
+    """
+
+    def __init__(self, test):
+        self._test = test
+        self.acl_index = 4294967295
+
+    def create_rule(self, is_ipv6=0, permit_deny=0, proto=-1,
+                    s_prefix=0, s_ip='\x00\x00\x00\x00', sport_from=0,
+                    sport_to=65535, d_prefix=0, d_ip='\x00\x00\x00\x00',
+                    dport_from=0, dport_to=65535):
+        if proto == -1 or proto == 0:
+            sport_to = 0
+            dport_to = sport_to
+        elif proto == 1 or proto == 58:
+            sport_to = 255
+            dport_to = sport_to
+        rule = ({'is_permit': permit_deny, 'is_ipv6': is_ipv6, 'proto': proto,
+                 'srcport_or_icmptype_first': sport_from,
+                 'srcport_or_icmptype_last': sport_to,
+                 'src_ip_prefix_len': s_prefix,
+                 'src_ip_addr': s_ip,
+                 'dstport_or_icmpcode_first': dport_from,
+                 'dstport_or_icmpcode_last': dport_to,
+                 'dst_ip_prefix_len': d_prefix,
+                 'dst_ip_addr': d_ip})
+        return rule
+
+    def add_vpp_config(self, rules):
+
+        reply = self._test.vapi.acl_add_replace(self.acl_index,
+                                                r=rules,
+                                                tag='GBPTest')
+        self.acl_index = reply.acl_index
+        return self.acl_index
+
+    def remove_vpp_config(self):
+        self._test.vapi.acl_del(self.acl_index)
+
+    def __str__(self):
+        return self.object_id()
+
+    def object_id(self):
+        return "gbp-acl:[%d]" % (self.acl_index)
+
+    def query_vpp_config(self):
+        cs = self._test.vapi.acl_dump()
+        for c in cs:
+            if c.acl_index == self.acl_index:
                 return True
         return False
 
@@ -115,254 +502,1790 @@ class TestGBP(VppTestCase):
     def setUp(self):
         super(TestGBP, self).setUp()
 
-        # create 6 pg interfaces for pg0 to pg5
-        self.create_pg_interfaces(range(6))
+        self.create_pg_interfaces(range(9))
+        self.create_loopback_interfaces(8)
+
+        self.router_mac = VppMacAddress("00:11:22:33:44:55")
 
         for i in self.pg_interfaces:
             i.admin_up()
-            i.config_ip4()
-            i.resolve_arp()
-            i.config_ip6()
-            i.resolve_ndp()
+        for i in self.lo_interfaces:
+            i.admin_up()
 
     def tearDown(self):
         for i in self.pg_interfaces:
-            i.unconfig_ip4()
-            i.unconfig_ip6()
+            i.admin_down()
 
         super(TestGBP, self).tearDown()
 
-    def test_gbp4(self):
-        """ Group Based Policy v4 """
-
-        ep1 = VppGbpEndpoint(self,
-                             self.pg0.sw_if_index,
-                             self.pg0.remote_ip4,
-                             220)
-        ep1.add_vpp_config()
-        ep2 = VppGbpEndpoint(self,
-                             self.pg1.sw_if_index,
-                             self.pg1.remote_ip4,
-                             220)
-        ep2.add_vpp_config()
-
-        ep3 = VppGbpEndpoint(self,
-                             self.pg2.sw_if_index,
-                             self.pg2.remote_ip4,
-                             221)
-        ep3.add_vpp_config()
-        ep4 = VppGbpEndpoint(self,
-                             self.pg3.sw_if_index,
-                             self.pg3.remote_ip4,
-                             222)
-        ep4.add_vpp_config()
+    def send_and_expect_bridged(self, src, tx, dst):
+        rx = self.send_and_expect(src, tx, dst)
 
-        self.logger.info(self.vapi.cli("sh gbp endpoint"))
+        for r in rx:
+            self.assertEqual(r[Ether].src, tx[0][Ether].src)
+            self.assertEqual(r[Ether].dst, tx[0][Ether].dst)
+            self.assertEqual(r[IP].src, tx[0][IP].src)
+            self.assertEqual(r[IP].dst, tx[0][IP].dst)
+        return rx
 
-        #
-        # in the abscense of policy, endpoints in the same EPG
-        # can communicate
-        #
-        pkt_intra_epg = (Ether(src=self.pg0.remote_mac,
-                               dst=self.pg0.local_mac) /
-                         IP(src=self.pg0.remote_ip4,
-                            dst=self.pg1.remote_ip4) /
-                         UDP(sport=1234, dport=1234) /
-                         Raw('\xa5' * 100))
+    def send_and_expect_bridged6(self, src, tx, dst):
+        rx = self.send_and_expect(src, tx, dst)
 
-        self.send_and_expect(self.pg0, pkt_intra_epg * 65, self.pg1)
+        for r in rx:
+            self.assertEqual(r[Ether].src, tx[0][Ether].src)
+            self.assertEqual(r[Ether].dst, tx[0][Ether].dst)
+            self.assertEqual(r[IPv6].src, tx[0][IPv6].src)
+            self.assertEqual(r[IPv6].dst, tx[0][IPv6].dst)
+        return rx
 
-        #
-        # in the abscense of policy, endpoints in the different EPG
-        # cannot communicate
-        #
-        pkt_inter_epg_220_to_221 = (Ether(src=self.pg0.remote_mac,
-                                          dst=self.pg0.local_mac) /
-                                    IP(src=self.pg0.remote_ip4,
-                                       dst=self.pg2.remote_ip4) /
-                                    UDP(sport=1234, dport=1234) /
-                                    Raw('\xa5' * 100))
-        pkt_inter_epg_220_to_222 = (Ether(src=self.pg0.remote_mac,
-                                          dst=self.pg0.local_mac) /
-                                    IP(src=self.pg0.remote_ip4,
-                                       dst=self.pg3.remote_ip4) /
-                                    UDP(sport=1234, dport=1234) /
-                                    Raw('\xa5' * 100))
-        pkt_inter_epg_221_to_220 = (Ether(src=self.pg2.remote_mac,
-                                          dst=self.pg2.local_mac) /
-                                    IP(src=self.pg2.remote_ip4,
-                                       dst=self.pg0.remote_ip4) /
-                                    UDP(sport=1234, dport=1234) /
-                                    Raw('\xa5' * 100))
+    def send_and_expect_routed(self, src, tx, dst, src_mac):
+        rx = self.send_and_expect(src, tx, dst)
 
-        self.send_and_assert_no_replies(self.pg0,
-                                        pkt_inter_epg_220_to_221 * 65)
-        self.send_and_assert_no_replies(self.pg0,
-                                        pkt_inter_epg_221_to_220 * 65)
+        for r in rx:
+            self.assertEqual(r[Ether].src, src_mac)
+            self.assertEqual(r[Ether].dst, dst.remote_mac)
+            self.assertEqual(r[IP].src, tx[0][IP].src)
+            self.assertEqual(r[IP].dst, tx[0][IP].dst)
+        return rx
 
-        #
-        # A uni-directional contract from EPG 220 -> 221
-        #
-        c1 = VppGbpContract(self, 220, 221, 0xffffffff)
-        c1.add_vpp_config()
+    def send_and_expect_natted(self, src, tx, dst, src_ip):
+        rx = self.send_and_expect(src, tx, dst)
 
-        self.send_and_expect(self.pg0,
-                             pkt_inter_epg_220_to_221 * 65,
-                             self.pg2)
-        self.send_and_assert_no_replies(self.pg2,
-                                        pkt_inter_epg_221_to_220 * 65)
+        for r in rx:
+            self.assertEqual(r[Ether].src, tx[0][Ether].src)
+            self.assertEqual(r[Ether].dst, tx[0][Ether].dst)
+            self.assertEqual(r[IP].src, src_ip)
+            self.assertEqual(r[IP].dst, tx[0][IP].dst)
+        return rx
 
-        #
-        # contract for the return direction
-        #
-        c2 = VppGbpContract(self, 221, 220, 0xffffffff)
-        c2.add_vpp_config()
+    def send_and_expect_natted6(self, src, tx, dst, src_ip):
+        rx = self.send_and_expect(src, tx, dst)
 
-        self.send_and_expect(self.pg0,
-                             pkt_inter_epg_220_to_221 * 65,
-                             self.pg2)
-        self.send_and_expect(self.pg2,
-                             pkt_inter_epg_221_to_220 * 65,
-                             self.pg0)
+        for r in rx:
+            self.assertEqual(r[Ether].src, tx[0][Ether].src)
+            self.assertEqual(r[Ether].dst, tx[0][Ether].dst)
+            self.assertEqual(r[IPv6].src, src_ip)
+            self.assertEqual(r[IPv6].dst, tx[0][IPv6].dst)
+        return rx
 
-        #
-        # check that inter group is still disabled for the groups
-        # not in the contract.
-        #
-        self.send_and_assert_no_replies(self.pg0,
-                                        pkt_inter_epg_220_to_222 * 65)
+    def send_and_expect_unnatted(self, src, tx, dst, dst_ip):
+        rx = self.send_and_expect(src, tx, dst)
 
-        self.logger.info(self.vapi.cli("sh gbp contract"))
+        for r in rx:
+            self.assertEqual(r[Ether].src, tx[0][Ether].src)
+            self.assertEqual(r[Ether].dst, tx[0][Ether].dst)
+            self.assertEqual(r[IP].dst, dst_ip)
+            self.assertEqual(r[IP].src, tx[0][IP].src)
+        return rx
 
-        #
-        # remove both contracts, traffic stops in both directions
-        #
-        c2.remove_vpp_config()
-        c1.remove_vpp_config()
+    def send_and_expect_unnatted6(self, src, tx, dst, dst_ip):
+        rx = self.send_and_expect(src, tx, dst)
 
-        self.send_and_assert_no_replies(self.pg2,
-                                        pkt_inter_epg_221_to_220 * 65)
-        self.send_and_assert_no_replies(self.pg0,
-                                        pkt_inter_epg_220_to_221 * 65)
-        self.send_and_expect(self.pg0, pkt_intra_epg * 65, self.pg1)
-
-    def test_gbp6(self):
-        """ Group Based Policy v6 """
-
-        ep1 = VppGbpEndpoint(self,
-                             self.pg0.sw_if_index,
-                             self.pg0.remote_ip6,
-                             220,
-                             is_ip6=1)
-        ep1.add_vpp_config()
-        ep2 = VppGbpEndpoint(self,
-                             self.pg1.sw_if_index,
-                             self.pg1.remote_ip6,
-                             220,
-                             is_ip6=1)
-        ep2.add_vpp_config()
-
-        ep3 = VppGbpEndpoint(self,
-                             self.pg2.sw_if_index,
-                             self.pg2.remote_ip6,
-                             221,
-                             is_ip6=1)
-        ep3.add_vpp_config()
-        ep4 = VppGbpEndpoint(self,
-                             self.pg3.sw_if_index,
-                             self.pg3.remote_ip6,
-                             222,
-                             is_ip6=1)
-        ep4.add_vpp_config()
+        for r in rx:
+            self.assertEqual(r[Ether].src, tx[0][Ether].src)
+            self.assertEqual(r[Ether].dst, tx[0][Ether].dst)
+            self.assertEqual(r[IPv6].dst, dst_ip)
+            self.assertEqual(r[IPv6].src, tx[0][IPv6].src)
+        return rx
 
-        self.logger.info(self.vapi.cli("sh gbp endpoint"))
+    def send_and_expect_double_natted(self, src, tx, dst, src_ip, dst_ip):
+        rx = self.send_and_expect(src, tx, dst)
 
-        #
-        # in the abscense of policy, endpoints in the same EPG
-        # can communicate
-        #
-        pkt_intra_epg = (Ether(src=self.pg0.remote_mac,
-                               dst=self.pg0.local_mac) /
-                         IPv6(src=self.pg0.remote_ip6,
-                              dst=self.pg1.remote_ip6) /
-                         UDP(sport=1234, dport=1234) /
-                         Raw('\xa5' * 100))
+        for r in rx:
+            self.assertEqual(r[Ether].src, self.router_mac.address)
+            self.assertEqual(r[Ether].dst, dst.remote_mac)
+            self.assertEqual(r[IP].dst, dst_ip)
+            self.assertEqual(r[IP].src, src_ip)
+        return rx
 
-        self.send_and_expect(self.pg0, pkt_intra_epg * 65, self.pg1)
+    def send_and_expect_double_natted6(self, src, tx, dst, src_ip, dst_ip):
+        rx = self.send_and_expect(src, tx, dst)
+
+        for r in rx:
+            self.assertEqual(r[Ether].src, self.router_mac.address)
+            self.assertEqual(r[Ether].dst, dst.remote_mac)
+            self.assertEqual(r[IPv6].dst, dst_ip)
+            self.assertEqual(r[IPv6].src, src_ip)
+        return rx
+
+    def test_gbp(self):
+        """ Group Based Policy """
 
         #
-        # in the abscense of policy, endpoints in the different EPG
-        # cannot communicate
+        # Bridge Domains
         #
-        pkt_inter_epg_220_to_221 = (Ether(src=self.pg0.remote_mac,
-                                          dst=self.pg0.local_mac) /
-                                    IPv6(src=self.pg0.remote_ip6,
-                                         dst=self.pg2.remote_ip6) /
-                                    UDP(sport=1234, dport=1234) /
-                                    Raw('\xa5' * 100))
-        pkt_inter_epg_220_to_222 = (Ether(src=self.pg0.remote_mac,
-                                          dst=self.pg0.local_mac) /
-                                    IPv6(src=self.pg0.remote_ip6,
-                                         dst=self.pg3.remote_ip6) /
-                                    UDP(sport=1234, dport=1234) /
-                                    Raw('\xa5' * 100))
-        pkt_inter_epg_221_to_220 = (Ether(src=self.pg2.remote_mac,
-                                          dst=self.pg2.local_mac) /
-                                    IPv6(src=self.pg2.remote_ip6,
-                                         dst=self.pg0.remote_ip6) /
-                                    UDP(sport=1234, dport=1234) /
-                                    Raw('\xa5' * 100))
+        bd1 = VppBridgeDomain(self, 1)
+        bd2 = VppBridgeDomain(self, 2)
+        bd20 = VppBridgeDomain(self, 20)
 
-        self.send_and_assert_no_replies(self.pg0,
-                                        pkt_inter_epg_220_to_221 * 65)
-        self.send_and_assert_no_replies(self.pg0,
-                                        pkt_inter_epg_221_to_220 * 65)
+        bd1.add_vpp_config()
+        bd2.add_vpp_config()
+        bd20.add_vpp_config()
+
+        gbd1 = VppGbpBridgeDomain(self, bd1, self.loop0)
+        gbd2 = VppGbpBridgeDomain(self, bd2, self.loop1)
+        gbd20 = VppGbpBridgeDomain(self, bd20, self.loop2)
+
+        gbd1.add_vpp_config()
+        gbd2.add_vpp_config()
+        gbd20.add_vpp_config()
 
         #
-        # A uni-directional contract from EPG 220 -> 221
+        # Route Domains
         #
-        c1 = VppGbpContract(self, 220, 221, 0xffffffff)
-        c1.add_vpp_config()
+        gt4 = VppIpTable(self, 0)
+        gt4.add_vpp_config()
+        gt6 = VppIpTable(self, 0, is_ip6=True)
+        gt6.add_vpp_config()
+        nt4 = VppIpTable(self, 20)
+        nt4.add_vpp_config()
+        nt6 = VppIpTable(self, 20, is_ip6=True)
+        nt6.add_vpp_config()
 
-        self.send_and_expect(self.pg0,
-                             pkt_inter_epg_220_to_221 * 65,
-                             self.pg2)
-        self.send_and_assert_no_replies(self.pg2,
-                                        pkt_inter_epg_221_to_220 * 65)
+        rd0 = VppGbpRouteDomain(self, 0, gt4, gt6, None, None)
+        rd20 = VppGbpRouteDomain(self, 20, nt4, nt6, None, None)
+
+        rd0.add_vpp_config()
+        rd20.add_vpp_config()
 
         #
-        # contract for the return direction
+        # 3 EPGs, 2 of which share a BD.
+        # 2 NAT EPGs, one for floating-IP subnets, the other for internet
         #
-        c2 = VppGbpContract(self, 221, 220, 0xffffffff)
-        c2.add_vpp_config()
+        epgs = [VppGbpEndpointGroup(self, 220, rd0, gbd1, self.pg4,
+                                    self.loop0,
+                                    "10.0.0.128",
+                                    "2001:10::128"),
+                VppGbpEndpointGroup(self, 221, rd0, gbd1, self.pg5,
+                                    self.loop0,
+                                    "10.0.1.128",
+                                    "2001:10:1::128"),
+                VppGbpEndpointGroup(self, 222, rd0, gbd2, self.pg6,
+                                    self.loop1,
+                                    "10.0.2.128",
+                                    "2001:10:2::128"),
+                VppGbpEndpointGroup(self, 333, rd20, gbd20, self.pg7,
+                                    self.loop2,
+                                    "11.0.0.128",
+                                    "3001::128"),
+                VppGbpEndpointGroup(self, 444, rd20, gbd20, self.pg8,
+                                    self.loop2,
+                                    "11.0.0.129",
+                                    "3001::129")]
+        recircs = [VppGbpRecirc(self, epgs[0],
+                                self.loop3),
+                   VppGbpRecirc(self, epgs[1],
+                                self.loop4),
+                   VppGbpRecirc(self, epgs[2],
+                                self.loop5),
+                   VppGbpRecirc(self, epgs[3],
+                                self.loop6, is_ext=True),
+                   VppGbpRecirc(self, epgs[4],
+                                self.loop7, is_ext=True)]
 
-        self.send_and_expect(self.pg0,
-                             pkt_inter_epg_220_to_221 * 65,
-                             self.pg2)
-        self.send_and_expect(self.pg2,
-                             pkt_inter_epg_221_to_220 * 65,
-                             self.pg0)
+        epg_nat = epgs[3]
+        recirc_nat = recircs[3]
 
         #
-        # check that inter group is still disabled for the groups
-        # not in the contract.
+        # 4 end-points, 2 in the same subnet, 3 in the same BD
         #
-        self.send_and_assert_no_replies(self.pg0,
-                                        pkt_inter_epg_220_to_222 * 65)
-
-        self.logger.info(self.vapi.cli("sh gbp contract"))
+        eps = [VppGbpEndpoint(self, self.pg0,
+                              epgs[0], recircs[0],
+                              "10.0.0.1", "11.0.0.1",
+                              "2001:10::1", "3001::1"),
+               VppGbpEndpoint(self, self.pg1,
+                              epgs[0], recircs[0],
+                              "10.0.0.2", "11.0.0.2",
+                              "2001:10::2", "3001::2"),
+               VppGbpEndpoint(self, self.pg2,
+                              epgs[1], recircs[1],
+                              "10.0.1.1", "11.0.0.3",
+                              "2001:10:1::1", "3001::3"),
+               VppGbpEndpoint(self, self.pg3,
+                              epgs[2], recircs[2],
+                              "10.0.2.1", "11.0.0.4",
+                              "2001:10:2::1", "3001::4")]
 
         #
-        # remove both contracts, traffic stops in both directions
+        # Config related to each of the EPGs
         #
-        c2.remove_vpp_config()
-        c1.remove_vpp_config()
+        for epg in epgs:
+            # IP config on the BVI interfaces
+            if epg != epgs[1] and epg != epgs[4]:
+                VppIpInterfaceBind(self, epg.bvi, epg.rd.t4).add_vpp_config()
+                VppIpInterfaceBind(self, epg.bvi, epg.rd.t6).add_vpp_config()
+                self.vapi.sw_interface_set_mac_address(
+                    epg.bvi.sw_if_index,
+                    self.router_mac.bytes)
 
-        self.send_and_assert_no_replies(self.pg2,
-                                        pkt_inter_epg_221_to_220 * 65)
-        self.send_and_assert_no_replies(self.pg0,
-                                        pkt_inter_epg_220_to_221 * 65)
-        self.send_and_expect(self.pg0, pkt_intra_epg * 65, self.pg1)
+                # The BVIs are NAT inside interfaces
+                self.vapi.nat44_interface_add_del_feature(epg.bvi.sw_if_index,
+                                                          is_inside=1,
+                                                          is_add=1)
+                self.vapi.nat66_add_del_interface(epg.bvi.sw_if_index,
+                                                  is_inside=1,
+                                                  is_add=1)
+
+            if_ip4 = VppIpInterfaceAddress(self, epg.bvi, epg.bvi_ip4, 32)
+            if_ip6 = VppIpInterfaceAddress(self, epg.bvi, epg.bvi_ip6, 128)
+            if_ip4.add_vpp_config()
+            if_ip6.add_vpp_config()
+
+            # EPG uplink interfaces in the RD
+            VppIpInterfaceBind(self, epg.uplink, epg.rd.t4).add_vpp_config()
+            VppIpInterfaceBind(self, epg.uplink, epg.rd.t6).add_vpp_config()
+
+            # add the BD ARP termination entry for BVI IP
+            epg.bd_arp_ip4 = VppBridgeDomainArpEntry(self, epg.bd.bd,
+                                                     self.router_mac.address,
+                                                     epg.bvi_ip4)
+            epg.bd_arp_ip6 = VppBridgeDomainArpEntry(self, epg.bd.bd,
+                                                     self.router_mac.address,
+                                                     epg.bvi_ip6)
+            epg.bd_arp_ip4.add_vpp_config()
+            epg.bd_arp_ip6.add_vpp_config()
+
+            # EPG in VPP
+            epg.add_vpp_config()
+
+        for recirc in recircs:
+            # EPG's ingress recirculation interface maps to its RD
+            VppIpInterfaceBind(self, recirc.recirc,
+                               recirc.epg.rd.t4).add_vpp_config()
+            VppIpInterfaceBind(self, recirc.recirc,
+                               recirc.epg.rd.t6).add_vpp_config()
+
+            self.vapi.sw_interface_set_l2_emulation(
+                recirc.recirc.sw_if_index)
+            self.vapi.nat44_interface_add_del_feature(
+                recirc.recirc.sw_if_index,
+                is_inside=0,
+                is_add=1)
+            self.vapi.nat66_add_del_interface(
+                recirc.recirc.sw_if_index,
+                is_inside=0,
+                is_add=1)
+
+            recirc.add_vpp_config()
+
+        for recirc in recircs:
+            self.assertTrue(find_bridge_domain_port(self,
+                                                    recirc.epg.bd.bd.bd_id,
+                                                    recirc.recirc.sw_if_index))
+
+        for ep in eps:
+            self.pg_enable_capture(self.pg_interfaces)
+            self.pg_start()
+            #
+            # routes to the endpoints. We need these since there are no
+            # adj-fibs due to the fact the the BVI address has /32 and
+            # the subnet is not attached.
+            #
+            for (ip, fip) in zip(ep.ips, ep.fips):
+                # Add static mappings for each EP from the 10/8 to 11/8 network
+                if ip.af == AF_INET:
+                    self.vapi.nat44_add_del_static_mapping(ip.bytes,
+                                                           fip.bytes,
+                                                           vrf_id=0,
+                                                           addr_only=1)
+                else:
+                    self.vapi.nat66_add_del_static_mapping(ip.bytes,
+                                                           fip.bytes,
+                                                           vrf_id=0)
+
+            # VPP EP create ...
+            ep.add_vpp_config()
+
+            self.logger.info(self.vapi.cli("sh gbp endpoint"))
+
+            # ... results in a Gratuitous ARP/ND on the EPG's uplink
+            rx = ep.epg.uplink.get_capture(len(ep.ips), timeout=0.2)
+
+            for ii, ip in enumerate(ep.ips):
+                p = rx[ii]
+
+                if ip.is_ip6:
+                    self.assertTrue(p.haslayer(ICMPv6ND_NA))
+                    self.assertEqual(p[ICMPv6ND_NA].tgt, ip.address)
+                else:
+                    self.assertTrue(p.haslayer(ARP))
+                    self.assertEqual(p[ARP].psrc, ip.address)
+                    self.assertEqual(p[ARP].pdst, ip.address)
+
+            # add the BD ARP termination entry for floating IP
+            for fip in ep.fips:
+                ba = VppBridgeDomainArpEntry(self, epg_nat.bd.bd, ep.mac, fip)
+                ba.add_vpp_config()
+
+                # floating IPs route via EPG recirc
+                r = VppIpRoute(self, fip.address, fip.length,
+                               [VppRoutePath(fip.address,
+                                             ep.recirc.recirc.sw_if_index,
+                                             is_dvr=1,
+                                             proto=fip.dpo_proto)],
+                               table_id=20,
+                               is_ip6=fip.is_ip6)
+                r.add_vpp_config()
+
+            # L2 FIB entries in the NAT EPG BD to bridge the packets from
+            # the outside direct to the internal EPG
+            lf = VppL2FibEntry(self, epg_nat.bd.bd, ep.mac,
+                               ep.recirc.recirc, bvi_mac=0)
+            lf.add_vpp_config()
+
+        #
+        # ARP packets for unknown IP are sent to the EPG uplink
+        #
+        pkt_arp = (Ether(dst="ff:ff:ff:ff:ff:ff",
+                         src=self.pg0.remote_mac) /
+                   ARP(op="who-has",
+                       hwdst="ff:ff:ff:ff:ff:ff",
+                       hwsrc=self.pg0.remote_mac,
+                       pdst="10.0.0.88",
+                       psrc="10.0.0.99"))
+
+        self.vapi.cli("clear trace")
+        self.pg0.add_stream(pkt_arp)
+
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+
+        rxd = epgs[0].uplink.get_capture(1)
+
+        #
+        # ARP/ND packets get a response
+        #
+        pkt_arp = (Ether(dst="ff:ff:ff:ff:ff:ff",
+                         src=self.pg0.remote_mac) /
+                   ARP(op="who-has",
+                       hwdst="ff:ff:ff:ff:ff:ff",
+                       hwsrc=self.pg0.remote_mac,
+                       pdst=epgs[0].bvi_ip4.address,
+                       psrc=eps[0].ip4.address))
+
+        self.send_and_expect(self.pg0, [pkt_arp], self.pg0)
+
+        nsma = in6_getnsma(inet_pton(AF_INET6, eps[0].ip6.address))
+        d = inet_ntop(AF_INET6, nsma)
+        pkt_nd = (Ether(dst=in6_getnsmac(nsma),
+                        src=self.pg0.remote_mac) /
+                  IPv6(dst=d, src=eps[0].ip6.address) /
+                  ICMPv6ND_NS(tgt=epgs[0].bvi_ip6.address) /
+                  ICMPv6NDOptSrcLLAddr(lladdr=self.pg0.remote_mac))
+        self.send_and_expect(self.pg0, [pkt_nd], self.pg0)
+
+        #
+        # broadcast packets are flooded
+        #
+        pkt_bcast = (Ether(dst="ff:ff:ff:ff:ff:ff",
+                           src=self.pg0.remote_mac) /
+                     IP(src=eps[0].ip4.address, dst="232.1.1.1") /
+                     UDP(sport=1234, dport=1234) /
+                     Raw('\xa5' * 100))
+
+        self.vapi.cli("clear trace")
+        self.pg0.add_stream(pkt_bcast)
+
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+
+        rxd = eps[1].itf.get_capture(1)
+        self.assertEqual(rxd[0][Ether].dst, pkt_bcast[Ether].dst)
+        rxd = epgs[0].uplink.get_capture(1)
+        self.assertEqual(rxd[0][Ether].dst, pkt_bcast[Ether].dst)
+
+        #
+        # packets to non-local L3 destinations dropped
+        #
+        pkt_intra_epg_220_ip4 = (Ether(src=self.pg0.remote_mac,
+                                       dst=self.router_mac.address) /
+                                 IP(src=eps[0].ip4.address,
+                                    dst="10.0.0.99") /
+                                 UDP(sport=1234, dport=1234) /
+                                 Raw('\xa5' * 100))
+        pkt_inter_epg_222_ip4 = (Ether(src=self.pg0.remote_mac,
+                                       dst=self.router_mac.address) /
+                                 IP(src=eps[0].ip4.address,
+                                    dst="10.0.1.99") /
+                                 UDP(sport=1234, dport=1234) /
+                                 Raw('\xa5' * 100))
+
+        self.send_and_assert_no_replies(self.pg0, pkt_intra_epg_220_ip4 * 65)
+
+        pkt_inter_epg_222_ip6 = (Ether(src=self.pg0.remote_mac,
+                                       dst=self.router_mac.address) /
+                                 IPv6(src=eps[0].ip6.address,
+                                      dst="2001:10::99") /
+                                 UDP(sport=1234, dport=1234) /
+                                 Raw('\xa5' * 100))
+        self.send_and_assert_no_replies(self.pg0, pkt_inter_epg_222_ip6 * 65)
+
+        #
+        # Add the subnet routes
+        #
+        s41 = VppGbpSubnet(
+            self, rd0, "10.0.0.0", 24,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_STITCHED_INTERNAL)
+        s42 = VppGbpSubnet(
+            self, rd0, "10.0.1.0", 24,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_STITCHED_INTERNAL)
+        s43 = VppGbpSubnet(
+            self, rd0, "10.0.2.0", 24,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_STITCHED_INTERNAL)
+        s61 = VppGbpSubnet(
+            self, rd0, "2001:10::1", 64,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_STITCHED_INTERNAL)
+        s62 = VppGbpSubnet(
+            self, rd0, "2001:10:1::1", 64,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_STITCHED_INTERNAL)
+        s63 = VppGbpSubnet(
+            self, rd0, "2001:10:2::1", 64,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_STITCHED_INTERNAL)
+        s41.add_vpp_config()
+        s42.add_vpp_config()
+        s43.add_vpp_config()
+        s61.add_vpp_config()
+        s62.add_vpp_config()
+        s63.add_vpp_config()
+
+        self.send_and_expect_bridged(eps[0].itf,
+                                     pkt_intra_epg_220_ip4 * 65,
+                                     eps[0].epg.uplink)
+        self.send_and_expect_bridged(eps[0].itf,
+                                     pkt_inter_epg_222_ip4 * 65,
+                                     eps[0].epg.uplink)
+        self.send_and_expect_bridged6(eps[0].itf,
+                                      pkt_inter_epg_222_ip6 * 65,
+                                      eps[0].epg.uplink)
+
+        self.logger.info(self.vapi.cli("sh ip fib 11.0.0.2"))
+        self.logger.info(self.vapi.cli("sh gbp endpoint-group"))
+        self.logger.info(self.vapi.cli("sh gbp endpoint"))
+        self.logger.info(self.vapi.cli("sh gbp recirc"))
+        self.logger.info(self.vapi.cli("sh int"))
+        self.logger.info(self.vapi.cli("sh int addr"))
+        self.logger.info(self.vapi.cli("sh int feat loop6"))
+        self.logger.info(self.vapi.cli("sh vlib graph ip4-gbp-src-classify"))
+        self.logger.info(self.vapi.cli("sh int feat loop3"))
+        self.logger.info(self.vapi.cli("sh int feat pg0"))
+
+        #
+        # Packet destined to unknown unicast is sent on the epg uplink ...
+        #
+        pkt_intra_epg_220_to_uplink = (Ether(src=self.pg0.remote_mac,
+                                             dst="00:00:00:33:44:55") /
+                                       IP(src=eps[0].ip4.address,
+                                          dst="10.0.0.99") /
+                                       UDP(sport=1234, dport=1234) /
+                                       Raw('\xa5' * 100))
+
+        self.send_and_expect_bridged(eps[0].itf,
+                                     pkt_intra_epg_220_to_uplink * 65,
+                                     eps[0].epg.uplink)
+        # ... and nowhere else
+        self.pg1.get_capture(0, timeout=0.1)
+        self.pg1.assert_nothing_captured(remark="Flood onto other VMS")
+
+        pkt_intra_epg_221_to_uplink = (Ether(src=self.pg2.remote_mac,
+                                             dst="00:00:00:33:44:66") /
+                                       IP(src=eps[0].ip4.address,
+                                          dst="10.0.0.99") /
+                                       UDP(sport=1234, dport=1234) /
+                                       Raw('\xa5' * 100))
+
+        self.send_and_expect_bridged(eps[2].itf,
+                                     pkt_intra_epg_221_to_uplink * 65,
+                                     eps[2].epg.uplink)
+
+        #
+        # Packets from the uplink are forwarded in the absence of a contract
+        #
+        pkt_intra_epg_220_from_uplink = (Ether(src="00:00:00:33:44:55",
+                                               dst=self.pg0.remote_mac) /
+                                         IP(src=eps[0].ip4.address,
+                                            dst="10.0.0.99") /
+                                         UDP(sport=1234, dport=1234) /
+                                         Raw('\xa5' * 100))
+
+        self.send_and_expect_bridged(self.pg4,
+                                     pkt_intra_epg_220_from_uplink * 65,
+                                     self.pg0)
+
+        #
+        # in the absence of policy, endpoints in the same EPG
+        # can communicate
+        #
+        pkt_intra_epg = (Ether(src=self.pg0.remote_mac,
+                               dst=self.pg1.remote_mac) /
+                         IP(src=eps[0].ip4.address,
+                            dst=eps[1].ip4.address) /
+                         UDP(sport=1234, dport=1234) /
+                         Raw('\xa5' * 100))
+
+        self.send_and_expect_bridged(self.pg0, pkt_intra_epg * 65, self.pg1)
+
+        #
+        # in the abscense of policy, endpoints in the different EPG
+        # cannot communicate
+        #
+        pkt_inter_epg_220_to_221 = (Ether(src=self.pg0.remote_mac,
+                                          dst=self.pg2.remote_mac) /
+                                    IP(src=eps[0].ip4.address,
+                                       dst=eps[2].ip4.address) /
+                                    UDP(sport=1234, dport=1234) /
+                                    Raw('\xa5' * 100))
+        pkt_inter_epg_221_to_220 = (Ether(src=self.pg2.remote_mac,
+                                          dst=self.pg0.remote_mac) /
+                                    IP(src=eps[2].ip4.address,
+                                       dst=eps[0].ip4.address) /
+                                    UDP(sport=1234, dport=1234) /
+                                    Raw('\xa5' * 100))
+        pkt_inter_epg_220_to_222 = (Ether(src=self.pg0.remote_mac,
+                                          dst=self.router_mac.address) /
+                                    IP(src=eps[0].ip4.address,
+                                       dst=eps[3].ip4.address) /
+                                    UDP(sport=1234, dport=1234) /
+                                    Raw('\xa5' * 100))
+
+        self.send_and_assert_no_replies(eps[0].itf,
+                                        pkt_inter_epg_220_to_221 * 65)
+        self.send_and_assert_no_replies(eps[0].itf,
+                                        pkt_inter_epg_220_to_222 * 65)
+
+        #
+        # A uni-directional contract from EPG 220 -> 221
+        #
+        acl = VppGbpAcl(self)
+        rule = acl.create_rule(permit_deny=1, proto=17)
+        rule2 = acl.create_rule(is_ipv6=1, permit_deny=1, proto=17)
+        acl_index = acl.add_vpp_config([rule, rule2])
+        c1 = VppGbpContract(self, 220, 221, acl_index)
+        c1.add_vpp_config()
+
+        self.send_and_expect_bridged(eps[0].itf,
+                                     pkt_inter_epg_220_to_221 * 65,
+                                     eps[2].itf)
+        self.send_and_assert_no_replies(eps[0].itf,
+                                        pkt_inter_epg_220_to_222 * 65)
+
+        #
+        # contract for the return direction
+        #
+        c2 = VppGbpContract(self, 221, 220, acl_index)
+        c2.add_vpp_config()
+
+        self.send_and_expect_bridged(eps[0].itf,
+                                     pkt_inter_epg_220_to_221 * 65,
+                                     eps[2].itf)
+        self.send_and_expect_bridged(eps[2].itf,
+                                     pkt_inter_epg_221_to_220 * 65,
+                                     eps[0].itf)
+
+        #
+        # check that inter group is still disabled for the groups
+        # not in the contract.
+        #
+        self.send_and_assert_no_replies(eps[0].itf,
+                                        pkt_inter_epg_220_to_222 * 65)
+
+        #
+        # A uni-directional contract from EPG 220 -> 222 'L3 routed'
+        #
+        c3 = VppGbpContract(self, 220, 222, acl_index)
+        c3.add_vpp_config()
+
+        self.logger.info(self.vapi.cli("sh gbp contract"))
+
+        self.send_and_expect_routed(eps[0].itf,
+                                    pkt_inter_epg_220_to_222 * 65,
+                                    eps[3].itf,
+                                    self.router_mac.address)
+
+        #
+        # remove both contracts, traffic stops in both directions
+        #
+        c2.remove_vpp_config()
+        c1.remove_vpp_config()
+        c3.remove_vpp_config()
+        acl.remove_vpp_config()
+
+        self.send_and_assert_no_replies(eps[2].itf,
+                                        pkt_inter_epg_221_to_220 * 65)
+        self.send_and_assert_no_replies(eps[0].itf,
+                                        pkt_inter_epg_220_to_221 * 65)
+        self.send_and_expect_bridged(eps[0].itf,
+                                     pkt_intra_epg * 65,
+                                     eps[1].itf)
+
+        #
+        # EPs to the outside world
+        #
+
+        # in the EP's RD an external subnet via the NAT EPG's recirc
+        se1 = VppGbpSubnet(
+            self, rd0, "0.0.0.0", 0,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_STITCHED_EXTERNAL,
+            sw_if_index=recirc_nat.recirc.sw_if_index,
+            epg=epg_nat.epg)
+        se2 = VppGbpSubnet(
+            self, rd0, "11.0.0.0", 8,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_STITCHED_EXTERNAL,
+            sw_if_index=recirc_nat.recirc.sw_if_index,
+            epg=epg_nat.epg)
+        se16 = VppGbpSubnet(
+            self, rd0, "::", 0,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_STITCHED_EXTERNAL,
+            sw_if_index=recirc_nat.recirc.sw_if_index,
+            epg=epg_nat.epg)
+        # in the NAT RD an external subnet via the NAT EPG's uplink
+        se3 = VppGbpSubnet(
+            self, rd20, "0.0.0.0", 0,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_STITCHED_EXTERNAL,
+            sw_if_index=epg_nat.uplink.sw_if_index,
+            epg=epg_nat.epg)
+        se36 = VppGbpSubnet(
+            self, rd20, "::", 0,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_STITCHED_EXTERNAL,
+            sw_if_index=epg_nat.uplink.sw_if_index,
+            epg=epg_nat.epg)
+        se4 = VppGbpSubnet(
+            self, rd20, "11.0.0.0", 8,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_STITCHED_EXTERNAL,
+            sw_if_index=epg_nat.uplink.sw_if_index,
+            epg=epg_nat.epg)
+        se1.add_vpp_config()
+        se2.add_vpp_config()
+        se16.add_vpp_config()
+        se3.add_vpp_config()
+        se36.add_vpp_config()
+        se4.add_vpp_config()
+
+        self.logger.info(self.vapi.cli("sh ip fib 0.0.0.0/0"))
+        self.logger.info(self.vapi.cli("sh ip fib 11.0.0.1"))
+        self.logger.info(self.vapi.cli("sh ip6 fib ::/0"))
+        self.logger.info(self.vapi.cli("sh ip6 fib %s" %
+                                       eps[0].fip6))
+
+        #
+        # From an EP to an outside addess: IN2OUT
+        #
+        pkt_inter_epg_220_to_global = (Ether(src=self.pg0.remote_mac,
+                                             dst=self.router_mac.address) /
+                                       IP(src=eps[0].ip4.address,
+                                          dst="1.1.1.1") /
+                                       UDP(sport=1234, dport=1234) /
+                                       Raw('\xa5' * 100))
+
+        # no policy yet
+        self.send_and_assert_no_replies(eps[0].itf,
+                                        pkt_inter_epg_220_to_global * 65)
+
+        acl2 = VppGbpAcl(self)
+        rule = acl2.create_rule(permit_deny=1, proto=17, sport_from=1234,
+                                sport_to=1234, dport_from=1234, dport_to=1234)
+        rule2 = acl2.create_rule(is_ipv6=1, permit_deny=1, proto=17,
+                                 sport_from=1234, sport_to=1234,
+                                 dport_from=1234, dport_to=1234)
+
+        acl_index2 = acl2.add_vpp_config([rule, rule2])
+        c4 = VppGbpContract(self, 220, 333, acl_index2)
+        c4.add_vpp_config()
+
+        self.send_and_expect_natted(eps[0].itf,
+                                    pkt_inter_epg_220_to_global * 65,
+                                    self.pg7,
+                                    eps[0].fip4.address)
+
+        pkt_inter_epg_220_to_global = (Ether(src=self.pg0.remote_mac,
+                                             dst=self.router_mac.address) /
+                                       IPv6(src=eps[0].ip6.address,
+                                            dst="6001::1") /
+                                       UDP(sport=1234, dport=1234) /
+                                       Raw('\xa5' * 100))
+
+        self.send_and_expect_natted6(self.pg0,
+                                     pkt_inter_epg_220_to_global * 65,
+                                     self.pg7,
+                                     eps[0].fip6.address)
+
+        #
+        # From a global address to an EP: OUT2IN
+        #
+        pkt_inter_epg_220_from_global = (Ether(src=self.router_mac.address,
+                                               dst=self.pg0.remote_mac) /
+                                         IP(dst=eps[0].fip4.address,
+                                            src="1.1.1.1") /
+                                         UDP(sport=1234, dport=1234) /
+                                         Raw('\xa5' * 100))
+
+        self.send_and_assert_no_replies(self.pg7,
+                                        pkt_inter_epg_220_from_global * 65)
+
+        c5 = VppGbpContract(self, 333, 220, acl_index2)
+        c5.add_vpp_config()
+
+        self.send_and_expect_unnatted(self.pg7,
+                                      pkt_inter_epg_220_from_global * 65,
+                                      eps[0].itf,
+                                      eps[0].ip4.address)
+
+        pkt_inter_epg_220_from_global = (Ether(src=self.router_mac.address,
+                                               dst=self.pg0.remote_mac) /
+                                         IPv6(dst=eps[0].fip6.address,
+                                              src="6001::1") /
+                                         UDP(sport=1234, dport=1234) /
+                                         Raw('\xa5' * 100))
+
+        self.send_and_expect_unnatted6(self.pg7,
+                                       pkt_inter_epg_220_from_global * 65,
+                                       eps[0].itf,
+                                       eps[0].ip6.address)
+
+        #
+        # From a local VM to another local VM using resp. public addresses:
+        #  IN2OUT2IN
+        #
+        pkt_intra_epg_220_global = (Ether(src=self.pg0.remote_mac,
+                                          dst=self.router_mac.address) /
+                                    IP(src=eps[0].ip4.address,
+                                       dst=eps[1].fip4.address) /
+                                    UDP(sport=1234, dport=1234) /
+                                    Raw('\xa5' * 100))
+
+        self.send_and_expect_double_natted(eps[0].itf,
+                                           pkt_intra_epg_220_global * 65,
+                                           eps[1].itf,
+                                           eps[0].fip4.address,
+                                           eps[1].ip4.address)
+
+        pkt_intra_epg_220_global = (Ether(src=self.pg0.remote_mac,
+                                          dst=self.router_mac.address) /
+                                    IPv6(src=eps[0].ip6.address,
+                                         dst=eps[1].fip6.address) /
+                                    UDP(sport=1234, dport=1234) /
+                                    Raw('\xa5' * 100))
+
+        self.send_and_expect_double_natted6(eps[0].itf,
+                                            pkt_intra_epg_220_global * 65,
+                                            eps[1].itf,
+                                            eps[0].fip6.address,
+                                            eps[1].ip6.address)
+
+        #
+        # cleanup
+        #
+        for ep in eps:
+            # del static mappings for each EP from the 10/8 to 11/8 network
+            self.vapi.nat44_add_del_static_mapping(ep.ip4.bytes,
+                                                   ep.fip4.bytes,
+                                                   vrf_id=0,
+                                                   addr_only=1,
+                                                   is_add=0)
+            self.vapi.nat66_add_del_static_mapping(ep.ip6.bytes,
+                                                   ep.fip6.bytes,
+                                                   vrf_id=0,
+                                                   is_add=0)
+
+        for epg in epgs:
+            # IP config on the BVI interfaces
+            if epg != epgs[0] and epg != epgs[3]:
+                self.vapi.nat44_interface_add_del_feature(epg.bvi.sw_if_index,
+                                                          is_inside=1,
+                                                          is_add=0)
+                self.vapi.nat66_add_del_interface(epg.bvi.sw_if_index,
+                                                  is_inside=1,
+                                                  is_add=0)
+
+        for recirc in recircs:
+            self.vapi.sw_interface_set_l2_emulation(
+                recirc.recirc.sw_if_index, enable=0)
+            self.vapi.nat44_interface_add_del_feature(
+                recirc.recirc.sw_if_index,
+                is_inside=0,
+                is_add=0)
+            self.vapi.nat66_add_del_interface(
+                recirc.recirc.sw_if_index,
+                is_inside=0,
+                is_add=0)
+
+    def test_gbp_learn_l2(self):
+        """ GBP L2 Endpoint Learning """
+
+        learnt = [{'mac': '00:00:11:11:11:01',
+                   'ip': '10.0.0.1',
+                   'ip6': '2001:10::2'},
+                  {'mac': '00:00:11:11:11:02',
+                   'ip': '10.0.0.2',
+                   'ip6': '2001:10::3'}]
+
+        #
+        # lower the inactive threshold so these tests pass in a
+        # reasonable amount of time
+        #
+        self.vapi.gbp_endpoint_learn_set_inactive_threshold(1)
+
+        #
+        # IP tables
+        #
+        gt4 = VppIpTable(self, 1)
+        gt4.add_vpp_config()
+        gt6 = VppIpTable(self, 1, is_ip6=True)
+        gt6.add_vpp_config()
+
+        rd1 = VppGbpRouteDomain(self, 1, gt4, gt6)
+        rd1.add_vpp_config()
+
+        #
+        # Pg2 hosts the vxlan tunnel, hosts on pg2 to act as TEPs
+        # Pg3 hosts the IP4 UU-flood VXLAN tunnel
+        # Pg4 hosts the IP6 UU-flood VXLAN tunnel
+        #
+        self.pg2.config_ip4()
+        self.pg2.resolve_arp()
+        self.pg2.generate_remote_hosts(4)
+        self.pg2.configure_ipv4_neighbors()
+        self.pg3.config_ip4()
+        self.pg3.resolve_arp()
+        self.pg4.config_ip4()
+        self.pg4.resolve_arp()
+
+        #
+        # a GBP bridge domain with a BVI and a UU-flood interface
+        #
+        bd1 = VppBridgeDomain(self, 1)
+        bd1.add_vpp_config()
+        gbd1 = VppGbpBridgeDomain(self, bd1, self.loop0, self.pg3)
+        gbd1.add_vpp_config()
+
+        self.logger.info(self.vapi.cli("sh bridge 1 detail"))
+        self.logger.info(self.vapi.cli("sh gbp bridge"))
+
+        # ... and has a /32 applied
+        ip_addr = VppIpInterfaceAddress(self, gbd1.bvi, "10.0.0.128", 32)
+        ip_addr.add_vpp_config()
+
+        #
+        # The Endpoint-group in which we are learning endpoints
+        #
+        epg_220 = VppGbpEndpointGroup(self, 220, rd1, gbd1,
+                                      None, self.loop0,
+                                      "10.0.0.128",
+                                      "2001:10::128")
+        epg_220.add_vpp_config()
+        epg_330 = VppGbpEndpointGroup(self, 330, rd1, gbd1,
+                                      None, self.loop1,
+                                      "10.0.1.128",
+                                      "2001:11::128")
+        epg_330.add_vpp_config()
+
+        #
+        # The VXLAN GBP tunnel is a bridge-port and has L2 endpoint
+        # leanring enabled
+        #
+        vx_tun_l2_1 = VppGbpVxlanTunnel(
+            self, 99, bd1.bd_id,
+            VppEnum.vl_api_gbp_vxlan_tunnel_mode_t.GBP_VXLAN_TUNNEL_MODE_L2)
+        vx_tun_l2_1.add_vpp_config()
+
+        #
+        # A static endpoint that the learnt endpoints are trying to
+        # talk to
+        #
+        ep = VppGbpEndpoint(self, self.pg0,
+                            epg_220, None,
+                            "10.0.0.127", "11.0.0.127",
+                            "2001:10::1", "3001::1")
+        ep.add_vpp_config()
+
+        self.assertTrue(find_route(self, ep.ip4.address, 32, table_id=1))
+
+        # a packet with an sclass from an unknwon EPG
+        p = (Ether(src=self.pg2.remote_mac,
+                   dst=self.pg2.local_mac) /
+             IP(src=self.pg2.remote_hosts[0].ip4,
+                dst=self.pg2.local_ip4) /
+             UDP(sport=1234, dport=48879) /
+             VXLAN(vni=99, gpid=88, flags=0x88) /
+             Ether(src=learnt[0]["mac"], dst=ep.mac) /
+             IP(src=learnt[0]["ip"], dst=ep.ip4.address) /
+             UDP(sport=1234, dport=1234) /
+             Raw('\xa5' * 100))
+
+        self.send_and_assert_no_replies(self.pg2, p)
+
+        #
+        # we should not have learnt a new tunnel endpoint, since
+        # the EPG was not learnt.
+        #
+        self.assertEqual(INDEX_INVALID,
+                         find_vxlan_gbp_tunnel(self,
+                                               self.pg2.local_ip4,
+                                               self.pg2.remote_hosts[0].ip4,
+                                               99))
+
+        # epg is not learnt, becasue the EPG is unknwon
+        self.assertEqual(len(self.vapi.gbp_endpoint_dump()), 1)
+
+        for ii, l in enumerate(learnt):
+            # a packet with an sclass from a knwon EPG
+            # arriving on an unknown TEP
+            p = (Ether(src=self.pg2.remote_mac,
+                       dst=self.pg2.local_mac) /
+                 IP(src=self.pg2.remote_hosts[1].ip4,
+                    dst=self.pg2.local_ip4) /
+                 UDP(sport=1234, dport=48879) /
+                 VXLAN(vni=99, gpid=220, flags=0x88) /
+                 Ether(src=l['mac'], dst=ep.mac) /
+                 IP(src=l['ip'], dst=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rx = self.send_and_expect(self.pg2, [p], self.pg0)
+
+            # the new TEP
+            tep1_sw_if_index = find_vxlan_gbp_tunnel(
+                self,
+                self.pg2.local_ip4,
+                self.pg2.remote_hosts[1].ip4,
+                99)
+            self.assertNotEqual(INDEX_INVALID, tep1_sw_if_index)
+
+            #
+            # the EP is learnt via the learnt TEP
+            # both from its MAC and its IP
+            #
+            self.assertTrue(find_gbp_endpoint(self,
+                                              vx_tun_l2_1.sw_if_index,
+                                              mac=l['mac']))
+            self.assertTrue(find_gbp_endpoint(self,
+                                              vx_tun_l2_1.sw_if_index,
+                                              ip=l['ip']))
+
+        self.logger.info(self.vapi.cli("show gbp endpoint"))
+        self.logger.info(self.vapi.cli("show gbp vxlan"))
+        self.logger.info(self.vapi.cli("show vxlan-gbp tunnel"))
+
+        #
+        # If we sleep for the threshold time, the learnt endpoints should
+        # age out
+        #
+        self.sleep(2)
+        for l in learnt:
+            self.assertFalse(find_gbp_endpoint(self,
+                                               tep1_sw_if_index,
+                                               mac=l['mac']))
+
+        self.logger.info(self.vapi.cli("show gbp endpoint"))
+        self.logger.info(self.vapi.cli("show gbp vxlan"))
+        self.logger.info(self.vapi.cli("show vxlan-gbp tunnel"))
+
+        #
+        # repeat. the do not learn bit is set so the EPs are not learnt
+        #
+        for l in learnt:
+            # a packet with an sclass from a knwon EPG
+            p = (Ether(src=self.pg2.remote_mac,
+                       dst=self.pg2.local_mac) /
+                 IP(src=self.pg2.remote_hosts[1].ip4,
+                    dst=self.pg2.local_ip4) /
+                 UDP(sport=1234, dport=48879) /
+                 VXLAN(vni=99, gpid=220, flags=0x88, gpflags="D") /
+                 Ether(src=l['mac'], dst=ep.mac) /
+                 IP(src=l['ip'], dst=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rx = self.send_and_expect(self.pg2, p*65, self.pg0)
+
+        for l in learnt:
+            self.assertFalse(find_gbp_endpoint(self,
+                                               vx_tun_l2_1.sw_if_index,
+                                               mac=l['mac']))
+
+        #
+        # repeat
+        #
+        for l in learnt:
+            # a packet with an sclass from a knwon EPG
+            p = (Ether(src=self.pg2.remote_mac,
+                       dst=self.pg2.local_mac) /
+                 IP(src=self.pg2.remote_hosts[1].ip4,
+                    dst=self.pg2.local_ip4) /
+                 UDP(sport=1234, dport=48879) /
+                 VXLAN(vni=99, gpid=220, flags=0x88) /
+                 Ether(src=l['mac'], dst=ep.mac) /
+                 IP(src=l['ip'], dst=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rx = self.send_and_expect(self.pg2, p*65, self.pg0)
+
+            self.assertTrue(find_gbp_endpoint(self,
+                                              vx_tun_l2_1.sw_if_index,
+                                              mac=l['mac']))
+
+        #
+        # Static EP replies to dynamics
+        #
+        self.logger.info(self.vapi.cli("sh l2fib bd_id 1"))
+        for l in learnt:
+            p = (Ether(src=ep.mac, dst=l['mac']) /
+                 IP(dst=l['ip'], src=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rxs = self.send_and_expect(self.pg0, p * 17, self.pg2)
+
+            for rx in rxs:
+                self.assertEqual(rx[IP].src, self.pg2.local_ip4)
+                self.assertEqual(rx[IP].dst, self.pg2.remote_hosts[1].ip4)
+                self.assertEqual(rx[UDP].dport, 48879)
+                # the UDP source port is a random value for hashing
+                self.assertEqual(rx[VXLAN].gpid, 220)
+                self.assertEqual(rx[VXLAN].vni, 99)
+                self.assertTrue(rx[VXLAN].flags.G)
+                self.assertTrue(rx[VXLAN].flags.Instance)
+                self.assertTrue(rx[VXLAN].gpflags.A)
+                self.assertFalse(rx[VXLAN].gpflags.D)
+
+        self.sleep(2)
+        for l in learnt:
+            self.assertFalse(find_gbp_endpoint(self,
+                                               vx_tun_l2_1.sw_if_index,
+                                               mac=l['mac']))
+
+        #
+        # repeat in the other EPG
+        # there's no contract between 220 and 330, but the A-bit is set
+        # so the packet is cleared for delivery
+        #
+        for l in learnt:
+            # a packet with an sclass from a knwon EPG
+            p = (Ether(src=self.pg2.remote_mac,
+                       dst=self.pg2.local_mac) /
+                 IP(src=self.pg2.remote_hosts[1].ip4,
+                    dst=self.pg2.local_ip4) /
+                 UDP(sport=1234, dport=48879) /
+                 VXLAN(vni=99, gpid=330, flags=0x88, gpflags='A') /
+                 Ether(src=l['mac'], dst=ep.mac) /
+                 IP(src=l['ip'], dst=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rx = self.send_and_expect(self.pg2, p*65, self.pg0)
+
+            self.assertTrue(find_gbp_endpoint(self,
+                                              vx_tun_l2_1.sw_if_index,
+                                              mac=l['mac']))
+
+        #
+        # static EP cannot reach the learnt EPs since there is no contract
+        #
+        self.logger.info(self.vapi.cli("show gbp endpoint"))
+        self.logger.info(self.vapi.cli("show l2fib all"))
+        for l in learnt:
+            p = (Ether(src=ep.mac, dst=l['mac']) /
+                 IP(dst=l['ip'], src=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            self.send_and_assert_no_replies(self.pg0, [p], timeout=0.2)
+
+        #
+        # refresh the entries after the check for no replies above
+        #
+        for l in learnt:
+            # a packet with an sclass from a knwon EPG
+            p = (Ether(src=self.pg2.remote_mac,
+                       dst=self.pg2.local_mac) /
+                 IP(src=self.pg2.remote_hosts[1].ip4,
+                    dst=self.pg2.local_ip4) /
+                 UDP(sport=1234, dport=48879) /
+                 VXLAN(vni=99, gpid=330, flags=0x88, gpflags='A') /
+                 Ether(src=l['mac'], dst=ep.mac) /
+                 IP(src=l['ip'], dst=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rx = self.send_and_expect(self.pg2, p*65, self.pg0)
+
+            self.assertTrue(find_gbp_endpoint(self,
+                                              vx_tun_l2_1.sw_if_index,
+                                              mac=l['mac']))
+
+        #
+        # Add the contract so they can talk
+        #
+        acl = VppGbpAcl(self)
+        rule = acl.create_rule(permit_deny=1, proto=17)
+        rule2 = acl.create_rule(is_ipv6=1, permit_deny=1, proto=17)
+        acl_index = acl.add_vpp_config([rule, rule2])
+        c1 = VppGbpContract(self, 220, 330, acl_index)
+        c1.add_vpp_config()
+
+        for l in learnt:
+            p = (Ether(src=ep.mac, dst=l['mac']) /
+                 IP(dst=l['ip'], src=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            self.send_and_expect(self.pg0, [p], self.pg2)
+
+        #
+        # send UU packets from the local EP
+        #
+        self.logger.info(self.vapi.cli("sh bridge 1 detail"))
+        self.logger.info(self.vapi.cli("sh gbp bridge"))
+        p_uu = (Ether(src=ep.mac, dst="00:11:11:11:11:11") /
+                IP(dst="10.0.0.133", src=ep.ip4.address) /
+                UDP(sport=1234, dport=1234) /
+                Raw('\xa5' * 100))
+        rxs = self.send_and_expect(ep.itf, [p_uu], gbd1.uu_flood)
+
+        #
+        # Add a mcast destination VXLAN-GBP tunnel for B&M traffic
+        #
+        tun_bm = VppVxlanGbpTunnel(self, self.pg4.local_ip4,
+                                   "239.1.1.1", 88,
+                                   mcast_itf=self.pg4)
+        tun_bm.add_vpp_config()
+        bp_bm = VppBridgeDomainPort(self, bd1, tun_bm,
+                                    port_type=L2_PORT_TYPE.NORMAL)
+        bp_bm.add_vpp_config()
+
+        self.logger.info(self.vapi.cli("sh bridge 1 detail"))
+
+        p_bm = (Ether(src=ep.mac, dst="ff:ff:ff:ff:ff:ff") /
+                IP(dst="10.0.0.133", src=ep.ip4.address) /
+                UDP(sport=1234, dport=1234) /
+                Raw('\xa5' * 100))
+        rxs = self.send_and_expect_only(ep.itf, [p_bm], tun_bm.mcast_itf)
+
+        #
+        # Check v6 Endpoints
+        #
+        for l in learnt:
+            # a packet with an sclass from a knwon EPG
+            p = (Ether(src=self.pg2.remote_mac,
+                       dst=self.pg2.local_mac) /
+                 IP(src=self.pg2.remote_hosts[1].ip4,
+                    dst=self.pg2.local_ip4) /
+                 UDP(sport=1234, dport=48879) /
+                 VXLAN(vni=99, gpid=330, flags=0x88, gpflags='A') /
+                 Ether(src=l['mac'], dst=ep.mac) /
+                 IPv6(src=l['ip6'], dst=ep.ip6.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rx = self.send_and_expect(self.pg2, p*65, self.pg0)
+
+            self.assertTrue(find_gbp_endpoint(self,
+                                              vx_tun_l2_1.sw_if_index,
+                                              mac=l['mac']))
+
+        #
+        # L3 Endpoint Learning
+        #  - configured on the bridge's BVI
+        #
+
+        #
+        # clean up
+        #
+        self.sleep(2)
+        for l in learnt:
+            self.assertFalse(find_gbp_endpoint(self,
+                                               vx_tun_l2_1.sw_if_index,
+                                               mac=l['mac']))
+
+        self.pg2.unconfig_ip4()
+        self.pg3.unconfig_ip4()
+        self.pg4.unconfig_ip4()
+
+        self.logger.info(self.vapi.cli("sh int"))
+        self.logger.info(self.vapi.cli("sh gbp vxlan"))
+
+    def test_gbp_learn_vlan_l2(self):
+        """ GBP L2 Endpoint w/ VLANs"""
+
+        learnt = [{'mac': '00:00:11:11:11:01',
+                   'ip': '10.0.0.1',
+                   'ip6': '2001:10::2'},
+                  {'mac': '00:00:11:11:11:02',
+                   'ip': '10.0.0.2',
+                   'ip6': '2001:10::3'}]
+
+        #
+        # lower the inactive threshold so these tests pass in a
+        # reasonable amount of time
+        #
+        self.vapi.gbp_endpoint_learn_set_inactive_threshold(1)
+
+        #
+        # IP tables
+        #
+        gt4 = VppIpTable(self, 1)
+        gt4.add_vpp_config()
+        gt6 = VppIpTable(self, 1, is_ip6=True)
+        gt6.add_vpp_config()
+
+        rd1 = VppGbpRouteDomain(self, 1, gt4, gt6)
+        rd1.add_vpp_config()
+
+        #
+        # Pg2 hosts the vxlan tunnel, hosts on pg2 to act as TEPs
+        #
+        self.pg2.config_ip4()
+        self.pg2.resolve_arp()
+        self.pg2.generate_remote_hosts(4)
+        self.pg2.configure_ipv4_neighbors()
+        self.pg3.config_ip4()
+        self.pg3.resolve_arp()
+
+        #
+        # The EP will be on a vlan sub-interface
+        #
+        vlan_11 = VppDot1QSubint(self, self.pg0, 11)
+        vlan_11.admin_up()
+        self.vapi.sw_interface_set_l2_tag_rewrite(vlan_11.sw_if_index,
+                                                  L2_VTR_OP.L2_POP_1,
+                                                  11)
+
+        bd_uu_fwd = VppVxlanGbpTunnel(self, self.pg3.local_ip4,
+                                      self.pg3.remote_ip4, 116)
+        bd_uu_fwd.add_vpp_config()
+
+        #
+        # a GBP bridge domain with a BVI and a UU-flood interface
+        # The BD is marked as do not learn, so no endpoints are ever
+        # learnt in this BD.
+        #
+        bd1 = VppBridgeDomain(self, 1)
+        bd1.add_vpp_config()
+        gbd1 = VppGbpBridgeDomain(self, bd1, self.loop0, bd_uu_fwd,
+                                  learn=False)
+        gbd1.add_vpp_config()
+
+        self.logger.info(self.vapi.cli("sh bridge 1 detail"))
+        self.logger.info(self.vapi.cli("sh gbp bridge"))
+
+        # ... and has a /32 applied
+        ip_addr = VppIpInterfaceAddress(self, gbd1.bvi, "10.0.0.128", 32)
+        ip_addr.add_vpp_config()
+
+        #
+        # The Endpoint-group in which we are learning endpoints
+        #
+        epg_220 = VppGbpEndpointGroup(self, 220, rd1, gbd1,
+                                      None, self.loop0,
+                                      "10.0.0.128",
+                                      "2001:10::128")
+        epg_220.add_vpp_config()
+
+        #
+        # The VXLAN GBP tunnel is a bridge-port and has L2 endpoint
+        # leanring enabled
+        #
+        vx_tun_l2_1 = VppGbpVxlanTunnel(
+            self, 99, bd1.bd_id,
+            VppEnum.vl_api_gbp_vxlan_tunnel_mode_t.GBP_VXLAN_TUNNEL_MODE_L2)
+        vx_tun_l2_1.add_vpp_config()
+
+        #
+        # A static endpoint that the learnt endpoints are trying to
+        # talk to
+        #
+        ep = VppGbpEndpoint(self, vlan_11,
+                            epg_220, None,
+                            "10.0.0.127", "11.0.0.127",
+                            "2001:10::1", "3001::1")
+        ep.add_vpp_config()
+
+        self.assertTrue(find_route(self, ep.ip4.address, 32, table_id=1))
+
+        #
+        # Send to the static EP
+        #
+        for ii, l in enumerate(learnt):
+            # a packet with an sclass from a knwon EPG
+            # arriving on an unknown TEP
+            p = (Ether(src=self.pg2.remote_mac,
+                       dst=self.pg2.local_mac) /
+                 IP(src=self.pg2.remote_hosts[1].ip4,
+                    dst=self.pg2.local_ip4) /
+                 UDP(sport=1234, dport=48879) /
+                 VXLAN(vni=99, gpid=220, flags=0x88) /
+                 Ether(src=l['mac'], dst=ep.mac) /
+                 IP(src=l['ip'], dst=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rxs = self.send_and_expect(self.pg2, [p], self.pg0)
+
+            #
+            # packet to EP has the EP's vlan tag
+            #
+            for rx in rxs:
+                self.assertEqual(rx[Dot1Q].vlan, 11)
+
+            #
+            # the EP is not learnt since the BD setting prevents it
+            # also no TEP too
+            #
+            self.assertFalse(find_gbp_endpoint(self,
+                                               vx_tun_l2_1.sw_if_index,
+                                               mac=l['mac']))
+            self.assertEqual(INDEX_INVALID,
+                             find_vxlan_gbp_tunnel(
+                                 self,
+                                 self.pg2.local_ip4,
+                                 self.pg2.remote_hosts[1].ip4,
+                                 99))
+
+        self.assertEqual(len(self.vapi.gbp_endpoint_dump()), 1)
+
+        #
+        # static to remotes
+        # we didn't learn the remotes so they are sent to the UU-fwd
+        #
+        for l in learnt:
+            p = (Ether(src=ep.mac, dst=l['mac']) /
+                 Dot1Q(vlan=11) /
+                 IP(dst=l['ip'], src=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rxs = self.send_and_expect(self.pg0, p * 17, self.pg3)
+
+            for rx in rxs:
+                self.assertEqual(rx[IP].src, self.pg3.local_ip4)
+                self.assertEqual(rx[IP].dst, self.pg3.remote_ip4)
+                self.assertEqual(rx[UDP].dport, 48879)
+                # the UDP source port is a random value for hashing
+                self.assertEqual(rx[VXLAN].gpid, 220)
+                self.assertEqual(rx[VXLAN].vni, 116)
+                self.assertTrue(rx[VXLAN].flags.G)
+                self.assertTrue(rx[VXLAN].flags.Instance)
+                self.assertFalse(rx[VXLAN].gpflags.A)
+                self.assertFalse(rx[VXLAN].gpflags.D)
+
+        self.pg2.unconfig_ip4()
+        self.pg3.unconfig_ip4()
+
+    def test_gbp_learn_l3(self):
+        """ GBP L3 Endpoint Learning """
+
+        routed_dst_mac = "00:0c:0c:0c:0c:0c"
+        routed_src_mac = "00:22:bd:f8:19:ff"
+
+        learnt = [{'mac': '00:00:11:11:11:02',
+                   'ip': '10.0.1.2',
+                   'ip6': '2001:10::2'},
+                  {'mac': '00:00:11:11:11:03',
+                   'ip': '10.0.1.3',
+                   'ip6': '2001:10::3'}]
+
+        #
+        # lower the inactive threshold so these tests pass in a
+        # reasonable amount of time
+        #
+        self.vapi.gbp_endpoint_learn_set_inactive_threshold(1)
+
+        #
+        # IP tables
+        #
+        t4 = VppIpTable(self, 1)
+        t4.add_vpp_config()
+        t6 = VppIpTable(self, 1, True)
+        t6.add_vpp_config()
+
+        tun_ip4_uu = VppVxlanGbpTunnel(self, self.pg4.local_ip4,
+                                       self.pg4.remote_ip4, 114)
+        tun_ip6_uu = VppVxlanGbpTunnel(self, self.pg4.local_ip4,
+                                       self.pg4.remote_ip4, 116)
+        tun_ip4_uu.add_vpp_config()
+        tun_ip6_uu.add_vpp_config()
+
+        rd1 = VppGbpRouteDomain(self, 2, t4, t6, tun_ip4_uu, tun_ip6_uu)
+        rd1.add_vpp_config()
+
+        self.loop0.set_mac(self.router_mac.address)
+
+        #
+        # Bind the BVI to the RD
+        #
+        VppIpInterfaceBind(self, self.loop0, t4).add_vpp_config()
+        VppIpInterfaceBind(self, self.loop0, t6).add_vpp_config()
+
+        #
+        # Pg2 hosts the vxlan tunnel
+        # hosts on pg2 to act as TEPs
+        # pg3 is BD uu-fwd
+        # pg4 is RD uu-fwd
+        #
+        self.pg2.config_ip4()
+        self.pg2.resolve_arp()
+        self.pg2.generate_remote_hosts(4)
+        self.pg2.configure_ipv4_neighbors()
+        self.pg3.config_ip4()
+        self.pg3.resolve_arp()
+        self.pg4.config_ip4()
+        self.pg4.resolve_arp()
+
+        #
+        # a GBP bridge domain with a BVI and a UU-flood interface
+        #
+        bd1 = VppBridgeDomain(self, 1)
+        bd1.add_vpp_config()
+        gbd1 = VppGbpBridgeDomain(self, bd1, self.loop0, self.pg3)
+        gbd1.add_vpp_config()
+
+        self.logger.info(self.vapi.cli("sh bridge 1 detail"))
+        self.logger.info(self.vapi.cli("sh gbp bridge"))
+        self.logger.info(self.vapi.cli("sh gbp route"))
+        self.logger.info(self.vapi.cli("show l2fib all"))
+
+        # ... and has a /32 and /128 applied
+        ip4_addr = VppIpInterfaceAddress(self, gbd1.bvi, "10.0.0.128", 32)
+        ip4_addr.add_vpp_config()
+        ip6_addr = VppIpInterfaceAddress(self, gbd1.bvi, "2001:10::128", 128)
+        ip6_addr.add_vpp_config()
+
+        #
+        # The Endpoint-group in which we are learning endpoints
+        #
+        epg_220 = VppGbpEndpointGroup(self, 220, rd1, gbd1,
+                                      None, self.loop0,
+                                      "10.0.0.128",
+                                      "2001:10::128")
+        epg_220.add_vpp_config()
+
+        #
+        # The VXLAN GBP tunnel is a bridge-port and has L2 endpoint
+        # leanring enabled
+        #
+        vx_tun_l3 = VppGbpVxlanTunnel(
+            self, 101, rd1.rd_id,
+            VppEnum.vl_api_gbp_vxlan_tunnel_mode_t.GBP_VXLAN_TUNNEL_MODE_L3)
+        vx_tun_l3.add_vpp_config()
+
+        #
+        # A static endpoint that the learnt endpoints are trying to
+        # talk to
+        #
+        ep = VppGbpEndpoint(self, self.pg0,
+                            epg_220, None,
+                            "10.0.0.127", "11.0.0.127",
+                            "2001:10::1", "3001::1")
+        ep.add_vpp_config()
+
+        #
+        # learn some remote IPv4 EPs
+        #
+        for ii, l in enumerate(learnt):
+            # a packet with an sclass from a knwon EPG
+            # arriving on an unknown TEP
+            p = (Ether(src=self.pg2.remote_mac,
+                       dst=self.pg2.local_mac) /
+                 IP(src=self.pg2.remote_hosts[1].ip4,
+                    dst=self.pg2.local_ip4) /
+                 UDP(sport=1234, dport=48879) /
+                 VXLAN(vni=101, gpid=220, flags=0x88) /
+                 Ether(src=l['mac'], dst="00:00:00:11:11:11") /
+                 IP(src=l['ip'], dst=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rx = self.send_and_expect(self.pg2, [p], self.pg0)
+
+            # the new TEP
+            tep1_sw_if_index = find_vxlan_gbp_tunnel(
+                self,
+                self.pg2.local_ip4,
+                self.pg2.remote_hosts[1].ip4,
+                vx_tun_l3.vni)
+            self.assertNotEqual(INDEX_INVALID, tep1_sw_if_index)
+
+            # endpoint learnt via the parent GBP-vxlan interface
+            self.assertTrue(find_gbp_endpoint(self,
+                                              vx_tun_l3._sw_if_index,
+                                              ip=l['ip']))
+
+        #
+        # Static IPv4 EP replies to learnt
+        #
+        for l in learnt:
+            p = (Ether(src=ep.mac, dst=self.loop0.local_mac) /
+                 IP(dst=l['ip'], src=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rxs = self.send_and_expect(self.pg0, p*1, self.pg2)
+
+            for rx in rxs:
+                self.assertEqual(rx[IP].src, self.pg2.local_ip4)
+                self.assertEqual(rx[IP].dst, self.pg2.remote_hosts[1].ip4)
+                self.assertEqual(rx[UDP].dport, 48879)
+                # the UDP source port is a random value for hashing
+                self.assertEqual(rx[VXLAN].gpid, 220)
+                self.assertEqual(rx[VXLAN].vni, 101)
+                self.assertTrue(rx[VXLAN].flags.G)
+                self.assertTrue(rx[VXLAN].flags.Instance)
+                self.assertTrue(rx[VXLAN].gpflags.A)
+                self.assertFalse(rx[VXLAN].gpflags.D)
+
+                inner = rx[VXLAN].payload
+
+                self.assertEqual(inner[Ether].src, routed_src_mac)
+                self.assertEqual(inner[Ether].dst, routed_dst_mac)
+                self.assertEqual(inner[IP].src, ep.ip4.address)
+                self.assertEqual(inner[IP].dst, l['ip'])
+
+        self.sleep(2)
+        for l in learnt:
+            self.assertFalse(find_gbp_endpoint(self,
+                                               tep1_sw_if_index,
+                                               ip=l['ip']))
+
+        #
+        # learn some remote IPv6 EPs
+        #
+        for ii, l in enumerate(learnt):
+            # a packet with an sclass from a knwon EPG
+            # arriving on an unknown TEP
+            p = (Ether(src=self.pg2.remote_mac,
+                       dst=self.pg2.local_mac) /
+                 IP(src=self.pg2.remote_hosts[1].ip4,
+                    dst=self.pg2.local_ip4) /
+                 UDP(sport=1234, dport=48879) /
+                 VXLAN(vni=101, gpid=220, flags=0x88) /
+                 Ether(src=l['mac'], dst="00:00:00:11:11:11") /
+                 IPv6(src=l['ip6'], dst=ep.ip6.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rx = self.send_and_expect(self.pg2, [p], self.pg0)
+
+            # the new TEP
+            tep1_sw_if_index = find_vxlan_gbp_tunnel(
+                self,
+                self.pg2.local_ip4,
+                self.pg2.remote_hosts[1].ip4,
+                vx_tun_l3.vni)
+            self.assertNotEqual(INDEX_INVALID, tep1_sw_if_index)
+
+            self.logger.info(self.vapi.cli("show gbp bridge"))
+            self.logger.info(self.vapi.cli("show vxlan-gbp tunnel"))
+            self.logger.info(self.vapi.cli("show gbp vxlan"))
+            self.logger.info(self.vapi.cli("show int addr"))
+
+            # endpoint learnt via the TEP
+            self.assertTrue(find_gbp_endpoint(self, ip=l['ip6']))
+
+        self.logger.info(self.vapi.cli("show gbp endpoint"))
+        self.logger.info(self.vapi.cli("show ip fib index 1 %s" % l['ip']))
+
+        #
+        # Static EP replies to learnt
+        #
+        for l in learnt:
+            p = (Ether(src=ep.mac, dst=self.loop0.local_mac) /
+                 IPv6(dst=l['ip6'], src=ep.ip6.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rxs = self.send_and_expect(self.pg0, p*65, self.pg2)
+
+            for rx in rxs:
+                self.assertEqual(rx[IP].src, self.pg2.local_ip4)
+                self.assertEqual(rx[IP].dst, self.pg2.remote_hosts[1].ip4)
+                self.assertEqual(rx[UDP].dport, 48879)
+                # the UDP source port is a random value for hashing
+                self.assertEqual(rx[VXLAN].gpid, 220)
+                self.assertEqual(rx[VXLAN].vni, 101)
+                self.assertTrue(rx[VXLAN].flags.G)
+                self.assertTrue(rx[VXLAN].flags.Instance)
+                self.assertTrue(rx[VXLAN].gpflags.A)
+                self.assertFalse(rx[VXLAN].gpflags.D)
+
+                inner = rx[VXLAN].payload
+
+                self.assertEqual(inner[Ether].src, routed_src_mac)
+                self.assertEqual(inner[Ether].dst, routed_dst_mac)
+                self.assertEqual(inner[IPv6].src, ep.ip6.address)
+                self.assertEqual(inner[IPv6].dst, l['ip6'])
+
+        self.logger.info(self.vapi.cli("sh gbp endpoint"))
+        self.sleep(2)
+        for l in learnt:
+            self.assertFalse(find_gbp_endpoint(self,
+                                               tep1_sw_if_index,
+                                               ip=l['ip']))
+
+        #
+        # Static sends to unknown EP with no route
+        #
+        p = (Ether(src=ep.mac, dst=self.loop0.local_mac) /
+             IP(dst="10.0.0.99", src=ep.ip4.address) /
+             UDP(sport=1234, dport=1234) /
+             Raw('\xa5' * 100))
+
+        self.send_and_assert_no_replies(self.pg0, [p])
+
+        #
+        # Add a route to static EP's v4 and v6 subnet
+        #  packets should be send on the v4/v6 uu=fwd interface resp.
+        #
+        se_10_24 = VppGbpSubnet(
+            self, rd1, "10.0.0.0", 24,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_TRANSPORT)
+        se_10_24.add_vpp_config()
+
+        p = (Ether(src=ep.mac, dst=self.loop0.local_mac) /
+             IP(dst="10.0.0.99", src=ep.ip4.address) /
+             UDP(sport=1234, dport=1234) /
+             Raw('\xa5' * 100))
+
+        rxs = self.send_and_expect(self.pg0, [p], self.pg4)
+        for rx in rxs:
+            self.assertEqual(rx[IP].src, self.pg4.local_ip4)
+            self.assertEqual(rx[IP].dst, self.pg4.remote_ip4)
+            self.assertEqual(rx[UDP].dport, 48879)
+            # the UDP source port is a random value for hashing
+            self.assertEqual(rx[VXLAN].gpid, 220)
+            self.assertEqual(rx[VXLAN].vni, 114)
+            self.assertTrue(rx[VXLAN].flags.G)
+            self.assertTrue(rx[VXLAN].flags.Instance)
+            # policy is not applied to packets sent to the uu-fwd interfaces
+            self.assertFalse(rx[VXLAN].gpflags.A)
+            self.assertFalse(rx[VXLAN].gpflags.D)
+
+        #
+        # learn some remote IPv4 EPs
+        #
+        for ii, l in enumerate(learnt):
+            # a packet with an sclass from a knwon EPG
+            # arriving on an unknown TEP
+            p = (Ether(src=self.pg2.remote_mac,
+                       dst=self.pg2.local_mac) /
+                 IP(src=self.pg2.remote_hosts[1].ip4,
+                    dst=self.pg2.local_ip4) /
+                 UDP(sport=1234, dport=48879) /
+                 VXLAN(vni=101, gpid=220, flags=0x88) /
+                 Ether(src=l['mac'], dst="00:00:00:11:11:11") /
+                 IP(src=l['ip'], dst=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rx = self.send_and_expect(self.pg2, [p], self.pg0)
+
+            # the new TEP
+            tep1_sw_if_index = find_vxlan_gbp_tunnel(
+                self,
+                self.pg2.local_ip4,
+                self.pg2.remote_hosts[1].ip4,
+                vx_tun_l3.vni)
+            self.assertNotEqual(INDEX_INVALID, tep1_sw_if_index)
+
+            # endpoint learnt via the parent GBP-vxlan interface
+            self.assertTrue(find_gbp_endpoint(self,
+                                              vx_tun_l3._sw_if_index,
+                                              ip=l['ip']))
+
+        #
+        # Add a remote endpoint from the API
+        #
+        rep_88 = VppGbpEndpoint(self, vx_tun_l3,
+                                epg_220, None,
+                                "10.0.0.88", "11.0.0.88",
+                                "2001:10::88", "3001::88",
+                                VppEnum.vl_api_gbp_endpoint_flags_t.REMOTE,
+                                self.pg2.local_ip4,
+                                self.pg2.remote_hosts[1].ip4,
+                                mac=None)
+        rep_88.add_vpp_config()
+
+        #
+        # Add a remote endpoint from the API that matches an existing one
+        #
+        rep_2 = VppGbpEndpoint(self, vx_tun_l3,
+                               epg_220, None,
+                               learnt[0]['ip'], "11.0.0.101",
+                               learnt[0]['ip6'], "3001::101",
+                               VppEnum.vl_api_gbp_endpoint_flags_t.REMOTE,
+                               self.pg2.local_ip4,
+                               self.pg2.remote_hosts[1].ip4,
+                               mac=None)
+        rep_2.add_vpp_config()
+
+        #
+        # Add a route to the leanred EP's v4 subnet
+        #  packets should be send on the v4/v6 uu=fwd interface resp.
+        #
+        se_10_1_24 = VppGbpSubnet(
+            self, rd1, "10.0.1.0", 24,
+            VppEnum.vl_api_gbp_subnet_type_t.GBP_API_SUBNET_TRANSPORT)
+        se_10_1_24.add_vpp_config()
+
+        self.logger.info(self.vapi.cli("show gbp endpoint"))
+
+        ips = ["10.0.0.88", learnt[0]['ip']]
+        for ip in ips:
+            p = (Ether(src=ep.mac, dst=self.loop0.local_mac) /
+                 IP(dst=ip, src=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rxs = self.send_and_expect(self.pg0, p*65, self.pg2)
+
+            for rx in rxs:
+                self.assertEqual(rx[IP].src, self.pg2.local_ip4)
+                self.assertEqual(rx[IP].dst, self.pg2.remote_hosts[1].ip4)
+                self.assertEqual(rx[UDP].dport, 48879)
+                # the UDP source port is a random value for hashing
+                self.assertEqual(rx[VXLAN].gpid, 220)
+                self.assertEqual(rx[VXLAN].vni, 101)
+                self.assertTrue(rx[VXLAN].flags.G)
+                self.assertTrue(rx[VXLAN].flags.Instance)
+                self.assertTrue(rx[VXLAN].gpflags.A)
+                self.assertFalse(rx[VXLAN].gpflags.D)
+
+                inner = rx[VXLAN].payload
+
+                self.assertEqual(inner[Ether].src, routed_src_mac)
+                self.assertEqual(inner[Ether].dst, routed_dst_mac)
+                self.assertEqual(inner[IP].src, ep.ip4.address)
+                self.assertEqual(inner[IP].dst, ip)
+
+        #
+        # remove the API remote EPs, they are now UU-fwd
+        #
+        rep_88.remove_vpp_config()
+        rep_2.remove_vpp_config()
+
+        self.logger.info(self.vapi.cli("show gbp endpoint"))
+
+        for ip in ips:
+            self.assertFalse(find_gbp_endpoint(self, ip=ip))
+
+            p = (Ether(src=ep.mac, dst=self.loop0.local_mac) /
+                 IP(dst=ip, src=ep.ip4.address) /
+                 UDP(sport=1234, dport=1234) /
+                 Raw('\xa5' * 100))
+
+            rxs = self.send_and_expect(self.pg0, [p], self.pg4)
+
+        #
+        # shutdown with learnt endpoint present
+        #
+        self.logger.info(self.vapi.cli("show gbp endpoint-group"))
+
+        #
+        # TODO
+        # remote endpoint becomes local
+        #
+        self.pg2.unconfig_ip4()
+        self.pg3.unconfig_ip4()
+        self.pg4.unconfig_ip4()
 
 
 if __name__ == '__main__':