public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/ovn] f44: Pick up all current bug fixes from upstream branch-26.03.
@ 2026-06-04  9:57 Dumitru Ceara
  0 siblings, 0 replies; only message in thread
From: Dumitru Ceara @ 2026-06-04  9:57 UTC (permalink / raw)
  To: git-commits

            A new commit has been pushed.

            Repo   : rpms/ovn
            Branch : f44
            Commit : 5052cd082422079dcdb24ab040d28ab12fc3dcd7
            Author : Dumitru Ceara <dceara@redhat.com>
            Date   : 2026-06-04T11:56:20+02:00
            Stats  : +5472/-3 in 2 file(s)
            URL    : https://src.fedoraproject.org/rpms/ovn/c/5052cd082422079dcdb24ab040d28ab12fc3dcd7?branch=f44

            Log:
            Pick up all current bug fixes from upstream branch-26.03.

Up to and including:
  9f04b8c5086a ("tests: Fix flaky "Loadbalancer add-route option" system test.").

Notable fixes:
  f49251b71531 ("northd: recompute on datapath index reuse")
  4230294124e0 ("mac-cache: Make sure we re-arp for proper IP for LRP with multiple IPs.")
  96860cc2c78c ("ovn-controller: Skip type-update check for new port bindings.")

Signed-off-by: Dumitru Ceara <dceara@redhat.com>

---
diff --git a/ovn.patch b/ovn.patch
index 0f73806..c010e2d 100644
--- a/ovn.patch
+++ b/ovn.patch
@@ -1,8 +1,1520 @@
+diff --git a/controller/bfd.c b/controller/bfd.c
+index 56bfa49361..3b0c3f6dae 100644
+--- a/controller/bfd.c
++++ b/controller/bfd.c
+@@ -117,14 +117,13 @@ bfd_calculate_active_tunnels(const struct ovsrec_bridge *br_int,
+  *
+  * If 'our_chassis' is C5 then this function returns empty bfd set.
+  */
+-bool
++void
+ bfd_calculate_chassis(
+     const struct sbrec_chassis *our_chassis,
+     const struct sbrec_ha_chassis_group_table *ha_chassis_grp_table,
+     struct sset *bfd_chassis)
+ {
+     const struct sbrec_ha_chassis_group *ha_chassis_grp;
+-    bool chassis_is_ha_gw = false;
+     SBREC_HA_CHASSIS_GROUP_TABLE_FOR_EACH (ha_chassis_grp,
+                                            ha_chassis_grp_table) {
+         bool is_ha_chassis = false;
+@@ -144,7 +143,6 @@ bfd_calculate_chassis(
+             sset_add(&grp_chassis, ha_ch->chassis->name);
+             if (our_chassis == ha_ch->chassis) {
+                 is_ha_chassis = true;
+-                chassis_is_ha_gw = true;
+                 bfd_setup_required = true;
+             }
+         }
+@@ -180,7 +178,6 @@ bfd_calculate_chassis(
+         }
+         sset_destroy(&grp_chassis);
+     }
+-    return chassis_is_ha_gw;
+ }
+ 
+ void
+diff --git a/controller/bfd.h b/controller/bfd.h
+index 3e33848912..f8fece5a58 100644
+--- a/controller/bfd.h
++++ b/controller/bfd.h
+@@ -16,8 +16,6 @@
+ #ifndef OVN_BFD_H
+ #define OVN_BFD_H 1
+ 
+-#include <stdbool.h>
+-
+ struct hmap;
+ struct ovsdb_idl;
+ struct ovsdb_idl_index;
+@@ -38,7 +36,7 @@ void bfd_run(const struct ovsrec_interface_table *,
+              const struct sbrec_sb_global_table *,
+              const struct ovsrec_open_vswitch_table *);
+ 
+-bool bfd_calculate_chassis(
++void bfd_calculate_chassis(
+     const struct sbrec_chassis *,
+     const struct sbrec_ha_chassis_group_table *,
+     struct sset *);
+diff --git a/controller/encaps.c b/controller/encaps.c
+index 61f41bf3ac..081fbe6716 100644
+--- a/controller/encaps.c
++++ b/controller/encaps.c
+@@ -25,7 +25,6 @@
+ #include "lib/ovn-sb-idl.h"
+ #include "lib/ovsdb-idl.h"
+ #include "ovn-controller.h"
+-#include "smap.h"
+ 
+ VLOG_DEFINE_THIS_MODULE(encaps);
+ 
+@@ -44,6 +43,7 @@ encaps_register_ovs_idl(struct ovsdb_idl *ovs_idl)
+     ovsdb_idl_track_add_column(ovs_idl, &ovsrec_interface_col_name);
+     ovsdb_idl_track_add_column(ovs_idl, &ovsrec_interface_col_type);
+     ovsdb_idl_track_add_column(ovs_idl, &ovsrec_interface_col_options);
++    ovsdb_idl_track_add_column(ovs_idl, &ovsrec_interface_col_other_config);
+ }
+ 
+ /* Enough context to create a new tunnel, using tunnel_add(). */
+@@ -201,12 +201,14 @@ out:
+ }
+ 
+ static void
+-tunnel_add(struct tunnel_ctx *tc, const struct sbrec_sb_global *sbg,
+-           const char *new_chassis_id, const struct sbrec_encap *encap,
+-           const char *local_ip,
++tunnel_add(struct tunnel_ctx *tc,
++           const struct sbrec_sb_global *sbg,
++           const struct sbrec_chassis *chassis_rec,
++           const struct sbrec_encap *encap, const char *local_ip,
+            const struct ovsrec_open_vswitch_table *ovs_table)
+ {
+     struct smap options = SMAP_INITIALIZER(&options);
++    struct smap other_config = SMAP_INITIALIZER(&other_config);
+     smap_add(&options, "remote_ip", encap->ip);
+     smap_add(&options, "local_ip", local_ip);
+     smap_add(&options, "key", "flow");
+@@ -221,9 +223,9 @@ tunnel_add(struct tunnel_ctx *tc, const struct sbrec_sb_global *sbg,
+      * combination of the chassis_name and the remote and local encap-ips to
+      * identify a specific tunnel to the remote chassis.
+      */
+-    tunnel_entry_id = encaps_tunnel_id_create(new_chassis_id, encap->ip,
++    tunnel_entry_id = encaps_tunnel_id_create(chassis_rec->name, encap->ip,
+                                               local_ip);
+-    tunnel_entry_id_old = encaps_tunnel_id_create_legacy(new_chassis_id,
++    tunnel_entry_id_old = encaps_tunnel_id_create_legacy(chassis_rec->name,
+                                                          encap->ip);
+     if (csum && (!strcmp(csum, "true") || !strcmp(csum, "false"))) {
+         smap_add(&options, "csum", csum);
+@@ -258,7 +260,7 @@ tunnel_add(struct tunnel_ctx *tc, const struct sbrec_sb_global *sbg,
+ 
+     /* Add auth info if ipsec is enabled. */
+     if (sbg->ipsec) {
+-        smap_add(&options, "remote_name", new_chassis_id);
++        smap_add(&options, "remote_name", chassis_rec->name);
+ 
+         /* Force NAT-T traversal via configuration */
+         /* Two ipsec backends are supported: libreswan and strongswan */
+@@ -276,6 +278,11 @@ tunnel_add(struct tunnel_ctx *tc, const struct sbrec_sb_global *sbg,
+         }
+     }
+ 
++    if (is_ramp_tunnel(&chassis_rec->other_config)) {
++        /* Propagate ramp switch flag from chassis to interface. */
++        smap_add(&other_config, "is-vtep", "true");
++    }
++
+     /* If there's an existing tunnel record that does not need any change,
+      * keep it.  Otherwise, create a new record (if there was an existing
+      * record, the new record will supplant it and encaps_run() will delete
+@@ -312,10 +319,10 @@ tunnel_add(struct tunnel_ctx *tc, const struct sbrec_sb_global *sbg,
+      * its name, otherwise generate a new, unique name. */
+     char *port_name = (tunnel
+                        ? xstrdup(tunnel->port->name)
+-                       : tunnel_create_name(tc, new_chassis_id));
++                       : tunnel_create_name(tc, chassis_rec->name));
+     if (!port_name) {
+         VLOG_WARN("Unable to allocate unique name for '%s' tunnel",
+-                  new_chassis_id);
++                  chassis_rec->name);
+         goto exit;
+     }
+ 
+@@ -323,6 +330,7 @@ tunnel_add(struct tunnel_ctx *tc, const struct sbrec_sb_global *sbg,
+     ovsrec_interface_set_name(iface, port_name);
+     ovsrec_interface_set_type(iface, encap->type);
+     ovsrec_interface_set_options(iface, &options);
++    ovsrec_interface_set_other_config(iface, &other_config);
+ 
+     struct ovsrec_port *port = ovsrec_port_insert(tc->ovs_txn);
+     ovsrec_port_set_name(port, port_name);
+@@ -338,6 +346,7 @@ exit:
+     free(tunnel_entry_id);
+     free(tunnel_entry_id_old);
+     smap_destroy(&options);
++    smap_destroy(&other_config);
+ }
+ 
+ static bool
+@@ -403,7 +412,7 @@ chassis_tunnel_add(const struct sbrec_chassis *chassis_rec,
+             }
+             VLOG_DBG("tunnel_add: '%s', local ip: %s", chassis_rec->name,
+                      this_chassis->encaps[j]->ip);
+-            tunnel_add(tc, sbg, chassis_rec->name, chassis_rec->encaps[i],
++            tunnel_add(tc, sbg, chassis_rec, chassis_rec->encaps[i],
+                        this_chassis->encaps[j]->ip, ovs_table);
+             tuncnt++;
+         }
+diff --git a/controller/encaps.h b/controller/encaps.h
+index fa5dc17e5f..0257d08c13 100644
+--- a/controller/encaps.h
++++ b/controller/encaps.h
+@@ -17,6 +17,7 @@
+ #define OVN_ENCAPS_H 1
+ 
+ #include <stdbool.h>
++#include "smap.h"
+ 
+ /*
+  * Given there could be multiple tunnels with different IPs to the same
+@@ -68,4 +69,10 @@ bool  encaps_tunnel_id_match(const char *tunnel_id, const char *chassis_id,
+ 
+ void encaps_destroy(void);
+ 
++static inline bool
++is_ramp_tunnel(const struct smap *other_config)
++{
++    return smap_get_bool(other_config, "is-vtep", false);
++}
++
+ #endif /* controller/encaps.h */
+diff --git a/controller/evpn-binding.c b/controller/evpn-binding.c
+index b176706c3f..4da8020e15 100644
+--- a/controller/evpn-binding.c
++++ b/controller/evpn-binding.c
+@@ -32,6 +32,7 @@ static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+ 
+ static void collect_evpn_datapaths(const struct hmap *local_datapaths,
+                                    struct hmap *evpn_datapaths);
++static void evpn_datapaths_clear(struct hmap *evpn_datapaths);
+ 
+ struct evpn_tunnel {
+     uint16_t dst_port;
+@@ -62,6 +63,7 @@ evpn_binding_run(const struct evpn_binding_ctx_in *b_ctx_in,
+     struct hmapx stale_mc_groups = HMAPX_INITIALIZER(&stale_mc_groups);
+     uint32_t hint = OVN_MIN_EVPN_KEY;
+ 
++    evpn_datapaths_clear(b_ctx_out->datapaths);
+     collect_evpn_datapaths(b_ctx_in->local_datapaths, b_ctx_out->datapaths);
+ 
+     struct evpn_binding *binding;
+@@ -233,7 +235,7 @@ evpn_datapath_find(const struct hmap *evpn_datapaths, uint32_t vni)
+     return NULL;
+ }
+ 
+-void
++static void
+ evpn_datapaths_clear(struct hmap *evpn_datapaths)
+ {
+     struct evpn_datapath *edp;
+diff --git a/controller/evpn-binding.h b/controller/evpn-binding.h
+index 29b85fc162..521db9d6e3 100644
+--- a/controller/evpn-binding.h
++++ b/controller/evpn-binding.h
+@@ -93,7 +93,6 @@ void evpn_vtep_binding_list(struct unixctl_conn *conn, int argc,
+                              const char *argv[], void *data_);
+ const struct evpn_datapath *evpn_datapath_find(
+         const struct hmap *evpn_datapaths, uint32_t vni);
+-void evpn_datapaths_clear(struct hmap *evpn_datapaths);
+ void evpn_datapaths_destroy(struct hmap *evpn_datapaths);
+ void evpn_multicast_groups_destroy(struct hmap *multicast_groups);
+ void evpn_multicast_group_list(struct unixctl_conn *conn, int argc,
+diff --git a/controller/garp_rarp.c b/controller/garp_rarp.c
+index 9d0c2c2e4b..1cf415a9fa 100644
+--- a/controller/garp_rarp.c
++++ b/controller/garp_rarp.c
+@@ -298,6 +298,7 @@ reset_timers_for_claimed_cr(struct if_status_mgr *mgr)
+     SSET_FOR_EACH_SAFE (cr_logical_port, claimed_cr) {
+         garp_rarp_node_reset_timers(cr_logical_port);
+         sset_delete(claimed_cr, SSET_NODE_FROM_NAME(cr_logical_port));
++        garp_rarp_data_has_changed = true;
+     }
+ 
+ }
+@@ -565,7 +566,7 @@ garp_rarp_get_data(void)
+ bool
+ garp_rarp_data_changed(void) {
+     bool ret = garp_rarp_data_has_changed;
+-    garp_rarp_data_has_changed = true;
++    garp_rarp_data_has_changed = false;
+     return ret;
+ }
+ 
+diff --git a/controller/lflow.c b/controller/lflow.c
+index 35ed6d30b7..382c2aecb9 100644
+--- a/controller/lflow.c
++++ b/controller/lflow.c
+@@ -229,6 +229,27 @@ is_chassis_resident_cb(const void *c_aux_, const char *port_name)
+     }
+ }
+ 
++static bool
++lport_id_is_local(const struct lflow_ctx_in *l_ctx_in,
++                  const struct uuid *lflow_uuid,
++                  int64_t dp_id, int64_t port_id)
++{
++    /* To/from EVPN VTEP logical port keys are always local. */
++    if (OVN_IS_EVPN_KEY(port_id)) {
++        return true;
++    }
++
++    char buf[16];
++    get_unique_lport_key(dp_id, port_id, buf, sizeof(buf));
++
++    if (!sset_contains(l_ctx_in->related_lport_ids, buf)) {
++        VLOG_DBG("lflow "UUID_FMT" port %s in match is not local, skip",
++                 UUID_ARGS(lflow_uuid), buf);
++        return false;
++    }
++    return true;
++}
++
+ /* Adds the logical flows from the Logical_Flow table to flow tables. */
+ static void
+ add_logical_flows(struct lflow_ctx_in *l_ctx_in,
+@@ -936,22 +957,15 @@ add_matches_to_flow_table(const struct sbrec_logical_flow *lflow,
+ 
+     struct expr_match *m;
+     HMAP_FOR_EACH (m, hmap_node, matches) {
+-        match_set_metadata(&m->match, htonll(ldp->datapath->tunnel_key));
++        int64_t dp_id = ldp->datapath->tunnel_key;
++        match_set_metadata(&m->match, htonll(dp_id));
+         if (ldp->is_switch) {
+             unsigned int reg_index
+                 = (ingress ? MFF_LOG_INPORT : MFF_LOG_OUTPORT) - MFF_REG0;
+             int64_t port_id = m->match.flow.regs[reg_index];
+-            if (port_id) {
+-                int64_t dp_id = ldp->datapath->tunnel_key;
+-                char buf[16];
+-                get_unique_lport_key(dp_id, port_id, buf, sizeof(buf));
+-                if (!sset_contains(l_ctx_in->related_lport_ids, buf)) {
+-                    VLOG_DBG("lflow "UUID_FMT
+-                             " port %s in match is not local, skip",
+-                             UUID_ARGS(&lflow->header_.uuid),
+-                             buf);
+-                    continue;
+-                }
++            if (port_id && !lport_id_is_local(l_ctx_in, &lflow->header_.uuid,
++                                              dp_id, port_id)) {
++                continue;
+             }
+         }
+ 
+@@ -1090,11 +1104,8 @@ consider_logical_flow__(const struct sbrec_logical_flow *lflow,
+                      "found, skip", UUID_ARGS(&lflow->header_.uuid), io_port);
+             return;
+         }
+-        char buf[16];
+-        get_unique_lport_key(dp->tunnel_key, pb->tunnel_key, buf, sizeof buf);
+-        if (!sset_contains(l_ctx_in->related_lport_ids, buf)) {
+-            VLOG_DBG("lflow "UUID_FMT" matches inport/outport %s that's not "
+-                     "local, skip", UUID_ARGS(&lflow->header_.uuid), io_port);
++        if (!lport_id_is_local(l_ctx_in, &lflow->header_.uuid,
++                               dp->tunnel_key, pb->tunnel_key)) {
+             return;
+         }
+     }
+diff --git a/controller/local_data.c b/controller/local_data.c
+index dda746d73e..af6c75b400 100644
+--- a/controller/local_data.c
++++ b/controller/local_data.c
+@@ -532,6 +532,7 @@ local_nonvif_data_run(const struct ovsrec_bridge *br_int,
+                 tun->ofport = u16_to_ofp(ofport);
+                 tun->type = tunnel_type;
+                 tun->is_ipv6 = ip ? addr_is_ipv6(ip) : false;
++                tun->is_ramp_tunnel = is_ramp_tunnel(&iface_rec->other_config);
+ 
+                 free(hash_id);
+                 free(ip);
+diff --git a/controller/local_data.h b/controller/local_data.h
+index 948c1a935e..cbb8899eb6 100644
+--- a/controller/local_data.h
++++ b/controller/local_data.h
+@@ -146,6 +146,7 @@ struct chassis_tunnel {
+     ofp_port_t ofport;
+     enum chassis_tunnel_type type;
+     bool is_ipv6;
++    bool is_ramp_tunnel;
+ };
+ 
+ /* Flow-based tunnel that consolidates multiple endpoints into a single
+diff --git a/controller/mac-cache.c b/controller/mac-cache.c
+index bdf35eeb75..c996fd6b9a 100644
+--- a/controller/mac-cache.c
++++ b/controller/mac-cache.c
+@@ -59,12 +59,11 @@ static void
+ mac_cache_update_req_delay(struct hmap *thresholds, uint64_t *req_delay);
+ 
+ static struct buffered_packets *
+-buffered_packets_find(struct buffered_packets_ctx *ctx,
++buffered_packets_find(struct cmap *bp_map,
+                       const struct mac_binding_data *mb_data);
+ 
+ static void
+-buffered_packets_remove(struct buffered_packets_ctx *ctx,
+-                        struct buffered_packets *bp);
++buffered_packets_free(struct buffered_packets *bp);
+ 
+ static void
+ buffered_packets_db_lookup(struct buffered_packets *bp,
+@@ -550,24 +549,24 @@ bp_packet_data_destroy(struct bp_packet_data *pd) {
+ }
+ 
+ struct buffered_packets *
+-buffered_packets_add(struct buffered_packets_ctx *ctx,
+-                     struct mac_binding_data mb_data) {
++buffered_packets_add(struct cmap *bp_map, struct mac_binding_data mb_data) {
+     uint32_t hash = mac_binding_data_hash(&mb_data);
+ 
+-    struct buffered_packets *bp = buffered_packets_find(ctx, &mb_data);
++    struct buffered_packets *bp = buffered_packets_find(bp_map, &mb_data);
+     if (!bp) {
+-        if (hmap_count(&ctx->buffered_packets) >= MAX_BUFFERED_PACKETS) {
++        if (cmap_count(bp_map) >= MAX_BUFFERED_PACKETS) {
+             return NULL;
+         }
+ 
+         bp = xmalloc(sizeof *bp);
+-        hmap_insert(&ctx->buffered_packets, &bp->hmap_node, hash);
+         bp->mb_data = mb_data;
++        atomic_init(&bp->resolved_mac, 0);
+         /* Schedule the freshly added buffered packet to do lookup
+          * immediately. */
+         bp->lookup_at_ms = 0;
+         bp->queue = VECTOR_CAPACITY_INITIALIZER(struct bp_packet_data,
+                                                 BUFFER_QUEUE_DEPTH);
++        cmap_insert(bp_map, &bp->cmap_node, hash);
+     }
+ 
+     bp->expire_at_ms = time_msec() + BUFFERED_PACKETS_TIMEOUT_MS;
+@@ -605,25 +604,28 @@ buffered_packets_packet_data_enqueue(struct buffered_packets *bp,
+     vector_push(&bp->queue, &pd);
+ }
+ 
+-void
+-buffered_packets_ctx_run(struct buffered_packets_ctx *ctx,
+-                         const struct hmap *recent_mbs,
+-                         struct ovsdb_idl_index *sbrec_pb_by_key,
+-                         struct ovsdb_idl_index *sbrec_dp_by_key,
+-                         struct ovsdb_idl_index *sbrec_pb_by_name,
+-                         struct ovsdb_idl_index *sbrec_mb_by_lport_ip) {
++bool
++buffered_packets_lookup_run(struct cmap *bp_map, const struct hmap *recent_mbs,
++                            struct ovsdb_idl_index *sbrec_pb_by_key,
++                            struct ovsdb_idl_index *sbrec_dp_by_key,
++                            struct ovsdb_idl_index *sbrec_pb_by_name,
++                            struct ovsdb_idl_index *sbrec_mb_by_lport_ip) {
+     struct ds ip = DS_EMPTY_INITIALIZER;
+     long long now = time_msec();
++    bool updated = false;
+ 
+     struct buffered_packets *bp;
+-    HMAP_FOR_EACH_SAFE (bp, hmap_node, &ctx->buffered_packets) {
+-        struct eth_addr mac = eth_addr_zero;
+-        /* Remove expired buffered packets. */
+-        if (now > bp->expire_at_ms) {
+-            buffered_packets_remove(ctx, bp);
++    CMAP_FOR_EACH (bp, cmap_node, bp_map) {
++        uint64_t mac64;
++        atomic_read(&bp->resolved_mac, &mac64);
++        /* MAC for given entry was already resolved,
++         * no need to resolve it again. */
++        if (mac64) {
+             continue;
+         }
+ 
++        struct eth_addr mac = eth_addr_zero;
++
+         struct mac_binding *mb = mac_binding_find(recent_mbs, &bp->mb_data);
+         if (mb) {
+             mac = mb->data.mac;
+@@ -634,13 +636,46 @@ buffered_packets_ctx_run(struct buffered_packets_ctx *ctx,
+                                        sbrec_mb_by_lport_ip);
+             /* Schedule next lookup even if we found the MAC address,
+              * if the address was found this struct will be deleted anyway. */
++
+             bp->lookup_at_ms = now + BUFFERED_PACKETS_LOOKUP_MS;
+         }
+ 
+-        if (eth_addr_is_zero(mac)) {
++        if (!eth_addr_is_zero(mac)) {
++            atomic_store(&bp->resolved_mac, eth_addr_to_uint64(mac));
++            updated = true;
++        }
++    }
++
++    ds_destroy(&ip);
++
++    return updated;
++}
++
++void
++buffered_packets_run(struct cmap *bp_map, struct vector *rpd)
++{
++    long long now = time_msec();
++
++    struct buffered_packets *bp;
++    CMAP_FOR_EACH (bp, cmap_node, bp_map) {
++        uint32_t hash = mac_binding_data_hash(&bp->mb_data);
++
++        /* Remove expired buffered packets. */
++        if (now > bp->expire_at_ms) {
++            cmap_remove(bp_map, &bp->cmap_node, hash);
++            ovsrcu_postpone(buffered_packets_free, bp);
++            continue;
++        }
++
++        uint64_t mac64;
++        atomic_read(&bp->resolved_mac, &mac64);
++        if (!mac64) {
+             continue;
+         }
+ 
++        struct eth_addr mac;
++        eth_addr_from_uint64(mac64, &mac);
++
+         struct bp_packet_data *pd;
+         VECTOR_FOR_EACH_PTR (&bp->queue, pd) {
+             struct dp_packet packet;
+@@ -650,45 +685,25 @@ buffered_packets_ctx_run(struct buffered_packets_ctx *ctx,
+             eth->eth_dst = mac;
+         }
+ 
+-        vector_push_array(&ctx->ready_packets_data,
+-                          vector_get_array(&bp->queue),
++        vector_push_array(rpd, vector_get_array(&bp->queue),
+                           vector_len(&bp->queue));
+         vector_clear(&bp->queue);
+-        buffered_packets_remove(ctx, bp);
+-    }
+-
+-    ds_destroy(&ip);
+-}
+-
+-bool
+-buffered_packets_ctx_is_ready_to_send(struct buffered_packets_ctx *ctx) {
+-    return !vector_is_empty(&ctx->ready_packets_data);
+-}
+-
+-bool
+-buffered_packets_ctx_has_packets(struct buffered_packets_ctx *ctx) {
+-    return !hmap_is_empty(&ctx->buffered_packets);
+-}
+ 
+-void
+-buffered_packets_ctx_init(struct buffered_packets_ctx *ctx) {
+-    ctx->ready_packets_data = VECTOR_EMPTY_INITIALIZER(struct bp_packet_data);
+-    hmap_init(&ctx->buffered_packets);
++        cmap_remove(bp_map, &bp->cmap_node, hash);
++        ovsrcu_postpone(buffered_packets_free, bp);
++    }
+ }
+ 
+ void
+-buffered_packets_ctx_destroy(struct buffered_packets_ctx *ctx) {
+-    struct bp_packet_data *pd;
+-    VECTOR_FOR_EACH_PTR (&ctx->ready_packets_data, pd) {
+-        bp_packet_data_destroy(pd);
+-    }
+-    vector_destroy(&ctx->ready_packets_data);
+-
++buffered_packets_map_destroy(struct cmap *bp_map) {
+     struct buffered_packets *bp;
+-    HMAP_FOR_EACH_SAFE (bp, hmap_node, &ctx->buffered_packets) {
+-        buffered_packets_remove(ctx, bp);
++    CMAP_FOR_EACH (bp, cmap_node, bp_map) {
++        cmap_remove(bp_map, &bp->cmap_node,
++                    mac_binding_data_hash(&bp->mb_data));
++        ovsrcu_postpone(buffered_packets_free, bp);
+     }
+-    hmap_destroy(&ctx->buffered_packets);
++
++    cmap_destroy(bp_map);
+ }
+ 
+ static uint32_t
+@@ -771,12 +786,12 @@ mac_cache_update_req_delay(struct hmap *thresholds, uint64_t *req_delay)
+ }
+ 
+ static struct buffered_packets *
+-buffered_packets_find(struct buffered_packets_ctx *ctx,
++buffered_packets_find(struct cmap *bp_map,
+                       const struct mac_binding_data *mb_data) {
+     uint32_t hash = mac_binding_data_hash(mb_data);
+ 
+     struct buffered_packets *bp;
+-    HMAP_FOR_EACH_WITH_HASH (bp, hmap_node, hash, &ctx->buffered_packets) {
++    CMAP_FOR_EACH_WITH_HASH (bp, cmap_node, hash, bp_map) {
+         if (mac_binding_data_equals(&bp->mb_data, mb_data)) {
+             return bp;
+         }
+@@ -786,14 +801,12 @@ buffered_packets_find(struct buffered_packets_ctx *ctx,
+ }
+ 
+ static void
+-buffered_packets_remove(struct buffered_packets_ctx *ctx,
+-                        struct buffered_packets *bp) {
++buffered_packets_free(struct buffered_packets *bp) {
+     struct bp_packet_data *pd;
+     VECTOR_FOR_EACH_PTR (&bp->queue, pd) {
+         bp_packet_data_destroy(pd);
+     }
+ 
+-    hmap_remove(&ctx->buffered_packets, &bp->hmap_node);
+     vector_destroy(&bp->queue);
+     free(bp);
+ }
+@@ -920,13 +933,29 @@ mac_binding_probe_stats_run(struct vector *stats_vec, uint64_t *req_delay,
+             continue;
+         }
+ 
+-        bool is_mb_v4 = IN6_IS_ADDR_V4MAPPED(&mb->data.ip);
+-        if ((is_mb_v4 && laddr.n_ipv4_addrs)
+-                || (!is_mb_v4 && laddr.n_ipv6_addrs)) {
+-            struct in6_addr local =
+-                is_mb_v4 ? in6_addr_mapped_ipv4(laddr.ipv4_addrs[0].addr)
+-                         : laddr.ipv6_addrs[0].addr;
++        struct in6_addr local = in6addr_any;
++        if (IN6_IS_ADDR_V4MAPPED(&mb->data.ip)) {
++            ovs_be32 ip4 = in6_addr_get_mapped_ipv4(&mb->data.ip);
++            for (size_t i = 0; i < laddr.n_ipv4_addrs; i++) {
++                struct ipv4_netaddr address = laddr.ipv4_addrs[i];
++                if (address.network == (ip4 & address.mask)) {
++                    local = in6_addr_mapped_ipv4(address.addr);
++                    break;
++                }
++            }
++        } else {
++            for (size_t i = 0; i < laddr.n_ipv6_addrs; i++) {
++                struct ipv6_netaddr address = laddr.ipv6_addrs[i];
++                struct in6_addr neigh_prefix =
++                    ipv6_addr_bitand(&mb->data.ip, &address.mask);
++                if (ipv6_addr_equals(&address.network, &neigh_prefix)) {
++                    local = address.addr;
++                    break;
++                }
++            }
++        }
+ 
++        if (!ipv6_addr_equals(&local, &in6addr_any)) {
+             mac_binding_update_log("Sending ARP/ND request for active",
+                                    &mb->data, true, threshold,
+                                    stats->idle_age_ms, since_updated_ms);
+diff --git a/controller/mac-cache.h b/controller/mac-cache.h
+index 7edb129d75..365219d334 100644
+--- a/controller/mac-cache.h
++++ b/controller/mac-cache.h
+@@ -18,9 +18,9 @@
+ 
+ #include <stdint.h>
+ 
++#include "cmap.h"
+ #include "dp-packet.h"
+ #include "openvswitch/hmap.h"
+-#include "openvswitch/hmap.h"
+ #include "openvswitch/list.h"
+ #include "openvswitch/ofpbuf.h"
+ #include "openvswitch/ofp-flow.h"
+@@ -115,25 +115,25 @@ struct bp_packet_data {
+ };
+ 
+ struct buffered_packets {
+-    struct hmap_node hmap_node;
++    struct cmap_node cmap_node;
+ 
+-    struct mac_binding_data mb_data;
++    struct mac_binding_data mb_data;  /* Immutable after insert. */
+ 
+-    /* Queue of packet_data associated with this struct. */
++    /* Queue of packet_data associated with this struct.
++     * Handler thread only. */
+     struct vector queue;
+ 
+-    /* Timestamp in ms when the buffered packet should expire. */
++    /* Timestamp in ms when the buffered packet should expire.
++     * Handler thread only. */
+     long long int expire_at_ms;
+ 
+-    /* Timestamp in ms when the buffered packet should do full SB lookup.*/
+-    long long int lookup_at_ms;
+-};
++    /* Resolved MAC address packed as uint64. 0 means unresolved.
++     * Written by main thread, read by handler thread. */
++    atomic_uint64_t resolved_mac;
+ 
+-struct buffered_packets_ctx {
+-    /* Map of all buffered packets waiting for the MAC address. */
+-    struct hmap buffered_packets;
+-    /* List of packet data that are ready to be sent. */
+-    struct vector ready_packets_data;
++    /* Timestamp in ms when the buffered packet should do full SB lookup.
++     * Main thread only. */
++    long long int lookup_at_ms;
+ };
+ 
+ /* Thresholds. */
+@@ -211,27 +211,23 @@ void fdb_stats_run(struct vector *stats_vec, uint64_t *req_delay, void *data);
+ void bp_packet_data_destroy(struct bp_packet_data *pd);
+ 
+ struct buffered_packets *
+-buffered_packets_add(struct buffered_packets_ctx *ctx,
++buffered_packets_add(struct cmap *bp_map,
+                      struct mac_binding_data mb_data);
+ 
+ void buffered_packets_packet_data_enqueue(struct buffered_packets *bp,
+                                           const struct ofputil_packet_in *pin,
+                                           const struct ofpbuf *continuation);
+ 
+-void buffered_packets_ctx_run(struct buffered_packets_ctx *ctx,
+-                              const struct hmap *recent_mbs,
+-                              struct ovsdb_idl_index *sbrec_pb_by_key,
+-                              struct ovsdb_idl_index *sbrec_dp_by_key,
+-                              struct ovsdb_idl_index *sbrec_pb_by_name,
+-                              struct ovsdb_idl_index *sbrec_mb_by_lport_ip);
+-
+-void buffered_packets_ctx_init(struct buffered_packets_ctx *ctx);
+-
+-void buffered_packets_ctx_destroy(struct buffered_packets_ctx *ctx);
++bool buffered_packets_lookup_run(struct cmap *bp_map,
++                                 const struct hmap *recent_mbs,
++                                 struct ovsdb_idl_index *sbrec_pb_by_key,
++                                 struct ovsdb_idl_index *sbrec_dp_by_key,
++                                 struct ovsdb_idl_index *sbrec_pb_by_name,
++                                 struct ovsdb_idl_index *sbrec_mb_by_lport_ip);
+ 
+-bool buffered_packets_ctx_is_ready_to_send(struct buffered_packets_ctx *ctx);
++void buffered_packets_run(struct cmap *bp_map, struct vector *rpd);
+ 
+-bool buffered_packets_ctx_has_packets(struct buffered_packets_ctx *ctx);
++void buffered_packets_map_destroy(struct cmap *bp_map);
+ 
+ void mac_binding_probe_stats_process_flow_stats(
+         struct vector *stats_vec,
+diff --git a/controller/ovn-controller.8.xml b/controller/ovn-controller.8.xml
+index 33281a4d66..57e7cf5dd2 100644
+--- a/controller/ovn-controller.8.xml
++++ b/controller/ovn-controller.8.xml
+@@ -531,17 +531,6 @@
+         65535.
+       </dd>
+ 
+-      <dt>
+-        <code>external_ids:ovn-managed-flow-restore-wait</code> in the
+-        <code>Open_vSwitch</code> table
+-      </dt>
+-      <dd>
+-        When set to true, this key indicates that <code>ovn-controller</code>
+-        has set the <code>other_config:flow-restore-wait</code> option.
+-        The key is set when <code>ovn-controller</code> enables
+-        flow-restore-wait and removed when it clears it.
+-      </dd>
+-
+       <dt>
+         <code>external_ids:ct-zone-*</code> in the <code>Bridge</code> table
+       </dt>
+diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
+index dcf67d789c..cdde157761 100644
+--- a/controller/ovn-controller.c
++++ b/controller/ovn-controller.c
+@@ -211,152 +211,6 @@ static char *get_file_system_id(void)
+     free(filename);
+     return ret;
+ }
+-
+-/* Set/unset flow-restore-wait, and inc ovs next_cfg if false
+- * When set to true, also sets ovn-managed-flow-restore-wait to true to
+- * indicate ownership */
+-static void
+-set_flow_restore_wait(struct ovsdb_idl_txn *ovs_idl_txn,
+-                      const struct ovsrec_open_vswitch *cfg,
+-                      const struct smap *other_config,
+-                      const bool val, bool ovn_managed)
+-{
+-    struct smap new_config;
+-    smap_clone(&new_config, other_config);
+-    smap_replace(&new_config, "flow-restore-wait", val ? "true": "false");
+-    ovsrec_open_vswitch_set_other_config(cfg, &new_config);
+-    if (val) {
+-        ovsrec_open_vswitch_update_external_ids_setkey(
+-            cfg, "ovn-managed-flow-restore-wait", "true");
+-    } else if (ovn_managed) {
+-        ovsrec_open_vswitch_update_external_ids_delkey(
+-            cfg, "ovn-managed-flow-restore-wait");
+-    }
+-    ovsdb_idl_txn_increment(ovs_idl_txn, &cfg->header_,
+-                            &ovsrec_open_vswitch_col_next_cfg, true);
+-    smap_destroy(&new_config);
+-}
+-
+-static void
+-manage_flow_restore_wait(struct ovsdb_idl_txn *ovs_idl_txn,
+-                         const struct ovsrec_open_vswitch *cfg,
+-                         uint64_t ofctrl_cur_cfg, uint64_t ovs_next_cfg,
+-                         int ovs_txn_status, bool is_ha_gw)
+-{
+-    enum flow_restore_wait_state {
+-        FRW_INIT,              /* Initial state */
+-        FRW_WAIT_TXN_COMPLETE, /* Sent false, waiting txn to complete */
+-        FRW_TXN_SUCCESS,       /* Txn completed. Waiting for OVS Ack. */
+-        FRW_DONE               /* Everything completed */
+-    };
+-
+-    static int64_t frw_next_cfg;
+-    static enum flow_restore_wait_state frw_state;
+-    static bool ofctrl_was_connected = false;
+-
+-    bool ofctrl_connected = ofctrl_is_connected();
+-
+-    if (!ovs_idl_txn || !cfg) {
+-        return;
+-    }
+-
+-    /* If OVS is stopped/started, make sure flow-restore-wait is toggled. */
+-    if (ofctrl_connected && !ofctrl_was_connected) {
+-        frw_state = FRW_INIT;
+-    }
+-    ofctrl_was_connected = ofctrl_connected;
+-
+-    if (!ofctrl_connected) {
+-        return;
+-    }
+-
+-    bool frw = smap_get_bool(&cfg->other_config, "flow-restore-wait", false);
+-    bool ovn_managed_once = smap_get_bool(&cfg->external_ids,
+-                                          "ovn-managed-flow-restore-wait",
+-                                          false);
+-
+-    if (frw && !ovn_managed_once) {
+-        /* frw has been set by ovs-ctl. Do not touch. */
+-        return;
+-    }
+-
+-    if (!is_ha_gw) {
+-        if (frw) {
+-            /* frw has once been set by OVN. We are now not an HA chassis
+-             * anymore, unset it. */
+-            set_flow_restore_wait(ovs_idl_txn, cfg, &cfg->other_config,
+-                                  false, ovn_managed_once);
+-        }
+-        /* else we are not an HA chassis and frw is false. Ignore it. */
+-        return;
+-    }
+-
+-    switch (frw_state) {
+-    case FRW_INIT:
+-        if (ofctrl_cur_cfg > 0) {
+-            set_flow_restore_wait(ovs_idl_txn, cfg, &cfg->other_config,
+-                                  false, ovn_managed_once);
+-            frw_state = FRW_WAIT_TXN_COMPLETE;
+-            VLOG_INFO("Setting flow-restore-wait=false "
+-                      "(cur_cfg=%"PRIu64")", ofctrl_cur_cfg);
+-        }
+-        break;
+-
+-    case FRW_WAIT_TXN_COMPLETE:
+-        /* if (ovs_idl_txn != NULL), the transaction completed.
+-         * When the transaction completed, it either failed
+-         * (ovs_txn_status == 0) or succeeded (ovs_txn_status != 0). */
+-        if (ovs_txn_status == 0) {
+-            /* Previous transaction failed. */
+-            set_flow_restore_wait(ovs_idl_txn, cfg, &cfg->other_config,
+-                                  false, ovn_managed_once);
+-            break;
+-        }
+-        /* txn succeeded, get next_cfg */
+-        frw_next_cfg = ovs_next_cfg;
+-        frw_state = FRW_TXN_SUCCESS;
+-        /* fall through */
+-
+-    case FRW_TXN_SUCCESS:
+-        if (ovs_next_cfg < frw_next_cfg) {
+-            /* DB was reset, next_cfg went backwards. */
+-            VLOG_INFO("OVS DB reset (next_cfg %"PRId64" -> %"PRIu64"), "
+-                      "resetting state",
+-                      frw_next_cfg, ovs_next_cfg);
+-            set_flow_restore_wait(ovs_idl_txn, cfg, &cfg->other_config,
+-                                  false, ovn_managed_once);
+-            frw_state = FRW_WAIT_TXN_COMPLETE;
+-            break;
+-        }
+-
+-        if (!frw) {
+-            if (cfg->cur_cfg >= frw_next_cfg) {
+-                set_flow_restore_wait(ovs_idl_txn, cfg, &cfg->other_config,
+-                                      true, ovn_managed_once);
+-                frw_state = FRW_DONE;
+-                VLOG_INFO("Setting flow-restore-wait=true");
+-            }
+-        } else {
+-            /* The transaction to false succeeded but frw is true.
+-             * So, another task already set it to true. */
+-            frw_state = FRW_DONE;
+-            VLOG_INFO("flow-restore-wait was already true");
+-        }
+-        break;
+-    case FRW_DONE:
+-        if (!frw) {
+-            /* frw has been removed (e.g. by ovs-ctl restart) or is false
+-             * (e.g. txn failed.) */
+-            set_flow_restore_wait(ovs_idl_txn, cfg, &cfg->other_config,
+-                                  false, ovn_managed_once);
+-            frw_state = FRW_WAIT_TXN_COMPLETE;
+-            VLOG_INFO("OVS frw cleared, restarting flow-restore-wait sequence "
+-                      "(cur_cfg=%"PRIu64")", ofctrl_cur_cfg);
+-        }
+-        break;
+-    }
+-}
+-
+ /* Only set monitor conditions on tables that are available in the
+  * server schema.
+  */
+@@ -3527,7 +3381,6 @@ en_mac_cache_cleanup(void *data)
+ 
+ struct ed_type_bfd_chassis {
+     struct sset bfd_chassis;
+-    bool is_ha_gw;
+ };
+ 
+ static void *
+@@ -3556,9 +3409,8 @@ en_bfd_chassis_run(struct engine_node *node, void *data OVS_UNUSED)
+         = chassis_lookup_by_name(sbrec_chassis_by_name, chassis_id);
+ 
+     sset_clear(&bfd_chassis->bfd_chassis);
+-    bfd_chassis->is_ha_gw = bfd_calculate_chassis(chassis,
+-                                                  ha_chassis_grp_table,
+-                                                  &bfd_chassis->bfd_chassis);
++    bfd_calculate_chassis(chassis, ha_chassis_grp_table,
++                          &bfd_chassis->bfd_chassis);
+     return EN_UPDATED;
+ }
+ 
+@@ -4933,12 +4785,14 @@ pflow_output_sb_port_binding_handler(struct engine_node *node,
+      */
+     const struct sbrec_port_binding *pb;
+     SBREC_PORT_BINDING_TABLE_FOR_EACH_TRACKED (pb, p_ctx.port_binding_table) {
++        bool removed = sbrec_port_binding_is_deleted(pb);
++
+         /* Trigger a full recompute if type column is updated. */
+-        if (sbrec_port_binding_is_updated(pb, SBREC_PORT_BINDING_COL_TYPE)) {
++        if (!removed && !sbrec_port_binding_is_new(pb) &&
++            sbrec_port_binding_is_updated(pb, SBREC_PORT_BINDING_COL_TYPE)) {
+             destroy_physical_ctx(&p_ctx);
+             return EN_UNHANDLED;
+         }
+-        bool removed = sbrec_port_binding_is_deleted(pb);
+         if (!physical_handle_flows_for_lport(pb, removed, &p_ctx,
+                                              &pfo->flow_table)) {
+             destroy_physical_ctx(&p_ctx);
+@@ -6392,7 +6246,6 @@ en_evpn_vtep_binding_clear_tracked_data(void *data_)
+     struct ed_type_evpn_vtep_binding *data = data_;
+     hmapx_clear(&data->updated_bindings);
+     uuidset_clear(&data->removed_bindings);
+-    evpn_datapaths_clear(&data->datapaths);
+     hmapx_clear(&data->updated_multicast_groups);
+     uuidset_clear(&data->removed_multicast_groups);
+ }
+@@ -7265,7 +7118,6 @@ main(int argc, char *argv[])
+     struct unixctl_server *unixctl;
+     struct ovn_exit_args exit_args = {0};
+     struct br_int_remote br_int_remote = {0};
+-    static uint64_t next_cfg = 0;
+     int retval;
+ 
+     /* Read from system-id-override file once on startup. */
+@@ -7593,7 +7445,6 @@ main(int argc, char *argv[])
+ 
+     /* Main loop. */
+     int ovnsb_txn_status = 1;
+-    int ovs_txn_status = 1;
+     bool sb_monitor_all = false;
+     struct tracked_acl_ids *tracked_acl_ids = NULL;
+     while (!exit_args.exiting) {
+@@ -7695,11 +7546,6 @@ main(int argc, char *argv[])
+         pinctrl_update_swconn(br_int_remote.target,
+                               br_int_remote.probe_interval);
+ 
+-        if (cfg && ovs_idl_txn && ovs_txn_status == -1) {
+-            /* txn was in progress and is now completed */
+-            next_cfg = cfg->next_cfg;
+-        }
+-
+         /* Enable ACL matching for double tagged traffic. */
+         if (ovs_idl_txn && cfg) {
+             int vlan_limit = smap_get_int(
+@@ -7708,6 +7554,24 @@ main(int argc, char *argv[])
+                 ovsrec_open_vswitch_update_other_config_setkey(
+                     cfg, "vlan-limit", "0");
+             }
++            /* Clear flow-restore-wait. OVN at one point would set
++             * flow-restore-wait in order to try to synchronize with
++             * OVS. However, that resulted in a bug, so that behavior
++             * was reverted. If upgrading from a version where OVN
++             * manipulted flow-restore-wait, then flow-restore-wait
++             * needs to be cleared in order for OVS to function
++             * properly. This is (hopefully) a temporary measure until
++             * a more reliable method of synchronizing with OVS is
++             * devised.
++             */
++            if (smap_get_bool(&cfg->external_ids,
++                              "ovn-managed-flow-restore-wait", false) &&
++                smap_get(&cfg->other_config, "flow-restore-wait")) {
++                ovsrec_open_vswitch_update_other_config_delkey(
++                    cfg, "flow-restore-wait");
++                ovsrec_open_vswitch_update_external_ids_delkey(
++                   cfg, "ovn-managed-flow-restore-wait");
++            }
+         }
+ 
+         static bool chassis_idx_stored = false;
+@@ -8049,13 +7913,6 @@ main(int argc, char *argv[])
+                     stopwatch_start(OFCTRL_SEQNO_RUN_STOPWATCH_NAME,
+                                     time_msec());
+                     ofctrl_seqno_run(ofctrl_get_cur_cfg());
+-                    if (ovs_idl_txn && bfd_chassis_data) {
+-                        manage_flow_restore_wait(ovs_idl_txn, cfg,
+-                                                 ofctrl_get_cur_cfg(),
+-                                                 next_cfg, ovs_txn_status,
+-                                                 bfd_chassis_data->is_ha_gw);
+-                    }
+-
+                     stopwatch_stop(OFCTRL_SEQNO_RUN_STOPWATCH_NAME,
+                                    time_msec());
+                     stopwatch_start(IF_STATUS_MGR_RUN_STOPWATCH_NAME,
+@@ -8155,7 +8012,7 @@ main(int argc, char *argv[])
+             OVS_NOT_REACHED();
+         }
+ 
+-        ovs_txn_status = ovsdb_idl_loop_commit_and_wait(&ovs_idl_loop);
++        int ovs_txn_status = ovsdb_idl_loop_commit_and_wait(&ovs_idl_loop);
+         if (!ovs_txn_status) {
+             /* The transaction failed. */
+             vif_plug_clear_deleted(
+@@ -8174,9 +8031,6 @@ main(int argc, char *argv[])
+                     &vif_plug_deleted_iface_ids);
+             vif_plug_finish_changed(
+                     &vif_plug_changed_iface_ids);
+-            if (cfg) {
+-                next_cfg = cfg->next_cfg;
+-            }
+         } else if (ovs_txn_status == -1) {
+             /* The commit is still in progress */
+         } else {
+@@ -8250,7 +8104,7 @@ loop_done:
+             }
+ 
+             ovsdb_idl_loop_commit_and_wait(&ovnsb_idl_loop);
+-            ovs_txn_status = ovsdb_idl_loop_commit_and_wait(&ovs_idl_loop);
++            int ovs_txn_status = ovsdb_idl_loop_commit_and_wait(&ovs_idl_loop);
+             if (!ovs_txn_status) {
+                 /* The transaction failed. */
+                 vif_plug_clear_deleted(
+diff --git a/controller/physical.c b/controller/physical.c
+index 228f3d171a..fd8a4be7f6 100644
+--- a/controller/physical.c
++++ b/controller/physical.c
+@@ -351,30 +351,35 @@ put_flow_based_remote_port_redirect_overlay(
+     }
+ }
+ 
++/* Add handling for E/W ICMPv4/v6 packets when tunneled packets exceed
++ * path MTU.
++ * If packet needs to be tunneled to another node and the physical
++ * interface used for tunneling has a lower MTU than the packet size,
++ * or if there is a route exception with a smaller MTU, kernel
++ * generates an ICMP "Fragmentation Needed" message, but packet
++ * metadata didn't change. Such packets might have been dropped due
++ * to required metadata modifications for returned packet.
++ *
++ * Mark these packets with MLF_RX_FROM_TUNNEL_BIT for further
++ * processing. Packets received from a RAMP tunnel should be passed
++ * through, and errors handled via normal processing path, since
++ * port metadata is not carried in RAMP packets in VNI.
++ */
+ static void
+-add_tunnel_ingress_flows(const struct chassis_tunnel *tun,
+-                         enum mf_field_id mff_ovn_geneve,
+-                         struct ovn_desired_flow_table *flow_table,
+-                         struct ofpbuf *ofpacts)
++add_tunnel_ingress_pmtud_flows(const struct chassis_tunnel *tun,
++                               struct ofpbuf *ofpacts,
++                               struct ovn_desired_flow_table *flow_table)
+ {
+-    /* Main ingress flow (priority 100) */
+-    struct match match = MATCH_CATCHALL_INITIALIZER;
+-    match_set_in_port(&match, tun->ofport);
+-
+-    ofpbuf_clear(ofpacts);
+-    put_decapsulation(mff_ovn_geneve, tun, ofpacts);
+-    put_resubmit(OFTABLE_LOCAL_OUTPUT, ofpacts);
++    if (tun->is_ramp_tunnel) {
++        return;
++    }
+ 
+-    ofctrl_add_flow(flow_table, OFTABLE_PHY_TO_LOG, 100, 0, &match,
+-                    ofpacts, hc_uuid);
++    struct match match = MATCH_CATCHALL_INITIALIZER;
+ 
+     /* Set allow rx from tunnel bit */
+     put_load(1, MFF_LOG_FLAGS, MLF_RX_FROM_TUNNEL_BIT, 1, ofpacts);
+     put_resubmit(OFTABLE_CT_ZONE_LOOKUP, ofpacts);
+ 
+-    /* Add specific flows for E/W ICMPv{4,6} packets if tunnelled packets
+-     * do not fit path MTU. */
+-
+     /* IPv4 ICMP flow (priority 120) */
+     match_init_catchall(&match);
+     match_set_in_port(&match, tun->ofport);
+@@ -398,6 +403,26 @@ add_tunnel_ingress_flows(const struct chassis_tunnel *tun,
+                     ofpacts, hc_uuid);
+ }
+ 
++static void
++add_tunnel_ingress_flows(const struct chassis_tunnel *tun,
++                         enum mf_field_id mff_ovn_geneve,
++                         struct ovn_desired_flow_table *flow_table,
++                         struct ofpbuf *ofpacts)
++{
++    /* Main ingress flow (priority 100) */
++    struct match match = MATCH_CATCHALL_INITIALIZER;
++    match_set_in_port(&match, tun->ofport);
++
++    ofpbuf_clear(ofpacts);
++    put_decapsulation(mff_ovn_geneve, tun, ofpacts);
++    put_resubmit(OFTABLE_LOCAL_OUTPUT, ofpacts);
++
++    ofctrl_add_flow(flow_table, OFTABLE_PHY_TO_LOG, 100, 0, &match,
++                    ofpacts, hc_uuid);
++
++    add_tunnel_ingress_pmtud_flows(tun, ofpacts, flow_table);
++}
++
+ static void
+ put_stack(enum mf_field_id field, struct ofpact_stack *stack)
+ {
+@@ -2827,12 +2852,6 @@ fanout_to_chassis_port_based(enum mf_field_id mff_ovn_geneve,
+     }
+ }
+ 
+-static bool
+-chassis_is_vtep(const struct sbrec_chassis *chassis)
+-{
+-    return smap_get_bool(&chassis->other_config, "is-vtep", false);
+-}
+-
+ static void
+ local_output_pb(int64_t tunnel_key, struct ofpbuf *ofpacts)
+ {
+@@ -3011,19 +3030,19 @@ consider_mc_group(const struct physical_ctx *ctx,
+              * otherwise multicast will reach remote ports through localnet
+              * port. */
+             if (port->chassis) {
+-                if (chassis_is_vtep(port->chassis)) {
++                if (is_ramp_tunnel(&port->chassis->other_config)) {
+                     sset_add(&vtep_chassis, port->chassis->name);
+                 } else {
+                     sset_add(&remote_chassis, port->chassis->name);
+                 }
+             }
+             for (size_t j = 0; j < port->n_additional_chassis; j++) {
+-                if (chassis_is_vtep(port->additional_chassis[j])) {
+-                    sset_add(&vtep_chassis,
+-                             port->additional_chassis[j]->name);
++                struct sbrec_chassis *additional_chassis =
++                    port->additional_chassis[j];
++                if (is_ramp_tunnel(&additional_chassis->other_config)) {
++                    sset_add(&vtep_chassis, additional_chassis->name);
+                 } else {
+-                    sset_add(&remote_chassis,
+-                             port->additional_chassis[j]->name);
++                    sset_add(&remote_chassis, additional_chassis->name);
+                 }
+             }
+         }
+@@ -3730,6 +3749,24 @@ physical_handle_flows_for_lport(const struct sbrec_port_binding *pb,
+         }
+     }
+ 
++    /* Chassisredirect ports on peer router datapaths may have bridged
++     * redirect flows that depend on this localnet port
++     * (put_remote_port_redirect_bridged() calls get_localnet_port()).
++     * Re-evaluate those CR ports. */
++    if (type == LP_LOCALNET && !removed && ldp) {
++        const struct peer_ports *pp;
++        VECTOR_FOR_EACH_PTR (&ldp->peer_ports, pp) {
++            const struct sbrec_port_binding *cr_pb =
++                lport_get_cr_port(p_ctx->sbrec_port_binding_by_name,
++                                  pp->remote, NULL);
++            if (cr_pb) {
++                ofctrl_remove_flows(flow_table, &cr_pb->header_.uuid);
++                physical_eval_port_binding(p_ctx, cr_pb, LP_CHASSISREDIRECT,
++                                           flow_table);
++            }
++        }
++    }
++
+     if (sbrec_port_binding_is_updated(
+             pb, SBREC_PORT_BINDING_COL_ADDITIONAL_CHASSIS) || removed) {
+         physical_multichassis_reprocess(pb, p_ctx, flow_table);
+@@ -3943,7 +3980,7 @@ physical_run(struct physical_ctx *p_ctx,
+     struct chassis_tunnel *tun;
+     HMAP_FOR_EACH (tun, hmap_node, p_ctx->chassis_tunnels) {
+         add_tunnel_ingress_flows(tun, p_ctx->mff_ovn_geneve, flow_table,
+-                                &ofpacts);
++                                 &ofpacts);
+     }
+ 
+     /* Process packets that arrive from flow-based tunnels. */
+@@ -3967,7 +4004,7 @@ physical_run(struct physical_ctx *p_ctx,
+                      i == GENEVE ? "geneve" : "vxlan");
+ 
+             add_tunnel_ingress_flows(&temp_tunnel, p_ctx->mff_ovn_geneve,
+-                                    flow_table, &ofpacts);
++                                     flow_table, &ofpacts);
+         }
+     }
+ 
+diff --git a/controller/pinctrl.c b/controller/pinctrl.c
+index 1a5407183c..bb8d20e7f4 100644
+--- a/controller/pinctrl.c
++++ b/controller/pinctrl.c
+@@ -185,16 +185,15 @@ struct pinctrl {
+ static struct pinctrl pinctrl;
+ 
+ static bool pinctrl_is_sb_commited(int64_t commit_cfg, int64_t cur_cfg);
+-static void init_buffered_packets_ctx(void);
+-static void destroy_buffered_packets_ctx(void);
++static void init_buffered_packets_map(void);
++static void destroy_buffered_packets_map(void);
+ static void
+ run_buffered_binding(const struct sbrec_mac_binding_table *mac_binding_table,
+                      const struct hmap *local_datapaths,
+                      struct ovsdb_idl_index *sbrec_port_binding_by_key,
+                      struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+                      struct ovsdb_idl_index *sbrec_port_binding_by_name,
+-                     struct ovsdb_idl_index *sbrec_mac_binding_by_lport_ip)
+-    OVS_REQUIRES(pinctrl_mutex);
++                     struct ovsdb_idl_index *sbrec_mac_binding_by_lport_ip);
+ 
+ static void pinctrl_handle_put_mac_binding(const struct flow *md,
+                                            const struct flow *headers,
+@@ -209,8 +208,7 @@ static void run_put_mac_bindings(
+     struct ovsdb_idl_index *sbrec_mac_binding_by_lport_ip)
+     OVS_REQUIRES(pinctrl_mutex);
+ static void wait_put_mac_bindings(void);
+-static void send_mac_binding_buffered_pkts(struct rconn *swconn)
+-    OVS_REQUIRES(pinctrl_mutex);
++static void send_mac_binding_buffered_pkts(struct rconn *swconn);
+ 
+ static void pinctrl_activation_strategy_handler(const struct match *md);
+ 
+@@ -389,6 +387,7 @@ COVERAGE_DEFINE(pinctrl_drop_buffered_packets_map);
+ COVERAGE_DEFINE(pinctrl_drop_controller_event);
+ COVERAGE_DEFINE(pinctrl_drop_put_vport_binding);
+ COVERAGE_DEFINE(pinctrl_notify_main_thread);
++COVERAGE_DEFINE(pinctrl_notify_handler_thread);
+ COVERAGE_DEFINE(pinctrl_total_pin_pkts);
+ 
+ /* DNS query statistics - thread-safe coverage counters */
+@@ -561,7 +560,7 @@ pinctrl_init(void)
+     init_send_arps_nds();
+     init_ipv6_ras();
+     init_ipv6_prefixd();
+-    init_buffered_packets_ctx();
++    init_buffered_packets_map();
+     init_activated_ports();
+     init_event_table();
+     ip_mcast_snoop_init();
+@@ -1576,18 +1575,18 @@ prepare_ipv6_prefixd(struct ovsdb_idl_txn *ovnsb_idl_txn,
+     }
+ }
+ 
+-static struct buffered_packets_ctx buffered_packets_ctx;
++static struct cmap buffered_packets_map;
+ 
+ static void
+-init_buffered_packets_ctx(void)
++init_buffered_packets_map(void)
+ {
+-    buffered_packets_ctx_init(&buffered_packets_ctx);
++    cmap_init(&buffered_packets_map);
+ }
+ 
+ static void
+-destroy_buffered_packets_ctx(void)
++destroy_buffered_packets_map(void)
+ {
+-    buffered_packets_ctx_destroy(&buffered_packets_ctx);
++    buffered_packets_map_destroy(&buffered_packets_map);
+ }
+ 
+ /* Called with in the pinctrl_handler thread context. */
+@@ -1595,7 +1594,6 @@ static void
+ pinctrl_handle_buffered_packets(const struct ofputil_packet_in *pin,
+                                 const struct ofpbuf *continuation,
+                                 bool is_arp)
+-OVS_REQUIRES(pinctrl_mutex)
+ {
+     const struct match *md = &pin->flow_metadata;
+     struct mac_binding_data mb_data;
+@@ -1612,7 +1610,7 @@ OVS_REQUIRES(pinctrl_mutex)
+                           md->flow.regs[MFF_LOG_OUTPORT - MFF_REG0],
+                           ip, eth_addr_zero);
+ 
+-    struct buffered_packets *bp = buffered_packets_add(&buffered_packets_ctx,
++    struct buffered_packets *bp = buffered_packets_add(&buffered_packets_map,
+                                                        mb_data);
+     if (!bp) {
+         COVERAGE_INC(pinctrl_drop_buffered_packets_map);
+@@ -1642,9 +1640,7 @@ pinctrl_handle_arp(struct rconn *swconn, const struct flow *ip_flow,
+         return;
+     }
+ 
+-    ovs_mutex_lock(&pinctrl_mutex);
+     pinctrl_handle_buffered_packets(pin, continuation, true);
+-    ovs_mutex_unlock(&pinctrl_mutex);
+ 
+     /* Compose an ARP packet. */
+     uint64_t packet_stub[128 / 8];
+@@ -4005,6 +4001,7 @@ pinctrl_recv(struct rconn *swconn, const struct ofp_header *oh,
+ static void
+ notify_pinctrl_handler(void)
+ {
++    COVERAGE_INC(pinctrl_notify_handler_thread);
+     seq_change(pinctrl_handler_seq);
+ }
+ 
+@@ -4079,12 +4076,12 @@ pinctrl_handler(void *arg_)
+                     send_arp_nd_run(swconn, &send_arp_nd_time);
+                     send_ipv6_ras(swconn, &send_ipv6_ra_time);
+                     send_ipv6_prefixd(swconn, &send_prefixd_time);
+-                    send_mac_binding_buffered_pkts(swconn);
+                     bfd_monitor_send_msg(swconn, &bfd_time);
+                     ovs_mutex_unlock(&pinctrl_mutex);
+                 } else {
+                     lock_failed = true;
+                 }
++                send_mac_binding_buffered_pkts(swconn);
+                 send_garp_rarp_run(swconn, &send_garp_rarp_time);
+                 ip_mcast_querier_run(swconn, &send_mcast_query_time);
+             }
+@@ -4118,6 +4115,8 @@ pinctrl_handler(void *arg_)
+         latch_wait(&pctrl->pinctrl_thread_exit);
+ 
+         ovsrcu_quiesce_start();
++        /* Wake-up periodicaly for coverage counters sync.*/
++        poll_timer_wait(1000);
+         poll_block();
+     }
+ 
+@@ -4222,11 +4221,6 @@ pinctrl_run(struct ovsdb_idl_txn *ovnsb_idl_txn,
+                   sbrec_port_binding_by_key,
+                   sbrec_igmp_groups,
+                   sbrec_ip_multicast_opts);
+-    run_buffered_binding(mac_binding_table, local_datapaths,
+-                         sbrec_port_binding_by_key,
+-                         sbrec_datapath_binding_by_key,
+-                         sbrec_port_binding_by_name,
+-                         sbrec_mac_binding_by_lport_ip);
+     sync_svc_monitors(ovnsb_idl_txn, svc_mon_table, sbrec_port_binding_by_name,
+                       chassis);
+     bfd_monitor_run(ovnsb_idl_txn, bfd_table, sbrec_port_binding_by_name,
+@@ -4237,6 +4231,12 @@ pinctrl_run(struct ovsdb_idl_txn *ovnsb_idl_txn,
+     run_activated_ports(ovnsb_idl_txn, sbrec_datapath_binding_by_key,
+                         sbrec_port_binding_by_key, chassis);
+     ovs_mutex_unlock(&pinctrl_mutex);
++
++    run_buffered_binding(mac_binding_table, local_datapaths,
++                         sbrec_port_binding_by_key,
++                         sbrec_datapath_binding_by_key,
++                         sbrec_port_binding_by_name,
++                         sbrec_mac_binding_by_lport_ip);
+ }
+ 
+ /* Table of ipv6_ra_state structures, keyed on logical port name.
+@@ -4754,7 +4754,7 @@ pinctrl_destroy(void)
+     destroy_send_arps_nds();
+     destroy_ipv6_ras();
+     destroy_ipv6_prefixd();
+-    destroy_buffered_packets_ctx();
++    destroy_buffered_packets_map();
+     destroy_activated_ports();
+     event_table_destroy();
+     destroy_put_mac_bindings();
+@@ -4839,30 +4839,24 @@ pinctrl_handle_put_mac_binding(const struct flow *md,
+     notify_pinctrl_main();
+ }
+ 
+-#define READY_PACKETS_VEC_CAPACITY_THRESHOLD 1024
+-
+ /* Called with in the pinctrl_handler thread context. */
+ static void
+ send_mac_binding_buffered_pkts(struct rconn *swconn)
+-    OVS_REQUIRES(pinctrl_mutex)
+ {
+     enum ofp_version version = rconn_get_version(swconn);
+     enum ofputil_protocol proto = ofputil_protocol_from_ofp_version(version);
+-    struct vector *rpd = &buffered_packets_ctx.ready_packets_data;
++    struct vector rpd = VECTOR_EMPTY_INITIALIZER(struct bp_packet_data);
++
++    buffered_packets_run(&buffered_packets_map, &rpd);
+ 
+     struct bp_packet_data *pd;
+-    VECTOR_FOR_EACH_PTR (rpd, pd) {
++    VECTOR_FOR_EACH_PTR (&rpd, pd) {
+         queue_msg(swconn, ofputil_encode_resume(&pd->pin, pd->continuation,
+                                                 proto));
+         bp_packet_data_destroy(pd);
+     }
+ 
+-    vector_clear(rpd);
+-    if (vector_capacity(rpd) >= READY_PACKETS_VEC_CAPACITY_THRESHOLD) {
+-        VLOG_DBG("The ready_packets_data vector capacity (%"PRIuSIZE") "
+-                 "is over threshold.", vector_capacity(rpd));
+-        vector_shrink_to_fit(rpd);
+-    }
++    vector_destroy(&rpd);
+ }
+ 
+ static void
+@@ -4931,9 +4925,8 @@ run_buffered_binding(const struct sbrec_mac_binding_table *mac_binding_table,
+                      struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+                      struct ovsdb_idl_index *sbrec_port_binding_by_name,
+                      struct ovsdb_idl_index *sbrec_mac_binding_by_lport_ip)
+-    OVS_REQUIRES(pinctrl_mutex)
+ {
+-    if (!buffered_packets_ctx_has_packets(&buffered_packets_ctx)) {
++    if (cmap_is_empty(&buffered_packets_map)) {
+         return;
+     }
+ 
+@@ -4979,18 +4972,16 @@ run_buffered_binding(const struct sbrec_mac_binding_table *mac_binding_table,
+         mac_binding_add(&recent_mbs, mb_data, smb, 0);
+     }
+ 
+-    buffered_packets_ctx_run(&buffered_packets_ctx, &recent_mbs,
+-                                 sbrec_port_binding_by_key,
+-                                 sbrec_datapath_binding_by_key,
+-                                 sbrec_port_binding_by_name,
+-                                 sbrec_mac_binding_by_lport_ip);
++    if (buffered_packets_lookup_run(&buffered_packets_map, &recent_mbs,
++                                    sbrec_port_binding_by_key,
++                                    sbrec_datapath_binding_by_key,
++                                    sbrec_port_binding_by_name,
++                                    sbrec_mac_binding_by_lport_ip)) {
++        notify_pinctrl_handler();
++    }
+ 
+     mac_bindings_clear(&recent_mbs);
+     hmap_destroy(&recent_mbs);
+-
+-    if (buffered_packets_ctx_is_ready_to_send(&buffered_packets_ctx)) {
+-        notify_pinctrl_handler();
+-    }
+ }
+ 
+ static void
+@@ -5819,6 +5810,9 @@ ip_mcast_sync(struct ovsdb_idl_txn *ovnsb_idl_txn,
+         struct ip_mcast_snoop_cfg cfg;
+         bool flush_groups = false;
+ 
++        if (!get_local_datapath(local_datapaths, dp_key)) {
++            continue;
++        }
+         ip_mcast_snoop_cfg_load(&cfg, ip_mcast);
+         if (ip_mcast_snoop_state_update(dp_key, &cfg, &flush_groups)) {
+             notify = true;
+@@ -6431,7 +6425,7 @@ may_inject_pkts(void)
+             !cmap_is_empty(&garp_rarp_get_data()->data) ||
+             ipv6_prefixd_should_inject() ||
+             !ovs_list_is_empty(&mcast_query_list) ||
+-            buffered_packets_ctx_is_ready_to_send(&buffered_packets_ctx) ||
++            !cmap_is_empty(&buffered_packets_map) ||
+             bfd_monitor_should_inject());
+ }
+ 
+@@ -6521,9 +6515,7 @@ pinctrl_handle_nd_ns(struct rconn *swconn, const struct flow *ip_flow,
+         return;
+     }
+ 
+-    ovs_mutex_lock(&pinctrl_mutex);
+     pinctrl_handle_buffered_packets(pin, continuation, false);
+-    ovs_mutex_unlock(&pinctrl_mutex);
+ 
+     uint64_t packet_stub[128 / 8];
+     struct dp_packet packet;
+@@ -6534,17 +6526,27 @@ pinctrl_handle_nd_ns(struct rconn *swconn, const struct flow *ip_flow,
+ 
+     /* We might be here without actually currently handling an IPv6 packet.
+      * This can happen in the case where we route IPv4 packets over an IPv6
+-     * link.
+-     * In these cases we have no destination IPv6 address from the packet that
+-     * we can reuse. But we receive the actual destination IPv6 address via
+-     * userdata anyway, so what we pass to compose_nd_ns is irrelevant.
+-     * This is just a hope since we do not parse the userdata. If we land here
+-     * for whatever reason without being an IPv6 packet and without userdata we
+-     * will send out a wrong packet.
+-     */
++     * link (e.g. RFC 5549 / BGP unnumbered, where an IPv4 destination is
++     * resolved via an IPv6 link-local nexthop).
++     *
++     * In that case we have no destination IPv6 address in the trigger packet
++     * to reuse. compose_nd_ns() needs a valid destination so it can derive
++     * the correct solicited-node multicast (ff02::1:ff{addr[13:16]}) for
++     * eth.dst and ip6.dst -- userdata only sets nd.target on the new packet
++     * and does not rewrite ip6.dst, so a wrong ipv6_dst here egresses on the
++     * wire as-is.
++     *
++     * The fallback nd_ns logical flow in S_ROUTER_IN_ARP_REQUEST stores the
++     * actual IPv6 nexthop in xxreg0 (REG_NEXT_HOP_IPV6) before invoking the
++     * nd_ns action, so for the IPv4-over-IPv6 case read xxreg0 from the
++     * trigger packet's flow metadata. */
+     struct in6_addr ipv6_dst = IN6ADDR_EXACT_INIT;
+     if (get_dl_type(ip_flow) == htons(ETH_TYPE_IPV6)) {
+         ipv6_dst = ip_flow->ipv6_dst;
++    } else {
++        ovs_be128 nexthop_be =
++            hton128(flow_get_xxreg(&pin->flow_metadata.flow, 0));
++        memcpy(&ipv6_dst, &nexthop_be, sizeof ipv6_dst);
+     }
+     compose_nd_ns(&packet, ip_flow->dl_src, &ipv6_src,
+                   &ipv6_dst);
+diff --git a/lib/expr.c b/lib/expr.c
+index 288e245c65..3c506c42c0 100644
+--- a/lib/expr.c
++++ b/lib/expr.c
+@@ -2366,7 +2366,7 @@ compare_expr_sort(const void *a_, const void *b_)
+         }
+ 
+         enum expr_type a_type = a->expr->type;
+-        enum expr_type b_type = a->expr->type;
++        enum expr_type b_type = b->expr->type;
+         return a_type < b_type ? -1 : a_type > b_type;
+     } else if (a->type == EXPR_T_AND || a->type == EXPR_T_OR) {
+         size_t a_len = ovs_list_size(&a->expr->andor);
 diff --git a/lib/ovn-util.h b/lib/ovn-util.h
-index 4ea5db1e22..5fe902da0a 100644
+index 4ea5db1e22..4ef93c470b 100644
 --- a/lib/ovn-util.h
 +++ b/lib/ovn-util.h
-@@ -548,8 +548,8 @@ dynamic_bitmap_last_set(const struct dynamic_bitmap *db)
+@@ -185,6 +185,7 @@ struct ovsdb_idl_txn *run_idl_loop(struct ovsdb_idl_loop *idl_loop,
+ #define OVN_EVPN_KEY_FLAG 31
+ #define OVN_MIN_EVPN_KEY (1u << OVN_EVPN_KEY_FLAG)
+ #define OVN_MAX_EVPN_KEY (OVN_MAX_DP_GLOBAL_NUM | OVN_MIN_EVPN_KEY)
++#define OVN_IS_EVPN_KEY(key) (((key) & OVN_MIN_EVPN_KEY) == OVN_MIN_EVPN_KEY)
+ 
+ struct hmap;
+ void ovn_destroy_tnlids(struct hmap *tnlids);
+@@ -548,8 +549,8 @@ dynamic_bitmap_last_set(const struct dynamic_bitmap *db)
              continue;
          }
  
@@ -13,6 +1525,1392 @@ index 4ea5db1e22..5fe902da0a 100644
      }
  
      return -1;
+diff --git a/northd/automake.mk b/northd/automake.mk
+index 8cd4fb3a13..45ca0337f9 100644
+--- a/northd/automake.mk
++++ b/northd/automake.mk
+@@ -44,8 +44,6 @@ northd_ovn_northd_SOURCES = \
+ 	northd/en-lr-stateful.h \
+ 	northd/en-ls-stateful.c \
+ 	northd/en-ls-stateful.h \
+-	northd/en-ls-arp.c \
+-	northd/en-ls-arp.h \
+ 	northd/en-sampling-app.c \
+ 	northd/en-sampling-app.h \
+ 	northd/en-acl-ids.c \
+diff --git a/northd/en-lflow.c b/northd/en-lflow.c
+index 704fae7095..752007e03a 100644
+--- a/northd/en-lflow.c
++++ b/northd/en-lflow.c
+@@ -23,7 +23,6 @@
+ #include "en-lr-nat.h"
+ #include "en-lr-stateful.h"
+ #include "en-ls-stateful.h"
+-#include "en-ls-arp.h"
+ #include "en-multicast.h"
+ #include "en-northd.h"
+ #include "en-meters.h"
+@@ -62,8 +61,6 @@ lflow_get_input_data(struct engine_node *node,
+         engine_get_input_data("lr_stateful", node);
+     struct ed_type_ls_stateful *ls_stateful_data =
+         engine_get_input_data("ls_stateful", node);
+-    struct ed_type_ls_arp *ls_arp_data =
+-        engine_get_input_data("ls_arp", node);
+     struct multicast_igmp_data *multicat_igmp_data =
+         engine_get_input_data("multicast_igmp", node);
+     struct ic_learned_svc_monitors_data *ic_learned_svc_monitors_data =
+@@ -90,7 +87,6 @@ lflow_get_input_data(struct engine_node *node,
+     lflow_input->ls_port_groups = &pg_data->ls_port_groups;
+     lflow_input->lr_stateful_table = &lr_stateful_data->table;
+     lflow_input->ls_stateful_table = &ls_stateful_data->table;
+-    lflow_input->ls_arp_table = &ls_arp_data->table;
+     lflow_input->meter_groups = &sync_meters_data->meter_groups;
+     lflow_input->lb_datapaths_map = &northd_data->lb_datapaths_map;
+     lflow_input->local_svc_monitors_map =
+@@ -230,31 +226,6 @@ lflow_ls_stateful_handler(struct engine_node *node, void *data)
+     return EN_HANDLED_UPDATED;
+ }
+ 
+-enum engine_input_handler_result
+-lflow_ls_arp_handler(struct engine_node *node, void *data)
+-{
+-    struct ed_type_ls_arp *ls_arp_data =
+-        engine_get_input_data("ls_arp", node);
+-
+-    if (!ls_arp_has_tracked_data(&ls_arp_data->trk_data)) {
+-        return EN_UNHANDLED;
+-    }
+-
+-    const struct engine_context *eng_ctx = engine_get_context();
+-    struct lflow_data *lflow_data = data;
+-    struct lflow_input lflow_input;
+-
+-    lflow_get_input_data(node, &lflow_input);
+-    if (!lflow_handle_ls_arp_changes(eng_ctx->ovnsb_idl_txn,
+-                                     &ls_arp_data->trk_data,
+-                                     &lflow_input,
+-                                     lflow_data->lflow_table)) {
+-        return EN_UNHANDLED;
+-    }
+-
+-    return EN_HANDLED_UPDATED;
+-}
+-
+ enum engine_input_handler_result
+ lflow_multicast_igmp_handler(struct engine_node *node, void *data)
+ {
+diff --git a/northd/en-lflow.h b/northd/en-lflow.h
+index d2a92e49f7..99bcfda151 100644
+--- a/northd/en-lflow.h
++++ b/northd/en-lflow.h
+@@ -25,8 +25,6 @@ lflow_lr_stateful_handler(struct engine_node *, void *data);
+ enum engine_input_handler_result
+ lflow_ls_stateful_handler(struct engine_node *node, void *data);
+ enum engine_input_handler_result
+-lflow_ls_arp_handler(struct engine_node *, void *);
+-enum engine_input_handler_result
+ lflow_multicast_igmp_handler(struct engine_node *node, void *data);
+ enum engine_input_handler_result
+ lflow_group_ecmp_route_change_handler(struct engine_node *node, void *data);
+diff --git a/northd/en-ls-arp.c b/northd/en-ls-arp.c
+deleted file mode 100644
+index 021f5054fe..0000000000
+--- a/northd/en-ls-arp.c
++++ /dev/null
+@@ -1,354 +0,0 @@
+-/*
+- * 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>
+-
+-/* OVS includes */
+-#include "include/openvswitch/hmap.h"
+-#include "openvswitch/util.h"
+-#include "openvswitch/vlog.h"
+-
+-/* OVN includes */
+-#include "en-lr-nat.h"
+-#include "en-ls-arp.h"
+-#include "lib/inc-proc-eng.h"
+-#include "lib/ovn-nb-idl.h"
+-#include "lib/ovn-sb-idl.h"
+-#include "lib/ovn-util.h"
+-#include "lflow-mgr.h"
+-#include "northd.h"
+-
+-VLOG_DEFINE_THIS_MODULE(en_ls_arp);
+-
+-/* Static functions. */
+-struct ls_arp_input {
+-    const struct ovn_datapaths *ls_datapaths;
+-    const struct lr_nat_table *lr_nats;
+-};
+-
+-static struct ls_arp_input
+-ls_arp_get_input_data(struct engine_node *node)
+-{
+-    const struct northd_data *northd_data =
+-        engine_get_input_data("northd", node);
+-    struct ed_type_lr_nat_data *lr_nat_data =
+-        engine_get_input_data("lr_nat", node);
+-
+-    return (struct ls_arp_input) {
+-        .ls_datapaths = &northd_data->ls_datapaths,
+-        .lr_nats = &lr_nat_data->lr_nats,
+-    };
+-}
+-
+-static void
+-ls_arp_record_clear(struct ls_arp_record *ls_arp_record)
+-{
+-    lflow_ref_destroy(ls_arp_record->lflow_ref);
+-    hmapx_destroy(&ls_arp_record->nat_records);
+-    free(ls_arp_record);
+-}
+-
+-static void
+-ls_arp_table_clear(struct ls_arp_table *table)
+-{
+-    struct ls_arp_record *ls_arp_record;
+-    HMAP_FOR_EACH_POP (ls_arp_record, key_node, &table->entries) {
+-        ls_arp_record_clear(ls_arp_record);
+-    }
+-}
+-
+-static inline bool
+-is_nat_dgp_connected(const struct ovn_nat *nat_entry)
+-{
+-    return nat_entry->is_valid
+-           && nat_entry->l3dgw_port
+-           && nat_entry->l3dgw_port->peer
+-           && nat_entry->l3dgw_port->peer->od;
+-}
+-
+-static void
+-nat_record_data_create(struct ls_arp_record *ls_arp_record,
+-                       const struct ovn_datapath *od,
+-                       const struct lr_nat_table *lr_nats)
+-{
+-    struct ovn_port *op;
+-    VECTOR_FOR_EACH (&od->router_ports, op) {
+-        const struct ovn_datapath *lr_od = op->peer->od;
+-        const struct lr_nat_record *lrnat_rec =
+-            lr_nat_table_find_by_uuid(lr_nats, lr_od->key);
+-
+-        if (!lrnat_rec) {
+-            continue;
+-        }
+-
+-        for (size_t i = 0; i < lrnat_rec->n_nat_entries; i++) {
+-            const struct ovn_nat *nat_entry = &lrnat_rec->nat_entries[i];
+-
+-            if (is_nat_dgp_connected(nat_entry)) {
+-                hmapx_add(&ls_arp_record->nat_records,
+-                          (struct lrnat_rec *) lrnat_rec);
+-                continue;
+-            }
+-        }
+-    }
+-}
+-
+-static struct ls_arp_record *
+-ls_arp_record_lookup_by_od_(const struct ls_arp_table *table,
+-                            const struct ovn_datapath *od)
+-{
+-    struct ls_arp_record *ls_arp_record;
+-    HMAP_FOR_EACH_WITH_HASH (ls_arp_record, key_node,
+-                             uuid_hash(&od->nbs->header_.uuid),
+-                             &table->entries) {
+-        if (uuid_equals(&ls_arp_record->nbs_uuid,
+-                        &od->nbs->header_.uuid)) {
+-            return ls_arp_record;
+-        }
+-    }
+-
+-    return NULL;
+-}
+-
+-static struct ls_arp_record *
+-ls_arp_record_create(struct ls_arp_table *table,
+-                     const struct ovn_datapath *od,
+-                     const struct lr_nat_table *lr_nats)
+-{
+-    struct ls_arp_record *ls_arp_record = xzalloc(sizeof *ls_arp_record);
+-
+-    ls_arp_record->ls_index = od->sdp->index;
+-    ls_arp_record->nbs_uuid = od->nbs->header_.uuid;
+-
+-    hmapx_init(&ls_arp_record->nat_records);
+-    nat_record_data_create(ls_arp_record, od, lr_nats);
+-
+-    ls_arp_record->lflow_ref = lflow_ref_create();
+-
+-    hmap_insert(&table->entries, &ls_arp_record->key_node,
+-                uuid_hash(&od->nbs->header_.uuid));
+-
+-    return ls_arp_record;
+-}
+-
+-/* Public functions. */
+-void*
+-en_ls_arp_init(struct engine_node *node OVS_UNUSED,
+-               struct engine_arg *arg OVS_UNUSED)
+-{
+-    struct ed_type_ls_arp *data = xzalloc(sizeof *data);
+-
+-    hmap_init(&data->table.entries);
+-    hmapx_init(&data->trk_data.crupdated);
+-    hmapx_init(&data->trk_data.deleted);
+-
+-    return data;
+-}
+-
+-void
+-en_ls_arp_clear_tracked_data(void *data_)
+-{
+-    struct ed_type_ls_arp *data = data_;
+-    hmapx_clear(&data->trk_data.crupdated);
+-
+-    struct hmapx_node *n;
+-    HMAPX_FOR_EACH_SAFE (n, &data->trk_data.deleted) {
+-        ls_arp_record_clear(n->data);
+-        hmapx_delete(&data->trk_data.deleted, n);
+-    }
+-    hmapx_clear(&data->trk_data.deleted);
+-}
+-
+-void
+-en_ls_arp_cleanup(void *data_)
+-{
+-    struct ed_type_ls_arp *data = data_;
+-
+-    ls_arp_table_clear(&data->table);
+-    hmap_destroy(&data->table.entries);
+-    hmapx_destroy(&data->trk_data.crupdated);
+-
+-    struct hmapx_node *n;
+-    HMAPX_FOR_EACH_SAFE (n, &data->trk_data.deleted) {
+-        ls_arp_record_clear(n->data);
+-        hmapx_delete(&data->trk_data.deleted, n);
+-    }
+-    hmapx_destroy(&data->trk_data.deleted);
+-}
+-
+-enum engine_node_state
+-en_ls_arp_run(struct engine_node *node, void *data_)
+-{
+-    struct ls_arp_input input_data = ls_arp_get_input_data(node);
+-    struct ed_type_ls_arp *data = data_;
+-
+-    ls_arp_table_clear(&data->table);
+-
+-    const struct ovn_datapath *od;
+-    HMAP_FOR_EACH (od, key_node, &input_data.ls_datapaths->datapaths) {
+-        /* Filtering ARP entries at logical switch works
+-         * when there are physical ports on the switch. */
+-        if (hmapx_is_empty(&od->phys_ports)) {
+-            continue;
+-        }
+-
+-        ls_arp_record_create(&data->table, od, input_data.lr_nats);
+-    }
+-
+-    return EN_UPDATED;
+-}
+-
+-/* Handler functions. */
+-enum engine_input_handler_result
+-ls_arp_northd_handler(struct engine_node *node, void *data_)
+-{
+-    struct northd_data *northd_data = engine_get_input_data("northd", node);
+-    if (!northd_has_tracked_data(&northd_data->trk_data)) {
+-        return EN_UNHANDLED;
+-    }
+-
+-    if (!northd_has_lswitches_in_tracked_data(&northd_data->trk_data)) {
+-        return EN_HANDLED_UNCHANGED;
+-    }
+-
+-    struct northd_tracked_data *nd_changes = &northd_data->trk_data;
+-    struct ls_arp_input input_data = ls_arp_get_input_data(node);
+-    struct ed_type_ls_arp *data = data_;
+-    struct hmapx_node *hmapx_node;
+-    struct ls_arp_record *ls_arp_record;
+-
+-    HMAPX_FOR_EACH (hmapx_node, &nd_changes->trk_switches.crupdated) {
+-        const struct ovn_datapath *od = hmapx_node->data;
+-
+-        ls_arp_record = ls_arp_record_lookup_by_od_(&data->table, od);
+-
+-        if (!ls_arp_record) {
+-            /* Filtering ARP entries at logical switch works
+-             * when there are physical ports on the switch. */
+-            if (hmapx_is_empty(&od->phys_ports)) {
+-                /* NOTE: If the switch used to have physical ports but those
+-                 * were removed the lr_nat node has recomputed and triggers
+-                 * the ls_arp_lr_nat_handler() which cannot incrementally
+-                 * process changes.  This implicitly triggers correct
+-                 * handling of the removal.*/
+-                continue;
+-            }
+-            ls_arp_record = ls_arp_record_create(&data->table,
+-                                                 od, input_data.lr_nats);
+-        } else {
+-            nat_record_data_create(ls_arp_record, od, input_data.lr_nats);
+-        }
+-
+-        hmapx_add(&data->trk_data.crupdated, ls_arp_record);
+-    }
+-
+-    HMAPX_FOR_EACH (hmapx_node, &nd_changes->trk_switches.deleted) {
+-        const struct ovn_datapath *od = hmapx_node->data;
+-
+-        ls_arp_record = ls_arp_record_lookup_by_od_(&data->table, od);
+-        if (ls_arp_record) {
+-            hmap_remove(&data->table.entries, &ls_arp_record->key_node);
+-            hmapx_add(&data->trk_data.deleted, ls_arp_record);
+-        }
+-    }
+-
+-    if (ls_arp_has_tracked_data(&data->trk_data)) {
+-        return EN_HANDLED_UPDATED;
+-    }
+-
+-    return EN_HANDLED_UNCHANGED;
+-}
+-
+-static void
+-nat_odmap_create(struct lr_nat_record *lrnat_rec,
+-                 struct hmapx *odmap)
+-{
+-    for (size_t i = 0; i < lrnat_rec->n_nat_entries; i++) {
+-        const struct ovn_nat *nat_entry = &lrnat_rec->nat_entries[i];
+-
+-        if (is_nat_dgp_connected(nat_entry)) {
+-            hmapx_add(odmap, nat_entry->l3dgw_port->peer->od);
+-        }
+-    }
+-}
+-
+-enum engine_input_handler_result
+-ls_arp_lr_nat_handler(struct engine_node *node, void *data_)
+-{
+-    struct ed_type_lr_nat_data *lr_nat_data =
+-        engine_get_input_data("lr_nat", node);
+-    struct ls_arp_input input_data = ls_arp_get_input_data(node);
+-
+-    if (!lr_nat_has_tracked_data(&lr_nat_data->trk_data)) {
+-        return EN_UNHANDLED;
+-    }
+-
+-    struct ed_type_ls_arp *data = data_;
+-
+-    struct hmapx_node *hmapx_node;
+-    struct ls_arp_record *ls_arp_record;
+-    HMAPX_FOR_EACH (hmapx_node, &lr_nat_data->trk_data.crupdated) {
+-        struct lr_nat_record *nat_record_p = hmapx_node->data;
+-
+-        struct hmapx ls_links_map = HMAPX_INITIALIZER(&ls_links_map);
+-        nat_odmap_create(nat_record_p, &ls_links_map);
+-
+-        LS_ARP_TABLE_FOR_EACH (ls_arp_record, &data->table) {
+-            struct hmapx_node *nr_node =
+-                hmapx_find(&ls_arp_record->nat_records, nat_record_p);
+-
+-            if (nr_node) {
+-                hmapx_add(&data->trk_data.crupdated, ls_arp_record);
+-                hmapx_delete(&ls_arp_record->nat_records, nr_node);
+-            }
+-        }
+-
+-        struct hmapx_node *crupdated_ls_hmapx;
+-        HMAPX_FOR_EACH (crupdated_ls_hmapx, &ls_links_map) {
+-            struct ovn_datapath *crupdated_ls = crupdated_ls_hmapx->data;
+-            ls_arp_record =
+-                ls_arp_record_lookup_by_od_(&data->table, crupdated_ls);
+-
+-            if (!ls_arp_record) {
+-                ls_arp_record = ls_arp_record_create(&data->table,
+-                                                     crupdated_ls,
+-                                                     input_data.lr_nats);
+-            }
+-
+-            hmapx_add(&data->trk_data.crupdated, ls_arp_record);
+-            hmapx_add(&ls_arp_record->nat_records, nat_record_p);
+-        }
+-        hmapx_destroy(&ls_links_map);
+-    }
+-
+-    HMAPX_FOR_EACH (hmapx_node, &lr_nat_data->trk_data.deleted) {
+-        struct lr_nat_record *nr_cur = hmapx_node->data;
+-
+-        struct ls_arp_record *ar;
+-        LS_ARP_TABLE_FOR_EACH (ar, &data->table) {
+-            struct hmapx_node *nr_node = hmapx_find(&ar->nat_records, nr_cur);
+-
+-            if (nr_node) {
+-                hmapx_add(&data->trk_data.crupdated, ar);
+-                hmapx_delete(&ar->nat_records, nr_node);
+-            }
+-        }
+-    }
+-
+-    if (ls_arp_has_tracked_data(&data->trk_data)) {
+-        return EN_HANDLED_UPDATED;
+-    }
+-
+-    return EN_HANDLED_UNCHANGED;
+-}
+diff --git a/northd/en-ls-arp.h b/northd/en-ls-arp.h
+deleted file mode 100644
+index 5eaf913bb6..0000000000
+--- a/northd/en-ls-arp.h
++++ /dev/null
+@@ -1,86 +0,0 @@
+-/*
+- * 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 EN_LS_ARP_H
+-#define EN_LS_ARP_H 1
+-
+-/* OVS includes. */
+-#include "lib/hmapx.h"
+-#include "openvswitch/hmap.h"
+-
+-/* OVN includes. */
+-#include "lib/inc-proc-eng.h"
+-#include "lib/ovn-nb-idl.h"
+-#include "lib/ovn-sb-idl.h"
+-#include "lib/ovn-util.h"
+-#include "lib/stopwatch-names.h"
+-
+-struct lflow_ref;
+-
+-struct ls_arp_record {
+-    struct hmap_node key_node;
+-
+-    /* UUID of the NB Logical switch. */
+-    struct uuid nbs_uuid;
+-
+-    /* Index of logical switch item in northd. */
+-    size_t ls_index;
+-
+-    /* 'lflow_ref' is used to reference logical flows generated for
+-     * this ls_arp record. */
+-    struct lflow_ref *lflow_ref;
+-
+-    /* lr_nat_record ptrs that trigger this od to rebuild lflow. */
+-    struct hmapx nat_records;
+-};
+-
+-struct ls_arp_table {
+-    struct hmap entries;
+-};
+-
+-#define LS_ARP_TABLE_FOR_EACH(LS_ARP_REC, TABLE) \
+-    HMAP_FOR_EACH (LS_ARP_REC, key_node, \
+-                   &(TABLE)->entries)
+-
+-#define LS_ARP_TABLE_FOR_EACH_IN_P(LS_ARP_REC, JOBID, TABLE) \
+-    HMAP_FOR_EACH_IN_PARALLEL (LS_ARP_REC, key_node, JOBID, \
+-                               &(TABLE)->entries)
+-
+-struct ls_arp_tracked_data {
+-    struct hmapx crupdated;
+-    struct hmapx deleted;
+-};
+-
+-struct ed_type_ls_arp {
+-    struct ls_arp_table table;
+-    struct ls_arp_tracked_data trk_data;
+-};
+-
+-void *en_ls_arp_init(struct engine_node *, struct engine_arg *);
+-void en_ls_arp_cleanup(void *);
+-void en_ls_arp_clear_tracked_data(void *);
+-enum engine_node_state en_ls_arp_run(struct engine_node *, void *);
+-
+-enum engine_input_handler_result
+-ls_arp_lr_nat_handler(struct engine_node *, void *);
+-enum engine_input_handler_result
+-ls_arp_northd_handler(struct engine_node *, void *);
+-
+-static inline bool
+-ls_arp_has_tracked_data(struct ls_arp_tracked_data *trk_data) {
+-    return !hmapx_is_empty(&trk_data->crupdated) ||
+-           !hmapx_is_empty(&trk_data->deleted);
+-}
+-
+-#endif /* EN_LS_ARP_H */
+diff --git a/northd/en-multicast.c b/northd/en-multicast.c
+index a7dfd71c47..b2775516a9 100644
+--- a/northd/en-multicast.c
++++ b/northd/en-multicast.c
+@@ -710,7 +710,7 @@ ovn_igmp_group_aggregate_ports(struct ovn_igmp_group *igmp_group,
+         free(entry);
+     }
+ 
+-    if (!vector_is_empty(&igmp_group->datapath->localnet_ports)) {
++    if (ls_has_localnet_port(igmp_group->datapath)) {
+         ovn_multicast_add_ports(mcast_groups, igmp_group->datapath,
+             &igmp_group->mcgroup,
+             vector_get_array(&igmp_group->datapath->localnet_ports),
+diff --git a/northd/inc-proc-northd.c b/northd/inc-proc-northd.c
+index 1884f98a07..dffac73090 100644
+--- a/northd/inc-proc-northd.c
++++ b/northd/inc-proc-northd.c
+@@ -34,7 +34,6 @@
+ #include "en-lr-stateful.h"
+ #include "en-lr-nat.h"
+ #include "en-ls-stateful.h"
+-#include "en-ls-arp.h"
+ #include "en-multicast.h"
+ #include "en-northd.h"
+ #include "en-lflow.h"
+@@ -176,7 +175,6 @@ static ENGINE_NODE(lb_data, CLEAR_TRACKED_DATA);
+ static ENGINE_NODE(lr_nat, CLEAR_TRACKED_DATA);
+ static ENGINE_NODE(lr_stateful, CLEAR_TRACKED_DATA);
+ static ENGINE_NODE(ls_stateful, CLEAR_TRACKED_DATA);
+-static ENGINE_NODE(ls_arp, CLEAR_TRACKED_DATA);
+ static ENGINE_NODE(route_policies);
+ static ENGINE_NODE(routes);
+ static ENGINE_NODE(bfd);
+@@ -310,9 +308,6 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
+                      ls_stateful_port_group_handler);
+     engine_add_input(&en_ls_stateful, &en_nb_acl, ls_stateful_acl_handler);
+ 
+-    engine_add_input(&en_ls_arp, &en_lr_nat, ls_arp_lr_nat_handler);
+-    engine_add_input(&en_ls_arp, &en_northd, ls_arp_northd_handler);
+-
+     engine_add_input(&en_mac_binding_aging, &en_sb_mac_binding, NULL);
+     engine_add_input(&en_mac_binding_aging, &en_northd, NULL);
+     engine_add_input(&en_mac_binding_aging, &en_mac_binding_aging_waker, NULL);
+@@ -422,7 +417,6 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
+     engine_add_input(&en_lflow, &en_port_group, engine_noop_handler);
+     engine_add_input(&en_lflow, &en_lr_stateful, lflow_lr_stateful_handler);
+     engine_add_input(&en_lflow, &en_ls_stateful, lflow_ls_stateful_handler);
+-    engine_add_input(&en_lflow, &en_ls_arp, lflow_ls_arp_handler);
+     engine_add_input(&en_lflow, &en_multicast_igmp,
+                      lflow_multicast_igmp_handler);
+     engine_add_input(&en_lflow, &en_sb_acl_id, NULL);
+diff --git a/northd/northd.c b/northd/northd.c
+index a4b32ee505..b948db6884 100644
+--- a/northd/northd.c
++++ b/northd/northd.c
+@@ -50,7 +50,6 @@
+ #include "en-lr-nat.h"
+ #include "en-lr-stateful.h"
+ #include "en-ls-stateful.h"
+-#include "en-ls-arp.h"
+ #include "en-multicast.h"
+ #include "en-sampling-app.h"
+ #include "en-datapath-logical-switch.h"
+@@ -143,7 +142,6 @@ static bool vxlan_mode;
+ #define REGBIT_IP_FRAG            "reg0[19]"
+ #define REGBIT_ACL_PERSIST_ID     "reg0[20]"
+ #define REGBIT_ACL_HINT_ALLOW_PERSISTED "reg0[21]"
+-#define REGBIT_EXT_ARP            "reg0[22]"
+ 
+ /* Register definitions for switches and routers. */
+ 
+@@ -571,7 +569,6 @@ ovn_datapath_create(struct hmap *datapaths, const struct uuid *key,
+     hmap_insert(datapaths, &od->key_node, uuid_hash(&od->key));
+     od->lr_group = NULL;
+     hmap_init(&od->ports);
+-    hmapx_init(&od->phys_ports);
+     sset_init(&od->router_ips);
+     od->ls_peers = VECTOR_EMPTY_INITIALIZER(struct ovn_datapath *);
+     od->router_ports = VECTOR_EMPTY_INITIALIZER(struct ovn_port *);
+@@ -611,7 +608,6 @@ ovn_datapath_destroy(struct ovn_datapath *od)
+         vector_destroy(&od->l3dgw_ports);
+         destroy_mcast_info_for_datapath(od);
+         destroy_ports_for_datapath(od);
+-        hmapx_destroy(&od->phys_ports);
+         sset_destroy(&od->router_ips);
+         free(od);
+     }
+@@ -1244,12 +1240,6 @@ lsp_is_vtep(const struct nbrec_logical_switch_port *nbsp)
+     return !strcmp(nbsp->type, "vtep");
+ }
+ 
+-static bool
+-lsp_is_l2gw(const struct nbrec_logical_switch_port *nbsp)
+-{
+-    return !strcmp(nbsp->type, "l2gateway");
+-}
+-
+ static bool
+ localnet_can_learn_mac(const struct nbrec_logical_switch_port *nbsp)
+ {
+@@ -1635,10 +1625,6 @@ join_logical_ports_lsp(struct hmap *ports,
+         od->has_vtep_lports = true;
+     }
+ 
+-    if (lsp_is_localnet(nbsp) || lsp_is_l2gw(nbsp)) {
+-        hmapx_add(&od->phys_ports, op);
+-    }
+-
+     parse_lsp_addrs(op);
+ 
+     op->od = od;
+@@ -1782,7 +1768,7 @@ peer_needs_cr_port_creation(struct ovn_port *op)
+ {
+     if ((op->nbrp->n_gateway_chassis || op->nbrp->ha_chassis_group)
+         && vector_len(&op->od->l3dgw_ports) == 1 && op->peer && op->peer->nbsp
+-        && vector_is_empty(&op->peer->od->localnet_ports)) {
++        && !ls_has_localnet_port(op->peer->od)) {
+         return true;
+     }
+ 
+@@ -2742,7 +2728,7 @@ ovn_port_update_sbrec(struct ovsdb_idl_txn *ovnsb_txn,
+             smap_clone(&options, &op->nbsp->options);
+ 
+             if (queue_id) {
+-                if (!vector_is_empty(&op->od->localnet_ports)) {
++                if (ls_has_localnet_port(op->od)) {
+                     struct ovn_port *port = vector_get(&op->od->localnet_ports,
+                                                        0, struct ovn_port *);
+                     const char *physical_network = smap_get(
+@@ -3856,7 +3842,7 @@ should_add_router_port_garp(const struct ovn_port *op, const char *chassis)
+                                 vector_len(&op->peer->od->l3dgw_ports));
+             }
+         }
+-    } else if (chassis && !vector_is_empty(&op->od->localnet_ports)) {
++    } else if (chassis && ls_has_localnet_port(op->od)) {
+         add_router_port_garp = true;
+     }
+ 
+@@ -5022,6 +5008,9 @@ northd_handle_ls_changes(struct ovsdb_idl_txn *ovnsb_idl_txn,
+             goto fail;
+         }
+ 
++        if (sparse_array_get(&nd->ls_datapaths.dps, synced->sdp->index)) {
++            goto fail;
++        }
+         struct ovn_datapath *od = ovn_datapath_create(
+             &nd->ls_datapaths.datapaths, &new_ls->header_.uuid, new_ls,
+             NULL, synced->sdp);
+@@ -5354,6 +5343,9 @@ northd_handle_lr_changes(const struct northd_input *ni,
+         if (new_lr->copp || (new_lr->n_ports > 0)) {
+             goto fail;
+         }
++        if (sparse_array_get(&nd->lr_datapaths.dps, synced->sdp->index)) {
++            goto fail;
++        }
+         struct ovn_datapath *od = ovn_datapath_create(
+             &nd->lr_datapaths.datapaths, &new_lr->header_.uuid,
+             NULL, new_lr, synced->sdp);
+@@ -6154,7 +6146,7 @@ build_lswitch_port_sec_op(struct ovn_port *op, struct lflow_table *lflows,
+                       WITH_IO_PORT(op->key), WITH_HINT(&op->nbsp->header_));
+ 
+         if (!lsp_is_localnet(op->nbsp) &&
+-            vector_is_empty(&op->od->localnet_ports)) {
++            !ls_has_localnet_port(op->od)) {
+             return;
+         }
+ 
+@@ -6168,7 +6160,7 @@ build_lswitch_port_sec_op(struct ovn_port *op, struct lflow_table *lflows,
+                           ds_cstr(match), ds_cstr(actions), op->lflow_ref,
+                           WITH_IO_PORT(op->key),
+                           WITH_HINT(&op->nbsp->header_));
+-        } else if (!vector_is_empty(&op->od->localnet_ports)) {
++        } else if (ls_has_localnet_port(op->od)) {
+             const struct ovn_port *lp = vector_get(&op->od->localnet_ports, 0,
+                                                    struct ovn_port *);
+             ds_put_format(match, "outport == %s && inport == %s",
+@@ -10050,88 +10042,6 @@ build_drop_arp_nd_flows_for_unbound_router_ports(struct ovn_port *op,
+     ds_destroy(&match);
+ }
+ 
+-/*
+- * Create ARP filtering flow for od, assumed logical switch,
+- * for the following condition:
+- * Given lswitch has either localnet or l2gateway ports and
+- * router connection ports that requires chassis residence.
+- * ARP requests coming from localnet/l2gateway ports
+- * allowed for processing on resident chassis only.
+- */
+-static void
+-build_lswitch_arp_chassis_resident(const struct ovn_datapath *od,
+-                                   struct lflow_table *lflows,
+-                                   const struct ls_arp_record *ar)
+-{
+-    struct hmapx resident_ports = HMAPX_INITIALIZER(&resident_ports);
+-    struct ds match = DS_EMPTY_INITIALIZER;
+-
+-    struct ovn_port *op;
+-    VECTOR_FOR_EACH (&od->router_ports, op) {
+-        struct ovn_port *op_r = op->peer;
+-
+-        if (lrp_is_l3dgw(op_r)) {
+-            hmapx_add(&resident_ports, op_r->cr_port);
+-        } else if (op_r->od->is_gw_router) {
+-            hmapx_add(&resident_ports, op_r);
+-        }
+-    }
+-
+-    if (!hmapx_is_empty(&od->phys_ports) && !hmapx_is_empty(&resident_ports)) {
+-        struct hmapx_node *node;
+-
+-        HMAPX_FOR_EACH (node, &od->phys_ports) {
+-            op = node->data;
+-
+-            ds_clear(&match);
+-            ds_put_format(&match, "arp.op == 1 && inport == %s",
+-                          op->json_key);
+-            ovn_lflow_add(lflows, od, S_SWITCH_IN_CHECK_PORT_SEC, 75,
+-                          ds_cstr(&match), REGBIT_EXT_ARP " = 1; next;",
+-                          ar->lflow_ref);
+-        }
+-
+-        HMAPX_FOR_EACH (node, &resident_ports) {
+-            op = node->data;
+-
+-            ds_clear(&match);
+-            ds_put_format(&match, REGBIT_EXT_ARP" == 1");
+-            if (od_is_centralized(op->od)) {
+-                ds_put_format(&match, " && is_chassis_resident(%s)",
+-                              op->json_key);
+-            }
+-            ovn_lflow_add(lflows, od, S_SWITCH_IN_APPLY_PORT_SEC, 75,
+-                          ds_cstr(&match), "next;", ar->lflow_ref);
+-        }
+-
+-        struct hmapx_node *hmapx_node;
+-        HMAPX_FOR_EACH (hmapx_node, &ar->nat_records) {
+-            struct lr_nat_record *nr = hmapx_node->data;
+-            for (size_t i = 0; i < nr->n_nat_entries; i++) {
+-                struct ovn_nat *ent = &nr->nat_entries[i];
+-                if (!ent->is_valid || !ent->is_distributed ||
+-                    nat_entry_is_v6(ent)) {
+-                    continue;
+-                }
+-
+-                ds_clear(&match);
+-                ds_put_format(&match, REGBIT_EXT_ARP " == 1 && arp.tpa == %s "
+-                              "&& is_chassis_resident(\"%s\")",
+-                              ent->ext_addrs.ipv4_addrs[0].addr_s,
+-                              ent->nb->logical_port);
+-                ovn_lflow_add(lflows, od, S_SWITCH_IN_APPLY_PORT_SEC, 85,
+-                              ds_cstr(&match), "next;", ar->lflow_ref);
+-            }
+-        }
+-
+-        ovn_lflow_add(lflows, od, S_SWITCH_IN_APPLY_PORT_SEC, 70,
+-                      REGBIT_EXT_ARP" == 1", "drop;", ar->lflow_ref);
+-    }
+-
+-    hmapx_destroy(&resident_ports);
+-    ds_destroy(&match);
+-}
+-
+ static bool
+ is_vlan_transparent(const struct ovn_datapath *od)
+ {
+@@ -10293,25 +10203,43 @@ build_arp_nd_service_monitor_lflow(const char *svc_monitor_mac,
+     }
+ }
+ 
+-/* Ingress table 24: ARP/ND responder, skip requests coming from localnet
+- * ports. (priority 100); see ovn-northd.8.xml for the rationale. */
+-
++/* Ingress table: Lookup FDB.  Set flags.localnet for packets arriving from
++ * localnet ports so that downstream stages (e.g., ARP/ND responder) can
++ * condition their behavior on whether the packet came from localnet. */
+ static void
+-build_lswitch_arp_nd_responder_skip_local(struct ovn_port *op,
+-                                          struct lflow_table *lflows,
+-                                          struct ds *match)
++build_lswitch_from_localnet_op(struct ovn_port *op,
++                               struct lflow_table *lflows,
++                               struct ds *match)
+ {
+     ovs_assert(op->nbsp);
+-    if (!lsp_is_localnet(op->nbsp) || op->od->has_arp_proxy_port) {
++    if (!lsp_is_localnet(op->nbsp)) {
+         return;
+     }
+     ds_clear(match);
+     ds_put_format(match, "inport == %s", op->json_key);
+-    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100, ds_cstr(match),
+-                  "next;", op->lflow_ref, WITH_IO_PORT(op->key),
++    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_LOOKUP_FDB, 50,
++                  ds_cstr(match), "flags.localnet = 1; next;",
++                  op->lflow_ref, WITH_IO_PORT(op->key),
+                   WITH_HINT(&op->nbsp->header_));
+ }
+ 
++/* On switches with localnet ports, restrict ARP/ND replies for
++ * localnet-sourced requests to the chassis hosting the target VIF
++ * (preventing duplicate replies from every hypervisor).  Non-localnet
++ * requests (VIF-to-VIF) are answered unconditionally as before. */
++static void
++build_lswitch_arp_nd_local_resp_match(struct ds *match,
++                                      const struct ovn_port *op)
++{
++    if (!ls_has_localnet_port(op->od)) {
++        return;
++    }
++
++    ds_put_format(match,
++        " && ((flags.localnet == 1 && is_chassis_resident(%s))"
++            " || flags.localnet == 0)", op->json_key);
++}
++
+ /* Ingress table 24: ARP/ND responder, reply for known IPs.
+  * (priority 50). */
+ static void
+@@ -10453,6 +10381,8 @@ build_lswitch_arp_nd_responder_known_ips(struct ovn_port *op,
+                     ds_truncate(match, match_len);
+                 }
+                 ds_put_cstr(match, " && eth.dst == ff:ff:ff:ff:ff:ff");
++                size_t match_arp_len = match->length;
++                build_lswitch_arp_nd_local_resp_match(match, op);
+ 
+                 ds_clear(actions);
+                 ds_put_format(actions,
+@@ -10484,6 +10414,7 @@ build_lswitch_arp_nd_responder_known_ips(struct ovn_port *op,
+                  * address is intended to detect situations where the
+                  * network is not working as configured, so dropping the
+                  * request would frustrate that intent.) */
++                ds_truncate(match, match_arp_len);
+                 ds_put_format(match, " && inport == %s", op->json_key);
+                 ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
+                               ds_cstr(match), "next;", op->lflow_ref,
+@@ -10523,6 +10454,8 @@ build_lswitch_arp_nd_responder_known_ips(struct ovn_port *op,
+                     "nd_ns_mcast && ip6.dst == %s && nd.target == %s",
+                     op->lsp_addrs[i].ipv6_addrs[j].sn_addr_s,
+                     op->lsp_addrs[i].ipv6_addrs[j].addr_s);
++                size_t match_nd_len = match->length;
++                build_lswitch_arp_nd_local_resp_match(match, op);
+ 
+                 ds_clear(actions);
+                 ds_put_format(actions,
+@@ -10549,6 +10482,7 @@ build_lswitch_arp_nd_responder_known_ips(struct ovn_port *op,
+ 
+                 /* Do not reply to a solicitation from the port that owns
+                  * the address (otherwise DAD detection will fail). */
++                ds_truncate(match, match_nd_len);
+                 ds_put_format(match, " && inport == %s", op->json_key);
+                 ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
+                               ds_cstr(match), "next;", op->lflow_ref,
+@@ -10788,7 +10722,7 @@ build_lswitch_dhcp_options_and_response(struct ovn_port *op,
+     }
+ 
+     bool is_external = lsp_is_external(op->nbsp);
+-    if (is_external && (vector_is_empty(&op->od->localnet_ports) ||
++    if (is_external && (!ls_has_localnet_port(op->od) ||
+                         !op->nbsp->ha_chassis_group)) {
+         /* If it's an external port and there are no localnet ports
+          * and if it doesn't belong to an HA chassis group ignore it. */
+@@ -11213,7 +11147,7 @@ build_lswitch_ip_unicast_lookup(struct ovn_port *op,
+ 
+         if (peer_lrp_is_centralized &&
+             !vector_is_empty(&op->peer->od->l3dgw_ports) &&
+-            !vector_is_empty(&op->od->localnet_ports)) {
++            ls_has_localnet_port(op->od)) {
+             add_lrp_chassis_resident_check(op->peer, match);
+         } else if (op->cr_port) {
+             /* If the op has a chassis resident port, it means
+@@ -12597,10 +12531,15 @@ add_ecmp_symmetric_reply_flows(struct lflow_table *lflows,
+                   ds_cstr(route_match));
+     ds_clear(&actions);
+     ds_put_format(&actions, "ip.ttl--; flags.loopback = 1; "
+-                  "eth.src = %s; %s = %s; outport = %s; next;",
+-                  out_port->lrp_networks.ea_s,
+-                  is_ipv4_nexthop ? REG_SRC_IPV4 : REG_SRC_IPV6,
+-                  port_ip, out_port->json_key);
++                  "eth.src = %s; ",
++                  out_port->lrp_networks.ea_s);
++    if (port_ip) {
++        ds_put_format(&actions, "%s = %s; ",
++                      is_ipv4_nexthop ? REG_SRC_IPV4 : REG_SRC_IPV6,
++                      port_ip);
++    }
++    ds_put_format(&actions, "outport = %s; next;",
++                  out_port->json_key);
+     ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_ROUTING, 10300, ds_cstr(&match),
+                   ds_cstr(&actions), lflow_ref, WITH_HINT(route->source_hint));
+ 
+@@ -12740,13 +12679,15 @@ build_ecmp_route_flow(struct lflow_table *lflows,
+         ds_put_format(&actions, "%s = ",
+                       is_ipv4_nexthop ? REG_NEXT_HOP_IPV4 : REG_NEXT_HOP_IPV6);
+         ipv6_format_mapped(route->nexthop, &actions);
+-        ds_put_format(&actions, "; "
+-                      "%s = %s; "
+-                      "eth.src = %s; "
++        if (route->lrp_addr_s) {
++            ds_put_format(&actions, "; %s = %s",
++                          is_ipv4_nexthop ? REG_SRC_IPV4 : REG_SRC_IPV6,
++                          route->lrp_addr_s);
++        }
++        ds_put_format(&actions,
++                      "; eth.src = %s; "
+                       "outport = %s; "
+                       REGBIT_NEXTHOP_IS_IPV4" = %d; ",
+-                      is_ipv4_nexthop ? REG_SRC_IPV4 : REG_SRC_IPV6,
+-                      route->lrp_addr_s,
+                       route->out_port->lrp_networks.ea_s,
+                       route->out_port->json_key,
+                       is_ipv4_nexthop);
+@@ -12806,15 +12747,17 @@ add_route(struct lflow_table *lflows, const struct ovn_datapath *od,
+                                            REG_NEXT_HOP_IPV6,
+                           is_ipv4_prefix ? "4" : "6");
+         }
+-        ds_put_format(&common_actions, "; "
+-                      "%s = %s; "
+-                      "eth.src = %s; "
++        if (lrp_addr_s) {
++            ds_put_format(&common_actions, "; %s = %s",
++                          is_ipv4_nexthop ? REG_SRC_IPV4 : REG_SRC_IPV6,
++                          lrp_addr_s);
++        }
++        ds_put_format(&common_actions,
++                      "; eth.src = %s; "
+                       "outport = %s; "
+                       "flags.loopback = 1; "
+                       REGBIT_NEXTHOP_IS_IPV4" = %d; "
+                       "next;",
+-                      is_ipv4_nexthop ? REG_SRC_IPV4 : REG_SRC_IPV6,
+-                      lrp_addr_s,
+                       op->lrp_networks.ea_s,
+                       op->json_key,
+                       is_ipv4_nexthop);
+@@ -14727,6 +14670,10 @@ build_neigh_learning_flows_for_lrouter_port(
+                           op->lrp_networks.ipv4_addrs[i].network_s,
+                           op->lrp_networks.ipv4_addrs[i].plen,
+                           op->lrp_networks.ipv4_addrs[i].addr_s);
++            if (lrp_is_l3dgw(op)) {
++                ds_put_format(match, " && is_chassis_resident(%s)",
++                              op->cr_port->json_key);
++            }
+             const char *actions_s = REGBIT_LOOKUP_NEIGHBOR_RESULT
+                               " = lookup_arp(inport, arp.spa, arp.sha); "
+                               REGBIT_LOOKUP_NEIGHBOR_IP_RESULT" = 1;"
+@@ -14741,6 +14688,10 @@ build_neigh_learning_flows_for_lrouter_port(
+                       op->json_key,
+                       op->lrp_networks.ipv4_addrs[i].network_s,
+                       op->lrp_networks.ipv4_addrs[i].plen);
++        if (lrp_is_l3dgw(op)) {
++            ds_put_format(match, " && is_chassis_resident(%s)",
++                          op->cr_port->json_key);
++        }
+         ds_clear(actions);
+         ds_put_format(actions, REGBIT_LOOKUP_NEIGHBOR_RESULT
+                       " = lookup_arp(inport, arp.spa, arp.sha); %snext;",
+@@ -17112,7 +17063,7 @@ build_lrouter_ipv4_ip_input(struct ovn_port *op,
+ 
+         if (od_is_centralized(op->od) &&
+             !vector_is_empty(&op->od->l3dgw_ports) && op->peer
+-            && !vector_is_empty(&op->peer->od->localnet_ports)) {
++            && ls_has_localnet_port(op->peer->od)) {
+             add_lrp_chassis_resident_check(op, match);
+         }
+ 
+@@ -19304,7 +19255,6 @@ struct lswitch_flow_build_info {
+     const struct ls_port_group_table *ls_port_groups;
+     const struct lr_stateful_table *lr_stateful_table;
+     const struct ls_stateful_table *ls_stateful_table;
+-    const struct ls_arp_table *ls_arp_table;
+     struct lflow_table *lflows;
+     const struct shash *meter_groups;
+     const struct hmap *lb_dps_map;
+@@ -19440,7 +19390,7 @@ build_lswitch_and_lrouter_iterate_by_lsp(struct ovn_port *op,
+     build_mirror_lflows(op, ls_ports, lflows);
+     build_lswitch_port_sec_op(op, lflows, actions, match);
+     build_lswitch_learn_fdb_op(op, lflows, actions, match);
+-    build_lswitch_arp_nd_responder_skip_local(op, lflows, match);
++    build_lswitch_from_localnet_op(op, lflows, match);
+     build_lswitch_arp_nd_responder_known_ips(op, lflows, ls_ports,
+                                              meter_groups, actions, match);
+     build_lswitch_dhcp_options_and_response(op, lflows, meter_groups);
+@@ -19506,7 +19456,6 @@ build_lflows_thread(void *arg)
+     struct worker_control *control = (struct worker_control *) arg;
+     const struct lr_stateful_record *lr_stateful_rec;
+     const struct ls_stateful_record *ls_stateful_rec;
+-    const struct ls_arp_record *ls_arp_rec;
+     struct lswitch_flow_build_info *lsi;
+     struct ovn_lb_datapaths *lb_dps;
+     struct ovn_datapath *od;
+@@ -19660,20 +19609,6 @@ build_lflows_thread(void *arg)
+                                             lsi->sbrec_acl_id_table);
+                 }
+             }
+-
+-            for (bnum = control->id;
+-                    bnum <= lsi->ls_arp_table->entries.mask;
+-                    bnum += control->pool->size)
+-            {
+-                LS_ARP_TABLE_FOR_EACH_IN_P (ls_arp_rec, bnum,
+-                                            lsi->ls_arp_table) {
+-                    od = ovn_datapaths_find_by_index(
+-                        lsi->ls_datapaths, ls_arp_rec->ls_index);
+-                    build_lswitch_arp_chassis_resident(od, lsi->lflows,
+-                                                       ls_arp_rec);
+-                }
+-            }
+-
+             lsi->thread_lflow_counter = thread_lflow_counter;
+         }
+         post_completed_work(control);
+@@ -19722,7 +19657,6 @@ build_lswitch_and_lrouter_flows(
+     const struct ls_port_group_table *ls_pgs,
+     const struct lr_stateful_table *lr_stateful_table,
+     const struct ls_stateful_table *ls_stateful_table,
+-    const struct ls_arp_table *ls_arp_table,
+     struct lflow_table *lflows,
+     const struct shash *meter_groups,
+     const struct hmap *lb_dps_map,
+@@ -19759,7 +19693,6 @@ build_lswitch_and_lrouter_flows(
+             lsiv[index].ls_port_groups = ls_pgs;
+             lsiv[index].lr_stateful_table = lr_stateful_table;
+             lsiv[index].ls_stateful_table = ls_stateful_table;
+-            lsiv[index].ls_arp_table = ls_arp_table;
+             lsiv[index].meter_groups = meter_groups;
+             lsiv[index].lb_dps_map = lb_dps_map;
+             lsiv[index].local_svc_monitor_map =
+@@ -19796,7 +19729,6 @@ build_lswitch_and_lrouter_flows(
+     } else {
+         const struct lr_stateful_record *lr_stateful_rec;
+         const struct ls_stateful_record *ls_stateful_rec;
+-        const struct ls_arp_record *ls_arp_rec;
+         struct ovn_lb_datapaths *lb_dps;
+         struct ovn_datapath *od;
+         struct ovn_port *op;
+@@ -19809,7 +19741,6 @@ build_lswitch_and_lrouter_flows(
+             .ls_port_groups = ls_pgs,
+             .lr_stateful_table = lr_stateful_table,
+             .ls_stateful_table = ls_stateful_table,
+-            .ls_arp_table = ls_arp_table,
+             .lflows = lflows,
+             .meter_groups = meter_groups,
+             .lb_dps_map = lb_dps_map,
+@@ -19906,12 +19837,6 @@ build_lswitch_and_lrouter_flows(
+                                     lsi.sbrec_acl_id_table);
+         }
+ 
+-        LS_ARP_TABLE_FOR_EACH (ls_arp_rec, ls_arp_table) {
+-            od = ovn_datapaths_find_by_index(lsi.ls_datapaths,
+-                                             ls_arp_rec->ls_index);
+-            build_lswitch_arp_chassis_resident(od, lsi.lflows, ls_arp_rec);
+-        }
+-
+         ds_destroy(&lsi.match);
+         ds_destroy(&lsi.actions);
+     }
+@@ -19995,7 +19920,6 @@ void build_lflows(struct ovsdb_idl_txn *ovnsb_txn,
+                                     input_data->ls_port_groups,
+                                     input_data->lr_stateful_table,
+                                     input_data->ls_stateful_table,
+-                                    input_data->ls_arp_table,
+                                     lflows,
+                                     input_data->meter_groups,
+                                     input_data->lb_datapaths_map,
+@@ -20441,42 +20365,6 @@ lflow_handle_ls_stateful_changes(struct ovsdb_idl_txn *ovnsb_txn,
+     return true;
+ }
+ 
+-bool
+-lflow_handle_ls_arp_changes(struct ovsdb_idl_txn *ovnsb_txn,
+-                            struct ls_arp_tracked_data *trk_data,
+-                            struct lflow_input *lflow_input,
+-                            struct lflow_table *lflows)
+-{
+-    struct hmapx_node *hmapx_node;
+-
+-    HMAPX_FOR_EACH (hmapx_node, &trk_data->crupdated) {
+-        const struct ls_arp_record *ls_arp_record = hmapx_node->data;
+-        const struct ovn_datapath *od =
+-            ovn_datapaths_find_by_index(lflow_input->ls_datapaths,
+-                                        ls_arp_record->ls_index);
+-        lflow_ref_unlink_lflows(ls_arp_record->lflow_ref);
+-
+-        build_lswitch_arp_chassis_resident(od, lflows, ls_arp_record);
+-
+-        bool handled = lflow_ref_sync_lflows(
+-            ls_arp_record->lflow_ref, lflows, ovnsb_txn,
+-            lflow_input->dps,
+-            lflow_input->ovn_internal_version_changed,
+-            lflow_input->sbrec_logical_flow_table,
+-            lflow_input->sbrec_logical_dp_group_table);
+-        if (!handled) {
+-            return false;
+-        }
+-    }
+-
+-    HMAPX_FOR_EACH (hmapx_node, &trk_data->deleted) {
+-        struct ls_arp_record *ls_arp_record = hmapx_node->data;
+-        lflow_ref_unlink_lflows(ls_arp_record->lflow_ref);
+-    }
+-
+-    return true;
+-}
+-
+ static bool
+ mirror_needs_update(const struct nbrec_mirror *nb_mirror,
+                     const struct sbrec_mirror *sb_mirror)
+diff --git a/northd/northd.h b/northd/northd.h
+index 7f2b4c9004..98ea4c8a17 100644
+--- a/northd/northd.h
++++ b/northd/northd.h
+@@ -29,7 +29,6 @@
+ #include "simap.h"
+ #include "ovs-thread.h"
+ #include "en-lr-stateful.h"
+-#include "en-ls-arp.h"
+ #include "vec.h"
+ #include "datapath-sync.h"
+ #include "sparse-array.h"
+@@ -271,7 +270,6 @@ struct lflow_input {
+     const struct ls_port_group_table *ls_port_groups;
+     const struct lr_stateful_table *lr_stateful_table;
+     const struct ls_stateful_table *ls_stateful_table;
+-    const struct ls_arp_table *ls_arp_table;
+     const struct shash *meter_groups;
+     const struct hmap *lb_datapaths_map;
+     const struct sset *bfd_ports;
+@@ -476,9 +474,6 @@ struct ovn_datapath {
+      * Valid only if it is logical router datapath. NULL otherwise. */
+     struct lrouter_group *lr_group;
+ 
+-    /* Set of localnet or l2gw ports. */
+-    struct hmapx phys_ports;
+-
+     /* Map of ovn_port objects belonging to this datapath.
+      * This map doesn't include derived ports. */
+     struct hmap ports;
+@@ -513,6 +508,12 @@ ovn_datapath_is_stale(const struct ovn_datapath *od)
+     return !od->nbr && !od->nbs;
+ };
+ 
++static inline bool
++ls_has_localnet_port(const struct ovn_datapath *od)
++{
++    return !vector_is_empty(&od->localnet_ports);
++}
++
+ /* Pipeline stages. */
+ #define PIPELINE_STAGES                                                   \
+     /* Logical switch ingress stages. */                                  \
+@@ -974,10 +975,6 @@ bool lflow_handle_ls_stateful_changes(struct ovsdb_idl_txn *,
+                                       struct ls_stateful_tracked_data *,
+                                       struct lflow_input *,
+                                       struct lflow_table *lflows);
+-bool lflow_handle_ls_arp_changes(struct ovsdb_idl_txn *,
+-                                 struct ls_arp_tracked_data *,
+-                                 struct lflow_input *,
+-                                 struct lflow_table *lflows);
+ bool northd_handle_sb_port_binding_changes(
+     const struct sbrec_port_binding_table *, struct hmap *ls_ports,
+     struct hmap *lr_ports);
+diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml
+index 4d6370da6b..7108ec7984 100644
+--- a/northd/ovn-northd.8.xml
++++ b/northd/ovn-northd.8.xml
+@@ -310,15 +310,6 @@
+         </p>
+       </li>
+ 
+-      <li>
+-        For each logical switch that has connected physical ports
+-        (localnet or l2gateway) and is also connected to a distributed router,
+-        filtering rules are added for ARP requests coming from localnet or
+-        l2gateway ports, allowed for processing on gateway chassis.
+-        The <code>REGBIT_EXT_ARP</code> register is set for all ARP requests
+-        originating from physical ports with priority 75 flow.
+-      </li>
+-
+       <li>
+         For each (enabled) vtep logical port, a priority 70 flow is added which
+         matches on all packets and applies the action
+@@ -405,13 +396,6 @@
+         One priority-0 fallback flow that matches all packets and advances to
+         the next table.
+       </li>
+-
+-      <li>
+-        Priority 75: Allows <code>REGBIT_EXT_ARP</code> packets only on gateway
+-        chassis and chassis with distributed NAT entries.
+-        Priority 70: Drops <code>REGBIT_EXT_ARP</code> packets on non-gateway
+-        chassis (complements the priority 75 flow).
+-      </li>
+     </ul>
+ 
+     <h3>Ingress Table 2: Mirror </h3>
+@@ -488,6 +472,21 @@
+         </ul>
+       </li>
+ 
++      <li>
++        <p>
++          For each localnet logical port <var>p</var>, a priority-50
++          fallback flow is added with the match
++          <code>inport == <var>p</var></code> and action
++          <code>flags.localnet = 1; next;</code>.  This marks traffic
++          arriving from localnet ports so that downstream stages (e.g.,
++          ARP/ND responder) can condition their behavior.  When FDB
++          learning is enabled on the localnet port, the priority-100
++          flow described above already sets <code>flags.localnet</code>,
++          so this priority-50 flow only takes effect when FDB learning
++          is not configured.
++        </p>
++      </li>
++
+       <li>
+         One priority-0 fallback flow that matches all packets and advances to
+         the next table.
+@@ -1734,12 +1733,16 @@
+     </p>
+ 
+     <p>
+-      Note that ARP requests received from <code>localnet</code> logical
+-      inports can either go directly to VMs, in which case the VM responds or
+-      can hit an ARP responder for a logical router port if the packet is used
+-      to resolve a logical router port next hop address.  In either case,
+-      logical switch ARP responder rules will not be hit.  It contains these
+-      logical flows:
++      ARP/ND requests received from <code>localnet</code> logical inports
++      do hit the ARP/ND responder, but the response is limited to the
++      chassis that hosts the target VIF.  This is achieved by adding
++      a <code>flags.localnet</code> check to the priority-50 reply flows
++      (see below): when the request arrives from a localnet port
++      (<code>flags.localnet == 1</code>), only the chassis on which the
++      target port is resident will reply.  When the request arrives from
++      a non-localnet port (<code>flags.localnet == 0</code>), the
++      response is unconditional, preserving VIF-to-VIF proxy ARP/ND
++      behavior.  It contains these logical flows:
+      </p>
+ 
+     <ul>
+@@ -1750,18 +1753,10 @@
+         router ingress pipeline.
+       </li>
+       <li>
+-        If the logical switch has no router ports with options:arp_proxy
+-        configured add a priority-100 flows to skip the ARP responder if inport
+-        is of type <code>localnet</code> advances directly to the next table.
+-        ARP requests sent to <code>localnet</code> ports can be received by
+-        multiple hypervisors.  Now, because the same mac binding rules are
+-        downloaded to all hypervisors, each of the multiple hypervisors will
+-        respond.  This will confuse L2 learning on the source of the ARP
+-        requests.  ARP requests received on an inport of type
+-        <code>router</code> are not expected to hit any logical switch ARP
+-        responder flows.  However, no skip flows are installed for these
+-        packets, as there would be some additional flow cost for this and the
+-        value appears limited.
++        ARP/ND requests received on an inport of type <code>router</code> are
++        not expected to hit any logical switch ARP responder flows.  However,
++        no skip flows are installed for these packets, as there would be some
++        additional flow cost for this and the value appears limited.
+       </li>
+ 
+       <li>
+@@ -1816,6 +1811,18 @@ flags.loopback = 1;
+ output;
+         </pre>
+ 
++        <p>
++          On logical switches that have a localnet port, the match for
++          these flows includes an additional condition:
++          <code>((flags.localnet == 1 &amp;&amp;
++          is_chassis_resident(<var>port</var>)) ||
++          flags.localnet == 0)</code>.
++          This ensures that when an ARP request arrives from a localnet
++          port, only the chassis hosting the target VIF responds.  When
++          the request arrives from a non-localnet port, the response is
++          unconditional, preserving VIF-to-VIF proxy ARP behavior.
++        </p>
++
+         <p>
+           These flows are omitted for logical ports (other than router ports or
+           <code>localport</code> ports) that are down (unless <code>
+@@ -1877,6 +1884,19 @@ nd_na_router {
+ };
+         </pre>
+ 
++        <p>
++          On logical switches that have a localnet port, the match for
++          these flows includes an additional condition:
++          <code>((flags.localnet == 1 &amp;&amp;
++          is_chassis_resident(<var>port</var>)) ||
++          flags.localnet == 0)</code>.
++          This ensures that when an ND solicitation arrives from a
++          localnet port, only the chassis hosting the target VIF
++          responds.  When the solicitation arrives from a non-localnet
++          port, the response is unconditional, preserving VIF-to-VIF
++          proxy ND behavior.
++        </p>
++
+         <p>
+           These flows are omitted for logical ports (other than router ports or
+           <code>localport</code> ports) that are down (unless <code>
+@@ -1896,8 +1916,8 @@ nd_na_router {
+ 
+       <li>
+         <p>
+-          Priority-100 flows with match criteria like the ARP and ND flows
+-          above, except that they only match packets from the
++          Priority-100 flows with match criteria similar to the ARP and ND
++          flows above, except that they only match packets from the
+           <code>inport</code> that owns the IP addresses in question, with
+           action <code>next;</code>.  These flows prevent OVN from replying to,
+           for example, an ARP request emitted by a VM for its own IP address.
+diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
+index 0ed2eb17ad..29333c3ec1 100644
+--- a/northd/ovn-northd.c
++++ b/northd/ovn-northd.c
+@@ -1111,10 +1111,18 @@ main(int argc, char *argv[])
+ 
+                 /* Make sure we don't bump the next_cfg when we shouldn't.
+                  * This should prevent ovn-nbctl sync calls to return before
+-                 * the SB updates are actually done. */
++                 * the SB updates are actually done.
++                 *
++                 * Track that the abort was intentional so we can distinguish
++                 * it from a real commit failure below; otherwise the abort
++                 * would feed back into the force-recompute path, creating a
++                 * busy loop whenever NB.nb_cfg keeps advancing but the
++                 * engine produces no SB activity. */
++                bool ovnsb_txn_aborted_intentionally = false;
+                 if (!activity && ovnsb_txn &&
+                     ovnsb_idl_loop.cur_cfg != ovnsb_idl_loop.next_cfg) {
+                     ovsdb_idl_txn_abort(ovnsb_txn);
++                    ovnsb_txn_aborted_intentionally = true;
+                 }
+ 
+                 /* If there are any errors, we force a full recompute in order
+@@ -1125,7 +1133,8 @@ main(int argc, char *argv[])
+                     inc_proc_northd_force_recompute_immediate();
+                 }
+ 
+-                if (!ovsdb_idl_loop_commit_and_wait(&ovnsb_idl_loop)) {
++                if (!ovsdb_idl_loop_commit_and_wait(&ovnsb_idl_loop) &&
++                    !ovnsb_txn_aborted_intentionally) {
+                     VLOG_INFO("OVNSB commit failed, "
+                               "force recompute next time.");
+                     inc_proc_northd_force_recompute_immediate();
 diff --git a/ovn-nb.xml b/ovn-nb.xml
 index 954ed11661..64fdf25e4e 100644
 --- a/ovn-nb.xml
@@ -26,3 +2924,2570 @@ index 954ed11661..64fdf25e4e 100644
          east-west traffic between nodes.
          Required configuration: <ref column="ip_port_mappings"/>.
          NOTE: The addressing of the underlay network must not overlap with the
+diff --git a/tests/multinode-macros.at b/tests/multinode-macros.at
+index 596de4c3af..4a74d51277 100644
+--- a/tests/multinode-macros.at
++++ b/tests/multinode-macros.at
+@@ -41,28 +41,6 @@ m4_define([M_START_TCPDUMP],
+     ]
+ )
+ 
+-m4_define([M_START_TCPDUMPS_RECURSIVE_], [
+-     m4_if(m4_eval($# > 3), [1], [dnl
+-        names="$names $3"
+-        echo "Running podman exec $1 tcpdump -l $2 >$3.tcpdump 2>$3.stderr"
+-        podman exec $1 tcpdump -l $2 >$3.tcpdump 2>$3.stderr &
+-        echo "podman exec $1 ps -ef | grep -v grep | grep tcpdump && podman exec $1 killall tcpdump" >> cleanup
+-        M_START_TCPDUMPS_RECURSIVE_(m4_shift(m4_shift(m4_shift($@))))
+-        ])
+-    ]
+-)
+-
+-# Start Multiple tcpdump. Useful to speed up when many tcpdump
+-# must be started as waiting for "listening" takes usually 1 second.
+-m4_define([M_START_TCPDUMPS],
+-    [
+-     names=""
+-     M_START_TCPDUMPS_RECURSIVE_($@)
+-     for name in $names; do
+-         OVS_WAIT_UNTIL([grep -q "listening" ${name}.stderr])
+-     done
+-    ]
+-)
+ 
+ # M_FORMAT_CT([ip-addr])
+ #
+@@ -502,30 +480,6 @@ m_is_fedora() {
+     m_central_as grep -qi fedora /etc/os-release
+ }
+ 
+-# Run ovs-vsctl using Host socket
+-host_ovs_vsctl() {
+-    # Discover host OVS socket on first call
+-    if [[ -z "$HOST_OVS_SOCK" ]]; then
+-        for sock in /run/openvswitch/db.sock /var/run/openvswitch/db.sock /usr/local/var/run/openvswitch/db.sock; do
+-            if [[ -S "$sock" ]]; then
+-                HOST_OVS_SOCK=$sock
+-                break
+-            fi
+-        done
+-        # Fallback on unusual prefix: discover from running process
+-        if [[ -z "$HOST_OVS_SOCK" ]]; then
+-            HOST_OVS_SOCK=$(ps aux | grep '[o]vsdb-server' | grep -oP 'punix:\K[^, ]+' | while read s; do
+-                [[ -S "$s" ]] && [[ "$s" != *"$OVS_RUNDIR"* ]] && echo "$s" && break
+-            done)
+-        fi
+-        if [[ -z "$HOST_OVS_SOCK" ]]; then
+-            echo "ERROR: Could not find host OVS socket" >&2
+-            AT_FAIL_IF([:])
+-        fi
+-    fi
+-    ovs-vsctl --db=unix:$HOST_OVS_SOCK "$@"
+-}
+-
+ # M_START_L4_SERVER([fake_node], [namespace], [ip_addr], [port], [reply_string], [pidfile])
+ #
+ # Helper to properly start l4 server in inside 'fake_node''s namespace'.
+diff --git a/tests/multinode.at b/tests/multinode.at
+index d07660797c..069f2a677d 100644
+--- a/tests/multinode.at
++++ b/tests/multinode.at
+@@ -2986,42 +2986,42 @@ AT_CLEANUP
+ 
+ AT_SETUP([HA: Check for missing garp on leader when BFD goes back up])
+ # Network topology
+-#    ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
+-#    │                                                                                                                                │
+-#    │   ┌───────────────────┐    ┌───────────────────┐    ┌───────────────────┐    ┌───────────────────┐    ┌───────────────────┐    │
+-#    │   │   ovn-chassis-1   │    │   ovn-chassis-2   │    │  ovn-gw-1         │    │  ovn-gw-2         │    │  ovn-chassis-3    │    │
+-#    │   └─────────┬─────────┘    └─────────┬─────────┘    └───────────────────┘    └───────────────────┘    └───────────────────┘    │
+-#    │   ┌─────────┴─────────┐    ┌─────────┴─────────┐                                                                               │
+-#    │   │       inside1     │    │       inside2     │                                                                               │
+-#    │   │   192.168.1.1/24  │    │   192.168.1.2/24  │                                                                               │
+-#    │   └─────────┬─────────┘    └─────────┬─────────┘                                                                               │
+-#    │           ┌─┴────────────────────────┴─┐                                                                                       │
+-#    │           │           inside           │                                                                                       │
+-#    │           └──────────────┬─────────────┘                                                                                       │
+-#    │                ┌─────────┴─────────┐                                                                                           │
+-#    │                │    192.168.1.254  │                                                                                           │
+-#    │                │         R1        │                                                                                           │
+-#    │                │    192.168.0.254  │                                                                                           │
+-#    │                └─────────┬─────────┘                                                                                           │
+-#    │                          └------eth1---------------------------┬--------eth1-----------┐                                       │
+-#    │                                                     ┌──────────┴────────┐    ┌─────────┴─────────┐                             │
+-#    │                                                     │    192.168.1.254  │    │   192.168.1.254   │                             │
+-#    │                                                     │         R1        │    │         R1        │                             │
+-#    │                                                     │    192.168.0.254  │    │   192.168.0.254   │                             │
+-#    │                                                     └─────────┬─────────┘    └─────────┬─────────┘                             │
+-#    │                                                               │                        │              ┌───────────────────┐    │
+-#    │                                                     ┌─────────┴─────────┐    ┌─────────┴─────────┐    │    192.168.0.1    │    │
+-#    │                                                     │       outside     │    │       outside     │    │        ext1       │    │
+-#    │                                                     └─────────┬─────────┘    └─────────┬─────────┘    └─────────┬─────────┘    │
+-#    │                                                     ┌─────────┴─────────┐    ┌─────────┴─────────┐    ┌─────────┴─────────┐    │
+-#    │                                                     │    ln-outside     │    │    ln-outside     │    │       ln-ext1     │    │
+-#    │                                                     └─────────┬─────────┘    └─────────┬─────────┘    └─────────┬─────────┘    │
+-#    │                                                     ┌─────────┴─────────┐    ┌─────────┴─────────┐    ┌─────────┴─────────┐    │
+-#    │                                                     │       br-ex       │    │       br-ex       │    │       br-ex       │    │
+-#    │                                                     └─────────┬─────────┘    └─────────┬─────────┘    └─────────┬─────────┘    │
+-#    │                                                               └---------eth2-----------┴-------eth2-------------┘              │
+-#    │                                                                                                                                │
+-#    └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
++#    ┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
++#    │                                                                                                        │
++#    │    ┌───────────────────┐    ┌───────────────────┐    ┌───────────────────┐    ┌───────────────────┐    │
++#    │    │   ovn-chassis-1   │    │  ovn-gw-1         │    │  ovn-gw-2         │    │  ovn-chassis-2    │    │
++#    │    └─────────┬─────────┘    └───────────────────┘    └───────────────────┘    └───────────────────┘    │
++#    │    ┌─────────┴─────────┐                                                                               │
++#    │    │       inside1     │                                                                               │
++#    │    │   192.168.1.1/24  │                                                                               │
++#    │    └─────────┬─────────┘                                                                               │
++#    │    ┌─────────┴─────────┐                                                                               │
++#    │    │       inside      │                                                                               │
++#    │    └─────────┬─────────┘                                                                               │
++#    │    ┌─────────┴─────────┐                                                                               │
++#    │    │    192.168.1.254  │                                                                               │
++#    │    │         R1        │                                                                               │
++#    │    │    192.168.0.254  │                                                                               │
++#    │    └─────────┬─────────┘                                                                               │
++#    │              └------eth1---------------┬--------eth1-----------┐                                       │
++#    │                             ┌──────────┴────────┐    ┌─────────┴─────────┐                             │
++#    │                             │    192.168.1.254  │    │   192.168.1.254   │                             │
++#    │                             │         R1        │    │         R1        │                             │
++#    │                             │    192.168.0.254  │    │   192.168.0.254   │                             │
++#    │                             └─────────┬─────────┘    └─────────┬─────────┘                             │
++#    │                                       │                        │              ┌───────────────────┐    │
++#    │                             ┌─────────┴─────────┐    ┌─────────┴─────────┐    │    192.168.0.1    │    │
++#    │                             │       outside     │    │       outside     │    │        ext1       │    │
++#    │                             └─────────┬─────────┘    └─────────┬─────────┘    └─────────┬─────────┘    │
++#    │                             ┌─────────┴─────────┐    ┌─────────┴─────────┐    ┌─────────┴─────────┐    │
++#    │                             │    ln-outside     │    │    ln-outside     │    │       ln-ext1     │    │
++#    │                             └─────────┬─────────┘    └─────────┬─────────┘    └─────────┬─────────┘    │
++#    │                             ┌─────────┴─────────┐    ┌─────────┴─────────┐    ┌─────────┴─────────┐    │
++#    │                             │       br-ex       │    │       br-ex       │    │       br-ex       │    │
++#    │                             └─────────┬─────────┘    └─────────┬─────────┘    └─────────┬─────────┘    │
++#    │                                       └---------eth2-----------┴-------eth2-------------┘              │
++#    │                                                                                                        │
++#    └────────────────────────────────────────────────────────────────────────────────────────────────────────┘
+ 
+ # The goal of this test is the check that GARP are properly generated by higest priority traffic when
+ # BFD goes down, and back up, and this whether the BFD event is due either to some bfd packet lost
+@@ -3030,12 +3030,6 @@ AT_SETUP([HA: Check for missing garp on leader when BFD goes back up])
+ # So gw3 should in this test neither send garp or receive packets.
+ #
+ # Enable vconn so we can check the GARP from a log perspective.
+-on_exit "podman exec ovn-gw-1 ovn-appctl vlog/set info"
+-on_exit "podman exec ovn-gw-1 ovn-appctl vlog/enable-rate-limit"
+-on_exit "podman exec ovn-gw-2 ovn-appctl vlog/set info"
+-on_exit "podman exec ovn-gw-2 ovn-appctl vlog/enable-rate-limit"
+-on_exit "podman exec ovn-gw-3 ovn-appctl vlog/set info"
+-on_exit "podman exec ovn-gw-3 ovn-appctl vlog/enable-rate-limit"
+ m_as ovn-gw-1 ovn-appctl vlog/set vconn:dbg
+ m_as ovn-gw-2 ovn-appctl vlog/set vconn:dbg
+ m_as ovn-gw-3 ovn-appctl vlog/set vconn:dbg
+@@ -3043,17 +3037,12 @@ m_as ovn-gw-1 ovn-appctl vlog/disable-rate-limit
+ m_as ovn-gw-2 ovn-appctl vlog/disable-rate-limit
+ m_as ovn-gw-3 ovn-appctl vlog/disable-rate-limit
+ 
+-# Decrease revalidation time on ovs switch simulating ToR.
+-check host_ovs_vsctl set Open_vSwitch . other_config:max-revalidator=100
+-on_exit "check host_ovs_vsctl remove Open_vSwitch . other_config max-revalidator"
+-
+ check_fake_multinode_setup
+ 
+ # Delete the multinode NB and OVS resources before starting the test.
+ cleanup_multinode_resources
+ 
+ ip_ch1=$(m_as ovn-chassis-1 ip a show dev eth1 | grep "inet " | awk '{print $2}'| cut -d '/' -f1)
+-ip_ch2=$(m_as ovn-chassis-2 ip a show dev eth1 | grep "inet " | awk '{print $2}'| cut -d '/' -f1)
+ ip_gw1=$(m_as ovn-gw-1 ip a show dev eth1 | grep "inet " | awk '{print $2}'| cut -d '/' -f1)
+ ip_gw2=$(m_as ovn-gw-2 ip a show dev eth1 | grep "inet " | awk '{print $2}'| cut -d '/' -f1)
+ ip_gw3=$(m_as ovn-gw-3 ip a show dev eth1 | grep "inet " | awk '{print $2}'| cut -d '/' -f1)
+@@ -3061,35 +3050,25 @@ ip_gw3=$(m_as ovn-gw-3 ip a show dev eth1 | grep "inet " | awk '{print $2}'| cut
+ from_gw1_to_gw2=$(m_as ovn-gw-1 ovs-vsctl --bare --columns=name find interface options:remote_ip=$ip_gw2)
+ from_gw1_to_gw3=$(m_as ovn-gw-1 ovs-vsctl --bare --columns=name find interface options:remote_ip=$ip_gw3)
+ from_gw1_to_ch1=$(m_as ovn-gw-1 ovs-vsctl --bare --columns=name find interface options:remote_ip=$ip_ch1)
+-from_gw1_to_ch2=$(m_as ovn-gw-1 ovs-vsctl --bare --columns=name find interface options:remote_ip=$ip_ch2)
+ from_gw2_to_gw1=$(m_as ovn-gw-2 ovs-vsctl --bare --columns=name find interface options:remote_ip=$ip_gw1)
+ from_gw2_to_gw3=$(m_as ovn-gw-2 ovs-vsctl --bare --columns=name find interface options:remote_ip=$ip_gw3)
+ from_gw2_to_ch1=$(m_as ovn-gw-2 ovs-vsctl --bare --columns=name find interface options:remote_ip=$ip_ch1)
+-from_gw2_to_ch2=$(m_as ovn-gw-2 ovs-vsctl --bare --columns=name find interface options:remote_ip=$ip_ch2)
+ from_ch1_to_gw1=$(m_as ovn-chassis-1 ovs-vsctl --bare --columns=name find interface options:remote_ip=$ip_gw1)
+ from_ch1_to_gw2=$(m_as ovn-chassis-1 ovs-vsctl --bare --columns=name find interface options:remote_ip=$ip_gw2)
+-from_ch2_to_gw1=$(m_as ovn-chassis-2 ovs-vsctl --bare --columns=name find interface options:remote_ip=$ip_gw1)
+-from_ch2_to_gw2=$(m_as ovn-chassis-2 ovs-vsctl --bare --columns=name find interface options:remote_ip=$ip_gw2)
+ 
+ m_as ovn-chassis-1 ip link del hv1-vif1-p
+-m_as ovn-chassis-2 ip link del hv2-vif1-p
+-m_as ovn-chassis-3 ip link del ext1-p
++m_as ovn-chassis-2 ip link del ext1-p
+ 
+ OVS_WAIT_UNTIL([m_as ovn-chassis-1 ip link show | grep -q genev_sys])
+ OVS_WAIT_UNTIL([m_as ovn-chassis-2 ip link show | grep -q genev_sys])
+-OVS_WAIT_UNTIL([m_as ovn-chassis-3 ip link show | grep -q genev_sys])
+ OVS_WAIT_UNTIL([m_as ovn-gw-1 ip link show | grep -q genev_sys])
+ OVS_WAIT_UNTIL([m_as ovn-gw-2 ip link show | grep -q genev_sys])
+ OVS_WAIT_UNTIL([m_as ovn-gw-3 ip link show | grep -q genev_sys])
+ 
+-# Use "aggressive" bfd parameters
+-check multinode_nbctl set NB_Global . options:"bfd-min-rx"=500
+-check multinode_nbctl set NB_Global . options:"bfd-min-tx"=100
+ check multinode_nbctl ls-add inside
+ check multinode_nbctl ls-add outside
+ check multinode_nbctl ls-add ext
+ check multinode_nbctl lsp-add inside inside1 -- lsp-set-addresses inside1 "f0:00:c0:a8:01:01 192.168.1.1"
+-check multinode_nbctl lsp-add inside inside2 -- lsp-set-addresses inside2 "f0:00:c0:a8:01:02 192.168.1.2"
+ check multinode_nbctl lsp-add ext ext1 -- lsp-set-addresses ext1 "00:00:c0:a8:00:01 192.168.0.1"
+ 
+ multinode_nbctl create Logical_Router name=R1
+@@ -3121,14 +3100,12 @@ m_as ovn-gw-3 ovs-vsctl remove open . external_ids garp-max-timeout-sec
+ 
+ m_as ovn-chassis-1 ovs-vsctl set open . external-ids:ovn-bridge-mappings=public:br-ex
+ m_as ovn-chassis-2 ovs-vsctl set open . external-ids:ovn-bridge-mappings=public:br-ex
+-m_as ovn-chassis-3 ovs-vsctl set open . external-ids:ovn-bridge-mappings=public:br-ex
+ m_as ovn-gw-1 ovs-vsctl set open . external-ids:ovn-bridge-mappings=public:br-ex
+ m_as ovn-gw-2 ovs-vsctl set open . external-ids:ovn-bridge-mappings=public:br-ex
+ m_as ovn-gw-3 ovs-vsctl set open . external-ids:ovn-bridge-mappings=public:br-ex
+ 
+ m_as ovn-chassis-1 /data/create_fake_vm.sh inside1 hv1-vif1 f0:00:c0:a8:01:01 1500 192.168.1.1 24 192.168.1.254 2000::1/64 2000::a
+-m_as ovn-chassis-2 /data/create_fake_vm.sh inside2 hv2-vif1 f0:00:c0:a8:01:02 1500 192.168.1.2 24 192.168.1.254 2000::2/64 2000::a
+-m_as ovn-chassis-3 /data/create_fake_vm.sh ext1 ext1 00:00:c0:a8:00:01 1500 192.168.0.1 24 192.168.0.254 1000::3/64 1000::a
++m_as ovn-chassis-2 /data/create_fake_vm.sh ext1 ext1 00:00:c0:a8:00:01 1500 192.168.0.1 24 192.168.0.254 1000::3/64 1000::a
+ 
+ # There should be one ha_chassis_group with the name "R1_outside"
+ m_check_row_count HA_Chassis_Group 1 name=R1_outside
+@@ -3183,67 +3160,53 @@ for chassis in $from_ch1_to_gw1 $from_ch1_to_gw2; do
+     wait_bfd_enabled ovn-chassis-1 $chassis
+ done
+ 
+-# check BFD enablement on tunnel ports from ovn-chassis-2 ###########
+-for chassis in $from_ch2_to_gw1 $from_ch2_to_gw2; do
+-    echo "checking ovn-chassis-2 -> $chassis"
+-    wait_bfd_enabled ovn-chassis-2 $chassis
+-done
+-
+ # Make sure there is no nft table left. Do not use nft directly as might not be installed in container.
+ gw1_pid=$(podman inspect -f '{{.State.Pid}}' ovn-gw-1)
+ nsenter --net=/proc/$gw1_pid/ns/net nft list tables | grep ovn-test && nsenter --net=/proc/$gw1_pid/ns/net nft delete table ip ovn-test
+-on_exit "if [[ -d "/proc/$gw1_pid" ]]; then nsenter --net=/proc/$gw1_pid/ns/net nft list tables | grep ovn-test && nsenter --net=/proc/$gw1_pid/ns/net nft delete table ip ovn-test; fi"
++on_exit "nsenter --net=/proc/$gw1_pid/ns/net nft list tables | grep ovn-test && nsenter --net=/proc/$gw1_pid/ns/net nft delete table ip ovn-test"
+ 
+-for chassis in $from_gw1_to_gw2 $from_gw1_to_gw3 $from_gw1_to_ch1 $from_gw1_to_ch2; do
++for chassis in $from_gw1_to_gw2 $from_gw1_to_gw3 $from_gw1_to_ch1; do
+     wait_bfd_up ovn-gw-1 $chassis
+ done
+-for chassis in $from_gw2_to_gw1 $from_gw2_to_gw3 $from_gw2_to_ch1 $from_gw2_to_ch2; do
++for chassis in $from_gw2_to_gw1 $from_gw2_to_gw3 $from_gw2_to_ch1; do
+     wait_bfd_up ovn-gw-2 $chassis
+ done
+ for chassis in $from_ch1_to_gw1 $from_ch1_to_gw2; do
+     wait_bfd_up ovn-chassis-1 $chassis
+ done
+-for chassis in $from_ch2_to_gw1 $from_ch2_to_gw2; do
+-    wait_bfd_up ovn-chassis-2 $chassis
+-done
+ 
+ m_wait_row_count Port_Binding 1 logical_port=cr-R1_outside chassis=$gw1_chassis
+ check multinode_nbctl --wait=hv sync
+ 
+ start_tcpdump() {
+     echo "$(date +%H:%M:%S.%03N) Starting tcpdump"
+-    M_START_TCPDUMPS([ovn-chassis-1], [-neei hv1-vif1-p], [ch1],
+-                    [ovn-chassis-2], [-neei hv2-vif1-p], [ch2],
+-                    [ovn-chassis-3], [-neei eth2], [ch3],
+-                    [ovn-gw-1], [-neei eth2], [gw1],
+-                    [ovn-gw-1], [-neei eth2 -Q out], [gw1_out],
+-                    [ovn-gw-2], [-neei eth2], [gw2],
+-                    [ovn-gw-2], [-neei eth2 -Q out], [gw2_out],
+-                    [ovn-gw-3], [-neei eth2], [gw3],
+-                    [ovn-gw-3], [-neei eth2 -Q out], [gw3_out],
+-                    [ovn-gw-1], [-neei eth1], [gw1_eth1],
+-                    [ovn-gw-2], [-neei eth1], [gw2_eth1],
+-                    [ovn-chassis-1], [-neei eth1], [ch1_eth1],
+-                    [ovn-chassis-2], [-neei eth1], [ch2_eth1])
++    M_START_TCPDUMP([ovn-chassis-1], [-neei hv1-vif1-p], [ch1])
++    M_START_TCPDUMP([ovn-chassis-2], [-neei eth2], [ch2])
++    M_START_TCPDUMP([ovn-gw-1], [-neei eth2], [gw1])
++    M_START_TCPDUMP([ovn-gw-1], [-neei eth2 -Q out], [gw1_out])
++    M_START_TCPDUMP([ovn-gw-2], [-neei eth2], [gw2])
++    M_START_TCPDUMP([ovn-gw-2], [-neei eth2 -Q out], [gw2_out])
++    M_START_TCPDUMP([ovn-gw-3], [-neei eth2], [gw3])
++    M_START_TCPDUMP([ovn-gw-3], [-neei eth2 -Q out], [gw3_out])
+ }
+ 
+ stop_tcpdump() {
+     echo "$(date +%H:%M:%S.%03N) Stopping tcpdump"
+-    m_kill 'ovn-gw-1 ovn-gw-2 ovn-gw-3 ovn-chassis-1 ovn-chassis-2 ovn-chassis-3' tcpdump
++    m_kill 'ovn-gw-1 ovn-gw-2 ovn-gw-3 ovn-chassis-1 ovn-chassis-2' tcpdump
+ }
+ 
+-# Send packets from ovn-chassis-3 (ext1) to ovn-chassis-1
++# Send packets from chassis2 (ext1) to chassis1
+ send_background_packets() {
+     echo "$(date +%H:%M:%S.%03N) Sending packets in Background"
+     start_tcpdump
+-    M_NS_DAEMONIZE([ovn-chassis-3], [ext1], [ping -f -i 0.1 192.168.1.1], [ping.pid])
++    M_NS_DAEMONIZE([ovn-chassis-2], [ext1], [ping -f -i 0.1 192.168.1.1], [ping.pid])
+ }
+ 
+ stop_sending_background_packets() {
+     echo "$(date +%H:%M:%S.%03N) Stopping Background process"
+     m_as ovn-chassis-1 ps -ef | grep -v grep | grep -q ping && \
+         m_as ovn-chassis-1 echo "Stopping ping on ovn-chassis-1" && killall ping
+-    m_as ovn-chassis-3 ps -ef | grep -v grep | grep -q ping && \
++    m_as ovn-chassis-2 ps -ef | grep -v grep | grep -q ping && \
+         m_as ovn-chassis-2 echo "Stopping ping on ovn-chassis-2" && killall ping
+     stop_tcpdump
+ }
+@@ -3253,8 +3216,8 @@ check_for_new_garps() {
+     expecting_garp=$2
+     n_new_garps=$(cat ${hv}_out.tcpdump | grep -c "f0:00:c0:a8:00:fe > Broadcast, ethertype ARP (0x0806), length 42: Request who-has 192.168.0.254 tell 192.168.0.254, length 28")
+ 
+-    if [[ "$expecting_garp" == "true" ]]; then
+-        echo "$(date +%H:%M:%S.%03N) Waiting/checking for garp from $hv - Starting with $n_new_garps"
++    if [ "$expecting_garp" == "true" ]; then
++        AS_BOX([$(date +%H:%M:%S.%03N) Waiting/checking for garp from $hv - Starting with $n_new_garps])
+         OVS_WAIT_UNTIL([
+             n_garps=$n_new_garps
+             n_new_garps=$(cat ${hv}_out.tcpdump | grep -c "f0:00:c0:a8:00:fe > Broadcast, ethertype ARP (0x0806), length 42: Request who-has 192.168.0.254 tell 192.168.0.254, length 28")
+@@ -3262,7 +3225,7 @@ check_for_new_garps() {
+             test "$n_garps" -ne "$n_new_garps"
+         ])
+     else
+-        echo "$(date +%H:%M:%S.%03N) Checking no garp from ${hv}"
++        AS_BOX([$(date +%H:%M:%S.%03N) Checking no garp from ${hv}])
+         # Waiting a few seconds to get a chance to see unexpected garps.
+         sleep 3
+         n_garps=$(cat ${hv}_out.tcpdump | grep -c "f0:00:c0:a8:00:fe > Broadcast, ethertype ARP (0x0806), length 42: Request who-has 192.168.0.254 tell 192.168.0.254, length 28")
+@@ -3278,8 +3241,8 @@ check_for_new_echo_pkts() {
+     n_new_echo_req=$(cat ${hv}.tcpdump | grep -c "$mac_src > $mac_dst, ethertype IPv4 (0x0800), length 98: 192.168.0.1 > 192.168.1.1: ICMP echo request")
+     n_new_echo_rep=$(cat ${hv}.tcpdump | grep -c "$mac_dst > $mac_src, ethertype IPv4 (0x0800), length 98: 192.168.1.1 > 192.168.0.1: ICMP echo reply")
+ 
+-    if [[ "$expecting_pkts" == "true" ]]; then
+-        echo "$(date +%H:%M:%S.%03N) Waiting/checking for echo pkts through ${hv}"
++    if [ "$expecting_pkts" == "true" ]; then
++        AS_BOX([$(date +%H:%M:%S.%03N) Waiting/checking for echo pkts through ${hv}])
+         echo "Starting with $n_new_echo_req requests and $n_new_echo_rep replies so far on ${hv}."
+         OVS_WAIT_UNTIL([
+             n_echo_req=$n_new_echo_req
+@@ -3290,7 +3253,7 @@ check_for_new_echo_pkts() {
+             test "$n_echo_req" -ne "$n_new_echo_req" && test "$n_echo_rep" -ne "$n_new_echo_rep"
+         ])
+     else
+-        echo "$(date +%H:%M:%S.%03N) Checking no pkts from ${hv}"
++        AS_BOX([$(date +%H:%M:%S.%03N) Checking no pkts from ${hv}])
+         # Waiting a few seconds to get a chance to see unexpected pkts.
+         sleep 3
+         n_echo_req=$(cat ${hv}.tcpdump | grep -c "$mac_src > $mac_dst, ethertype IPv4 (0x0800), length 98: 192.168.0.1 > 192.168.1.1: ICMP echo request")
+@@ -3308,44 +3271,22 @@ dump_statistics() {
+     ch1_rep=$(grep -c "ICMP echo reply" ch1.tcpdump)
+     ch2_req=$(grep -c "ICMP echo request" ch2.tcpdump)
+     ch2_rep=$(grep -c "ICMP echo reply" ch2.tcpdump)
+-    ch3_req=$(grep -c "ICMP echo request" ch3.tcpdump)
+-    ch3_rep=$(grep -c "ICMP echo reply" ch3.tcpdump)
+     gw1_req=$(grep -c "ICMP echo request" gw1.tcpdump)
+     gw1_rep=$(grep -c "ICMP echo reply" gw1.tcpdump)
+     gw2_req=$(grep -c "ICMP echo request" gw2.tcpdump)
+     gw2_rep=$(grep -c "ICMP echo reply" gw2.tcpdump)
+     gw3_req=$(grep -c "ICMP echo request" gw3.tcpdump)
+     gw3_rep=$(grep -c "ICMP echo reply" gw3.tcpdump)
+-    echo "$n1 claims in gw1, $n2 in gw2 and $n3 on gw3" >&2
+-    echo "ch3_req=$ch3_req gw_req=($gw1_req + $gw2_req +$gw3_req) ch1_req=$ch1_req ch1_rep=$ch1_rep gw_rep=($gw1_rep + $gw2_rep + $gw3_rep) ch3_rep=$ch3_rep ch2=($ch2_req+$ch2_rep)" >&2
+-    echo "$((ch3_req - ch3_rep))"
+-}
+-
+-add_port() {
+-    bridge=$1
+-    interface=$2
+-    address=$3
+-    echo "Adding $bridge $interface $address"
+-
+-    pid=$(podman inspect -f '{{.State.Pid}}' ovn-gw-1)
+-    ln -sf /proc/$pid/ns/net /var/run/netns/$pid
+-    port=$(host_ovs_vsctl --data=bare --no-heading --columns=name find interface \
+-           external_ids:container_id=ovn-gw-1 external_ids:container_iface="$interface")
+-    port="${port:0:13}"
+-    ip link add "${port}_l" type veth peer name "${port}_c"
+-    ip link set "${port}_l" up
+-    ip link set "${port}_c" netns $pid
+-    ip netns exec $pid ip link set dev "${port}_c" name "$interface"
+-    ip netns exec $pid ip link set "$interface" up
+-    if [[ -n "$address" ]]; then
+-        ip netns exec $pid ip addr add "$address" dev "$interface"
+-    fi
++    echo "$n1 claims in gw1, $n2 in gw2 and $n3 on gw3"
++    echo "ch2_request=$ch2_req gw1_request=$gw1_req gw2_request=$gw2_req gw3_request=$gw3_req ch1_request=$ch1_req ch1_reply=$ch1_rep gw1_reply=$gw1_rep gw2_reply=$gw2_rep gw3_reply=$gw3_rep ch2_reply=$ch2_rep"
+ }
+ 
+-prepare() {
++check_migration_between_gw1_and_gw2() {
++    action=$1
+     send_background_packets
++
+     # We make sure gw1 is leader since enough time that it generated all its garps.
+-    echo $(date +%H:%M:%S.%03N) Waiting all garps sent by gw1
++    AS_BOX([$(date +%H:%M:%S.%03N) Waiting all garps sent by gw1])
+     n_new_garps=$(cat gw1_out.tcpdump | grep -c "f0:00:c0:a8:00:fe > Broadcast, ethertype ARP (0x0806), length 42: Request who-has 192.168.0.254 tell 192.168.0.254, length 28")
+     OVS_WAIT_UNTIL([
+         n_garps=$n_new_garps
+@@ -3361,269 +3302,130 @@ prepare() {
+     check_for_new_echo_pkts gw2 "00:00:c0:a8:00:01" "f0:00:c0:a8:00:fe" "false"
+     check_for_new_echo_pkts gw3 "00:00:c0:a8:00:01" "f0:00:c0:a8:00:fe" "false"
+ 
+-    # All packets should go through gw1, and none through gw2 or gw3.
+-    check_packets "true" "false" "false" "true"
+     flap_count_gw_1=$(m_as ovn-gw-1 ovs-vsctl get interface $from_gw1_to_gw2 bfd_status | sed 's/.*flap_count=\"\([[0-9]]*\).*/\1/g')
+     flap_count_gw_2=$(m_as ovn-gw-2 ovs-vsctl get interface $from_gw2_to_gw1 bfd_status | sed 's/.*flap_count=\"\([[0-9]]*\).*/\1/g')
+-}
+ 
+-check_loss_after_flap()
+-{
+-    dead=$1
+-    max_expected_loss=$2
++    if [ test "$action" == "stop_bfd" ]; then
++        AS_BOX([$(date +%H:%M:%S.%03N) Blocking bfd on gw1 (from $ip_gw1 to $ip_gw2)])
++        nsenter --net=/proc/$gw1_pid/ns/net nft add table ip ovn-test
++        nsenter --net=/proc/$gw1_pid/ns/net nft 'add chain ip ovn-test INPUT { type filter hook input priority 0; policy accept; }'
++        # Drop BFD from gw-1 to gw-2: geneve port (6081), inner port 3784 (0xec8), Session state Up, Init, Down.
++        nsenter --net=/proc/$gw1_pid/ns/net nft add rule ip ovn-test INPUT ip daddr $ip_gw1 ip saddr $ip_gw2 udp dport 6081 '@th,416,16 == 0x0ec8 @th,472,8 == 0xc0  counter drop'
++        nsenter --net=/proc/$gw1_pid/ns/net nft add rule ip ovn-test INPUT ip daddr $ip_gw1 ip saddr $ip_gw2 udp dport 6081 '@th,416,16 == 0x0ec8 @th,472,8 == 0x80  counter drop'
++        nsenter --net=/proc/$gw1_pid/ns/net nft add rule ip ovn-test INPUT ip daddr $ip_gw1 ip saddr $ip_gw2 udp dport 6081 '@th,416,16 == 0x0ec8 @th,472,8 == 0x40  counter drop'
++
++        # We do not check that packets go through gw2 as BFD between chassis-2 and gw1 is still up
++    fi
++
++    if [ test "$action" == "kill_gw2" ]; then
++        AS_BOX([$(date +%H:%M:%S.%03N) Killing gw2 ovn-controller])
++        on_exit 'm_as ovn-gw-2 /usr/share/openvswitch/scripts/ovs-ctl status ||
++                 m_as ovn-gw-2 /usr/share/openvswitch/scripts/ovs-ctl start --system-id=ovn-gw-2'
++        on_exit 'm_as ovn-gw-2 /usr/share/ovn/scripts/ovn-ctl status_controller ||
++                 m_as ovn-gw-2 /usr/share/ovn/scripts/ovn-ctl start_controller ${CONTROLLER_SSL_ARGS}'
++
++        m_as ovn-gw-2 kill -9 $(m_as ovn-gw-2 cat /run/ovn/ovn-controller.pid)
++        m_as ovn-gw-2 kill -9 $(m_as ovn-gw-2 cat /run/openvswitch/ovs-vswitchd.pid)
++        m_as ovn-gw-2 kill -9 $(m_as ovn-gw-2 cat /run/openvswitch/ovsdb-server.pid)
++        # Also delete datapath (flows)
++        m_as ovn-gw-2 ovs-dpctl del-dp system@ovs-system
++    fi
++
++    if [ test "$action" == "kill_gw1" ]; then
++        AS_BOX([$(date +%H:%M:%S.%03N) Killing gw1 ovn-controller])
++        on_exit 'm_as ovn-gw-1 /usr/share/openvswitch/scripts/ovs-ctl status ||
++                 m_as ovn-gw-1 /usr/share/openvswitch/scripts/ovs-ctl start --system-id=ovn-gw-1'
++        on_exit 'm_as ovn-gw-1 /usr/share/ovn/scripts/ovn-ctl status_controller ||
++                 m_as ovn-gw-1 /usr/share/ovn/scripts/ovn-ctl start_controller ${CONTROLLER_SSL_ARGS}'
++
++        m_as ovn-gw-1 kill -9 $(m_as ovn-gw-1 cat /run/ovn/ovn-controller.pid)
++        m_as ovn-gw-1 kill -9 $(m_as ovn-gw-1 cat /run/openvswitch/ovs-vswitchd.pid)
++        m_as ovn-gw-1 kill -9 $(m_as ovn-gw-1 cat /run/openvswitch/ovsdb-server.pid)
++        # Also delete datapath (flows)
++        m_as ovn-gw-1 ovs-dpctl del-dp system@ovs-system
++    fi
+ 
+-    if [[ "$dead" == "gw2" ]]; then
+-        echo "$(date +%H:%M:%S.%03N) Waiting for flap count between gw1 and gw2 to increase"
++    if [ test "$action" == "kill_gw2" ]; then
++        AS_BOX([$(date +%H:%M:%S.%03N) Waiting for flap count between gw1 and gw2 to increase])
+         OVS_WAIT_UNTIL([
+             new_flap_count=$(m_as ovn-gw-1 ovs-vsctl get interfac $from_gw1_to_gw2 bfd_status | sed 's/.*flap_count=\"\([[0-9]]*\).*/\1/g')
+             echo "Comparing $new_flap_count versus $flap_count_gw_1"
+             test "$new_flap_count" -gt "$((flap_count_gw_1))"
+         ])
+     else
+-        echo "$(date +%H:%M:%S.%03N) Waiting for flap count between gw2 and gw1 to increase])"
++        AS_BOX([$(date +%H:%M:%S.%03N) Waiting for flap count between gw2 and gw1 to increase])
+         OVS_WAIT_UNTIL([
+             new_flap_count=$(m_as ovn-gw-2 ovs-vsctl get interfac $from_gw2_to_gw1 bfd_status | sed 's/.*flap_count=\"\([[0-9]]*\).*/\1/g')
+             echo "Comparing $new_flap_count versus $flap_count_gw_2"
+             test "$new_flap_count" -gt "$((flap_count_gw_2))"
+         ])
++
+     fi
++    AS_BOX([$(date +%H:%M:%S.%03N) Flapped!])
+ 
+-    echo "$(date +%H:%M:%S.%03N) Flapped!"
+     # Wait a few more second for the fight.
+-    sleep 4
+-
+-    echo "$(date +%H:%M:%S.%03N) Statistics after flapping"
+-    lost=$(dump_statistics)
+-    echo "===> $lost packet lost while handling migration"
+-    AT_CHECK([test "$lost" -le "$max_expected_loss"])
+-}
+-
+-final_check()
+-{
+-    action=$1
+-    lost=$2
+-    max_expected_loss_after_restoration=$3
+-
+-    # Wait a little more to get packets while network is restored
+     sleep 2
+-    echo "$(date +%H:%M:%S.%03N) Statistics after network restored (after $action)"
+-    new_lost=$(dump_statistics)
+-    echo "===> $((new_lost - lost)) packets lost during network restoration"
+-    AT_CHECK([test "$((new_lost - lost))" -le "$max_expected_loss_after_restoration"])
+-    stop_sending_background_packets
+-}
+-
+-check_garps()
+-{
+-    check_for_new_garps gw1 "$1"
+-    check_for_new_garps gw2 "$2"
+-    check_for_new_garps gw3 "$3"
+-}
+-
+-check_packets()
+-{
+-    check_for_new_echo_pkts gw1 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "$1"
+-    check_for_new_echo_pkts gw2 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "$2"
+-    check_for_new_echo_pkts gw3 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "$3"
+-    check_for_new_echo_pkts ch1 f0:00:c0:a8:01:fe f0:00:c0:a8:01:01 "$4"
+-}
+-
+-check_migration_between_gw1_and_gw2_bfd_stop()
+-{
+-    AS_BOX([$(date +%H:%M:%S.%03N) Testing migration after bfd_stop])
+-    max_expected_max_expected_loss1=$1
+-    max_expected_max_expected_loss2=$2
+-    prepare
+-
+-    echo "$(date +%H:%M:%S.%03N) Blocking bfd on gw1 (from $ip_gw1 to $ip_gw2)"
+-    nsenter --net=/proc/$gw1_pid/ns/net nft add table ip ovn-test
+-    nsenter --net=/proc/$gw1_pid/ns/net nft 'add chain ip ovn-test INPUT { type filter hook input priority 0; policy accept; }'
+-    # Drop BFD from gw-1 to gw-2: geneve port (6081), inner port 3784 (0xec8), Session state Up, Init, Down.
+-    nsenter --net=/proc/$gw1_pid/ns/net nft add rule ip ovn-test INPUT ip daddr $ip_gw1 ip saddr $ip_gw2 udp dport 6081 '@th,416,16 == 0x0ec8 @th,472,8 == 0xc0  counter drop'
+-    nsenter --net=/proc/$gw1_pid/ns/net nft add rule ip ovn-test INPUT ip daddr $ip_gw1 ip saddr $ip_gw2 udp dport 6081 '@th,416,16 == 0x0ec8 @th,472,8 == 0x80  counter drop'
+-    nsenter --net=/proc/$gw1_pid/ns/net nft add rule ip ovn-test INPUT ip daddr $ip_gw1 ip saddr $ip_gw2 udp dport 6081 '@th,416,16 == 0x0ec8 @th,472,8 == 0x40  counter drop'
+-
+-    check_loss_after_flap "gw1" $max_expected_max_expected_loss1
+-
+-    # gw1 still alive and gw2 tried to claim => gw1 should restart generating garps.
+-    check_garps "true" "false" "false"
+-    check_packets "true" "false" "false" "true"
+-
+-    echo "$(date +%H:%M:%S.%03N) Unblocking bfd on gw1"
+-    nsenter --net=/proc/$gw1_pid/ns/net nft -a list ruleset
+-    nsenter --net=/proc/$gw1_pid/ns/net nft delete table ip ovn-test
+-
+-    # The network is now restored => packets should go through gw1 and reach chassis-1.
+-    check_packets "true" "false" "false" "true"
+-    final_check "bfd_stop" $lost $max_expected_max_expected_loss2
+-}
+-
+-check_migration_between_gw1_and_gw2_kill_gw2() {
+-    AS_BOX([$(date +%H:%M:%S.%03N) Check migration after killing gw2 ovn-controller & vswitchd])
+-    max_expected_loss1=$1
+-    max_expected_loss2=$2
+-    prepare
+-
+-    on_exit 'm_as ovn-gw-2 /usr/share/openvswitch/scripts/ovs-ctl status ||
+-             m_as ovn-gw-2 /usr/share/openvswitch/scripts/ovs-ctl start --system-id=ovn-gw-2'
+-    on_exit 'm_as ovn-gw-2 /usr/share/ovn/scripts/ovn-ctl status_controller ||
+-             m_as ovn-gw-2 /usr/share/ovn/scripts/ovn-ctl start_controller ${CONTROLLER_SSL_ARGS}'
+-
+-    m_as ovn-gw-2 kill -9 $(m_as ovn-gw-2 cat /run/ovn/ovn-controller.pid)
+-    m_as ovn-gw-2 kill -9 $(m_as ovn-gw-2 cat /run/openvswitch/ovs-vswitchd.pid)
+-    m_as ovn-gw-2 kill -9 $(m_as ovn-gw-2 cat /run/openvswitch/ovsdb-server.pid)
+-    m_as ovn-gw-2 ovs-dpctl del-dp system@ovs-system
+-
+-    check_loss_after_flap "gw2" $max_expected_loss1
+-
+-    # gw1 still alive, but gw2 did not try to claim => gw1 should not generate new garps.
+-    check_garps "false" "false" "false"
+-    check_packets "true" "fals" "false" "true"
+-
+-    echo "$(date +%H:%M:%S.%03N) Restarting gw2 ovn-vswitchd]"
+-    m_as ovn-gw-2 /usr/share/openvswitch/scripts/ovs-ctl start --system-id=ovn-gw-2
+-
+-    echo "$(date +%H:%M:%S.%03N) Restarting gw2 ovn-controller"
+-    m_as ovn-gw-2 /usr/share/ovn/scripts/ovn-ctl start_controller ${CONTROLLER_SSL_ARGS}
+-
+-    # The network is now restored => packets should go through gw1 and reach chassis-1.
+-    check_packets "true" "false" "false" "true"
+-    final_check "kill_gw2" $lost $max_expected_loss2
+-}
+-
+-check_migration_between_gw1_and_gw2_update_ovs() {
+-    AS_BOX([$(date +%H:%M:%S.%03N) Check migration after restarting gw1 ovs-vswitchd ("update")])
+-    max_expected_loss1=$1
+-    max_expected_loss2=$2
+-    prepare
+-
+-    m_as ovn-gw-1 /usr/share/openvswitch/scripts/ovs-ctl restart --system-id=ovn-gw-1
+-
+-    check_loss_after_flap "gw1" $max_expected_loss1
+-
+-    # The network is now restored => packets should go through gw1 and reach chassis-1.
+-    check_packets "true" "false" "false" "true"
+-    final_check "ovs_update" $lost $max_expected_loss2
+-}
+-
+-check_migration_between_gw1_and_gw2_kill_gw1() {
+-    AS_BOX([$(date +%H:%M:%S.%03N) Killing gw1 ovn-controller and ovs-vswitchd])
+-    max_expected_loss1=$1
+-    max_expected_loss2=$2
+-    prepare
+-
+-    on_exit 'm_as ovn-gw-1 /usr/share/openvswitch/scripts/ovs-ctl status ||
+-             m_as ovn-gw-1 /usr/share/openvswitch/scripts/ovs-ctl start --system-id=ovn-gw-1'
+-    on_exit 'm_as ovn-gw-1 /usr/share/ovn/scripts/ovn-ctl status_controller ||
+-             m_as ovn-gw-1 /usr/share/ovn/scripts/ovn-ctl start_controller ${CONTROLLER_SSL_ARGS}'
+-
+-    m_as ovn-gw-1 kill -9 $(m_as ovn-gw-1 cat /run/ovn/ovn-controller.pid)
+-    m_as ovn-gw-1 kill -9 $(m_as ovn-gw-1 cat /run/openvswitch/ovs-vswitchd.pid)
+-    m_as ovn-gw-1 kill -9 $(m_as ovn-gw-1 cat /run/openvswitch/ovsdb-server.pid)
+-    # Also delete datapath (flows)
+-    m_as ovn-gw-1 ovs-dpctl del-dp system@ovs-system
+-
+-    check_loss_after_flap "gw1" $max_expected_loss1
+-
+-    # gw1 died => gw2 should generate garps.
+-    check_garps "false" "true" "false"
+-    check_packets "false" "true" "false" "true"
+-    echo "$(date +%H:%M:%S.%03N) Restarting gw1 ovn-vswitchd after killing gw1"
+-    m_as ovn-gw-1 /usr/share/openvswitch/scripts/ovs-ctl start --system-id=ovn-gw-1
+-
+-    # Wait some long time before restarting ovn-controller
+-    sleep 10
+-
+-    # gw2 should still be handling packets as OVN not restarted on gw1
+-    check_packets "false" "true" "false" "true"
+-
+-    echo "$(date +%H:%M:%S.%03N) Restarting gw1 ovn-controller after killing gw1"
+-    m_as ovn-gw-1 /usr/share/ovn/scripts/ovn-ctl start_controller ${CONTROLLER_SSL_ARGS}
+-
+-    # The network is now restored => packets should go through gw1 and reach chassis-1.
+-    check_packets "true" "false" "false" "true"
+-    final_check "kill_gw1" $lost $max_expected_loss2
+-}
+-
+-check_migration_between_gw1_and_gw2_reboot_gw1() {
+-    ip_gw1_eth1=$(podman exec ovn-gw-1 ip -brief address show eth1 | awk '{print $3}' | cut -d/ -f1)
+-    cidr=$(podman exec ovn-gw-1 ip -brief address show eth1 | awk '{print $3}' | cut -d/ -f2)
+-    AS_BOX([$(date +%H:%M:%S.%03N) Rebooting ovn-gw-1 with $ip_gw1_eth1/$cidr])
+-    max_expected_loss1=$1
+-    max_expected_loss2=$2
+-    prepare
+-
+-    podman stop -t 0 ovn-gw-1
+-    (exec 3>&- 4>&- 5>&- 6>&-; podman start ovn-gw-1)
+-
+-    # As ovn-gw-1 got stopped and restarted, its ports (added by fake
+-    # multinode) got deleted. Add them back.
+-    add_port br-ovn eth1 $ip_gw1_eth1/$cidr
+-    add_port br-ovn-ext eth2
+-    M_START_TCPDUMPS([ovn-gw-1], [-neei eth2], [gw1], [ovn-gw-1], [-neei eth1], [gw1_eth1], [ovn-gw-1], [-neei eth2 -Q out], [gw1_out])
+-    check_loss_after_flap "gw1" $max_expected_loss1
+-
+-    # gw1 died => gw2 should generate garps.
+-    check_garps "false" "true" "false"
+-    check_packets "false" "true" "false" "true"
+-
+-    echo "$(date +%H:%M:%S.%03N) Restarting gw1 ovn-vswitchd after rebooting gw1"
+-    m_as ovn-gw-1 /usr/share/openvswitch/scripts/ovs-ctl start --system-id=ovn-gw-1
+-
+-    # Wait some long time before restarting ovn-controller
+-    sleep 10
++    AS_BOX([$(date +%H:%M:%S.%03N) Statistics after flapping])
++    dump_statistics
++
++    if [ test "$action" == "stop_bfd" ]; then
++        # gw1 still alive and gw2 tried to claim => gw1 should restart generating garps.
++        check_for_new_garps gw1 "true"
++        check_for_new_garps gw2 "false"
++        check_for_new_garps gw3 "false"
++        check_for_new_echo_pkts gw1 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "true"
++        check_for_new_echo_pkts gw2 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "false"
++        check_for_new_echo_pkts gw3 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "false"
++        check_for_new_echo_pkts ch1 f0:00:c0:a8:01:fe f0:00:c0:a8:01:01 "true"
++        AS_BOX([$(date +%H:%M:%S.%03N) Unblocking bfd on gw1])
++        nsenter --net=/proc/$gw1_pid/ns/net nft -a list ruleset
++        nsenter --net=/proc/$gw1_pid/ns/net nft delete table ip ovn-test
++    fi
+ 
+-    # gw2 should still be handling packets as OVN not restarted on gw1
+-    check_packets "false" "true" "false" "true"
++    if [ test "$action" == "kill_gw2" ]; then
++        # gw1 still alive, but gw2 did not try to claim => gw1 should not generate new garps.
++        check_for_new_garps gw1 "false"
++        check_for_new_garps gw2 "false"
++        check_for_new_garps gw3 "false"
++        check_for_new_echo_pkts gw1 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "true"
++        check_for_new_echo_pkts gw2 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "false"
++        check_for_new_echo_pkts gw3 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "false"
++        check_for_new_echo_pkts ch1 f0:00:c0:a8:01:fe f0:00:c0:a8:01:01 "true"
++        AS_BOX([$(date +%H:%M:%S.%03N) Restarting gw2 ovn-vswitchd])
++        m_as ovn-gw-2 /usr/share/openvswitch/scripts/ovs-ctl start --system-id=ovn-gw-2
++
++        AS_BOX([$(date +%H:%M:%S.%03N) Restarting gw2 ovn-controller])
++        m_as ovn-gw-2 /usr/share/ovn/scripts/ovn-ctl start_controller ${CONTROLLER_SSL_ARGS}
++    fi
+ 
+-    echo "$(date +%H:%M:%S.%03N) Restarting gw1 ovn-controller after rebooting gw1"
+-    m_as ovn-gw-1 /usr/share/ovn/scripts/ovn-ctl start_controller ${CONTROLLER_SSL_ARGS}
++    if [ test "$action" == "kill_gw1" ]; then
++        # gw1 died => gw2 should generate garps.
++        check_for_new_garps gw1 "false"
++        check_for_new_garps gw2 "true"
++        check_for_new_garps gw3 "false"
++        check_for_new_echo_pkts gw1 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "false"
++        check_for_new_echo_pkts gw2 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "true"
++        check_for_new_echo_pkts gw3 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "false"
++        check_for_new_echo_pkts ch1 f0:00:c0:a8:01:fe f0:00:c0:a8:01:01 "true"
++        AS_BOX([$(date +%H:%M:%S.%03N) Restarting gw1 ovn-vswitchd])
++        m_as ovn-gw-1 /usr/share/openvswitch/scripts/ovs-ctl start --system-id=ovn-gw-1
++
++        AS_BOX([$(date +%H:%M:%S.%03N) Restarting gw1 ovn-controller])
++        m_as ovn-gw-1 /usr/share/ovn/scripts/ovn-ctl start_controller ${CONTROLLER_SSL_ARGS}
++    fi
+ 
+     # The network is now restored => packets should go through gw1 and reach chassis-1.
+-    check_packets "true" "false" "false" "true"
+-    final_check "kill_gw1" $lost $max_expected_loss2
+-}
+-
+-check_compute_restart() {
+-    AS_BOX([$(date +%H:%M:%S.%03N) Killing ovn-chassis-1 ovn-controller and ovs-vswitchd])
+-    max_expected_loss=$1
+-    prepare
+-
+-    # Kill ovn-chassis-1
+-    echo "$(date +%H:%M:%S.%03N) Killing chassis-1"
+-    on_exit 'm_as ovn-chassis-1 /usr/share/openvswitch/scripts/ovs-ctl status ||
+-             m_as ovn-chassis-1 /usr/share/openvswitch/scripts/ovs-ctl start --system-id=ovn-chassis-1'
+-    on_exit 'm_as ovn-chassis-1 /usr/share/ovn/scripts/ovn-ctl status_controller ||
+-             m_as ovn-chassis-1 /usr/share/ovn/scripts/ovn-ctl start_controller ${CONTROLLER_SSL_ARGS}'
+-
+-    m_as ovn-chassis-1 kill -9 $(m_as ovn-chassis-1 cat /run/ovn/ovn-controller.pid)
+-    m_as ovn-chassis-1 kill -9 $(m_as ovn-chassis-1 cat /run/openvswitch/ovs-vswitchd.pid)
+-    m_as ovn-chassis-1 kill -9 $(m_as ovn-chassis-1 cat /run/openvswitch/ovsdb-server.pid)
+-
+-    # Now restart chassis-1
+-    flap_count=$(m_as ovn-gw-2 ovs-vsctl get interfac $from_gw2_to_ch1 bfd_status | sed 's/.*flap_count=\"\([[0-9]]*\).*/\1/g')
+-    echo "$(date +%H:%M:%S.%03N) Restarting ovn-chassis-1 ovn-vswitchd."
+-    m_as ovn-chassis-1 /usr/share/openvswitch/scripts/ovs-ctl start --system-id=ovn-chassis-1
+-
+-    echo "$(date +%H:%M:%S.%03N) Waiting for flap count between gw-1 and chassis-1 to increase"
+-    OVS_WAIT_UNTIL([
+-        new_flap_count=$(m_as ovn-gw-1 ovs-vsctl get interfac $from_gw1_to_ch1 bfd_status | sed 's/.*flap_count=\"\([[0-9]]*\).*/\1/g')
+-        echo "Comparing $new_flap_count versus $flap_count"
+-        test "$new_flap_count" -gt "$((flap_count))"
+-    ])
+-
+-    wait_bfd_up ovn-chassis-1 $from_ch1_to_gw1
+-
+-    echo "$(date +%H:%M:%S.%03N) Restarting ovn-chassis-1 ovn-controller."
+-    m_as ovn-chassis-1 /usr/share/ovn/scripts/ovn-ctl start_controller ${CONTROLLER_SSL_ARGS}
+-
+-    # Wait a long time to catch losses
+-    sleep 5
+-    final_check "compute" 0 $max_expected_loss
++    check_for_new_echo_pkts gw1 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "true"
++    check_for_new_echo_pkts gw2 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "false"
++    check_for_new_echo_pkts gw3 00:00:c0:a8:00:01 f0:00:c0:a8:00:fe "false"
++    check_for_new_echo_pkts ch1 f0:00:c0:a8:01:fe f0:00:c0:a8:01:01 "true"
++    AS_BOX([$(date +%H:%M:%S.%03N) Statistics after network restored])
++    dump_statistics
++    stop_sending_background_packets
+ }
+ 
+ start_tcpdump
+-echo "$(date +%H:%M:%S.%03N) Sending packet from hv1-vif1(inside1) to ext1"
++AS_BOX([$(date +%H:%M:%S.%03N) Sending packet from hv1-vif1(inside1) to ext1])
+ M_NS_CHECK_EXEC([ovn-chassis-1], [hv1-vif1], [ping -c3 -q -i 0.1 192.168.0.1 | FORMAT_PING],
+ [0], [dnl
+ 3 packets transmitted, 3 received, 0% packet loss, time 0ms
+@@ -3631,7 +3433,7 @@ M_NS_CHECK_EXEC([ovn-chassis-1], [hv1-vif1], [ping -c3 -q -i 0.1 192.168.0.1 | F
+ stop_tcpdump
+ 
+ # It should have gone through gw1 and not gw2
+-echo "$(date +%H:%M:%S.%03N) Checking it went through gw1 and not gw2"
++AS_BOX([$(date +%H:%M:%S.%03N) Checking it went through gw1 and not gw2])
+ AT_CHECK([cat gw2.tcpdump | grep "ICMP echo"], [1], [dnl
+ ])
+ 
+@@ -3644,29 +3446,17 @@ f0:00:c0:a8:00:fe > 00:00:c0:a8:00:01, ethertype IPv4 (0x0800), length 98: 192.1
+ 00:00:c0:a8:00:01 > f0:00:c0:a8:00:fe, ethertype IPv4 (0x0800), length 98: 192.168.0.1 > 192.168.1.1: ICMP echo reply,
+ ])
+ 
+-# We stop bfd between gw1 & gw2, but keep gw1 & gw2 running. We should not lose packets.
+-check_migration_between_gw1_and_gw2_bfd_stop 1 1
++# We stop bfd between gw1 & gw2, but keep gw1 & gw2 running.
++check_migration_between_gw1_and_gw2 "stop_bfd"
+ 
+ # We simulate death of gw2. It should not have any effect.
+-check_migration_between_gw1_and_gw2_kill_gw2 1 1
+-
+-# We simulate ovs update on gw1. When ovs is stopped, flows should still be handled by Kernel datapath.
+-# When OVS is restarted, BFD should go down immediately, and gw2 will start handling packets.
+-# There will be packet losses as gw2 will usually see BFD from gw1 up (and hence relase port) before gw1 sees
+-# BFD up (and claim port).
+-check_migration_between_gw1_and_gw2_update_ovs 20 1
+-
+-# We simulate restart of both OVS & OVN gw1. gw2 should take over.
+-check_migration_between_gw1_and_gw2_kill_gw1 40 20
++check_migration_between_gw1_and_gw2 "kill_gw2"
+ 
+ # We simulate death of gw1. gw2 should take over.
+-check_migration_between_gw1_and_gw2_reboot_gw1 40 20
+-
+-# We simulate restart of ovn-chassis-1. We expect for ~4 sec losses as we wait for bfd up before starting
+-# ovn-controller (1 sec to restart ovs, 2 sec for bfd to go up, 1 sec to detect it).
+-check_compute_restart 40
++check_migration_between_gw1_and_gw2 "kill_gw1"
+ 
+ AT_CLEANUP
++])
+ 
+ AT_SETUP([ovn multinode bgp L2 EVPN])
+ check_fake_multinode_setup
+@@ -3837,12 +3627,17 @@ check multinode_nbctl --wait=hv \
+     -- acl-add ls from-lport 100 "ip" allow-related \
+     -- acl-add ls to-lport 100 "ip" allow-related
+ 
+-dnl Verify fabric-to-workload pings still work with stateful ACL.
++dnl Verify fabric-to-workload pings still work with stateful ACL (both directions).
+ 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-1 ip netns exec fabric_workload ping -6 -W 1 -c 1 10::11])
+ OVS_WAIT_UNTIL([m_as ovn-gw-2 ip netns exec fabric_workload ping    -W 1 -c 1 10.0.0.12])
+ OVS_WAIT_UNTIL([m_as ovn-gw-2 ip netns exec fabric_workload ping -6 -W 1 -c 1 10::12])
+ 
++OVS_WAIT_UNTIL([m_as ovn-gw-1 ip netns exec w1 ping    -W 1 -c 1 10.0.0.41])
++OVS_WAIT_UNTIL([m_as ovn-gw-1 ip netns exec w1 ping -6 -W 1 -c 1 10::41])
++OVS_WAIT_UNTIL([m_as ovn-gw-2 ip netns exec w2 ping    -W 1 -c 1 10.0.0.42])
++OVS_WAIT_UNTIL([m_as ovn-gw-2 ip netns exec w2 ping -6 -W 1 -c 1 10::42])
++
+ dnl Also add a load balancer and verify pings still work.
+ check multinode_nbctl --wait=hv \
+     -- lb-add lb1 10.0.0.100:80 10.0.0.11:80 \
+@@ -3853,6 +3648,11 @@ OVS_WAIT_UNTIL([m_as ovn-gw-1 ip netns exec fabric_workload ping -6 -W 1 -c 1 10
+ OVS_WAIT_UNTIL([m_as ovn-gw-2 ip netns exec fabric_workload ping    -W 1 -c 1 10.0.0.12])
+ OVS_WAIT_UNTIL([m_as ovn-gw-2 ip netns exec fabric_workload ping -6 -W 1 -c 1 10::12])
+ 
++OVS_WAIT_UNTIL([m_as ovn-gw-1 ip netns exec w1 ping    -W 1 -c 1 10.0.0.41])
++OVS_WAIT_UNTIL([m_as ovn-gw-1 ip netns exec w1 ping -6 -W 1 -c 1 10::41])
++OVS_WAIT_UNTIL([m_as ovn-gw-2 ip netns exec w2 ping    -W 1 -c 1 10.0.0.42])
++OVS_WAIT_UNTIL([m_as ovn-gw-2 ip netns exec w2 ping -6 -W 1 -c 1 10::42])
++
+ dnl Cleanup ACL and LB.
+ check multinode_nbctl --wait=hv \
+     -- acl-del ls \
+diff --git a/tests/ovn-controller-vtep.at b/tests/ovn-controller-vtep.at
+index 35402b00aa..ffd394a992 100644
+--- a/tests/ovn-controller-vtep.at
++++ b/tests/ovn-controller-vtep.at
+@@ -635,6 +635,10 @@ AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_PHY_TO_LOG | grep 'priority=
+ priority=110,tun_id=0x<>,in_port=<> actions=move:NXM_NX_TUN_ID[[0..23]]->OXM_OF_METADATA[[0..23]],load:0x<>->NXM_NX_REG14[[0..14]],load:0x<>->NXM_NX_REG10[[1]],resubmit(,OFTABLE_LOG_INGRESS_PIPELINE)
+ ])
+ 
++# Skip processing ICMP "packet too big" errors in this table if the packet came from a VTEP tunnel.
++AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_PHY_TO_LOG | \
++          grep -E 'icmp_type=3,icmp_code=4|icmp_type=2,icmp_code=0'], [1], [])
++
+ OVN_CONTROLLER_VTEP_STOP([], vtep1)
+ OVN_CLEANUP([hv1])
+ AT_CLEANUP
+diff --git a/tests/ovn-controller.at b/tests/ovn-controller.at
+index c98de9bc46..2fce19e7ba 100644
+--- a/tests/ovn-controller.at
++++ b/tests/ovn-controller.at
+@@ -962,7 +962,7 @@ AT_CLEANUP
+ ])
+ 
+ OVN_FOR_EACH_NORTHD([
+-AT_SETUP([ovn-controller - ovn IP check path ports])
++AT_SETUP([ovn-controller - ovn IP check patch ports])
+ AT_KEYWORDS([ovn-ip-patch-ports])
+ 
+ ovn_start
+@@ -1003,10 +1003,110 @@ check as hv1 ovn-appctl -t ovn-controller inc-engine/clear-stats
+ check ovn-nbctl --wait=hv lsp-set-type ls0-rp localnet
+ check_controller_engine_stats hv1 pflow_output recompute nocompute
+ 
++# Check that adding a new port with a non-default type does not trigger
++# a pflow_output recompute.
++check as hv1 ovn-appctl -t ovn-controller inc-engine/clear-stats
++check ovn-nbctl --wait=hv lsp-add ls0 lsp-remote -- lsp-set-type lsp-remote remote
++check_controller_engine_stats hv1 pflow_output norecompute compute
++
++# Check that deleting a port with a non-default type does not trigger
++# a pflow_output recompute.
++check as hv1 ovn-appctl -t ovn-controller inc-engine/clear-stats
++check ovn-nbctl --wait=hv lsp-del lsp-remote
++check_controller_engine_stats hv1 pflow_output norecompute compute
++
+ OVN_CLEANUP([hv1])
+ AT_CLEANUP
+ ])
+ 
++OVN_FOR_EACH_NORTHD([
++AT_SETUP([ovn-controller - localnet port change and chassisredirect bridged redirect])
++AT_KEYWORDS([ovn-localnet-cr-bridged])
++
++ovn_start
++
++net_add n1
++
++sim_add hv1
++as hv1
++check ovs-vsctl add-br br-phys
++check ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
++ovn_attach n1 br-phys 192.168.0.1
++
++sim_add hv2
++as hv2
++check ovs-vsctl add-br br-phys
++check ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
++ovn_attach n1 br-phys 192.168.0.2
++
++dnl Create the full topology with localnet ports, router,
++dnl gateway chassis, and bridged redirect.  Bind VIFs so the
++dnl datapaths are local.
++check ovn-nbctl                                                                \
++    -- ls-add ls1                                                              \
++      -- lsp-add-localnet-port ls1 ln1 phys                                    \
++        -- set logical_switch_port ln1 tag_request=101                         \
++      -- lsp-add ls1 lp1                                                       \
++        -- lsp-set-addresses lp1 "00:00:00:00:00:01 192.168.1.10"              \
++      -- lsp-add ls1 lp2                                                       \
++        -- lsp-set-addresses lp2 "00:00:00:00:00:02 192.168.1.11"              \
++      -- lsp-add-router-port ls1 ls1-to-router router-to-ls1                   \
++    -- ls-add ls-underlay                                                      \
++      -- lsp-add-localnet-port ls-underlay ln-underlay phys                    \
++        -- set logical_switch_port ln-underlay tag_request=1000                \
++      -- lsp-add-router-port ls-underlay underlay-to-router router-to-underlay \
++    -- lr-add lr1                                                              \
++      -- lrp-add lr1 router-to-ls1 00:00:01:01:02:03 192.168.1.1/24            \
++      -- lrp-add lr1 router-to-underlay 00:00:01:01:02:07 172.31.0.1/24        \
++        -- lrp-set-gateway-chassis router-to-underlay hv1                      \
++        -- lrp-set-redirect-type router-to-underlay bridged
++
++check as hv1 ovs-vsctl add-port br-int vif0 \
++    -- set Interface vif0 external_ids:iface-id=lp1
++check as hv2 ovs-vsctl add-port br-int vif1 \
++    -- set Interface vif1 external_ids:iface-id=lp2
++
++wait_for_ports_up
++check ovn-nbctl --wait=hv sync
++
++dnl Verify initial state: hv2 has the CR bridged redirect flow.
++router_dp_key=$(printf "%x" $(fetch_column datapath tunnel_key external_ids:name=lr1))
++cr_key=$(printf "%x" $(fetch_column port_binding tunnel_key logical_port=cr-router-to-underlay))
++AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=OFTABLE_LOCAL_OUTPUT | grep -q "reg15=0x${cr_key},metadata=0x${router_dp_key}"])
++
++dnl Delete the localnet port on ls-underlay.
++check ovn-nbctl --wait=hv lsp-del ln-underlay
++AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=OFTABLE_LOCAL_OUTPUT | grep "reg15=0x${cr_key},metadata=0x${router_dp_key}"], [1])
++
++dnl Pre-create the patch port that the new localnet port
++dnl would need.  This way, when the localnet port binding arrives,
++dnl patch_run() sees the patch port already exists and non_vif_data
++dnl does NOT change, forcing pflow_output to handle the
++dnl change incrementally instead of recomputing.
++check as hv2 ovs-vsctl                                     \
++    -- add-port br-int patch-br-int-to-ln-underlay         \
++    -- set Interface patch-br-int-to-ln-underlay           \
++       type=patch options:peer=patch-ln-underlay-to-br-int
++check as hv2 ovs-vsctl                                     \
++    -- add-port br-phys patch-ln-underlay-to-br-int        \
++    -- set Interface patch-ln-underlay-to-br-int           \
++       type=patch options:peer=patch-br-int-to-ln-underlay
++check ovn-nbctl --wait=hv sync
++
++dnl Re-add the localnet port.  The patch port already exists,
++dnl so non_vif_data should not change, and pflow_output should
++dnl be handled incrementally.
++check ovn-nbctl --wait=hv                                   \
++    -- lsp-add-localnet-port ls-underlay ln-underlay phys   \
++    -- set logical_switch_port ln-underlay tag_request=1000
++
++dnl Verify the CR bridged redirect flow is back.
++OVS_WAIT_UNTIL([as hv2 ovs-ofctl dump-flows br-int table=OFTABLE_LOCAL_OUTPUT | grep -q "reg15=0x${cr_key},metadata=0x${router_dp_key}"])
++
++OVN_CLEANUP([hv1], [hv2])
++AT_CLEANUP
++])
++
+ AT_SETUP([ovn-controller - I-P for address set update: no conjunction])
+ AT_KEYWORDS([as-i-p])
+ 
+diff --git a/tests/ovn-inc-proc-graph-dump.at b/tests/ovn-inc-proc-graph-dump.at
+index 25ff200a40..ef91014a64 100644
+--- a/tests/ovn-inc-proc-graph-dump.at
++++ b/tests/ovn-inc-proc-graph-dump.at
+@@ -172,9 +172,6 @@ digraph "Incremental-Processing-Engine" {
+ 	northd -> ls_stateful [[label="ls_stateful_northd_handler"]];
+ 	port_group -> ls_stateful [[label="ls_stateful_port_group_handler"]];
+ 	NB_acl -> ls_stateful [[label="ls_stateful_acl_handler"]];
+-	ls_arp [[style=filled, shape=box, fillcolor=white, label="ls_arp"]];
+-	lr_nat -> ls_arp [[label="ls_arp_lr_nat_handler"]];
+-	northd -> ls_arp [[label="ls_arp_northd_handler"]];
+ 	SB_igmp_group [[style=filled, shape=box, fillcolor=white, label="SB_igmp_group"]];
+ 	multicast_igmp [[style=filled, shape=box, fillcolor=white, label="multicast_igmp"]];
+ 	northd -> multicast_igmp [[label="multicast_igmp_northd_handler"]];
+@@ -196,7 +193,6 @@ digraph "Incremental-Processing-Engine" {
+ 	port_group -> lflow [[label="engine_noop_handler"]];
+ 	lr_stateful -> lflow [[label="lflow_lr_stateful_handler"]];
+ 	ls_stateful -> lflow [[label="lflow_ls_stateful_handler"]];
+-	ls_arp -> lflow [[label="lflow_ls_arp_handler"]];
+ 	multicast_igmp -> lflow [[label="lflow_multicast_igmp_handler"]];
+ 	SB_acl_id -> lflow [[label=""]];
+ 	ic_learned_svc_monitors -> lflow [[label="lflow_ic_learned_svc_mons_handler"]];
+diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
+index 39f03ba62b..cc41bbd77b 100644
+--- a/tests/ovn-macros.at
++++ b/tests/ovn-macros.at
+@@ -1314,6 +1314,38 @@ send_na() {
+     check as $hv ovs-appctl netdev-dummy/receive $dev $packet >&2
+ }
+ 
++send_udp() {
++    local hv=$1 dev=$2 hdst=$3 hsrc=$4 idst=$5 isrc=$6
++    local packet=$(fmt_pkt "Ether(dst='${hdst}', src='${hsrc}')/ \
++                            IP(dst='${idst}', src='${isrc}')/UDP()")
++    check as $hv ovs-appctl netdev-dummy/receive $dev $packet
++}
++
++send_udp6() {
++    local hv=$1 dev=$2 hdst=$3 hsrc=$4 idst=$5 isrc=$6
++    local packet=$(fmt_pkt "Ether(dst='${hdst}', src='${hsrc}')/ \
++                            IPv6(dst='${idst}', src='${isrc}')/UDP()")
++    check as $hv ovs-appctl netdev-dummy/receive $dev $packet
++}
++
++dump_arp() {
++    local op=$1 eth_src=$2 eth_dst=$3 spa=$4 tpa=$5 hwdst=$6
++
++    local packet=$(fmt_pkt "Ether(dst='${eth_dst}', src='${eth_src}')/ \
++                      ARP(op=$op, hwsrc='${eth_src}', hwdst='${hwdst}', \
++                      psrc='${spa}', pdst='${tpa}')")
++    echo $packet
++}
++
++dump_ns() {
++    local hdst=$1 hsrc=$2 idst=$3 isrc=$4 tgt=$5
++    local packet=$(fmt_pkt "Ether(dst='${hdst}', src='${hsrc}')/ \
++                            IPv6(dst='${idst}', src='${isrc}')/ \
++                            ICMPv6ND_NS(tgt='${tgt}')/ \
++                            ICMPv6NDOptSrcLLAddr(lladdr='${hsrc}')")
++    echo $packet
++}
++
+ # Wrapper on top of ovn-trace, stripping some things and storing the trace
+ # output to a file called 'trace'.  For now it strips the rows starting
+ # with a '#'.  This should correspond to the flow key and might be displayed
+diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
+index f1349f2133..80edbb284e 100644
+--- a/tests/ovn-northd.at
++++ b/tests/ovn-northd.at
+@@ -7448,6 +7448,9 @@ AT_CHECK([grep lr_in_admission lrflows | grep cr-DR | ovn_strip_lflows], [0], [d
+ ])
+ # Check the flows in lr_in_lookup_neighbor stage
+ AT_CHECK([grep lr_in_lookup_neighbor lrflows | grep cr-DR | ovn_strip_lflows], [0], [dnl
++  table=??(lr_in_lookup_neighbor), priority=100  , match=(inport == "DR-S1" && arp.spa == 172.16.1.0/24 && arp.op == 1 && is_chassis_resident("cr-DR-S1")), action=(reg9[[2]] = lookup_arp(inport, arp.spa, arp.sha); next;)
++  table=??(lr_in_lookup_neighbor), priority=100  , match=(inport == "DR-S2" && arp.spa == 172.16.2.0/24 && arp.op == 1 && is_chassis_resident("cr-DR-S2")), action=(reg9[[2]] = lookup_arp(inport, arp.spa, arp.sha); next;)
++  table=??(lr_in_lookup_neighbor), priority=100  , match=(inport == "DR-S3" && arp.spa == 172.16.3.0/24 && arp.op == 1 && is_chassis_resident("cr-DR-S3")), action=(reg9[[2]] = lookup_arp(inport, arp.spa, arp.sha); next;)
+   table=??(lr_in_lookup_neighbor), priority=120  , match=(inport == "DR-S1" && (nd_na || nd_ns) && eth.mcast && !is_chassis_resident("cr-DR-S1")), action=(reg9[[2]] = 1; next;)
+   table=??(lr_in_lookup_neighbor), priority=120  , match=(inport == "DR-S2" && (nd_na || nd_ns) && eth.mcast && !is_chassis_resident("cr-DR-S2")), action=(reg9[[2]] = 1; next;)
+   table=??(lr_in_lookup_neighbor), priority=120  , match=(inport == "DR-S3" && (nd_na || nd_ns) && eth.mcast && !is_chassis_resident("cr-DR-S3")), action=(reg9[[2]] = 1; next;)
+@@ -7636,7 +7639,9 @@ AT_CHECK([grep -e "ls_in_.*_fdb.*S1-vm1" S1flows | ovn_strip_lflows], [0], [dnl
+ ])
+ 
+ #Verify the flows for a non-default port type (localnet port)
+-AT_CHECK([grep -e "ls_in_.*_fdb.*S1-localnet" S1flows], [1], [])
++AT_CHECK([grep -e "ls_in_.*_fdb.*S1-localnet" S1flows | ovn_strip_lflows], [0], [dnl
++  table=??(ls_in_lookup_fdb   ), priority=50   , match=(inport == "S1-localnet"), action=(flags.localnet = 1; next;)
++])
+ 
+ OVN_CLEANUP_NORTHD
+ AT_CLEANUP
+@@ -9945,6 +9950,7 @@ AT_CHECK([ovn-nbctl --wait=sb sync])
+ # Check MAC learning flows with 'localnet_learn_fdb' default (false)
+ AT_CHECK([ovn-sbctl dump-flows ls0 | grep -e 'ls_in_\(put\|lookup\)_fdb' | ovn_strip_lflows], [0], [dnl
+   table=??(ls_in_lookup_fdb   ), priority=0    , match=(1), action=(next;)
++  table=??(ls_in_lookup_fdb   ), priority=50   , match=(inport == "ln_port"), action=(flags.localnet = 1; next;)
+   table=??(ls_in_put_fdb      ), priority=0    , match=(1), action=(next;)
+ ])
+ 
+@@ -9953,6 +9959,7 @@ AT_CHECK([ovn-nbctl --wait=sb lsp-set-options ln_port localnet_learn_fdb=true])
+ AT_CHECK([ovn-sbctl dump-flows ls0 | grep -e 'ls_in_\(put\|lookup\)_fdb' | ovn_strip_lflows], [0], [dnl
+   table=??(ls_in_lookup_fdb   ), priority=0    , match=(1), action=(next;)
+   table=??(ls_in_lookup_fdb   ), priority=100  , match=(inport == "ln_port"), action=(flags.localnet = 1; reg0[[11]] = lookup_fdb(inport, eth.src); next;)
++  table=??(ls_in_lookup_fdb   ), priority=50   , match=(inport == "ln_port"), action=(flags.localnet = 1; next;)
+   table=??(ls_in_put_fdb      ), priority=0    , match=(1), action=(next;)
+   table=??(ls_in_put_fdb      ), priority=100  , match=(inport == "ln_port" && reg0[[11]] == 0), action=(put_fdb(inport, eth.src); next;)
+ ])
+@@ -9961,6 +9968,7 @@ AT_CHECK([ovn-sbctl dump-flows ls0 | grep -e 'ls_in_\(put\|lookup\)_fdb' | ovn_s
+ AT_CHECK([ovn-nbctl --wait=sb lsp-set-options ln_port localnet_learn_fdb=false])
+ AT_CHECK([ovn-sbctl dump-flows ls0 | grep -e 'ls_in_\(put\|lookup\)_fdb' | ovn_strip_lflows], [0], [dnl
+   table=??(ls_in_lookup_fdb   ), priority=0    , match=(1), action=(next;)
++  table=??(ls_in_lookup_fdb   ), priority=50   , match=(inport == "ln_port"), action=(flags.localnet = 1; next;)
+   table=??(ls_in_put_fdb      ), priority=0    , match=(1), action=(next;)
+ ])
+ 
+@@ -10310,6 +10318,110 @@ OVN_CLEANUP_NORTHD
+ AT_CLEANUP
+ ])
+ 
++OVN_FOR_EACH_NORTHD_NO_HV([
++AT_SETUP([ARP/ND responder for localnet-sourced requests])
++ovn_start
++
++dnl Switch with localnet port.
++check ovn-nbctl ls-add ls1
++check ovn-nbctl lsp-add-localnet-port ls1 ln1 physnet1
++check ovn-nbctl lsp-add ls1 vm1 \
++    -- lsp-set-addresses vm1 "00:00:00:00:00:01 10.0.0.1 fd01::1"
++check ovn-nbctl lsp-add ls1 vm2 \
++    -- lsp-set-addresses vm2 "00:00:00:00:00:02 10.0.0.2 fd01::2"
++
++dnl Switch without localnet port.
++check ovn-nbctl ls-add ls2
++check ovn-nbctl --wait=sb lsp-add ls2 vm3 \
++    -- lsp-set-addresses vm3 "00:00:00:00:00:03 10.0.0.3 fd01::3"
++
++AS_BOX([FDB learning disabled])
++
++dnl ls1: ls_in_lookup_fdb should have priority 0 default +
++dnl priority 50 flags.localnet.
++AT_CHECK([ovn-sbctl dump-flows ls1 | grep -e 'ls_in_lookup_fdb' | ovn_strip_lflows], [0], [dnl
++  table=??(ls_in_lookup_fdb   ), priority=0    , match=(1), action=(next;)
++  table=??(ls_in_lookup_fdb   ), priority=50   , match=(inport == "ln1"), action=(flags.localnet = 1; next;)
++])
++
++dnl ls1: ls_in_arp_rsp should include flags.localnet condition for
++dnl priority 50 ARP/ND reply flows but NOT for priority 100 self-reply
++dnl flows (since those match on inport == VIF, flags.localnet is always 0).
++AT_CHECK([ovn-sbctl dump-flows ls1 | grep -e 'ls_in_arp_rsp' | ovn_strip_lflows], [0], [dnl
++  table=??(ls_in_arp_rsp      ), priority=0    , match=(1), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.1 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm1"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.2 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm2"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:1 && nd.target == fd01::1 && inport == "vm1"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:2 && nd.target == fd01::2 && inport == "vm2"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.1 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && ((flags.localnet == 1 && is_chassis_resident("vm1")) || flags.localnet == 0)), action=(eth.dst = eth.src; eth.src = 00:00:00:00:00:01; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 00:00:00:00:00:01; arp.tpa = arp.spa; arp.spa = 10.0.0.1; outport = inport; flags.loopback = 1; output;)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.2 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && ((flags.localnet == 1 && is_chassis_resident("vm2")) || flags.localnet == 0)), action=(eth.dst = eth.src; eth.src = 00:00:00:00:00:02; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 00:00:00:00:00:02; arp.tpa = arp.spa; arp.spa = 10.0.0.2; outport = inport; flags.loopback = 1; output;)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:1 && nd.target == fd01::1 && ((flags.localnet == 1 && is_chassis_resident("vm1")) || flags.localnet == 0)), action=(nd_na { eth.src = 00:00:00:00:00:01; ip6.src = fd01::1; nd.target = fd01::1; nd.tll = 00:00:00:00:00:01; outport = inport; flags.loopback = 1; output; };)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:2 && nd.target == fd01::2 && ((flags.localnet == 1 && is_chassis_resident("vm2")) || flags.localnet == 0)), action=(nd_na { eth.src = 00:00:00:00:00:02; ip6.src = fd01::2; nd.target = fd01::2; nd.tll = 00:00:00:00:00:02; outport = inport; flags.loopback = 1; output; };)
++])
++
++dnl ls2: ls_in_arp_rsp should NOT include flags.localnet condition.
++AT_CHECK([ovn-sbctl dump-flows ls2 | grep -e 'ls_in_arp_rsp' | ovn_strip_lflows], [0], [dnl
++  table=??(ls_in_arp_rsp      ), priority=0    , match=(1), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.3 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm3"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:3 && nd.target == fd01::3 && inport == "vm3"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.3 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff), action=(eth.dst = eth.src; eth.src = 00:00:00:00:00:03; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 00:00:00:00:00:03; arp.tpa = arp.spa; arp.spa = 10.0.0.3; outport = inport; flags.loopback = 1; output;)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:3 && nd.target == fd01::3), action=(nd_na { eth.src = 00:00:00:00:00:03; ip6.src = fd01::3; nd.target = fd01::3; nd.tll = 00:00:00:00:00:03; outport = inport; flags.loopback = 1; output; };)
++])
++
++dnl ls2: ls_in_lookup_fdb should only have priority 0 default,
++dnl no priority 50 flags.localnet.
++AT_CHECK([ovn-sbctl dump-flows ls2 | grep -e 'ls_in_lookup_fdb' | ovn_strip_lflows], [0], [dnl
++  table=??(ls_in_lookup_fdb   ), priority=0    , match=(1), action=(next;)
++])
++
++AS_BOX([Enable FDB learning on ln1])
++check ovn-nbctl --wait=sb lsp-set-options ln1 localnet_learn_fdb=true
++
++dnl ls1: ls_in_lookup_fdb should have priority 100 FDB +
++dnl priority 50 fallback.
++AT_CHECK([ovn-sbctl dump-flows ls1 | grep -e 'ls_in_lookup_fdb' | ovn_strip_lflows], [0], [dnl
++  table=??(ls_in_lookup_fdb   ), priority=0    , match=(1), action=(next;)
++  table=??(ls_in_lookup_fdb   ), priority=100  , match=(inport == "ln1"), action=(flags.localnet = 1; reg0[[11]] = lookup_fdb(inport, eth.src); next;)
++  table=??(ls_in_lookup_fdb   ), priority=50   , match=(inport == "ln1"), action=(flags.localnet = 1; next;)
++])
++
++dnl ls1: ls_in_arp_rsp should be unchanged.
++AT_CHECK([ovn-sbctl dump-flows ls1 | grep -e 'ls_in_arp_rsp' | ovn_strip_lflows], [0], [dnl
++  table=??(ls_in_arp_rsp      ), priority=0    , match=(1), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.1 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm1"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.2 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm2"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:1 && nd.target == fd01::1 && inport == "vm1"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:2 && nd.target == fd01::2 && inport == "vm2"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.1 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && ((flags.localnet == 1 && is_chassis_resident("vm1")) || flags.localnet == 0)), action=(eth.dst = eth.src; eth.src = 00:00:00:00:00:01; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 00:00:00:00:00:01; arp.tpa = arp.spa; arp.spa = 10.0.0.1; outport = inport; flags.loopback = 1; output;)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.2 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && ((flags.localnet == 1 && is_chassis_resident("vm2")) || flags.localnet == 0)), action=(eth.dst = eth.src; eth.src = 00:00:00:00:00:02; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 00:00:00:00:00:02; arp.tpa = arp.spa; arp.spa = 10.0.0.2; outport = inport; flags.loopback = 1; output;)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:1 && nd.target == fd01::1 && ((flags.localnet == 1 && is_chassis_resident("vm1")) || flags.localnet == 0)), action=(nd_na { eth.src = 00:00:00:00:00:01; ip6.src = fd01::1; nd.target = fd01::1; nd.tll = 00:00:00:00:00:01; outport = inport; flags.loopback = 1; output; };)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:2 && nd.target == fd01::2 && ((flags.localnet == 1 && is_chassis_resident("vm2")) || flags.localnet == 0)), action=(nd_na { eth.src = 00:00:00:00:00:02; ip6.src = fd01::2; nd.target = fd01::2; nd.tll = 00:00:00:00:00:02; outport = inport; flags.loopback = 1; output; };)
++])
++
++AS_BOX([Disable FDB learning])
++check ovn-nbctl --wait=sb lsp-set-options ln1 localnet_learn_fdb=false
++
++AT_CHECK([ovn-sbctl dump-flows ls1 | grep -e 'ls_in_lookup_fdb' | ovn_strip_lflows], [0], [dnl
++  table=??(ls_in_lookup_fdb   ), priority=0    , match=(1), action=(next;)
++  table=??(ls_in_lookup_fdb   ), priority=50   , match=(inport == "ln1"), action=(flags.localnet = 1; next;)
++])
++
++AT_CHECK([ovn-sbctl dump-flows ls1 | grep -e 'ls_in_arp_rsp' | ovn_strip_lflows], [0], [dnl
++  table=??(ls_in_arp_rsp      ), priority=0    , match=(1), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.1 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm1"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.2 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm2"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:1 && nd.target == fd01::1 && inport == "vm1"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:2 && nd.target == fd01::2 && inport == "vm2"), action=(next;)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.1 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && ((flags.localnet == 1 && is_chassis_resident("vm1")) || flags.localnet == 0)), action=(eth.dst = eth.src; eth.src = 00:00:00:00:00:01; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 00:00:00:00:00:01; arp.tpa = arp.spa; arp.spa = 10.0.0.1; outport = inport; flags.loopback = 1; output;)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.2 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && ((flags.localnet == 1 && is_chassis_resident("vm2")) || flags.localnet == 0)), action=(eth.dst = eth.src; eth.src = 00:00:00:00:00:02; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 00:00:00:00:00:02; arp.tpa = arp.spa; arp.spa = 10.0.0.2; outport = inport; flags.loopback = 1; output;)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:1 && nd.target == fd01::1 && ((flags.localnet == 1 && is_chassis_resident("vm1")) || flags.localnet == 0)), action=(nd_na { eth.src = 00:00:00:00:00:01; ip6.src = fd01::1; nd.target = fd01::1; nd.tll = 00:00:00:00:00:01; outport = inport; flags.loopback = 1; output; };)
++  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && ip6.dst == ff02::1:ff00:2 && nd.target == fd01::2 && ((flags.localnet == 1 && is_chassis_resident("vm2")) || flags.localnet == 0)), action=(nd_na { eth.src = 00:00:00:00:00:02; ip6.src = fd01::2; nd.target = fd01::2; nd.tll = 00:00:00:00:00:02; outport = inport; flags.loopback = 1; output; };)
++])
++
++OVN_CLEANUP_NORTHD
++AT_CLEANUP
++])
++
+ OVN_FOR_EACH_NORTHD_NO_HV([
+ AT_SETUP([Address set incremental processing])
+ ovn_start
+@@ -14294,7 +14406,7 @@ AT_CHECK([grep -Fe "172.168.0.110" -e "172.168.0.120" -e "10.0.0.3" -e "20.0.0.3
+   table=??(lr_out_undnat      ), priority=100  , match=(ip && ip4.src == 20.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_dnat;)
+ ])
+ 
+-AT_CHECK([grep -Fe "172.168.0.110" -e "172.168.0.120" -e "10.0.0.3" -e "20.0.0.3" -e "30:54:00:00:00:03"  -e "sw0-port1" publicflows | ovn_strip_lflows | grep -v "reg0.*22"], [0], [dnl
++AT_CHECK([grep -Fe "172.168.0.110" -e "172.168.0.120" -e "10.0.0.3" -e "20.0.0.3" -e "30:54:00:00:00:03"  -e "sw0-port1" publicflows | ovn_strip_lflows], [0], [dnl
+   table=??(ls_in_l2_lkup      ), priority=50   , match=(eth.dst == 30:54:00:00:00:03 && is_chassis_resident("sw0-port1")), action=(outport = "public-lr0"; output;)
+   table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == {00:00:00:00:ff:02, 30:54:00:00:00:03} && eth.dst == ff:ff:ff:ff:ff:ff && (arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = "_MC_flood_l2"; output;)
+   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 172.168.0.110), action=(clone {outport = "public-lr0"; output; }; outport = "_MC_flood_l2"; output;)
+@@ -16064,6 +16176,53 @@ OVN_CLEANUP_NORTHD
+ AT_CLEANUP
+ ])
+ 
++OVN_FOR_EACH_NORTHD_NO_HV([
++AT_SETUP([dynamic-routing - learned routes on unnumbered LRP])
++AT_KEYWORDS([dynamic-routing])
++ovn_start
++
++check ovn-nbctl lr-add lr0
++check ovn-nbctl set Logical_Router lr0 option:dynamic-routing=true
++check ovn-nbctl lrp-add lr0 lrp1 00:00:00:00:ff:01 10.0.0.1/24
++check ovn-nbctl lrp-add lr0 lrp2 00:00:00:00:ff:02
++check ovn-nbctl --wait=sb lrp-add lr0 lrp3 00:00:00:00:ff:03
++datapath=$(fetch_column datapath_binding _uuid external_ids:name=lr0)
++lrp2=$(fetch_column port_binding _uuid logical_port=lrp2)
++lrp3=$(fetch_column port_binding _uuid logical_port=lrp3)
++
++check_uuid ovn-sbctl create Learned_Route \
++    datapath=$datapath                    \
++    logical_port=$lrp2                    \
++    ip_prefix=42.42.42.42/32              \
++    nexthop=192.168.2.42
++check ovn-nbctl --wait=sb sync
++
++ovn-sbctl dump-flows lr0 > lr0flows
++AT_CHECK([grep -F "42.42.42.42" lr0flows | ovn_strip_lflows], [0], [dnl
++  table=??(lr_in_ip_routing   ), priority=258  , match=(reg7 == 0 && ip4.dst == 42.42.42.42/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.2.42; eth.src = 00:00:00:00:ff:02; outport = "lrp2"; flags.loopback = 1; reg9[[9]] = 1; next;)
++])
++
++check_uuid ovn-sbctl create Learned_Route \
++    datapath=$datapath                    \
++    logical_port=$lrp3                    \
++    ip_prefix=42.42.42.42/32              \
++    nexthop=192.168.3.42
++check ovn-nbctl --wait=sb sync
++
++ovn-sbctl dump-flows lr0 > lr0flows
++AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | grep -F "42.42.42.42" | ovn_strip_lflows], [0], [dnl
++  table=??(lr_in_ip_routing   ), priority=258  , match=(reg7 == 0 && ip4.dst == 42.42.42.42/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);)
++])
++
++AT_CHECK([grep "lr_in_ip_routing_ecmp" lr0flows | grep "42" | sed -e 's/reg8\[[16..31\]] == [[12]]/reg8\[[16..31\]] == ??/g' | ovn_strip_lflows], [0], [dnl
++  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == ??), action=(reg0 = 192.168.2.42; eth.src = 00:00:00:00:ff:02; outport = "lrp2"; reg9[[9]] = 1; next;)
++  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == ??), action=(reg0 = 192.168.3.42; eth.src = 00:00:00:00:ff:03; outport = "lrp3"; reg9[[9]] = 1; next;)
++])
++
++OVN_CLEANUP_NORTHD
++AT_CLEANUP
++])
++
+ OVN_FOR_EACH_NORTHD_NO_HV([
+ AT_SETUP([dynamic-routing - route learning cleanup - router recreation])
+ AT_KEYWORDS([dynamic-routing])
+@@ -18467,6 +18626,26 @@ OVN_CLEANUP_NORTHD
+ AT_CLEANUP
+ ])
+ 
++OVN_FOR_EACH_NORTHD_NO_HV([
++AT_SETUP([Datapath incremental processing index reuse])
++ovn_start
++
++check ovn-nbctl --wait=sb ls-add sw-old
++check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
++check ovn-nbctl --wait=sb ls-del sw-old -- ls-add sw-new
++check_engine_compute northd recompute
++CHECK_NO_CHANGE_AFTER_RECOMPUTE
++
++check ovn-nbctl --wait=sb lr-add lr-old
++check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
++check ovn-nbctl --wait=sb lr-del lr-old -- lr-add lr-new
++check_engine_compute northd recompute
++CHECK_NO_CHANGE_AFTER_RECOMPUTE
++
++OVN_CLEANUP_NORTHD
++AT_CLEANUP
++])
++
+ OVN_FOR_EACH_NORTHD_NO_HV([
+ AT_SETUP([Synced logical switch and router incremental procesesing])
+ ovn_start
+@@ -19345,6 +19524,7 @@ check ovn-nbctl ls-add $nfsw
+ for i in {1..4}; do
+     port=$nfsw-p$i
+     check ovn-nbctl lsp-add $nfsw $port
++    check ovn-nbctl --wait=sb sync
+     check ovn-sbctl set port_binding $port up=true
+     check ovn-nbctl lsp-add $sw child-$i $port 100
+ done
+@@ -19693,177 +19873,6 @@ OVN_CLEANUP_NORTHD
+ AT_CLEANUP
+ ])
+ 
+-OVN_FOR_EACH_NORTHD_NO_HV([
+-AT_SETUP([Logical Switch ARP filtering])
+-ovn_start
+-
+-check ovn-nbctl lr-add lr1
+-check ovn-nbctl lrp-add lr1 down_link f0:00:00:00:00:f1 192.168.1.1/24
+-
+-check ovn-nbctl ls-add ls1
+-check ovn-nbctl lsp-add ls1 up_link
+-check ovn-nbctl lsp-add ls1 down_vif1
+-check ovn-nbctl lsp-add ls1 down_vif2
+-check ovn-nbctl lsp-add ls1 down_ext
+-
+-check ovn-nbctl set Logical_Switch_Port up_link \
+-    type=router \
+-    options:router-port=down_link \
+-    addresses=router
+-
+-check ovn-nbctl lsp-set-addresses down_vif1 'f0:00:00:00:00:01 192.168.1.101'
+-check ovn-nbctl lsp-set-addresses down_vif2 'f0:00:00:00:00:02 192.168.1.102'
+-check ovn-nbctl lrp-set-gateway-chassis down_link hv1
+-check ovn-nbctl --wait=sb sync
+-AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_apply_port_sec | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), action=(drop;)
+-])
+-
+-# Check localnet port addings trigger ls-arp flow
+-check ovn-nbctl --wait=sb lsp-set-type down_ext localnet
+-AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_apply_port_sec | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=70   , match=(reg0[[22]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=75   , match=(reg0[[22]] == 1 && is_chassis_resident("cr-down_link")), action=(next;)
+-])
+-
+-# Check nat adding to dgr attached to logical switch trigger ls-arp flow.
+-check ovn-nbctl lr-nat-add lr1 dnat_and_snat 192.168.0.4 10.0.0.4
+-check ovn-nbctl lr-nat-add lr1 dnat_and_snat 192.168.0.3 10.0.0.3 down_vif1 f0:00:00:00:00:03
+-check ovn-nbctl --wait=sb sync
+-AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_apply_port_sec | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=70   , match=(reg0[[22]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=75   , match=(reg0[[22]] == 1 && is_chassis_resident("cr-down_link")), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=85   , match=(reg0[[22]] == 1 && arp.tpa == 192.168.0.3 && is_chassis_resident("down_vif1")), action=(next;)
+-])
+-
+-check ovn-nbctl --wait=sb lr-nat-del lr1 dnat_and_snat 192.168.0.3
+-AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_apply_port_sec | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=70   , match=(reg0[[22]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=75   , match=(reg0[[22]] == 1 && is_chassis_resident("cr-down_link")), action=(next;)
+-])
+-
+-# Check changing logical port type to l2gateway.
+-check ovn-nbctl --wait=sb lsp-set-type down_ext l2gateway
+-AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_apply_port_sec | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=70   , match=(reg0[[22]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=75   , match=(reg0[[22]] == 1 && is_chassis_resident("cr-down_link")), action=(next;)
+-])
+-
+-# Check changing logical port type to vif.
+-check ovn-nbctl --wait=sb lsp-set-type down_ext ''
+-AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_apply_port_sec | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), action=(drop;)
+-])
+-
+-# Check changing logical port type back to localnet.
+-check ovn-nbctl --wait=sb lsp-set-type down_ext localnet
+-AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_apply_port_sec | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=70   , match=(reg0[[22]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=75   , match=(reg0[[22]] == 1 && is_chassis_resident("cr-down_link")), action=(next;)
+-])
+-
+-# Check changing removing logical port.
+-check ovn-nbctl --wait=sb lsp-del down_ext
+-AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_apply_port_sec | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), action=(drop;)
+-])
+-
+-OVN_CLEANUP_NORTHD
+-AT_CLEANUP
+-])
+-
+-OVN_FOR_EACH_NORTHD_NO_HV([
+-AT_SETUP([Logical Switch ARP filtering - only distributed NATs])
+-ovn_start
+-
+-check ovn-sbctl chassis-add ch1 geneve 127.0.0.1
+-
+-check ovn-nbctl ls-add sw0
+-check ovn-nbctl lsp-add sw0 sw0-port1
+-check ovn-nbctl lsp-set-addresses sw0-port1 "50:54:00:00:00:03 10.0.0.3 10.0.0.5"
+-check ovn-nbctl lsp-add sw0 sw0-port2
+-check ovn-nbctl lsp-set-addresses sw0-port2 "50:54:00:00:00:04 10.0.0.4 10.0.0.6"
+-
+-# Create a logical router and attach both logical switches
+-check ovn-nbctl lr-add lr0
+-check ovn-nbctl lrp-add lr0 lr0-sw0 00:00:00:00:ff:01 10.0.0.1/24 1000::a/64
+-check ovn-nbctl lsp-add-router-port sw0 sw0-lr0 lr0-sw0
+-
+-
+-check ovn-nbctl ls-add public
+-check ovn-nbctl lrp-add lr0 lr0-public 00:00:20:20:12:13 172.168.0.100/24
+-check ovn-nbctl lsp-add public public-lr0 -- set Logical_Switch_Port public-lr0 \
+-    type=router options:router-port=lr0-public \
+-    -- lsp-set-addresses public-lr0 router
+-
+-check ovn-nbctl lrp-set-gateway-chassis lr0-public hv1
+-
+-# localnet port
+-check ovn-nbctl lsp-add-localnet-port public ln-public public
+-
+-check ovn-nbctl lr-nat-add lr0 dnat_and_snat 172.168.0.50 10.0.0.3 sw0-port1 f0:00:00:00:00:03
+-check ovn-nbctl lr-nat-add lr0 dnat_and_snat 172.168.0.55 10.0.0.5 sw0-port1 f0:00:00:00:00:03
+-check ovn-nbctl lr-nat-add lr0 dnat_and_snat 172.168.0.60 10.0.0.4 sw0-port2 f0:00:00:00:00:04
+-check ovn-nbctl lr-nat-add lr0 dnat_and_snat 172.168.0.66 10.0.0.6 sw0-port2 f0:00:00:00:00:04
+-
+-check ovn-nbctl --wait=sb sync
+-
+-AT_CHECK([ovn-sbctl lflow-list public | grep ls_in_apply_port_sec | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=70   , match=(reg0[[22]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=75   , match=(reg0[[22]] == 1 && is_chassis_resident("cr-lr0-public")), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=85   , match=(reg0[[22]] == 1 && arp.tpa == 172.168.0.50 && is_chassis_resident("sw0-port1")), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=85   , match=(reg0[[22]] == 1 && arp.tpa == 172.168.0.55 && is_chassis_resident("sw0-port1")), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=85   , match=(reg0[[22]] == 1 && arp.tpa == 172.168.0.60 && is_chassis_resident("sw0-port2")), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=85   , match=(reg0[[22]] == 1 && arp.tpa == 172.168.0.66 && is_chassis_resident("sw0-port2")), action=(next;)
+-])
+-
+-ovn-nbctl show
+-ovn-sbctl show
+-
+-check ovn-nbctl clear logical_router_port lr0-public gateway_chassis
+-check ovn-nbctl --wait=sb sync
+-
+-AT_CHECK([ovn-sbctl lflow-list public | grep ls_in_apply_port_sec | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), action=(drop;)
+-])
+-
+-check ovn-nbctl --wait=sb ha-chassis-group-add hagrp1
+-check ovn-nbctl --wait=sb ha-chassis-group-add-chassis hagrp1 ch1 30
+-hagrp1_uuid=$(fetch_column nb:ha_chassis_group _uuid name=hagrp1)
+-check ovn-nbctl --wait=sb set logical_router_port lr0-public ha_chassis_group=$hagrp1_uuid
+-check ovn-nbctl --wait=sb sync
+-
+-AT_CHECK([ovn-sbctl lflow-list public | grep ls_in_apply_port_sec | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=70   , match=(reg0[[22]] == 1), action=(drop;)
+-  table=??(ls_in_apply_port_sec), priority=75   , match=(reg0[[22]] == 1 && is_chassis_resident("cr-lr0-public")), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=85   , match=(reg0[[22]] == 1 && arp.tpa == 172.168.0.50 && is_chassis_resident("sw0-port1")), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=85   , match=(reg0[[22]] == 1 && arp.tpa == 172.168.0.55 && is_chassis_resident("sw0-port1")), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=85   , match=(reg0[[22]] == 1 && arp.tpa == 172.168.0.60 && is_chassis_resident("sw0-port2")), action=(next;)
+-  table=??(ls_in_apply_port_sec), priority=85   , match=(reg0[[22]] == 1 && arp.tpa == 172.168.0.66 && is_chassis_resident("sw0-port2")), action=(next;)
+-])
+-
+-OVN_CLEANUP_NORTHD
+-AT_CLEANUP
+-])
+-
+ OVN_FOR_EACH_NORTHD_NO_HV([
+ AT_SETUP([IGMP northd crash])
+ ovn_start
+@@ -20084,14 +20093,6 @@ AT_CHECK([cat lr1_lflows_before | grep lr_in_dnat | grep priority=120 | ovn_stri
+   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip6 && ip6.dst == 2000::1 && reg1[[16..23]] == 6 && reg1[[0..15]] == 80 && is_chassis_resident("cr-lr1-up")), action=(ct_lb_mark(backends=[[2001:db8:abcd:1::2]]:10882);)
+ ])
+ 
+-AT_CHECK([cat outside_lflows_before | grep ls_in_check_port_sec | grep priority=75 | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_check_port_sec), priority=75   , match=(arp.op == 1 && inport == "outside"), action=(reg0[[22]] = 1; next;)
+-])
+-
+-AT_CHECK([cat outside_lflows_before | grep ls_in_apply_port_sec | grep priority=75 | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_apply_port_sec), priority=75   , match=(reg0[[22]] == 1 && is_chassis_resident("cr-lr1-up")), action=(next;)
+-])
+-
+ check ovn-nbctl clear logical_router_port $lr1_up_uuid ha_chassis_group
+ check ovn-nbctl ha-chassis-group-del gateway
+ check ovn-nbctl ha-chassis-group-add gateway2
+@@ -20143,14 +20144,6 @@ AT_CHECK([cat lr1_lflows_after | grep lr_in_dnat | grep priority=120 | ovn_strip
+   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip6 && ip6.dst == 2000::1 && reg1[[16..23]] == 6 && reg1[[0..15]] == 80 && is_chassis_resident("cr-lr1-up")), action=(ct_lb_mark(backends=[[2001:db8:abcd:1::2]]:10882);)
+ ])
+ 
+-AT_CHECK([cat outside_lflows_after | grep ls_in_check_port_sec | grep priority=75 | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_check_port_sec), priority=75   , match=(arp.op == 1 && inport == "outside"), action=(reg0[[22]] = 1; next;)
+-])
+-
+-AT_CHECK([cat outside_lflows_after | grep ls_in_apply_port_sec | grep priority=75 | ovn_strip_lflows], [0], [dnl
+-  table=??(ls_in_apply_port_sec), priority=75   , match=(reg0[[22]] == 1), action=(next;)
+-])
+-
+ check ovn-nbctl --wait=sb set load_balancer lb1_ipv6 options:distributed=true
+ 
+ ovn-sbctl lflow-list outside > outside_lflows_after
+diff --git a/tests/ovn-performance.at b/tests/ovn-performance.at
+index 2bccbb06dd..114917832b 100644
+--- a/tests/ovn-performance.at
++++ b/tests/ovn-performance.at
+@@ -386,6 +386,13 @@ for i in 1 2; do
+     done
+ done
+ 
++# Check that adding a new port with a non-default type does not trigger
++# a physical_run.
++OVN_CONTROLLER_EXPECT_NO_HIT(
++    [hv1 hv2], [physical_run],
++    [ovn-nbctl --wait=hv lsp-add ls1 lsp-remote -- lsp-set-type lsp-remote remote]
++)
++
+ for i in 1 2; do
+     j=$((i%2 + 1))
+     as=as$i
+diff --git a/tests/ovn.at b/tests/ovn.at
+index e6cb5995af..5e0ed2635e 100644
+--- a/tests/ovn.at
++++ b/tests/ovn.at
+@@ -10184,6 +10184,199 @@ OVN_CLEANUP([hv1])
+ AT_CLEANUP
+ ])
+ 
++OVN_FOR_EACH_NORTHD([
++AT_SETUP([ARP/ND from localnet -- proxy reply on resident chassis only])
++AT_SKIP_IF([test $HAVE_SCAPY = no])
++ovn_start
++
++dnl Create logical switch with localnet port.
++check ovn-nbctl ls-add ls1
++check ovn-nbctl lsp-add-localnet-port ls1 ln1 physnet1
++check ovn-nbctl lsp-add ls1 vm1 \
++    -- lsp-set-addresses vm1 "f0:00:00:00:00:01 10.0.0.1 fd01::1"
++check ovn-nbctl lsp-add ls1 vm2 \
++    -- lsp-set-addresses vm2 "f0:00:00:00:00:02 10.0.0.2 fd01::2"
++
++dnl Two hypervisors with bridge-mappings.
++net_add n1
++
++sim_add hv1
++as hv1
++ovs-vsctl \
++    -- add-br br-phys \
++    -- add-br br-eth0
++ovn_attach n1 br-phys 192.168.0.1
++check ovs-vsctl set Open_vSwitch . external-ids:ovn-bridge-mappings=physnet1:br-eth0
++check ovs-vsctl add-port br-eth0 snoopvif1 \
++    -- set Interface snoopvif1 options:tx_pcap=hv1/snoopvif-tx.pcap \
++                                options:rxq_pcap=hv1/snoopvif-rx.pcap
++check ovs-vsctl add-port br-int vm1 \
++    -- set Interface vm1 external-ids:iface-id=vm1 \
++                         options:tx_pcap=hv1/vm1-tx.pcap \
++                         options:rxq_pcap=hv1/vm1-rx.pcap
++
++sim_add hv2
++as hv2
++ovs-vsctl \
++    -- add-br br-phys \
++    -- add-br br-eth0
++ovn_attach n1 br-phys 192.168.0.2
++check ovs-vsctl set Open_vSwitch . external-ids:ovn-bridge-mappings=physnet1:br-eth0
++check ovs-vsctl add-port br-eth0 snoopvif2 \
++    -- set Interface snoopvif2 options:tx_pcap=hv2/snoopvif-tx.pcap \
++                                options:rxq_pcap=hv2/snoopvif-rx.pcap
++check ovs-vsctl add-port br-int vm2 \
++    -- set Interface vm2 external-ids:iface-id=vm2 \
++                         options:tx_pcap=hv2/vm2-tx.pcap \
++                         options:rxq_pcap=hv2/vm2-rx.pcap
++
++wait_for_ports_up vm1 vm2
++OVN_POPULATE_ARP
++check ovn-nbctl --wait=hv sync
++
++dnl Helper: construct ARP request.
++build_arp_request() {
++    local sha=$1 spa=$2 tpa=$3
++    fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
++             ARP(hwsrc='${sha}', hwdst='ff:ff:ff:ff:ff:ff', \
++                 psrc='${spa}', pdst='${tpa}')"
++}
++
++dnl Helper: construct expected ARP reply.
++build_arp_reply() {
++    local req_sha=$1 req_spa=$2 reply_sha=$3 reply_spa=$4
++    fmt_pkt "Ether(dst='${req_sha}', src='${reply_sha}')/ \
++             ARP(op=2, hwsrc='${reply_sha}', hwdst='${req_sha}', \
++                 psrc='${reply_spa}', pdst='${req_spa}')"
++}
++
++dnl Helper: construct ND solicitation.
++build_nd_ns() {
++    local sha=$1 spa=$2 tpa=$3 sol_mcast=$4
++    fmt_pkt "Ether(dst='33:33:ff:00:00:0${tpa##*:}', src='${sha}')/ \
++             IPv6(src='${spa}', dst='${sol_mcast}')/ \
++             ICMPv6ND_NS(tgt='${tpa}')/ \
++             ICMPv6NDOptSrcLLAddr(lladdr='${sha}')"
++}
++
++dnl Helper: construct expected ND advertisement.
++build_nd_na() {
++    local req_sha=$1 req_spa=$2 reply_sha=$3 reply_tgt=$4
++    fmt_pkt "Ether(dst='${req_sha}', src='${reply_sha}')/ \
++             IPv6(src='${reply_tgt}', dst='${req_spa}')/ \
++             ICMPv6ND_NA(tgt='${reply_tgt}', R=0, S=1, O=1)/ \
++             ICMPv6NDOptDstLLAddr(lladdr='${reply_sha}')"
++}
++
++test_arp_nd_localnet() {
++    AS_BOX([ARP from localnet on hv1 for vm1 - expect reply])
++    as hv1 reset_pcap_file snoopvif1 hv1/snoopvif
++    as hv2 reset_pcap_file snoopvif2 hv2/snoopvif
++    as hv1 reset_pcap_file vm1 hv1/vm1
++    as hv2 reset_pcap_file vm2 hv2/vm2
++
++    dnl ARP request from br-eth0 on hv1 for vm1 (10.0.0.1).
++    dnl vm1 is resident on hv1, so hv1 should reply.
++    local arp_req=$(build_arp_request "f0:00:00:00:00:99" "10.0.0.99" "10.0.0.1")
++    as hv1 ovs-appctl netdev-dummy/receive snoopvif1 $arp_req
++    local arp_rep=$(build_arp_reply "f0:00:00:00:00:99" "10.0.0.99" \
++                                    "f0:00:00:00:00:01" "10.0.0.1")
++    echo $arp_rep > expected_arp_reply
++    OVN_CHECK_PACKETS_CONTAIN([hv1/snoopvif-tx.pcap], [expected_arp_reply])
++
++    AS_BOX([ARP from localnet on hv2 for vm1 - expect no reply])
++    as hv2 reset_pcap_file snoopvif2 hv2/snoopvif
++
++    dnl ARP request from br-eth0 on hv2 for vm1 (10.0.0.1).
++    dnl vm1 is NOT resident on hv2, so hv2 should NOT reply.
++    dnl To avoid relying on sleep, we also send an ARP request for vm2
++    dnl (which IS resident on hv2) and wait for that reply.  This proves
++    dnl the pipeline is running and any reply for vm1 would have appeared.
++    as hv2 ovs-appctl netdev-dummy/receive snoopvif2 $arp_req
++
++    local arp_req_vm2=$(build_arp_request "f0:00:00:00:00:99" "10.0.0.99" "10.0.0.2")
++    as hv2 ovs-appctl netdev-dummy/receive snoopvif2 $arp_req_vm2
++    local arp_rep_vm2=$(build_arp_reply "f0:00:00:00:00:99" "10.0.0.99" \
++                                        "f0:00:00:00:00:02" "10.0.0.2")
++    echo $arp_rep_vm2 > expected_arp_vm2
++    OVN_CHECK_PACKETS_CONTAIN([hv2/snoopvif-tx.pcap], [expected_arp_vm2])
++
++    dnl Now verify that no ARP reply for vm1 was generated on hv2.
++    AT_CHECK([$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv2/snoopvif-tx.pcap | \
++              grep -c "$arp_rep"], [1], [dnl
++0
++])
++
++    AS_BOX([ARP from vm2 VIF for vm1 - expect proxy reply])
++    as hv2 reset_pcap_file vm2 hv2/vm2
++    local arp_req2=$(build_arp_request "f0:00:00:00:00:02" "10.0.0.2" "10.0.0.1")
++    as hv2 ovs-appctl netdev-dummy/receive vm2 $arp_req2
++    local arp_rep2=$(build_arp_reply "f0:00:00:00:00:02" "10.0.0.2" \
++                                     "f0:00:00:00:00:01" "10.0.0.1")
++    echo $arp_rep2 > expected_arp_proxy
++    OVN_CHECK_PACKETS_CONTAIN([hv2/vm2-tx.pcap], [expected_arp_proxy])
++
++    AS_BOX([ND from localnet on hv1 for vm1 - expect reply])
++    as hv1 reset_pcap_file snoopvif1 hv1/snoopvif
++    as hv2 reset_pcap_file snoopvif2 hv2/snoopvif
++
++    dnl ND solicitation from br-eth0 on hv1 for vm1 IPv6 (fd01::1).
++    dnl vm1 is resident on hv1, so hv1 should reply.
++    local nd_ns=$(build_nd_ns "f0:00:00:00:00:99" "fd01::99" "fd01::1" "ff02::1:ff00:1")
++    as hv1 ovs-appctl netdev-dummy/receive snoopvif1 $nd_ns
++    local nd_na=$(build_nd_na "f0:00:00:00:00:99" "fd01::99" \
++                               "f0:00:00:00:00:01" "fd01::1")
++    echo $nd_na > expected_nd_reply
++    OVN_CHECK_PACKETS_CONTAIN([hv1/snoopvif-tx.pcap], [expected_nd_reply])
++
++    AS_BOX([ND from localnet on hv2 for vm1 - expect no reply])
++    as hv2 reset_pcap_file snoopvif2 hv2/snoopvif
++
++    dnl ND solicitation from br-eth0 on hv2 for vm1 IPv6 (fd01::1).
++    dnl vm1 is NOT resident on hv2, so hv2 should NOT reply.
++    dnl Same technique: send ND for vm2 (resident) and wait for that reply.
++    as hv2 ovs-appctl netdev-dummy/receive snoopvif2 $nd_ns
++
++    local nd_ns_vm2=$(build_nd_ns "f0:00:00:00:00:99" "fd01::99" "fd01::2" "ff02::1:ff00:2")
++    as hv2 ovs-appctl netdev-dummy/receive snoopvif2 $nd_ns_vm2
++    local nd_na_vm2=$(build_nd_na "f0:00:00:00:00:99" "fd01::99" \
++                                   "f0:00:00:00:00:02" "fd01::2")
++    echo $nd_na_vm2 > expected_nd_vm2
++    OVN_CHECK_PACKETS_CONTAIN([hv2/snoopvif-tx.pcap], [expected_nd_vm2])
++
++    dnl Now verify that no ND advertisement for vm1 was generated on hv2.
++    AT_CHECK([$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv2/snoopvif-tx.pcap | \
++              grep -c "$nd_na"], [1], [dnl
++0
++])
++
++    AS_BOX([ND from vm2 VIF for vm1 - expect proxy reply])
++    as hv2 reset_pcap_file vm2 hv2/vm2
++    local nd_ns2=$(build_nd_ns "f0:00:00:00:00:02" "fd01::2" "fd01::1" "ff02::1:ff00:1")
++    as hv2 ovs-appctl netdev-dummy/receive vm2 $nd_ns2
++    local nd_na2=$(build_nd_na "f0:00:00:00:00:02" "fd01::2" \
++                                "f0:00:00:00:00:01" "fd01::1")
++    echo $nd_na2 > expected_nd_proxy
++    OVN_CHECK_PACKETS_CONTAIN([hv2/vm2-tx.pcap], [expected_nd_proxy])
++}
++
++AS_BOX([FDB learning disabled])
++test_arp_nd_localnet
++
++AS_BOX([FDB learning enabled])
++dnl Use 'set' instead of 'lsp-set-options' to preserve network_name.
++check ovn-nbctl --wait=hv set Logical_Switch_Port ln1 \
++    options:localnet_learn_fdb=true
++test_arp_nd_localnet
++
++OVN_CLEANUP([hv1
++/left allocated/d
++], [hv2
++/left allocated/d
++])
++AT_CLEANUP
++])
++
+ OVN_FOR_EACH_NORTHD([
+ AT_SETUP([send reverse arp for router without ipv4 address])
+ ovn_start
+@@ -14170,8 +14363,8 @@ AT_CHECK([
+     test 0 -eq $pkts
+ ])
+ 
+-spa=$(ip_to_hex 10 0 0 1)
+-tpa=$(ip_to_hex 10 0 0 100)
++spa=10.0.0.1
++tpa=10.0.0.100
+ send_garp hv1 vif1 1 "00:00:00:00:00:01" "ff:ff:ff:ff:ff:ff" $spa $tpa
+ 
+ dnl traffic from localport should not be sent to localnet
+@@ -31350,6 +31543,13 @@ check ovn-nbctl lrp-set-gateway-chassis lr0-public hv1 20
+ 
+ # Create NAT entries for the ports
+ 
++# sw0-port1
++check ovn-nbctl lr-nat-add lr0 dnat_and_snat 172.16.0.110 10.0.0.3 sw0-port1 30:54:00:00:00:03
++check ovn-nbctl lr-nat-add lr0 dnat_and_snat 3000::c 1000::3 sw0-port1 40:54:00:00:00:03
++# sw1-port1
++check ovn-nbctl lr-nat-add lr0 dnat_and_snat 172.16.0.120 20.0.0.3 sw1-port1 30:54:00:00:00:04
++check ovn-nbctl lr-nat-add lr0 dnat_and_snat 3000::d 2000::3 sw1-port1 40:54:00:00:00:04
++
+ # Add snat entriess
+ check ovn-nbctl lr-nat-add lr0 snat 172.16.0.100 10.0.0.0/24
+ check ovn-nbctl lr-nat-add lr0 snat 172.16.0.101 10.0.0.10
+@@ -31458,19 +31658,6 @@ test_arp_response 000020201213 $(ip_to_hex 172 16 0 102) hv1 hv2 hv3
+ test_arp_response 000030303233 $(ip_to_hex 172 16 0 200) hv3 hv1 hv2
+ test_arp_response 000030303233 $(ip_to_hex 172 16 0 201) hv3 hv1 hv2
+ 
+-# Add some extra dnat_and_snat, that should generate extra flows for external ARPs.
+-# sw0-port1
+-check ovn-nbctl lr-nat-add lr0 dnat_and_snat 172.16.0.110 10.0.0.3 sw0-port1 30:54:00:00:00:03
+-check ovn-nbctl lr-nat-add lr0 dnat_and_snat 3000::c 1000::3 sw0-port1 40:54:00:00:00:03
+-# sw1-port1
+-check ovn-nbctl lr-nat-add lr0 dnat_and_snat 172.16.0.120 20.0.0.3 sw1-port1 30:54:00:00:00:04
+-check ovn-nbctl lr-nat-add lr0 dnat_and_snat 3000::d 2000::3 sw1-port1 40:54:00:00:00:04
+-check ovn-nbctl --wait=hv sync
+-
+-# Send ARP request for the IP which belongs to gw_router
+-test_arp_response 000030303233 $(ip_to_hex 172 16 0 200) hv3 hv1 hv2
+-test_arp_response 000030303233 $(ip_to_hex 172 16 0 201) hv3 hv1 hv2
+-
+ # Make hv3 claim the cr-lr0-public
+ check ovn-nbctl lrp-set-gateway-chassis lr0-public hv1 20
+ check ovn-nbctl lrp-set-gateway-chassis lr0-public hv2 30
+@@ -36535,15 +36722,6 @@ AT_CHECK([ovn-nbctl --wait=hv sync])
+ OVS_WAIT_UNTIL([grep pinctrl hv1/ovn-controller.log | grep -c connected])
+ OVS_WAIT_UNTIL([grep pinctrl hv2/ovn-controller.log | grep -c connected])
+ 
+-send_udp() {
+-    hv=$1
+-    dev=$2
+-    byte=$3
+-
+-    packet=$(fmt_pkt "Ether(dst='00:00:00:00:10:00', src='00:00:00:00:10:${byte}')/ \
+-                      IP(dst='192.168.20.${byte}', src='192.168.10.${byte}')/UDP()")
+-    as $hv ovs-appctl netdev-dummy/receive $dev $packet
+-}
+ # Check if the option is not present by default
+ AT_CHECK([fetch_column nb:logical_router options name="gw-1" | grep -q mac_binding_age_threshold], [1])
+ AT_CHECK([fetch_column nb:logical_router options name="gw-2" | grep -q mac_binding_age_threshold], [1])
+@@ -36577,8 +36755,8 @@ $sorted_table
+ timestamp=$(fetch_column mac_binding timestamp ip="192.168.10.20")
+ binding_uuid=$(fetch_column mac_binding _uuid ip="192.168.10.20")
+ 
+-send_udp hv1 ext1 10
+-send_udp hv2 ext2 20
++send_udp hv1 ext1 00:00:00:00:10:00 00:00:00:00:10:10 192.168.20.10 192.168.10.10
++send_udp hv2 ext2 00:00:00:00:10:00 00:00:00:00:10:20 192.168.20.20 192.168.10.20
+ 
+ OVS_WAIT_UNTIL([as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_MAC_CACHE_USE | grep "192.168.10.10" | grep -q "n_packets=1"])
+ OVS_WAIT_UNTIL([as hv2 ovs-ofctl dump-flows br-int table=OFTABLE_MAC_CACHE_USE | grep "192.168.10.20" | grep -q "n_packets=1"])
+@@ -36589,9 +36767,9 @@ AT_CHECK([fetch_column nb:logical_router options | grep -q mac_binding_age_thres
+ AT_CHECK([ovn-nbctl --wait=sb sync])
+ 
+ # Wait send few packets for "192.168.10.20" to indicate that it is still in use
+-send_udp hv2 ext2 20
++send_udp hv2 ext2 00:00:00:00:10:00 00:00:00:00:10:20 192.168.20.20 192.168.10.20
+ sleep 1
+-send_udp hv2 ext2 20
++send_udp hv2 ext2 00:00:00:00:10:00 00:00:00:00:10:20 192.168.20.20 192.168.10.20
+ 
+ # Set the timeout for OVS_WAIT* functions to 10 seconds
+ OVS_CTL_TIMEOUT=10
+@@ -36869,18 +37047,10 @@ sleep 2
+ send_garp hv1 vif1 2 00:00:00:00:10:10 ff:ff:ff:ff:ff:ff 192.168.10.10 192.168.10.10
+ wait_row_count mac_binding 1 ip="192.168.10.10" logical_port="lr-ls1"
+ 
+-send_udp() {
+-    local hv=$1 dev=$2 byte=$3
+-
+-    packet=$(fmt_pkt "Ether(dst='00:00:00:00:10:00', src='00:00:00:00:10:${byte}')/ \
+-                      IP(dst='192.168.20.${byte}', src='192.168.10.${byte}')/UDP()")
+-    check as $hv ovs-appctl netdev-dummy/receive $dev $packet
+-}
+-
+ uuid=$(fetch_column mac_binding _uuid ip="192.168.10.10" logical_port="lr-ls1")
+ for i in $(seq 12); do
+     # Keep one entry alive by sending traffic that uses it.
+-    send_udp hv1 vif1 10
++    send_udp hv1 vif1 00:00:00:00:10:00 00:00:00:00:10:10 192.168.20.10 192.168.10.10
+     sleep 1
+     # The entry must not expire.
+     check_row_count mac_binding 1 ip="192.168.10.10" logical_port="lr-ls1"
+@@ -36963,38 +37133,6 @@ AT_SETUP([MAC binding aging - probing])
+ AT_SKIP_IF([test $HAVE_SCAPY = no])
+ ovn_start
+ 
+-send_udp() {
+-    local hv=$1 dev=$2 hdst=$3 hsrc=$4 idst=$5 isrc=$6
+-    local packet=$(fmt_pkt "Ether(dst='${hdst}', src='${hsrc}')/ \
+-                            IP(dst='${idst}', src='${isrc}')/UDP()")
+-    as $hv ovs-appctl netdev-dummy/receive $dev $packet
+-}
+-
+-send_udp6() {
+-    local hv=$1 dev=$2 hdst=$3 hsrc=$4 idst=$5 isrc=$6
+-    local packet=$(fmt_pkt "Ether(dst='${hdst}', src='${hsrc}')/ \
+-                            IPv6(dst='${idst}', src='${isrc}')/UDP()")
+-    as $hv ovs-appctl netdev-dummy/receive $dev $packet
+-}
+-
+-dump_arp() {
+-    local op=$1 eth_src=$2 eth_dst=$3 spa=$4 tpa=$5 hwdst=$6
+-
+-    local packet=$(fmt_pkt "Ether(dst='${eth_dst}', src='${eth_src}')/ \
+-                      ARP(op=$op, hwsrc='${eth_src}', hwdst='${hwdst}', \
+-                      psrc='${spa}', pdst='${tpa}')")
+-    echo $packet
+-}
+-
+-dump_ns() {
+-    local hdst=$1 hsrc=$2 idst=$3 isrc=$4 tgt=$5
+-    local packet=$(fmt_pkt "Ether(dst='${hdst}', src='${hsrc}')/ \
+-                            IPv6(dst='${idst}', src='${isrc}')/ \
+-                            ICMPv6ND_NS(tgt='${tgt}')/
+-                            ICMPv6NDOptSrcLLAddr(lladdr='${hsrc}')")
+-    echo $packet
+-}
+-
+ aging_th=10
+ net_add n1
+ sim_add hv1
+@@ -37112,6 +37250,123 @@ OVN_CLEANUP([hv1])
+ AT_CLEANUP
+ ])
+ 
++OVN_FOR_EACH_NORTHD([
++AT_SETUP([MAC binding aging - probing multi-subnet source IP])
++AT_SKIP_IF([test $HAVE_SCAPY = no])
++ovn_start
++
++aging_th=10
++net_add n1
++sim_add hv1
++as hv1
++check ovs-vsctl add-br br-phys
++ovn_attach n1 br-phys 192.168.0.1
++ovn-appctl -t ovn-controller vlog/set mac_cache:file:dbg pinctrl:file:dbg
++
++check ovn-nbctl                                                             \
++    -- ls-add ls1                                                           \
++    -- lr-add lr                                                            \
++    -- set logical_router lr options:mac_binding_age_threshold=$aging_th    \
++    -- lrp-add lr lr-ls1 00:00:00:00:10:00 10.10.10.1/24 42.42.42.1/24      \
++       fd11::1/64 fd12::1/64                                                \
++    -- lsp-add-router-port ls1 ls1-lr lr-ls1                                \
++    -- lsp-add ls1 vif1                                                     \
++    -- lsp-set-addresses vif1 "unknown"
++
++check ovs-vsctl                                                             \
++    -- add-port br-int vif1                                                 \
++    -- set interface vif1 external-ids:iface-id=vif1                        \
++    options:tx_pcap=hv1/vif1-tx.pcap options:rxq_pcap=hv1/vif1-rx.pcap
++
++OVN_POPULATE_ARP
++wait_for_ports_up
++check ovn-nbctl --wait=hv sync
++
++# Wait for pinctrl thread to be connected.
++OVS_WAIT_UNTIL([grep pinctrl hv1/ovn-controller.log | grep -q connected])
++
++# Create MAC bindings in both IPv4 subnets.
++send_garp hv1 vif1 2 00:00:00:00:10:1a ff:ff:ff:ff:ff:ff 10.10.10.100 10.10.10.100
++wait_row_count mac_binding 1 ip="10.10.10.100" logical_port="lr-ls1"
++
++send_garp hv1 vif1 2 00:00:00:00:10:1b ff:ff:ff:ff:ff:ff 42.42.42.253 42.42.42.253
++wait_row_count mac_binding 1 ip="42.42.42.253" logical_port="lr-ls1"
++
++# Create MAC bindings in both IPv6 subnets.
++send_na hv1 vif1 00:00:00:00:10:1a 00:00:00:00:10:00 fd11::64 fd11::1
++wait_row_count mac_binding 1 ip=\"fd11::64\" logical_port=\"lr-ls1\"
++
++send_na hv1 vif1 00:00:00:00:10:1b 00:00:00:00:10:00 fd12::64 fd12::1
++wait_row_count mac_binding 1 ip=\"fd12::64\" logical_port=\"lr-ls1\"
++
++# Record UUIDs for all MAC bindings.
++uuid_v4_1=$(fetch_column Mac_Binding _uuid ip=10.10.10.100)
++uuid_v4_2=$(fetch_column Mac_Binding _uuid ip=42.42.42.253)
++uuid_v6_1=$(fetch_column Mac_Binding _uuid ip=\"fd11::64\")
++uuid_v6_2=$(fetch_column Mac_Binding _uuid ip=\"fd12::64\")
++
++# Send IPv4 and IPv6 UDP traffic to refresh entries in OFTABLE_MAC_BINDING.
++# Use different src MACs than the ones from GARP/NA to avoid resetting
++# OFTABLE_MAC_CACHE_USE idle_age (which would keep the timestamp fresh
++# and suppress probing via the cooldown mechanism).
++send_udp hv1 vif1 00:00:00:00:10:00 00:00:00:00:10:2a 42.42.42.253 10.10.10.100
++send_udp hv1 vif1 00:00:00:00:10:00 00:00:00:00:10:2b 10.10.10.100 42.42.42.253
++send_udp6 hv1 vif1 00:00:00:00:10:00 00:00:00:00:10:2a fd12::64 fd11::64
++send_udp6 hv1 vif1 00:00:00:00:10:00 00:00:00:00:10:2b fd11::64 fd12::64
++
++OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_BINDING | \
++                  sed 's/reg15=0x.,metadata=0x./reg15=<cleared>,metadata=<cleared>/g' | \
++                  grep -q "reg0=0xa0a0a64,reg15=<cleared>,metadata=<cleared> actions=mod_dl_dst:00:00:00:00:10:1a"])
++OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int table=OFTABLE_MAC_BINDING | \
++                  sed 's/reg15=0x.,metadata=0x./reg15=<cleared>,metadata=<cleared>/g' | \
++                  grep -q "reg0=0x2a2a2afd,reg15=<cleared>,metadata=<cleared> actions=mod_dl_dst:00:00:00:00:10:1b"])
++
++# Wait until all entries in OFTABLE_MAC_CACHE_USE are stale.
++OVS_WAIT_UNTIL([test $(ovs-ofctl dump-flows br-int table=OFTABLE_MAC_CACHE_USE | \
++                       awk '/nw_src=10.10.10.100/{print substr($6,10,1)}') -ge $((aging_th/2))])
++OVS_WAIT_UNTIL([test $(ovs-ofctl dump-flows br-int table=OFTABLE_MAC_CACHE_USE | \
++                       awk '/nw_src=42.42.42.253/{print substr($6,10,1)}') -ge $((aging_th/2))])
++OVS_WAIT_UNTIL([test $(ovs-ofctl dump-flows br-int table=OFTABLE_MAC_CACHE_USE | \
++                       awk '/ipv6_src=fd11::64/{print substr($6,10,1)}') -ge $((aging_th/2))])
++OVS_WAIT_UNTIL([test $(ovs-ofctl dump-flows br-int table=OFTABLE_MAC_CACHE_USE | \
++                       awk '/ipv6_src=fd12::64/{print substr($6,10,1)}') -ge $((aging_th/2))])
++
++# Send traffic to trigger probing for all entries.
++# Again use different src MACs to only hit OFTABLE_MAC_BINDING (not MAC_CACHE_USE).
++send_udp hv1 vif1 00:00:00:00:10:00 00:00:00:00:10:2a 42.42.42.253 10.10.10.100
++send_udp hv1 vif1 00:00:00:00:10:00 00:00:00:00:10:2b 10.10.10.100 42.42.42.253
++send_udp6 hv1 vif1 00:00:00:00:10:00 00:00:00:00:10:2a fd12::64 fd11::64
++send_udp6 hv1 vif1 00:00:00:00:10:00 00:00:00:00:10:2b fd11::64 fd12::64
++
++# Verify ARP probes use the correct source IPs from matching subnets.
++# ARP for 10.10.10.100 must use source IP 10.10.10.1 (first subnet).
++dump_arp 1 00:00:00:00:10:00 ff:ff:ff:ff:ff:ff 10.10.10.1 10.10.10.100 00:00:00:00:00:00 > expected
++# ARP for 42.42.42.253 must use source IP 42.42.42.1 (second subnet).
++dump_arp 1 00:00:00:00:10:00 ff:ff:ff:ff:ff:ff 42.42.42.1 42.42.42.253 00:00:00:00:00:00 >> expected
++OVN_CHECK_PACKETS_CONTAIN([hv1/vif1-tx.pcap], [expected])
++
++# Verify NS probes use the correct source IPs from matching subnets.
++# NS for fd11::64 must use source IP fd11::1 (first IPv6 subnet).
++dump_ns 33:33:ff:00:00:64 00:00:00:00:10:00 ff02::1:ff00:64 fd11::1 fd11::64 > expected_v6
++# NS for fd12::64 must use source IP fd12::1 (second IPv6 subnet).
++dump_ns 33:33:ff:00:00:64 00:00:00:00:10:00 ff02::1:ff00:64 fd12::1 fd12::64 >> expected_v6
++OVN_CHECK_PACKETS_CONTAIN([hv1/vif1-tx.pcap], [expected_v6])
++
++# Send ARP/NA replies and check MAC_Binding UUIDs remain consistent.
++send_garp hv1 vif1 2 00:00:00:00:10:1a 00:00:00:00:10:00 10.10.10.100 10.10.10.1
++send_garp hv1 vif1 2 00:00:00:00:10:1b 00:00:00:00:10:00 42.42.42.253 42.42.42.1
++send_na hv1 vif1 00:00:00:00:10:1a 00:00:00:00:10:00 fd11::64 fd11::1
++send_na hv1 vif1 00:00:00:00:10:1b 00:00:00:00:10:00 fd12::64 fd12::1
++
++check_column "$uuid_v4_1" Mac_Binding _uuid ip=10.10.10.100
++check_column "$uuid_v4_2" Mac_Binding _uuid ip=42.42.42.253
++check_column "$uuid_v6_1" Mac_Binding _uuid ip=\"fd11::64\"
++check_column "$uuid_v6_2" Mac_Binding _uuid ip=\"fd12::64\"
++
++OVN_CLEANUP([hv1])
++AT_CLEANUP
++])
++
+ OVN_FOR_EACH_NORTHD([
+ AT_SETUP([MAC binding aging - probing distributed GW router])
+ AT_SKIP_IF([test $HAVE_SCAPY = no])
+@@ -44599,160 +44854,6 @@ OVN_CLEANUP([hv1])
+ AT_CLEANUP
+ ])
+ 
+-OVN_FOR_EACH_NORTHD([
+-AT_SETUP([GARP delivery: gw and external ports])
+-AT_SKIP_IF([test $HAVE_SCAPY = no])
+-ovn_start
+-
+-# Configure initial environment
+-# LR1: down_link <-> LS1: up_link
+-# set lr_down: gateway port (chassis redirect) bound to hv1
+-# LS1: down_vif1 - vif port bound to hv1
+-#      down_vif2 - vif port bound to hv2
+-#      down_ext - outer (port will be iterated as localnet, l2gateway)
+-#
+-# Test: send GARP request from virtual ports (down_vif1, down_vif2)
+-#       ensure mac_binding is always updated.
+-#       (Fixing the issue: mac_binding is only updated for packets came from
+-#        down_link's resident chassis)
+-#       send GARP request from from localnet.
+-#       ensure mac_binding is updated only if localnet bound to same hv as l3dgw
+-
+-check ovn-nbctl lr-add lr1
+-check ovn-nbctl lrp-add lr1 down_link f0:00:00:00:00:f1 192.168.1.1/24
+-
+-check ovn-nbctl ls-add ls1
+-check ovn-nbctl lsp-add ls1 up_link
+-check ovn-nbctl lsp-add ls1 down_vif1
+-check ovn-nbctl lsp-add ls1 down_vif2
+-check ovn-nbctl lsp-add ls1 down_ext
+-
+-check ovn-nbctl set Logical_Switch_Port up_link \
+-    type=router \
+-    options:router-port=down_link \
+-    addresses=router
+-
+-check ovn-nbctl lsp-set-addresses down_vif1 'f0:00:00:00:00:01 192.168.1.101'
+-check ovn-nbctl lsp-set-addresses down_vif2 'f0:00:00:00:00:02 192.168.1.102'
+-
+-check ovn-nbctl lsp-set-type down_ext localnet
+-check ovn-nbctl lsp-set-options down_ext network_name=physnet1
+-check ovn-nbctl lrp-set-gateway-chassis down_link hv1
+-
+-net_add n1
+-
+-# Create hypervisor hv1 connected to n1
+-sim_add hv1
+-as hv1
+-ovs-vsctl add-br br-phys
+-ovn_attach n1 br-phys 192.168.0.1
+-ovs-vsctl add-port br-int vif1 -- \
+-    set Interface vif1 external-ids:iface-id=down_vif1 \
+-    options:tx_pcap=hv1/vif1-tx.pcap options:rxq_pcap=hv1/vif1-rx.pcap
+-
+-# Create hypervisor hv2 connected to n1, add localnet here
+-sim_add hv2
+-as hv2
+-ovs-vsctl add-br br-phys
+-ovs-vsctl add-br br-eth0
+-ovn_attach n1 br-phys 192.168.0.2
+-ovs-vsctl add-port br-int vif2 -- \
+-    set Interface vif2 external-ids:iface-id=down_vif2 \
+-    options:tx_pcap=hv2/vif2-tx.pcap options:rxq_pcap=hv2/vif2-rx.pcap
+-
+-ovs-vsctl set Open_vSwitch . external_ids:ovn-bridge-mappings="physnet1:br-eth0"
+-
+-ovs-vsctl add-port br-eth0 vif_ext -- \
+-    set Interface vif_ext options:tx_pcap=hv2/vif_ext-tx.pcap \
+-    options:rxq_pcap=hv2/vif_ext-rx.pcap
+-
+-# Pre-populate the hypervisors' ARP tables so that we don't lose any
+-# packets for ARP resolution (native tunneling doesn't queue packets
+-# for ARP resolution).
+-OVN_POPULATE_ARP
+-
+-wait_for_ports_up
+-check ovn-nbctl --wait=hv sync
+-
+-# Annonce 192.168.1.222 from localnet in hv2
+-# result: drop, hv2 is not gateway chassis for down_link
+-sha=02:00:00:00:00:ee
+-tha=00:00:00:00:00:00
+-spa=192.168.1.222
+-tpa=$spa
+-garp=$(fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
+-                ARP(hwsrc='${sha}', hwdst='${tha}', psrc='${spa}', pdst='${tpa}')")
+-as hv2 ovs-appctl netdev-dummy/receive vif_ext $garp
+-
+-# Make hv2 gateway chassis
+-# Annonce 192.168.1.223 from localnet in hv2
+-# result: ok, hv2 is gateway chassis for down_link
+-#
+-check ovn-nbctl lrp-set-gateway-chassis down_link hv2
+-
+-wait_row_count Port_Binding 1 logical_port=cr-down_link 'chassis!=[[]]'
+-check ovn-nbctl --wait=hv sync
+-
+-sha=02:00:00:00:00:ee
+-tha=00:00:00:00:00:00
+-spa=192.168.1.223
+-tpa=$spa
+-garp=$(fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
+-                ARP(hwsrc='${sha}', hwdst='${tha}', psrc='${spa}', pdst='${tpa}')")
+-as hv2 ovs-appctl netdev-dummy/receive vif_ext $garp
+-
+-# Annonce 192.168.1.111, 112 from vif1, vif2 in hv1, hv2
+-# result: ok, vif1, vif2 are virtual ports, restrictions are not applied.
+-sha=f0:00:00:00:00:01
+-tha=00:00:00:00:00:00
+-spa=192.168.1.111
+-tpa=0.0.0.0
+-garp=$(fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
+-                ARP(hwsrc='${sha}', hwdst='${tha}', psrc='${spa}', pdst='${tpa}')")
+-as hv1 ovs-appctl netdev-dummy/receive vif1 $garp
+-
+-sha=f0:00:00:00:00:02
+-tha=00:00:00:00:00:00
+-spa=192.168.1.112
+-tpa=0.0.0.0
+-garp=$(fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
+-                ARP(hwsrc='${sha}', hwdst='${tha}', psrc='${spa}', pdst='${tpa}')")
+-as hv2 ovs-appctl netdev-dummy/receive vif2 $garp
+-
+-# Set down_ext type to l2gateway
+-# Annonce 192.168.1.113, 114 from vif1, vif2 in hv1, hv2
+-# result: ok, vif1, vif2 are virtual ports, restrictions are not applied.
+-check ovn-nbctl --wait=hv lsp-set-type down_ext l2gateway
+-
+-sha=f0:00:00:00:00:01
+-tha=00:00:00:00:00:00
+-spa=192.168.1.113
+-tpa=0.0.0.0
+-garp=$(fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
+-                ARP(hwsrc='${sha}', hwdst='${tha}', psrc='${spa}', pdst='${tpa}')")
+-as hv1 ovs-appctl netdev-dummy/receive vif1 $garp
+-
+-sha=f0:00:00:00:00:02
+-tha=00:00:00:00:00:00
+-spa=192.168.1.114
+-tpa=0.0.0.0
+-garp=$(fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
+-                ARP(hwsrc='${sha}', hwdst='${tha}', psrc='${spa}', pdst='${tpa}')")
+-as hv2 ovs-appctl netdev-dummy/receive vif2 $garp
+-
+-wait_row_count MAC_Binding 1 ip="192.168.1.111" mac='"f0:00:00:00:00:01"' logical_port='"down_link"'
+-wait_row_count MAC_Binding 1 ip="192.168.1.112" mac='"f0:00:00:00:00:02"' logical_port='"down_link"'
+-wait_row_count MAC_Binding 1 ip="192.168.1.113" mac='"f0:00:00:00:00:01"' logical_port='"down_link"'
+-wait_row_count MAC_Binding 1 ip="192.168.1.114" mac='"f0:00:00:00:00:02"' logical_port='"down_link"'
+-wait_row_count MAC_Binding 1 ip="192.168.1.223" mac='"02:00:00:00:00:ee"' logical_port='"down_link"'
+-wait_row_count MAC_Binding 0 ip="192.168.1.222" mac='"02:00:00:00:00:ee"' logical_port='"down_link"'
+-
+-check ovn-nbctl --wait=hv lsp-set-type down_ext localnet
+-
+-OVN_CLEANUP([hv1],[hv2])
+-AT_CLEANUP
+-])
+-
+ OVN_FOR_EACH_NORTHD([
+ AT_SETUP([Port security - VRRPv3 ARP/ND])
+ AT_SKIP_IF([test $HAVE_SCAPY = no])
+@@ -45968,3 +46069,66 @@ AT_CHECK([grep -q "WARN.*dynamic-routing" hv/ovn-controller.log], [1])
+ OVN_CLEANUP([hv])
+ AT_CLEANUP
+ ])
++
++OVN_FOR_EACH_NORTHD([
++AT_SETUP([IPv4 over v6 Neigh solicitation test])
++ovn_start
++
++net_add n1
++sim_add hv
++ovs-vsctl add-br br-phys
++ovn_attach n1 br-phys 192.168.0.1
++ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
++
++check ovn-nbctl ls-add sw0
++check ovn-nbctl lsp-add sw0 sw0-port1
++check ovn-nbctl lsp-set-addresses sw0-port1 "50:54:00:00:00:01 10.0.0.3"
++
++check ovn-nbctl lr-add lr0
++check ovn-nbctl lrp-add lr0 lr0-sw0 00:00:00:00:ff:01 10.0.0.1/24 1000::1/64
++check ovn-nbctl lsp-add-router-port sw0 sw0-lr0 lr0-sw0
++
++check ovn-nbctl ls-add public
++check ovn-nbctl lrp-add lr0 lr0-public 00:00:20:20:12:13
++check ovn-nbctl lsp-add-router-port public public-lr0 lr0-public
++
++# localnet port
++check ovn-nbctl lsp-add-localnet-port public ln-public phys
++
++check ovn-nbctl lrp-set-gateway-chassis lr0-public hv 20
++check ovn-nbctl lr-nat-add lr0 dnat_and_snat 172.168.0.110 10.0.0.3
++
++ovs-vsctl -- add-port br-int vif1 -- \
++    set interface vif1 external-ids:iface-id=sw0-port1 \
++    options:tx_pcap=hv/vif1-tx.pcap \
++    options:rxq_pcap=hv/vif1-rx.pcap \
++    ofport-request=1
++
++wait_for_ports_up
++
++# Add a learnt route manually
++dp_uuid=$(fetch_column datapath _uuid external_ids:name=lr0)
++lrp_uuid=$(fetch_column port_binding _uuid logical_port=lr0-public)
++
++check_uuid ovn-sbctl create learned_route datapath=$dp_uuid logical_port=$lrp_uuid \
++ip_prefix=0.0.0.0/0 nexthop='"fe80::42:ff:fe00:1ff"'
++
++check ovn-nbctl --wait=hv sync
++
++# Send an IPv4 packet from sw0-port1 destined to outside
++packet=$(fmt_pkt "Ether(dst='00:00:00:00:ff:01', src='50:54:00:00:00:01')/ \
++                      IP(dst='8.8.8.8', src='10.0.0.3')/ICMP()")
++check as hv ovs-appctl netdev-dummy/receive vif1 $packet
++
++nd_ns=$(fmt_pkt "Ether(dst='33:33:ff:00:01:ff', src='00:00:20:20:12:13')/ \
++                       IPv6(src='fe80::200:20ff:fe20:1213', \
++                       dst='ff02::1:ff00:1ff')/ICMPv6ND_NS(tgt='fe80::42:ff:fe00:1ff')/\
++                       ICMPv6NDOptSrcLLAddr(lladdr='00:00:20:20:12:13')")
++
++echo $nd_ns > expected_nd_ns
++OVN_CHECK_PACKETS_CONTAIN([hv/br-phys_n1-tx.pcap], [expected_nd_ns])
++
++OVN_CLEANUP([hv])
++AT_CLEANUP
++])
++
+diff --git a/tests/system-dpdk-macros.at b/tests/system-dpdk-macros.at
+index 0d0a191305..6eb6888386 100644
+--- a/tests/system-dpdk-macros.at
++++ b/tests/system-dpdk-macros.at
+@@ -43,7 +43,7 @@ m4_define([OVS_TRAFFIC_VSWITCHD_START],
+   [OVS_DPDK_PRE_CHECK()
+    OVS_WAIT_WHILE([ip link show ovs-netdev])
+    _OVS_VSWITCHD_START([--disable-system],
+-                       [-- set Open_vSwitch . other_config:dpdk-init=true other_config:dpdk-extra="--log-level=pmd.*:error --no-pci"])
++                       [-- set Open_vSwitch . other_config:dpdk-init=true other_config:pmd-cpu-mask=0x1 other_config:dpdk-extra="--log-level=pmd.*:error --no-pci"])
+    dnl Add bridges, ports, etc.
+    OVS_WAIT_WHILE([ip link show br0])
+    AT_CHECK([ovs-vsctl -- _ADD_BR([br0]) -- $1 m4_if([$2], [], [], [| uuidfilt])], [0], [$2])
+diff --git a/tests/system-ovn.at b/tests/system-ovn.at
+index 582ed194b5..6747782ece 100644
+--- a/tests/system-ovn.at
++++ b/tests/system-ovn.at
+@@ -4616,8 +4616,7 @@ OVN_CLEANUP_NORTHD
+ as
+ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+ /connection dropped.*/d
+-/Service monitor not found.*/d
+-/handle service check: Unsupported protocol*/d"])
++/Service monitor not found.*/d"])
+ 
+ AT_CLEANUP
+ ])
+@@ -4816,8 +4815,7 @@ OVN_CLEANUP_NORTHD
+ as
+ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+ /connection dropped.*/d
+-/Service monitor not found.*/d
+-/handle service check: Unsupported protocol*/d"])
++/Service monitor not found.*/d"])
+ 
+ AT_CLEANUP
+ ])
+@@ -5027,9 +5025,12 @@ OVS_WAIT_UNTIL([
+     test "${n_reset}" = "1"
+ ])
+ 
++ovn-appctl coverage/show > ovn_counters
++AT_CHECK([test 50 -gt $(ovn-appctl -t ovn-controller coverage/read-counter pinctrl_notify_handler_thread)], [0])
++
+ # Check that we do not get too many immediate wake up.
+ # Tolerate a few for any race conditions.
+-AT_CHECK([test 5 -gt `cat ovn-controller.log | \
++AT_CHECK([test 5 -gt `grep ovn_pinctrl0 ovn-controller.log | \
+ grep -c "wakeup due to 0-ms timeout at controller/pinctrl.c:"`])
+ 
+ OVN_CLEANUP_CONTROLLER([hv1])
+@@ -13842,99 +13843,6 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/.*error receiving.*/d
+ AT_CLEANUP
+ ])
+ 
+-OVN_FOR_EACH_NORTHD([
+-AT_SETUP([SB Disconnect - MAC_Binding buffer limit])
+-ovn_start
+-OVS_TRAFFIC_VSWITCHD_START()
+-
+-ADD_BR([br-int])
+-ADD_BR([br-ext])
+-
+-SB_PATH="unix:$ovs_base/ovn-sb/ovn-sb.sock"
+-ovs-ofctl add-flow br-ext action=normal
+-# Set external-ids in br-int needed for ovn-controller
+-ovs-vsctl \
+-        -- set Open_vSwitch . external-ids:system-id=hv1 \
+-        -- set Open_vSwitch . external-ids:ovn-remote=$SB_PATH \
+-        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+-        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+-        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+-
+-# Start ovn-controller
+-start_daemon ovn-controller
+-
+-ADD_NAMESPACES(sw01)
+-ADD_VETH(sw01, sw01, br-int, "192.168.1.10/24", "f0:00:00:01:02:03", \
+-        "192.168.1.1")
+-ADD_NAMESPACES(server)
+-ADD_VETH(s1, server, br-ext, "172.16.1.1/24", "f0:00:00:01:02:05", \
+-         "172.16.1.254")
+-
+-check ovn-nbctl lr-add R1
+-
+-check ovn-nbctl ls-add sw0
+-check ovn-nbctl ls-add sw1
+-check ovn-nbctl ls-add sw-ext
+-
+-check ovn-nbctl lrp-add R1 rp-sw0 00:00:01:01:02:03 192.168.1.1/24
+-check ovn-nbctl lrp-add R1 rp-ext 00:00:02:01:02:03 172.16.1.254/16
+-
+-check ovn-nbctl lrp-set-gateway-chassis rp-ext hv1
+-
+-check ovn-nbctl lsp-add sw0 sw0-rp -- set Logical_Switch_Port sw0-rp \
+-    type=router options:router-port=rp-sw0 \
+-    -- lsp-set-addresses sw0-rp router
+-
+-check ovn-nbctl set Logical_Switch sw0 other_config:dhcp_relay_port=sw0-rp
+-
+-check ovn-nbctl lsp-add sw-ext ext-rp -- set Logical_Switch_Port ext-rp \
+-    type=router options:router-port=rp-ext \
+-    -- lsp-set-addresses ext-rp router
+-check ovn-nbctl lsp-add-localnet-port sw-ext lnet phynet
+-
+-check ovn-nbctl lsp-add sw0 sw01 \
+-    -- lsp-set-addresses sw01 "f0:00:00:01:02:03 192.168.1.10"
+-
+-AT_CHECK([ovs-vsctl set Open_vSwitch . external-ids:ovn-bridge-mappings=phynet:br-ext])
+-
+-OVN_POPULATE_ARP
+-
+-check ovn-nbctl --wait=hv sync
+-
+-AS_BOX([Disconnect SB and send ping to generate multiple ARPs (>1000)])
+-AT_CHECK([ovs-vsctl set Open_vSwitch . external-ids:ovn-remote=tcp:127.0.0.1:1234])
+-AT_CHECK([ovs-vsctl set Open_vSwitch . external-ids:ovn-remote-probe-interval=100])
+-
+-sleep 1
+-AT_CHECK([ovn-appctl connection-status], [0], [dnl
+-not connected
+-])
+-
+-for i in {1..20}; do
+-  for j in {1..100}; do
+-    NS_EXEC([sw01], [timeout 0.1 ping -q -c 1 -W 0.1 172.16.$i.$j >/dev/null 2>&1 &])
+-  done
+-  sleep 0.5
+-done
+-
+-AS_BOX([Verify pinctrl_drop_buffered_packets_map counter])
+-ovn-appctl  coverage/show >ovn_counters
+-AT_CAPTURE_FILE([ovn_counters])
+-AT_CHECK([grep -q "pinctrl_drop_buffered_packets_map" ovn_counters], [0])
+-
+-AT_CHECK([ovs-vsctl set Open_vSwitch . external-ids:ovn-remote=$SB_PATH])
+-
+-OVN_CLEANUP_CONTROLLER([hv1])
+-
+-OVN_CLEANUP_NORTHD
+-
+-as
+-OVS_TRAFFIC_VSWITCHD_STOP(["/.*error receiving.*/d
+-/failed to query port patch-.*/d
+-/.*terminating with signal 15.*/d"])
+-AT_CLEANUP
+-])
+-
+ OVN_FOR_EACH_NORTHD([
+ AT_SETUP([Routing protocol redirect - l3 gateway])
+ AT_SKIP_IF([test $HAVE_NC = no])
+@@ -14958,6 +14866,8 @@ NS_CHECK_EXEC([vm1], [ping -q -c 3 -i 0.3 -w 2 172.18.1.12 | FORMAT_PING], \
+ ])
+ 
+ check ovn-nbctl --wait=hv set load_balancer lb0 options:add_route=false
++# Wait for revalidation of stale megaflows which may lag behind.
++check ovs-appctl revalidator/wait
+ NS_CHECK_EXEC([vm1], [ping -q -c 3 -i 0.3 -w 2 172.18.1.12 | FORMAT_PING], \
+ [0], [dnl
+ 7 packets transmitted, 0 received, 100% packet loss, time 0ms
+@@ -21497,93 +21407,6 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+ AT_CLEANUP
+ ])
+ 
+-OVN_FOR_EACH_NORTHD([
+-AT_SETUP([Unsupported protocol message])
+-AT_SKIP_IF([test $HAVE_NC = no])
+-
+-ovn_start
+-OVS_TRAFFIC_VSWITCHD_START()
+-ADD_BR([br-int])
+-
+-# 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 bridge br-int fail-mode=secure other-config:disable-in-band=true
+-
+-# Start ovn-controller.
+-start_daemon ovn-controller
+-
+-check ovn-nbctl ls-add ls1
+-check ovn-nbctl lsp-add ls1 ls1p1
+-check ovn-nbctl lsp-set-addresses ls1p1 "00:00:00:01:01:01 192.168.1.1"
+-check ovn-nbctl lsp-add ls1 ls1p2
+-check ovn-nbctl lsp-set-addresses ls1p2 "00:00:00:01:01:02 192.168.1.2"
+-
+-check ovn-nbctl lr-add lr1
+-check ovn-nbctl lrp-add lr1 lr1-ls1 00:00:00:00:00:01 192.168.1.254/24
+-check ovn-nbctl lsp-add ls1 ls1-lr1
+-check ovn-nbctl lsp-set-addresses ls1-lr1 "00:00:00:00:00:01 192.168.1.254"
+-check ovn-nbctl lsp-set-type ls1-lr1 router
+-check ovn-nbctl lsp-set-options ls1-lr1 router-port=lr1-ls1
+-
+-check ovn-nbctl lrp-add lr1 lr1-ls2 00:00:00:00:00:02 192.168.2.254/24
+-
+-check ovn-nbctl ls-add ls2
+-check ovn-nbctl lsp-add ls2 ls2-lr1
+-check ovn-nbctl lsp-set-addresses ls2-lr1 "00:00:00:00:00:02 192.168.2.254"
+-check ovn-nbctl lsp-set-type ls2-lr1 router
+-check ovn-nbctl lsp-set-options ls2-lr1 router-port=lr1-ls2
+-
+-check ovn-nbctl lsp-add ls2 ls2p1
+-check ovn-nbctl lsp-set-addresses ls2p1 "00:00:00:01:02:01 192.168.2.1"
+-
+-ADD_NAMESPACES(ls1p1)
+-ADD_VETH(ls1p1, ls1p1, br-int, "192.168.1.1/24", "00:00:00:01:01:01",
+-         "192.168.1.254")
+-
+-ADD_NAMESPACES(ls2p1)
+-ADD_VETH(ls2p1, ls2p1, br-int, "192.168.2.1/24", "00:00:00:01:02:01",
+-         "192.168.2.254")
+-
+-ADD_NAMESPACES(ls1p2)
+-ADD_VETH(ls1p2, ls1p2, br-int, "192.168.1.2/24", "00:00:00:01:01:02",
+-         "192.168.1.254")
+-
+-check ovn-nbctl lb-add lb0 192.168.5.1:12345 192.168.1.1:12345,192.168.1.2:12345
+-check ovn-nbctl ls-lb-add ls1 lb0
+-check ovn-nbctl lr-lb-add lr1 lb0
+-lb_uuid=$(fetch_column nb:Load_Balancer _uuid name=lb0)
+-check ovn-nbctl set Load_Balancer $lb_uuid protocol=udp
+-check ovn-nbctl --wait=hv set Logical_Router lr1 options:chassis="hv1"
+-
+-wait_for_ports_up ls1p1 ls1p2 ls2p1
+-
+-NETNS_DAEMONIZE([ls1p1], [nc -l 12345 --udp -k --sh-exec ls], [nc1.pid])
+-NETNS_DAEMONIZE([ls1p2], [nc -l 12345 --udp -k --sh-exec ls], [nc2.pid])
+-
+-hc_uuid=$(ovn-nbctl --id=@hc create Load_Balancer_Health_Check vip="192.168.5.1\:12345" -- \
+-          add Load_Balancer $lb_uuid health_check @hc)
+-check ovn-nbctl set Load_Balancer_Health_Check $hc_uuid options:timeout=20 options:success_count=3 options:failure_count=3
+-check ovn-nbctl --wait=sb set load_balancer $lb_uuid ip_port_mappings:192.168.1.1=ls1p1:192.168.1.254
+-
+-NS_EXEC([ls2p1], [nc --udp 192.168.5.1 12345 <<< h])
+-
+-# It may not seem like we're actually testing anything in this test.
+-# If there is a warning or error in the ovn-controller log about
+-# an unsupported health check protocol, it will cause a test failure
+-# when we stop ovn-controller.
+-OVN_CLEANUP_CONTROLLER([hv1])
+-OVN_CLEANUP_NORTHD
+-
+-as
+-OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+-/connection dropped.*/d"])
+-AT_CLEANUP
+-])
+-
+ OVN_FOR_EACH_NORTHD([
+ AT_SETUP([Load balancer health checks - service monitor source MAC matching])
+ AT_SKIP_IF([test $HAVE_NC = no])
+@@ -21814,3 +21637,77 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+ 
+ AT_CLEANUP
+ ])
++
++OVN_FOR_EACH_NORTHD([
++AT_SETUP([VIF port connected to localnet network])
++#
++# Topology:
++#    (fabric) -- localnet-port -- LS --- DGP(chassis2) -- LR
++#                                 |
++#                                 |
++#                               VM (chassis1)
++#
++# It is expected that ARP requests to this port are allowed on the chassis that hosts this port.
++
++ovn_start
++OVS_TRAFFIC_VSWITCHD_START()
++ADD_BR([br-int])
++ADD_BR([br-ext])
++
++ovs-ofctl add-flow br-ext action=normal
++# Set external-ids in br-int needed for ovn-controller
++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 bridge br-int fail-mode=secure other-config:disable-in-band=true
++
++# Start ovn-controller
++start_daemon ovn-controller
++
++check ovn-nbctl lr-add lr1
++check ovn-nbctl ls-add public
++
++check ovn-nbctl lrp-add lr1 rp-public 00:00:02:01:02:03 172.31.1.1/24
++check ovn-nbctl lsp-add-router-port public public-rp rp-public
++check ovn-nbctl lsp-add-localnet-port public localnet phynet
++check ovn-nbctl lrp-set-gateway-chassis rp-public hv2
++
++ADD_NAMESPACES(ext)
++ADD_VETH(ext, ext, br-ext, "172.31.1.2/24", "f0:00:00:01:02:02", \
++         "172.31.1.1")
++ADD_NAMESPACES(lsp1)
++ADD_VETH(lsp1, lsp1, br-int, "172.31.1.3/24", "f0:00:00:01:02:03", \
++         "172.31.1.1")
++ADD_NAMESPACES(lsp2)
++ADD_VETH(lsp2, lsp2, br-int, "172.31.1.4/24", "f0:00:00:01:02:04", \
++         "172.31.1.1")
++
++check ovn-nbctl lsp-add public lsp1
++check ovn-nbctl lsp-set-addresses lsp1 "f0:00:00:01:02:03 172.31.1.3"
++check ovn-nbctl lsp-add public lsp2
++check ovn-nbctl lsp-set-addresses lsp2 "f0:00:00:01:02:04 172.31.1.4"
++
++check ovn-nbctl --wait=hv sync
++
++NS_CHECK_EXEC([ext], [ping -q -c 3 -i 0.3 -w 2 172.31.1.3 | FORMAT_PING], \
++[0], [dnl
++3 packets transmitted, 3 received, 0% packet loss, time 0ms
++])
++
++NS_CHECK_EXEC([lsp1], [ping -q -c 3 -i 0.3 -w 2 172.31.1.4 | FORMAT_PING], \
++[0], [dnl
++3 packets transmitted, 3 received, 0% packet loss, time 0ms
++])
++
++OVN_CLEANUP_CONTROLLER([hv1])
++OVN_CLEANUP_NORTHD
++
++as
++OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
++/connection dropped.*/d"])
++
++AT_CLEANUP
++])

diff --git a/ovn.spec b/ovn.spec
index b5beab3..6cd7438 100644
--- a/ovn.spec
+++ b/ovn.spec
@@ -46,7 +46,7 @@ Name: ovn
 Summary: Open Virtual Network support
 URL: http://www.openvswitch.org/
 Version: 26.03.1
-Release: 10%{?commit0:.%{date}git%{shortcommit0}}%{?dist}
+Release: 50%{?commit0:.%{date}git%{shortcommit0}}%{?dist}
 Obsoletes: openvswitch-ovn-common < %{?epoch_ovs:%{epoch_ovs}:}2.11.0-8
 Provides: openvswitch-ovn-common = %{?epoch:%{epoch}:}%{version}-%{release}
 
@@ -455,6 +455,10 @@ fi
 %{_unitdir}/ovn-br-db.service
 
 %changelog
+* Thu Jun 4 2026 Dumitru Ceara <dceara@redhat.com> - 26.03.1-50
+- Updated the OVN sources to pick up the commits from v26.03.1 till
+  the tip of branch-26.03 (9f04b8c5086a74db299daeea4908518bbb27d139).
+
 * Wed Apr 22 2026 Dumitru Ceara <dceara@redhat.com> - 26.03.3-10
 - Updated the OVN sources to upstream release v26.03.1 with the
   commit 0cc1ea5bb71d29b91244f5368ecbbda8837bc542 and picked up

^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-06-04  9:57 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-04  9:57 [rpms/ovn] f44: Pick up all current bug fixes from upstream branch-26.03 Dumitru Ceara

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox