On 9/29/25 10:40 AM, Ales Musil wrote:
> On Sat, Sep 27, 2025 at 12:20 AM Dumitru Ceara <[email protected]> wrote:
> 
>> For logical switches that are EVPN-enabled (with "dynamic-routing-vni"
>> set), automatically learn IP MAC bindings that were advertised by remote
>> EVPN peers as Type-2 MAC+IP routes.
>>
>> As with FDB, these are learned by monitoring through NetLink the VRF
>> linux bridge associated to the EVPN logical switch.  The IP <-> MAC
>> mappings are stored in memory in ovn-controller.  The OpenFlow pipelines
>> of the adjacent logical routers are automatically updated by
>> ovn-controller so that the neighbor entries are handled as if they were
>> learned as regular Southbound MAC_Bindings.
>>
>> NOTE: By default the EVPN learned neighbors have a higher priority than
>> the ones learned through the SB MAC_Binding table.  This can be tweaked
>> by setting the new Logical_Switch "dynamic-routing-arp-prefer-local"
>> option to "true" (default "false").  If set to true, SB MAC_Binding
>> records have precedence.
>>
>> Reported-at: https://issues.redhat.com/browse/FDP-1388
>> Signed-off-by: Dumitru Ceara <[email protected]>
>> ---
>>
> 
> Hi Dumitru,
> 

Hi Ales,

> thank you for the patch. I have one comment down below.
> 

Thanks for the review!

> 
>>  NEWS                                   |   8 ++
>>  controller/automake.mk                 |   2 +
>>  controller/evpn-arp.c                  | 174 +++++++++++++++++++++++++
>>  controller/evpn-arp.h                  |  65 +++++++++
>>  controller/evpn-fdb.c                  |   2 +-
>>  controller/evpn-fdb.h                  |   2 +-
>>  controller/neighbor-exchange-netlink.c |   9 ++
>>  controller/neighbor-exchange-netlink.h |   1 +
>>  controller/neighbor-exchange-stub.c    |   2 +-
>>  controller/neighbor-exchange.c         |  91 ++++++++-----
>>  controller/neighbor-exchange.h         |  12 +-
>>  controller/neighbor-of.h               |  11 +-
>>  controller/ovn-controller.c            | 132 ++++++++++++++++++-
>>  controller/physical.c                  |  53 +++++++-
>>  controller/physical.h                  |   5 +
>>  northd/en-datapath-logical-switch.c    |   7 +
>>  ovn-nb.xml                             |  23 ++++
>>  tests/multinode.at                     | 104 +++++++++++++--
>>  tests/system-ovn.at                    | 147 +++++++++++++++++++++
>>  19 files changed, 792 insertions(+), 58 deletions(-)
>>  create mode 100644 controller/evpn-arp.c
>>  create mode 100644 controller/evpn-arp.h
>>
>> diff --git a/NEWS b/NEWS
>> index dc5d49a94d..f9ad8ae75e 100644
>> --- a/NEWS
>> +++ b/NEWS
>> @@ -6,6 +6,14 @@ Post v25.09.0
>>     - Added "ic-route-deny-adv" and "ic-route-deny-learn" options to
>>       the Logical_Router/Logical_Router_Port tables to allow users to
>>       deny filter advertised/learned IC routes.
>> +   - Dynamic Routing:
>> +     * Extend the Logical Switch EVPN support to now automatically learn
>> +       IP neighbors (Type-2 MAC+IP EVPN routes) and automatically inject
>> them
>> +       into the pipelines of the adjacent logical routers.
>> +     * Add the "other_config:dynamic-routing-arp-prefer-local" to Logical
>> +       Switches. If set to "true" ovn-controller will give preference to
>> SB
>> +       (Static_)MAC_Bindings of adjacent logical routers over ARPs learned
>> +       through EVPN on the switch.
>>
>>  OVN v25.09.0 - xxx xx xxxx
>>  --------------------------
>> diff --git a/controller/automake.mk b/controller/automake.mk
>> index b3c6293fa9..fb27f2ae98 100644
>> --- a/controller/automake.mk
>> +++ b/controller/automake.mk
>> @@ -10,6 +10,8 @@ controller_ovn_controller_SOURCES = \
>>         controller/chassis.h \
>>         controller/encaps.c \
>>         controller/encaps.h \
>> +       controller/evpn-arp.c \
>> +       controller/evpn-arp.h \
>>         controller/evpn-binding.c \
>>         controller/evpn-binding.h \
>>         controller/evpn-fdb.c \
>> diff --git a/controller/evpn-arp.c b/controller/evpn-arp.c
>> new file mode 100644
>> index 0000000000..f73e09504a
>> --- /dev/null
>> +++ b/controller/evpn-arp.c
>> @@ -0,0 +1,174 @@
>> +/* Copyright (c) 2025, Red Hat, Inc.
>> + *
>> + * Licensed under the Apache License, Version 2.0 (the "License");
>> + * you may not use this file except in compliance with the License.
>> + * You may obtain a copy of the License at:
>> + *
>> + *     http://www.apache.org/licenses/LICENSE-2.0
>> + *
>> + * Unless required by applicable law or agreed to in writing, software
>> + * distributed under the License is distributed on an "AS IS" BASIS,
>> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
>> implied.
>> + * See the License for the specific language governing permissions and
>> + * limitations under the License.
>> + */
>> +
>> +#include <config.h>
>> +
>> +#include "evpn-binding.h"
>> +#include "neighbor-exchange.h"
>> +#include "openvswitch/dynamic-string.h"
>> +#include "openvswitch/vlog.h"
>> +#include "ovn-sb-idl.h"
>> +#include "packets.h"
>> +#include "unixctl.h"
>> +
>> +#include "evpn-arp.h"
>> +
>> +VLOG_DEFINE_THIS_MODULE(evpn_arp);
>> +
>> +static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
>> +
>> +static struct evpn_arp *evpn_arp_add(struct hmap *evpn_arps, struct
>> eth_addr,
>> +                                     const struct in6_addr *, uint32_t
>> vni);
>> +static struct evpn_arp *evpn_arp_find(const struct hmap *evpn_arps,
>> +                                      struct eth_addr,
>> +                                      const struct in6_addr *,
>> +                                      uint32_t vni);
>> +
>> +void
>> +evpn_arp_run(const struct evpn_arp_ctx_in *arp_ctx_in,
>> +             struct evpn_arp_ctx_out *arp_ctx_out)
>> +{
>> +    struct hmapx stale_arps = HMAPX_INITIALIZER(&stale_arps);
>> +
>> +    struct evpn_arp *arp;
>> +    HMAP_FOR_EACH (arp, hmap_node, arp_ctx_out->arps) {
>> +        hmapx_add(&stale_arps, arp);
>> +    }
>> +
>> +    const struct evpn_static_entry *static_arp;
>> +    HMAP_FOR_EACH (static_arp, hmap_node, arp_ctx_in->static_arps) {
>> +        const struct evpn_datapath *edp =
>> +            evpn_datapath_find(arp_ctx_in->datapaths, static_arp->vni);
>> +        if (!edp) {
>> +            char addr_s[INET6_ADDRSTRLEN + 1];
>> +            VLOG_WARN_RL(&rl, "Couldn't find EVPN datapath for ARP entry:
>> "
>> +                              "VNI: %"PRIu32" MAC: "ETH_ADDR_FMT" IP:
>> %s.",
>> +                         static_arp->vni, ETH_ADDR_ARGS(static_arp->mac),
>> +                         ipv6_string_mapped(addr_s, &static_arp->ip)
>> +                         ? addr_s : "(invalid)");
>> +            continue;
>> +        }
>> +
>> +        arp = evpn_arp_find(arp_ctx_out->arps, static_arp->mac,
>> +                            &static_arp->ip, static_arp->vni);
>> +        if (!arp) {
>> +            arp = evpn_arp_add(arp_ctx_out->arps, static_arp->mac,
>> +                               &static_arp->ip, static_arp->vni);
>> +        }
>> +
>> +        bool updated = false;
>> +        if (arp->ldp != edp->ldp) {
>> +            arp->ldp = edp->ldp;
>> +            updated = true;
>> +        }
>> +
>> +        enum neigh_of_rule_prio priority =
>> +            smap_get_bool(&arp->ldp->datapath->external_ids,
>> +                          "dynamic-routing-arp-prefer-local",
>> +                          false)
>> +            ? NEIGH_OF_EVPN_MAC_BINDING_LOW_PRIO
>> +            : NEIGH_OF_EVPN_MAC_BINDING_HIGH_PRIO;
>> +        if (arp->priority != priority) {
>> +            arp->priority = priority;
>> +            updated = true;
>> +        }
>> +
>> +        if (updated) {
>> +            hmapx_add(arp_ctx_out->updated_arps, arp);
>> +        }
>> +
>> +        hmapx_find_and_delete(&stale_arps, arp);
>> +    }
>> +
>> +    struct hmapx_node *node;
>> +    HMAPX_FOR_EACH (node, &stale_arps) {
>> +        arp = node->data;
>> +
>> +        uuidset_insert(arp_ctx_out->removed_arps, &arp->flow_uuid);
>> +        hmap_remove(arp_ctx_out->arps, &arp->hmap_node);
>> +        free(arp);
>> +    }
>> +
>> +    hmapx_destroy(&stale_arps);
>> +}
>> +
>> +void
>> +evpn_arps_destroy(struct hmap *arps)
>> +{
>> +    struct evpn_arp *arp;
>> +    HMAP_FOR_EACH_POP (arp, hmap_node, arps) {
>> +        free(arp);
>> +    }
>> +    hmap_destroy(arps);
>> +}
>> +
>> +void
>> +evpn_arp_list(struct unixctl_conn *conn, int argc OVS_UNUSED,
>> +              const char *argv[] OVS_UNUSED, void *data_)
>> +{
>> +    struct hmap *arps = data_;
>> +    struct ds ds = DS_EMPTY_INITIALIZER;
>> +
>> +    const struct evpn_arp *arp;
>> +    HMAP_FOR_EACH (arp, hmap_node, arps) {
>> +        char addr_s[INET6_ADDRSTRLEN + 1];
>> +        ds_put_format(&ds, "UUID: "UUID_FMT", VNI: %"PRIu32", "
>> +                           "MAC: "ETH_ADDR_FMT", IP: %s, "
>> +                           "dp_key: %"PRId64"\n",
>> +                      UUID_ARGS(&arp->flow_uuid), arp->vni,
>> +                      ETH_ADDR_ARGS(arp->mac),
>> +                      ipv6_string_mapped(addr_s, &arp->ip)
>> +                      ? addr_s : "(invalid)",
>> +                      arp->ldp->datapath->tunnel_key);
>> +    }
>> +
>> +    unixctl_command_reply(conn, ds_cstr_ro(&ds));
>> +    ds_destroy(&ds);
>> +}
>> +
>> +static struct evpn_arp *
>> +evpn_arp_add(struct hmap *evpn_arps, struct eth_addr mac,
>> +             const struct in6_addr *ip, uint32_t vni)
>> +{
>> +    struct evpn_arp *arp = xmalloc(sizeof *arp);
>> +    *arp = (struct evpn_arp) {
>> +        .flow_uuid = uuid_random(),
>> +        .mac = mac,
>> +        .ip = *ip,
>> +        .vni = vni,
>> +    };
>> +
>> +    uint32_t hash = hash_bytes(&mac, sizeof mac, 0);
>> +    hmap_insert(evpn_arps, &arp->hmap_node, hash);
>> +
>> +    return arp;
>> +}
>> +
>> +static struct evpn_arp *
>> +evpn_arp_find(const struct hmap *evpn_arps, struct eth_addr mac,
>> +              const struct in6_addr *ip, uint32_t vni)
>> +{
>> +    uint32_t hash = hash_bytes(&mac, sizeof mac, 0);
>>
> 
> We should have separate hash function that hashes
> all three keys (ip + mac + vni) instead of only the mac.
> 

Good catch!  I had copied these lines from evpn-fdb.c without thinking
too much.

> 
>> +
>> +    struct evpn_arp *arp;
>> +    HMAP_FOR_EACH_WITH_HASH (arp, hmap_node, hash, evpn_arps) {
>> +        if (arp->vni == vni && eth_addr_equals(arp->mac, mac) &&
>> +                ipv6_addr_equals(&arp->ip, ip)) {
>> +            return arp;
>> +        }
>> +    }
>> +
>> +    return NULL;
>> +}
>> diff --git a/controller/evpn-arp.h b/controller/evpn-arp.h
>> new file mode 100644
>> index 0000000000..7f3a4c0e44
>> --- /dev/null
>> +++ b/controller/evpn-arp.h
>> @@ -0,0 +1,65 @@
>> +/* Copyright (c) 2025, Red Hat, Inc.
>> + *
>> + * Licensed under the Apache License, Version 2.0 (the "License");
>> + * you may not use this file except in compliance with the License.
>> + * You may obtain a copy of the License at:
>> + *
>> + *     http://www.apache.org/licenses/LICENSE-2.0
>> + *
>> + * Unless required by applicable law or agreed to in writing, software
>> + * distributed under the License is distributed on an "AS IS" BASIS,
>> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
>> implied.
>> + * See the License for the specific language governing permissions and
>> + * limitations under the License.
>> + */
>> +
>> +#ifndef EVPN_ARP_H
>> +#define EVPN_ARP_H 1
>> +
>> +#include <stdint.h>
>> +
>> +#include "hmapx.h"
>> +#include "local_data.h"
>> +#include "neighbor-of.h"
>> +#include "openvswitch/hmap.h"
>> +#include "uuidset.h"
>> +
>> +struct unixctl_conn;
>> +
>> +struct evpn_arp_ctx_in {
>> +    /* Contains 'struct evpn_datapath'. */
>> +    const struct hmap *datapaths;
>> +    /* Contains 'struct evpn_static_entry' one for each ARP. */
>> +    const struct hmap *static_arps;
>> +};
>> +
>> +struct evpn_arp_ctx_out {
>> +    /* Contains 'struct evpn_arp'. */
>> +    struct hmap *arps;
>> +    /* Contains pointers to 'struct evpn_binding'. */
>> +    struct hmapx *updated_arps;
>> +    /* Contains 'flow_uuid' from removed 'struct evpn_binding'. */
>> +    struct uuidset *removed_arps;
>> +};
>> +
>> +struct evpn_arp {
>> +    struct hmap_node hmap_node;
>> +    /* UUID used to identify physical flows related to this ARP entry. */
>> +    struct uuid flow_uuid;
>> +    /* MAC address of the remote workload. */
>> +    struct eth_addr mac;
>> +    /* IP address of the remote workload. */
>> +    struct in6_addr ip;
>> +    uint32_t vni;
>> +    /* Logical datapath of the switch this was learned on. */
>> +    const struct local_datapath *ldp;
>> +    /* Priority to use for this ARP entry at OpenFlow level. */
>> +    enum neigh_of_rule_prio priority;
>> +};
>> +
>> +void evpn_arp_run(const struct evpn_arp_ctx_in *, struct evpn_arp_ctx_out
>> *);
>> +void evpn_arps_destroy(struct hmap *arps);
>> +void evpn_arp_list(struct unixctl_conn *conn, int argc,
>> +                   const char *argv[], void *data_);
>> +
>> +#endif /* EVPN_ARP_H */
>> diff --git a/controller/evpn-fdb.c b/controller/evpn-fdb.c
>> index 53312fbb2c..acef8f0a42 100644
>> --- a/controller/evpn-fdb.c
>> +++ b/controller/evpn-fdb.c
>> @@ -43,7 +43,7 @@ evpn_fdb_run(const struct evpn_fdb_ctx_in *f_ctx_in,
>>          hmapx_add(&stale_fdbs, fdb);
>>      }
>>
>> -    const struct evpn_static_fdb *static_fdb;
>> +    const struct evpn_static_entry *static_fdb;
>>      HMAP_FOR_EACH (static_fdb, hmap_node, f_ctx_in->static_fdbs) {
>>          const struct evpn_binding *binding =
>>              evpn_binding_find(f_ctx_in->bindings, &static_fdb->ip,
>> diff --git a/controller/evpn-fdb.h b/controller/evpn-fdb.h
>> index a38718cd93..de58df813e 100644
>> --- a/controller/evpn-fdb.h
>> +++ b/controller/evpn-fdb.h
>> @@ -27,7 +27,7 @@ struct unixctl_conn;
>>  struct evpn_fdb_ctx_in {
>>      /* Contains 'struct evpn_binding'. */
>>      const struct hmap *bindings;
>> -    /* Contains 'struct evpn_static_fdb'. */
>> +    /* Contains 'struct evpn_static_entry', one for each FDB. */
>>      const struct hmap *static_fdbs;
>>  };
>>
>> diff --git a/controller/neighbor-exchange-netlink.c
>> b/controller/neighbor-exchange-netlink.c
>> index 66711d4980..fa2444e2f3 100644
>> --- a/controller/neighbor-exchange-netlink.c
>> +++ b/controller/neighbor-exchange-netlink.c
>> @@ -189,6 +189,15 @@ ne_is_valid_static_fdb(struct ne_nl_received_neigh
>> *ne)
>>             ipv6_addr_is_set(&ne->addr) && ne->flags & NTF_EXT_LEARNED;
>>  }
>>
>> +/* OVN expects that the ARP entry has an IP address, a MAC address,
>> + * the entry is marked as "extern learned" and "static" (noarp). */
>> +bool
>> +ne_is_valid_static_arp(struct ne_nl_received_neigh *ne)
>> +{
>> +    return !eth_addr_is_zero(ne->lladdr) && ipv6_addr_is_set(&ne->addr) &&
>> +           ne->state & NUD_NOARP && ne->flags & NTF_EXT_LEARNED;
>> +}
>> +
>>  static void
>>  ne_table_dump_one_ifindex(unsigned char address_family, int32_t if_index,
>>                            ne_table_handle_msg_callback *handle_msg_cb,
>> diff --git a/controller/neighbor-exchange-netlink.h
>> b/controller/neighbor-exchange-netlink.h
>> index ee04691ebd..6d907938eb 100644
>> --- a/controller/neighbor-exchange-netlink.h
>> +++ b/controller/neighbor-exchange-netlink.h
>> @@ -56,6 +56,7 @@ int ne_nl_sync_neigh(uint8_t family, int32_t if_index,
>>  bool ne_is_ovn_owned(const struct ne_nl_received_neigh *nd);
>>  bool ne_is_valid_remote_vtep(struct ne_nl_received_neigh *ne);
>>  bool ne_is_valid_static_fdb(struct ne_nl_received_neigh *ne);
>> +bool ne_is_valid_static_arp(struct ne_nl_received_neigh *ne);
>>
>>  int ne_table_parse(struct ofpbuf *, void *change);
>>
>> diff --git a/controller/neighbor-exchange-stub.c
>> b/controller/neighbor-exchange-stub.c
>> index 272c14007f..a1c89ed2ba 100644
>> --- a/controller/neighbor-exchange-stub.c
>> +++ b/controller/neighbor-exchange-stub.c
>> @@ -42,6 +42,6 @@ evpn_remote_vtep_list(struct unixctl_conn *conn
>> OVS_UNUSED,
>>  }
>>
>>  void
>> -evpn_static_fdbs_clear(struct hmap *static_fdbs OVS_UNUSED)
>> +evpn_static_entries_clear(struct hmap *static_entries OVS_UNUSED)
>>  {
>>  }
>> diff --git a/controller/neighbor-exchange.c
>> b/controller/neighbor-exchange.c
>> index 2c436b4141..ea6c287b1f 100644
>> --- a/controller/neighbor-exchange.c
>> +++ b/controller/neighbor-exchange.c
>> @@ -38,13 +38,15 @@ static void evpn_remote_vtep_add(struct hmap
>> *remote_vteps, struct in6_addr ip,
>>  static struct evpn_remote_vtep *evpn_remote_vtep_find(
>>      const struct hmap *remote_vteps, const struct in6_addr *ip,
>>      uint16_t port, uint32_t vni);
>> -static void evpn_static_fdb_add(struct hmap *static_fdbs, struct eth_addr
>> mac,
>> -                                struct in6_addr ip, uint32_t vni);
>> -static struct evpn_static_fdb *evpn_static_fdb_find(
>> -    const struct hmap *static_fdbs, struct eth_addr mac,
>> +static void evpn_static_entry_add(struct hmap *static_entries,
>> +                                  struct eth_addr mac, struct in6_addr ip,
>> +                                  uint32_t vni);
>> +static struct evpn_static_entry *evpn_static_entry_find(
>> +    const struct hmap *static_entries, struct eth_addr mac,
>>      struct in6_addr ip, uint32_t vni);
>> -static uint32_t evpn_static_fdb_hash(const struct eth_addr *mac,
>> -                                     const struct in6_addr *ip, uint32_t
>> vni);
>> +static uint32_t evpn_static_entry_hash(const struct eth_addr *mac,
>> +                                       const struct in6_addr *ip,
>> +                                       uint32_t vni);
>>
>>  /* Last neighbor_exchange netlink operation. */
>>  static int neighbor_exchange_nl_status;
>> @@ -92,8 +94,22 @@ neighbor_exchange_run(const struct
>> neighbor_exchange_ctx_in *n_ctx_in,
>>                               &received_neighbors)
>>          );
>>
>> -        if (nim->type == NEIGH_IFACE_VXLAN) {
>> -            struct ne_nl_received_neigh *ne;
>> +        struct ne_nl_received_neigh *ne;
>> +        switch (nim->type) {
>> +        case NEIGH_IFACE_BRIDGE:
>> +            VECTOR_FOR_EACH_PTR (&received_neighbors, ne) {
>> +                if (ne_is_valid_static_arp(ne)) {
>> +                    if (!evpn_static_entry_find(n_ctx_out->static_arps,
>> +                                                ne->lladdr, ne->addr,
>> +                                                nim->vni)) {
>> +                        evpn_static_entry_add(n_ctx_out->static_arps,
>> +                                              ne->lladdr, ne->addr,
>> +                                              nim->vni);
>> +                    }
>> +                }
>> +            }
>> +            break;
>> +        case NEIGH_IFACE_VXLAN:
>>              VECTOR_FOR_EACH_PTR (&received_neighbors, ne) {
>>                  if (ne_is_valid_remote_vtep(ne)) {
>>                      uint16_t port = ne->port ? ne->port :
>> DEFAULT_VXLAN_PORT;
>> @@ -103,14 +119,19 @@ neighbor_exchange_run(const struct
>> neighbor_exchange_ctx_in *n_ctx_in,
>>                                               port, nim->vni);
>>                      }
>>                  } else if (ne_is_valid_static_fdb(ne)) {
>> -                    if (!evpn_static_fdb_find(n_ctx_out->static_fdbs,
>> +                    if (!evpn_static_entry_find(n_ctx_out->static_fdbs,
>> +                                                ne->lladdr, ne->addr,
>> +                                                nim->vni)) {
>> +                        evpn_static_entry_add(n_ctx_out->static_fdbs,
>>                                                ne->lladdr, ne->addr,
>> -                                              nim->vni)) {
>> -                        evpn_static_fdb_add(n_ctx_out->static_fdbs,
>> ne->lladdr,
>> -                                            ne->addr, nim->vni);
>> +                                              nim->vni);
>>                      }
>>                  }
>>              }
>> +            break;
>> +        case NEIGH_IFACE_LOOPBACK:
>> +            /* No learning from the loopback interface required. */
>> +            break;
>>          }
>>
>>
>>  neighbor_table_add_watch_request(&n_ctx_out->neighbor_table_watches,
>> @@ -154,11 +175,11 @@ evpn_remote_vtep_list(struct unixctl_conn *conn, int
>> argc OVS_UNUSED,
>>  }
>>
>>  void
>> -evpn_static_fdbs_clear(struct hmap *static_fdbs)
>> +evpn_static_entries_clear(struct hmap *static_entries)
>>  {
>> -    struct evpn_static_fdb *fdb;
>> -    HMAP_FOR_EACH_POP (fdb, hmap_node, static_fdbs) {
>> -        free(fdb);
>> +    struct evpn_static_entry *e;
>> +    HMAP_FOR_EACH_POP (e, hmap_node, static_entries) {
>> +        free(e);
>>      }
>>  }
>>
>> @@ -208,32 +229,32 @@ evpn_remote_vtep_hash(const struct in6_addr *ip,
>> uint16_t port,
>>  }
>>
>>  static void
>> -evpn_static_fdb_add(struct hmap *static_fdbs, struct eth_addr mac,
>> -                    struct in6_addr ip, uint32_t vni)
>> +evpn_static_entry_add(struct hmap *static_entries, struct eth_addr mac,
>> +                      struct in6_addr ip, uint32_t vni)
>>  {
>> -    struct evpn_static_fdb *fdb = xmalloc(sizeof *fdb);
>> -    *fdb = (struct evpn_static_fdb) {
>> +    struct evpn_static_entry *e = xmalloc(sizeof *e);
>> +    *e = (struct evpn_static_entry) {
>>          .mac = mac,
>>          .ip = ip,
>>          .vni = vni,
>>      };
>>
>> -    hmap_insert(static_fdbs, &fdb->hmap_node,
>> -                evpn_static_fdb_hash(&mac, &ip, vni));
>> +    hmap_insert(static_entries, &e->hmap_node,
>> +                evpn_static_entry_hash(&mac, &ip, vni));
>>  }
>>
>> -static struct evpn_static_fdb *
>> -evpn_static_fdb_find(const struct hmap *static_fdbs, struct eth_addr mac,
>> -                     struct in6_addr ip, uint32_t vni)
>> +static struct evpn_static_entry *
>> +evpn_static_entry_find(const struct hmap *static_entries, struct eth_addr
>> mac,
>> +                       struct in6_addr ip, uint32_t vni)
>>  {
>> -    uint32_t hash = evpn_static_fdb_hash(&mac, &ip, vni);
>> -
>> -    struct evpn_static_fdb *fdb;
>> -    HMAP_FOR_EACH_WITH_HASH (fdb, hmap_node, hash, static_fdbs) {
>> -        if (eth_addr_equals(fdb->mac, mac) &&
>> -            ipv6_addr_equals(&fdb->ip, &ip) &&
>> -            fdb->vni == vni) {
>> -            return fdb;
>> +    uint32_t hash = evpn_static_entry_hash(&mac, &ip, vni);
>> +
>> +    struct evpn_static_entry *e;
>> +    HMAP_FOR_EACH_WITH_HASH (e, hmap_node, hash, static_entries) {
>> +        if (eth_addr_equals(e->mac, mac) &&
>> +            ipv6_addr_equals(&e->ip, &ip) &&
>> +            e->vni == vni) {
>> +            return e;
>>          }
>>      }
>>
>> @@ -241,8 +262,8 @@ evpn_static_fdb_find(const struct hmap *static_fdbs,
>> struct eth_addr mac,
>>  }
>>
>>  static uint32_t
>> -evpn_static_fdb_hash(const struct eth_addr *mac, const struct in6_addr
>> *ip,
>> -                     uint32_t vni)
>> +evpn_static_entry_hash(const struct eth_addr *mac, const struct in6_addr
>> *ip,
>> +                       uint32_t vni)
>>  {
>>      uint32_t hash = 0;
>>      hash = hash_bytes(mac, sizeof *mac, hash);
>> diff --git a/controller/neighbor-exchange.h
>> b/controller/neighbor-exchange.h
>> index 6122d8dee5..281733d905 100644
>> --- a/controller/neighbor-exchange.h
>> +++ b/controller/neighbor-exchange.h
>> @@ -34,8 +34,12 @@ struct neighbor_exchange_ctx_out {
>>      struct hmap neighbor_table_watches;
>>      /* Contains 'struct evpn_remote_vtep'. */
>>      struct hmap *remote_vteps;
>> -    /* Contains 'struct evpn_static_fdb'. */
>> +    /* Contains 'struct evpn_static_entry', remote FDB entries learnt
>> through
>> +     * EVPN. */
>>      struct hmap *static_fdbs;
>> +    /* Contains 'struct evpn_static_entry', remote ARP entries learnt
>> through
>> +     * EVPN. */
>> +    struct hmap *static_arps;
>>  };
>>
>>  struct evpn_remote_vtep {
>> @@ -48,11 +52,11 @@ struct evpn_remote_vtep {
>>      uint32_t vni;
>>  };
>>
>> -struct evpn_static_fdb {
>> +struct evpn_static_entry {
>>      struct hmap_node hmap_node;
>>      /* MAC address of the remote workload. */
>>      struct eth_addr mac;
>> -    /* Destination ip of the remote tunnel. */
>> +    /* Destination ip of the remote tunnel or remote IP. */
>>      struct in6_addr ip;
>>      /* VNI of the VTEP. */
>>      uint32_t vni;
>> @@ -64,6 +68,6 @@ int neighbor_exchange_status_run(void);
>>  void evpn_remote_vteps_clear(struct hmap *remote_vteps);
>>  void evpn_remote_vtep_list(struct unixctl_conn *, int argc,
>>                             const char *argv[], void *data_);
>> -void evpn_static_fdbs_clear(struct hmap *static_fdbs);
>> +void evpn_static_entries_clear(struct hmap *static_entries);
>>
>>  #endif  /* NEIGHBOR_EXCHANGE_H */
>> diff --git a/controller/neighbor-of.h b/controller/neighbor-of.h
>> index 874200e8be..4138d1577e 100644
>> --- a/controller/neighbor-of.h
>> +++ b/controller/neighbor-of.h
>> @@ -23,15 +23,20 @@
>>
>>  /* Priorities of ovn-controller generated flows for various types of MAC
>>   * Bindings in different situations.  Valid preference orders, based on
>> - * the SB.Static_MAC_Binding.override_dynamic_mac value are:
>> + * the "dynamic-routing-arp-prefer-local" logical switch config and the
>> + * SB.Static_MAC_Binding.override_dynamic_mac value are:
>>   *
>> - * - static-mac-binding < dynamic-mac-binding
>> - * - dynamic-mac-binding < static-mac-binding
>> + * - EVPN-learned < static-mac-binding < dynamic-mac-binding
>> + * - EVPN-learned < dynamic-mac-binding < static-mac-binding
>> + * - static-mac-binding < dynamic-mac-binding < EVPN-learned
>> + * - dynamic-mac-binding < static-mac-binding < EVPN-learned
>>   */
>>  enum neigh_of_rule_prio {
>> +    NEIGH_OF_EVPN_MAC_BINDING_LOW_PRIO    = 20,
>>      NEIGH_OF_STATIC_MAC_BINDING_LOW_PRIO  = 50,
>>      NEIGH_OF_DYNAMIC_MAC_BINDING_PRIO     = 100,
>>      NEIGH_OF_STATIC_MAC_BINDING_HIGH_PRIO = 150,
>> +    NEIGH_OF_EVPN_MAC_BINDING_HIGH_PRIO   = 200,
>>  };
>>
>>  void
>> diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
>> index c8b76feb52..09bb83e3a2 100644
>> --- a/controller/ovn-controller.c
>> +++ b/controller/ovn-controller.c
>> @@ -98,6 +98,7 @@
>>  #include "neighbor.h"
>>  #include "neighbor-exchange.h"
>>  #include "neighbor-table-notify.h"
>> +#include "evpn-arp.h"
>>  #include "evpn-binding.h"
>>  #include "evpn-fdb.h"
>>
>> @@ -4589,6 +4590,15 @@ struct ed_type_evpn_fdb {
>>      struct uuidset removed_fdbs;
>>  };
>>
>> +struct ed_type_evpn_arp {
>> +    /* Contains 'struct evpn_arp'. */
>> +    struct hmap arps;
>> +    /* Contains pointers to 'struct evpn_arp'. */
>> +    struct hmapx updated_arps;
>> +    /* Contains 'flow_uuid' from removed 'struct evpn_arps'. */
>> +    struct uuidset removed_arps;
>> +};
>> +
>>  static void init_physical_ctx(struct engine_node *node,
>>                                struct ed_type_runtime_data *rt_data,
>>                                struct ed_type_non_vif_data *non_vif_data,
>> @@ -4649,6 +4659,9 @@ static void init_physical_ctx(struct engine_node
>> *node,
>>      struct ed_type_evpn_fdb *efdb_data =
>>          engine_get_input_data("evpn_fdb", node);
>>
>> +    struct ed_type_evpn_arp *earp_data =
>> +        engine_get_input_data("evpn_arp", node);
>> +
>>      parse_encap_ips(ovs_table, &p_ctx->n_encap_ips, &p_ctx->encap_ips);
>>      p_ctx->sbrec_port_binding_by_name = sbrec_port_binding_by_name;
>>      p_ctx->sbrec_port_binding_by_datapath =
>> sbrec_port_binding_by_datapath;
>> @@ -4669,6 +4682,7 @@ static void init_physical_ctx(struct engine_node
>> *node,
>>      p_ctx->evpn_bindings = &eb_data->bindings;
>>      p_ctx->evpn_multicast_groups = &eb_data->multicast_groups;
>>      p_ctx->evpn_fdbs = &efdb_data->fdbs;
>> +    p_ctx->evpn_arps = &earp_data->arps;
>>
>>      struct controller_engine_ctx *ctrl_ctx =
>> engine_get_context()->client_ctx;
>>      p_ctx->if_mgr = ctrl_ctx->if_mgr;
>> @@ -4993,6 +5007,22 @@ pflow_output_fdb_handler(struct engine_node *node,
>> void *data)
>>      return EN_HANDLED_UPDATED;
>>  }
>>
>> +static enum engine_input_handler_result
>> +pflow_output_arp_handler(struct engine_node *node, void *data)
>> +{
>> +    struct ed_type_pflow_output *pfo = data;
>> +    struct ed_type_runtime_data *rt_data =
>> +        engine_get_input_data("runtime_data", node);
>> +    struct ed_type_evpn_arp *ea_data =
>> +        engine_get_input_data("evpn_arp", node);
>> +
>> +    physical_handle_evpn_arp_changes(&rt_data->local_datapaths,
>> +                                     &pfo->flow_table,
>> +                                     &ea_data->updated_arps,
>> +                                     &ea_data->removed_arps);
>> +    return EN_HANDLED_UPDATED;
>> +}
>> +
>>  static void *
>>  en_controller_output_init(struct engine_node *node OVS_UNUSED,
>>                            struct engine_arg *arg OVS_UNUSED)
>> @@ -6040,8 +6070,12 @@ en_neighbor_table_notify_run(struct engine_node
>> *node OVS_UNUSED,
>>  struct ed_type_neighbor_exchange {
>>      /* Contains 'struct evpn_remote_vtep'. */
>>      struct hmap remote_vteps;
>> -    /* Contains 'struct evpn_static_fdb'. */
>> +    /* Contains 'struct evpn_static_entry', remote FDB entries learnt
>> through
>> +     * EVPN. */
>>      struct hmap static_fdbs;
>> +    /* Contains 'struct evpn_static_entry', remote ARP entries learnt
>> through
>> +     * EVPN. */
>> +    struct hmap static_arps;
>>  };
>>
>>  static void *
>> @@ -6052,6 +6086,7 @@ en_neighbor_exchange_init(struct engine_node *node
>> OVS_UNUSED,
>>      *data = (struct ed_type_neighbor_exchange) {
>>          .remote_vteps = HMAP_INITIALIZER(&data->remote_vteps),
>>          .static_fdbs = HMAP_INITIALIZER(&data->static_fdbs),
>> +        .static_arps = HMAP_INITIALIZER(&data->static_arps),
>>      };
>>
>>      return data;
>> @@ -6062,9 +6097,11 @@ en_neighbor_exchange_cleanup(void *data_)
>>  {
>>      struct ed_type_neighbor_exchange *data = data_;
>>      evpn_remote_vteps_clear(&data->remote_vteps);
>> -    evpn_static_fdbs_clear(&data->static_fdbs);
>> +    evpn_static_entries_clear(&data->static_fdbs);
>> +    evpn_static_entries_clear(&data->static_arps);
>>      hmap_destroy(&data->remote_vteps);
>>      hmap_destroy(&data->static_fdbs);
>> +    hmap_destroy(&data->static_arps);
>>  }
>>
>>  static enum engine_node_state
>> @@ -6075,7 +6112,8 @@ en_neighbor_exchange_run(struct engine_node *node,
>> void *data_)
>>          engine_get_input_data("neighbor", node);
>>
>>      evpn_remote_vteps_clear(&data->remote_vteps);
>> -    evpn_static_fdbs_clear(&data->static_fdbs);
>> +    evpn_static_entries_clear(&data->static_fdbs);
>> +    evpn_static_entries_clear(&data->static_arps);
>>
>>      struct neighbor_exchange_ctx_in n_ctx_in = {
>>          .monitored_interfaces = &neighbor_data->monitored_interfaces,
>> @@ -6085,6 +6123,7 @@ en_neighbor_exchange_run(struct engine_node *node,
>> void *data_)
>>              HMAP_INITIALIZER(&n_ctx_out.neighbor_table_watches),
>>          .remote_vteps = &data->remote_vteps,
>>          .static_fdbs = &data->static_fdbs,
>> +        .static_arps = &data->static_arps,
>>      };
>>
>>      neighbor_exchange_run(&n_ctx_in, &n_ctx_out);
>> @@ -6355,6 +6394,81 @@ evpn_fdb_vtep_binding_handler(struct engine_node
>> *node, void *data OVS_UNUSED)
>>      return EN_UNHANDLED;
>>  }
>>
>> +static void *
>> +en_evpn_arp_init(struct engine_node *node OVS_UNUSED,
>> +                 struct engine_arg *arg OVS_UNUSED)
>> +{
>> +    struct ed_type_evpn_arp *data = xmalloc(sizeof *data);
>> +    *data = (struct ed_type_evpn_arp) {
>> +        .arps = HMAP_INITIALIZER(&data->arps),
>> +        .updated_arps = HMAPX_INITIALIZER(&data->updated_arps),
>> +        .removed_arps = UUIDSET_INITIALIZER(&data->removed_arps),
>> +    };
>> +
>> +    return data;
>> +}
>> +
>> +static void
>> +en_evpn_arp_clear_tracked_data(void *data_)
>> +{
>> +    struct ed_type_evpn_arp *data = data_;
>> +    hmapx_clear(&data->updated_arps);
>> +    uuidset_clear(&data->removed_arps);
>> +}
>> +
>> +static void
>> +en_evpn_arp_cleanup(void *data_)
>> +{
>> +    struct ed_type_evpn_arp *data = data_;
>> +    evpn_arps_destroy(&data->arps);
>> +    hmapx_destroy(&data->updated_arps);
>> +    uuidset_destroy(&data->removed_arps);
>> +}
>> +
>> +static enum engine_node_state
>> +en_evpn_arp_run(struct engine_node *node, void *data_)
>> +{
>> +    struct ed_type_evpn_arp *data = data_;
>> +    const struct ed_type_neighbor_exchange *ne_data =
>> +        engine_get_input_data("neighbor_exchange", node);
>> +    const struct ed_type_evpn_vtep_binding *eb_data =
>> +        engine_get_input_data("evpn_vtep_binding", node);
>> +
>> +    struct evpn_arp_ctx_in f_ctx_in = {
>> +        .datapaths = &eb_data->datapaths,
>> +        .static_arps = &ne_data->static_arps,
>> +    };
>> +
>> +    struct evpn_arp_ctx_out f_ctx_out = {
>> +        .arps = &data->arps,
>> +        .updated_arps = &data->updated_arps,
>> +        .removed_arps = &data->removed_arps,
>> +    };
>> +
>> +    evpn_arp_run(&f_ctx_in, &f_ctx_out);
>> +
>> +    if (hmapx_count(&data->updated_arps) ||
>> +        uuidset_count(&data->removed_arps)) {
>> +        return EN_UPDATED;
>> +    }
>> +
>> +    return EN_UNCHANGED;
>> +}
>> +
>> +static enum engine_input_handler_result
>> +evpn_arp_vtep_binding_handler(struct engine_node *node, void *data
>> OVS_UNUSED)
>> +{
>> +    const struct ed_type_evpn_vtep_binding *eb_data =
>> +        engine_get_input_data("evpn_vtep_binding", node);
>> +
>> +    if (hmapx_is_empty(&eb_data->updated_bindings) &&
>> +        uuidset_is_empty(&eb_data->removed_bindings)) {
>> +        return EN_HANDLED_UNCHANGED;
>> +    }
>> +
>> +    return EN_UNHANDLED;
>> +}
>> +
>>  /* Returns false if the northd internal version stored in SB_Global
>>   * and ovn-controller internal version don't match.
>>   */
>> @@ -6681,6 +6795,7 @@ main(int argc, char *argv[])
>>      ENGINE_NODE(neighbor_exchange_status);
>>      ENGINE_NODE(evpn_vtep_binding, CLEAR_TRACKED_DATA);
>>      ENGINE_NODE(evpn_fdb, CLEAR_TRACKED_DATA);
>> +    ENGINE_NODE(evpn_arp, CLEAR_TRACKED_DATA);
>>
>>  #define SB_NODE(NAME) ENGINE_NODE_SB(NAME);
>>      SB_NODES
>> @@ -6938,10 +7053,16 @@ main(int argc, char *argv[])
>>      engine_add_input(&en_evpn_fdb, &en_evpn_vtep_binding,
>>                       evpn_fdb_vtep_binding_handler);
>>
>> +    engine_add_input(&en_evpn_arp, &en_neighbor_exchange, NULL);
>> +    engine_add_input(&en_evpn_arp, &en_evpn_vtep_binding,
>> +                     evpn_arp_vtep_binding_handler);
>> +
>>      engine_add_input(&en_pflow_output, &en_evpn_vtep_binding,
>>                       pflow_output_evpn_binding_handler);
>>      engine_add_input(&en_pflow_output, &en_evpn_fdb,
>>                       pflow_output_fdb_handler);
>> +    engine_add_input(&en_pflow_output, &en_evpn_arp,
>> +                     pflow_output_arp_handler);
>>
>>      engine_add_input(&en_controller_output, &en_dns_cache,
>>                       NULL);
>> @@ -7025,6 +7146,8 @@ main(int argc, char *argv[])
>>          engine_get_internal_data(&en_evpn_vtep_binding);
>>      struct ed_type_evpn_fdb *efdb_data =
>>          engine_get_internal_data(&en_evpn_fdb);
>> +    struct ed_type_evpn_arp *earp_data =
>> +        engine_get_internal_data(&en_evpn_arp);
>>
>>      ofctrl_init(&lflow_output_data->group_table,
>>                  &lflow_output_data->meter_table);
>> @@ -7053,6 +7176,9 @@ main(int argc, char *argv[])
>>      unixctl_command_register("evpn/vtep-fdb-list", "", 0, 0,
>>                               evpn_fdb_list,
>>                               &efdb_data->fdbs);
>> +    unixctl_command_register("evpn/vtep-arp-list", "", 0, 0,
>> +                             evpn_arp_list,
>> +                             &earp_data->arps);
>>
>>      struct pending_pkt pending_pkt = { .conn = NULL };
>>      unixctl_command_register("inject-pkt", "MICROFLOW", 1, 1, inject_pkt,
>> diff --git a/controller/physical.c b/controller/physical.c
>> index 9ca535a6cd..23a579f857 100644
>> --- a/controller/physical.c
>> +++ b/controller/physical.c
>> @@ -20,6 +20,7 @@
>>  #include "ct-zone.h"
>>  #include "encaps.h"
>>  #include "evpn-binding.h"
>> +#include "evpn-arp.h"
>>  #include "evpn-fdb.h"
>>  #include "flow.h"
>>  #include "ha-chassis.h"
>> @@ -54,6 +55,7 @@
>>  #include "util.h"
>>  #include "vswitch-idl.h"
>>  #include "hmapx.h"
>> +#include "neighbor-of.h"
>>
>>  VLOG_DEFINE_THIS_MODULE(physical);
>>
>> @@ -2778,6 +2780,28 @@ physical_consider_evpn_fdb(const struct evpn_fdb
>> *fdb,
>>                      match, ofpacts, &fdb->flow_uuid);
>>  }
>>
>> +static void
>> +physical_consider_evpn_arp(const struct hmap *local_datapaths,
>> +                           const struct evpn_arp *arp,
>> +                           struct ovn_desired_flow_table *flow_table)
>> +{
>> +    /* Walk connected OVN routers and install neighbor flows for the ARPs
>> +     * learned on EVPN datapaths.*/
>> +    const struct peer_ports *peers;
>> +    VECTOR_FOR_EACH_PTR (&arp->ldp->peer_ports, peers) {
>> +        const struct sbrec_port_binding *remote_pb = peers->remote;
>> +        struct local_datapath *peer_ld =
>> +            get_local_datapath(local_datapaths,
>> +                               remote_pb->datapath->tunnel_key);
>> +        if (!peer_ld || peer_ld->is_switch) {
>> +            continue;
>> +        }
>> +
>> +        consider_neighbor_flow(remote_pb, &arp->flow_uuid, &arp->ip,
>> arp->mac,
>> +                               flow_table, arp->priority, false);
>> +    }
>> +}
>> +
>>  static void
>>  physical_eval_evpn_flows(const struct physical_ctx *ctx,
>>                           struct ofpbuf *ofpacts,
>> @@ -2785,7 +2809,8 @@ physical_eval_evpn_flows(const struct physical_ctx
>> *ctx,
>>  {
>>      if (hmap_is_empty(ctx->evpn_bindings) &&
>>          hmap_is_empty(ctx->evpn_multicast_groups) &&
>> -        hmap_is_empty(ctx->evpn_fdbs)) {
>> +        hmap_is_empty(ctx->evpn_fdbs) &&
>> +        hmap_is_empty(ctx->evpn_arps)) {
>>          return;
>>      }
>>
>> @@ -2818,6 +2843,11 @@ physical_eval_evpn_flows(const struct physical_ctx
>> *ctx,
>>      HMAP_FOR_EACH (fdb, hmap_node, ctx->evpn_fdbs) {
>>          physical_consider_evpn_fdb(fdb, ofpacts, &match, flow_table);
>>      }
>> +
>> +    const struct evpn_arp *arp;
>> +    HMAP_FOR_EACH (arp, hmap_node, ctx->evpn_arps) {
>> +        physical_consider_evpn_arp(ctx->local_datapaths, arp, flow_table);
>> +    }
>>  }
>>
>>  static void
>> @@ -3001,6 +3031,27 @@ physical_handle_evpn_fdb_changes(struct
>> ovn_desired_flow_table *flow_table,
>>      }
>>  }
>>
>> +void
>> +physical_handle_evpn_arp_changes(const struct hmap *local_datapaths,
>> +                                 struct ovn_desired_flow_table
>> *flow_table,
>> +                                 const struct hmapx *updated_arps,
>> +                                 const struct uuidset *removed_arps)
>> +{
>> +
>> +    const struct hmapx_node *node;
>> +    HMAPX_FOR_EACH (node, updated_arps) {
>> +        const struct evpn_arp *arp = node->data;
>> +
>> +        ofctrl_remove_flows(flow_table, &arp->flow_uuid);
>> +        physical_consider_evpn_arp(local_datapaths, arp, flow_table);
>> +    }
>> +
>> +    const struct uuidset_node *uuidset_node;
>> +    UUIDSET_FOR_EACH (uuidset_node, removed_arps) {
>> +        ofctrl_remove_flows(flow_table, &uuidset_node->uuid);
>> +    }
>> +}
>> +
>>  void
>>  physical_run(struct physical_ctx *p_ctx,
>>               struct ovn_desired_flow_table *flow_table)
>> diff --git a/controller/physical.h b/controller/physical.h
>> index 37be46380e..0dc544823a 100644
>> --- a/controller/physical.h
>> +++ b/controller/physical.h
>> @@ -72,6 +72,7 @@ struct physical_ctx {
>>      const struct hmap *evpn_bindings;
>>      const struct hmap *evpn_multicast_groups;
>>      const struct hmap *evpn_fdbs;
>> +    const struct hmap *evpn_arps;
>>
>>      /* Set of port binding names that have been already reprocessed during
>>       * the I-P run. */
>> @@ -99,4 +100,8 @@ void physical_handle_evpn_binding_changes(
>>  void physical_handle_evpn_fdb_changes(struct ovn_desired_flow_table *,
>>                                        const struct hmapx *updated_fdbs,
>>                                        const struct uuidset *removed_fdbs);
>> +void physical_handle_evpn_arp_changes(const struct hmap *local_datapaths,
>> +                                      struct ovn_desired_flow_table *,
>> +                                      const struct hmapx *updated_arps,
>> +                                      const struct uuidset *removed_arps);
>>  #endif /* controller/physical.h */
>> diff --git a/northd/en-datapath-logical-switch.c
>> b/northd/en-datapath-logical-switch.c
>> index 0527239cec..c3fefd100a 100644
>> --- a/northd/en-datapath-logical-switch.c
>> +++ b/northd/en-datapath-logical-switch.c
>> @@ -98,6 +98,13 @@ gather_external_ids(const struct nbrec_logical_switch
>> *nbs,
>>          smap_add(external_ids, "dynamic-routing-redistribute",
>> redistribute);
>>      }
>>
>> +    const char *prefer_evpn_arp_local =
>> +        smap_get(&nbs->other_config, "dynamic-routing-arp-prefer-local");
>> +    if (prefer_evpn_arp_local) {
>> +        smap_add(external_ids, "dynamic-routing-arp-prefer-local",
>> +                 prefer_evpn_arp_local);
>> +    }
>> +
>>      /* For backwards-compatibility, also store the NB UUID in
>>       * external-ids:logical-switch. This is useful if ovn-controller
>>       * has not updated and expects this to be where to find the
>> diff --git a/ovn-nb.xml b/ovn-nb.xml
>> index ea7164e6cf..97da0177ed 100644
>> --- a/ovn-nb.xml
>> +++ b/ovn-nb.xml
>> @@ -908,6 +908,29 @@
>>          </p>
>>        </column>
>>
>> +      <column name="other_config" key="dynamic-routing-arp-prefer-local"
>> +              type='{"type": "boolean"}'>
>> +        <p>
>> +          This option defines the preference of ARP/ND lookup.  If set to
>> +          true OVN routers connected to EVPN Logical Switches on which
>> remote
>> +          neighbor entries have been learned (Type-2 MAC+IP EVPN routes)
>> will
>> +          give precedence to any ARP/ND entries they might have in the SB
>> +          <code>Mac_Binding</code> table before trying to resolve the MAC
>> +          address via the <code>ovn-controller</code> local EVPN ARP/ND
>> cache.
>> +          The option defaults to false.
>> +        </p>
>> +
>> +        <p>
>> +          Only relevant if <ref column="other_config"
>> key="dynamic-routing-vni"
>> +                                table="Logical_switch"/> is set to valid
>> VNI.
>> +        </p>
>> +
>> +        <p>
>> +          NOTE: this feature is experimental and may be subject to
>> +          removal/change in the future.
>> +        </p>
>> +      </column>
>> +
>>        <column name="other_config" key="dynamic-routing-redistribute"
>>                type='{"type": "string"}'>
>>          <p>
>> diff --git a/tests/multinode.at b/tests/multinode.at
>> index 959c5ba8a8..b5d3df5b33 100644
>> --- a/tests/multinode.at
>> +++ b/tests/multinode.at
>> @@ -3667,6 +3667,7 @@ check multinode_nbctl set logical_switch ls       \
>>      other_config:dynamic-routing-vni=$vni         \
>>      other_config:dynamic-routing-redistribute=fdb
>>  check multinode_nbctl --wait=hv sync
>> +dp_key=$(m_fetch_column Datapath_Binding tunnel_key external_ids:name=ls)
>>
>>  OVS_WAIT_UNTIL([m_as ovn-gw-1 bridge fdb | grep vxlan-10 | grep -q
>> "00:00:00:00:00:00"])
>>  OVS_WAIT_UNTIL([m_as ovn-gw-2 bridge fdb | grep vxlan-10 | grep -q
>> "00:00:00:00:00:00"])
>> @@ -3680,23 +3681,33 @@ IP: $ext_bgp_ip_gw2, port: 4789, vni: 10
>>  ])
>>
>>  AS_BOX([Check traffic to "fabric" hosts - simulate external workloads])
>> +check m_as ovn-gw-1 ip netns add fabric_workload
>> +on_exit "m_as ovn-gw-1 ip netns del fabric_workload"
>>  check m_as ovn-gw-1 ip link add evpn_host type veth peer evpn_host_peer
>>  on_exit "m_as ovn-gw-1 ip link del evpn_host"
>>  check m_as ovn-gw-1 ip link set netns frr-ns evpn_host_peer
>>  check m_as ovn-gw-1 ip netns exec frr-ns ip link set evpn_host_peer
>> master br-10
>>  check m_as ovn-gw-1 ip netns exec frr-ns ip link set evpn_host_peer up
>> -check m_as ovn-gw-1 ip link set evpn_host addr 00:00:00:00:01:00
>> -check m_as ovn-gw-1 ip addr add dev evpn_host 10.0.0.41/24
>> -check m_as ovn-gw-1 ip link set evpn_host up
>> -
>> +check m_as ovn-gw-1 ip link set netns fabric_workload evpn_host
>> +check m_as ovn-gw-1 ip netns exec fabric_workload ip link set evpn_host
>> addr 00:00:00:00:01:00
>> +check m_as ovn-gw-1 ip netns exec fabric_workload ip addr add dev
>> evpn_host 10.0.0.41/24
>> +check m_as ovn-gw-1 ip netns exec fabric_workload ip link set evpn_host up
>> +check m_as ovn-gw-1 ip netns exec fabric_workload ip r a default via
>> 10.0.0.1
>> +check m_as ovn-gw-1 ip netns exec frr-ns ip a a dev br-10 10.0.0.81/24
>> +
>> +check m_as ovn-gw-2 ip netns add fabric_workload
>> +on_exit "m_as ovn-gw-2 ip netns del fabric_workload"
>>  check m_as ovn-gw-2 ip link add evpn_host type veth peer evpn_host_peer
>>  on_exit "m_as ovn-gw-2 ip link del evpn_host"
>>  check m_as ovn-gw-2 ip link set netns frr-ns evpn_host_peer
>>  check m_as ovn-gw-2 ip netns exec frr-ns ip link set evpn_host_peer
>> master br-10
>>  check m_as ovn-gw-2 ip netns exec frr-ns ip link set evpn_host_peer up
>> -check m_as ovn-gw-2 ip link set evpn_host addr 00:00:00:00:02:00
>> -check m_as ovn-gw-2 ip addr add dev evpn_host 10.0.0.42/24
>> -check m_as ovn-gw-2 ip link set evpn_host up
>> +check m_as ovn-gw-2 ip link set netns fabric_workload evpn_host
>> +check m_as ovn-gw-2 ip netns exec fabric_workload ip link set evpn_host
>> addr 00:00:00:00:02:00
>> +check m_as ovn-gw-2 ip netns exec fabric_workload ip addr add dev
>> evpn_host 10.0.0.42/24
>> +check m_as ovn-gw-2 ip netns exec fabric_workload ip link set evpn_host up
>> +check m_as ovn-gw-2 ip netns exec fabric_workload ip r a default via
>> 10.0.0.1
>> +check m_as ovn-gw-2 ip netns exec frr-ns ip a a dev br-10 10.0.0.82/24
>>
>>  AS_BOX([Checking EVPN MACs on External BGP host])
>>  OVS_WAIT_FOR_OUTPUT([m_as ovn-gw-1 ip netns exec frr-ns vtysh
>> --vty_socket /run/frr/frr-ns -c 'show evpn mac vni all'], [0], [dnl
>> @@ -3720,8 +3731,83 @@ MAC               Type   Flags Intf/Remote ES/VTEP
>>           VLAN  Seq #'s
>>  ])
>>
>>  AS_BOX([Check traffic to "fabric" hosts - ping from fabric])
>> -OVS_WAIT_UNTIL([m_as ovn-gw-1 ping -W 1 -c 1 10.0.0.11])
>> -OVS_WAIT_UNTIL([m_as ovn-gw-2 ping -W 1 -c 1 10.0.0.12])
>> +OVS_WAIT_UNTIL([m_as ovn-gw-1 ip netns exec fabric_workload ping -W 1 -c
>> 1 10.0.0.11])
>> +OVS_WAIT_UNTIL([m_as ovn-gw-2 ip netns exec fabric_workload ping -W 1 -c
>> 1 10.0.0.12])
>> +
>> +AS_BOX([Check type-2 MAC+IP EVPN route advertisements])
>> +# Ping from the frr-ns to the fabric workload so that its IP is learned on
>> +# the fabric EVPN peer (and advertised to OVN).
>> +OVS_WAIT_UNTIL([m_as ovn-gw-1 ip netns exec frr-ns ip vrf exec vrf-10
>> ping -W 1 -c 1 10.0.0.41])
>> +OVS_WAIT_UNTIL([m_as ovn-gw-2 ip netns exec frr-ns ip vrf exec vrf-10
>> ping -W 1 -c 1 10.0.0.42])
>> +
>> +# Check that OVN learned the ARPs.
>> +OVS_WAIT_FOR_OUTPUT_UNQUOTED([m_as ovn-gw-1 ovn-appctl evpn/vtep-arp-list
>> | cut -d',' -f2- | sort], [0], [dnl
>> + VNI: 10, MAC: 00:00:00:00:01:00, IP: 10.0.0.41, dp_key: $dp_key
>> +])
>> +OVS_WAIT_FOR_OUTPUT_UNQUOTED([m_as ovn-gw-2 ovn-appctl evpn/vtep-arp-list
>> | cut -d',' -f2- | sort], [0], [dnl
>> + VNI: 10, MAC: 00:00:00:00:02:00, IP: 10.0.0.42, dp_key: $dp_key
>> +])
>> +
>> +AS_BOX([Check that OVN routers used ARP entries learned through type-2
>> EVPN MAC+IP routes])
>> +# Add an OVN router with an internal switch and internal workload.
>> +check multinode_nbctl --wait=hv                               \
>> +    -- lr-add lr                                              \
>> +    -- lrp-add lr lr-ls 00:00:00:01:00:00 10.0.0.1/24         \
>> +    -- lrp-add lr lr-ls-int 00:00:00:02:00:00 20.0.0.1/24     \
>> +    -- lsp-add ls ls-lr                                       \
>> +    -- lsp-set-type ls-lr router                              \
>> +    -- lsp-set-options ls-lr router-port=lr-ls                \
>> +    -- lsp-set-addresses ls-lr router                         \
>> +    -- ls-add ls-int                                          \
>> +    -- lsp-add ls-int ls-int-lr                               \
>> +    -- lsp-set-type ls-int-lr router                          \
>> +    -- lsp-set-options ls-int-lr router-port=lr-ls-int        \
>> +    -- lsp-set-addresses ls-int-lr router                     \
>> +    -- lsp-add ls-int w-int1                                  \
>> +    -- lsp-set-addresses w-int1 "00:00:00:02:00:01 20.0.0.11" \
>> +    -- lsp-add ls-int w-int2                                  \
>> +    -- lsp-set-addresses w-int2 "00:00:00:02:00:02 20.0.0.12"
>> +
>> +rtr_dp_key=$(m_fetch_column Datapath tunnel_key external_ids:name=lr)
>> +rtr_port_key=$(m_fetch_column Port_Binding tunnel_key logical_port=lr-ls)
>> +
>> +check m_as ovn-gw-1 /data/create_fake_vm.sh w-int1 w-int1
>> 00:00:00:02:00:01 1500 20.0.0.11 24 20.0.0.1 2000::11/64 2000::1
>> +check m_as ovn-gw-2 /data/create_fake_vm.sh w-int2 w-int2
>> 00:00:00:02:00:02 1500 20.0.0.12 24 20.0.0.1 2000::12/64 2000::1
>> +m_wait_for_ports_up
>> +
>> +# Check that flows are created for the type-2 EVPN MAC+IP routes, in the
>> +# router pipeline.
>> +AT_CHECK_UNQUOTED([m_as ovn-gw-1 ovs-ofctl dump-flows br-int
>> table=OFTABLE_MAC_BINDING | grep priority | \
>> +                   awk '{print $7, $8}' | sort], [0], [dnl
>> +priority=100,reg0=0xa00000b,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
>> actions=mod_dl_dst:00:00:00:00:00:01,load:0x1->NXM_NX_REG10[[6]]
>> +priority=100,reg0=0xa00000c,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
>> actions=mod_dl_dst:00:00:00:00:00:02,load:0x1->NXM_NX_REG10[[6]]
>> +priority=200,reg0=0xa000029,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
>> actions=mod_dl_dst:00:00:00:00:01:00,load:0x1->NXM_NX_REG10[[6]]
>> +])
>> +
>> +AT_CHECK_UNQUOTED([m_as ovn-gw-1 ovs-ofctl dump-flows br-int
>> table=OFTABLE_MAC_LOOKUP | grep priority | \
>> +                   awk '{print $7, $8}' | sort], [0], [dnl
>> +priority=100,arp,reg0=0xa00000b,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=00:00:00:00:00:01
>> actions=load:0x1->NXM_NX_REG10[[6]]
>> +priority=100,arp,reg0=0xa00000c,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=00:00:00:00:00:02
>> actions=load:0x1->NXM_NX_REG10[[6]]
>> +priority=200,arp,reg0=0xa000029,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=00:00:00:00:01:00
>> actions=load:0x1->NXM_NX_REG10[[6]]
>> +])
>> +
>> +AT_CHECK_UNQUOTED([m_as ovn-gw-2 ovs-ofctl dump-flows br-int
>> table=OFTABLE_MAC_BINDING | grep priority | \
>> +                   awk '{print $7, $8}' | sort], [0], [dnl
>> +priority=100,reg0=0xa00000b,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
>> actions=mod_dl_dst:00:00:00:00:00:01,load:0x1->NXM_NX_REG10[[6]]
>> +priority=100,reg0=0xa00000c,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
>> actions=mod_dl_dst:00:00:00:00:00:02,load:0x1->NXM_NX_REG10[[6]]
>> +priority=200,reg0=0xa00002a,reg15=0x$rtr_port_key,metadata=0x$rtr_dp_key
>> actions=mod_dl_dst:00:00:00:00:02:00,load:0x1->NXM_NX_REG10[[6]]
>> +])
>> +
>> +AT_CHECK_UNQUOTED([m_as ovn-gw-2 ovs-ofctl dump-flows br-int
>> table=OFTABLE_MAC_LOOKUP | grep priority | \
>> +                   awk '{print $7, $8}' | sort], [0], [dnl
>> +priority=100,arp,reg0=0xa00000b,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=00:00:00:00:00:01
>> actions=load:0x1->NXM_NX_REG10[[6]]
>> +priority=100,arp,reg0=0xa00000c,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=00:00:00:00:00:02
>> actions=load:0x1->NXM_NX_REG10[[6]]
>> +priority=200,arp,reg0=0xa00002a,reg14=0x$rtr_port_key,metadata=0x$rtr_dp_key,dl_src=00:00:00:00:02:00
>> actions=load:0x1->NXM_NX_REG10[[6]]
>> +])
>> +
>> +AS_BOX([Check traffic to "fabric" hosts - ping from internal hosts])
>> +OVS_WAIT_UNTIL([m_as ovn-gw-1 ip netns exec w-int1 ping -W 1 -c 1
>> 10.0.0.41])
>> +OVS_WAIT_UNTIL([m_as ovn-gw-2 ip netns exec w-int2 ping -W 1 -c 1
>> 10.0.0.42])
>>
>>  # Remove "workloads" (VIF LSPs) on both chassis.
>>  check multinode_nbctl --wait=hv lsp-del w1 -- lsp-del w2
>> diff --git a/tests/system-ovn.at b/tests/system-ovn.at
>> index 85e432e75d..4737a804b3 100644
>> --- a/tests/system-ovn.at
>> +++ b/tests/system-ovn.at
>> @@ -18337,6 +18337,8 @@ on_exit "ip link del lo-$vni"
>>  check ip link set lo-$vni master br-$vni
>>  check ip link set lo-$vni 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)
>> @@ -18450,6 +18452,8 @@ 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([bridge fdb show | grep "lo-10" | grep
>> "f0:00:0f:16:01" | sort], [0], [dnl
>>  f0:00:0f:16:01:10 dev lo-10 master br-10 static
>> @@ -18468,6 +18472,149 @@ check ovn-nbctl --wait=hv remove logical_switch
>> ls-evpn other_config dynamic-rou
>>  OVS_WAIT_FOR_OUTPUT([bridge fdb show | grep "lo-10" | grep
>> "f0:00:0f:16:01" | sort], [0], [dnl
>>  ])
>>
>> +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 \
>> +    -- lsp-add ls-evpn ls-evpn-lr                            \
>> +    -- lsp-set-type ls-evpn-lr router                        \
>> +    -- lsp-set-options ls-evpn-lr router-port=lr-ls-evpn     \
>> +    -- lsp-set-addresses ls-evpn-lr router
>> +
>> +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-10 172.16.1.50 lladdr f0:00:0f:16:10:50 nud
>> noarp extern_learn
>> +check ip neigh add dev br-10 172.16.1.60 lladdr f0:00:0f:16:10:60 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:60, IP: 172.16.1.60, 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]]
>> +])
>> +
>> +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]]
>> +])
>> +
>> +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]]
>> +])
>> +
>> +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]]
>> +])
>> +
>> +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-10 172.16.1.50
>> +check ip neigh del dev br-10 172.16.1.60
>> +
>> +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-10 172.16.1.50 lladdr f0:00:0f:16:10:50 nud
>> noarp extern_learn
>> +check ip neigh add dev br-10 172.16.1.60 lladdr f0:00:0f:16:10:60 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:60, IP: 172.16.1.60, 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]]
>> +])
>> +
>> +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]]
>> +])
>> +
>> +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:60, IP: 172.16.1.60, 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
>> +])
>> +
>>  OVN_CLEANUP_CONTROLLER([hv1])
>>
>>  as ovn-sb
>> --
>> 2.51.0
>>
>>
> With that addressed:
> Acked-by: Ales Musil <[email protected]>
> 

I applied the patch to main with the following incremental squashed in:

diff --git a/controller/evpn-arp.c b/controller/evpn-arp.c
index f73e09504a..f4c3514f4e 100644
--- a/controller/evpn-arp.c
+++ b/controller/evpn-arp.c
@@ -35,6 +35,8 @@ static struct evpn_arp *evpn_arp_find(const struct hmap 
*evpn_arps,
                                       struct eth_addr,
                                       const struct in6_addr *,
                                       uint32_t vni);
+static uint32_t evpn_arp_hash(const struct eth_addr *, const struct in6_addr *,
+                              uint32_t vni);
 
 void
 evpn_arp_run(const struct evpn_arp_ctx_in *arp_ctx_in,
@@ -150,8 +152,7 @@ evpn_arp_add(struct hmap *evpn_arps, struct eth_addr mac,
         .vni = vni,
     };
 
-    uint32_t hash = hash_bytes(&mac, sizeof mac, 0);
-    hmap_insert(evpn_arps, &arp->hmap_node, hash);
+    hmap_insert(evpn_arps, &arp->hmap_node, evpn_arp_hash(&mac, ip, vni));
 
     return arp;
 }
@@ -160,7 +161,7 @@ static struct evpn_arp *
 evpn_arp_find(const struct hmap *evpn_arps, struct eth_addr mac,
               const struct in6_addr *ip, uint32_t vni)
 {
-    uint32_t hash = hash_bytes(&mac, sizeof mac, 0);
+    uint32_t hash = evpn_arp_hash(&mac, ip, vni);
 
     struct evpn_arp *arp;
     HMAP_FOR_EACH_WITH_HASH (arp, hmap_node, hash, evpn_arps) {
@@ -172,3 +173,15 @@ evpn_arp_find(const struct hmap *evpn_arps, struct 
eth_addr mac,
 
     return NULL;
 }
+
+static uint32_t
+evpn_arp_hash(const struct eth_addr *mac, const struct in6_addr *ip,
+              uint32_t vni)
+{
+    uint32_t hash = 0;
+    hash = hash_bytes(mac, sizeof *mac, hash);
+    hash = hash_add_in6_addr(hash, ip);
+    hash = hash_add(hash, vni);
+
+    return hash_finish(hash, 26);
+}
diff --git a/controller/neighbor-exchange.h b/controller/neighbor-exchange.h
index 281733d905..b4257f14c3 100644
--- a/controller/neighbor-exchange.h
+++ b/controller/neighbor-exchange.h
@@ -34,10 +34,10 @@ struct neighbor_exchange_ctx_out {
     struct hmap neighbor_table_watches;
     /* Contains 'struct evpn_remote_vtep'. */
     struct hmap *remote_vteps;
-    /* Contains 'struct evpn_static_entry', remote FDB entries learnt through
+    /* Contains 'struct evpn_static_entry', remote FDB entries learned through
      * EVPN. */
     struct hmap *static_fdbs;
-    /* Contains 'struct evpn_static_entry', remote ARP entries learnt through
+    /* Contains 'struct evpn_static_entry', remote ARP entries learned through
      * EVPN. */
     struct hmap *static_arps;
 };
diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
index 09bb83e3a2..ae76c8b224 100644
--- a/controller/ovn-controller.c
+++ b/controller/ovn-controller.c
@@ -6070,10 +6070,10 @@ en_neighbor_table_notify_run(struct engine_node *node 
OVS_UNUSED,
 struct ed_type_neighbor_exchange {
     /* Contains 'struct evpn_remote_vtep'. */
     struct hmap remote_vteps;
-    /* Contains 'struct evpn_static_entry', remote FDB entries learnt through
+    /* Contains 'struct evpn_static_entry', remote FDB entries learned through
      * EVPN. */
     struct hmap static_fdbs;
-    /* Contains 'struct evpn_static_entry', remote ARP entries learnt through
+    /* Contains 'struct evpn_static_entry', remote ARP entries learned through
      * EVPN. */
     struct hmap static_arps;
 };
---

Regards,
Dumitru

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

Reply via email to