__ip6_rt_update_pmtu() creates the exception route only on the nexthop
the Packet Too Big came in through. With a multipath route the other
paths never see that ICMP error and keep sending with the link MTU, so
flows hashed to them blackhole.

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

Link: 
https://lore.kernel.org/netdev/[email protected]/
Signed-off-by: Vladimir Vdovin <[email protected]>
---
 net/ipv6/route.c | 88 +++++++++++++++++++++++++++++++++---------------
 1 file changed, 61 insertions(+), 27 deletions(-)

diff --git a/net/ipv6/route.c b/net/ipv6/route.c
index 34ab7f330540..d870c20f2728 100644
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -2888,6 +2888,30 @@ 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_exception_arg {
+       struct fib6_result      *res;
+       const struct in6_addr   *daddr;
+       const struct in6_addr   *saddr;
+       u32                     mtu;
+};
+
+static int rt6_nh_insert_pmtu_exception(struct fib6_nh *nh, void *_arg)
+{
+       struct rt6_pmtu_exception_arg *arg = _arg;
+       struct fib6_result *res = arg->res;
+       struct rt6_info *nrt6;
+
+       res->nh = nh;
+       nrt6 = ip6_rt_cache_alloc(res, arg->daddr, arg->saddr);
+       if (nrt6) {
+               rt6_do_update_pmtu(nrt6, arg->mtu);
+               if (rt6_insert_exception(nrt6, res))
+                       dst_release_immediate(&nrt6->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)
@@ -2925,42 +2949,52 @@ static void __ip6_rt_update_pmtu(struct dst_entry *dst, 
const struct sock *sk,
                if (rt6->rt6i_flags & RTF_CACHE)
                        rt6_update_exception_stamp_rt(rt6);
        } else if (daddr) {
+               struct fib6_info *f6i, *sibling;
                struct fib6_result res = {};
-               struct rt6_info *nrt6;
+               struct rt6_pmtu_exception_arg arg = {
+                       .res = &res,
+                       .daddr = daddr,
+                       .saddr = saddr,
+                       .mtu = mtu,
+               };
 
                rcu_read_lock();
-               res.f6i = rcu_dereference(rt6->from);
-               if (!res.f6i)
+               f6i = rcu_dereference(rt6->from);
+               if (!f6i)
                        goto out_unlock;
 
-               res.fib6_flags = res.f6i->fib6_flags;
-               res.fib6_type = res.f6i->fib6_type;
+               res.f6i = f6i;
+               res.fib6_flags = f6i->fib6_flags;
+               res.fib6_type = f6i->fib6_type;
 
-               if (res.f6i->nh) {
-                       struct fib6_nh_match_arg arg = {
-                               .dev = dst_dev_rcu(dst),
-                               .gw = &rt6->rt6i_gateway,
-                       };
-
-                       nexthop_for_each_fib6_nh(res.f6i->nh,
-                                                fib6_nh_find_match, &arg);
+               /* A Packet Too Big only ever arrives through the path the
+                * offending packet took, so cache the pmtu on every path of a
+                * multipath route: the other paths will not get their own
+                * ICMP error and would keep sending with the link MTU.
+                */
+               if (f6i->nh) {
+                       nexthop_for_each_fib6_nh(f6i->nh,
+                                                rt6_nh_insert_pmtu_exception,
+                                                &arg);
+                       goto out_unlock;
+               }
 
-                       /* 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;
+               rt6_nh_insert_pmtu_exception(f6i->fib6_nh, &arg);
 
-                       res.nh = arg.match;
-               } else {
-                       res.nh = res.f6i->fib6_nh;
-               }
+               if (!READ_ONCE(f6i->fib6_nsiblings))
+                       goto out_unlock;
 
-               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);
+               /* fib6_del_route() zeroes fib6_nsiblings before unlinking
+                * f6i, stop then as its list_head may no longer be reached.
+                */
+               list_for_each_entry_rcu(sibling, &f6i->fib6_siblings,
+                                       fib6_siblings) {
+                       res.f6i = sibling;
+                       res.fib6_flags = sibling->fib6_flags;
+                       res.fib6_type = sibling->fib6_type;
+                       rt6_nh_insert_pmtu_exception(sibling->fib6_nh, &arg);
+                       if (!READ_ONCE(f6i->fib6_nsiblings))
+                               break;
                }
 out_unlock:
                rcu_read_unlock();
-- 
2.56.0


Reply via email to