The userspace implementation of OVS_HASH_ALG_SYM_L4 calls flow_hash_symmetric_l3l4() with UDP port hashing disabled. As a result, UDP packets with identical addresses but different ports receive the same datapath hash.
The Linux datapath computes its L4 hash with __skb_get_hash_symmetric(), whose flow dissector includes UDP ports. Although the exact dp_hash implementation is datapath-specific, the difference can produce surprising select-group behavior. With OVN EVPN and multiple underlay paths, encapsulated flows can share VTEP addresses and the VXLAN destination port while varying the UDP source port. Ignoring that port leaves no entropy to distribute those flows and can send all traffic through one select-group bucket. Include UDP ports in the userspace SYM_L4 algorithm. This aligns its field selection with Linux, but changes user-visible behavior: asymmetric UDP protocols such as VXLAN no longer necessarily hash identically in both directions. Document the change in NEWS. Add dummy-datapath coverage that verifies reversed UDP tuples remain symmetric while changing only the UDP source port produces more than one datapath hash. Tested manually with OVN and observed 128 flows distributed across 2 EVPN uplinks with OVN. Assisted-by: GPT-5, OpenAI Codex Signed-off-by: Tim Rozet <[email protected]> --- v2: - Change the patch area to odp-execute. - Describe this as a user-visible behavior change instead of a bug fix. - Explain the OVN EVPN underlay ECMP use case. - Remove the Fixes tag and add a NEWS entry. - Rebase on current main. NEWS | 3 +++ lib/odp-execute.c | 2 +- tests/ofproto-dpif.at | 46 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index de1a030ad..9de0a9854 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,8 @@ Post-v4.0.0 -------------------- + - Userspace datapath: + * Symmetric L4 datapath hashing now includes UDP source and destination + ports. This can change select-group bucket selection for UDP traffic. v4.0.0 - 17 Aug 2026 diff --git a/lib/odp-execute.c b/lib/odp-execute.c index 618fb5ac8..a22d60295 100644 --- a/lib/odp-execute.c +++ b/lib/odp-execute.c @@ -1014,7 +1014,7 @@ odp_execute_actions(void *dp, struct dp_packet_batch *batch, bool steal, flow_extract(packet, &flow); hash = flow_hash_symmetric_l3l4(&flow, hash_act->hash_basis, - false); + true); packet->md.dp_hash = hash; } break; diff --git a/tests/ofproto-dpif.at b/tests/ofproto-dpif.at index efb36e058..af40ba1ed 100644 --- a/tests/ofproto-dpif.at +++ b/tests/ofproto-dpif.at @@ -1201,6 +1201,52 @@ n_flows=ok n_buckets=ok OVS_VSWITCHD_STOP AT_CLEANUP +AT_SETUP([ofproto-dpif - symmetric dp_hash includes UDP ports]) + +OVS_VSWITCHD_START +add_of_ports br0 1 10 11 + +AT_CHECK([ovs-ofctl -O OpenFlow12 add-group br0 \ + 'group_id=1234,type=select,bucket=output:10,bucket=output:11']) +AT_CHECK([ovs-ofctl -O OpenFlow12 add-flow br0 \ + 'udp actions=group:1234']) + +dp_hash_flow_count () { + ovs-appctl dpctl/dump-flows \ + | sed -n '/recirc_id(0x[[0-9a-f]][[0-9a-f]]*),dp_hash/p' \ + | wc -l +} + +forward_flow="in_port(1),eth(src=50:54:00:00:00:01,dst=50:54:00:00:00:02),\ +eth_type(0x0800),ipv4(src=192.0.2.1,dst=198.51.100.1,proto=17,\ +tos=0,ttl=64,frag=no)" +reverse_flow="in_port(1),eth(src=50:54:00:00:00:02,dst=50:54:00:00:00:01),\ +eth_type(0x0800),ipv4(src=198.51.100.1,dst=192.0.2.1,proto=17,\ +tos=0,ttl=64,frag=no)" + +AT_CHECK([ovs-appctl netdev-dummy/receive p1 \ + "$forward_flow,udp(src=10000,dst=4789)"]) +OVS_WAIT_UNTIL_EQUAL([dp_hash_flow_count], [1]) + +dnl Reversing the complete L3/L4 tuple must produce the same hash. +AT_CHECK([ovs-appctl netdev-dummy/receive p1 \ + "$reverse_flow,udp(src=4789,dst=10000)"]) +OVS_WAIT_UNTIL([ovs-appctl dpctl/dump-flows \ + | grep 'recirc_id(0),' | grep -q 'packets:1']) +AT_CHECK([dp_hash_flow_count], [0], [1 +]) + +dnl Vary only the UDP source port. At least one packet must produce a +dnl different hash value from the first packet. +for port in `seq 10001 10016`; do + AT_CHECK([ovs-appctl netdev-dummy/receive p1 \ + "$forward_flow,udp(src=$port,dst=4789)"]) +done +OVS_WAIT_UNTIL([test `dp_hash_flow_count` -gt 1]) + +OVS_VSWITCHD_STOP +AT_CLEANUP + AT_SETUP([ofproto-dpif - select group with watch port]) OVS_VSWITCHD_START -- 2.55.0 _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
