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 that already has an exception for the
destination gets it updated in place, so its gateway from a redirect
stays and its pmtu never goes up, and that also covers a dst that is
itself a cached exception, which used to be updated alone.
Link: https://lore.kernel.org/netdev/ad29493c-9e48-4a94-b327-e47af3a9d137@kernel.org/ (local)
Signed-off-by: Vladimir Vdovin <redacted>
---
net/ipv6/route.c | 124 +++++++++++++++++++++++++++++++----------------
1 file changed, 82 insertions(+), 42 deletions(-)
diff --git a/net/ipv6/route.c b/net/ipv6/route.c
index 04098cf32eab..dc982400fbab 100644
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -2911,12 +2911,52 @@ 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: update the exception the path already
+ * has for daddr, keeping its gateway and never raising its pmtu, or create one.
+ */
+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) {
+ u32 mtu = dst_metric_raw(&rt->dst, RTAX_MTU);
+
+ if (!mtu || arg->mtu < mtu) {
+ rt6_do_update_pmtu(rt, arg->mtu);
+ fib6_nh_update_exception(nh, res->f6i->fib6_src.plen, rt);
+ }
+ 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.@@ -2942,52 +2982,52 @@ static void __ip6_rt_update_pmtu(struct dst_entry *dst, const struct sock *sk,
if (mtu >= dst6_mtu(dst))
return;
- if (!rt6_cache_allowed_for_pmtu(rt6)) {
- 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,
- };
+ rcu_read_lock();
+ f6i = rcu_dereference(rt6->from);
+ /* fib6_del_route() and a NLM_F_REPLACE clear fib6_node before the
+ * route and its siblings are released, don't walk them then.
+ */
+ if (!daddr || !f6i || !rcu_access_pointer(f6i->fib6_node)) {
+ if (!rt6_cache_allowed_for_pmtu(rt6)) {
+ rt6_do_update_pmtu(rt6, mtu);
+ /* update rt6_ex->stamp for cache */
+ if (rt6->rt6i_flags & RTF_CACHE)
+ rt6_update_exception_stamp_rt(rt6);
+ }
+ 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;
+ /* ip6_update_pmtu() looks dst up without the protocol and ports the
+ * too big packet was hashed on, so dst may be another path of a
+ * multipath route than the one that packet took. Cache the pmtu on
+ * every path, or that flow would keep sending too big packets.
+ */
+ 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