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

Reply via email to