diff mbox series

[ovs-dev,v4] Support selection fields for ECMP routes.

Message ID 20241014162743.136480-1-karthik.c@nutanix.com
State New
Headers show
Series [ovs-dev,v4] Support selection fields for ECMP routes. | expand

Checks

Context Check Description
ovsrobot/apply-robot warning apply and check: warning
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/github-robot-_ovn-kubernetes success github build: passed

Commit Message

Karthik Chandrashekar Oct. 14, 2024, 4:27 p.m. UTC
From: Karthik Chandrashekar <karthik.c@nutanix.com>

- This patch adds the ability to specify a custom set of packet headers
  during hash computation for ECMP routes. This is similar to the support
  that was added for LB in 5af304e
- For ECMP routes OVN by default use dp_hash as a selection_method
  in the OVS flows that have the group action. With this patch when
  selection_fields is specified, the selection_method will change
  to hash with the specified list of fields used for computing the hash.
- List of fields that are used in the select action is an intersection
  of all the fields specified in each Logical_Route_Static_Route
  that is part of a given ECMP route.
- In order to allow match based on L4 port numbers, the lr_in_ip_routing
  rules have been split into separate lflows with protocol specific
  fields when src_port or dst_port is specified in the ecmp_selection_fields.
  (This is based on the requirement that pre-requisites of fields
  must be provided by any flows that output to the group)

Signed-off-by: Karthik Chandrashekar <karthik.c@nutanix.com>

---
v2:
  - Install separate logical flows for TCP and UDP in lr_in_ip_routing.
  - Add more test coverage.
---
v3:
  - Address code review comments.
  - Install separate logical flows for TCP, UDP, SCTP in lr_in_ip_routing
    only when match on src_port or dst_port is specified.
  - Update NB and SB documentation.
---
v4:
  - Address code review comments.
  - Use selection_fields column for Logical_Router_Static_Route instead
    of options.
---
 include/ovn/actions.h |   1 +
 lib/actions.c         |  44 ++++++-
 northd/northd.c       | 103 ++++++++++++++---
 northd/northd.h       |   1 +
 ovn-nb.ovsschema      |  10 +-
 ovn-nb.xml            |  26 +++++
 ovn-sb.xml            |  10 ++
 tests/ovn-northd.at   |  69 +++++++----
 tests/ovn.at          | 258 +++++++++++++++++++++++++++++++++++++++++-
 9 files changed, 478 insertions(+), 44 deletions(-)

Comments

0-day Robot Oct. 14, 2024, 4:41 p.m. UTC | #1
References:  <20241014162743.136480-1-karthik.c@nutanix.com>
 

Bleep bloop.  Greetings Karthik Chandrashekar, I am a robot and I have tried out your patch.
Thanks for your contribution.

I encountered some error that I wasn't expecting.  See the details below.


checkpatch:
WARNING: Line is 80 characters long (recommended limit is 79)
#311 FILE: northd/northd.c:11625:
            /* If tp_src and tp_dst is specified in selection_fields, replace it

WARNING: Line is 180 characters long (recommended limit is 79)
#458 FILE: ovn-sb.xml:2548:
        <dt><code><var>R</var> = select(values=<var>N1</var>[=<var>W1</var>], <var>N2</var>[=<var>W2</var>], ...; hash_felds="<var>field1</var>,<var>field2</var>,...");</code></dt>

WARNING: Line is 126 characters long (recommended limit is 79)
#481 FILE: ovn-sb.xml:2586:
            <b>Example:</b> <code>reg8[16..31] = select(values=1=20, 2=30, 3=50; hash_fields="ip_proto,src_ip,dst_ip");</code>

Lines checked: 901, Warnings: 3, Errors: 0


Please check this out.  If you feel there has been an error, please email aconole@redhat.com

Thanks,
0-day Robot
diff mbox series

Patch

diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index a95a0daf7..63a12a882 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -339,6 +339,7 @@  struct ovnact_select {
     struct ovnact_select_dst *dsts;
     size_t n_dsts;
     uint8_t ltable;             /* Logical table ID of next table. */
+    char *hash_fields;
     struct expr_field res_field;
 };
 
diff --git a/lib/actions.c b/lib/actions.c
index 2e05d4134..c5bde996b 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -1541,10 +1541,18 @@  parse_select_action(struct action_context *ctx, struct expr_field *res_field)
     struct ovnact_select_dst *dsts = NULL;
     size_t allocated_dsts = 0;
     size_t n_dsts = 0;
+    bool requires_hash_fields = false;
+    char *hash_fields = NULL;
 
     lexer_get(ctx->lexer); /* Skip "select". */
     lexer_get(ctx->lexer); /* Skip '('. */
 
+    if (lexer_match_id(ctx->lexer, "values")) {
+        lexer_force_match(ctx->lexer, LEX_T_EQUALS);
+        lexer_force_match(ctx->lexer, LEX_T_LPAREN);
+        requires_hash_fields = true;
+    }
+
     while (!lexer_match(ctx->lexer, LEX_T_RPAREN)) {
         struct ovnact_select_dst dst;
         if (!action_parse_uint16(ctx, &dst.id, "id")) {
@@ -1581,11 +1589,31 @@  parse_select_action(struct action_context *ctx, struct expr_field *res_field)
         return;
     }
 
+    if (requires_hash_fields) {
+        lexer_force_match(ctx->lexer, LEX_T_SEMICOLON);
+        if (!lexer_match_id(ctx->lexer, "hash_fields")) {
+            lexer_syntax_error(ctx->lexer, "expecting hash_fields");
+            free(dsts);
+            return;
+        }
+        if (!lexer_match(ctx->lexer, LEX_T_EQUALS) ||
+            ctx->lexer->token.type != LEX_T_STRING ||
+            lexer_lookahead(ctx->lexer) != LEX_T_RPAREN) {
+            lexer_syntax_error(ctx->lexer, "invalid hash_fields");
+            free(dsts);
+            return;
+        }
+        hash_fields = xstrdup(ctx->lexer->token.s);
+        lexer_get(ctx->lexer);
+        lexer_force_match(ctx->lexer, LEX_T_RPAREN);
+    }
+
     struct ovnact_select *select = ovnact_put_SELECT(ctx->ovnacts);
     select->ltable = ctx->pp->cur_ltable + 1;
     select->dsts = dsts;
     select->n_dsts = n_dsts;
     select->res_field = *res_field;
+    select->hash_fields = hash_fields;
 }
 
 static void
@@ -1595,6 +1623,9 @@  format_SELECT(const struct ovnact_select *select, struct ds *s)
     ds_put_cstr(s, " = ");
     ds_put_cstr(s, "select");
     ds_put_char(s, '(');
+    if (select->hash_fields) {
+        ds_put_format(s, "values=(");
+    }
     for (size_t i = 0; i < select->n_dsts; i++) {
         if (i) {
             ds_put_cstr(s, ", ");
@@ -1605,6 +1636,9 @@  format_SELECT(const struct ovnact_select *select, struct ds *s)
         ds_put_format(s, "=%"PRIu16, dst->weight);
     }
     ds_put_char(s, ')');
+    if (select->hash_fields) {
+      ds_put_format(s, "; hash_fields=\"%s\")", select->hash_fields);
+    }
     ds_put_char(s, ';');
 }
 
@@ -1619,9 +1653,14 @@  encode_SELECT(const struct ovnact_select *select,
     struct ofpact_group *og;
 
     struct ds ds = DS_EMPTY_INITIALIZER;
-    ds_put_format(&ds, "type=select,selection_method=dp_hash");
+    ds_put_format(&ds, "type=select,selection_method=%s",
+                  select->hash_fields ? "hash": "dp_hash");
+    if (select->hash_fields) {
+      ds_put_format(&ds, ",fields(%s)", select->hash_fields);
+    }
 
-    if (ovs_feature_is_supported(OVS_DP_HASH_L4_SYM_SUPPORT)) {
+    if (ovs_feature_is_supported(OVS_DP_HASH_L4_SYM_SUPPORT) &&
+        !select->hash_fields) {
         /* Select dp-hash l4_symmetric by setting the upper 32bits of
          * selection_method_param to value 1 (1 << 32): */
         ds_put_cstr(&ds, ",selection_method_param=0x100000000");
@@ -1654,6 +1693,7 @@  static void
 ovnact_select_free(struct ovnact_select *select)
 {
     free(select->dsts);
+    free(select->hash_fields);
 }
 
 static void
diff --git a/northd/northd.c b/northd/northd.c
index 0aa0de637..9b02efb89 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -302,9 +302,9 @@  BUILD_ASSERT_DECL(ACL_OBS_STAGE_MAX < (1 << 2));
  * same ip_prefix values:
  *  -  connected route overrides static one;
  *  -  static route overrides src-ip route. */
-#define ROUTE_PRIO_OFFSET_MULTIPLIER 3
-#define ROUTE_PRIO_OFFSET_STATIC 1
-#define ROUTE_PRIO_OFFSET_CONNECTED 2
+#define ROUTE_PRIO_OFFSET_MULTIPLIER 5
+#define ROUTE_PRIO_OFFSET_STATIC 2
+#define ROUTE_PRIO_OFFSET_CONNECTED 4
 
 /* Returns the type of the datapath to which a flow with the given 'stage' may
  * be added. */
@@ -11155,6 +11155,22 @@  parsed_routes_add(struct ovn_datapath *od, const struct hmap *lr_ports,
                                                  "ecmp_symmetric_reply",
                                                  false);
     new_pr->is_discard_route = is_discard_route;
+    sset_init(&new_pr->ecmp_selection_fields);
+
+    /* If tp_src or tp_dst is included in the selection_fields, implicitly
+     * include match on ip_proto. */
+    if (route->n_selection_fields) {
+        struct sset field_set = SSET_INITIALIZER(&field_set);
+        for (size_t i = 0; i < route->n_selection_fields; i++) {
+            char *field = route->selection_fields[i];
+            if (!strcmp(field, "tp_src") || !strcmp(field, "tp_dst")) {
+                sset_add(&field_set, "ip_proto");
+            }
+            sset_add(&field_set, field);
+        }
+        sset_clone(&new_pr->ecmp_selection_fields, &field_set);
+        sset_destroy(&field_set);
+    }
 
     size_t hash = uuid_hash(&od->key);
     struct parsed_route *pr = parsed_route_lookup(routes, hash, new_pr);
@@ -11162,6 +11178,7 @@  parsed_routes_add(struct ovn_datapath *od, const struct hmap *lr_ports,
         hmap_insert(routes, &new_pr->key_node, hash);
     } else {
         pr->stale = false;
+        sset_destroy(&new_pr->ecmp_selection_fields);
         free(new_pr);
     }
 }
@@ -11211,6 +11228,7 @@  struct ecmp_groups_node {
     uint32_t route_table_id;
     uint16_t route_count;
     struct ovs_list route_list; /* Contains ecmp_route_list_node */
+    struct sset selection_fields;
 };
 
 static void
@@ -11226,6 +11244,14 @@  ecmp_groups_add_route(struct ecmp_groups_node *group,
     struct ecmp_route_list_node *er = xmalloc(sizeof *er);
     er->route = route;
     er->id = ++group->route_count;
+
+    if (group->route_count == 1) {
+        sset_clone(&group->selection_fields, &route->ecmp_selection_fields);
+    } else {
+        sset_intersect(&group->selection_fields,
+                       &route->ecmp_selection_fields);
+    }
+
     ovs_list_insert(&group->route_list, &er->list_node);
 }
 
@@ -11248,6 +11274,7 @@  ecmp_groups_add(struct hmap *ecmp_groups,
     eg->is_src_route = route->is_src_route;
     eg->origin = smap_get_def(&route->route->options, "origin", "");
     eg->route_table_id = route->route_table_id;
+    sset_init(&eg->selection_fields);
     ovs_list_init(&eg->route_list);
     ecmp_groups_add_route(eg, route);
 
@@ -11280,6 +11307,7 @@  ecmp_groups_destroy(struct hmap *ecmp_groups)
             free(er);
         }
         hmap_remove(ecmp_groups, &eg->hmap_node);
+        sset_destroy(&eg->selection_fields);
         free(eg);
     }
     hmap_destroy(ecmp_groups);
@@ -11350,7 +11378,8 @@  build_route_prefix_s(const struct in6_addr *prefix, unsigned int plen)
 static void
 build_route_match(const struct ovn_port *op_inport, uint32_t rtb_id,
                   const char *network_s, int plen, bool is_src_route,
-                  bool is_ipv4, struct ds *match, uint16_t *priority, int ofs)
+                  bool is_ipv4, struct ds *match, uint16_t *priority, int ofs,
+                  bool has_protocol_match)
 {
     const char *dir;
     /* The priority here is calculated to implement longest-prefix-match
@@ -11362,14 +11391,18 @@  build_route_match(const struct ovn_port *op_inport, uint32_t rtb_id,
         dir = "dst";
     }
 
-    *priority = (plen * ROUTE_PRIO_OFFSET_MULTIPLIER) + ofs;
-
     if (op_inport) {
         ds_put_format(match, "inport == %s && ", op_inport->json_key);
     }
     if (rtb_id || ofs == ROUTE_PRIO_OFFSET_STATIC) {
         ds_put_format(match, "%s == %d && ", REG_ROUTE_TABLE_ID, rtb_id);
     }
+
+    if (has_protocol_match) {
+        ofs += 1;
+    }
+    *priority = (plen * ROUTE_PRIO_OFFSET_MULTIPLIER) + ofs;
+
     ds_put_format(match, "ip%s.%s == %s/%d", is_ipv4 ? "4" : "6", dir,
                   network_s, plen);
 }
@@ -11548,7 +11581,7 @@  add_ecmp_symmetric_reply_flows(struct lflow_table *lflows,
 static void
 build_ecmp_route_flow(struct lflow_table *lflows, struct ovn_datapath *od,
                       const struct hmap *lr_ports, struct ecmp_groups_node *eg,
-                      struct lflow_ref *lflow_ref)
+                      struct lflow_ref *lflow_ref, const char *protocol)
 
 {
     bool is_ipv4 = IN6_IS_ADDR_V4MAPPED(&eg->prefix);
@@ -11560,7 +11593,8 @@  build_ecmp_route_flow(struct lflow_table *lflows, struct ovn_datapath *od,
     int ofs = !strcmp(eg->origin, ROUTE_ORIGIN_CONNECTED) ?
         ROUTE_PRIO_OFFSET_CONNECTED: ROUTE_PRIO_OFFSET_STATIC;
     build_route_match(NULL, eg->route_table_id, prefix_s, eg->plen,
-                      eg->is_src_route, is_ipv4, &route_match, &priority, ofs);
+                      eg->is_src_route, is_ipv4, &route_match, &priority, ofs,
+                      protocol != NULL);
     free(prefix_s);
 
     struct ds actions = DS_EMPTY_INITIALIZER;
@@ -11568,18 +11602,48 @@  build_ecmp_route_flow(struct lflow_table *lflows, struct ovn_datapath *od,
                   "; %s = ", REG_ECMP_GROUP_ID, eg->id, REG_ECMP_MEMBER_ID);
 
     if (!ovs_list_is_singleton(&eg->route_list)) {
+        if (protocol && !sset_is_empty(&eg->selection_fields)) {
+            ds_put_format(&route_match, " && %s", protocol);
+        }
+
+        struct ds values = DS_EMPTY_INITIALIZER;
         bool is_first = true;
 
-        ds_put_cstr(&actions, "select(");
         LIST_FOR_EACH (er, list_node, &eg->route_list) {
             if (is_first) {
                 is_first = false;
             } else {
-                ds_put_cstr(&actions, ", ");
+                ds_put_cstr(&values, ", ");
             }
-            ds_put_format(&actions, "%"PRIu16, er->id);
+            ds_put_format(&values, "%"PRIu16, er->id);
+        }
+
+        if (!sset_is_empty(&eg->selection_fields)) {
+            struct sset field_set = SSET_INITIALIZER(&field_set);
+            sset_clone(&field_set, &eg->selection_fields);
+
+            /* If tp_src and tp_dst is specified in selection_fields, replace it
+             * with protocol specific src and dst port fields */
+            if (protocol && sset_contains(&eg->selection_fields, "tp_src")) {
+                sset_add_and_free(&field_set, xasprintf("%s_src", protocol));
+            }
+            if (protocol && sset_contains(&eg->selection_fields, "tp_dst")) {
+                sset_add_and_free(&field_set, xasprintf("%s_dst", protocol));
+            }
+            sset_find_and_delete(&field_set, "tp_src");
+            sset_find_and_delete(&field_set, "tp_dst");
+
+            char *hash_fields = sset_join(&field_set, ",", "");
+            ds_put_format(&actions, "select(values=(%s); hash_fields=\"%s\"",
+                          ds_cstr(&values), hash_fields);
+
+            free(hash_fields);
+            sset_destroy(&field_set);
+        } else {
+            ds_put_format(&actions, "select(%s", ds_cstr(&values));
         }
         ds_put_cstr(&actions, ");");
+        ds_destroy(&values);
     } else {
         er = CONTAINER_OF(ovs_list_front(&eg->route_list),
                           struct ecmp_route_list_node, list_node);
@@ -11662,7 +11726,7 @@  add_route(struct lflow_table *lflows, struct ovn_datapath *od,
         }
     }
     build_route_match(op_inport, rtb_id, network_s, plen, is_src_route,
-                      is_ipv4, &match, &priority, ofs);
+                      is_ipv4, &match, &priority, ofs, false);
 
     struct ds common_actions = DS_EMPTY_INITIALIZER;
     struct ds actions = DS_EMPTY_INITIALIZER;
@@ -13560,7 +13624,19 @@  build_static_route_flows_for_lrouter(
     HMAP_FOR_EACH (group, hmap_node, &ecmp_groups) {
         /* add a flow in IP_ROUTING, and one flow for each member in
          * IP_ROUTING_ECMP. */
-        build_ecmp_route_flow(lflows, od, lr_ports, group, lflow_ref);
+        build_ecmp_route_flow(lflows, od, lr_ports, group, lflow_ref, NULL);
+
+        /* If src or dst port is specified for selection_fields, install
+         * separate ECMP flows with protocol match of TCP, UDP and SCTP */
+        if (sset_contains(&group->selection_fields, "tp_src") ||
+            sset_contains(&group->selection_fields, "tp_dst")) {
+            build_ecmp_route_flow(lflows, od, lr_ports, group, lflow_ref,
+                                  "tcp");
+            build_ecmp_route_flow(lflows, od, lr_ports, group, lflow_ref,
+                                  "udp");
+            build_ecmp_route_flow(lflows, od, lr_ports, group, lflow_ref,
+                                  "sctp");
+        }
     }
     const struct unique_routes_node *ur;
     HMAP_FOR_EACH (ur, hmap_node, &unique_routes) {
@@ -18811,6 +18887,7 @@  static_routes_destroy(struct static_routes_data *data)
 {
     struct parsed_route *r;
     HMAP_FOR_EACH_POP (r, key_node, &data->parsed_routes) {
+        sset_destroy(&r->ecmp_selection_fields);
         free(r);
     }
     hmap_destroy(&data->parsed_routes);
diff --git a/northd/northd.h b/northd/northd.h
index 8f76d642d..c1442ff40 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -707,6 +707,7 @@  struct parsed_route {
     bool is_discard_route;
     const struct nbrec_logical_router *nbr;
     bool stale;
+    struct sset ecmp_selection_fields;
 };
 
 void ovnnb_db_run(struct northd_input *input_data,
diff --git a/ovn-nb.ovsschema b/ovn-nb.ovsschema
index b4a395c56..c4a48183d 100644
--- a/ovn-nb.ovsschema
+++ b/ovn-nb.ovsschema
@@ -1,7 +1,7 @@ 
 {
     "name": "OVN_Northbound",
-    "version": "7.6.0",
-    "cksum": "2171465655 38284",
+    "version": "7.7.0",
+    "cksum": "116357561 38626",
     "tables": {
         "NB_Global": {
             "columns": {
@@ -506,6 +506,12 @@ 
                                           "refType": "weak"},
                                   "min": 0,
                                   "max": 1}},
+                "selection_fields": {
+                    "type": {"key": {"type": "string",
+                             "enum": ["set",
+                                ["eth_src", "eth_dst", "ip_proto", "ip_src",
+                                 "ip_dst", "tp_src", "tp_dst"]]},
+                             "min": 0, "max": "unlimited"}},
                 "options": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}},
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 2836f58f5..3729a5bee 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -3806,6 +3806,32 @@  or
       </p>
     </column>
 
+    <column name="selection_fields">
+        <p>
+          ECMP routes use OpenFlow groups of type <code>select</code> to
+          pick a nexthop among the list of available nexthops.
+          OVS supports two selection methods: <code>dp_hash</code> and
+          <code>hash</code> for hash computation and selecting
+          the buckets of a group. OVN by default uses <code>dp_hash</code>.
+          In order to use the <code>hash</code> selection method,
+          specify the allowed match fields in selection fields.
+          Please see the OVS documentation (man ovs-ofctl) for more
+          details on selection methods and fields.
+        </p>
+        <p>
+          To match on Layer 4 ports use <code>tp_port</code> and
+          <code>tp_port</code>. This match is applicable only for TCP,
+          UDP, SCTP and will be ignored for all other IP packets. When
+          matching on Layer 4 ports, match on ip_proto will be implicitly
+          added in the select action.
+        </p>
+        <p>
+          Example: <code>{ip_proto,ip_src,ip_dst}</code> for a 3-tuple match.
+          Example: <code>{ip_proto,ip_src,ip_dst,tp_src,tp_dst}</code>
+          for a 5-tuple match.
+        </p>
+    </column>
+
     <column name="route_table">
       <p>
         Any string to place route to separate routing table. If Logical Router
diff --git a/ovn-sb.xml b/ovn-sb.xml
index 5285cae30..e079018d8 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -2545,6 +2545,7 @@  tcp.flags = RST;
         </dd>
 
         <dt><code><var>R</var> = select(<var>N1</var>[=<var>W1</var>], <var>N2</var>[=<var>W2</var>], ...);</code></dt>
+        <dt><code><var>R</var> = select(values=<var>N1</var>[=<var>W1</var>], <var>N2</var>[=<var>W2</var>], ...; hash_felds="<var>field1</var>,<var>field2</var>,...");</code></dt>
         <dd>
           <p>
             <b>Parameters</b>: Integer <var>N1</var>, <var>N2</var>..., with
@@ -2564,6 +2565,14 @@  tcp.flags = RST;
             selection method is based on the 5-tuple hash of packet header.
           </p>
 
+          <p>
+            By default, <code>dp_hash</code> is used as the OpenFlow group
+            selection method, but if <code>values</code> and
+            <code>hash_fields</code> are specified, <code>hash</code> is used
+            as the selection method, and the fields listed are used as the
+            hash fields.
+          </p>
+
           <p>
             Processing automatically moves on to the next table, as if
             <code>next;</code> were specified.  The <code>select</code> action
@@ -2574,6 +2583,7 @@  tcp.flags = RST;
 
           <p>
             <b>Example:</b> <code>reg8[16..31] = select(1=20, 2=30, 3=50);</code>
+            <b>Example:</b> <code>reg8[16..31] = select(values=1=20, 2=30, 3=50; hash_fields="ip_proto,src_ip,dst_ip");</code>
           </p>
         </dd>
 
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index d6a8c4640..adec96d1f 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -6810,9 +6810,9 @@  AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_ip_routing   ), priority=0    , match=(1), action=(drop;)
   table=??(lr_in_ip_routing   ), priority=10300, match=(ct_mark.ecmp_reply_port == 1 && reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; eth.src = 00:00:20:20:12:13; reg1 = 192.168.0.1; outport = "lr0-public"; next;)
   table=??(lr_in_ip_routing   ), priority=10550, match=(nd_rs || nd_ra), action=(drop;)
-  table=??(lr_in_ip_routing   ), priority=194  , match=(inport == "lr0-public" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=74   , match=(ip4.dst == 192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=97   , match=(reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=124  , match=(ip4.dst == 192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=162  , match=(reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=324  , match=(inport == "lr0-public" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
 ])
 
 AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | ovn_strip_lflows], [0], [dnl
@@ -6828,9 +6828,9 @@  AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_ip_routing   ), priority=0    , match=(1), action=(drop;)
   table=??(lr_in_ip_routing   ), priority=10300, match=(ct_mark.ecmp_reply_port == 1 && reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; eth.src = 00:00:20:20:12:13; reg1 = 192.168.0.1; outport = "lr0-public"; next;)
   table=??(lr_in_ip_routing   ), priority=10550, match=(nd_rs || nd_ra), action=(drop;)
-  table=??(lr_in_ip_routing   ), priority=194  , match=(inport == "lr0-public" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=74   , match=(ip4.dst == 192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=97   , match=(reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);)
+  table=??(lr_in_ip_routing   ), priority=124  , match=(ip4.dst == 192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=162  , match=(reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);)
+  table=??(lr_in_ip_routing   ), priority=324  , match=(inport == "lr0-public" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
 ])
 AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed 's/192\.168\.0\..0/192.168.0.??/' | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_ip_routing_ecmp), priority=0    , match=(1), action=(drop;)
@@ -6857,9 +6857,9 @@  AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_ip_routing   ), priority=0    , match=(1), action=(drop;)
   table=??(lr_in_ip_routing   ), priority=10300, match=(ct_mark.ecmp_reply_port == 1 && reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; eth.src = 00:00:20:20:12:13; reg1 = 192.168.0.1; outport = "lr0-public"; next;)
   table=??(lr_in_ip_routing   ), priority=10550, match=(nd_rs || nd_ra), action=(drop;)
-  table=??(lr_in_ip_routing   ), priority=194  , match=(inport == "lr0-public" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=74   , match=(ip4.dst == 192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=97   , match=(reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);)
+  table=??(lr_in_ip_routing   ), priority=124  , match=(ip4.dst == 192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=162  , match=(reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);)
+  table=??(lr_in_ip_routing   ), priority=324  , match=(inport == "lr0-public" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
 ])
 AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed 's/192\.168\.0\..0/192.168.0.??/' | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_ip_routing_ecmp), priority=0    , match=(1), action=(drop;)
@@ -6875,14 +6875,41 @@  check ovn-nbctl --wait=sb lr-route-add lr0 1.0.0.0/24 192.168.0.10
 ovn-sbctl dump-flows lr0 > lr0flows
 
 AT_CHECK([grep -e "lr_in_ip_routing.*192.168.0.10" lr0flows | ovn_strip_lflows], [0], [dnl
-  table=??(lr_in_ip_routing   ), priority=73   , match=(reg7 == 0 && ip4.dst == 1.0.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=122  , match=(reg7 == 0 && ip4.dst == 1.0.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
 ])
 
 check ovn-nbctl --wait=sb lr-route-add lr0 2.0.0.0/24 lr0-public
 
 ovn-sbctl dump-flows lr0 > lr0flows
 AT_CHECK([grep -e "lr_in_ip_routing.*2.0.0.0" lr0flows | ovn_strip_lflows], [0], [dnl
-  table=??(lr_in_ip_routing   ), priority=73   , match=(reg7 == 0 && ip4.dst == 2.0.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=122  , match=(reg7 == 0 && ip4.dst == 2.0.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
+])
+
+check ovn-nbctl lr-route-add lr0 3.3.0.0/16 192.168.0.11
+check ovn-nbctl --ecmp lr-route-add lr0 3.3.0.0/16 192.168.0.12
+
+route1_uuid=$(fetch_column nb:logical_router_static_route _uuid nexthop="192.168.0.11")
+route2_uuid=$(fetch_column nb:logical_router_static_route _uuid nexthop="192.168.0.12")
+
+check ovn-nbctl set logical_router_static_route $route1_uuid selection_fields="ip_proto,ip_src,ip_dst"
+check ovn-nbctl set logical_router_static_route $route2_uuid selection_fields="ip_proto,ip_src,ip_dst"
+
+check ovn-nbctl --wait=sb sync
+ovn-sbctl dump-flows lr0 > lr0flows
+AT_CHECK([grep -e "(lr_in_ip_routing   ).*3.3.0.0" lr0flows | sed 's/table=../table=??/' | sort], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=82   , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(values=(1, 2); hash_fields="ip_dst,ip_proto,ip_src");)
+])
+
+check ovn-nbctl set logical_router_static_route $route1_uuid selection_fields="ip_src,ip_dst,tp_src,tp_dst"
+check ovn-nbctl set logical_router_static_route $route2_uuid selection_fields="ip_src,ip_dst,tp_src,tp_dst"
+
+check ovn-nbctl --wait=sb sync
+ovn-sbctl dump-flows lr0 > lr0flows
+AT_CHECK([grep -e "(lr_in_ip_routing   ).*3.3.0.0" lr0flows | sed 's/table=../table=??/' | sort], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=82   , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(values=(1, 2); hash_fields="ip_dst,ip_proto,ip_src");)
+  table=??(lr_in_ip_routing   ), priority=83   , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16 && sctp), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(values=(1, 2); hash_fields="ip_dst,ip_proto,ip_src,sctp_dst,sctp_src");)
+  table=??(lr_in_ip_routing   ), priority=83   , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16 && tcp), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(values=(1, 2); hash_fields="ip_dst,ip_proto,ip_src,tcp_dst,tcp_src");)
+  table=??(lr_in_ip_routing   ), priority=83   , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16 && udp), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(values=(1, 2); hash_fields="ip_dst,ip_proto,ip_src,udp_dst,udp_src");)
 ])
 
 AT_CLEANUP
@@ -7306,16 +7333,16 @@  AT_CHECK([grep "lr_in_ip_routing_pre" lr0flows | ovn_strip_lflows], [0], [dnl
 grep -e "(lr_in_ip_routing   ).*outport" lr0flows
 
 AT_CHECK([grep -e "(lr_in_ip_routing   ).*outport" lr0flows | ovn_strip_lflows], [0], [dnl
-  table=??(lr_in_ip_routing   ), priority=1    , match=(reg7 == 0 && ip4.dst == 0.0.0.0/0), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1; eth.src = 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=1    , match=(reg7 == 2 && ip4.dst == 0.0.0.0/0), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1; eth.src = 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=194  , match=(inport == "lrp0" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:1; eth.src = 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=194  , match=(inport == "lrp1" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:101; eth.src = 00:00:00:00:01:01; outport = "lrp1"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=194  , match=(inport == "lrp2" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:201; eth.src = 00:00:00:00:02:01; outport = "lrp2"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=73   , match=(reg7 == 1 && ip4.dst == 192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.1.10; reg1 = 192.168.1.1; eth.src = 00:00:00:00:01:01; outport = "lrp1"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=74   , match=(ip4.dst == 192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src = 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=74   , match=(ip4.dst == 192.168.1.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.1.1; eth.src = 00:00:00:00:01:01; outport = "lrp1"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=74   , match=(ip4.dst == 192.168.2.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.2.1; eth.src = 00:00:00:00:02:01; outport = "lrp2"; flags.loopback = 1; next;)
-  table=??(lr_in_ip_routing   ), priority=97   , match=(reg7 == 2 && ip4.dst == 1.1.1.1/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.20; reg1 = 192.168.0.1; eth.src = 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=122  , match=(reg7 == 1 && ip4.dst == 192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.1.10; reg1 = 192.168.1.1; eth.src = 00:00:00:00:01:01; outport = "lrp1"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=124  , match=(ip4.dst == 192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src = 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=124  , match=(ip4.dst == 192.168.1.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.1.1; eth.src = 00:00:00:00:01:01; outport = "lrp1"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=124  , match=(ip4.dst == 192.168.2.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.2.1; eth.src = 00:00:00:00:02:01; outport = "lrp2"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=162  , match=(reg7 == 2 && ip4.dst == 1.1.1.1/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.20; reg1 = 192.168.0.1; eth.src = 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=2    , match=(reg7 == 0 && ip4.dst == 0.0.0.0/0), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1; eth.src = 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=2    , match=(reg7 == 2 && ip4.dst == 0.0.0.0/0), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1; eth.src = 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=324  , match=(inport == "lrp0" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:1; eth.src = 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=324  , match=(inport == "lrp1" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:101; eth.src = 00:00:00:00:01:01; outport = "lrp1"; flags.loopback = 1; next;)
+  table=??(lr_in_ip_routing   ), priority=324  , match=(inport == "lrp2" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:201; eth.src = 00:00:00:00:02:01; outport = "lrp2"; flags.loopback = 1; next;)
 ])
 
 AT_CLEANUP
diff --git a/tests/ovn.at b/tests/ovn.at
index d7f01169c..7674dd297 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -2133,6 +2133,16 @@  reg0 = select(1, 2);
     encodes as group:20
     uses group: id(20), name(type=select,selection_method=dp_hash,bucket=bucket_id=0,weight:100,actions=load:1->xxreg0[[96..127]],resubmit(,oflow_in_table),bucket=bucket_id=1,weight:100,actions=load:2->xxreg0[[96..127]],resubmit(,oflow_in_table))
 
+reg9[[16..31]] = select(values=(1=50, 2=100, 3); hash_fields="ip_src,ip_dst");
+    formats as reg9[[16..31]] = select(values=(1=50, 2=100, 3=100); hash_fields="ip_src,ip_dst");
+    encodes as group:21
+    uses group: id(21), name(type=select,selection_method=hash,fields(ip_src,ip_dst),bucket=bucket_id=0,weight:50,actions=load:1->xreg4[[16..31]],resubmit(,oflow_in_table),bucket=bucket_id=1,weight:100,actions=load:2->xreg4[[16..31]],resubmit(,oflow_in_table),bucket=bucket_id=2,weight:100,actions=load:3->xreg4[[16..31]],resubmit(,oflow_in_table))
+
+reg0 = select(values=(1, 2); hash_fields="ip_dst,ip_src");
+    formats as reg0 = select(values=(1=100, 2=100); hash_fields="ip_dst,ip_src");
+    encodes as group:22
+    uses group: id(22), name(type=select,selection_method=hash,fields(ip_dst,ip_src),bucket=bucket_id=0,weight:100,actions=load:1->xxreg0[[96..127]],resubmit(,oflow_in_table),bucket=bucket_id=1,weight:100,actions=load:2->xxreg0[[96..127]],resubmit(,oflow_in_table))
+
 reg0 = select(1=, 2);
     Syntax error at `,' expecting weight.
 reg0 = select(1=0, 2);
@@ -2141,6 +2151,14 @@  reg0 = select(1=123456, 2);
     Syntax error at `123456' expecting weight.
 reg0 = select(123);
     Syntax error at `;' expecting at least 2 group members.
+reg0 = select(values=1, 2);
+    Syntax error at `1' expecting `('.
+reg0 = select(values=(1, 2); test_fields="hello,world");
+    Syntax error at `test_fields' expecting hash_fields.
+reg0 = select(values=(1, 2); hash_fields);
+    Syntax error at `)' invalid hash_fields.
+reg0 = select(values=(1, 2); hash_fields=);
+    Syntax error at `)' invalid hash_fields.
 ip.proto = select(1, 2, 3);
     Field ip.proto is not modifiable.
 reg0[[0..14]] = select(1, 2, 3);
@@ -2148,12 +2166,12 @@  reg0[[0..14]] = select(1, 2, 3);
 
 fwd_group(liveness=true, childports="eth0", "lsp1");
     formats as fwd_group(liveness="true", childports="eth0", "lsp1");
-    encodes as group:21
-    uses group: id(21), name(type=select,selection_method=dp_hash,bucket=watch_port:5,load=0x5->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT),bucket=watch_port:17,load=0x17->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT))
+    encodes as group:23
+    uses group: id(23), name(type=select,selection_method=dp_hash,bucket=watch_port:5,load=0x5->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT),bucket=watch_port:17,load=0x17->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT))
 
 fwd_group(childports="eth0", "lsp1");
-    encodes as group:22
-    uses group: id(22), name(type=select,selection_method=dp_hash,bucket=load=0x5->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT),bucket=load=0x17->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT))
+    encodes as group:24
+    uses group: id(24), name(type=select,selection_method=dp_hash,bucket=load=0x5->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT),bucket=load=0x17->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT))
 
 fwd_group(childports=eth0);
     Syntax error at `eth0' expecting logical switch port.
@@ -2162,8 +2180,8 @@  fwd_group();
     Syntax error at `)' expecting `;'.
 
 fwd_group(childports="eth0", "lsp1");
-    encodes as group:22
-    uses group: id(22), name(type=select,selection_method=dp_hash,bucket=load=0x5->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT),bucket=load=0x17->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT))
+    encodes as group:24
+    uses group: id(24), name(type=select,selection_method=dp_hash,bucket=load=0x5->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT),bucket=load=0x17->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT))
 
 fwd_group(liveness=xyzzy, childports="eth0", "lsp1");
     Syntax error at `xyzzy' expecting true or false.
@@ -26620,6 +26638,234 @@  OVN_CLEANUP([hv1])
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ECMP static routes - custom hash])
+ovn_start
+
+# Logical network:
+# ls1 (192.168.1.0/24) - lr1 - ls2 (192.168.2.0/24)
+# lsl has lsp11 (192.168.1.11) and ls2 has lsp21 (192.168.2.21) and lsp22
+# (192.168.2.22)
+#
+# Static routes on lr1:
+# 10.0.0.0/24 nexthop 192.168.2.21
+# 10.0.0.0/24 nexthop 192.168.2.22
+#
+# ECMP hash on ip_proto,src_ip,dst_ip,tp_dst
+#
+# Test:
+# lsp11 sends packets to 10.0.0.100 with different source ports
+# Migrate all the VIFs to a hv2
+# Generate the same traffic from lsp11 again
+#
+# Expected result:
+# All packets should go out of a either lsp21 or lsp22 on hv1
+# All packets should go out of the same port even on hv2
+
+ovn-nbctl lr-add lr1
+
+ovn-nbctl ls-add ls1
+ovn-nbctl ls-add ls2
+
+for i in 1 2; do
+    ovn-nbctl lrp-add lr1 lrp-lr1-ls${i} 00:00:00:01:0${i}:01 192.168.${i}.1/24
+    ovn-nbctl lsp-add ls${i} lsp-ls${i}-lr1 -- lsp-set-type lsp-ls${i}-lr1 router \
+        -- lsp-set-options lsp-ls${i}-lr1 router-port=lrp-lr1-ls${i} \
+        -- lsp-set-addresses lsp-ls${i}-lr1 router
+done
+
+#install static routes
+ovn-nbctl lr-route-add lr1 10.0.0.0/24 192.168.2.21
+route_uuid=$(fetch_column nb:logical_router_static_route _uuid nexthop="192.168.2.21")
+check ovn-nbctl set logical_router_static_route $route_uuid selection_fields="eth_src,ip_proto,ip_src,ip_dst,tp_dst"
+
+ovn-nbctl --ecmp lr-route-add lr1 10.0.0.0/24 192.168.2.22
+route_uuid=$(fetch_column nb:logical_router_static_route _uuid nexthop="192.168.2.22")
+check ovn-nbctl set logical_router_static_route $route_uuid selection_fields="eth_dst,ip_proto,ip_src,ip_dst,tp_dst"
+
+# Create logical ports
+ovn-nbctl lsp-add ls1 lsp11 -- \
+    lsp-set-addresses lsp11 "f0:00:00:00:01:11 192.168.1.11"
+ovn-nbctl lsp-add ls2 lsp21 -- \
+    lsp-set-addresses lsp21 "f0:00:00:00:02:21 192.168.2.21"
+ovn-nbctl lsp-add ls2 lsp22 -- \
+    lsp-set-addresses lsp22 "f0:00:00:00:02:22 192.168.2.22"
+
+net_add n1
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+
+sim_add hv2
+as hv2
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.2
+
+as hv1
+ovs-vsctl -- add-port br-int hv1-vif1 -- \
+    set interface hv1-vif1 external-ids:iface-id=lsp11 \
+    options:tx_pcap=hv1/vif1-tx.pcap \
+    options:rxq_pcap=hv1/vif1-rx.pcap \
+    ofport-request=1
+
+ovs-vsctl -- add-port br-int hv1-vif2 -- \
+    set interface hv1-vif2 external-ids:iface-id=lsp21 \
+    options:tx_pcap=hv1/vif2-tx.pcap \
+    options:rxq_pcap=hv1/vif2-rx.pcap \
+    ofport-request=2
+
+ovs-vsctl -- add-port br-int hv1-vif3 -- \
+    set interface hv1-vif3 external-ids:iface-id=lsp22 \
+    options:tx_pcap=hv1/vif3-tx.pcap \
+    options:rxq_pcap=hv1/vif3-rx.pcap \
+    ofport-request=3
+
+# wait for earlier changes to take effect
+check ovn-nbctl --wait=hv sync
+wait_for_ports_up
+
+ovn-sbctl dump-flows > sbflows
+AT_CAPTURE_FILE([sbflows])
+
+AT_CAPTURE_FILE([ofgroups])
+OVS_WAIT_FOR_OUTPUT([as hv1 ovs-ofctl dump-groups br-int > ofgroups
+    grep "selection_method=hash,fields" ofgroups | \
+    grep "nw_proto" | grep "ip_src" | grep "ip_dst" | wc -l], [0], [4
+])
+
+as hv1 ovs-ofctl dump-groups br-int > ofgroups
+AT_CHECK([grep "selection_method=hash,fields(ip_src,ip_dst,nw_proto)" ofgroups |  wc -l], [0], [1
+])
+AT_CHECK([grep "selection_method=hash,fields(ip_src,ip_dst,nw_proto,tcp_dst)" ofgroups |  wc -l], [0], [1
+])
+AT_CHECK([grep "selection_method=hash,fields(ip_src,ip_dst,nw_proto,udp_dst)" ofgroups |  wc -l], [0], [1
+])
+AT_CHECK([grep "selection_method=hash,fields(ip_src,ip_dst,nw_proto,sctp_dst)" ofgroups |  wc -l], [0], [1
+])
+
+as hv1 ovs-ofctl dump-flows br-int > oflows
+AT_CAPTURE_FILE([oflows])
+
+for i in $(seq 5001 5010); do
+    packet="inport==\"lsp11\" && eth.src==f0:00:00:00:01:11 && eth.dst==00:00:00:01:01:01 &&
+            ip4 && ip.ttl==64 && ip4.src==192.168.1.11 && ip4.dst==10.0.0.100 &&
+            tcp && tcp.src==$i && tcp.dst==80"
+    OVS_WAIT_UNTIL([as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"])
+
+    for j in 1 2; do
+        # Assume all packets go to lsp2${j}.
+        exp_packet="eth.src==00:00:00:01:02:01 && eth.dst==f0:00:00:00:02:2${j} &&
+                ip4 && ip.ttl==63 && ip4.src==192.168.1.11 && ip4.dst==10.0.0.100 &&
+                tcp && tcp.src==$i && tcp.dst==80"
+        echo $exp_packet | ovstest test-ovn expr-to-packets >> expected_lsp2${j}
+    done
+done
+
+# All packets should go out of a single port given the hashing is based on ip_proto,ip_src,ip_dst,tp_dst which is fixed
+OVS_WAIT_UNTIL([
+    hv1_rcv_n1=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap > lsp21.packets && cat lsp21.packets | wc -l`
+    hv1_rcv_n2=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif3-tx.pcap > lsp22.packets && cat lsp22.packets | wc -l`
+    echo $hv1_rcv_n1 $hv1_rcv_n2
+    test $(($hv1_rcv_n1 + $hv1_rcv_n2)) -ge 10])
+
+if test $hv1_rcv_n1 = 0; then
+    AT_CHECK([test $hv1_rcv_n2 -ge 10], [0], [])
+else
+    AT_CHECK([test $hv1_rcv_n1 -ge 10], [0], [])
+fi
+
+# Move all VIFs to hv2 and send the same packets again
+as hv1
+ovs-vsctl del-port hv1-vif1
+ovs-vsctl del-port hv1-vif2
+ovs-vsctl del-port hv1-vif3
+
+wait_column "" Port_Binding chassis logical_port=lsp11
+wait_column "" Port_Binding chassis logical_port=lsp21
+wait_column "" Port_Binding chassis logical_port=lsp22
+
+as hv2
+ovs-vsctl -- add-port br-int hv2-vif1 -- \
+    set interface hv2-vif1 external-ids:iface-id=lsp11 \
+    options:tx_pcap=hv2/vif1-tx.pcap \
+    options:rxq_pcap=hv2/vif1-rx.pcap \
+    ofport-request=1
+
+ovs-vsctl -- add-port br-int hv2-vif2 -- \
+    set interface hv2-vif2 external-ids:iface-id=lsp21 \
+    options:tx_pcap=hv2/vif2-tx.pcap \
+    options:rxq_pcap=hv2/vif2-rx.pcap \
+    ofport-request=2
+
+ovs-vsctl -- add-port br-int hv2-vif3 -- \
+    set interface hv2-vif3 external-ids:iface-id=lsp22 \
+    options:tx_pcap=hv2/vif3-tx.pcap \
+    options:rxq_pcap=hv2/vif3-rx.pcap \
+    ofport-request=3
+
+# wait for earlier changes to take effect
+check ovn-nbctl --wait=hv sync
+wait_for_ports_up
+
+AT_CAPTURE_FILE([ofgroups])
+OVS_WAIT_FOR_OUTPUT([as hv2 ovs-ofctl dump-groups br-int > ofgroups
+    grep "selection_method=hash,fields" ofgroups | \
+    grep "nw_proto" | grep "ip_src" | grep "ip_dst" | wc -l], [0], [4
+])
+
+as hv2 ovs-ofctl dump-groups br-int > ofgroups
+AT_CHECK([grep "selection_method=hash,fields(ip_src,ip_dst,nw_proto)" ofgroups |  wc -l], [0], [1
+])
+AT_CHECK([grep "selection_method=hash,fields(ip_src,ip_dst,nw_proto,tcp_dst)" ofgroups |  wc -l], [0], [1
+])
+AT_CHECK([grep "selection_method=hash,fields(ip_src,ip_dst,nw_proto,udp_dst)" ofgroups |  wc -l], [0], [1
+])
+AT_CHECK([grep "selection_method=hash,fields(ip_src,ip_dst,nw_proto,sctp_dst)" ofgroups |  wc -l], [0], [1
+])
+
+as hv2 ovs-ofctl dump-flows br-int > oflows
+AT_CAPTURE_FILE([oflows])
+
+for i in $(seq 5001 5010); do
+    packet="inport==\"lsp11\" && eth.src==f0:00:00:00:01:11 && eth.dst==00:00:00:01:01:01 &&
+            ip4 && ip.ttl==64 && ip4.src==192.168.1.11 && ip4.dst==10.0.0.100 &&
+            tcp && tcp.src==$i && tcp.dst==80"
+    OVS_WAIT_UNTIL([as hv2 ovs-appctl -t ovn-controller inject-pkt "$packet"])
+
+    for j in 1 2; do
+        # Assume all packets go to lsp2${j}.
+        exp_packet="eth.src==00:00:00:01:02:01 && eth.dst==f0:00:00:00:02:2${j} &&
+                ip4 && ip.ttl==63 && ip4.src==192.168.1.11 && ip4.dst==10.0.0.100 &&
+                tcp && tcp.src==$i && tcp.dst==80"
+        echo $exp_packet | ovstest test-ovn expr-to-packets >> expected_lsp2${j}
+    done
+done
+
+# All packets should go out of a single port given the hashing is based on ip_proto,ip_src,ip_dst,tp_dst which is fixed
+OVS_WAIT_UNTIL([
+    hv2_rcv_n1=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv2/vif2-tx.pcap > lsp21.packets && cat lsp21.packets | wc -l`
+    hv2_rcv_n2=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv2/vif3-tx.pcap > lsp22.packets && cat lsp22.packets | wc -l`
+    echo $hv2_rcv_n1 $hv2_rcv_n2
+    test $(($hv2_rcv_n1 + $hv2_rcv_n2)) -ge 10])
+
+if test $hv2_rcv_n1 = 0; then
+    AT_CHECK([test $hv2_rcv_n2 -ge 10], [0], [])
+else
+    AT_CHECK([test $hv2_rcv_n1 -ge 10], [0], [])
+fi
+
+# All packets should out of the same port on both hosts
+if test $hv1_rcv_n1 = 0; then
+    AT_CHECK([test $hv2_rcv_n1 -eq 0], [0], [])
+else
+    AT_CHECK([test $hv2_rcv_n2 -eq 0], [0], [])
+fi
+
+OVN_CLEANUP([hv1], [hv2])
+
+AT_CLEANUP
+])
 
 OVN_FOR_EACH_NORTHD([
 AT_SETUP([route tables -- <main> route table routes])