On Mon, Oct 19, 2026 at 2:25 PM Srikar Tati via dev
<[email protected]> wrote:
>
> From: Srikar Tati <[email protected]>
>
> An EVPN FDB entry with more than one path (multi-homing) uses an
> OpenFlow select group with selection_method=dp_hash.  dp_hash needs a
> recirculation, which the tc datapath cannot offload, so the group stays
> in software on a NIC with hardware offload.
>
> Add the "dynamic-routing-fdb-ecmp-selection-fields" option to Logical
> Switches.  It takes the same fields as the "selection_fields" column of
> a static route.  northd validates the option and copies it to the
> Datapath_Binding of an EVPN switch.  ovn-controller then creates the
> FDB select groups with selection_method=hash on these fields.  tp_src
> and tp_dst become the TCP, UDP and SCTP ports, because one group serves
> every protocol and ovs-vswitchd hashes only the fields that are present
> in a packet.
>
> evpn_fdb now also depends on SB Datapath_Binding, so that a change of
> the option updates the groups.
>
> Assisted-by: Claude Opus 5.5, Claude Code
> Signed-off-by: Srikar Tati <[email protected]>

Hi Srikar,

Thanks for the patches.  Please see a few comments below.

Thanks
Numan

> ---
>  NEWS                                |  4 +++
>  controller/evpn-fdb.c               | 33 +++++++++++++++++++++
>  controller/evpn-fdb.h               |  3 ++
>  controller/ovn-controller.c         | 46 +++++++++++++++++++++++++++++
>  controller/physical.c               |  7 ++++-
>  lib/ovn-util.c                      | 31 +++++++++++++++++++
>  lib/ovn-util.h                      |  2 ++
>  northd/en-datapath-logical-switch.c | 19 ++++++++++++
>  ovn-nb.xml                          | 25 ++++++++++++++++
>  tests/ovn-inc-proc-graph-dump.at    |  2 ++
>  tests/ovn-northd.at                 | 42 ++++++++++++++++++++++++++
>  tests/system-ovn.at                 | 29 ++++++++++++++++++
>  12 files changed, 242 insertions(+), 1 deletion(-)
>
> diff --git a/NEWS b/NEWS
> index 5a822cc21..cfb331a45 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -32,6 +32,10 @@ Post v26.09.0
>         Logical Routers.  It selects the fields that ECMP groups of learned
>         routes hash on, the same as the "selection_fields" column of a
>         static route.
> +     * Add the "other_config:dynamic-routing-fdb-ecmp-selection-fields"
> +       option to Logical Switches.  It selects the fields that the
> +       OpenFlow groups of multi-path EVPN FDB entries hash on, instead of
> +       dp_hash.
>     - Removed OVN's ovs-bugtool plugin and helper scripts.
>     - Removed ovn-sim utility scripts.
>     - Removed ovn-docker utility scripts.
> diff --git a/controller/evpn-fdb.c b/controller/evpn-fdb.c
> index 76ab816f3..acb97ab8f 100644
> --- a/controller/evpn-fdb.c
> +++ b/controller/evpn-fdb.c
> @@ -24,6 +24,7 @@
>  #include "openvswitch/dynamic-string.h"
>  #include "openvswitch/vlog.h"
>  #include "ovn-sb-idl.h"
> +#include "ovn-util.h"
>  #include "packets.h"
>  #include "unixctl.h"
>
> @@ -125,6 +126,27 @@ evpn_fdb_resolve_paths(const struct evpn_fdb_ctx_in 
> *f_ctx_in,
>      vector_qsort(paths, evpn_fdb_path_cmp);
>  }
>
> +/* Returns the fields of the ECMP select group of the FDB entries of 'edp'
> + * for selection_method=hash, or NULL for selection_method=dp_hash. */
> +static char *
Please put a comment that the caller has to free the returned string.



> +evpn_fdb_hash_fields(const struct evpn_datapath *edp)
> +{
> +    const char *fields_s =
> +        smap_get(&edp->ldp->datapath->external_ids,
> +                 "dynamic-routing-fdb-ecmp-selection-fields");
> +    if (!fields_s) {
> +        return NULL;
> +    }
> +
> +    struct sset fields = SSET_INITIALIZER(&fields);
> +    char *hash_fields = NULL;
> +    if (ovn_ecmp_selection_fields_parse(fields_s, &fields)) {
> +        hash_fields = ovn_ecmp_selection_fields_to_hash_fields(&fields);
> +    }
> +    sset_destroy(&fields);
> +    return hash_fields;
> +}
> +
>  /* Returns true if the paths in 'fdb' match the contents of 'paths'.
>   * Caller must ensure 'paths' is non-empty (memcmp with NULL is UB). */
>  static bool
> @@ -192,6 +214,15 @@ evpn_fdb_run(const struct evpn_fdb_ctx_in *f_ctx_in,
>              updated = true;
>          }
>
> +        char *hash_fields = evpn_fdb_hash_fields(edp);
> +        if (!nullable_string_is_equal(fdb->hash_fields, hash_fields)) {
> +            free(fdb->hash_fields);
> +            fdb->hash_fields = hash_fields;
> +            updated = true;
> +        } else {
> +            free(hash_fields);
> +        }
> +
>          if (updated) {
>              hmapx_add(f_ctx_out->updated_fdbs, fdb);
>          }
> @@ -208,6 +239,7 @@ evpn_fdb_run(const struct evpn_fdb_ctx_in *f_ctx_in,
>          uuidset_insert(f_ctx_out->removed_fdbs, &fdb->flow_uuid);
>          hmap_remove(f_ctx_out->fdbs, &fdb->hmap_node);
>          vector_destroy(&fdb->paths);
> +        free(fdb->hash_fields);
>          free(fdb);
>      }
>
> @@ -220,6 +252,7 @@ evpn_fdbs_destroy(struct hmap *fdbs)
>      struct evpn_fdb *fdb;
>      HMAP_FOR_EACH_POP (fdb, hmap_node, fdbs) {
>          vector_destroy(&fdb->paths);
> +        free(fdb->hash_fields);
>          free(fdb);
>      }
>      hmap_destroy(fdbs);
> diff --git a/controller/evpn-fdb.h b/controller/evpn-fdb.h
> index 65e1fc4fd..6ed3a343e 100644
> --- a/controller/evpn-fdb.h
> +++ b/controller/evpn-fdb.h
> @@ -65,6 +65,9 @@ struct evpn_fdb {
>      /* Contains 'struct evpn_fdb_path', one per ECMP path.
>       * For single-path entries len == 1. */
>      struct vector paths;
> +    /* Fields of the ECMP select group for selection_method=hash, NULL for
> +     * selection_method=dp_hash. */
> +    char *hash_fields;
>  };
>
>  void evpn_fdb_run(const struct evpn_fdb_ctx_in *, struct evpn_fdb_ctx_out *);
> diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
> index d57ff316d..84842c2c7 100644
> --- a/controller/ovn-controller.c
> +++ b/controller/ovn-controller.c
> @@ -6967,6 +6967,47 @@ evpn_fdb_vtep_binding_handler(struct engine_node 
> *node, void *data OVS_UNUSED)
>      return EN_UNHANDLED;
>  }
>
> +static enum engine_input_handler_result
> +evpn_fdb_datapath_binding_handler(struct engine_node *node,
> +                                  void *data OVS_UNUSED)
> +{
> +    const struct sbrec_datapath_binding_table *dp_table =
> +        EN_OVSDB_GET(engine_get_input("SB_datapath_binding", node));
> +    const struct ed_type_runtime_data *rt_data =
> +        engine_get_input_data("runtime_data", node);
> +
> +    const struct sbrec_datapath_binding *dp;
> +    SBREC_DATAPATH_BINDING_TABLE_FOR_EACH_TRACKED (dp, dp_table) {
> +        if (sbrec_datapath_binding_is_new(dp) ||
> +            sbrec_datapath_binding_is_deleted(dp)) {
> +            /* The removal and addition is handled via the
> +             * en_neighbor_exchange I-P node. */
> +            continue;
> +        }

> +
> +        struct local_datapath *ld =
> +            get_local_datapath(&rt_data->local_datapaths, dp->tunnel_key);
> +        if (!ld || !ld->is_switch) {
> +            continue;
> +        }
> +
> +        int64_t vni = ovn_smap_get_llong(&dp->external_ids,
> +                                         "dynamic-routing-vni", -1);
> +        if (!ovn_is_valid_vni(vni)) {
> +            continue;
> +        }
> +
> +        /* The ECMP selection fields of the FDB groups are stored in
> +         * external_ids. */
> +        if (sbrec_datapath_binding_is_updated(
> +                dp, SBREC_DATAPATH_BINDING_COL_EXTERNAL_IDS)) {
> +            return EN_UNHANDLED;
> +        }
> +    }


> +
> +    return EN_HANDLED_UNCHANGED;
> +}
> +
>  static void *
>  en_evpn_arp_init(struct engine_node *node OVS_UNUSED,
>                   struct engine_arg *arg OVS_UNUSED)
> @@ -7621,6 +7662,11 @@ inc_proc_ovn_controller_init(
>      engine_add_input(&en_evpn_fdb, &en_evpn_vtep_binding,
>                       evpn_fdb_vtep_binding_handler);
>      engine_add_input(&en_evpn_fdb, &en_nexthop_exchange, NULL);
> +    /* The runtime_data are needed only for local datapaths, any update of
> +     * local datapath will be reflected via en_neighbor_exchange. */
> +    engine_add_input(&en_evpn_fdb, &en_runtime_data, engine_noop_handler);
> +    engine_add_input(&en_evpn_fdb, &en_sb_datapath_binding,
> +                     evpn_fdb_datapath_binding_handler);
>
>      engine_add_input(&en_evpn_arp, &en_neighbor_exchange, NULL);
>      engine_add_input(&en_evpn_arp, &en_evpn_vtep_binding,
> diff --git a/controller/physical.c b/controller/physical.c
> index 8c5d9b940..93022d94f 100644
> --- a/controller/physical.c
> +++ b/controller/physical.c
> @@ -3652,7 +3652,12 @@ physical_consider_evpn_fdb(const struct evpn_fdb *fdb,
>          const struct mf_field *mf = mf_from_id(MFF_LOG_REMOTE_OUTPORT);
>
>          ds_clear(group_ds);
> -        ds_put_cstr(group_ds, "type=select,selection_method=dp_hash");
> +        if (fdb->hash_fields) {
> +            ds_put_format(group_ds, "type=select,selection_method=hash,"
> +                          "fields(%s)", fdb->hash_fields);
> +        } else {
> +            ds_put_cstr(group_ds, "type=select,selection_method=dp_hash");
> +        }
>
>          for (size_t i = 0; i < vector_len(&fdb->paths); i++) {
>              struct evpn_fdb_path path =
> diff --git a/lib/ovn-util.c b/lib/ovn-util.c
> index 4ec648318..9ded1c042 100644
> --- a/lib/ovn-util.c
> +++ b/lib/ovn-util.c
> @@ -1685,6 +1685,37 @@ ovn_ecmp_selection_fields_parse(const char *fields_s,
>      return ok;
>  }
>
> +/* Returns the fields of an OpenFlow select group with selection_method=hash
> + * for the ECMP 'selection_fields', as a malloc'ed string that the caller
> + * must free, or NULL if 'selection_fields' is empty.  One group serves
> + * every protocol, so tp_src and tp_dst become the ports of TCP, UDP and
> + * SCTP; ovs-vswitchd hashes only the fields that are present in a
> + * packet. */

Here too.  Put a comment that the caller has to free the returned string.

With the comments addressed:
Acked-by: Numan Siddique <[email protected]>

Numan


> +char *
> +ovn_ecmp_selection_fields_to_hash_fields(const struct sset *selection_fields)
> +{
> +    if (sset_is_empty(selection_fields)) {
> +        return NULL;
> +    }
> +
> +    struct sset of_fields = SSET_INITIALIZER(&of_fields);
> +    const char *field;
> +    SSET_FOR_EACH (field, selection_fields) {
> +        if (!strcmp(field, "tp_src") || !strcmp(field, "tp_dst")) {
> +            const char *dir = field + strlen("tp_");
> +            sset_add_and_free(&of_fields, xasprintf("tcp_%s", dir));
> +            sset_add_and_free(&of_fields, xasprintf("udp_%s", dir));
> +            sset_add_and_free(&of_fields, xasprintf("sctp_%s", dir));
> +        } else {
> +            sset_add(&of_fields, field);
> +        }
> +    }
> +
> +    char *hash_fields = sset_join(&of_fields, ",", "");
> +    sset_destroy(&of_fields);
> +    return hash_fields;
> +}
> +
>  struct sset *
>  lrp_network_sset(const char **networks, int n_networks)
>  {
> diff --git a/lib/ovn-util.h b/lib/ovn-util.h
> index fc88bf6d6..f17df69d8 100644
> --- a/lib/ovn-util.h
> +++ b/lib/ovn-util.h
> @@ -739,6 +739,8 @@ void ovn_ecmp_selection_fields_add(struct sset 
> *selection_fields,
>                                     const char *field);
>  bool ovn_ecmp_selection_fields_parse(const char *fields_s,
>                                       struct sset *selection_fields);
> +char *ovn_ecmp_selection_fields_to_hash_fields(
> +    const struct sset *selection_fields);
>
>  const struct sbrec_port_binding *lport_lookup_by_name(
>      struct ovsdb_idl_index *sbrec_port_binding_by_name,
> diff --git a/northd/en-datapath-logical-switch.c 
> b/northd/en-datapath-logical-switch.c
> index 874a608a7..9ce66cbd5 100644
> --- a/northd/en-datapath-logical-switch.c
> +++ b/northd/en-datapath-logical-switch.c
> @@ -173,6 +173,25 @@ gather_external_ids(const struct nbrec_logical_switch 
> *nbs,
>              smap_add(external_ids, "dynamic-routing-redistribute",
>                       redistribute);
>          }
> +
> +        const char *fdb_ecmp_fields =
> +            smap_get(&nbs->other_config,
> +                     "dynamic-routing-fdb-ecmp-selection-fields");
> +        if (fdb_ecmp_fields) {
> +            struct sset fields = SSET_INITIALIZER(&fields);
> +            if (ovn_ecmp_selection_fields_parse(fdb_ecmp_fields, &fields)) {
> +                smap_add(external_ids,
> +                         "dynamic-routing-fdb-ecmp-selection-fields",
> +                         fdb_ecmp_fields);
> +            } else {
> +                static struct vlog_rate_limit rl =
> +                    VLOG_RATE_LIMIT_INIT(5, 1);
> +                VLOG_WARN_RL(&rl, "bad 'dynamic-routing-fdb-ecmp-selection-"
> +                             "fields' %s on switch %s, using the default "
> +                             "selection method", fdb_ecmp_fields, nbs->name);
> +            }
> +            sset_destroy(&fields);
> +        }
>      }
>
>      /* For backwards-compatibility, also store the NB UUID in
> diff --git a/ovn-nb.xml b/ovn-nb.xml
> index 0a3154858..b67eca7fc 100644
> --- a/ovn-nb.xml
> +++ b/ovn-nb.xml
> @@ -1014,6 +1014,31 @@
>          </p>
>        </column>
>
> +      <column name="other_config"
> +              key="dynamic-routing-fdb-ecmp-selection-fields"
> +              type='{"type": "string"}'>
> +        <p>
> +          Only relevant if <ref column="other_config" 
> key="dynamic-routing-vni"
> +                                table="Logical_switch"/> is set to valid VNI.
> +        </p>
> +
> +        <p>
> +          A comma separated list of the fields that select the remote VTEP
> +          for an EVPN FDB entry with more than one path (multi-homing), for
> +          example <code>ip_proto,ip_src,ip_dst,tp_src,tp_dst</code>.  The
> +          supported fields are the same as for the
> +          <ref column="selection_fields" 
> table="Logical_Router_Static_Route"/>
> +          column of a static route.  <code>tp_src</code> and
> +          <code>tp_dst</code> select the TCP, UDP and SCTP ports.  When set,
> +          <code>ovn-controller</code> creates the OpenFlow select group of
> +          the entry with the <code>hash</code> selection method on these
> +          fields instead of <code>dp_hash</code>.  This lets a NIC offload
> +          the group, because <code>hash</code> does not need a
> +          recirculation.  If the list contains a field that is not
> +          supported, OVN uses <code>dp_hash</code>.
> +        </p>
> +      </column>
> +
>        <column name="other_config" key="dynamic-routing-redistribute"
>                type='{"type": "string"}'>
>          <p>
> diff --git a/tests/ovn-inc-proc-graph-dump.at 
> b/tests/ovn-inc-proc-graph-dump.at
> index 8348c3467..e1ef5b612 100644
> --- a/tests/ovn-inc-proc-graph-dump.at
> +++ b/tests/ovn-inc-proc-graph-dump.at
> @@ -422,6 +422,8 @@ digraph "Incremental-Processing-Engine" {
>         neighbor_exchange -> evpn_fdb [[label=""]];
>         evpn_vtep_binding -> evpn_fdb 
> [[label="evpn_fdb_vtep_binding_handler"]];
>         nexthop_exchange -> evpn_fdb [[label=""]];
> +       runtime_data -> evpn_fdb [[label="engine_noop_handler"]];
> +       SB_datapath_binding -> evpn_fdb 
> [[label="evpn_fdb_datapath_binding_handler"]];
>         evpn_arp [[style=filled, shape=box, fillcolor=white, 
> label="evpn_arp"]];
>         neighbor_exchange -> evpn_arp [[label=""]];
>         evpn_vtep_binding -> evpn_arp 
> [[label="evpn_arp_vtep_binding_handler"]];
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index 6e99e9aa4..f1ef4ddc9 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -22306,6 +22306,48 @@ OVN_CLEANUP_NORTHD
>  AT_CLEANUP
>  ])
>
> +OVN_FOR_EACH_NORTHD_NO_HV([
> +AT_SETUP([dynamic-routing - EVPN FDB ecmp selection fields])
> +AT_KEYWORDS([dynamic-routing])
> +ovn_start
> +
> +check ovn-nbctl --wait=sb ls-add ls-evpn \
> +    -- set logical_switch ls-evpn \
> +       other_config:dynamic-routing-fdb-ecmp-selection-fields="ip_src,ip_dst"
> +dp=$(fetch_column datapath_binding _uuid external_ids:name=ls-evpn)
> +
> +# The option is only synced for an EVPN switch.
> +AT_CHECK([ovn-sbctl get datapath_binding $dp \
> +    external_ids:dynamic-routing-fdb-ecmp-selection-fields], [1], [ignore],
> +    [ignore])
> +
> +check ovn-nbctl --wait=sb set logical_switch ls-evpn \
> +    other_config:dynamic-routing-vni=10
> +AT_CHECK([ovn-sbctl get datapath_binding $dp \
> +    external_ids:dynamic-routing-fdb-ecmp-selection-fields], [0], [dnl
> +"ip_src,ip_dst"
> +])
> +
> +check ovn-nbctl --wait=sb set logical_switch ls-evpn \
> +    other_config:dynamic-routing-fdb-ecmp-selection-fields="ip_src,tp_src"
> +AT_CHECK([ovn-sbctl get datapath_binding $dp \
> +    external_ids:dynamic-routing-fdb-ecmp-selection-fields], [0], [dnl
> +"ip_src,tp_src"
> +])
> +
> +# A field that is not supported is not synced.
> +check ovn-nbctl --wait=sb set logical_switch ls-evpn \
> +    other_config:dynamic-routing-fdb-ecmp-selection-fields="ip_src,foo"
> +AT_CHECK([ovn-sbctl get datapath_binding $dp \
> +    external_ids:dynamic-routing-fdb-ecmp-selection-fields], [1], [ignore],
> +    [ignore])
> +check grep -q "bad 'dynamic-routing-fdb-ecmp-selection-fields' ip_src,foo" \
> +    northd/ovn-northd.log
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
> +
>  OVN_FOR_EACH_NORTHD_NO_HV([
>  AT_SETUP([LS incremental processing - other_config fields])
>  AT_KEYWORDS([ovn, incremental])
> diff --git a/tests/system-ovn.at b/tests/system-ovn.at
> index 13e62bf9b..388df9651 100644
> --- a/tests/system-ovn.at
> +++ b/tests/system-ovn.at
> @@ -18386,6 +18386,35 @@ AT_CHECK_UNQUOTED(
>  
> type=select,selection_method=dp_hash,bucket=bucket_id=0,weight:100,actions=load:2147483649->reg1[[0..31]],bucket=bucket_id=1,weight:200,actions=load:2147483650->reg1[[0..31]]:
>  GID
>  ])
>
> +# With ECMP selection fields the group uses the hash selection method,
> +# which a NIC can offload.
> +check ovn-nbctl --wait=hv set logical_switch ls-evpn \
> +    
> other_config:dynamic-routing-fdb-ecmp-selection-fields="ip_src,ip_dst,tp_src,tp_dst"
> +AT_CHECK_UNQUOTED(
> +    [ovn-appctl group-table-list | \
> +     sed 's/:[[[:space:]]]*[[0-9]]*$/: GID/'], [0], [dnl
> +type=select,selection_method=hash,fields(ip_dst,ip_proto,ip_src,sctp_dst,sctp_src,tcp_dst,tcp_src,udp_dst,udp_src),bucket=bucket_id=0,weight:100,actions=load:2147483649->reg1[[0..31]],bucket=bucket_id=1,weight:200,actions=load:2147483650->reg1[[0..31]]:
>  GID
> +])
> +OVS_WAIT_UNTIL([ovs-ofctl -O OpenFlow15 dump-groups br-int | \
> +                grep -q 'selection_method=hash'])
> +
> +# A field that is not supported keeps dp_hash.
> +check ovn-nbctl --wait=hv set logical_switch ls-evpn \
> +    other_config:dynamic-routing-fdb-ecmp-selection-fields="ip_src,foo"
> +AT_CHECK_UNQUOTED(
> +    [ovn-appctl group-table-list | \
> +     sed 's/:[[[:space:]]]*[[0-9]]*$/: GID/'], [0], [dnl
> +type=select,selection_method=dp_hash,bucket=bucket_id=0,weight:100,actions=load:2147483649->reg1[[0..31]],bucket=bucket_id=1,weight:200,actions=load:2147483650->reg1[[0..31]]:
>  GID
> +])
> +
> +check ovn-nbctl --wait=hv remove logical_switch ls-evpn \
> +    other_config dynamic-routing-fdb-ecmp-selection-fields
> +AT_CHECK_UNQUOTED(
> +    [ovn-appctl group-table-list | \
> +     sed 's/:[[[:space:]]]*[[0-9]]*$/: GID/'], [0], [dnl
> +type=select,selection_method=dp_hash,bucket=bucket_id=0,weight:100,actions=load:2147483649->reg1[[0..31]],bucket=bucket_id=1,weight:200,actions=load:2147483650->reg1[[0..31]]:
>  GID
> +])
> +
>  # Verify learning suppression flows: two ECMP drop flows added.
>  AT_CHECK_UNQUOTED(
>      [ovs-ofctl dump-flows br-int table=OFTABLE_LEARN_REMOTE_FDB | grep 
> priority | \
> --
> 2.54.0
>
> _______________________________________________
> dev mailing list
> [email protected]
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to