Hi

Since this series hasn't moved since June, and I've been debugging the
same code path independently, sharing what I found in the hope it
unblocks things. The kernel fix should of course remain Laika's;
consider this supporting data.

> 1. Please split the selftest fix to a separate patch (patch #1), explain
> why the test is currently passing and why it's going to break with the
> subsequent code change.

Below is that explanation (1-3), plus history (4) and a reproducer for
a July 2025 field report of what looks like the same regression.

In short: there are two independent defects. The stale dst in the
reply builder (what this series fixes), and a selftest stimulus that
can only pass while that reply is *not* delivered -- so any correct
kernel fix turns the bridged subtests red until the test is fixed
first. Everything below was measured on net at v7.2-7323-gf967455fb2a5,
under virtme-ng.

1. Selftest problem

In test_pmtu_ipvX_over_bridged_vxlanY_or_geneveY_exception():

    run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -w 1 ... || return 1

With a deadline (-w) set, iputils exits on the first socket error
(main_loop(): "if (rts->deadline && rts->nerrors) break;"), before any
echo reply can arrive, and finish() then returns non-zero because
nreceived is still 0. So this line fails exactly when the ICMP error
is delivered, and passes when the error dies in the kernel. The ns_c
line just above already uses -c 10 and survives the delivered error:
it reports "+1 errors" in its statistics yet exits 0.

2. Kernel side

When iptunnel_pmtud_build_icmp{,v6}() gets the skb it doesn't touch
the attached dst, so on reinjection skb_valid_dst() is true, input
routing is skipped, and the reply is dispatched through the stale
dst. What happens next depends on the dst:

- Locally generated traffic (a socket on the bridge host itself):
  the dst is an output route, whose ->input is still the
  dst_discard() stub from dst_init(). The reinjected PTB is freed
  silently. Verified with bpftrace on kfree_skb().

- Forwarded traffic (the WireGuard setup from the original report):
  same mechanism up to the ->input call, but here the dst is the
  forward route, so ->input is ip_forward and the reply is forwarded
  along the stale route -- out the original egress, into the VXLAN
  instead of back to the sender. That is exactly the reported
  symptom. Reproducer script for this case is attached at the end of
  the mail.

  In the reproducer every oversized DF packet is lost with zero
  errors on the sender's socket -- all feedback channels are closed.
  The router's own Frag Needed never fires because the forwarding
  path deliberately ignores learned path MTUs
  (net.ipv4.ip_forward_use_pmtu=0 by default), and the route
  exception written by skb_dst_update_pmtu_no_confirm() is readable
  only by local sockets for the same reason. Setting
  ip_forward_use_pmtu=1 takes {10 tx, 0 rx, 0 errors} to
  {10 tx, 7 rx, +1 error}, which pins the mechanism -- but it is
  not a fix: under default policy the generated PTB is the only
  feedback channel this sender class has, and the stale dst kills
  exactly that channel.

- IPv6 differs in shape, not outcome: the unicast branch of
  ip6_rt_init_dst() sets ->input = ip6_forward, for output and
  forward routes alike -- and ip6_rcv_core() takes IP6CB(skb)->iif
  from the stale dst's device, so the redirect precondition
  iif == oif holds by construction. Measured with an on-link stale
  route: forwarding=0 drops the PTB at the forwarding check;
  forwarding=1 emits a spurious ICMPv6 Redirect (target equal to
  the node's own address, PTB quoted inside) and then dies
  resolving that same address as a neighbour. With a gatewayed
  stale route it should misroute out the original egress like IPv4.
  Never delivered to the sender either way.

3. Why the tests are green today

- skb_dst_update_pmtu_no_confirm() runs before building the reply
  in skb_tunnel_check_pmtu(), so for a local sender the route
  exception is created whether or not the ICMP ever arrives -- and
  the test only asserts the exception's existence.

- With br_netfilter loaded, br_nf_pre_routing_finish() replaces the
  stale dst on the reinjected PTB (it re-enters through the vxlan
  port) with the bridge's fake rtable, which is dropped again before
  ip_rcv -- so the PTB is routed from scratch and delivered, and
  then (1) turns delivery into FAIL. Reproduced with a couple of
  commands:

    modprobe br_netfilter
    ./pmtu.sh

  All 16 bridged subtests fail on a vanilla kernel. A fixed kernel
  fails this recipe identically -- which is the point: the test has
  to be fixed in any scenario. The same happens in a plain
  sequential selftest run, because fcnal-test.sh loads br_netfilter
  and does not unload it.

4. History

The root cause is 8930424777e4 ("tunnels: Accept PACKET_HOST in
skb_tunnel_check_pmtu().") combined with 4cb47a8644cc ("tunnels:
PMTU discovery support for directly bridged IP packets").

4cb47a8644cc shipped the builder with

    if (!reply || skb->pkt_type == PACKET_HOST)
        return 0;

so replies were only generated for bridged-through L2 frames. Those
reach the builder with no dst -- they never visit the host's L3 --
so the missing dst invalidation was unreachable, and the selftest
(df40e39c0df0, same series) was written against that semantic: its
-w 1 line could not receive an error by design.

8930424777e4 (6.15-rc1, since backported to stable, at least
6.1.135 per the Debian report) removed the pkt_type check. From
that point on local and forwarded senders -- the first callers with
a live dst, attached by the host's own routing -- reach the
builder, the stale dst problem became reachable, and the selftest
assumption was silently invalidated.

This matches Debian bug #1108860 [1] (July 2025): the same
VXLAN-over-WireGuard breakage in production, bisected to
8930424777e4, revert confirmed to fix it. That thread ended with a
request for more details and, ideally, a self-contained reproducer,
and stalled -- the script below is meant to be that reproducer. The
two reports look like the same regression, so I'm Cc'ing the people
from that thread.

5. What I suggest

- Selftest fix first, as its own patch -- otherwise any correct
  kernel fix turns the bridged subtests red: switch the -w 1 line
  to -c 10, keeping "|| return 1". Measured on both a masked and a
  delivering kernel: without a deadline the first error does not
  terminate ping (it consumes -c budget instead), exceptions still
  get created, and the line exits 0 in both worlds. Unlike dropping
  the "|| return 1" guards (the v3 approach), this fixes the
  stimulus while the guards keep catching real breakage. Neither v1
  nor v3 carried Fixes tags; for this patch the lines come from
  df40e39c0df0 and their assumption was invalidated by
  8930424777e4, so dual Fixes tags would route the backport to
  every stable tree that has the latter. I have this patch ready
  and can send it right away -- it is independent of the kernel fix
  and passes on both kernels -- unless Laika prefers to fold it
  into the series. If I don't hear back either way in a week or so,
  I'll send it on its own with a Link: to this thread.

> This probably needs to be:
>
> if (skb_valid_dst(skb))
>       skb_dst_drop(skb);
>
> Both VXLAN and GENEVE use the dst after skb_tunnel_check_pmtu() when in
> external mode, so you can't drop it unconditionally.

- Can confirm this guarded variant works on both sides: the 16 OVS
  subtest failures from v1 were exactly this metadata issue, and
  with the guard all 16 pass on a guarded kernel; on the bridged
  side the reproducer below goes from 10/0 (no notification at all)
  to 10 tx / 8 rx / +1 error, the error being the tunnel's PTB
  finally reaching the sender. Happy to give Tested-by on a v4 with
  the guard. Fixes-wise: 4cb47a8644cc introduced the missing
  invalidation and 8930424777e4 made it reachable (and is what
  Debian bisected to) -- dual tags again, since prose in the commit
  message won't route stable backports. Given #1108860,
  Reported-by:/Closes: tags for the Debian report may be
  appropriate.

- The in-tree test cannot catch this bug class at all: it only
  asserts the exception, which the silent update always creates.
  The reproducer below is nearly a pmtu.sh subtest for the
  forwarded case (assert that the PTB actually reaches the sender);
  I can follow up with that as a separate patch.

[1] https://bugs.debian.org/1108860

------reproducer: forwarded sender, persistent PMTU blackhole------

for n in D A B; do ip netns del $n 2>/dev/null; done
ip netns add D; ip netns add A; ip netns add B

ip link add veth_D-A netns D type veth peer name veth_A-D netns A
ip -n D addr add 192.168.3.10/24 dev veth_D-A
ip -n A addr add 192.168.3.1/24  dev veth_A-D
ip -n D link set veth_D-A mtu 5000 up
ip -n A link set veth_A-D mtu 5000 up
ip -n D link set lo up; ip -n A link set lo up; ip -n B link set lo up

ip link add veth_A-B netns A type veth peer name veth_B-A netns B
ip -n A addr add 10.0.1.1/24 dev veth_A-B
ip -n B addr add 10.0.1.2/24 dev veth_B-A
ip -n A link set veth_A-B mtu 4000 up
ip -n B link set veth_B-A mtu 4000 up

ip -n A link add br0 type bridge
ip -n A addr add 192.168.2.1/24 dev br0
ip -n A link set br0 mtu 5000 up
ip -n A link add vxlan_a type vxlan id 1 local 10.0.1.1 \
        remote 10.0.1.2 dstport 4789 df set ttl 64
ip -n A link set vxlan_a mtu 5000 master br0 up
ip -n B link add vxlan_b type vxlan id 1 local 10.0.1.2 \
        remote 10.0.1.1 dstport 4789 df set ttl 64
ip -n B addr add 192.168.2.2/24 dev vxlan_b
ip -n B link set vxlan_b mtu 5000 up

ip -n B addr add 192.168.4.1/32 dev lo
ip -n A route add 192.168.4.1/32 via 192.168.2.2
ip -n D route add 192.168.2.0/24 via 192.168.3.1
ip -n D route add 192.168.4.1/32 via 192.168.3.1
ip -n B route add 192.168.3.0/24 via 192.168.2.1
ip netns exec A sysctl -qw net.ipv4.ip_forward=1

ip netns exec D ping -c 2 -s 56 192.168.4.1 >/dev/null
for n in D A B; do ip netns exec $n ip route flush cached; done
ip netns exec D ping -q -M want -i 0.1 -c 10 -s 4500 192.168.4.1 || true

  Result on an unfixed kernel, default sysctls: 10 transmitted, 0 received,
  no ICMP errors on the socket, and 10 misrouted PTBs inside the tunnel
  (ns_b IpInAddrErrors +10). Persistent: no packet ever heals the path.
  Mechanism check: sysctl -w net.ipv4.ip_forward_use_pmtu=1 in ns_a, flush
  caches, re-run -> 10/7/+1 error (the router's own Frag Needed takes over).
  On a fixed (guarded) kernel, default sysctls: 10/8/+1, the error being
  "From 192.168.4.1 icmp_seq=1 Frag needed and DF set (mtu = 3950)" --
  the tunnel's PTB finally reaching the sender.
-------------------------------------------------------------------

Thanks,
Yaroslav Dudkov

Reply via email to