[PATCH net 1/2] udp: restore outer transport header after tunnel GRO

From: Bryon Fryer

Date: Wed Nov 11 2026 - 22:09:40 EST


UDP tunnel GRO can produce invalid virtio tunnel headers, causing
virtio-net to drop whole aggregates. Losing several TCP segments at
once causes retransmissions and can collapse throughput.

Observed as slow VXLAN TCP traffic with kernel logs reporting dropped
packets. Reproduced with ordinary tunnel traffic in disposable VMs. The
unpatched reproductions required no kernel modifications or error
injection.

Inner GRO processing moves skb->transport_header to inner TCP or
UDP. The inner completion callback saves it in inner_transport_header,
but udp_gro_complete() leaves the main transport header there too.
The virtio tunnel exporter added by commit a2fb4bc4e2a6 ("net: implement
virtio helpers to handle UDP GSO tunneling.") treats that field as the
outer UDP location. The exported outer header then appears after
the inner network header, so the receiver rejects the descriptor.

Restore skb->transport_header from nhoff after successful inner
completion. This parsed outer UDP offset retains the inner transport
and checksum offsets without assuming fixed header lengths.

Tested under virtme-ng with socat sending bulk TCP over IPv4 VXLAN
and Geneve between network namespaces. A raw sender TAP supplies
wire-sized packets to a TAP/bridge path with ingress GRO. Exported
virtio headers are relayed unchanged to a receiving TAP using the
shared virtio decoder. Kernels used the same base, config and toolchain
under KVM. For 32 MiB, median transfer times and rejected aggregates
across three paired runs were:

Before patch After patch
VXLAN, GRO on 69.8s, 1418 rejects 0.15s, 0 rejects
Geneve, GRO on 70.8s, 1414 rejects 0.16s, 0 rejects
VXLAN, GRO off 0.22s, 0 rejects 0.23s, 0 rejects
Geneve, GRO off 0.23s, 0 rejects 0.21s, 0 rejects

Rejections are receiving TAP writes returning -EINVAL. Median TCP
retransmissions fell from 2852 (VXLAN) and 2854 (Geneve) to zero.
Every transfer verified all payload bytes; no TAP queue drops occurred.

Separate eight-packet tests measured these outer UDP offsets, in
bytes from the Ethernet frame start:

TAP-exported outer_th_offset
Actual UDP Before patch After patch
VXLAN over IPv4 34 84 34
VXLAN-GPE over IPv4 34 70 34
Geneve over IPv4 34 84 34

The inner network and checksum-start offsets were unchanged. The
accompanying TUN regression fails only its outer-offset assertion
before patching and passes afterward. Selected existing TUN and UDP
GSO selftests also passed before and after patching.

VXLAN-GPE raw-TAP/GRO-on software segmentation still returns -EINVAL
before and after this patch. This is a separate issue.

Fixes: a2fb4bc4e2a6 ("net: implement virtio helpers to handle UDP GSO tunneling.")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Bryon Fryer <bryon@xxxxxxxx>
---
net/ipv4/udp_offload.c | 2 ++
1 file changed, 2 insertions(+)

diff --git a/net/ipv4/udp_offload.c b/net/ipv4/udp_offload.c
index b77ae2f00c..1a1d1a125d 100644
--- a/net/ipv4/udp_offload.c
+++ b/net/ipv4/udp_offload.c
@@ -948,6 +948,8 @@ int udp_gro_complete(struct sk_buff *skb, int nhoff,
skb->encapsulation = 1;
err = udp_sk(sk)->gro_complete(sk, skb,
nhoff + sizeof(struct udphdr));
+ if (!err)
+ skb_set_transport_header(skb, nhoff);
} else {
err = udp_gro_complete_segment(skb);
}
--
2.47.3