* [PATCH net-next] net: advertise TCP MSS from the configured MTU, not the learned PMTU
@ 2026-08-14 6:35 Jiayuan Chen
2026-08-14 9:21 ` Eric Dumazet
0 siblings, 1 reply; 3+ messages in thread
From: Jiayuan Chen @ 2026-08-14 6:35 UTC (permalink / raw)
To: netdev
Cc: Jiayuan Chen, David Ahern, Ido Schimmel, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
linux-kernel
The MSS a host puts in its SYN tells the peer how big a segment it may
send us. Right now we can shrink it with a PMTU we learned on our own
send path, which is the wrong direction entirely.
On asymmetric paths this bites - think DSR load balancers, where the
request side goes through a smaller-MTU overlay. We learn a small PMTU
going out, then advertise a small MSS, and the peer stays capped for the
whole connection even though its path back to us is wide. MSS only shows
up in the SYN and never grows back.
On symmetric paths we lose nothing by dropping it either: the peer runs
its own PMTU discovery and usually already knows the real path MTU.
So work out the advertised MSS from the configured route or device MTU
and ignore the learned PMTU. Our send side is unchanged, still clamped by
tcp_current_mss(). Add ip_dst_mtu_configured()/ip6_dst_mtu_configured()
and use them from the two default_advmss() paths.
Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
---
include/net/ip.h | 25 +++++++++++++++++++++++++
include/net/ip6_route.h | 37 +++++++++++++++++++++++++++++++++++++
net/ipv4/route.c | 4 ++--
net/ipv6/route.c | 2 +-
4 files changed, 65 insertions(+), 3 deletions(-)
diff --git a/include/net/ip.h b/include/net/ip.h
index 7f2fe1a8401b..a8f57b4f4aa2 100644
--- a/include/net/ip.h
+++ b/include/net/ip.h
@@ -506,6 +506,31 @@ static inline unsigned int ip_dst_mtu_maybe_forward(const struct dst_entry *dst,
return res;
}
+/* Configured/administrative MTU of a route, for advertising the TCP MSS.
+ *
+ * Unlike ip_dst_mtu_maybe_forward(), this deliberately ignores the
+ * ICMP-learned path MTU (rt->rt_pmtu). The advertised MSS bounds what the
+ * peer may send to us and must reflect our receive capability (the device or
+ * route-configured MTU), not a path MTU learned on the reverse (send)
+ * direction, which may not apply to the peer->us path and outlives the fnhe
+ * for the whole connection. See RFC 2923 section 2.3 and the comment above
+ * tcp_advertise_mss().
+ */
+static inline unsigned int ip_dst_mtu_configured(const struct dst_entry *dst)
+{
+ unsigned int mtu, res;
+
+ rcu_read_lock();
+ mtu = dst_metric_raw(dst, RTAX_MTU);
+ if (!mtu)
+ mtu = READ_ONCE(dst_dev_rcu(dst)->mtu);
+ mtu = min_t(unsigned int, mtu, IP_MAX_MTU);
+ res = mtu - lwtunnel_headroom(dst->lwtstate, mtu);
+ rcu_read_unlock();
+
+ return res;
+}
+
static inline unsigned int ip_skb_dst_mtu(struct sock *sk,
const struct sk_buff *skb)
{
diff --git a/include/net/ip6_route.h b/include/net/ip6_route.h
index 09ffe0f13ce7..ac1acc0b7436 100644
--- a/include/net/ip6_route.h
+++ b/include/net/ip6_route.h
@@ -385,6 +385,43 @@ static inline unsigned int ip6_dst_mtu_maybe_forward(const struct dst_entry *dst
return mtu - lwtunnel_headroom(dst->lwtstate, mtu);
}
+/* Configured/administrative MTU of a route, for advertising the TCP MSS.
+ *
+ * Unlike ip6_dst_mtu_maybe_forward(), this ignores any ICMPv6-learned path
+ * MTU (which is kept on the RTF_CACHE exception route) and returns the MTU of
+ * the underlying route (fib6_pmtu) or the egress device. The advertised MSS
+ * bounds what the peer may send to us and must reflect our receive
+ * capability, not a path MTU learned on the reverse (send) direction. See
+ * RFC 2923 section 2.3 and the comment above tcp_advertise_mss().
+ */
+static inline unsigned int ip6_dst_mtu_configured(const struct dst_entry *dst)
+{
+ const struct rt6_info *rt = dst_rt6_info(dst);
+ const struct fib6_info *from;
+ struct inet6_dev *idev;
+ unsigned int mtu = 0;
+
+ rcu_read_lock();
+ /* IPv6 keeps the learned PMTU and the configured MTU in the same
+ * RTAX_MTU slot: the learned value sits on this (possibly RTF_CACHE)
+ * dst, the configured one on the underlying route. Reach the latter
+ * via ->from (fib6_pmtu), populated by ip6_route_info_create().
+ */
+ from = rcu_dereference(rt->from);
+ if (from)
+ mtu = from->fib6_pmtu;
+ if (!mtu) {
+ mtu = IPV6_MIN_MTU;
+ idev = __in6_dev_get(dst_dev_rcu(dst));
+ if (idev)
+ mtu = max_t(unsigned int, mtu, READ_ONCE(idev->cnf.mtu6));
+ }
+ rcu_read_unlock();
+
+ mtu = min_t(unsigned int, mtu, IP6_MAX_MTU);
+ return mtu - lwtunnel_headroom(dst->lwtstate, mtu);
+}
+
u32 ip6_mtu_from_fib6(const struct fib6_result *res,
const struct in6_addr *daddr,
const struct in6_addr *saddr);
diff --git a/net/ipv4/route.c b/net/ipv4/route.c
index fd688e1f879f..46aa98c92183 100644
--- a/net/ipv4/route.c
+++ b/net/ipv4/route.c
@@ -1334,8 +1334,8 @@ static unsigned int ipv4_default_advmss(const struct dst_entry *dst)
rcu_read_lock();
net = dst_dev_net_rcu(dst);
- advmss = max_t(unsigned int, ipv4_mtu(dst) - header_size,
- net->ipv4.ip_rt_min_advmss);
+ advmss = max_t(unsigned int, ip_dst_mtu_configured(dst) - header_size,
+ net->ipv4.ip_rt_min_advmss);
rcu_read_unlock();
return min(advmss, IPV4_MAX_PMTU - header_size);
diff --git a/net/ipv6/route.c b/net/ipv6/route.c
index 5968ce5ad150..e2056bd0df3c 100644
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -3261,7 +3261,7 @@ void ip6_sk_redirect(struct sk_buff *skb, struct sock *sk)
static unsigned int ip6_default_advmss(const struct dst_entry *dst)
{
- unsigned int mtu = dst6_mtu(dst);
+ unsigned int mtu = ip6_dst_mtu_configured(dst);
struct net *net;
mtu -= sizeof(struct ipv6hdr) + sizeof(struct tcphdr);
--
2.43.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH net-next] net: advertise TCP MSS from the configured MTU, not the learned PMTU
2026-08-14 6:35 [PATCH net-next] net: advertise TCP MSS from the configured MTU, not the learned PMTU Jiayuan Chen
@ 2026-08-14 9:21 ` Eric Dumazet
2026-08-14 10:42 ` Jiayuan Chen
0 siblings, 1 reply; 3+ messages in thread
From: Eric Dumazet @ 2026-08-14 9:21 UTC (permalink / raw)
To: Jiayuan Chen, Neal Cardwell, Willem de Bruijn
Cc: netdev, David Ahern, Ido Schimmel, David S. Miller,
Jakub Kicinski, Paolo Abeni, Simon Horman, linux-kernel
On Fri, Aug 14, 2026 at 8:36 AM Jiayuan Chen <jiayuan.chen@linux.dev> wrote:
>
> The MSS a host puts in its SYN tells the peer how big a segment it may
> send us. Right now we can shrink it with a PMTU we learned on our own
> send path, which is the wrong direction entirely.
>
> On asymmetric paths this bites - think DSR load balancers, where the
> request side goes through a smaller-MTU overlay. We learn a small PMTU
> going out, then advertise a small MSS, and the peer stays capped for the
> whole connection even though its path back to us is wide. MSS only shows
> up in the SYN and never grows back.
>
> On symmetric paths we lose nothing by dropping it either: the peer runs
> its own PMTU discovery and usually already knows the real path MTU.
>
> So work out the advertised MSS from the configured route or device MTU
> and ignore the learned PMTU. Our send side is unchanged, still clamped by
> tcp_current_mss(). Add ip_dst_mtu_configured()/ip6_dst_mtu_configured()
> and use them from the two default_advmss() paths.
>
> Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
LGTM, but this probably needs Fixes: tags and should be sent to net tree.
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Fixes: 164a5e7ad531 ("ipv4: ipv4_default_advmss() should use route mtu")
Cc: stable@vger.kernel.org
I added the following packetdrill test, please add it in a series.
commit f0c619ca446b669c48b5c2e2c78e23af1a0850b6
Author: Eric Dumazet <edumazet@google.com>
Date: Fri Aug 14 08:12:32 2026 +0000
selftests: net: packetdrill: add tests for advertised MSS with
PMTU exceptions
Add packetdrill tests for IPv4 and IPv6 to verify that the advertised
MSS in SYN-ACK is derived from the configured interface/route MTU,
and is not shrunk by learned Path MTU exceptions from previous
outbound connections.
Signed-off-by: Eric Dumazet <edumazet@google.com>
diff --git a/tools/testing/selftests/net/packetdrill/tcp_advmss_pmtu_ipv4.pkt
b/tools/testing/selftests/net/packetdrill/tcp_advmss_pmtu_ipv4.pkt
new file mode 100644
index 0000000000000000000000000000000000000000..f2ef931b77a1c50e77b7568c7f03d10002be4152
--- /dev/null
+++ b/tools/testing/selftests/net/packetdrill/tcp_advmss_pmtu_ipv4.pkt
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: GPL-2.0
+//
+// Test that IPv4 advertised MSS in SYN-ACK is derived from the configured
+// interface MTU (1500 -> MSS 1460), not the ICMP-learned Path MTU.
+
+--ip_version=ipv4
+
+`./defaults.sh
+ethtool -K tun0 tso off
+`
+
+//
+// Connection 1: Learn PMTU exception (MTU 1200 -> MSS 1160)
+//
+ 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+ +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+ +0 bind(3, ..., ...) = 0
+ +0 listen(3, 1) = 0
+
+ +0 < S 0:0(0) win 65535 <mss 1460,sackOK,nop,nop,nop,wscale 8>
+ +0 > S. 0:0(0) ack 1 <mss 1460,nop,nop,sackOK,nop,wscale 8>
+ +.1 < . 1:1(0) ack 1 win 257
+ +0 accept(3, ..., ...) = 4
+
+// Send a full 1460-byte segment
+ +0 write(4, ..., 1460) = 1460
+ +0 > P. 1:1461(1460) ack 1
+
+// ICMP Fragmentation Needed arrives indicating next-hop MTU 1200
+ +0 < icmp unreachable frag_needed mtu 1200 [1:1461(1460)]
+
+// Local host retransmits using the learned MTU 1200 (MSS = 1200 - 40 = 1160)
+ +0 > . 1:1161(1160) ack 1
+ +0 > P. 1161:1461(300) ack 1
+ +0 < R 1:1(0) ack 1461 win 0
+
+// Close connection 1 and listener
+ +0 close(4) = 0
+ +0 close(3) = 0
+
+//
+// Connection 2: New connection from the same peer
+//
+ +0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+ +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+ +0 bind(3, ..., ...) = 0
+ +0 listen(3, 1) = 0
+
+ +0 < S 0:0(0) win 65535 <mss 1460,sackOK,nop,nop,nop,wscale 8>
+
+// Verify: SYN-ACK MUST advertise configured MSS 1460, NOT the
learned PMTU MSS 1160
+ +0 > S. 0:0(0) ack 1 <mss 1460,nop,nop,sackOK,nop,wscale 8>
+ +0 < . 1:1(0) ack 1 win 257
+ +0 accept(3, ..., ...) = 4
+
+// Verify: Outgoing transmit MSS is still constrained by the learned PMTU 1200
+ +0 write(4, ..., 1460) = 1460
+ +0 > . 1:1161(1160) ack 1
+ +0 > P. 1161:1461(300) ack 1
+ +0 < . 1:1(0) ack 1461 win 257
+
+// Clean up
+ +0 close(4) = 0
+ +0 > F. 1461:1461(0) ack 1
+ +0 < F. 1:1(0) ack 1462 win 257
+ +0 > . 1462:1462(0) ack 2
+ +0 close(3) = 0
diff --git a/tools/testing/selftests/net/packetdrill/tcp_advmss_pmtu_ipv6.pkt
b/tools/testing/selftests/net/packetdrill/tcp_advmss_pmtu_ipv6.pkt
new file mode 100644
index 0000000000000000000000000000000000000000..c7638b11a815b20f70c08a894fed9a65f12327f4
--- /dev/null
+++ b/tools/testing/selftests/net/packetdrill/tcp_advmss_pmtu_ipv6.pkt
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: GPL-2.0
+//
+// Test that IPv6 advertised MSS in SYN-ACK is derived from the configured
+// interface MTU (1520 -> MSS 1460), not the ICMPv6-learned Path MTU.
+
+--ip_version=ipv6
+
+`./defaults.sh
+ethtool -K tun0 tso off
+`
+
+//
+// Connection 1: Learn PMTU exception (MTU 1280 -> MSS 1220)
+//
+ 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+ +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+ +0 bind(3, ..., ...) = 0
+ +0 listen(3, 1) = 0
+
+ +0 < S 0:0(0) win 65535 <mss 1460,sackOK,nop,nop,nop,wscale 8>
+ +0 > S. 0:0(0) ack 1 <mss 1460,nop,nop,sackOK,nop,wscale 8>
+ +.1 < . 1:1(0) ack 1 win 257
+ +0 accept(3, ..., ...) = 4
+
+// Send a full 1460-byte segment
+ +0 write(4, ..., 1460) = 1460
+ +0 > P. 1:1461(1460) ack 1
+
+// ICMPv6 Packet Too Big arrives indicating next-hop MTU 1280
+ +0 < icmp packet_too_big mtu 1280 [1:1461(1460)]
+
+// Local host retransmits using the learned MTU 1280 (MSS = 1280 - 40
- 20 = 1220)
+ +0 > . 1:1221(1220) ack 1
+ +0 > P. 1221:1461(240) ack 1
+ +0 < R 1:1(0) ack 1461 win 0
+
+// Close connection 1 and listener
+ +0 close(4) = 0
+ +0 close(3) = 0
+
+//
+// Connection 2: New connection from the same peer
+//
+ +0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+ +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+ +0 bind(3, ..., ...) = 0
+ +0 listen(3, 1) = 0
+
+ +0 < S 0:0(0) win 65535 <mss 1460,sackOK,nop,nop,nop,wscale 8>
+
+// Verify: SYN-ACK MUST advertise configured MSS 1460, NOT the
learned PMTU MSS 1220
+ +0 > S. 0:0(0) ack 1 <mss 1460,nop,nop,sackOK,nop,wscale 8>
+ +0 < . 1:1(0) ack 1 win 257
+ +0 accept(3, ..., ...) = 4
+
+// Verify: Outgoing transmit MSS is still constrained by the learned PMTU 1280
+ +0 write(4, ..., 1460) = 1460
+ +0 > . 1:1221(1220) ack 1
+ +0 > P. 1221:1461(240) ack 1
+ +0 < . 1:1(0) ack 1461 win 257
+
+// Clean up
+ +0 close(4) = 0
+ +0 > F. 1461:1461(0) ack 1
+ +0 < F. 1:1(0) ack 1462 win 257
+ +0 > . 1462:1462(0) ack 2
+ +0 close(3) = 0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH net-next] net: advertise TCP MSS from the configured MTU, not the learned PMTU
2026-08-14 9:21 ` Eric Dumazet
@ 2026-08-14 10:42 ` Jiayuan Chen
0 siblings, 0 replies; 3+ messages in thread
From: Jiayuan Chen @ 2026-08-14 10:42 UTC (permalink / raw)
To: Eric Dumazet, Neal Cardwell, Willem de Bruijn
Cc: netdev, David Ahern, Ido Schimmel, David S. Miller,
Jakub Kicinski, Paolo Abeni, Simon Horman, linux-kernel
On 8/14/26 5:21 PM, Eric Dumazet wrote:
> On Fri, Aug 14, 2026 at 8:36 AM Jiayuan Chen <jiayuan.chen@linux.dev> wrote:
>> The MSS a host puts in its SYN tells the peer how big a segment it may
>> send us. Right now we can shrink it with a PMTU we learned on our own
>> send path, which is the wrong direction entirely.
>>
>> On asymmetric paths this bites - think DSR load balancers, where the
>> request side goes through a smaller-MTU overlay. We learn a small PMTU
>> going out, then advertise a small MSS, and the peer stays capped for the
>> whole connection even though its path back to us is wide. MSS only shows
>> up in the SYN and never grows back.
>>
>> On symmetric paths we lose nothing by dropping it either: the peer runs
>> its own PMTU discovery and usually already knows the real path MTU.
>>
>> So work out the advertised MSS from the configured route or device MTU
>> and ignore the learned PMTU. Our send side is unchanged, still clamped by
>> tcp_current_mss(). Add ip_dst_mtu_configured()/ip6_dst_mtu_configured()
>> and use them from the two default_advmss() paths.
>>
>> Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
>
> LGTM, but this probably needs Fixes: tags and should be sent to net tree.
>
> Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
> Fixes: 164a5e7ad531 ("ipv4: ipv4_default_advmss() should use route mtu")
> Cc: stable@vger.kernel.org
>
> I added the following packetdrill test, please add it in a series.
>
> commit f0c619ca446b669c48b5c2e2c78e23af1a0850b6
> Author: Eric Dumazet <edumazet@google.com>
Thanks Eric, I will do it after cold time.
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-08-14 10:43 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-14 6:35 [PATCH net-next] net: advertise TCP MSS from the configured MTU, not the learned PMTU Jiayuan Chen
2026-08-14 9:21 ` Eric Dumazet
2026-08-14 10:42 ` Jiayuan Chen
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox