Skip to content
89 changes: 89 additions & 0 deletions vppbld/patches/0015-vxlan-encap-inner-aware-flow-hash.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aaron Bernardino <aaronber@microsoft.com>
Date: Fri, 10 Jul 2026 00:00:00 +0000
Subject: [PATCH] vxlan: inner-aware flow hash in the encap node

The VXLAN encap node derives the outer UDP source port (and the buffer
flow hash used for underlay ECMP) from vnet_l2_compute_flow_hash(). That
helper reads the inner ethertype at current_data + l2.l2_len. For an
L3-routed VXLAN tunnel the inner packet arrives already routed (the
ip4/ip6-rewrite adjacency prepends the inner Ethernet header) and
l2.l2_len is 0/unset, so the helper misparses the frame and returns a
value that does not depend on the inner L3/L4 headers. Every inner flow
to a given tunnel endpoint then maps to the same outer UDP source port
and collapses onto a single underlay ECMP path / bond member.

This is the exact case that patch 0011 (inner-aware flow hash) left out of
scope on the assumption that "VXLAN ... their outer UDP source port is the
standard-mandated entropy carrier (RFC 7348 section 4.2), so the existing
outer-5-tuple hash already distributes inner flows." That assumption only
holds if the encap node actually populates the outer UDP source port from
the inner flow -- which it does not for L3-routed tunnels. This patch
fixes the entropy carrier at its source rather than adding the peek-inner
machinery of 0011 to the underlay.

Read the inner Ethernet ethertype directly at the buffer current pointer
(the inner Ethernet header is present for both L2 and L3 tunnels at the
encap call sites) and hash the inner IPv4/IPv6 5-tuple. Fall back to the
generic vnet_l2_compute_flow_hash() (which honours l2_len and VLAN tags)
for anything that is not plain inner IPv4/IPv6.

Signed-off-by: Aaron Bernardino <aaronber@microsoft.com>
---
diff --git a/src/plugins/vxlan/encap.c b/src/plugins/vxlan/encap.c
index 9d304b76d..71024b3b4 100644
--- a/src/plugins/vxlan/encap.c
+++ b/src/plugins/vxlan/encap.c
@@ -57,6 +57,30 @@ format_vxlan_encap_trace (u8 * s, va_list * args)
}
#endif

+static_always_inline u32
+vxlan_encap_inner_flow_hash (vlib_buffer_t * b)
+{
+ /* For L3 VXLAN tunnels the inner packet is routed (not bridged) into this
+ * encap node, so vnet_buffer(b)->l2.l2_len is not set. The generic
+ * vnet_l2_compute_flow_hash() then misparses the frame and returns a value
+ * that does not depend on the inner L3/L4 headers, collapsing every inner
+ * flow for a given tunnel endpoint onto a single outer UDP source port and
+ * a single underlay ECMP path. The inner Ethernet header is present at the
+ * current data pointer, so read its ethertype directly and hash the inner
+ * IP 5-tuple. Fall back to the generic L2 hash (which honours l2_len and
+ * VLAN tags) for anything that is not plain inner IPv4/IPv6. */
+ ethernet_header_t *eh = vlib_buffer_get_current (b);
+ u16 ethertype = clib_net_to_host_u16 (eh->type);
+ u8 *l3h = (u8 *) (eh + 1);
+
+ if (ethertype == ETHERNET_TYPE_IP4)
+ return ip4_compute_flow_hash ((ip4_header_t *) l3h, IP_FLOW_HASH_DEFAULT);
+ else if (ethertype == ETHERNET_TYPE_IP6)
+ return ip6_compute_flow_hash ((ip6_header_t *) l3h, IP_FLOW_HASH_DEFAULT);
+
+ return vnet_l2_compute_flow_hash (b);
+}
+
always_inline uword
vxlan_encap_inline (vlib_main_t *vm, vlib_node_runtime_t *node,
vlib_frame_t *from_frame, u8 is_ip4)
@@ -125,8 +149,8 @@ vxlan_encap_inline (vlib_main_t *vm, vlib_node_runtime_t *node,
vlib_buffer_t *b1 = b[1];
b += 2;

- u32 flow_hash0 = vnet_l2_compute_flow_hash (b0);
- u32 flow_hash1 = vnet_l2_compute_flow_hash (b1);
+ u32 flow_hash0 = vxlan_encap_inner_flow_hash (b0);
+ u32 flow_hash1 = vxlan_encap_inner_flow_hash (b1);

/* Get next node index and adj index from tunnel next_dpo */
if (sw_if_index0 != vnet_buffer (b0)->sw_if_index[VLIB_TX])
@@ -351,7 +375,7 @@ vxlan_encap_inline (vlib_main_t *vm, vlib_node_runtime_t *node,
vlib_buffer_t *b0 = b[0];
b += 1;

- u32 flow_hash0 = vnet_l2_compute_flow_hash (b0);
+ u32 flow_hash0 = vxlan_encap_inner_flow_hash (b0);

/* Get next node index and adj index from tunnel next_dpo */
if (sw_if_index0 != vnet_buffer (b0)->sw_if_index[VLIB_TX])
--
2.34.1
Loading