[PATCH net-next v3 2/3] ipv6: Cache pmtu for all paths of a multipath route

From: Vladimir Vdovin

Date: Fri Nov 13 2026 - 08:12:51 EST


ip6_update_pmtu() looks the destination up without the protocol and
ports the offending packet was hashed on, so with a multipath route the
exception may be created on another path than the one the packet took.
The flow then keeps sending too big packets on its path, every Packet
Too Big lands on the same wrong path, and the flow blackholes.

Same problem and same fix as for IPv4 in commit 7d3f3b4367f3 ("net:
ipv4: Cache pmtu for all packet paths if multipath enabled"): cache the
pmtu on every path of the route. For a nexthop object walk all of its
fib6_nh, for a legacy multipath route walk the siblings of the fib6_info
the dst came from.

A path only ever lowers what it uses: the pmtu of the exception it
already has for the destination, which is updated in place so that a
gateway from a redirect stays, the mtu of the device for an exception
without a pmtu, or the mtu of the route and the device when a new
exception is created. That check moves from the looked-up dst to each
path: the lookup may land on a path narrower than the Packet Too Big,
which used to end the update before the path the flow is on had learned
anything.

A dst that is a cached exception, or can't have one, is still updated
itself as before: the walk is keyed by the destination and would not
find the first hop dst of a socket with a routing header.

Link: https://lore.kernel.org/netdev/ad29493c-9e48-4a94-b327-e47af3a9d137@xxxxxxxxxx/
Signed-off-by: Vladimir Vdovin <deliran@xxxxxxxxxx>
---
net/ipv6/route.c | 121 ++++++++++++++++++++++++++++++++---------------
1 file changed, 82 insertions(+), 39 deletions(-)

diff --git a/net/ipv6/route.c b/net/ipv6/route.c
index 04098cf32eab..ccb11a465528 100644
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -2911,12 +2911,55 @@ static bool rt6_cache_allowed_for_pmtu(const struct rt6_info *rt)
(rt->rt6i_flags & RTF_PCPU || rcu_access_pointer(rt->from));
}

+struct rt6_pmtu_arg {
+ struct fib6_result res;
+ const struct in6_addr *daddr;
+ const struct in6_addr *saddr;
+ u32 mtu;
+};
+
+/* Cache a pmtu on one path of a route, never raising what the path
+ * uses for daddr. An existing exception is updated in place, a new
+ * one would lose a gateway learned from a redirect.
+ */
+static int rt6_nh_update_pmtu(struct fib6_nh *nh, void *_arg)
+{
+ struct rt6_pmtu_arg *arg = _arg;
+ struct fib6_result *res = &arg->res;
+ struct rt6_info *rt;
+
+ res->nh = nh;
+ rt = rt6_find_cached_rt(res, arg->daddr, arg->saddr);
+ if (rt) {
+ if (arg->mtu < dst6_mtu(&rt->dst)) {
+ rt6_do_update_pmtu(rt, arg->mtu);
+ fib6_nh_update_exception(nh, res->f6i->fib6_src.plen,
+ rt);
+ }
+ return 0;
+ }
+
+ if (arg->mtu >= fib6_mtu(res))
+ return 0;
+
+ rt = ip6_rt_cache_alloc(res, arg->daddr, arg->saddr);
+ if (rt) {
+ rt6_do_update_pmtu(rt, arg->mtu);
+ if (rt6_insert_exception(rt, res))
+ dst_release_immediate(&rt->dst);
+ }
+
+ return 0;
+}
+
static void __ip6_rt_update_pmtu(struct dst_entry *dst, const struct sock *sk,
const struct ipv6hdr *iph, u32 mtu,
bool confirm_neigh)
{
- const struct in6_addr *daddr, *saddr;
+ struct rt6_pmtu_arg arg = { .mtu = mtu };
struct rt6_info *rt6 = dst_rt6_info(dst);
+ const struct in6_addr *daddr, *saddr;
+ struct fib6_info *f6i, *sibling;

/* Note: do *NOT* check dst_metric_locked(dst, RTAX_MTU)
* IPv6 pmtu discovery isn't optional, so 'mtu lock' cannot disable it.
@@ -2939,55 +2982,55 @@ static void __ip6_rt_update_pmtu(struct dst_entry *dst, const struct sock *sk,

if (mtu < IPV6_MIN_MTU)
return;
- if (mtu >= dst6_mtu(dst))
- return;

- if (!rt6_cache_allowed_for_pmtu(rt6)) {
+ rcu_read_lock();
+ /* A dst that is a cached exception, or can't have one, is updated
+ * itself: the walk below is keyed by daddr and may not find it, a
+ * socket with a routing header holds the dst of the first hop while
+ * daddr is the final destination.
+ */
+ if (!rt6_cache_allowed_for_pmtu(rt6) && mtu < dst6_mtu(dst)) {
rt6_do_update_pmtu(rt6, mtu);
/* update rt6_ex->stamp for cache */
if (rt6->rt6i_flags & RTF_CACHE)
rt6_update_exception_stamp_rt(rt6);
- } else if (daddr) {
- struct fib6_result res = {};
- struct rt6_info *nrt6;
-
- rcu_read_lock();
- res.f6i = rcu_dereference(rt6->from);
- if (!res.f6i)
- goto out_unlock;
-
- res.fib6_flags = res.f6i->fib6_flags;
- res.fib6_type = res.f6i->fib6_type;
+ }

- if (res.f6i->nh) {
- struct fib6_nh_match_arg arg = {
- .dev = dst_dev_rcu(dst),
- .gw = &rt6->rt6i_gateway,
- };
+ f6i = rcu_dereference(rt6->from);
+ /* Don't walk the siblings of a route being removed from the tree */
+ if (!daddr || !f6i || !rcu_access_pointer(f6i->fib6_node))
+ goto out;

- nexthop_for_each_fib6_nh(res.f6i->nh,
- fib6_nh_find_match, &arg);
+ arg.daddr = daddr;
+ arg.saddr = saddr;
+ arg.res.f6i = f6i;
+ arg.res.fib6_flags = f6i->fib6_flags;
+ arg.res.fib6_type = f6i->fib6_type;

- /* fib6_info uses a nexthop that does not have fib6_nh
- * using the dst->dev + gw. Should be impossible.
- */
- if (!arg.match)
- goto out_unlock;
+ /* The lookup in ip6_update_pmtu() doesn't hash on the protocol and
+ * ports, dst may be another path than the too big packet took:
+ * cache the pmtu on every path it lowers.
+ */
+ if (f6i->nh) {
+ nexthop_for_each_fib6_nh(f6i->nh, rt6_nh_update_pmtu, &arg);
+ goto out;
+ }

- res.nh = arg.match;
- } else {
- res.nh = res.f6i->fib6_nh;
- }
+ rt6_nh_update_pmtu(f6i->fib6_nh, &arg);

- nrt6 = ip6_rt_cache_alloc(&res, daddr, saddr);
- if (nrt6) {
- rt6_do_update_pmtu(nrt6, mtu);
- if (rt6_insert_exception(nrt6, &res))
- dst_release_immediate(&nrt6->dst);
- }
-out_unlock:
- rcu_read_unlock();
+ /* fib6_del_route() zeroes fib6_nsiblings before unlinking f6i, stop
+ * then as the walk would never get back to its head.
+ */
+ list_for_each_entry_rcu(sibling, &f6i->fib6_siblings, fib6_siblings) {
+ arg.res.f6i = sibling;
+ arg.res.fib6_flags = sibling->fib6_flags;
+ arg.res.fib6_type = sibling->fib6_type;
+ rt6_nh_update_pmtu(sibling->fib6_nh, &arg);
+ if (!READ_ONCE(f6i->fib6_nsiblings))
+ break;
}
+out:
+ rcu_read_unlock();
}

static void ip6_rt_update_pmtu(struct dst_entry *dst, struct sock *sk,
--
2.56.0