Hi, thanks for all the patches and revisions in addition to lorenzo's
comments I have one to add.

On Fri, Sep 19, 2025 at 12:48 PM Lorenzo Bianconi via dev <
ovs-dev@openvswitch.org> wrote:

> > 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 <arukomoinikova@k2.cloud>
> > Signed-off-by: Aleksandr Smirnov <alekssmirnov@k2.cloud>
> > Signed-off-by: Alexandra Rukomoinikova <arukomoinikova@k2.cloud>
>
> Hi Aleksandr and Alexandra,
>
> thx for the patches. Some comments and questions below.
>
> Regards,
> Lorenzo
>
> >
> > ---
> > v2: Minor fixes, add co-authored-by
> > v3: Apply Dumitru comments, code rework, move part of processing
> >     into new I-P node.
> > ---
> >  lib/stopwatch-names.h    |   1 +
> >  northd/automake.mk       |   2 +
> >  northd/en-lflow.c        |  29 ++++
> >  northd/en-lflow.h        |   2 +
> >  northd/en-ls-arp.c       | 284 +++++++++++++++++++++++++++++++++++++++
> >  northd/en-ls-arp.h       | 104 ++++++++++++++
> >  northd/inc-proc-northd.c |   8 ++
> >  northd/northd.c          | 179 ++++++++++++++++++++++--
> >  northd/northd.h          |  10 ++
> >  northd/ovn-northd.c      |   1 +
> >  tests/ovn-northd.at      |  10 +-
> >  tests/ovn.at             | 157 ++++++++++++++++++++++
> >  12 files changed, 771 insertions(+), 16 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 f903f5e3a..772daf623 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"
> > @@ -60,6 +61,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 =
> > @@ -84,6 +87,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 =
> > @@ -215,6 +219,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..fab778d03 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 *node, void *data);
> > +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..26f2f815e
> > --- /dev/null
> > +++ b/northd/en-ls-arp.c
> > @@ -0,0 +1,284 @@
> > +/*
> > +  * Copyright (c) 2024, Red Hat, Inc.
> > + *
> > + * Licensed under the Apache License, Version 2.0 (the "License");
> > + * you may not use this file except in compliance with the License.
> > + * You may obtain a copy of the License at:
> > + *
> > + *     http://www.apache.org/licenses/LICENSE-2.0
> > + *
> > + * Unless required by applicable law or agreed to in writing, software
> > + * distributed under the License is distributed on an "AS IS" BASIS,
> > + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> > + * See the License for the specific language governing permissions and
> > + * limitations under the License.
> > + */
> > +
> > +#include <config.h>
> > +
> > +/* 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 hmap *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.entries,
> > +    };
> > +}
> > +
> > +static void
> > +ls_arp_table_clear(struct ls_arp_table *table)
> > +{
> > +    struct ls_arp_record *ar;
> > +    HMAP_FOR_EACH_POP (ar, key_node, &table->entries) {
> > +        lflow_ref_destroy(ar->lflow_ref);
> > +        hmapx_destroy(&ar->nat_records);
> > +        free(ar);
> > +    }
> > +}
> > +
> > +/*
> > + *   Return hmapx (odmap) of datapaths, assumed lswitches,
> > + *   that are gateways for given nat.
> > + */
> > +static void
> > +nat_odmap_create(struct lr_nat_record *nr,
> > +                 struct hmapx *odmap)
> > +{
> > +    for (size_t i = 0; i < nr->n_nat_entries; i++) {
> > +        struct ovn_nat *ent = &nr->nat_entries[i];
> > +
> > +        if (ent->is_valid
> > +            && ent->l3dgw_port
> > +            && ent->l3dgw_port->peer
> > +            && ent->l3dgw_port->peer->od
> > +            && !ent->is_distributed) {
> > +            hmapx_add(odmap, ent->l3dgw_port->peer->od);
> > +        }
> > +    }
> > +}
> > +
> > +static bool
> > +ods_find_by_index(struct hmapx *odmap, size_t index)
> > +{
> > +    struct hmapx_node *hmapx_node;
> > +    HMAPX_FOR_EACH (hmapx_node, odmap) {
> > +        struct ovn_datapath *od = hmapx_node->data;
> > +
> > +        if (od->index == index) {
> > +            return true;
> > +        }
> > +    }
> > +
> > +    return false;
> > +}
> > +
> > +static struct ls_arp_record*
> > +ars_find_by_index(const struct ls_arp_table *table, size_t index)
> > +{
> > +    struct ls_arp_record *ar;
> > +    HMAP_FOR_EACH (ar, key_node, &table->entries) {
> > +        if (ar->ls_index == index) {
> > +            return ar;
> > +        }
> > +    }
> > +
> > +    return NULL;
> > +}
> > +
> > +/*
> > + * Fill nat_records in given ls_arp_record with nat records that have
> > + * lswitch, owned by arp record, as nat gateway.
> > + */
> > +static void
> > +ls_arp_record_set_nats(struct ls_arp_record *ar,
> > +                       const struct hmap *nats)
> > +{
> > +    hmapx_init(&ar->nat_records);
> > +
> > +    struct lr_nat_record *nr;
> > +    HMAP_FOR_EACH (nr, key_node, nats) {
> > +        struct hmapx ods = HMAPX_INITIALIZER(&ods);
> > +
> > +        nat_odmap_create(nr, &ods);
> > +
> > +        if (ods_find_by_index(&ods, ar->ls_index)) {
> > +            hmapx_add(&ar->nat_records, nr);
> > +        }
> > +
> > +        hmapx_destroy(&ods);
> > +    }
> > +}
> > +
> > +static struct ls_arp_record *
> > +ls_arp_record_create(struct ls_arp_table *table,
> > +                     const struct ovn_datapath *od,
> > +                     const struct hmap *lr_nats)
> > +{
> > +    struct ls_arp_record *ar = xzalloc(sizeof *ar);
> > +
> > +    ar->ls_index = od->index;
> > +    ls_arp_record_set_nats(ar, lr_nats);
> > +    ar->lflow_ref = lflow_ref_create();
> > +
> > +    hmap_insert(&table->entries, &ar->key_node,
> > +                uuid_hash(&od->nbs->header_.uuid));
> > +
> > +    return ar;
> > +}
> > +
> > +/* 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);
> > +
> > +    return data;
> > +}
> > +
> > +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);
> > +}
> > +
> > +void
> > +en_ls_arp_clear_tracked_data(void *data_)
> > +{
> > +    struct ed_type_ls_arp *data = data_;
> > +    hmapx_clear(&data->trk_data.crupdated);
> > +}
> > +
> > +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) {
> > +        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 *x OVS_UNUSED)
> > +{
> > +    struct northd_data *northd_data = engine_get_input_data("northd",
> node);
> > +    if (!northd_has_tracked_data(&northd_data->trk_data)) {
> > +        return EN_UNHANDLED;
> > +    }
>
> Now we support IP for logical-switch creation/deletion. Should we take
> care of
> it too?
>
> > +
> > +    return EN_HANDLED_UNCHANGED;
> > +}
> > +
> > +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);
> > +
> > +    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;
> > +    HMAPX_FOR_EACH (hmapx_node, &lr_nat_data->trk_data.crupdated) {
> > +        struct lr_nat_record *nr_cur = hmapx_node->data;
> > +        struct hmapx ods = HMAPX_INITIALIZER(&ods);
> > +
> > +        /* Collect all ods (lswitch) that are gateways for given nat */
> > +        nat_odmap_create(nr_cur, &ods);
> > +
> > +        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);
> > +
> > +            /* current nat record is already registered for given
> arp_record */
> > +            if (nr_node) {
> > +                /* trigger this arp_record to regenerate od lflow */
> > +                hmapx_add(&data->trk_data.crupdated, ar);
> > +
> > +                /* ... but not part of affected ods anymore,
> > +                   i.e. the change of the nat removes this gateway */
> > +                if (!ods_find_by_index(&ods, ar->ls_index)) {
> > +                    hmapx_delete(&ar->nat_records, nr_node);
> > +                }
> > +            }
> > +        }
> > +
> > +        /* Process gateways that are new */
> > +        struct hmapx_node *hmapx_node2;
> > +        HMAPX_FOR_EACH (hmapx_node2, &ods) {
> > +            struct ovn_datapath *od = hmapx_node2->data;
> > +
> > +            /* Determine which arp_record is affected */
> > +            ar = ars_find_by_index(&data->table, od->index);
> > +            ovs_assert(ar);
> > +
> > +            /* new gateway triggers lflow regeneration for this
> arp_records */
> > +            hmapx_add(&data->trk_data.crupdated, ar);
> > +            hmapx_add(&ar->nat_records, nr_cur);
> > +        }
> > +
> > +        hmapx_destroy(&ods);
> > +    }
> > +
> > +
> > +    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..ecc39eb77
> > --- /dev/null
> > +++ b/northd/en-ls-arp.h
> > @@ -0,0 +1,104 @@
> > +/*
> > + * Copyright (c) 2024, Red Hat, Inc.
> > + *
> > + * Licensed under the Apache License, Version 2.0 (the "License");
> > + * you may not use this file except in compliance with the License.
> > + * You may obtain a copy of the License at:
> > + *
> > + *     http://www.apache.org/licenses/LICENSE-2.0
> > + *
> > + * Unless required by applicable law or agreed to in writing, software
> > + * distributed under the License is distributed on an "AS IS" BASIS,
> > + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> > + * See the License for the specific language governing permissions and
> > + * limitations under the License.
> > + */
> > +#ifndef 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;
> > +
> > +    /* Unique id of the logical switch.  Note : This id is assigned
> > +     * by the northd engine node for each logical switch. */
> > +    size_t ls_index;
> > +
> > +    /* 'lflow_ref' is used to reference logical flows generated for
> > +     * this ls_arp record.
> > +     *
> > +     * This data is initialized and destroyed by the en_ls_arp node,
> > +     * but populated and used only by the en_lflow node. Ideally this
> data
> > +     * should be maintained as part of en_lflow's data.  However, it
> would
> > +     * be less efficient and more complex:
> > +     *
> > +     * 1. It would require an extra search (using the index) to find the
> > +     * lflows.
> > +     *
> > +     * 2. Building the index needs to be thread-safe, using either a
> global
> > +     * lock which is obviously less efficient, or hash-based lock array
> which
> > +     * is more complex.
> > +     *
> > +     * Adding the lflow_ref here is more straightforward. The drawback
> is that
> > +     * we need to keep in mind that this data belongs to en_lflow node,
> so
> > +     * never access it from any other nodes.
> > +     *
> > +     * Note: lflow_ref is not thread safe.  Only one thread should
> > +     * access ls_arp_record->lflow_ref at any given time.
> > +     */
> > +    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; /* Stores 'struct ls_arp_record'. */
> > +};
> > +
> > +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 *data);
> > +void en_ls_arp_clear_tracked_data(void *data);
> > +enum engine_node_state en_ls_arp_run(struct engine_node *, void *data);
> > +
> > +enum engine_input_handler_result
> > +ls_arp_lr_nat_handler(struct engine_node *node, void *data);
> > +enum engine_input_handler_result
> > +ls_arp_northd_handler(struct engine_node *node, void *data);
> > +
> > +static inline bool
> > +ls_arp_has_tracked_data(struct ls_arp_tracked_data *trk_data) {
> > +    return !hmapx_is_empty(&trk_data->crupdated);
> > +}
> > +
> > +#endif /* EN_LS_ARP_H */
> > +
> > diff --git a/northd/inc-proc-northd.c b/northd/inc-proc-northd.c
> > index bfb893a1a..8068123f0 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"
> > @@ -171,6 +172,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);
> > @@ -296,6 +298,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);
>

nit: the above two lines should probably be in the opposite order, I don't
think it really matters from the execution standpoint but the other engines
seem to list the inputs in execution order feel free to ignore if this is
not the case


> > +
> >      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);
> > @@ -390,6 +395,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 e0a329a17..33a3ff9b6 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"
> > @@ -135,6 +136,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. */
> >
> > @@ -532,6 +534,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 *);
> > @@ -569,6 +572,7 @@ ovn_datapath_destroy(struct hmap *datapaths, 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);
> >          free(od);
> >      }
> > @@ -1177,6 +1181,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)
> >  {
> > @@ -1563,6 +1573,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;
> > @@ -1956,6 +1970,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
> > @@ -2014,9 +2037,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;
> > @@ -9210,6 +9231,90 @@
> 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 port_check;
> > +    struct sset resident_check;
> > +
> > +    sset_init(&port_check);
> > +    sset_init(&resident_check);
> > +
> > +    struct ds match = DS_EMPTY_INITIALIZER;
> > +
> > +    struct hmapx_node *node;
> > +    HMAPX_FOR_EACH (node, &od->ph_ports) {
> > +        struct ovn_port *op = node->data;
> > +
> > +        ds_clear(&match);
> > +        ds_put_format(&match, "(arp.op == 1 || arp.op == 2) && inport
> == %s",
> > +                      op->json_key);
> > +        sset_add(&port_check, ds_cstr(&match));
>
> Have you done any evaluation of the impact of these new flows at scale?
>
> > +    }
> > +
> > +    struct ovn_port *op;
> > +    VECTOR_FOR_EACH (&od->router_ports, op) {
> > +        struct ovn_port *op_r = op->peer;
> > +
> > +        if (lrp_is_l3dgw(op_r)) {
> > +            ds_clear(&match);
> > +            ds_put_format(&match,
> > +                          REGBIT_EXT_ARP" == 1 &&
> is_chassis_resident(%s)",
> > +                          op_r->cr_port->json_key);
> > +            sset_add(&resident_check, ds_cstr(&match));
> > +        }
> > +    }
> > +
> > +    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) {
> > +                ds_clear(&match);
> > +                ds_put_format(&match,
> > +                              REGBIT_EXT_ARP
> > +                              " == 1 && is_chassis_resident(\"%s\")",
> > +                              ent->nb->logical_port);
> > +                sset_add(&resident_check, ds_cstr(&match));
> > +            }
> > +        }
> > +    }
> > +
> > +    if (!sset_is_empty(&port_check) && !sset_is_empty(&resident_check))
> {
> > +        const char *item;
> > +
> > +        SSET_FOR_EACH (item, &port_check) {
> > +            ovn_lflow_add(lflows, od, S_SWITCH_IN_CHECK_PORT_SEC, 75,
> > +                          item, REGBIT_EXT_ARP" = 1; next;",
> > +                          ar->lflow_ref);
> > +        }
> > +
> > +        SSET_FOR_EACH (item, &resident_check) {
> > +            ovn_lflow_add(lflows, od, S_SWITCH_IN_APPLY_PORT_SEC, 75,
> > +                          item, "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(&port_check);
> > +    sset_destroy(&resident_check);
> > +}
> > +
> >  static bool
> >  is_vlan_transparent(const struct ovn_datapath *od)
> >  {
> > @@ -13594,10 +13699,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);
>
> Have you done any evaluation of the impact of these new flows added to OvS
> at scale?
> (the same apply for each flow where you removed 'is_chassis_resident()')
>
> > -            }
> >              const char *actions_s = REGBIT_LOOKUP_NEIGHBOR_RESULT
> >                                " = lookup_arp(inport, arp.spa, arp.sha);
> "
> >                                REGBIT_LOOKUP_NEIGHBOR_IP_RESULT" = 1;"
> > @@ -13614,10 +13715,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;",
> > @@ -17532,6 +17629,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;
> > @@ -17714,6 +17812,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;
> > @@ -17870,6 +17969,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);
>

 Now that logical switch datapaths can be created/deleted by the
incremental processor on logical switch this call to
ovn_datapaths_find_by_index will crash on router deletion. if the
northd_handler deletes the logical switch datapath from the vector but the
ls_arp_northd_handler returns EN_UNCHANGED (in the current iteration) and
when the lflows get regenerated the ls_arp_rec with the stale index still
exists but the datapath does not


> +                    build_lswitch_arp_chassis_resident(od, lsi->lflows,
> > +                                                       ls_arp_rec);
> > +                }
> > +            }
> > +
> >              lsi->thread_lflow_counter = thread_lflow_counter;
> >          }
> >          post_completed_work(control);
> > @@ -17918,6 +18030,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,
> > @@ -17954,6 +18067,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 =
> > @@ -17988,6 +18102,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;
> > @@ -18000,6 +18115,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,
> > @@ -18095,6 +18211,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);
> >      }
> > @@ -18178,6 +18300,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,
> > @@ -18603,6 +18726,40 @@ 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 *ar = hmapx_node->data;
> > +        const struct ovn_datapath *od =
> > +            ovn_datapaths_find_by_index(lflow_input->ls_datapaths,
> > +                                        ar->ls_index);
> > +        lflow_ref_unlink_lflows(ar->lflow_ref);
> > +
> > +        /* Generate new lflows. */
> > +        build_lswitch_arp_chassis_resident(od, lflows, ar);
> > +
> > +        /* Sync the new flows to SB. */
> > +        bool handled = lflow_ref_sync_lflows(
> > +            ar->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;
> > +        }
> > +    }
> > +
> > +    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 8f865e8b3..d75883612 100644
> > --- a/northd/northd.h
> > +++ b/northd/northd.h
> > @@ -20,12 +20,14 @@
> >  #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 "openvswitch/hmap.h"
> >  #include "simap.h"
> >  #include "ovs-thread.h"
> >  #include "en-lr-stateful.h"
> > +#include "en-ls-arp.h"
> >  #include "vec.h"
> >  #include "datapath-sync.h"
> >
> > @@ -253,6 +255,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;
> > @@ -445,6 +448,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;
> > @@ -922,6 +928,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.c b/northd/ovn-northd.c
> > index 62da4a3aa..ab9339369 100644
> > --- a/northd/ovn-northd.c
> > +++ b/northd/ovn-northd.c
> > @@ -989,6 +989,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 b2b9f092c..57372a1e6 100644
> > --- a/tests/ovn-northd.at
> > +++ b/tests/ovn-northd.at
> > @@ -7274,10 +7274,10 @@ AT_CHECK([grep lr_in_admission lrflows | grep
> cr-DR | ovn_strip_lflows], [0], [d
> >    table=??(lr_in_admission    ), priority=50   , match=(eth.dst ==
> 04:ac:10:01:00:01 && inport == "DR-S3" && is_chassis_resident("cr-DR-S3")),
> action=(xreg0[[0..47]] = 04:ac:10:01:00:01; next;)
> >  ])
> >  # 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;)
> > +AT_CHECK([grep lr_in_lookup_neighbor lrflows | grep DR-S. |
> 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), 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), 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), action=(reg9[[2]] =
> lookup_arp(inport, arp.spa, arp.sha); next;)
> >  ])
> >  # Check the flows in lr_in_gw_redirect stage
> >  AT_CHECK([grep lr_in_gw_redirect lrflows | grep cr-DR |
> ovn_strip_lflows], [0], [dnl
> > @@ -13902,7 +13902,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;)
> > diff --git a/tests/ovn.at b/tests/ovn.at
> > index 292ca0dae..926efde08 100644
> > --- a/tests/ovn.at
> > +++ b/tests/ovn.at
> > @@ -43872,3 +43872,160 @@ wait_for_ports_up sw0-vir
> >  OVN_CLEANUP([hv1])
> >  AT_CLEANUP
> >  ])
> > +
> > +AT_SETUP([GARP delivery: gw and extenal ports])
> > +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 \
> > +    ofport_request=1
> > +
> > +# 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 \
> > +    ofport_request=1
> > +
> > +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=0200000000ee
> > +tha=000000000000
> > +spa=`ip_to_hex 192 168 1 222`
> > +tpa=$spa
> > +garp=ffffffffffff${sha}08060001080006040001${sha}${spa}${tha}${tpa}
>
> Packets are supposed to be created with 'fmt_pkt'.
>
> > +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=0200000000ee
> > +tha=000000000000
> > +spa=`ip_to_hex 192 168 1 223`
> > +tpa=$spa
> > +garp=ffffffffffff${sha}08060001080006040001${sha}${spa}${tha}${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=f00000000001
> > +tha=000000000000
> > +spa=`ip_to_hex 192 168 1 111`
> > +tpa=000000000000
> > +garp=ffffffffffff${sha}08060001080006040001${sha}${spa}${tha}${tpa}
> > +as hv1 ovs-appctl netdev-dummy/receive vif1 $garp
> > +
> > +sha=f00000000002
> > +tha=000000000000
> > +spa=`ip_to_hex 192 168 1 112`
> > +tpa=000000000000
> > +garp=ffffffffffff${sha}08060001080006040001${sha}${spa}${tha}${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=f00000000001
> > +tha=000000000000
> > +spa=`ip_to_hex 192 168 1 113`
> > +tpa=000000000000
> > +garp=ffffffffffff${sha}08060001080006040001${sha}${spa}${tha}${tpa}
> > +as hv1 ovs-appctl netdev-dummy/receive vif1 $garp
> > +
> > +sha=f00000000002
> > +tha=000000000000
> > +spa=`ip_to_hex 192 168 1 114`
> > +tpa=000000000000
> > +garp=ffffffffffff${sha}08060001080006040001${sha}${spa}${tha}${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.49.0
> >
> > _______________________________________________
> > dev mailing list
> > d...@openvswitch.org
> > https://mail.openvswitch.org/mailman/listinfo/ovs-dev
> >

_______________________________________________
> dev mailing list
> d...@openvswitch.org
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev


Thanks,
Jacob Tanenbaum

On Fri, Sep 19, 2025 at 12:48 PM Lorenzo Bianconi via dev <
ovs-dev@openvswitch.org> wrote:

> > 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 <arukomoinikova@k2.cloud>
> > Signed-off-by: Aleksandr Smirnov <alekssmirnov@k2.cloud>
> > Signed-off-by: Alexandra Rukomoinikova <arukomoinikova@k2.cloud>
>
> Hi Aleksandr and Alexandra,
>
> thx for the patches. Some comments and questions below.
>
> Regards,
> Lorenzo
>
> >
> > ---
> > v2: Minor fixes, add co-authored-by
> > v3: Apply Dumitru comments, code rework, move part of processing
> >     into new I-P node.
> > ---
> >  lib/stopwatch-names.h    |   1 +
> >  northd/automake.mk       |   2 +
> >  northd/en-lflow.c        |  29 ++++
> >  northd/en-lflow.h        |   2 +
> >  northd/en-ls-arp.c       | 284 +++++++++++++++++++++++++++++++++++++++
> >  northd/en-ls-arp.h       | 104 ++++++++++++++
> >  northd/inc-proc-northd.c |   8 ++
> >  northd/northd.c          | 179 ++++++++++++++++++++++--
> >  northd/northd.h          |  10 ++
> >  northd/ovn-northd.c      |   1 +
> >  tests/ovn-northd.at      |  10 +-
> >  tests/ovn.at             | 157 ++++++++++++++++++++++
> >  12 files changed, 771 insertions(+), 16 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 f903f5e3a..772daf623 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"
> > @@ -60,6 +61,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 =
> > @@ -84,6 +87,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 =
> > @@ -215,6 +219,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..fab778d03 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 *node, void *data);
> > +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..26f2f815e
> > --- /dev/null
> > +++ b/northd/en-ls-arp.c
> > @@ -0,0 +1,284 @@
> > +/*
> > +  * Copyright (c) 2024, Red Hat, Inc.
> > + *
> > + * Licensed under the Apache License, Version 2.0 (the "License");
> > + * you may not use this file except in compliance with the License.
> > + * You may obtain a copy of the License at:
> > + *
> > + *     http://www.apache.org/licenses/LICENSE-2.0
> > + *
> > + * Unless required by applicable law or agreed to in writing, software
> > + * distributed under the License is distributed on an "AS IS" BASIS,
> > + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> > + * See the License for the specific language governing permissions and
> > + * limitations under the License.
> > + */
> > +
> > +#include <config.h>
> > +
> > +/* 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 hmap *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.entries,
> > +    };
> > +}
> > +
> > +static void
> > +ls_arp_table_clear(struct ls_arp_table *table)
> > +{
> > +    struct ls_arp_record *ar;
> > +    HMAP_FOR_EACH_POP (ar, key_node, &table->entries) {
> > +        lflow_ref_destroy(ar->lflow_ref);
> > +        hmapx_destroy(&ar->nat_records);
> > +        free(ar);
> > +    }
> > +}
> > +
> > +/*
> > + *   Return hmapx (odmap) of datapaths, assumed lswitches,
> > + *   that are gateways for given nat.
> > + */
> > +static void
> > +nat_odmap_create(struct lr_nat_record *nr,
> > +                 struct hmapx *odmap)
> > +{
> > +    for (size_t i = 0; i < nr->n_nat_entries; i++) {
> > +        struct ovn_nat *ent = &nr->nat_entries[i];
> > +
> > +        if (ent->is_valid
> > +            && ent->l3dgw_port
> > +            && ent->l3dgw_port->peer
> > +            && ent->l3dgw_port->peer->od
> > +            && !ent->is_distributed) {
> > +            hmapx_add(odmap, ent->l3dgw_port->peer->od);
> > +        }
> > +    }
> > +}
> > +
> > +static bool
> > +ods_find_by_index(struct hmapx *odmap, size_t index)
> > +{
> > +    struct hmapx_node *hmapx_node;
> > +    HMAPX_FOR_EACH (hmapx_node, odmap) {
> > +        struct ovn_datapath *od = hmapx_node->data;
> > +
> > +        if (od->index == index) {
> > +            return true;
> > +        }
> > +    }
> > +
> > +    return false;
> > +}
> > +
> > +static struct ls_arp_record*
> > +ars_find_by_index(const struct ls_arp_table *table, size_t index)
> > +{
> > +    struct ls_arp_record *ar;
> > +    HMAP_FOR_EACH (ar, key_node, &table->entries) {
> > +        if (ar->ls_index == index) {
> > +            return ar;
> > +        }
> > +    }
> > +
> > +    return NULL;
> > +}
> > +
> > +/*
> > + * Fill nat_records in given ls_arp_record with nat records that have
> > + * lswitch, owned by arp record, as nat gateway.
> > + */
> > +static void
> > +ls_arp_record_set_nats(struct ls_arp_record *ar,
> > +                       const struct hmap *nats)
> > +{
> > +    hmapx_init(&ar->nat_records);
> > +
> > +    struct lr_nat_record *nr;
> > +    HMAP_FOR_EACH (nr, key_node, nats) {
> > +        struct hmapx ods = HMAPX_INITIALIZER(&ods);
> > +
> > +        nat_odmap_create(nr, &ods);
> > +
> > +        if (ods_find_by_index(&ods, ar->ls_index)) {
> > +            hmapx_add(&ar->nat_records, nr);
> > +        }
> > +
> > +        hmapx_destroy(&ods);
> > +    }
> > +}
> > +
> > +static struct ls_arp_record *
> > +ls_arp_record_create(struct ls_arp_table *table,
> > +                     const struct ovn_datapath *od,
> > +                     const struct hmap *lr_nats)
> > +{
> > +    struct ls_arp_record *ar = xzalloc(sizeof *ar);
> > +
> > +    ar->ls_index = od->index;
> > +    ls_arp_record_set_nats(ar, lr_nats);
> > +    ar->lflow_ref = lflow_ref_create();
> > +
> > +    hmap_insert(&table->entries, &ar->key_node,
> > +                uuid_hash(&od->nbs->header_.uuid));
> > +
> > +    return ar;
> > +}
> > +
> > +/* 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);
> > +
> > +    return data;
> > +}
> > +
> > +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);
> > +}
> > +
> > +void
> > +en_ls_arp_clear_tracked_data(void *data_)
> > +{
> > +    struct ed_type_ls_arp *data = data_;
> > +    hmapx_clear(&data->trk_data.crupdated);
> > +}
> > +
> > +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) {
> > +        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 *x OVS_UNUSED)
> > +{
> > +    struct northd_data *northd_data = engine_get_input_data("northd",
> node);
> > +    if (!northd_has_tracked_data(&northd_data->trk_data)) {
> > +        return EN_UNHANDLED;
> > +    }
>
> Now we support IP for logical-switch creation/deletion. Should we take
> care of
> it too?
>
> > +
> > +    return EN_HANDLED_UNCHANGED;
> > +}
> > +
> > +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);
> > +
> > +    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;
> > +    HMAPX_FOR_EACH (hmapx_node, &lr_nat_data->trk_data.crupdated) {
> > +        struct lr_nat_record *nr_cur = hmapx_node->data;
> > +        struct hmapx ods = HMAPX_INITIALIZER(&ods);
> > +
> > +        /* Collect all ods (lswitch) that are gateways for given nat */
> > +        nat_odmap_create(nr_cur, &ods);
> > +
> > +        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);
> > +
> > +            /* current nat record is already registered for given
> arp_record */
> > +            if (nr_node) {
> > +                /* trigger this arp_record to regenerate od lflow */
> > +                hmapx_add(&data->trk_data.crupdated, ar);
> > +
> > +                /* ... but not part of affected ods anymore,
> > +                   i.e. the change of the nat removes this gateway */
> > +                if (!ods_find_by_index(&ods, ar->ls_index)) {
> > +                    hmapx_delete(&ar->nat_records, nr_node);
> > +                }
> > +            }
> > +        }
> > +
> > +        /* Process gateways that are new */
> > +        struct hmapx_node *hmapx_node2;
> > +        HMAPX_FOR_EACH (hmapx_node2, &ods) {
> > +            struct ovn_datapath *od = hmapx_node2->data;
> > +
> > +            /* Determine which arp_record is affected */
> > +            ar = ars_find_by_index(&data->table, od->index);
> > +            ovs_assert(ar);
> > +
> > +            /* new gateway triggers lflow regeneration for this
> arp_records */
> > +            hmapx_add(&data->trk_data.crupdated, ar);
> > +            hmapx_add(&ar->nat_records, nr_cur);
> > +        }
> > +
> > +        hmapx_destroy(&ods);
> > +    }
> > +
> > +
> > +    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..ecc39eb77
> > --- /dev/null
> > +++ b/northd/en-ls-arp.h
> > @@ -0,0 +1,104 @@
> > +/*
> > + * Copyright (c) 2024, Red Hat, Inc.
> > + *
> > + * Licensed under the Apache License, Version 2.0 (the "License");
> > + * you may not use this file except in compliance with the License.
> > + * You may obtain a copy of the License at:
> > + *
> > + *     http://www.apache.org/licenses/LICENSE-2.0
> > + *
> > + * Unless required by applicable law or agreed to in writing, software
> > + * distributed under the License is distributed on an "AS IS" BASIS,
> > + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> > + * See the License for the specific language governing permissions and
> > + * limitations under the License.
> > + */
> > +#ifndef 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;
> > +
> > +    /* Unique id of the logical switch.  Note : This id is assigned
> > +     * by the northd engine node for each logical switch. */
> > +    size_t ls_index;
> > +
> > +    /* 'lflow_ref' is used to reference logical flows generated for
> > +     * this ls_arp record.
> > +     *
> > +     * This data is initialized and destroyed by the en_ls_arp node,
> > +     * but populated and used only by the en_lflow node. Ideally this
> data
> > +     * should be maintained as part of en_lflow's data.  However, it
> would
> > +     * be less efficient and more complex:
> > +     *
> > +     * 1. It would require an extra search (using the index) to find the
> > +     * lflows.
> > +     *
> > +     * 2. Building the index needs to be thread-safe, using either a
> global
> > +     * lock which is obviously less efficient, or hash-based lock array
> which
> > +     * is more complex.
> > +     *
> > +     * Adding the lflow_ref here is more straightforward. The drawback
> is that
> > +     * we need to keep in mind that this data belongs to en_lflow node,
> so
> > +     * never access it from any other nodes.
> > +     *
> > +     * Note: lflow_ref is not thread safe.  Only one thread should
> > +     * access ls_arp_record->lflow_ref at any given time.
> > +     */
> > +    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; /* Stores 'struct ls_arp_record'. */
> > +};
> > +
> > +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 *data);
> > +void en_ls_arp_clear_tracked_data(void *data);
> > +enum engine_node_state en_ls_arp_run(struct engine_node *, void *data);
> > +
> > +enum engine_input_handler_result
> > +ls_arp_lr_nat_handler(struct engine_node *node, void *data);
> > +enum engine_input_handler_result
> > +ls_arp_northd_handler(struct engine_node *node, void *data);
> > +
> > +static inline bool
> > +ls_arp_has_tracked_data(struct ls_arp_tracked_data *trk_data) {
> > +    return !hmapx_is_empty(&trk_data->crupdated);
> > +}
> > +
> > +#endif /* EN_LS_ARP_H */
> > +
> > diff --git a/northd/inc-proc-northd.c b/northd/inc-proc-northd.c
> > index bfb893a1a..8068123f0 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"
> > @@ -171,6 +172,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);
> > @@ -296,6 +298,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);
> > @@ -390,6 +395,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 e0a329a17..33a3ff9b6 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"
> > @@ -135,6 +136,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. */
> >
> > @@ -532,6 +534,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 *);
> > @@ -569,6 +572,7 @@ ovn_datapath_destroy(struct hmap *datapaths, 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);
> >          free(od);
> >      }
> > @@ -1177,6 +1181,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)
> >  {
> > @@ -1563,6 +1573,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;
> > @@ -1956,6 +1970,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
> > @@ -2014,9 +2037,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;
> > @@ -9210,6 +9231,90 @@
> 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 port_check;
> > +    struct sset resident_check;
> > +
> > +    sset_init(&port_check);
> > +    sset_init(&resident_check);
> > +
> > +    struct ds match = DS_EMPTY_INITIALIZER;
> > +
> > +    struct hmapx_node *node;
> > +    HMAPX_FOR_EACH (node, &od->ph_ports) {
> > +        struct ovn_port *op = node->data;
> > +
> > +        ds_clear(&match);
> > +        ds_put_format(&match, "(arp.op == 1 || arp.op == 2) && inport
> == %s",
> > +                      op->json_key);
> > +        sset_add(&port_check, ds_cstr(&match));
>
> Have you done any evaluation of the impact of these new flows at scale?
>
> > +    }
> > +
> > +    struct ovn_port *op;
> > +    VECTOR_FOR_EACH (&od->router_ports, op) {
> > +        struct ovn_port *op_r = op->peer;
> > +
> > +        if (lrp_is_l3dgw(op_r)) {
> > +            ds_clear(&match);
> > +            ds_put_format(&match,
> > +                          REGBIT_EXT_ARP" == 1 &&
> is_chassis_resident(%s)",
> > +                          op_r->cr_port->json_key);
> > +            sset_add(&resident_check, ds_cstr(&match));
> > +        }
> > +    }
> > +
> > +    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) {
> > +                ds_clear(&match);
> > +                ds_put_format(&match,
> > +                              REGBIT_EXT_ARP
> > +                              " == 1 && is_chassis_resident(\"%s\")",
> > +                              ent->nb->logical_port);
> > +                sset_add(&resident_check, ds_cstr(&match));
> > +            }
> > +        }
> > +    }
> > +
> > +    if (!sset_is_empty(&port_check) && !sset_is_empty(&resident_check))
> {
> > +        const char *item;
> > +
> > +        SSET_FOR_EACH (item, &port_check) {
> > +            ovn_lflow_add(lflows, od, S_SWITCH_IN_CHECK_PORT_SEC, 75,
> > +                          item, REGBIT_EXT_ARP" = 1; next;",
> > +                          ar->lflow_ref);
> > +        }
> > +
> > +        SSET_FOR_EACH (item, &resident_check) {
> > +            ovn_lflow_add(lflows, od, S_SWITCH_IN_APPLY_PORT_SEC, 75,
> > +                          item, "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(&port_check);
> > +    sset_destroy(&resident_check);
> > +}
> > +
> >  static bool
> >  is_vlan_transparent(const struct ovn_datapath *od)
> >  {
> > @@ -13594,10 +13699,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);
>
> Have you done any evaluation of the impact of these new flows added to OvS
> at scale?
> (the same apply for each flow where you removed 'is_chassis_resident()')
>
> > -            }
> >              const char *actions_s = REGBIT_LOOKUP_NEIGHBOR_RESULT
> >                                " = lookup_arp(inport, arp.spa, arp.sha);
> "
> >                                REGBIT_LOOKUP_NEIGHBOR_IP_RESULT" = 1;"
> > @@ -13614,10 +13715,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;",
> > @@ -17532,6 +17629,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;
> > @@ -17714,6 +17812,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;
> > @@ -17870,6 +17969,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);
> > @@ -17918,6 +18030,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,
> > @@ -17954,6 +18067,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 =
> > @@ -17988,6 +18102,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;
> > @@ -18000,6 +18115,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,
> > @@ -18095,6 +18211,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);
> >      }
> > @@ -18178,6 +18300,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,
> > @@ -18603,6 +18726,40 @@ 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 *ar = hmapx_node->data;
> > +        const struct ovn_datapath *od =
> > +            ovn_datapaths_find_by_index(lflow_input->ls_datapaths,
> > +                                        ar->ls_index);
> > +        lflow_ref_unlink_lflows(ar->lflow_ref);
> > +
> > +        /* Generate new lflows. */
> > +        build_lswitch_arp_chassis_resident(od, lflows, ar);
> > +
> > +        /* Sync the new flows to SB. */
> > +        bool handled = lflow_ref_sync_lflows(
> > +            ar->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;
> > +        }
> > +    }
> > +
> > +    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 8f865e8b3..d75883612 100644
> > --- a/northd/northd.h
> > +++ b/northd/northd.h
> > @@ -20,12 +20,14 @@
> >  #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 "openvswitch/hmap.h"
> >  #include "simap.h"
> >  #include "ovs-thread.h"
> >  #include "en-lr-stateful.h"
> > +#include "en-ls-arp.h"
> >  #include "vec.h"
> >  #include "datapath-sync.h"
> >
> > @@ -253,6 +255,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;
> > @@ -445,6 +448,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;
> > @@ -922,6 +928,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.c b/northd/ovn-northd.c
> > index 62da4a3aa..ab9339369 100644
> > --- a/northd/ovn-northd.c
> > +++ b/northd/ovn-northd.c
> > @@ -989,6 +989,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 b2b9f092c..57372a1e6 100644
> > --- a/tests/ovn-northd.at
> > +++ b/tests/ovn-northd.at
> > @@ -7274,10 +7274,10 @@ AT_CHECK([grep lr_in_admission lrflows | grep
> cr-DR | ovn_strip_lflows], [0], [d
> >    table=??(lr_in_admission    ), priority=50   , match=(eth.dst ==
> 04:ac:10:01:00:01 && inport == "DR-S3" && is_chassis_resident("cr-DR-S3")),
> action=(xreg0[[0..47]] = 04:ac:10:01:00:01; next;)
> >  ])
> >  # 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;)
> > +AT_CHECK([grep lr_in_lookup_neighbor lrflows | grep DR-S. |
> 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), 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), 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), action=(reg9[[2]] =
> lookup_arp(inport, arp.spa, arp.sha); next;)
> >  ])
> >  # Check the flows in lr_in_gw_redirect stage
> >  AT_CHECK([grep lr_in_gw_redirect lrflows | grep cr-DR |
> ovn_strip_lflows], [0], [dnl
> > @@ -13902,7 +13902,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;)
> > diff --git a/tests/ovn.at b/tests/ovn.at
> > index 292ca0dae..926efde08 100644
> > --- a/tests/ovn.at
> > +++ b/tests/ovn.at
> > @@ -43872,3 +43872,160 @@ wait_for_ports_up sw0-vir
> >  OVN_CLEANUP([hv1])
> >  AT_CLEANUP
> >  ])
> > +
> > +AT_SETUP([GARP delivery: gw and extenal ports])
> > +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 \
> > +    ofport_request=1
> > +
> > +# 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 \
> > +    ofport_request=1
> > +
> > +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=0200000000ee
> > +tha=000000000000
> > +spa=`ip_to_hex 192 168 1 222`
> > +tpa=$spa
> > +garp=ffffffffffff${sha}08060001080006040001${sha}${spa}${tha}${tpa}
>
> Packets are supposed to be created with 'fmt_pkt'.
>
> > +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=0200000000ee
> > +tha=000000000000
> > +spa=`ip_to_hex 192 168 1 223`
> > +tpa=$spa
> > +garp=ffffffffffff${sha}08060001080006040001${sha}${spa}${tha}${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=f00000000001
> > +tha=000000000000
> > +spa=`ip_to_hex 192 168 1 111`
> > +tpa=000000000000
> > +garp=ffffffffffff${sha}08060001080006040001${sha}${spa}${tha}${tpa}
> > +as hv1 ovs-appctl netdev-dummy/receive vif1 $garp
> > +
> > +sha=f00000000002
> > +tha=000000000000
> > +spa=`ip_to_hex 192 168 1 112`
> > +tpa=000000000000
> > +garp=ffffffffffff${sha}08060001080006040001${sha}${spa}${tha}${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=f00000000001
> > +tha=000000000000
> > +spa=`ip_to_hex 192 168 1 113`
> > +tpa=000000000000
> > +garp=ffffffffffff${sha}08060001080006040001${sha}${spa}${tha}${tpa}
> > +as hv1 ovs-appctl netdev-dummy/receive vif1 $garp
> > +
> > +sha=f00000000002
> > +tha=000000000000
> > +spa=`ip_to_hex 192 168 1 114`
> > +tpa=000000000000
> > +garp=ffffffffffff${sha}08060001080006040001${sha}${spa}${tha}${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.49.0
> >
> > _______________________________________________
> > dev mailing list
> > d...@openvswitch.org
> > https://mail.openvswitch.org/mailman/listinfo/ovs-dev
> >
> _______________________________________________
> dev mailing list
> d...@openvswitch.org
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev
>
_______________________________________________
dev mailing list
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to