On Mon, Oct 19, 2026 at 2:25 PM Srikar Tati via dev <[email protected]> wrote: > > From: Srikar Tati <[email protected]> > > Static routes can choose the fields that their ECMP group hashes on > with the "selection_fields" column. With selection fields the group > uses the "hash" selection method instead of "dp_hash". "dp_hash" > needs a recirculation, which the tc datapath cannot offload, so only > groups that use "hash" can be offloaded to a NIC. > > Learned routes have no NB row, so they always use "dp_hash". Add the > "dynamic-routing-ecmp-selection-fields" option to Logical Routers. It > takes the same fields as "selection_fields" and applies them to the > ECMP groups of the router's learned routes. If the option has a field > that is not supported, northd logs a warning and keeps "dp_hash". > > Assisted-by: Claude Opus 5.5, Claude Code > Signed-off-by: Srikar Tati <[email protected]>
Acked-by: Numan Siddique <[email protected]> Numan > --- > NEWS | 4 ++ > northd/en-learned-route-sync.c | 23 +++++++++-- > ovn-nb.xml | 22 +++++++++++ > tests/ovn-northd.at | 71 ++++++++++++++++++++++++++++++++++ > 4 files changed, 116 insertions(+), 4 deletions(-) > > diff --git a/NEWS b/NEWS > index 13ffd6e2e..5a822cc21 100644 > --- a/NEWS > +++ b/NEWS > @@ -28,6 +28,10 @@ Post v26.09.0 > now written to the SB MAC_Binding table and consumed at the > same priority as dynamic entries, making the preference option > obsolete. > + * Add the "options:dynamic-routing-ecmp-selection-fields" option to > + 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. > - Removed OVN's ovs-bugtool plugin and helper scripts. > - Removed ovn-sim utility scripts. > - Removed ovn-docker utility scripts. > diff --git a/northd/en-learned-route-sync.c b/northd/en-learned-route-sync.c > index cbd516b68..959cadf9f 100644 > --- a/northd/en-learned-route-sync.c > +++ b/northd/en-learned-route-sync.c > @@ -199,10 +199,25 @@ parse_route_from_sbrec_route(struct hmap > *parsed_routes_out, > return NULL; > } > > - return parsed_route_add(od, nexthop, &prefix, plen, false, lrp_addr_s, > - out_port, 0, false, false, false, NULL, > - ROUTE_SOURCE_LEARNED, true, &route->header_, > NULL, > - parsed_routes_out); > + struct sset ecmp_selection_fields = > + SSET_INITIALIZER(&ecmp_selection_fields); > + const char *fields_s = smap_get(&od->nbr->options, > + "dynamic-routing-ecmp-selection-fields"); > + if (fields_s && !ovn_ecmp_selection_fields_parse(fields_s, > + > &ecmp_selection_fields)) { > + static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1); > + VLOG_WARN_RL(&rl, "bad 'dynamic-routing-ecmp-selection-fields' %s " > + "on router %s, using the default selection method", > + fields_s, od->nbr->name); > + } > + > + struct parsed_route *pr = > + parsed_route_add(od, nexthop, &prefix, plen, false, lrp_addr_s, > + out_port, 0, false, false, false, > + &ecmp_selection_fields, ROUTE_SOURCE_LEARNED, true, > + &route->header_, NULL, parsed_routes_out); > + sset_destroy(&ecmp_selection_fields); > + return pr; > } > > static void > diff --git a/ovn-nb.xml b/ovn-nb.xml > index feea50a25..0a3154858 100644 > --- a/ovn-nb.xml > +++ b/ovn-nb.xml > @@ -3716,6 +3716,28 @@ or > </p> > </column> > > + <column name="options" key="dynamic-routing-ecmp-selection-fields" > + type='{"type": "string"}'> > + <p> > + Only relevant if <ref column="options" key="dynamic-routing"/> > + is set to <code>true</code>. > + </p> > + > + <p> > + A comma separated list of the fields that select the nexthop of an > + ECMP group of learned routes, for example > + <code>ip_proto,ip_src,ip_dst,tp_src,tp_dst</code>. The supported > + fields and their meaning are the same as for the > + <ref column="selection_fields" > table="Logical_Router_Static_Route"/> > + column of a static route. When set, the ECMP groups of learned > + routes use 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="options" key="dynamic-routing-v4-prefix-nexthop" > type='{"type": "string"}'> > <p> > diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at > index ec04d4d67..6e99e9aa4 100644 > --- a/tests/ovn-northd.at > +++ b/tests/ovn-northd.at > @@ -17552,6 +17552,77 @@ OVN_CLEANUP_NORTHD > AT_CLEANUP > ]) > > +OVN_FOR_EACH_NORTHD_NO_HV([ > +AT_SETUP([dynamic-routing - learned route ecmp selection fields]) > +AT_KEYWORDS([dynamic-routing]) > +ovn_start > + > +check ovn-nbctl lr-add lr0 > +check ovn-nbctl set Logical_Router lr0 option:dynamic-routing=true > +check ovn-nbctl lrp-add lr0 lr0-sw0 00:00:00:00:ff:01 10.0.0.1/24 > +check ovn-nbctl --wait=sb lrp-add lr0 lr0-sw1 00:00:00:00:ff:02 10.0.1.1/24 > +sw0=$(fetch_column port_binding _uuid logical_port=lr0-sw0) > +sw1=$(fetch_column port_binding _uuid logical_port=lr0-sw1) > +datapath=$(fetch_column datapath_binding _uuid external_ids:name=lr0) > + > +# Learn 192.168.1.0/24 through both ports, so that it is an ECMP group. > +check_uuid ovn-sbctl create Learned_Route \ > + datapath=$datapath \ > + logical_port=$sw0 \ > + ip_prefix=192.168.1.0/24 \ > + nexthop=10.0.0.20 > +check_uuid ovn-sbctl create Learned_Route \ > + datapath=$datapath \ > + logical_port=$sw1 \ > + ip_prefix=192.168.1.0/24 \ > + nexthop=10.0.1.20 > +check ovn-nbctl --wait=sb sync > + > +# Without selection fields the group uses dp_hash. > +ovn-sbctl dump-flows lr0 > lr0flows > +AT_CHECK([grep -e "(lr_in_ip_routing ).*192.168.1.0" lr0flows | > ovn_strip_lflows], [0], [dnl > + table=??(lr_in_ip_routing ), priority=1838 , match=(reg7 == 0 && ip4.dst > == 192.168.1.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; > reg8[[16..31]] = select(1, 2);) > +]) > + > +check ovn-nbctl --wait=sb set Logical_Router lr0 \ > + options:dynamic-routing-ecmp-selection-fields="ip_proto,ip_src,ip_dst" > +ovn-sbctl dump-flows lr0 > lr0flows > +AT_CHECK([grep -e "(lr_in_ip_routing ).*192.168.1.0" lr0flows | > ovn_strip_lflows], [0], [dnl > + table=??(lr_in_ip_routing ), priority=1838 , match=(reg7 == 0 && ip4.dst > == 192.168.1.0/24), 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");) > +]) > + > +# tp_src and tp_dst give one flow for each L4 protocol. > +check ovn-nbctl --wait=sb set Logical_Router lr0 \ > + > options:dynamic-routing-ecmp-selection-fields="ip_src,ip_dst,tp_src,tp_dst" > +ovn-sbctl dump-flows lr0 > lr0flows > +AT_CHECK([grep -e "(lr_in_ip_routing ).*192.168.1.0" lr0flows | > ovn_strip_lflows], [0], [dnl > + table=??(lr_in_ip_routing ), priority=1838 , match=(reg7 == 0 && ip4.dst > == 192.168.1.0/24), 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=1839 , match=(reg7 == 0 && ip4.dst > == 192.168.1.0/24 && 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=1839 , match=(reg7 == 0 && ip4.dst > == 192.168.1.0/24 && 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=1839 , match=(reg7 == 0 && ip4.dst > == 192.168.1.0/24 && 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");) > +]) > + > +# A field that is not supported falls back to dp_hash. > +check ovn-nbctl --wait=sb set Logical_Router lr0 \ > + options:dynamic-routing-ecmp-selection-fields="ip_src,foo" > +ovn-sbctl dump-flows lr0 > lr0flows > +AT_CHECK([grep -e "(lr_in_ip_routing ).*192.168.1.0" lr0flows | > ovn_strip_lflows], [0], [dnl > + table=??(lr_in_ip_routing ), priority=1838 , match=(reg7 == 0 && ip4.dst > == 192.168.1.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; > reg8[[16..31]] = select(1, 2);) > +]) > +check grep -q "bad 'dynamic-routing-ecmp-selection-fields' ip_src,foo" \ > + northd/ovn-northd.log > + > +check ovn-nbctl --wait=sb remove Logical_Router lr0 \ > + options dynamic-routing-ecmp-selection-fields > +ovn-sbctl dump-flows lr0 > lr0flows > +AT_CHECK([grep -e "(lr_in_ip_routing ).*192.168.1.0" lr0flows | > ovn_strip_lflows], [0], [dnl > + table=??(lr_in_ip_routing ), priority=1838 , match=(reg7 == 0 && ip4.dst > == 192.168.1.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; > reg8[[16..31]] = select(1, 2);) > +]) > + > +OVN_CLEANUP_NORTHD > +AT_CLEANUP > +]) > + > OVN_FOR_EACH_NORTHD_NO_HV([ > AT_SETUP([dynamic-routing - learned routes on unnumbered LRP]) > AT_KEYWORDS([dynamic-routing]) > -- > 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
