Skip to contentSkip to conversation

udp: fix FOU/GUE over multicast

RFC PATCH Superseded Anton Danilov 2 participants 11 messages Apr 15 – Jun 22

Subsystems (4)

v2 of 6 Superseded Latest is v6 →

lore
Share & tools
lore
#0 lore raw

v2 cover udp: fix FOU/GUE over multicast

UDP encapsulation (FOU, GUE) has never worked correctly with multicast destination addresses. When a FOU-encapsulated packet arrives at a multicast address, it enters __udp4_lib_mcast_deliver() which calls consume_skb() on packets that need resubmission to the inner protocol handler, silently dropping them instead.

The unicast delivery path and the GSO segmentation path both handle this correctly, but the multicast path was never updated to support UDP encapsulation resubmit.

This causes silent packet loss for FOU/GRETAP tunnels configured with multicast remote addresses. The loss ratio depends on the early demux cache hit rate - packets that hit early demux bypass the multicast path and work correctly, masking the issue.

Reproducing the issue:

ip netns add ns_a && ip netns add ns_b
ip link add veth0 type veth peer name veth1
ip link set veth0 netns ns_a && ip link set veth1 netns ns_b

  ip -n ns_a addr add 10.0.0.1/24 dev veth0 && ip -n ns_a link set veth0 up
  ip -n ns_b addr add 10.0.0.2/24 dev veth1 && ip -n ns_b link set veth1 up

# Multicast routes
ip -n ns_a route add 239.0.0.0/8 dev veth0
ip -n ns_b route add 239.0.0.0/8 dev veth1

# Disable early demux to expose the issue (otherwise it's partially masked) ip netns exec ns_b sysctl -w net.ipv4.ip_early_demux=0

# Join multicast group on receiver
ip -n ns_b addr add 239.0.0.1/32 dev veth1 autojoin

  # Sender: FOU + GRETAP with multicast remote
  ip netns exec ns_a ip fou add port 4797 ipproto 47
  ip -n ns_a link add eoudp0 type gretap \
      remote 239.0.0.1 local 10.0.0.1 \
      encap fou encap-sport 4797 encap-dport 4797 key 239.0.0.1
  ip -n ns_a link set eoudp0 up
  ip -n ns_a addr add 192.168.99.1/24 dev eoudp0

  # Receiver: FOU + GRETAP with multicast remote
  ip netns exec ns_b ip fou add port 4797 ipproto 47
  ip -n ns_b link add eoudp0 type gretap \
      remote 239.0.0.1 local 10.0.0.2 \
      encap fou encap-sport 4797 encap-dport 4797 key 239.0.0.1
  ip -n ns_b link set eoudp0 up
  ip -n ns_b addr add 192.168.99.2/24 dev eoudp0

# Test: ping through the FOU/GRETAP tunnel ip netns exec ns_a ping -c 100 192.168.99.2 # -> without this patch: 0 packets received on eoudp0 # -> with this patch: all packets received on eoudp0

AI assistance (Claude, claude-opus-4-6) was used during root cause analysis of the kernel source code (tracing the call chain from udp_queue_rcv_skb through encap_rcv to ip_protocol_deliver_rcu, comparing unicast/GSO/multicast paths) and during patch and selftest authoring. The fix approach was identified by observing that the unicast path (udp_unicast_rcv_skb) and the GSO path (udp_queue_rcv_skb) both already handle encap resubmit correctly, while the multicast path did not.

Changes since v1 (RFC):
- Moved inline Python packet generator from the shell script into a separate helper (fou_mcast_encap.py) and added it to TEST_FILES in the Makefile

Anton Danilov (2):
udp: fix encapsulation packet resubmit in multicast deliver
selftests: net: add FOU multicast encapsulation resubmit test

 net/ipv4/udp.c                                |  13 ++-
 net/ipv6/udp.c                                |  13 ++-
 tools/testing/selftests/net/Makefile          |   2 +
 .../testing/selftests/net/fou_mcast_encap.py  |  81 ++++++++++++++
 .../testing/selftests/net/fou_mcast_encap.sh  | 105 ++++++++++++++++++
 5 files changed, 206 insertions(+), 8 deletions(-)
 create mode 100755 tools/testing/selftests/net/fou_mcast_encap.py
 create mode 100755 tools/testing/selftests/net/fou_mcast_encap.sh

--
2.47.3

#1 lore raw

RFC PATCH v2 1/2 udp: fix encapsulation packet resubmit in multicast deliver

When a UDP encapsulation socket (e.g., FOU) receives a multicast packet, __udp4_lib_mcast_deliver() and __udp6_lib_mcast_deliver() incorrectly call consume_skb() when udp_queue_rcv_skb() returns a positive value. A positive return value from udp_queue_rcv_skb() indicates that the encap_rcv handler (e.g., fou_udp_recv) has consumed the UDP header and wants the packet to be resubmitted to the IP protocol handler for further processing (e.g., as a GRE packet).

The unicast path in udp_unicast_rcv_skb() handles this correctly by returning -ret, which propagates up to ip_protocol_deliver_rcu() for resubmission. The GSO path in udp_queue_rcv_skb() also handles this correctly by calling ip_protocol_deliver_rcu() directly. However, the multicast path destroys the packet via consume_skb() instead of resubmitting it, causing silent packet loss.

This affects any UDP encapsulation (FOU, GUE) combined with multicast destination addresses. In practice, it causes ~50% packet loss on FOU/GRETAP tunnels configured with multicast remote addresses, with the exact ratio depending on the early demux cache hit rate (packets that hit early demux take the unicast path and are handled correctly).

Fix this by calling ip_protocol_deliver_rcu() (IPv4) or ip6_protocol_deliver_rcu() (IPv6) instead of consume_skb() when the return value is positive, matching the behavior of the GSO path.

Assisted-by Claude:claude-opus-4-6
---
 net/ipv4/udp.c | 13 +++++++++----
 net/ipv6/udp.c | 13 +++++++++----
 2 files changed, 18 insertions(+), 8 deletions(-)
net/ipv4/udp.c +9 -4
index e9e2ce9522ef..8c2d4367cba2 100644
@@ -2467,6 +2467,7 @@ static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	struct udp_hslot *hslot;
 	struct sk_buff *nskb;
 	bool use_hash2;
+	int ret;
 
 	hash2_any = 0;
 	hash2 = 0;
@@ -2500,8 +2501,10 @@ static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 			__UDP_INC_STATS(net, UDP_MIB_INERRORS);
 			continue;
 		}
-		if (udp_queue_rcv_skb(sk, nskb) > 0)
-			consume_skb(nskb);
+		ret = udp_queue_rcv_skb(sk, nskb);
+		if (ret > 0)
+			ip_protocol_deliver_rcu(dev_net(nskb->dev), nskb,
+						ret);
 	}
 
 	/* Also lookup *:port if we are using hash2 and haven't done so yet. */
@@ -2511,8 +2514,10 @@ static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	}
 
 	if (first) {
-		if (udp_queue_rcv_skb(first, skb) > 0)
-			consume_skb(skb);
+		ret = udp_queue_rcv_skb(first, skb);
+		if (ret > 0)
+			ip_protocol_deliver_rcu(dev_net(skb->dev), skb,
+						ret);
 	} else {
 		kfree_skb(skb);
 		__UDP_INC_STATS(net, UDP_MIB_IGNOREDMULTI);
net/ipv6/udp.c +9 -4
index 15e032194ecc..f74935d9f7d7 100644
@@ -949,6 +949,7 @@ static int __udp6_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	struct udp_hslot *hslot;
 	struct sk_buff *nskb;
 	bool use_hash2;
+	int ret;
 
 	hash2_any = 0;
 	hash2 = 0;
@@ -987,8 +988,10 @@ static int __udp6_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 			continue;
 		}
 
-		if (udpv6_queue_rcv_skb(sk, nskb) > 0)
-			consume_skb(nskb);
+		ret = udpv6_queue_rcv_skb(sk, nskb);
+		if (ret > 0)
+			ip6_protocol_deliver_rcu(dev_net(nskb->dev), nskb,
+						 ret, true);
 	}
 
 	/* Also lookup *:port if we are using hash2 and haven't done so yet. */
@@ -998,8 +1001,10 @@ static int __udp6_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	}
 
 	if (first) {
-		if (udpv6_queue_rcv_skb(first, skb) > 0)
-			consume_skb(skb);
+		ret = udpv6_queue_rcv_skb(first, skb);
+		if (ret > 0)
+			ip6_protocol_deliver_rcu(dev_net(skb->dev), skb,
+						 ret, true);
 	} else {
 		kfree_skb(skb);
 		__UDP6_INC_STATS(net, UDP_MIB_IGNOREDMULTI);
Signature · 1 line
2.47.3
#2 lore raw

Re: [RFC PATCH net-next v2 1/2] udp: fix encapsulation packet resubmit in multicast deliver

From: Anton Danilov <[email protected]>
Date: Wed, 15 Apr 2026 21:50:55 +0300

Quoted 46 lines

When a UDP encapsulation socket (e.g., FOU) receives a multicast packet, __udp4_lib_mcast_deliver() and __udp6_lib_mcast_deliver() incorrectly call consume_skb() when udp_queue_rcv_skb() returns a positive value. A positive return value from udp_queue_rcv_skb() indicates that the encap_rcv handler (e.g., fou_udp_recv) has consumed the UDP header and wants the packet to be resubmitted to the IP protocol handler for further processing (e.g., as a GRE packet).

The unicast path in udp_unicast_rcv_skb() handles this correctly by returning -ret, which propagates up to ip_protocol_deliver_rcu() for resubmission. The GSO path in udp_queue_rcv_skb() also handles this correctly by calling ip_protocol_deliver_rcu() directly. However, the multicast path destroys the packet via consume_skb() instead of resubmitting it, causing silent packet loss.

This affects any UDP encapsulation (FOU, GUE) combined with multicast destination addresses. In practice, it causes ~50% packet loss on FOU/GRETAP tunnels configured with multicast remote addresses, with the exact ratio depending on the early demux cache hit rate (packets that hit early demux take the unicast path and are handled correctly).

Fix this by calling ip_protocol_deliver_rcu() (IPv4) or ip6_protocol_deliver_rcu() (IPv6) instead of consume_skb() when the return value is positive, matching the behavior of the GSO path.

Assisted-by Claude:claude-opus-4-6
---
 net/ipv4/udp.c | 13 +++++++++----
 net/ipv6/udp.c | 13 +++++++++----
 2 files changed, 18 insertions(+), 8 deletions(-)
net/ipv4/udp.c +1 -0
index e9e2ce9522ef..8c2d4367cba2 100644
@@ -2467,6 +2467,7 @@ static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	struct udp_hslot *hslot;
 	struct sk_buff *nskb;
 	bool use_hash2;
+	int ret;
 
 	hash2_any = 0;
 	hash2 = 0;

net/ipv4/udp.c static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb, · line 2507

Quoted 9 lines
net/ipv4/udp.c +4 -2
@@ -2500,8 +2501,10 @@ static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 			__UDP_INC_STATS(net, UDP_MIB_INERRORS);
 			continue;
 		}
-		if (udp_queue_rcv_skb(sk, nskb) > 0)
-			consume_skb(nskb);
+		ret = udp_queue_rcv_skb(sk, nskb);
+		if (ret > 0)
+			ip_protocol_deliver_rcu(dev_net(nskb->dev), nskb,
+						ret);

Is this path reachable in the first place ?

Maybe I'm missing something, but UDP tunnel sockets should not have SO_REUSEADDR/SO_REUSEPORT.

net/ipv4/udp.c static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb, · line 2520

Quoted 13 lines
net/ipv4/udp.c +4 -2
 	}
 
 	/* Also lookup *:port if we are using hash2 and haven't done so yet. */
@@ -2511,8 +2514,10 @@ static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	}
 
 	if (first) {
-		if (udp_queue_rcv_skb(first, skb) > 0)
-			consume_skb(skb);
+		ret = udp_queue_rcv_skb(first, skb);
+		if (ret > 0)
+			ip_protocol_deliver_rcu(dev_net(skb->dev), skb,
+						ret);

If the above is true, we can simply return -ret here to avoid possible stack overflow with too many FOU encapsulation that syzbot is fond of.

Please wait 24h before next submission.
https://docs.kernel.org/process/maintainer-netdev.html

Quoted 44 lines
net/ipv4/udp.c +0 -0
 	} else {
 		kfree_skb(skb);
 		__UDP_INC_STATS(net, UDP_MIB_IGNOREDMULTI);
net/ipv6/udp.c +9 -4
index 15e032194ecc..f74935d9f7d7 100644
@@ -949,6 +949,7 @@ static int __udp6_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	struct udp_hslot *hslot;
 	struct sk_buff *nskb;
 	bool use_hash2;
+	int ret;
 
 	hash2_any = 0;
 	hash2 = 0;
@@ -987,8 +988,10 @@ static int __udp6_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 			continue;
 		}
 
-		if (udpv6_queue_rcv_skb(sk, nskb) > 0)
-			consume_skb(nskb);
+		ret = udpv6_queue_rcv_skb(sk, nskb);
+		if (ret > 0)
+			ip6_protocol_deliver_rcu(dev_net(nskb->dev), nskb,
+						 ret, true);
 	}
 
 	/* Also lookup *:port if we are using hash2 and haven't done so yet. */
@@ -998,8 +1001,10 @@ static int __udp6_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	}
 
 	if (first) {
-		if (udpv6_queue_rcv_skb(first, skb) > 0)
-			consume_skb(skb);
+		ret = udpv6_queue_rcv_skb(first, skb);
+		if (ret > 0)
+			ip6_protocol_deliver_rcu(dev_net(skb->dev), skb,
+						 ret, true);
 	} else {
 		kfree_skb(skb);
 		__UDP6_INC_STATS(net, UDP_MIB_IGNOREDMULTI);
Signature · 1 line
2.47.3
#3 lore raw

RFC PATCH v2 2/2 selftests: net: add FOU multicast encapsulation resubmit test

In reply to Anton Danilov

Add a selftest to verify that FOU-encapsulated packets addressed to a multicast destination are correctly resubmitted to the inner protocol handler (GRE) via the UDP multicast delivery path.

The test creates two network namespaces connected by a veth pair. The receiver namespace has a FOU/GRETAP tunnel with a multicast remote address (239.0.0.1). A Python helper script (fou_mcast_encap.py) crafts GRE-over-UDP packets and sends them to the multicast address.

The early demux optimization (net.ipv4.ip_early_demux) is disabled on the receiver to force packets through __udp4_lib_mcast_deliver(), which is the code path that was previously broken.

Assisted-by Claude:claude-opus-4-6
---
 tools/testing/selftests/net/Makefile          |   2 +
 .../testing/selftests/net/fou_mcast_encap.py  |  81 ++++++++++++++
 .../testing/selftests/net/fou_mcast_encap.sh  | 105 ++++++++++++++++++
 3 files changed, 188 insertions(+)
 create mode 100755 tools/testing/selftests/net/fou_mcast_encap.py
 create mode 100755 tools/testing/selftests/net/fou_mcast_encap.sh
tools/testing/selftests/net/Makefile +2 -0
index a275ed584026..2b463dda5ac6 100644
@@ -38,6 +38,7 @@ TEST_PROGS := \
 	fib_rule_tests.sh \
 	fib_tests.sh \
 	fin_ack_lat.sh \
+	fou_mcast_encap.sh \
 	fq_band_pktlimit.sh \
 	gre_gso.sh \
 	gre_ipv6_lladdr.sh \
@@ -195,6 +196,7 @@ TEST_GEN_PROGS := \
 
 TEST_FILES := \
 	fcnal-test.sh \
+	fou_mcast_encap.py \
 	in_netns.sh \
 	lib.sh \
 	settings \
tools/testing/selftests/net/fou_mcast_encap.py new file +81 -0

81 lines not on this page · raw message

tools/testing/selftests/net/fou_mcast_encap.sh new file +105 -0

105 lines not on this page · raw message

Signature · 1 line
2.47.3
#4 lore raw

v3 cover udp: fix FOU/GUE over multicast

In reply to Anton Danilov

UDP encapsulation (FOU, GUE) has never worked correctly with multicast destination addresses. When a FOU-encapsulated packet arrives at a multicast address, it enters __udp4_lib_mcast_deliver() which calls consume_skb() on packets that need resubmission to the inner protocol handler, silently dropping them instead.

The unicast delivery path handles this correctly by returning -ret, but the multicast path was never updated to support UDP encapsulation resubmit.

This causes silent packet loss for FOU/GRETAP tunnels configured with multicast remote addresses. The loss ratio depends on the early demux cache hit rate - packets that hit early demux bypass the multicast path and work correctly, masking the issue.

Reproducing the issue:

ip netns add ns_a && ip netns add ns_b
ip link add veth0 type veth peer name veth1
ip link set veth0 netns ns_a && ip link set veth1 netns ns_b

  ip -n ns_a addr add 10.0.0.1/24 dev veth0 && ip -n ns_a link set veth0 up
  ip -n ns_b addr add 10.0.0.2/24 dev veth1 && ip -n ns_b link set veth1 up

# Multicast routes
ip -n ns_a route add 239.0.0.0/8 dev veth0
ip -n ns_b route add 239.0.0.0/8 dev veth1

# Disable early demux to expose the issue (otherwise it's partially masked) ip netns exec ns_b sysctl -w net.ipv4.ip_early_demux=0

# Join multicast group on receiver
ip -n ns_b addr add 239.0.0.1/32 dev veth1 autojoin

  # Sender: GRETAP with FOU encap
  ip -n ns_a link add eoudp0 type gretap \
      remote 239.0.0.1 local 10.0.0.1 \
      encap fou encap-sport 4797 encap-dport 4797 key 239.0.0.1
  ip -n ns_a link set eoudp0 up
  ip -n ns_a addr add 192.168.99.1/24 dev eoudp0

  # Receiver: FOU listener + GRETAP
  ip netns exec ns_b ip fou add port 4797 ipproto 47
  ip -n ns_b link add eoudp0 type gretap \
      remote 239.0.0.1 local 10.0.0.2 \
      encap fou encap-sport 4797 encap-dport 4797 key 239.0.0.1
  ip -n ns_b link set eoudp0 up
  ip -n ns_b addr add 192.168.99.2/24 dev eoudp0

  # Static neigh: ARP replies can't traverse unidirectional mcast tunnel
  recv_mac=$(ip -n ns_b link show eoudp0 | awk '/ether/{print $2}')
  ip -n ns_a neigh add 192.168.99.2 lladdr $recv_mac dev eoudp0

# Test: ping through the FOU/GRETAP tunnel ip netns exec ns_a ping -c 100 192.168.99.2 # -> without this patch: 0 packets received on eoudp0 # -> with this patch: all packets received on eoudp0

AI assistance (Claude, claude-opus-4-6) was used during root cause analysis of the kernel source code (tracing the call chain from udp_queue_rcv_skb through encap_rcv to ip_protocol_deliver_rcu, comparing unicast/GSO/multicast paths) and during patch and selftest authoring. The fix approach was identified by observing that the unicast path (udp_unicast_rcv_skb) already handles encap resubmit correctly via return -ret, while the multicast path did not.

Changes since v2:
- Use return -ret instead of calling ip_protocol_deliver_rcu() directly, matching the unicast path and avoiding call stack growth with nested encapsulations (Eric Dumazet)
- Only change the first-socket path; the clone loop is not reachable for tunnel sockets (no SO_REUSEADDR/SO_REUSEPORT)
- Replace Python packet generator with ping through a properly configured FOU/GRETAP tunnel in the selftest
- Add static neighbor entry in the test (ARP replies cannot traverse the unidirectional multicast tunnel)

Changes since v1 (RFC):
- Moved inline Python packet generator into a separate helper
- Fixed author email typo in Signed-off-by

Anton Danilov (2):
udp: fix encapsulation packet resubmit in multicast deliver
selftests: net: add FOU multicast encapsulation resubmit test

 net/ipv4/udp.c                                |   6 +-
 net/ipv6/udp.c                                |   6 +-
 tools/testing/selftests/net/Makefile          |   1 +
 .../testing/selftests/net/fou_mcast_encap.sh  | 112 ++++++++++++++++++
 4 files changed, 121 insertions(+), 4 deletions(-)
 create mode 100755 tools/testing/selftests/net/fou_mcast_encap.sh
Signature · 1 line
2.47.3
#5 lore raw

RFC PATCH v3 1/2 udp: fix encapsulation packet resubmit in multicast deliver

When a UDP encapsulation socket (e.g., FOU) receives a multicast packet, __udp4_lib_mcast_deliver() and __udp6_lib_mcast_deliver() call consume_skb() when udp_queue_rcv_skb() returns a positive value. A positive return value from udp_queue_rcv_skb() indicates that the encap_rcv handler (e.g., fou_udp_recv) has consumed the UDP header and wants the packet to be resubmitted to the IP protocol handler for further processing (e.g., as a GRE packet).

The unicast path in udp_unicast_rcv_skb() handles this correctly by returning -ret, which propagates up to ip_protocol_deliver_rcu() for resubmission. However, the multicast path destroys the packet via consume_skb() instead of resubmitting it, causing silent packet loss.

This affects any UDP encapsulation (FOU, GUE) combined with multicast destination addresses.

Fix this by returning -ret instead of calling consume_skb() when the return value is positive, matching the behavior of the unicast path. This avoids growing the call stack compared to calling ip_protocol_deliver_rcu() directly.

Assisted-by Claude:claude-opus-4-6
---
 net/ipv4/udp.c | 6 ++++--
 net/ipv6/udp.c | 6 ++++--
 2 files changed, 8 insertions(+), 4 deletions(-)
net/ipv4/udp.c +4 -2
index e9e2ce9522ef..84a98631b556 100644
@@ -2467,6 +2467,7 @@ static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	struct udp_hslot *hslot;
 	struct sk_buff *nskb;
 	bool use_hash2;
+	int ret;
 
 	hash2_any = 0;
 	hash2 = 0;
@@ -2511,8 +2512,9 @@ static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	}
 
 	if (first) {
-		if (udp_queue_rcv_skb(first, skb) > 0)
-			consume_skb(skb);
+		ret = udp_queue_rcv_skb(first, skb);
+		if (ret > 0)
+			return -ret;
 	} else {
 		kfree_skb(skb);
 		__UDP_INC_STATS(net, UDP_MIB_IGNOREDMULTI);
net/ipv6/udp.c +4 -2
index 15e032194ecc..ff2e389e286b 100644
@@ -949,6 +949,7 @@ static int __udp6_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	struct udp_hslot *hslot;
 	struct sk_buff *nskb;
 	bool use_hash2;
+	int ret;
 
 	hash2_any = 0;
 	hash2 = 0;
@@ -998,8 +999,9 @@ static int __udp6_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	}
 
 	if (first) {
-		if (udpv6_queue_rcv_skb(first, skb) > 0)
-			consume_skb(skb);
+		ret = udpv6_queue_rcv_skb(first, skb);
+		if (ret > 0)
+			return -ret;
 	} else {
 		kfree_skb(skb);
 		__UDP6_INC_STATS(net, UDP_MIB_IGNOREDMULTI);
Signature · 1 line
2.47.3
#6 lore raw

RFC PATCH v3 2/2 selftests: net: add FOU multicast encapsulation resubmit test

In reply to Anton Danilov

Add a selftest to verify that FOU-encapsulated packets addressed to a multicast destination are correctly resubmitted to the inner protocol handler (GRE) via the UDP multicast delivery path.

The test creates two network namespaces connected by a veth pair with a FOU/GRETAP tunnel using a multicast remote address (239.0.0.1). Ping is sent through the tunnel and received packets are counted on the receiver's tunnel interface.

A static neighbor entry is configured on the sender because ARP replies from the receiver cannot traverse the unidirectional multicast tunnel back to the sender.

The early demux optimization (net.ipv4.ip_early_demux) is disabled on the receiver to force packets through __udp4_lib_mcast_deliver(), which is the code path being tested.

Assisted-by Claude:claude-opus-4-6
---
 tools/testing/selftests/net/Makefile          |   1 +
 .../testing/selftests/net/fou_mcast_encap.sh  | 112 ++++++++++++++++++
 2 files changed, 113 insertions(+)
 create mode 100755 tools/testing/selftests/net/fou_mcast_encap.sh
tools/testing/selftests/net/Makefile +1 -0
index a275ed584026..9b2a573e4af2 100644
@@ -38,6 +38,7 @@ TEST_PROGS := \
 	fib_rule_tests.sh \
 	fib_tests.sh \
 	fin_ack_lat.sh \
+	fou_mcast_encap.sh \
 	fq_band_pktlimit.sh \
 	gre_gso.sh \
 	gre_ipv6_lladdr.sh \
tools/testing/selftests/net/fou_mcast_encap.sh new file +112 -0

112 lines not on this page · raw message

Signature · 1 line
2.47.3
#7 lore raw

v3 cover udp: fix FOU/GUE over multicast

This is a resend of the v3 series originally posted on 2026-05-05, which did not receive review feedback during the previous net-next window. No changes since v3; rebased cleanly on current net-next (after the net-next-7.2 merge).

v3 archive:
https://lore.kernel.org/netdev/[email protected]/

UDP encapsulation (FOU, GUE) has never worked correctly with multicast destination addresses. When a FOU-encapsulated packet arrives at a multicast address, it enters __udp4_lib_mcast_deliver() which calls consume_skb() on packets that need resubmission to the inner protocol handler, silently dropping them instead.

The unicast delivery path handles this correctly by returning -ret, but the multicast path was never updated to support UDP encapsulation resubmit.

This causes silent packet loss for FOU/GRETAP tunnels configured with multicast remote addresses. The loss ratio depends on the early demux cache hit rate - packets that hit early demux bypass the multicast path and work correctly, masking the issue.

Reproducing the issue:

ip netns add ns_a && ip netns add ns_b
ip link add veth0 type veth peer name veth1
ip link set veth0 netns ns_a && ip link set veth1 netns ns_b

  ip -n ns_a addr add 10.0.0.1/24 dev veth0 && ip -n ns_a link set veth0 up
  ip -n ns_b addr add 10.0.0.2/24 dev veth1 && ip -n ns_b link set veth1 up

# Multicast routes
ip -n ns_a route add 239.0.0.0/8 dev veth0
ip -n ns_b route add 239.0.0.0/8 dev veth1

# Disable early demux to expose the issue (otherwise it's partially masked) ip netns exec ns_b sysctl -w net.ipv4.ip_early_demux=0

# Join multicast group on receiver
ip -n ns_b addr add 239.0.0.1/32 dev veth1 autojoin

  # Sender: GRETAP with FOU encap
  ip -n ns_a link add eoudp0 type gretap \
      remote 239.0.0.1 local 10.0.0.1 \
      encap fou encap-sport 4797 encap-dport 4797 key 239.0.0.1
  ip -n ns_a link set eoudp0 up
  ip -n ns_a addr add 192.168.99.1/24 dev eoudp0

  # Receiver: FOU listener + GRETAP
  ip netns exec ns_b ip fou add port 4797 ipproto 47
  ip -n ns_b link add eoudp0 type gretap \
      remote 239.0.0.1 local 10.0.0.2 \
      encap fou encap-sport 4797 encap-dport 4797 key 239.0.0.1
  ip -n ns_b link set eoudp0 up
  ip -n ns_b addr add 192.168.99.2/24 dev eoudp0

  # Static neigh: ARP replies can't traverse unidirectional mcast tunnel
  recv_mac=$(ip -n ns_b link show eoudp0 | awk '/ether/{print $2}')
  ip -n ns_a neigh add 192.168.99.2 lladdr $recv_mac dev eoudp0

# Test: ping through the FOU/GRETAP tunnel ip netns exec ns_a ping -c 100 192.168.99.2 # -> without this patch: 0 packets received on eoudp0 # -> with this patch: all packets received on eoudp0

AI assistance (Claude, claude-opus-4-6) was used during root cause analysis of the kernel source code (tracing the call chain from udp_queue_rcv_skb through encap_rcv to ip_protocol_deliver_rcu, comparing unicast/GSO/multicast paths) and during patch and selftest authoring. The fix approach was identified by observing that the unicast path (udp_unicast_rcv_skb) already handles encap resubmit correctly via return -ret, while the multicast path did not.

Changes since v2:
- Use return -ret instead of calling ip_protocol_deliver_rcu() directly, matching the unicast path and avoiding call stack growth with nested encapsulations (Kuniyuki Iwashima)
- Only change the first-socket path; the clone loop is not reachable for tunnel sockets (no SO_REUSEADDR/SO_REUSEPORT)
- Replace Python packet generator with ping through a properly configured FOU/GRETAP tunnel in the selftest
- Add static neighbor entry in the test (ARP replies cannot traverse the unidirectional multicast tunnel)

Changes since v1 (RFC):
- Moved inline Python packet generator into a separate helper
- Fixed author email typo in Signed-off-by

Anton Danilov (2):
udp: fix encapsulation packet resubmit in multicast deliver
selftests: net: add FOU multicast encapsulation resubmit test

 net/ipv4/udp.c                                |   6 +-
 net/ipv6/udp.c                                |   6 +-
 tools/testing/selftests/net/Makefile          |   1 +
 .../testing/selftests/net/fou_mcast_encap.sh  | 112 ++++++++++++++++++
 4 files changed, 121 insertions(+), 4 deletions(-)
 create mode 100755 tools/testing/selftests/net/fou_mcast_encap.sh
Signature · 1 line
2.47.3
#8 lore raw

PATCH RESEND v3 1/2 udp: fix encapsulation packet resubmit in multicast deliver

When a UDP encapsulation socket (e.g., FOU) receives a multicast packet, __udp4_lib_mcast_deliver() and __udp6_lib_mcast_deliver() call consume_skb() when udp_queue_rcv_skb() returns a positive value. A positive return value from udp_queue_rcv_skb() indicates that the encap_rcv handler (e.g., fou_udp_recv) has consumed the UDP header and wants the packet to be resubmitted to the IP protocol handler for further processing (e.g., as a GRE packet).

The unicast path in udp_unicast_rcv_skb() handles this correctly by returning -ret, which propagates up to ip_protocol_deliver_rcu() for resubmission. However, the multicast path destroys the packet via consume_skb() instead of resubmitting it, causing silent packet loss.

This affects any UDP encapsulation (FOU, GUE) combined with multicast destination addresses.

Fix this by returning -ret instead of calling consume_skb() when the return value is positive, matching the behavior of the unicast path. This avoids growing the call stack compared to calling ip_protocol_deliver_rcu() directly.

Assisted-by Claude:claude-opus-4-6
---
 net/ipv4/udp.c | 6 ++++--
 net/ipv6/udp.c | 6 ++++--
 2 files changed, 8 insertions(+), 4 deletions(-)
net/ipv4/udp.c +4 -2
index 70f6cbd4ef73..b0910659391e 100644
@@ -2475,6 +2475,7 @@ static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	struct udp_hslot *hslot;
 	struct sk_buff *nskb;
 	bool use_hash2;
+	int ret;
 
 	hash2_any = 0;
 	hash2 = 0;
@@ -2519,8 +2520,9 @@ static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	}
 
 	if (first) {
-		if (udp_queue_rcv_skb(first, skb) > 0)
-			consume_skb(skb);
+		ret = udp_queue_rcv_skb(first, skb);
+		if (ret > 0)
+			return -ret;
 	} else {
 		kfree_skb(skb);
 		__UDP_INC_STATS(net, UDP_MIB_IGNOREDMULTI);
net/ipv6/udp.c +4 -2
index 15e032194ecc..ff2e389e286b 100644
@@ -949,6 +949,7 @@ static int __udp6_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	struct udp_hslot *hslot;
 	struct sk_buff *nskb;
 	bool use_hash2;
+	int ret;
 
 	hash2_any = 0;
 	hash2 = 0;
@@ -998,8 +999,9 @@ static int __udp6_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
 	}
 
 	if (first) {
-		if (udpv6_queue_rcv_skb(first, skb) > 0)
-			consume_skb(skb);
+		ret = udpv6_queue_rcv_skb(first, skb);
+		if (ret > 0)
+			return -ret;
 	} else {
 		kfree_skb(skb);
 		__UDP6_INC_STATS(net, UDP_MIB_IGNOREDMULTI);
Signature · 1 line
2.47.3
#9 lore raw

PATCH RESEND v3 2/2 selftests: net: add FOU multicast encapsulation resubmit test

In reply to Anton Danilov

Add a selftest to verify that FOU-encapsulated packets addressed to a multicast destination are correctly resubmitted to the inner protocol handler (GRE) via the UDP multicast delivery path.

The test creates two network namespaces connected by a veth pair with a FOU/GRETAP tunnel using a multicast remote address (239.0.0.1). Ping is sent through the tunnel and received packets are counted on the receiver's tunnel interface.

A static neighbor entry is configured on the sender because ARP replies from the receiver cannot traverse the unidirectional multicast tunnel back to the sender.

The early demux optimization (net.ipv4.ip_early_demux) is disabled on the receiver to force packets through __udp4_lib_mcast_deliver(), which is the code path being tested.

Assisted-by Claude:claude-opus-4-6
---
 tools/testing/selftests/net/Makefile          |   1 +
 .../testing/selftests/net/fou_mcast_encap.sh  | 112 ++++++++++++++++++
 2 files changed, 113 insertions(+)
 create mode 100755 tools/testing/selftests/net/fou_mcast_encap.sh
tools/testing/selftests/net/Makefile +1 -0
index 708d960ae07d..7e9ae937cffa 100644
@@ -39,6 +39,7 @@ TEST_PROGS := \
 	fib_rule_tests.sh \
 	fib_tests.sh \
 	fin_ack_lat.sh \
+	fou_mcast_encap.sh \
 	fq_band_pktlimit.sh \
 	gre_gso.sh \
 	gre_ipv6_lladdr.sh \
tools/testing/selftests/net/fou_mcast_encap.sh new file +112 -0

112 lines not on this page · raw message

Signature · 1 line
2.47.3
#10 lore raw
In reply to Anton Danilov
Quoted 4 lines · Anton Danilov

This is a resend of the v3 series originally posted on 2026-05-05, which did not receive review feedback during the previous net-next window.

Probably due to RFC, which means it is not official yet, and the series is also dropped from patchwork.

Quoted 2 lines

No changes since v3; rebased cleanly on current net-next (after the net-next-7.2 merge).

net-next is closed, please repost next Monday, following the process doc:

https://docs.kernel.org/7.0/process/maintainer-netdev.html#resending-after-review
---8<---
The new version of patches should be posted as a separate thread, not as a reply to the previous posting.
---8<---