From: Aleksandr Smirnov <[email protected]>

The northd creates flows that restrict processing ARP requests:
If ARP request comes from lswitch that has either localnet or
l2gateway ports and router's attachment port is L3 dgw port such
ARP will be allowed only on resident chassis.

However, same lswitch could also have 'normal' aka virtual lports.
For these lports restrictions described above should not be applied.

The fix moves is_chassis_resident check from lrouter to lswitch level.
The residence check is only applied for ARP requests that were
originated from localnet/l2gateway ports. If such check is succeeded
ARP request is allowed otherwise ARP request packet is dropped.
For ARP requests coming from virtual ports no residence restrictions
are applied.

Co-authored-by: Alexandra Rukomoinikova <[email protected]>
Signed-off-by: Aleksandr Smirnov <[email protected]>
Signed-off-by: Alexandra Rukomoinikova <[email protected]>
---
v4 --> v5:
1) Addressed Dumitru's comments:
   - Eliminated O(n) search by implementing uuid based lookup within ls_arp 
nodes
   - Retained ls_index in ls_arp_record structure for exclusive use in northd.c 
- It seems that this is the best option for searching for a datapass

2) Optimized ls_arp_record creation:
   - Previous approach: Iterated through all NATs for each ls_arp_record
   - New approach: Iterate through logical switch router ports to get required 
NAT records - i think it should reduce number of iterations

3) Restricted ls_arp_record creation scope: Records now created only for 
logical switches with physical ports - there shouldn't be many of them

4) Added ovn-northd documentation and ovn-northd tests for lflow functionality
---
 lib/stopwatch-names.h    |   1 +
 northd/automake.mk       |   2 +
 northd/en-lflow.c        |  29 ++++
 northd/en-lflow.h        |   2 +
 northd/en-ls-arp.c       | 355 +++++++++++++++++++++++++++++++++++++++
 northd/en-ls-arp.h       |  86 ++++++++++
 northd/inc-proc-northd.c |   8 +
 northd/northd.c          | 191 +++++++++++++++++++--
 northd/northd.h          |  10 ++
 northd/ovn-northd.8.xml  |  16 ++
 northd/ovn-northd.c      |   1 +
 tests/ovn-northd.at      |  79 ++++++++-
 tests/ovn.at             | 153 +++++++++++++++++
 13 files changed, 918 insertions(+), 15 deletions(-)
 create mode 100644 northd/en-ls-arp.c
 create mode 100644 northd/en-ls-arp.h

diff --git a/lib/stopwatch-names.h b/lib/stopwatch-names.h
index f3a931c40..b912e813c 100644
--- a/lib/stopwatch-names.h
+++ b/lib/stopwatch-names.h
@@ -34,6 +34,7 @@
 #define LR_NAT_RUN_STOPWATCH_NAME "lr_nat_run"
 #define LR_STATEFUL_RUN_STOPWATCH_NAME "lr_stateful"
 #define LS_STATEFUL_RUN_STOPWATCH_NAME "ls_stateful"
+#define LS_ARP_RUN_STOPWATCH_NAME "ls_arp"
 #define ADVERTISED_ROUTE_SYNC_RUN_STOPWATCH_NAME "advertised_route_sync"
 #define LEARNED_ROUTE_SYNC_RUN_STOPWATCH_NAME "learned_route_sync"
 #define DYNAMIC_ROUTES_RUN_STOPWATCH_NAME "dynamic_routes"
diff --git a/northd/automake.mk b/northd/automake.mk
index 45ca0337f..8cd4fb3a1 100644
--- a/northd/automake.mk
+++ b/northd/automake.mk
@@ -44,6 +44,8 @@ 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 30bf7e35c..21a244a1b 100644
--- a/northd/en-lflow.c
+++ b/northd/en-lflow.c
@@ -23,6 +23,7 @@
 #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,6 +63,8 @@ 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 =
@@ -86,6 +89,7 @@ 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 =
@@ -229,6 +233,31 @@ 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 99bcfda15..d2a92e49f 100644
--- a/northd/en-lflow.h
+++ b/northd/en-lflow.h
@@ -25,6 +25,8 @@ 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
new file mode 100644
index 000000000..69d76ef3e
--- /dev/null
+++ b/northd/en-ls-arp.c
@@ -0,0 +1,355 @@
+/*
+ * 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"
+#include "stopwatch.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 "lib/stopwatch-names.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_centralized_nat_record(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
+           && !nat_entry->is_distributed;
+}
+
+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_centralized_nat_record(nat_entry)) {
+                hmapx_add(&ls_arp_record->nat_records,
+                         (struct lrnat_rec *) lrnat_rec);
+            }
+        }
+    }
+}
+
+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->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_;
+
+    stopwatch_start(LS_ARP_RUN_STOPWATCH_NAME, time_msec());
+
+    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->ph_ports)) {
+            continue;
+        }
+
+        ls_arp_record_create(&data->table, od, input_data.lr_nats);
+    }
+
+    stopwatch_stop(LS_ARP_RUN_STOPWATCH_NAME, time_msec());
+
+    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->ph_ports)) {
+                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_centralized_nat_record(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
new file mode 100644
index 000000000..5eaf913bb
--- /dev/null
+++ b/northd/en-ls-arp.h
@@ -0,0 +1,86 @@
+/*
+ * 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/inc-proc-northd.c b/northd/inc-proc-northd.c
index 94199de12..748e09769 100644
--- a/northd/inc-proc-northd.c
+++ b/northd/inc-proc-northd.c
@@ -34,6 +34,7 @@
 #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"
@@ -174,6 +175,7 @@ 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);
@@ -305,6 +307,9 @@ 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);
@@ -409,6 +414,9 @@ 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 e978b66c2..a0db6f226 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -50,6 +50,7 @@
 #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"
@@ -136,6 +137,7 @@ 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. */
 
@@ -547,6 +549,7 @@ 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->ph_ports);
     sset_init(&od->router_ips);
     od->ls_peers = VECTOR_EMPTY_INITIALIZER(struct ovn_datapath *);
     od->router_ports = VECTOR_EMPTY_INITIALIZER(struct ovn_port *);
@@ -584,6 +587,7 @@ 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->ph_ports);
         sset_destroy(&od->router_ips);
         lflow_ref_destroy(od->datapath_lflows);
         free(od);
@@ -1218,6 +1222,12 @@ 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)
 {
@@ -1604,6 +1614,10 @@ join_logical_ports_lsp(struct hmap *ports,
         od->has_vtep_lports = true;
     }
 
+    if (lsp_is_localnet(nbsp) || lsp_is_l2gw(nbsp)) {
+        hmapx_add(&od->ph_ports, op);
+    }
+
     parse_lsp_addrs(op);
 
     op->od = od;
@@ -1997,6 +2011,15 @@ join_logical_ports(const struct sbrec_port_binding_table 
*sbrec_pb_table,
     }
 }
 
+static bool
+is_nat_distributed(const struct nbrec_nat *nat,
+                   const struct ovn_datapath *od)
+{
+    return !vector_is_empty(&od->l3dgw_ports)
+           && nat->logical_port && nat->external_mac
+           && !strcmp(nat->type, "dnat_and_snat");
+}
+
 /* Returns an array of strings, each consisting of a MAC address followed
  * by one or more IP addresses, and if the port is a distributed gateway
  * port, followed by 'is_chassis_resident("LPORT_NAME")', where the
@@ -2055,9 +2078,7 @@ get_nat_addresses(const struct ovn_port *op, size_t *n, 
bool routable_only,
 
         /* Determine whether this NAT rule satisfies the conditions for
          * distributed NAT processing. */
-        if (!vector_is_empty(&op->od->l3dgw_ports) &&
-            !strcmp(nat->type, "dnat_and_snat") &&
-            nat->logical_port && nat->external_mac) {
+        if (is_nat_distributed(nat, op->od)) {
             /* Distributed NAT rule. */
             if (eth_addr_from_string(nat->external_mac, &mac)) {
                 struct ds address = DS_EMPTY_INITIALIZER;
@@ -9676,6 +9697,99 @@ 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 sset inports;
+    struct sset resident_ports;
+    struct sset distributed_nat_ports;
+
+    sset_init(&inports);
+    sset_init(&resident_ports);
+    sset_init(&distributed_nat_ports);
+
+    struct ds match = DS_EMPTY_INITIALIZER;
+
+    struct hmapx_node *node;
+    HMAPX_FOR_EACH (node, &od->ph_ports) {
+        struct ovn_port *op = node->data;
+        sset_add(&inports, op->json_key);
+    }
+
+    struct ovn_port *op;
+    VECTOR_FOR_EACH (&od->router_ports, op) {
+        struct ovn_port *op_r = op->peer;
+
+        if (lrp_is_l3dgw(op_r)) {
+            sset_add(&resident_ports, op_r->cr_port->json_key);
+        }
+    }
+
+    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) {
+                sset_add(&distributed_nat_ports, ent->nb->logical_port);
+            }
+        }
+    }
+
+
+    if (!sset_is_empty(&inports) && !sset_is_empty(&resident_ports)) {
+        const char *item;
+
+        SSET_FOR_EACH (item, &inports) {
+            ds_clear(&match);
+            ds_put_format(&match,
+                          "(arp.op == 1 || arp.op == 2) && inport == %s",
+                          item);
+            ovn_lflow_add(lflows, od, S_SWITCH_IN_CHECK_PORT_SEC, 75,
+                          ds_cstr(&match), REGBIT_EXT_ARP " = 1; next;",
+                          ar->lflow_ref);
+        }
+
+        SSET_FOR_EACH (item, &resident_ports) {
+            ds_clear(&match);
+            ds_put_format(&match,
+                          REGBIT_EXT_ARP" == 1 && is_chassis_resident(%s)",
+                          item);
+            ovn_lflow_add(lflows, od, S_SWITCH_IN_APPLY_PORT_SEC, 75,
+                          ds_cstr(&match), "next;", ar->lflow_ref);
+        }
+
+        SSET_FOR_EACH (item, &distributed_nat_ports) {
+            ds_clear(&match);
+            ds_put_format(&match,
+                          REGBIT_EXT_ARP
+                          " == 1 && is_chassis_resident(\"%s\")",
+                          item);
+            ovn_lflow_add(lflows, od, S_SWITCH_IN_APPLY_PORT_SEC, 75,
+                          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);
+    }
+
+    ds_destroy(&match);
+    sset_destroy(&inports);
+    sset_destroy(&resident_ports);
+    sset_destroy(&distributed_nat_ports);
+}
+
 static bool
 is_vlan_transparent(const struct ovn_datapath *od)
 {
@@ -14117,10 +14231,6 @@ 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;"
@@ -14137,10 +14247,6 @@ 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;",
@@ -18552,6 +18658,7 @@ 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;
@@ -18744,6 +18851,7 @@ 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;
@@ -18900,6 +19008,19 @@ build_lflows_thread(void *arg)
                 }
             }
 
+            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);
@@ -18948,6 +19069,7 @@ 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,
@@ -18984,6 +19106,7 @@ 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 =
@@ -19020,6 +19143,7 @@ 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;
@@ -19032,6 +19156,7 @@ 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,
@@ -19127,6 +19252,12 @@ 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);
     }
@@ -19210,6 +19341,7 @@ 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,
@@ -19712,6 +19844,43 @@ 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->ls_datapaths,
+            lflow_input->lr_datapaths,
+            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 32134d36e..c41c41e00 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -20,6 +20,7 @@
 #include "lib/ovn-util.h"
 #include "lib/ovs-atomic.h"
 #include "lib/sset.h"
+#include "lib/hmapx.h"
 #include "northd/en-port-group.h"
 #include "northd/ipam.h"
 #include "northd/lb.h"
@@ -27,6 +28,7 @@
 #include "simap.h"
 #include "ovs-thread.h"
 #include "en-lr-stateful.h"
+#include "en-ls-arp.h"
 #include "vec.h"
 #include "datapath-sync.h"
 
@@ -272,6 +274,7 @@ 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;
@@ -470,6 +473,9 @@ 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 ph_ports;
+
     /* Map of ovn_port objects belonging to this datapath.
      * This map doesn't include derived ports. */
     struct hmap ports;
@@ -957,6 +963,10 @@ 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 005fd87d1..0f6693b2f 100644
--- a/northd/ovn-northd.8.xml
+++ b/northd/ovn-northd.8.xml
@@ -310,6 +310,15 @@
         </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
@@ -396,6 +405,13 @@
         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>
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index 52a3c7883..25677d73e 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -1016,6 +1016,7 @@ main(int argc, char *argv[])
     stopwatch_create(LR_NAT_RUN_STOPWATCH_NAME, SW_MS);
     stopwatch_create(LR_STATEFUL_RUN_STOPWATCH_NAME, SW_MS);
     stopwatch_create(LS_STATEFUL_RUN_STOPWATCH_NAME, SW_MS);
+    stopwatch_create(LS_ARP_RUN_STOPWATCH_NAME, SW_MS);
     stopwatch_create(ADVERTISED_ROUTE_SYNC_RUN_STOPWATCH_NAME, SW_MS);
     stopwatch_create(LEARNED_ROUTE_SYNC_RUN_STOPWATCH_NAME, SW_MS);
     stopwatch_create(DYNAMIC_ROUTES_RUN_STOPWATCH_NAME, SW_MS);
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index f7ec6a33a..1533405b2 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -7335,9 +7335,6 @@ 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;)
@@ -14031,7 +14028,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], [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 | grep -v "reg0.*22"], [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;)
@@ -18760,3 +18757,77 @@ check_row_count Advertised_MAC_Binding 0
 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
+
+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 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
+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=75   , match=(reg0[[22]] == 1 && 
is_chassis_resident("down_vif1")), action=(next;)
+])
+
+check ovn-nbctl 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 changinf logical port type
+check ovn-nbctl 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 ovn-nbctl 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;)
+])
+
+AT_CLEANUP
+])
\ No newline at end of file
diff --git a/tests/ovn.at b/tests/ovn.at
index 30560f883..113d64229 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -43885,3 +43885,156 @@ OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], expected)
 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 - outher port will be iteratred as localnet, l2gateway
+#
+# Test: throw ARP annonce request from vitrual 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)
+#       throw ARP annonce 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!=[[]]'
+
+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
+])
-- 
2.48.1

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

Reply via email to