Introduce the capability to specify multiple ips for ovn-evpn-local-ip
option in order to enable dual-stack for EVPN vxlan tunnels.
The IPs used for vxlan tunnel can be specified using the following
syntax in the external_ids column of Open_vSwitch table:

external_ids:ovn-evpn-local-ip=10-1.1.1.1,20-2::2,30-3.3.3.3,40-4::4,30-3::3

Reported-at: https://issues.redhat.com/browse/FDP-2768
Signed-off-by: Lorenzo Bianconi <[email protected]>
---
Changes in v5:
- Drop ovn-evpn-local-ip-mapping config option and rely on the following
  syntax:
  external_ids:ovn-evpn-local-ip=10-1.1.1.1,20-2::2,30-3.3.3.3,40-4::4,30-3::3
- Do not use global variable for default_ip4 and default_ip6
- improve code readability
- cosmetics
Changes in v4:
- Do not use linear lookup in evpn_local_ip_lookup_by_vni()
- Introduce ovn-evpn-local-ip-mapping config option
Changes in v3:
- Add default IP configuration
- Fix possible parsing crashes in evpn_vni_local_ip_map_alloc()
- Use hashmap for vni_local_ip mapping
Changes in v2:
- Add NEWS entry
- Update documentation
- Add ip_address_from_str utility routine
- Fix IPv4 vs IPv6 openflow management in physical_consider_evpn_multicast()
- Add Dual-Stack entry in EVPN_SWITCH_TESTS function
---
 NEWS                            |   2 +
 TODO.rst                        |   4 -
 controller/neighbor.c           |   5 +-
 controller/ovn-controller.8.xml |  11 +-
 controller/physical.c           | 324 +++++++++++++----
 tests/system-common-macros.at   |  10 +-
 tests/system-ovn.at             | 597 +++++++++++++++++++++++++++++++-
 7 files changed, 867 insertions(+), 86 deletions(-)

diff --git a/NEWS b/NEWS
index 87500de03..9883fb81d 100644
--- a/NEWS
+++ b/NEWS
@@ -80,6 +80,8 @@ Post v25.09.0
    - Add fallback support for Network Function.
    - Introduce the capability to specify EVPN device names using Logical_Switch
      other_config column.
+   - Introduce the capability to specify multiple ips for ovn-evpn-local-ip
+     option.
 
 OVN v25.09.0 - xxx xx xxxx
 --------------------------
diff --git a/TODO.rst b/TODO.rst
index 0c9b9598b..9f5e0976d 100644
--- a/TODO.rst
+++ b/TODO.rst
@@ -156,10 +156,6 @@ OVN To-do List
     Otherwise we could try to add duplicated Learned_Routes and the ovnsb
     commit would fail.
 
-  * Allow ovn-evpn-local-ip to accept list of
-    $VNI1:$LOCAL_IP1,$VNI2:$LOCAL_IP2 combinations which will be properly
-    reflected in physical flows for given LS with VNI.
-
   * Add support for EVPN L3, that involves MAC Binding learning and
     advertisement.
 
diff --git a/controller/neighbor.c b/controller/neighbor.c
index 9aeb1e36b..e77b41d29 100644
--- a/controller/neighbor.c
+++ b/controller/neighbor.c
@@ -91,7 +91,6 @@ neigh_parse_device_name(struct sset *device_names, struct 
local_datapath *ld,
 {
     const char *names = smap_get_def(&ld->datapath->external_ids,
                                      neighbor_opt_name[type], "");
-    sset_clear(device_names);
     sset_from_delimited_string(device_names, names, ",");
     if (sset_is_empty(device_names)) {
         /* Default device name if not specified. */
@@ -123,7 +122,7 @@ neighbor_run(struct neighbor_ctx_in *n_ctx_in,
             continue;
         }
 
-        struct sset device_names = SSET_INITIALIZER(&device_names);
+        struct sset device_names;
         neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_VXLAN, vni);
         const char *name;
         SSET_FOR_EACH (name, &device_names) {
@@ -132,6 +131,7 @@ neighbor_run(struct neighbor_ctx_in *n_ctx_in,
                                                  NEIGH_IFACE_VXLAN, vni, name);
             vector_push(n_ctx_out->monitored_interfaces, &vxlan);
         }
+        sset_destroy(&device_names);
 
         neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_LOOPBACK, vni);
         if (sset_count(&device_names) > 1) {
@@ -145,6 +145,7 @@ neighbor_run(struct neighbor_ctx_in *n_ctx_in,
                                              NEIGH_IFACE_LOOPBACK, vni,
                                              SSET_FIRST(&device_names));
         vector_push(n_ctx_out->monitored_interfaces, &lo);
+        sset_destroy(&device_names);
 
         neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_BRIDGE, vni);
         if (sset_count(&device_names) > 1) {
diff --git a/controller/ovn-controller.8.xml b/controller/ovn-controller.8.xml
index dfc7cc217..0c5535f3e 100644
--- a/controller/ovn-controller.8.xml
+++ b/controller/ovn-controller.8.xml
@@ -432,10 +432,13 @@
 
       <dt><code>external_ids:ovn-evpn-local-ip</code></dt>
       <dd>
-        IP address used as a source address for the EVPN traffic leaving this
-        OVN setup. There is currently support only for single IP address
-        being specified. NOTE: this feature is experimental and may be subject
-        to removal/change in the future.
+        IP address list used as a source addresses for the EVPN traffic
+        leaving this OVN setup. The <code>ovn-evpn-local-ip</code>
+        can be specified by the CMS according to the following syntax:
+        <code>external_ids:ovn-evpn-local-ip=vni0-IPv4,vni1-IPv4,
+        vni1-IPv6,IPv4,IPv6</code>, where if no VNI value is specified,
+        OVN will use the provided IP as default IP. NOTE: this feature
+        is experimental and may be object to removal/change in the future.
       </dd>
     </dl>
 
diff --git a/controller/physical.c b/controller/physical.c
index b9c60c8ab..ac685f268 100644
--- a/controller/physical.c
+++ b/controller/physical.c
@@ -3178,12 +3178,161 @@ physical_eval_remote_chassis_flows(const struct 
physical_ctx *ctx,
     ofpbuf_uninit(&ingress_ofpacts);
 }
 
+struct vni_local_ip {
+    struct hmap_node hmap_node;
+    struct in6_addr ip;
+    uint32_t vni;
+};
+
+struct evpn_local_ip_map {
+    struct hmap vni_ip_v4;       /* Per VNI local IPv4 vni_local_ips. */
+    struct hmap vni_ip_v6;       /* Per VNI local IPv4 vni_local_ips. */
+    struct in6_addr default_ip4; /* Default local IPv4. */
+    struct in6_addr default_ip6; /* Default local IPv6. */
+};
+
+static const struct in6_addr *
+evpn_local_ip_lookup(const struct hmap *map, uint32_t vni)
+{
+    struct vni_local_ip *e;
+    HMAP_FOR_EACH_WITH_HASH (e, hmap_node, hash_add(vni, 0), map) {
+        if (e->vni == vni) {
+            return &e->ip;
+        }
+    }
+    return NULL;
+}
+
+static ovs_be32
+evpn_local_ip_find_v4(const struct evpn_local_ip_map *vni_ip_map,
+                      uint32_t vni)
+{
+    const struct in6_addr *addr = evpn_local_ip_lookup(&vni_ip_map->vni_ip_v4,
+                                                       vni);
+    if (addr) {
+        return in6_addr_get_mapped_ipv4(addr);
+    }
+
+    if (ipv6_addr_is_set(&vni_ip_map->default_ip4)) {
+        return in6_addr_get_mapped_ipv4(&vni_ip_map->default_ip4);
+    }
+
+    return 0;
+}
+
+static const struct in6_addr *
+evpn_local_ip_find_v6(const struct evpn_local_ip_map *vni_ip_map,
+                      uint32_t vni)
+{
+    const struct in6_addr *addr = evpn_local_ip_lookup(&vni_ip_map->vni_ip_v6,
+                                                       vni);
+    if (addr) {
+        return addr;
+    }
+
+    if (ipv6_addr_is_set(&vni_ip_map->default_ip6)) {
+        return &vni_ip_map->default_ip6;
+    }
+
+    return NULL;
+}
+
+static void
+evpn_local_ip_map_init(struct evpn_local_ip_map *vni_ip_map,
+                       const struct smap *config)
+{
+    char *tokstr, *token, *ptr0 = NULL;
+
+    const char *local_ip_str = smap_get_def(config, "ovn-evpn-local-ip", "");
+    tokstr = xstrdup(local_ip_str);
+    for (token = strtok_r(tokstr, ",", &ptr0); token;
+         token = strtok_r(NULL, ",", &ptr0)) {
+        char *ptr1 = NULL, *vni_str = strtok_r(token, "-", &ptr1);
+        char *ip_str = strtok_r(NULL, "-", &ptr1);
+        struct in6_addr ip;
+        uint32_t vni;
+
+        if (strlen(ptr1)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "Malformed ovn-evpn-local-ip: %s", tokstr);
+            break;
+        }
+
+        if (!ip_str) { /* default IP */
+            if (!ip46_parse(vni_str, &ip)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "Invalid IP: %s", vni_str);
+                continue;
+            }
+            struct in6_addr *ip_ptr = IN6_IS_ADDR_V4MAPPED(&ip)
+                ? &vni_ip_map->default_ip4 : &vni_ip_map->default_ip6;
+            if (ipv6_addr_is_set(ip_ptr)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "Default EVPN local IP%d already configured",
+                             IN6_IS_ADDR_V4MAPPED(&ip) ? 4 : 6);
+                continue;
+            }
+            *ip_ptr = ip;
+        } else {
+            if (!ip46_parse(ip_str, &ip)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl,
+                             "EVPN enabled, but required 'evpn-local-ip' is "
+                             "missing or invalid %s ", local_ip_str);
+                continue;
+            }
+
+            if (!vni_str) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "Required VNI not configured");
+                continue;
+            }
+
+            if (!ovs_scan(vni_str, "%u", &vni) || !ovn_is_valid_vni(vni)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "Invalid VNI: %s", vni_str);
+                continue;
+            }
+
+            struct hmap *map = IN6_IS_ADDR_V4MAPPED(&ip)
+                ? &vni_ip_map->vni_ip_v4 : &vni_ip_map->vni_ip_v6;
+            if (evpn_local_ip_lookup(map, vni)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "Duplicated VNI entry: %s", vni_str);
+                continue;
+            }
+
+            struct vni_local_ip *e = xmalloc(sizeof *e);
+            *e = (struct vni_local_ip) {
+                .vni = vni,
+                .ip = ip,
+            };
+            hmap_insert(map, &e->hmap_node, hash_add(vni, 0));
+        }
+    }
+    free(tokstr);
+}
+
+static void
+evpn_local_ip_map_destroy(struct evpn_local_ip_map *map)
+{
+    struct vni_local_ip *e;
+    HMAP_FOR_EACH_POP (e, hmap_node, &map->vni_ip_v4) {
+        free(e);
+    }
+    hmap_destroy(&map->vni_ip_v4);
+
+    HMAP_FOR_EACH_POP (e, hmap_node, &map->vni_ip_v6) {
+        free(e);
+    }
+    hmap_destroy(&map->vni_ip_v6);
+}
+
 static void
 physical_consider_evpn_binding(const struct evpn_binding *binding,
-                               const struct in6_addr *local_ip,
+                               const struct evpn_local_ip_map *vni_ip_map,
                                struct ofpbuf *ofpacts, struct match *match,
-                               struct ovn_desired_flow_table *flow_table,
-                               bool ipv4)
+                               struct ovn_desired_flow_table *flow_table)
 {
     /* Ingress flows. */
     ofpbuf_clear(ofpacts);
@@ -3191,13 +3340,30 @@ physical_consider_evpn_binding(const struct 
evpn_binding *binding,
 
     match_set_in_port(match, binding->tunnel_ofport);
     match_set_tun_id(match, htonll(binding->vni));
-    if (ipv4) {
+
+    const struct in6_addr *local_ip6 = NULL;
+    ovs_be32 local_ip4 = 0;
+
+    if (IN6_IS_ADDR_V4MAPPED(&binding->remote_ip)) {
+        local_ip4 = evpn_local_ip_find_v4(vni_ip_map, binding->vni);
+    } else {
+        local_ip6 = evpn_local_ip_find_v6(vni_ip_map, binding->vni);
+    }
+
+    if (!local_ip4 && !local_ip6) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+        VLOG_WARN_RL(&rl, "failed to get local tunnel ip for VNI %d",
+                     binding->vni);
+        return;
+    }
+
+    if (local_ip4) {
         match_set_tun_src(match,
                           in6_addr_get_mapped_ipv4(&binding->remote_ip));
-        match_set_tun_dst(match, in6_addr_get_mapped_ipv4(local_ip));
+        match_set_tun_dst(match, local_ip4);
     } else {
         match_set_tun_ipv6_src(match, &binding->remote_ip);
-        match_set_tun_ipv6_dst(match, local_ip);
+        match_set_tun_ipv6_dst(match, local_ip6);
     }
 
     put_load(binding->dp_key, MFF_LOG_DATAPATH, 0, 32, ofpacts);
@@ -3216,13 +3382,13 @@ physical_consider_evpn_binding(const struct 
evpn_binding *binding,
     match_outport_dp_and_port_keys(match, binding->dp_key,
                                    binding->binding_key);
 
-    if (ipv4) {
-        ovs_be32 ip4 = in6_addr_get_mapped_ipv4(local_ip);
-        put_load_bytes(&ip4, sizeof ip4, MFF_TUN_SRC, 0, 32, ofpacts);
-        ip4 = in6_addr_get_mapped_ipv4(&binding->remote_ip);
+    if (local_ip4) {
+        put_load_bytes(&local_ip4, sizeof local_ip4, MFF_TUN_SRC, 0, 32,
+                       ofpacts);
+        ovs_be32 ip4 = in6_addr_get_mapped_ipv4(&binding->remote_ip);
         put_load_bytes(&ip4, sizeof ip4, MFF_TUN_DST, 0, 32, ofpacts);
     } else {
-        put_load_bytes(local_ip, sizeof *local_ip, MFF_TUN_IPV6_SRC,
+        put_load_bytes(local_ip6, sizeof *local_ip6, MFF_TUN_IPV6_SRC,
                        0, 128, ofpacts);
         put_load_bytes(&binding->remote_ip, sizeof binding->remote_ip,
                        MFF_TUN_IPV6_DST, 0, 128, ofpacts);
@@ -3308,49 +3474,82 @@ physical_consider_evpn_binding(const struct 
evpn_binding *binding,
 
 static void
 physical_consider_evpn_multicast(const struct evpn_multicast_group *mc_group,
-                                 const struct in6_addr *local_ip,
+                                 const struct evpn_local_ip_map *vni_ip_map,
                                  struct ofpbuf *ofpacts, struct match *match,
-                                 struct ovn_desired_flow_table *flow_table,
-                                 bool ipv4)
+                                 struct ovn_desired_flow_table *flow_table)
 {
-    const struct evpn_binding *binding = NULL;
+    ovs_be32 local_ip4 = evpn_local_ip_find_v4(vni_ip_map, mc_group->vni);
+    const struct in6_addr *local_ip6 = evpn_local_ip_find_v6(vni_ip_map,
+                                                             mc_group->vni);
 
     ofpbuf_clear(ofpacts);
     uint32_t multicast_tunnel_keys[] = {OVN_MCAST_FLOOD_TUNNEL_KEY,
                                         OVN_MCAST_UNKNOWN_TUNNEL_KEY,
                                         OVN_MCAST_FLOOD_L2_TUNNEL_KEY};
-    if (ipv4) {
-        ovs_be32 ip4 = in6_addr_get_mapped_ipv4(local_ip);
-        put_load_bytes(&ip4, sizeof ip4, MFF_TUN_SRC, 0, 32, ofpacts);
-    } else {
-        put_load_bytes(local_ip, sizeof *local_ip, MFF_TUN_IPV6_SRC,
-                       0, 128, ofpacts);
-    }
     put_load(mc_group->vni, MFF_TUN_ID, 0, 24, ofpacts);
 
-    const struct hmapx_node *node;
-    HMAPX_FOR_EACH (node, &mc_group->bindings) {
-        binding = node->data;
-        if (ipv4) {
-            ovs_be32 ip4 = in6_addr_get_mapped_ipv4(&binding->remote_ip);
-            put_load_bytes(&ip4, sizeof ip4, MFF_TUN_DST, 0, 32, ofpacts);
-        } else {
+    const struct evpn_binding *binding = NULL;
+    if (local_ip4) {
+        put_load_bytes(&local_ip4, sizeof local_ip4, MFF_TUN_SRC,
+                       0, 32, ofpacts);
+
+        const struct hmapx_node *node;
+        HMAPX_FOR_EACH (node, &mc_group->bindings) {
+            binding = node->data;
+            if (!IN6_IS_ADDR_V4MAPPED(&binding->remote_ip)) {
+                continue;
+            }
+
+            ovs_be32 remote_ip4 =
+                in6_addr_get_mapped_ipv4(&binding->remote_ip);
+            put_load_bytes(&remote_ip4, sizeof remote_ip4, MFF_TUN_DST, 0, 32,
+                           ofpacts);
+            ofpact_put_OUTPUT(ofpacts)->port = binding->tunnel_ofport;
+        }
+        put_resubmit(OFTABLE_LOCAL_OUTPUT, ofpacts);
+    }
+
+    /* We first walk all the v4 remote IPs and generate tun encap actions for
+     * them. Then we will iterate over all the v6 remotes and generate tun
+     * encap actions for them.
+     * We need to set tun_v4 OVS fields to zero here in order to avoid having
+     * inconsistent OVS flow actions. */
+    if (local_ip4 && local_ip6) {
+        local_ip4 = 0;
+        put_load_bytes(&local_ip4, sizeof local_ip4, MFF_TUN_SRC, 0, 32,
+                       ofpacts);
+        put_load_bytes(&local_ip4, sizeof local_ip4, MFF_TUN_DST, 0, 32,
+                       ofpacts);
+    }
+
+    if (local_ip6) {
+        put_load_bytes(local_ip6, sizeof *local_ip6, MFF_TUN_IPV6_SRC,
+                       0, 128, ofpacts);
+        const struct hmapx_node *node;
+        HMAPX_FOR_EACH (node, &mc_group->bindings) {
+            binding = node->data;
+            if (IN6_IS_ADDR_V4MAPPED(&binding->remote_ip)) {
+                continue;
+            }
+
             put_load_bytes(&binding->remote_ip, sizeof binding->remote_ip,
                            MFF_TUN_IPV6_DST, 0, 128, ofpacts);
+            ofpact_put_OUTPUT(ofpacts)->port = binding->tunnel_ofport;
         }
-        ofpact_put_OUTPUT(ofpacts)->port = binding->tunnel_ofport;
+        put_resubmit(OFTABLE_LOCAL_OUTPUT, ofpacts);
     }
-    put_resubmit(OFTABLE_LOCAL_OUTPUT, ofpacts);
 
-    ovs_assert(!hmapx_is_empty(&mc_group->bindings));
-    for (size_t i = 0; i < ARRAY_SIZE(multicast_tunnel_keys); i++) {
-        match_init_catchall(match);
-        match_outport_dp_and_port_keys(match, binding->dp_key,
-                                       multicast_tunnel_keys[i]);
+    if (binding) {
+        ovs_assert(!hmapx_is_empty(&mc_group->bindings));
+        for (size_t i = 0; i < ARRAY_SIZE(multicast_tunnel_keys); i++) {
+            match_init_catchall(match);
+            match_outport_dp_and_port_keys(match, binding->dp_key,
+                                           multicast_tunnel_keys[i]);
 
-        ofctrl_add_flow(flow_table, OFTABLE_REMOTE_VTEP_OUTPUT, 50,
-                        mc_group->flow_uuid.parts[0],
-                        match, ofpacts, &mc_group->flow_uuid);
+            ofctrl_add_flow(flow_table, OFTABLE_REMOTE_VTEP_OUTPUT, 50,
+                            mc_group->flow_uuid.parts[0],
+                            match, ofpacts, &mc_group->flow_uuid);
+        }
     }
 }
 
@@ -3418,30 +3617,26 @@ physical_eval_evpn_flows(const struct physical_ctx *ctx,
         return;
     }
 
-    const char *local_ip_str = smap_get_def(&ctx->chassis->other_config,
-                                            "ovn-evpn-local-ip", "");
-    struct in6_addr local_ip;
-    if (!ip46_parse(local_ip_str, &local_ip)) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-        VLOG_WARN_RL(&rl, "EVPN enabled, but required 'evpn-local-ip' is "
-                          "missing or invalid %s ", local_ip_str);
-        return;
-    }
+    struct evpn_local_ip_map vni_ip_map = {
+        .vni_ip_v4 = HMAP_INITIALIZER(&vni_ip_map.vni_ip_v4),
+        .vni_ip_v6 = HMAP_INITIALIZER(&vni_ip_map.vni_ip_v6),
+    };
+    evpn_local_ip_map_init(&vni_ip_map, &ctx->chassis->other_config);
 
     struct match match = MATCH_CATCHALL_INITIALIZER;
-    bool ipv4 = IN6_IS_ADDR_V4MAPPED(&local_ip);
-
     const struct evpn_binding *binding;
+
     HMAP_FOR_EACH (binding, hmap_node, ctx->evpn_bindings) {
-        physical_consider_evpn_binding(binding, &local_ip, ofpacts,
-                                       &match, flow_table, ipv4);
+        physical_consider_evpn_binding(binding, &vni_ip_map, ofpacts,
+                                       &match, flow_table);
     }
 
     const struct evpn_multicast_group *mc_group;
     HMAP_FOR_EACH (mc_group, hmap_node, ctx->evpn_multicast_groups) {
-        physical_consider_evpn_multicast(mc_group, &local_ip, ofpacts,
-                                         &match, flow_table, ipv4);
+        physical_consider_evpn_multicast(mc_group, &vni_ip_map, ofpacts,
+                                         &match, flow_table);
     }
+    evpn_local_ip_map_destroy(&vni_ip_map);
 
     const struct evpn_fdb *fdb;
     HMAP_FOR_EACH (fdb, hmap_node, ctx->evpn_fdbs) {
@@ -3569,34 +3764,33 @@ physical_handle_evpn_binding_changes(
     const struct uuidset *removed_bindings,
     const struct uuidset *removed_multicast_groups)
 {
-    const char *local_ip_str = smap_get_def(&ctx->chassis->other_config,
-                                            "ovn-evpn-local-ip", "");
-    struct in6_addr local_ip;
-    if (!ip46_parse(local_ip_str, &local_ip)) {
-        return;
-    }
+    struct evpn_local_ip_map vni_ip_map = {
+        .vni_ip_v4 = HMAP_INITIALIZER(&vni_ip_map.vni_ip_v4),
+        .vni_ip_v6 = HMAP_INITIALIZER(&vni_ip_map.vni_ip_v6),
+    };
+    evpn_local_ip_map_init(&vni_ip_map, &ctx->chassis->other_config);
 
     struct ofpbuf ofpacts;
     ofpbuf_init(&ofpacts, 0);
     struct match match = MATCH_CATCHALL_INITIALIZER;
-    bool ipv4 = IN6_IS_ADDR_V4MAPPED(&local_ip);
 
     const struct hmapx_node *node;
     HMAPX_FOR_EACH (node, updated_bindings) {
         const struct evpn_binding *binding = node->data;
 
         ofctrl_remove_flows(flow_table, &binding->flow_uuid);
-        physical_consider_evpn_binding(binding, &local_ip, &ofpacts,
-                                       &match, flow_table, ipv4);
+        physical_consider_evpn_binding(binding, &vni_ip_map, &ofpacts,
+                                       &match, flow_table);
     }
 
     HMAPX_FOR_EACH (node, updated_multicast_groups) {
         const struct evpn_multicast_group *mc_group = node->data;
 
         ofctrl_remove_flows(flow_table, &mc_group->flow_uuid);
-        physical_consider_evpn_multicast(mc_group, &local_ip, &ofpacts,
-                                         &match, flow_table, ipv4);
+        physical_consider_evpn_multicast(mc_group, &vni_ip_map, &ofpacts,
+                                         &match, flow_table);
     }
+    evpn_local_ip_map_destroy(&vni_ip_map);
 
     ofpbuf_uninit(&ofpacts);
 
diff --git a/tests/system-common-macros.at b/tests/system-common-macros.at
index 9c5a124a0..0f4f8952c 100644
--- a/tests/system-common-macros.at
+++ b/tests/system-common-macros.at
@@ -209,18 +209,20 @@ m4_define([SET_EVPN_IFACE_NAMES],
         ifname=$1 switch=$2 vni=$3
 
         [[ $ifname = "default" ]] && BR_NAME=br-$vni || BR_NAME=br-$ifname
-        [[ $ifname = "default" ]] && VXLAN_NAME=vxlan-$vni || 
VXLAN_NAME=vxlan-$ifname
+        [[ $ifname = "default" ]] && VXLAN_NAME=vxlan-$vni || 
VXLAN_NAME=vxlan-v4-$ifname
+        [[ $ifname = "default" ]] && VXLAN_V6_NAME=vxlan-v6-$vni || 
VXLAN_V6_NAME=vxlan-v6-$ifname
         [[ $ifname = "default" ]] && LO_NAME=lo-$vni || LO_NAME=lo-$ifname
 
         if [[ $ifname != "default" ]]; then
             check ovn-nbctl set logical_switch $switch                 \
                 other_config:dynamic-routing-bridge-ifname=$BR_NAME    \
-                other_config:dynamic-routing-vxlan-ifname=$VXLAN_NAME  \
                 other_config:dynamic-routing-advertise-ifname=$LO_NAME
         fi
+        check ovn-nbctl set logical_switch $switch  \
+            
other_config:dynamic-routing-vxlan-ifname=$VXLAN_NAME","$VXLAN_V6_NAME
 
-        export BR_NAME VXLAN_NAME LO_NAME
-        on_exit 'unset BR_NAME VXLAN_NAME LO_NAME'
+        export BR_NAME VXLAN_NAME VXLAN_V6_NAME LO_NAME
+        on_exit 'unset BR_NAME VXLAN_NAME VXLAN_V6_NAME LO_NAME'
     ]
 )
 
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index ec3b3735f..584f91894 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -17731,10 +17731,10 @@ 
priority=1050,tun_id=0xa,tun_src=169.0.0.10,tun_dst=169.0.0.1,in_port=$ofport ac
 
 AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int 
table=OFTABLE_REMOTE_VTEP_OUTPUT | grep output | \
                    awk '{print $[7], $[8]}' | sort], [0], [dnl
-priority=50,reg15=0x8000,metadata=0x$dp_key 
actions=load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
+priority=50,reg15=0x8000,metadata=0x$dp_key 
actions=load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
 priority=50,reg15=0x80000001,metadata=0x$dp_key 
actions=load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],load:0xa->NXM_NX_TUN_ID[[0..23]],output:$ofport
-priority=50,reg15=0x8001,metadata=0x$dp_key 
actions=load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
-priority=50,reg15=0x8004,metadata=0x$dp_key 
actions=load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
+priority=50,reg15=0x8001,metadata=0x$dp_key 
actions=load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
+priority=50,reg15=0x8004,metadata=0x$dp_key 
actions=load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
 priority=55,reg10=0x1/0x1,reg15=0x80000001,metadata=0x$dp_key 
actions=load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xffff->NXM_OF_IN_PORT[[]],output:$ofport
 ])
 
@@ -18170,7 +18170,7 @@ check ovs-vsctl \
     -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
     -- set Open_vSwitch . external-ids:ovn-encap-ip=169::1 \
     -- set Open_vSwitch . external-ids:ovn-bridge-mappings=phynet:br-ext \
-    -- set Open_vSwitch . external-ids:ovn-evpn-local-ip=169::1 \
+    -- set Open_vSwitch . external-ids:ovn-evpn-local-ip=169.0.0.1,169::1 \
     -- set Open_vSwitch . external-ids:ovn-evpn-vxlan-ports=4789 \
     -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
 
@@ -18268,10 +18268,10 @@ 
priority=1050,tun_id=0xa,tun_ipv6_src=169::10,tun_ipv6_dst=169::1,in_port=$ofpor
 
 AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int 
table=OFTABLE_REMOTE_VTEP_OUTPUT | grep output | \
                    awk '{print $[7], $[8]}' | sort], [0], [dnl
-priority=50,reg15=0x8000,metadata=0x$dp_key 
actions=load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
+priority=50,reg15=0x8000,metadata=0x$dp_key 
actions=load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],resubmit(,OFTABLE_LOCAL_OUTPUT),load:0->NXM_NX_TUN_IPV4_SRC[[]],load:0->NXM_NX_TUN_IPV4_DST[[]],load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
 priority=50,reg15=0x80000001,metadata=0x$dp_key 
actions=load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],load:0xa->NXM_NX_TUN_ID[[0..23]],output:$ofport
-priority=50,reg15=0x8001,metadata=0x$dp_key 
actions=load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
-priority=50,reg15=0x8004,metadata=0x$dp_key 
actions=load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
+priority=50,reg15=0x8001,metadata=0x$dp_key 
actions=load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],resubmit(,OFTABLE_LOCAL_OUTPUT),load:0->NXM_NX_TUN_IPV4_SRC[[]],load:0->NXM_NX_TUN_IPV4_DST[[]],load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
+priority=50,reg15=0x8004,metadata=0x$dp_key 
actions=load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],resubmit(,OFTABLE_LOCAL_OUTPUT),load:0->NXM_NX_TUN_IPV4_SRC[[]],load:0->NXM_NX_TUN_IPV4_DST[[]],load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
 priority=55,reg10=0x1/0x1,reg15=0x80000001,metadata=0x$dp_key 
actions=load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xffff->NXM_OF_IN_PORT[[]],output:$ofport
 ])
 
@@ -18684,6 +18684,589 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port 
patch-.*/d
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([dynamic-routing - EVPN $1 naming - Dual Stack])
+AT_KEYWORDS([dynamic-routing])
+
+CHECK_VRF()
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_NAT()
+
+IFNAME=$1
+vni=10
+VRF_RESERVE([$vni])
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+ADD_BR([br-ext], [set Bridge br-ext fail-mode=standalone])
+
+# Set external-ids in br-int needed for ovn-controller.
+check ovs-vsctl \
+    -- set Open_vSwitch . external-ids:system-id=hv1 \
+    -- set Open_vSwitch . 
external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+    -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+    -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+    -- set Open_vSwitch . external-ids:ovn-bridge-mappings=phynet:br-ext \
+    -- set Open_vSwitch . 
external-ids:ovn-evpn-local-ip=$vni-169.0.0.1,$vni-169::1 \
+    -- set Open_vSwitch . external-ids:ovn-evpn-vxlan-ports=4789 \
+    -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+# Start ovn-controller.
+start_daemon ovn-controller
+
+OVS_WAIT_WHILE([ip link | grep -q ovnvrf$vni:.*UP])
+
+check ovn-nbctl                                                               \
+    -- ls-add ls-evpn                                                         \
+    -- lsp-add ls-evpn workload1                                              \
+    -- lsp-set-addresses workload1 "f0:00:0f:16:01:10 172.16.1.10 172:16::10" \
+    -- lsp-add ls-evpn workload2                                              \
+    -- lsp-set-addresses workload2 "f0:00:0f:16:01:20 172.16.1.20 172:16::20" \
+    -- lsp-add-localnet-port ls-evpn ln_port phynet
+
+SET_EVPN_IFACE_NAMES([$IFNAME], [ls-evpn], [$vni])
+
+ADD_NAMESPACES(workload1)
+ADD_VETH(workload1, workload1, br-int, "172:16::10/64", "f0:00:0f:16:01:10", \
+         "172:16::1", "nodad", "172.16.1.10/24", "172.16.1.1")
+
+ADD_NAMESPACES(workload2)
+ADD_VETH(workload2, workload2, br-int, "172:16::20/64", "f0:00:0f:16:01:20", \
+         "172:16::1", "nodad", "172.16.1.20/24", "172.16.1.1")
+
+OVN_POPULATE_ARP
+check ovn-nbctl --wait=hv sync
+wait_for_ports_up
+
+# Setup a VRF for the VNI.
+check ip link add vrf-$vni type vrf table $vni
+on_exit "ip link del vrf-$vni"
+check ip link set vrf-$vni up
+
+# Add VNI bridge.
+check ip link add $BR_NAME type bridge
+on_exit "ip link del $BR_NAME"
+check ip link set $BR_NAME master vrf-$vni addrgenmode none
+check ip link set dev $BR_NAME up
+
+# Add VXLAN VTEP for the VNI (linked to the OVS vxlan_sys_<port> interface).
+# Use a dstport different than the one used by OVS.
+# This is fine because we don't actually want traffic to pass through
+# the $vxlan interface.  FRR should read the dstport from the linked
+# vxlan_sys_${vxlan_port} device.
+dstport=$((60000 + $vni))
+check ip link add $VXLAN_NAME type vxlan \
+    id $vni dstport $dstport local 169.0.0.1 nolearning
+check ip link add $VXLAN_V6_NAME type vxlan \
+    id $vni dstport $dstport local 169::1 nolearning
+on_exit "ip link del $VXLAN_NAME"
+on_exit "ip link del $VXLAN_V6_NAME"
+check ip link set dev $VXLAN_NAME up
+check ip link set dev $VXLAN_V6_NAME up
+check ip link set $VXLAN_NAME master $BR_NAME
+check ip link set $VXLAN_V6_NAME master $BR_NAME
+
+# Add a dummy loopback to the VNI bridge to be used for advertising local
+# MACs.
+check ip link add name $LO_NAME type dummy
+on_exit "ip link del $LO_NAME"
+check ip link set $LO_NAME master $BR_NAME
+check ip link set $LO_NAME up
+
+AS_BOX([L2 EVPN VTEP and FDB learning])
+
+check ovn-nbctl --wait=hv set logical_switch ls-evpn 
other_config:dynamic-routing-vni=$vni
+ofport=$(ovs-vsctl --bare --columns ofport find Interface name="ovn-evpn-4789")
+dp_key=$(fetch_column Datapath tunnel_key external_ids:name=ls-evpn)
+
+AT_CHECK([ovn-appctl evpn/remote-vtep-list], [0], [dnl
+])
+
+AT_CHECK([ovn-appctl evpn/vtep-binding-list], [0], [dnl
+])
+
+AT_CHECK([ovn-appctl evpn/vtep-multicast-group-list], [0], [dnl
+])
+
+# Simulate remote VTEP.
+check bridge fdb append 00:00:00:00:00:00 dev $VXLAN_NAME dst 169.0.0.10 
static permanent
+check bridge fdb append 00:00:00:00:00:00 dev $VXLAN_V6_NAME dst 169::10 
static permanent
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovn-appctl evpn/remote-vtep-list], [0], [dnl
+IP: 169::10, port: 4789, vni: $vni
+IP: 169.0.0.10, port: 4789, vni: $vni
+])
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovn-appctl evpn/vtep-binding-list | cut -d',' 
-f2-], [0], [dnl
+ Remote IP: 169::10, vni: $vni, binding_key: 0x80000002, tunnel_ofport: 
$ofport, dp_key: $dp_key
+ Remote IP: 169.0.0.10, vni: $vni, binding_key: 0x80000001, tunnel_ofport: 
$ofport, dp_key: $dp_key
+])
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovn-appctl evpn/vtep-multicast-group-list | grep 
-q 169.0.0.10])
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovn-appctl evpn/vtep-multicast-group-list | grep 
-q 169::10])
+AT_CHECK([ovn-appctl evpn/vtep-multicast-group-list | wc -l], [0], [1
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_PHY_TO_LOG | grep 
priority=1050 | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=1050,tun_id=0xa,tun_ipv6_src=169::10,tun_ipv6_dst=169::1,in_port=$ofport
 
actions=load:0x$dp_key->OXM_OF_METADATA[[0..31]],load:0x80000002->NXM_NX_REG14[[]],resubmit(,OFTABLE_LEARN_REMOTE_FDB),resubmit(,OFTABLE_LOG_INGRESS_PIPELINE)
+priority=1050,tun_id=0xa,tun_src=169.0.0.10,tun_dst=169.0.0.1,in_port=$ofport 
actions=load:0x$dp_key->OXM_OF_METADATA[[0..31]],load:0x80000001->NXM_NX_REG14[[]],resubmit(,OFTABLE_LEARN_REMOTE_FDB),resubmit(,OFTABLE_LOG_INGRESS_PIPELINE)
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int 
table=OFTABLE_REMOTE_VTEP_OUTPUT | grep output | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=50,reg15=0x8000,metadata=0x$dp_key 
actions=load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT),load:0->NXM_NX_TUN_IPV4_SRC[[]],load:0->NXM_NX_TUN_IPV4_DST[[]],load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
+priority=50,reg15=0x80000001,metadata=0x$dp_key 
actions=load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],load:0xa->NXM_NX_TUN_ID[[0..23]],output:$ofport
+priority=50,reg15=0x80000002,metadata=0x$dp_key 
actions=load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],load:0xa->NXM_NX_TUN_ID[[0..23]],output:$ofport
+priority=50,reg15=0x8001,metadata=0x$dp_key 
actions=load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT),load:0->NXM_NX_TUN_IPV4_SRC[[]],load:0->NXM_NX_TUN_IPV4_DST[[]],load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
+priority=50,reg15=0x8004,metadata=0x$dp_key 
actions=load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT),load:0->NXM_NX_TUN_IPV4_SRC[[]],load:0->NXM_NX_TUN_IPV4_DST[[]],load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],output:$ofport,resubmit(,OFTABLE_LOCAL_OUTPUT)
+priority=55,reg10=0x1/0x1,reg15=0x80000001,metadata=0x$dp_key 
actions=load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xffff->NXM_OF_IN_PORT[[]],output:$ofport
+priority=55,reg10=0x1/0x1,reg15=0x80000002,metadata=0x$dp_key 
actions=load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xffff->NXM_OF_IN_PORT[[]],output:$ofport
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_LEARN_REMOTE_FDB 
| grep priority | \
+                   awk '{print $[7], $[8]}' | strip_cookie | sort], [0], [dnl
+priority=100,reg14=0x80000001,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000002,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+])
+
+# Simulate more remote VTEPs.
+check bridge fdb append 00:00:00:00:00:00 dev $VXLAN_NAME dst 169.0.0.20 
static permanent
+check bridge fdb append 00:00:00:00:00:01 dev $VXLAN_NAME dst 169.0.0.30 
extern_learn
+check bridge fdb append 00:00:00:00:00:10 dev $VXLAN_V6_NAME dst 169::30 
extern_learn
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovn-appctl evpn/remote-vtep-list | sort], [0], 
[dnl
+IP: 169.0.0.10, port: 4789, vni: $vni
+IP: 169.0.0.20, port: 4789, vni: $vni
+IP: 169.0.0.30, port: 4789, vni: $vni
+IP: 169::10, port: 4789, vni: 10
+IP: 169::30, port: 4789, vni: 10
+])
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovn-appctl evpn/vtep-binding-list | cut -d',' 
-f2- | sort], [0], [dnl
+ Remote IP: 169.0.0.10, vni: $vni, binding_key: 0x80000001, tunnel_ofport: 
$ofport, dp_key: $dp_key
+ Remote IP: 169.0.0.20, vni: $vni, binding_key: 0x80000003, tunnel_ofport: 
$ofport, dp_key: $dp_key
+ Remote IP: 169.0.0.30, vni: $vni, binding_key: 0x80000004, tunnel_ofport: 
$ofport, dp_key: $dp_key
+ Remote IP: 169::10, vni: $vni, binding_key: 0x80000002, tunnel_ofport: 
$ofport, dp_key: $dp_key
+ Remote IP: 169::30, vni: $vni, binding_key: 0x80000005, tunnel_ofport: 
$ofport, dp_key: $dp_key
+])
+
+# We cannot check the output directly because the order might change.
+OVS_WAIT_UNTIL([ovn-appctl evpn/vtep-multicast-group-list | grep -q 
"169.0.0.10"])
+OVS_WAIT_UNTIL([ovn-appctl evpn/vtep-multicast-group-list | grep -q 
"169.0.0.20"])
+OVS_WAIT_UNTIL([ovn-appctl evpn/vtep-multicast-group-list | grep -q 
"169.0.0.30"])
+OVS_WAIT_UNTIL([ovn-appctl evpn/vtep-multicast-group-list | grep -q "169::10"])
+OVS_WAIT_UNTIL([ovn-appctl evpn/vtep-multicast-group-list | grep -q "169::30"])
+AT_CHECK([ovn-appctl evpn/vtep-multicast-group-list | wc -l], [0], [1
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_PHY_TO_LOG | grep 
priority=1050 | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=1050,tun_id=0xa,tun_ipv6_src=169::10,tun_ipv6_dst=169::1,in_port=$ofport
 
actions=load:0x1->OXM_OF_METADATA[[0..31]],load:0x80000002->NXM_NX_REG14[[]],resubmit(,OFTABLE_LEARN_REMOTE_FDB),resubmit(,OFTABLE_LOG_INGRESS_PIPELINE)
+priority=1050,tun_id=0xa,tun_ipv6_src=169::30,tun_ipv6_dst=169::1,in_port=$ofport
 
actions=load:0x1->OXM_OF_METADATA[[0..31]],load:0x80000005->NXM_NX_REG14[[]],resubmit(,OFTABLE_LEARN_REMOTE_FDB),resubmit(,OFTABLE_LOG_INGRESS_PIPELINE)
+priority=1050,tun_id=0xa,tun_src=169.0.0.10,tun_dst=169.0.0.1,in_port=$ofport 
actions=load:0x$dp_key->OXM_OF_METADATA[[0..31]],load:0x80000001->NXM_NX_REG14[[]],resubmit(,OFTABLE_LEARN_REMOTE_FDB),resubmit(,OFTABLE_LOG_INGRESS_PIPELINE)
+priority=1050,tun_id=0xa,tun_src=169.0.0.20,tun_dst=169.0.0.1,in_port=$ofport 
actions=load:0x$dp_key->OXM_OF_METADATA[[0..31]],load:0x80000003->NXM_NX_REG14[[]],resubmit(,OFTABLE_LEARN_REMOTE_FDB),resubmit(,OFTABLE_LOG_INGRESS_PIPELINE)
+priority=1050,tun_id=0xa,tun_src=169.0.0.30,tun_dst=169.0.0.1,in_port=$ofport 
actions=load:0x$dp_key->OXM_OF_METADATA[[0..31]],load:0x80000004->NXM_NX_REG14[[]],resubmit(,OFTABLE_LEARN_REMOTE_FDB),resubmit(,OFTABLE_LOG_INGRESS_PIPELINE)
+])
+
+ovs-ofctl dump-flows br-int table=OFTABLE_REMOTE_VTEP_OUTPUT > 
oftable_remote_vtep_output
+AT_CHECK_UNQUOTED([grep "output" oftable_remote_vtep_output | grep -v resubmit 
| \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=50,reg15=0x80000001,metadata=0x$dp_key 
actions=load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],load:0xa->NXM_NX_TUN_ID[[0..23]],output:$ofport
+priority=50,reg15=0x80000002,metadata=0x$dp_key 
actions=load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],load:0xa->NXM_NX_TUN_ID[[0..23]],output:$ofport
+priority=50,reg15=0x80000003,metadata=0x$dp_key 
actions=load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa9000014->NXM_NX_TUN_IPV4_DST[[]],load:0xa->NXM_NX_TUN_ID[[0..23]],output:$ofport
+priority=50,reg15=0x80000004,metadata=0x$dp_key 
actions=load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900001e->NXM_NX_TUN_IPV4_DST[[]],load:0xa->NXM_NX_TUN_ID[[0..23]],output:$ofport
+priority=50,reg15=0x80000005,metadata=0x$dp_key 
actions=load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x30->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],load:0xa->NXM_NX_TUN_ID[[0..23]],output:$ofport
+priority=55,reg10=0x1/0x1,reg15=0x80000001,metadata=0x$dp_key 
actions=load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900000a->NXM_NX_TUN_IPV4_DST[[]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xffff->NXM_OF_IN_PORT[[]],output:$ofport
+priority=55,reg10=0x1/0x1,reg15=0x80000002,metadata=0x$dp_key 
actions=load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x10->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xffff->NXM_OF_IN_PORT[[]],output:$ofport
+priority=55,reg10=0x1/0x1,reg15=0x80000003,metadata=0x$dp_key 
actions=load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa9000014->NXM_NX_TUN_IPV4_DST[[]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xffff->NXM_OF_IN_PORT[[]],output:$ofport
+priority=55,reg10=0x1/0x1,reg15=0x80000004,metadata=0x$dp_key 
actions=load:0xa9000001->NXM_NX_TUN_IPV4_SRC[[]],load:0xa900001e->NXM_NX_TUN_IPV4_DST[[]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xffff->NXM_OF_IN_PORT[[]],output:$ofport
+priority=55,reg10=0x1/0x1,reg15=0x80000005,metadata=0x1 
actions=load:0x1->NXM_NX_TUN_IPV6_SRC[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_SRC[[64..127]],load:0x30->NXM_NX_TUN_IPV6_DST[[0..63]],load:0x169000000000000->NXM_NX_TUN_IPV6_DST[[64..127]],load:0xa->NXM_NX_TUN_ID[[0..23]],load:0xffff->NXM_OF_IN_PORT[[]],output:$ofport
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_LEARN_REMOTE_FDB 
| grep priority | \
+                   awk '{print $[7], $[8]}' | strip_cookie | sort], [0], [dnl
+priority=100,reg14=0x80000001,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000002,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000003,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000004,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000005,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=150,reg14=0x80000004,metadata=0x$dp_key,dl_src=00:00:00:00:00:01 
actions=drop
+priority=150,reg14=0x80000005,metadata=0x$dp_key,dl_src=00:00:00:00:00:10 
actions=drop
+])
+
+AT_CHECK([grep "resubmit" oftable_remote_vtep_output | grep -c 
"load:0xa900000a"], [0], [3
+])
+AT_CHECK([grep "resubmit" oftable_remote_vtep_output | grep -c 
"load:0xa9000014"], [0], [3
+])
+AT_CHECK([grep "resubmit" oftable_remote_vtep_output | grep -c 
"load:0xa900001e"], [0], [3
+])
+
+# Simulate remote workload.
+check bridge fdb add f0:00:0f:16:10:50 dev $VXLAN_NAME dst 169.0.0.10 static 
extern_learn
+check bridge fdb add f0:00:0f:16:10:60 dev $VXLAN_NAME dst 169.0.0.20 static 
extern_learn
+check bridge fdb add f0:00:0f:16:10:70 dev $VXLAN_NAME dst 169.0.0.30 static 
extern_learn
+check bridge fdb add f0:00:0f:16:10:80 dev $VXLAN_V6_NAME dst 169::30 static 
extern_learn
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovn-appctl evpn/vtep-fdb-list | cut -d',' -f2- | 
sort], [0], [dnl
+ MAC: 00:00:00:00:00:01, vni: $vni, binding_key: 0x80000004, dp_key: $dp_key
+ MAC: 00:00:00:00:00:10, vni: $vni, binding_key: 0x80000005, dp_key: $dp_key
+ MAC: f0:00:0f:16:10:50, vni: $vni, binding_key: 0x80000001, dp_key: $dp_key
+ MAC: f0:00:0f:16:10:60, vni: $vni, binding_key: 0x80000003, dp_key: $dp_key
+ MAC: f0:00:0f:16:10:70, vni: $vni, binding_key: 0x80000004, dp_key: $dp_key
+ MAC: f0:00:0f:16:10:80, vni: $vni, binding_key: 0x80000005, dp_key: $dp_key
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_GET_REMOTE_FDB | 
grep priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=0 actions=load:0->NXM_NX_REG1[[]]
+priority=150,metadata=0x$dp_key,dl_dst=00:00:00:00:00:01 
actions=load:0x80000004->NXM_NX_REG1[[]]
+priority=150,metadata=0x$dp_key,dl_dst=00:00:00:00:00:10 
actions=load:0x80000005->NXM_NX_REG1[[]]
+priority=150,metadata=0x$dp_key,dl_dst=f0:00:0f:16:10:50 
actions=load:0x80000001->NXM_NX_REG1[[]]
+priority=150,metadata=0x$dp_key,dl_dst=f0:00:0f:16:10:60 
actions=load:0x80000003->NXM_NX_REG1[[]]
+priority=150,metadata=0x$dp_key,dl_dst=f0:00:0f:16:10:70 
actions=load:0x80000004->NXM_NX_REG1[[]]
+priority=150,metadata=0x$dp_key,dl_dst=f0:00:0f:16:10:80 
actions=load:0x80000005->NXM_NX_REG1[[]]
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_LEARN_REMOTE_FDB 
| grep priority | \
+                   awk '{print $[7], $[8]}' | strip_cookie | sort], [0], [dnl
+priority=100,reg14=0x80000001,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000002,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000003,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000004,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000005,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=150,reg14=0x80000001,metadata=0x$dp_key,dl_src=f0:00:0f:16:10:50 
actions=drop
+priority=150,reg14=0x80000003,metadata=0x$dp_key,dl_src=f0:00:0f:16:10:60 
actions=drop
+priority=150,reg14=0x80000004,metadata=0x$dp_key,dl_src=00:00:00:00:00:01 
actions=drop
+priority=150,reg14=0x80000004,metadata=0x$dp_key,dl_src=f0:00:0f:16:10:70 
actions=drop
+priority=150,reg14=0x80000005,metadata=0x$dp_key,dl_src=00:00:00:00:00:10 
actions=drop
+priority=150,reg14=0x80000005,metadata=0x$dp_key,dl_src=f0:00:0f:16:10:80 
actions=drop
+])
+
+# Check that the recompute won't change the UUIDs and tunnel keys.
+ovn-appctl evpn/vtep-binding-list > bindings_before
+ovn-appctl evpn/vtep-multicast-group-list > mc_groups_before
+ovn-appctl evpn/vtep-fdb-list > fdb_before
+
+check ovn-appctl inc-engine/recompute
+check ovn-nbctl --wait=hv sync
+
+ovn-appctl evpn/vtep-binding-list > bindings_after
+ovn-appctl evpn/vtep-multicast-group-list > mc_groups_after
+ovn-appctl evpn/vtep-fdb-list > fdb_after
+
+check diff -q bindings_before bindings_after
+check diff -q mc_groups_before mc_groups_after
+check diff -q fdb_before fdb_after
+
+AS_BOX([L2 EVPN FDB advertising])
+
+check ovn-nbctl --wait=hv set logical_switch ls-evpn 
other_config:dynamic-routing-redistribute=fdb
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([bridge fdb show | grep $LO_NAME | grep 
"f0:00:0f:16:01" | sort], [0], [dnl
+f0:00:0f:16:01:10 dev $LO_NAME master $BR_NAME static
+f0:00:0f:16:01:10 dev $LO_NAME vlan 1 master $BR_NAME static
+f0:00:0f:16:01:20 dev $LO_NAME master $BR_NAME static
+f0:00:0f:16:01:20 dev $LO_NAME vlan 1 master $BR_NAME static
+])
+
+check ovn-nbctl --wait=hv lsp-del workload2
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([bridge fdb show | grep $LO_NAME | grep 
"f0:00:0f:16:01" | sort], [0], [dnl
+f0:00:0f:16:01:10 dev $LO_NAME master $BR_NAME static
+f0:00:0f:16:01:10 dev $LO_NAME vlan 1 master $BR_NAME static
+])
+
+check ovn-nbctl --wait=hv remove logical_switch ls-evpn other_config 
dynamic-routing-redistribute
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([bridge fdb show | grep $LO_NAME | grep 
"f0:00:0f:16:01" | sort], [0], [dnl
+])
+
+check ovn-nbctl --wait=hv set logical_switch ls-evpn 
other_config:fdb_age_threshold=300
+ovs-ofctl dump-flows br-int table=OFTABLE_LEARN_REMOTE_FDB
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_LEARN_REMOTE_FDB 
| grep priority | \
+                   awk '{print $[7], $[8]}' | strip_cookie | sort], [0], [dnl
+priority=100,reg14=0x80000001,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,hard_timeout=300,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000002,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,hard_timeout=300,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000003,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,hard_timeout=300,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000004,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,hard_timeout=300,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000005,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,hard_timeout=300,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=150,reg14=0x80000001,metadata=0x$dp_key,dl_src=f0:00:0f:16:10:50 
actions=drop
+priority=150,reg14=0x80000003,metadata=0x$dp_key,dl_src=f0:00:0f:16:10:60 
actions=drop
+priority=150,reg14=0x80000004,metadata=0x$dp_key,dl_src=00:00:00:00:00:01 
actions=drop
+priority=150,reg14=0x80000004,metadata=0x$dp_key,dl_src=f0:00:0f:16:10:70 
actions=drop
+priority=150,reg14=0x80000005,metadata=0x$dp_key,dl_src=00:00:00:00:00:10 
actions=drop
+priority=150,reg14=0x80000005,metadata=0x$dp_key,dl_src=f0:00:0f:16:10:80 
actions=drop
+])
+
+check ovn-nbctl --wait=hv remove logical_switch ls-evpn other_config 
fdb_age_threshold
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_LEARN_REMOTE_FDB 
| grep priority | \
+                   awk '{print $[7], $[8]}' | strip_cookie | sort], [0], [dnl
+priority=100,reg14=0x80000001,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000002,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000003,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000004,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=100,reg14=0x80000005,metadata=0x$dp_key 
actions=learn(table=OFTABLE_GET_REMOTE_FDB,priority=150,delete_learned,OXM_OF_METADATA[[]],NXM_OF_ETH_DST[[]]=NXM_OF_ETH_SRC[[]],load:NXM_NX_REG14[[]]->NXM_NX_REG1[[]])
+priority=150,reg14=0x80000001,metadata=0x$dp_key,dl_src=f0:00:0f:16:10:50 
actions=drop
+priority=150,reg14=0x80000003,metadata=0x$dp_key,dl_src=f0:00:0f:16:10:60 
actions=drop
+priority=150,reg14=0x80000004,metadata=0x$dp_key,dl_src=00:00:00:00:00:01 
actions=drop
+priority=150,reg14=0x80000004,metadata=0x$dp_key,dl_src=f0:00:0f:16:10:70 
actions=drop
+priority=150,reg14=0x80000005,metadata=0x$dp_key,dl_src=00:00:00:00:00:10 
actions=drop
+priority=150,reg14=0x80000005,metadata=0x$dp_key,dl_src=f0:00:0f:16:10:80 
actions=drop
+])
+
+AS_BOX([L2 EVPN ARP learning])
+# Add a router connected to the EVPN logical switch.
+check ovn-nbctl --wait=hv                                                 \
+    -- lr-add lr                                                          \
+    -- lrp-add lr lr-ls-evpn f0:00:0f:16:01:01 172.16.1.1/24 172:16::1/64 \
+    -- lsp-add-router-port ls-evpn ls-evpn-lr lr-ls-evpn
+
+rtr_dp_key=$(fetch_column Datapath tunnel_key external_ids:name=lr)
+rtr_port_key=$(fetch_column Port_Binding tunnel_key logical_port=lr-ls-evpn)
+
+# Simulate remote workload ARPs (type-2 MAC+IP EVPN route).
+# ovn-controller needs to add OF rules for ARP lookup but no rules for
+# MAC_CACHE use.  These entries do not age out automatically, their lifetime
+# is controlled by the BGP-EVPN control plane.
+check ip neigh add dev $BR_NAME 172.16.1.50 lladdr f0:00:0f:16:10:50 nud noarp 
extern_learn
+check ip neigh add dev $BR_NAME 172.16.1.60 lladdr f0:00:0f:16:10:60 nud noarp 
extern_learn
+check ip neigh add dev $BR_NAME 172.16.1.70 lladdr f0:00:0f:16:10:70 nud noarp 
extern_learn
+
+check ip -6 neigh add dev $BR_NAME 172:16::50 lladdr f0:00:0f:16:10:50 nud 
noarp extern_learn
+check ip -6 neigh add dev $BR_NAME 172:16::60 lladdr f0:00:0f:16:10:60 nud 
noarp extern_learn
+check ip -6 neigh add dev $BR_NAME 172:16::70 lladdr f0:00:0f:16:10:70 nud 
noarp extern_learn
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovn-appctl evpn/vtep-arp-list | cut -d',' -f2- | 
sort], [0], [dnl
+ VNI: 10, MAC: f0:00:0f:16:10:50, IP: 172.16.1.50, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:50, IP: 172:16::50, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:60, IP: 172.16.1.60, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:60, IP: 172:16::60, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:70, IP: 172.16.1.70, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:70, IP: 172:16::70, dp_key: $dp_key
+])
+
+AS_BOX([Check dynamic-routing-arp-prefer-local=true])
+check ovn-nbctl --wait=hv set Logical_Switch ls-evpn 
other_config:dynamic-routing-arp-prefer-local=true
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_BINDING | 
grep priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=100,reg0=0xac10010a,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key 
actions=mod_dl_dst:f0:00:0f:16:01:10,load:0x1->NXM_NX_REG10[[6]]
+priority=20,reg0=0xac100132,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key 
actions=mod_dl_dst:f0:00:0f:16:10:50,load:0x1->NXM_NX_REG10[[6]]
+priority=20,reg0=0xac10013c,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key 
actions=mod_dl_dst:f0:00:0f:16:10:60,load:0x1->NXM_NX_REG10[[6]]
+priority=20,reg0=0xac100146,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key 
actions=mod_dl_dst:f0:00:0f:16:10:70,load:0x1->NXM_NX_REG10[[6]]
+priority=20,reg4=0x1720016,reg5=0,reg6=0,reg7=0x50,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
 actions=mod_dl_dst:f0:00:0f:16:10:50,load:0x1->NXM_NX_REG10[[6]]
+priority=20,reg4=0x1720016,reg5=0,reg6=0,reg7=0x60,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
 actions=mod_dl_dst:f0:00:0f:16:10:60,load:0x1->NXM_NX_REG10[[6]]
+priority=20,reg4=0x1720016,reg5=0,reg6=0,reg7=0x70,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
 actions=mod_dl_dst:f0:00:0f:16:10:70,load:0x1->NXM_NX_REG10[[6]]
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_LOOKUP | grep 
priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=100,arp,reg0=0xac10010a,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:01:10
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=20,arp,reg0=0xac100132,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:50
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=20,arp,reg0=0xac10013c,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:60
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=20,arp,reg0=0xac100146,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:70
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=20,icmp6,reg0=0x1720016,reg1=0,reg2=0,reg3=0x50,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:50,icmp_code=0
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=20,icmp6,reg0=0x1720016,reg1=0,reg2=0,reg3=0x60,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:60,icmp_code=0
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=20,icmp6,reg0=0x1720016,reg1=0,reg2=0,reg3=0x70,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:70,icmp_code=0
 actions=load:0x1->NXM_NX_REG10[[6]]
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_CACHE_USE | 
grep priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=100,arp,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:01:10,arp_spa=172.16.1.10,arp_op=2
 actions=drop
+priority=100,ip,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:01:10,nw_src=172.16.1.10
 actions=drop
+])
+
+AS_BOX([Check dynamic-routing-arp-prefer-local=false])
+check ovn-nbctl --wait=hv set Logical_Switch ls-evpn 
other_config:dynamic-routing-arp-prefer-local=false
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_BINDING | 
grep priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=100,reg0=0xac10010a,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key 
actions=mod_dl_dst:f0:00:0f:16:01:10,load:0x1->NXM_NX_REG10[[6]]
+priority=200,reg0=0xac100132,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key 
actions=mod_dl_dst:f0:00:0f:16:10:50,load:0x1->NXM_NX_REG10[[6]]
+priority=200,reg0=0xac10013c,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key 
actions=mod_dl_dst:f0:00:0f:16:10:60,load:0x1->NXM_NX_REG10[[6]]
+priority=200,reg0=0xac100146,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key 
actions=mod_dl_dst:f0:00:0f:16:10:70,load:0x1->NXM_NX_REG10[[6]]
+priority=200,reg4=0x1720016,reg5=0,reg6=0,reg7=0x50,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
 actions=mod_dl_dst:f0:00:0f:16:10:50,load:0x1->NXM_NX_REG10[[6]]
+priority=200,reg4=0x1720016,reg5=0,reg6=0,reg7=0x60,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
 actions=mod_dl_dst:f0:00:0f:16:10:60,load:0x1->NXM_NX_REG10[[6]]
+priority=200,reg4=0x1720016,reg5=0,reg6=0,reg7=0x70,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
 actions=mod_dl_dst:f0:00:0f:16:10:70,load:0x1->NXM_NX_REG10[[6]]
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_LOOKUP | grep 
priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=100,arp,reg0=0xac10010a,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:01:10
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=200,arp,reg0=0xac100132,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:50
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=200,arp,reg0=0xac10013c,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:60
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=200,arp,reg0=0xac100146,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:70
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=200,icmp6,reg0=0x1720016,reg1=0,reg2=0,reg3=0x50,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:50,icmp_code=0
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=200,icmp6,reg0=0x1720016,reg1=0,reg2=0,reg3=0x60,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:60,icmp_code=0
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=200,icmp6,reg0=0x1720016,reg1=0,reg2=0,reg3=0x70,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:70,icmp_code=0
 actions=load:0x1->NXM_NX_REG10[[6]]
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_CACHE_USE | 
grep priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=100,arp,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:01:10,arp_spa=172.16.1.10,arp_op=2
 actions=drop
+priority=100,ip,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:01:10,nw_src=172.16.1.10
 actions=drop
+])
+
+# Check that the recompute won't change the UUIDs and flows.
+ovn-appctl evpn/vtep-arp-list > arp_before
+
+check ovn-appctl inc-engine/recompute
+check ovn-nbctl --wait=hv sync
+
+ovn-appctl evpn/vtep-arp-list > arp_after
+
+check diff -q arp_before arp_after
+
+# Remove remote workload ARP entries and check ovn-controller's state.
+check ip neigh del dev $BR_NAME 172.16.1.50
+check ip neigh del dev $BR_NAME 172.16.1.60
+check ip neigh del dev $BR_NAME 172.16.1.70
+
+check ip -6 neigh del dev $BR_NAME 172:16::50
+check ip -6 neigh del dev $BR_NAME 172:16::60
+check ip -6 neigh del dev $BR_NAME 172:16::70
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovn-appctl evpn/vtep-arp-list | cut -d',' -f2- | 
sort], [0], [dnl
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_BINDING | 
grep priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=100,reg0=0xac10010a,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key 
actions=mod_dl_dst:f0:00:0f:16:01:10,load:0x1->NXM_NX_REG10[[6]]
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_LOOKUP | grep 
priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=100,arp,reg0=0xac10010a,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:01:10
 actions=load:0x1->NXM_NX_REG10[[6]]
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_CACHE_USE | 
grep priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=100,arp,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:01:10,arp_spa=172.16.1.10,arp_op=2
 actions=drop
+priority=100,ip,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:01:10,nw_src=172.16.1.10
 actions=drop
+])
+
+# Re-add the remote workload ARPs, remove the router, check that flows are
+# removed (vtep-arp-list should still list the ARPs as they're learned on
+# the logical switch that still exists).
+check ip neigh add dev $BR_NAME 172.16.1.50 lladdr f0:00:0f:16:10:50 nud noarp 
extern_learn
+check ip neigh add dev $BR_NAME 172.16.1.60 lladdr f0:00:0f:16:10:60 nud noarp 
extern_learn
+check ip neigh add dev $BR_NAME 172.16.1.70 lladdr f0:00:0f:16:10:70 nud noarp 
extern_learn
+
+check ip -6 neigh add dev $BR_NAME 172:16::50 lladdr f0:00:0f:16:10:50 nud 
noarp extern_learn
+check ip -6 neigh add dev $BR_NAME 172:16::60 lladdr f0:00:0f:16:10:60 nud 
noarp extern_learn
+check ip -6 neigh add dev $BR_NAME 172:16::70 lladdr f0:00:0f:16:10:70 nud 
noarp extern_learn
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovn-appctl evpn/vtep-arp-list | cut -d',' -f2- | 
sort], [0], [dnl
+ VNI: 10, MAC: f0:00:0f:16:10:50, IP: 172.16.1.50, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:50, IP: 172:16::50, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:60, IP: 172.16.1.60, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:60, IP: 172:16::60, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:70, IP: 172.16.1.70, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:70, IP: 172:16::70, dp_key: $dp_key
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_BINDING | 
grep priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=100,reg0=0xac10010a,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key 
actions=mod_dl_dst:f0:00:0f:16:01:10,load:0x1->NXM_NX_REG10[[6]]
+priority=200,reg0=0xac100132,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key 
actions=mod_dl_dst:f0:00:0f:16:10:50,load:0x1->NXM_NX_REG10[[6]]
+priority=200,reg0=0xac10013c,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key 
actions=mod_dl_dst:f0:00:0f:16:10:60,load:0x1->NXM_NX_REG10[[6]]
+priority=200,reg0=0xac100146,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key 
actions=mod_dl_dst:f0:00:0f:16:10:70,load:0x1->NXM_NX_REG10[[6]]
+priority=200,reg4=0x1720016,reg5=0,reg6=0,reg7=0x50,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
 actions=mod_dl_dst:f0:00:0f:16:10:50,load:0x1->NXM_NX_REG10[[6]]
+priority=200,reg4=0x1720016,reg5=0,reg6=0,reg7=0x60,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
 actions=mod_dl_dst:f0:00:0f:16:10:60,load:0x1->NXM_NX_REG10[[6]]
+priority=200,reg4=0x1720016,reg5=0,reg6=0,reg7=0x70,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
 actions=mod_dl_dst:f0:00:0f:16:10:70,load:0x1->NXM_NX_REG10[[6]]
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_LOOKUP | grep 
priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+priority=100,arp,reg0=0xac10010a,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:01:10
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=200,arp,reg0=0xac100132,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:50
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=200,arp,reg0=0xac10013c,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:60
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=200,arp,reg0=0xac100146,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:70
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=200,icmp6,reg0=0x1720016,reg1=0,reg2=0,reg3=0x50,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:50,icmp_code=0
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=200,icmp6,reg0=0x1720016,reg1=0,reg2=0,reg3=0x60,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:60,icmp_code=0
 actions=load:0x1->NXM_NX_REG10[[6]]
+priority=200,icmp6,reg0=0x1720016,reg1=0,reg2=0,reg3=0x70,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=f0:00:0f:16:10:70,icmp_code=0
 actions=load:0x1->NXM_NX_REG10[[6]]
+])
+
+check ovn-nbctl --wait=hv lr-del lr
+AT_CHECK_UNQUOTED([ovn-appctl evpn/vtep-arp-list | cut -d',' -f2- | sort], 
[0], [dnl
+ VNI: 10, MAC: f0:00:0f:16:10:50, IP: 172.16.1.50, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:50, IP: 172:16::50, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:60, IP: 172.16.1.60, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:60, IP: 172:16::60, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:70, IP: 172.16.1.70, dp_key: $dp_key
+ VNI: 10, MAC: f0:00:0f:16:10:70, IP: 172:16::70, dp_key: $dp_key
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_BINDING | 
grep priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+])
+
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_LOOKUP | grep 
priority | \
+                   awk '{print $[7], $[8]}' | sort], [0], [dnl
+])
+
+AS_BOX([L2 EVPN ARP advertising])
+# Add a router connected to the EVPN logical switch.
+check ovn-nbctl --wait=hv                                                 \
+    -- lr-add lr                                                          \
+    -- set Logical_Router lr options:chassis=hv1                          \
+    -- lrp-add lr lr-ls-evpn f0:00:0f:16:01:01 172.16.1.1/24 172:16::1/64
+
+ls_evpn_uuid=$(fetch_column Datapath_Binding _uuid external_ids:name=ls-evpn)
+check ovn-nbctl --wait=hv set logical_switch ls-evpn 
other_config:dynamic-routing-redistribute=fdb,ip
+check_row_count Advertised_MAC_Binding 1 ip='172.16.1.10' 
mac='f0\:00\:0f\:16\:01\:10' datapath=$ls_evpn_uuid
+check_row_count Advertised_MAC_Binding 1 ip='172.16.1.1' 
mac='f0\:00\:0f\:16\:01\:01' datapath=$ls_evpn_uuid
+check_row_count Advertised_MAC_Binding 1 ip='172\:16\:\:10' 
mac='f0\:00\:0f\:16\:01\:10' datapath=$ls_evpn_uuid
+check_row_count Advertised_MAC_Binding 1 ip='172\:16\:\:1' 
mac='f0\:00\:0f\:16\:01\:01' datapath=$ls_evpn_uuid
+check_row_count Advertised_MAC_Binding 4
+
+AT_CHECK([ip neigh show dev $BR_NAME nud noarp | grep -q '172.16.1.10 lladdr 
f0:00:0f:16:01:10 NOARP'])
+AT_CHECK([ip neigh show dev $BR_NAME nud noarp | grep -q '172.16.1.1 lladdr 
f0:00:0f:16:01:01 NOARP'])
+AT_CHECK([ip -6 neigh show dev $BR_NAME nud noarp | grep -q '172:16::10 lladdr 
f0:00:0f:16:01:10 NOARP'])
+AT_CHECK([ip -6 neigh show dev $BR_NAME nud noarp | grep -q '172:16::1 lladdr 
f0:00:0f:16:01:01 NOARP'])
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([bridge fdb show | grep $LO_NAME | grep 
"f0:00:0f:16:01" | sort], [0], [dnl
+f0:00:0f:16:01:01 dev $LO_NAME master $BR_NAME static
+f0:00:0f:16:01:01 dev $LO_NAME vlan 1 master $BR_NAME static
+f0:00:0f:16:01:10 dev $LO_NAME master $BR_NAME static
+f0:00:0f:16:01:10 dev $LO_NAME vlan 1 master $BR_NAME static
+])
+
+check ovn-nbctl --wait=hv lsp-add ls-evpn workload2 \
+    -- lsp-set-addresses workload2 "f0:00:0f:16:01:20 172.16.1.20 172:16::20"
+
+check_row_count Advertised_MAC_Binding 1 ip='172.16.1.20' 
mac='f0\:00\:0f\:16\:01\:20' datapath=$ls_evpn_uuid
+AT_CHECK([ip neigh show dev $BR_NAME nud noarp | grep -q '172.16.1.20 lladdr 
f0:00:0f:16:01:20 NOARP'])
+
+check_row_count Advertised_MAC_Binding 1 ip='172\:16\:\:20' 
mac='f0\:00\:0f\:16\:01\:20' datapath=$ls_evpn_uuid
+AT_CHECK([ip -6 neigh show dev $BR_NAME nud noarp | grep -q '172:16::20 lladdr 
f0:00:0f:16:01:20 NOARP'])
+
+check ovn-nbctl --wait=hv lsp-del workload2
+AT_CHECK([ip neigh show dev $BR_NAME nud noarp | grep -q '172.16.1.20 lladdr 
f0:00:0f:16:01:20 NOARP'], [1])
+AT_CHECK([ip -6 neigh show dev $BR_NAME nud noarp | grep -q '172:16::20 lladdr 
f0:00:0f:16:01:20 NOARP'], [1])
+
+check ovn-nbctl --wait=hv lsp-del workload1
+check_row_count Advertised_MAC_Binding 2
+AT_CHECK([ip neigh show dev $BR_NAME nud noarp | grep -q '172.16.1.10 lladdr 
f0:00:0f:16:01:10 NOARP'], [1])
+AT_CHECK([ip -6 neigh show dev $BR_NAME nud noarp | grep -q '172:16::10 lladdr 
f0:00:0f:16:01:10 NOARP'], [1])
+
+check ovn-nbctl --wait=hv lrp-del lr-ls-evpn
+check_row_count Advertised_MAC_Binding 0
+AT_CHECK([ip neigh show dev $BR_NAME nud noarp | grep -q '172.16.1.1 lladdr 
f0:00:0f:16:01:01 NOARP'], [1])
+AT_CHECK([ip -6 neigh show dev $BR_NAME nud noarp | grep -q '172:16::1 lladdr 
f0:00:0f:16:01:01 NOARP'], [1])
+
+check ovn-nbctl --wait=hv ls-del ls-evpn
+check ovn-nbctl --wait=hv lr-del lr
+
+OVN_CLEANUP_CONTROLLER([hv1])
+
+OVN_CLEANUP_NORTHD
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+/Failed to acquire.*/d
+/connection dropped.*/d"])
+AT_CLEANUP
+])
+
 ]) dnl EVPN_SWITCH_TESTS
 
 EVPN_SWITCH_TESTS([default])
-- 
2.52.0

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to