public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Dumitru Ceara <dceara@redhat.com>
To: git-commits@fedoraproject.org
Subject: [rpms/ovn] f44: Pick up all current bug fixes from upstream branch-26.03.
Date: Thu, 04 Jun 2026 09:57:20 GMT [thread overview]
Message-ID: <178056704023.1.4884118893245116999.rpms-ovn-5052cd082422@fedoraproject.org> (raw)
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 &&
++ 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 &&
++ 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
reply other threads:[~2026-06-04 9:57 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=178056704023.1.4884118893245116999.rpms-ovn-5052cd082422@fedoraproject.org \
--to=dceara@redhat.com \
--cc=git-commits@fedoraproject.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox