* [PATCH net-next v3 1/2] tcp: keep the route window metric when SO_RCVBUF is set before connect
2026-10-12 13:26 [PATCH net-next v3 0/2] tcp: keep the route window metric when SO_RCVBUF is set before connect Shashank Mohan Jain
@ 2026-10-12 13:26 ` Shashank Mohan Jain
2026-10-12 13:26 ` [PATCH net-next v3 2/2] selftests/net/packetdrill: cover SO_RCVBUF vs the route window clamp Shashank Mohan Jain
2026-10-12 13:30 ` [PATCH net-next v3 0/2] tcp: keep the route window metric when SO_RCVBUF is set before connect netdev-bot+sinfo
2 siblings, 0 replies; 5+ messages in thread
From: Shashank Mohan Jain @ 2026-10-12 13:26 UTC (permalink / raw)
To: Eric Dumazet, Neal Cardwell, Jakub Kicinski, Paolo Abeni,
David S. Miller
Cc: Kuniyuki Iwashima, Simon Horman, netdev, linux-kernel
Since commit b025461303d8 ("tcp: update window_clamp when SO_RCVBUF is
set"), setsockopt(SO_RCVBUF) writes tp->window_clamp in every socket
state through tcp_set_window_clamp().
tcp_connect_init() and tcp_openreq_init_rwin() treat any non-zero
window_clamp as a clamp the user asked for and, in that case, skip the
route window metric (RTAX_WINDOW, "ip route ... window N"). A clamp left
behind by an SO_RCVBUF call therefore has two observable effects:
(1) on a route that carries an RTAX_WINDOW metric, SO_RCVBUF set before
connect() (or on a listener) changes the window and window scale
advertised in the SYN / SYN-ACK, because the route metric is no
longer applied;
(2) a window clamp the application set with setsockopt(TCP_WINDOW_CLAMP)
is overwritten by a later SO_RCVBUF on the still unconnected socket
or on a listener.
Without an RTAX_WINDOW metric there is no change: both before and after
this patch the advertised window and scale are derived from sk_rcvbuf
(tcp_full_space()), so only (1) and (2) are affected.
Keying the decision off the socket state is not enough, because the
socket is already in TCP_SYN_SENT before tcp_connect_init() runs.
SO_RCVBUF applied in that gap - through TCP_FASTOPEN_CONNECT before the
first write(), or through bpf_setsockopt() in a
BPF_SOCK_OPS_TCP_CONNECT_CB program (as samples/bpf/tcp_clamp_kern.c and
friends do) - still lands before tcp_connect_init() and still overrides
the metric.
Record instead whether window_clamp was set explicitly by the user via
setsockopt(TCP_WINDOW_CLAMP), in a new tp->window_clamp_user bit, and key
tcp_connect_init() and tcp_openreq_init_rwin() off that bit rather than
off a non-zero window_clamp. The route metric is then applied on all
pre-connection paths, regardless of socket state, whenever the clamp was
not set by the user; a passive socket inherits the decision from its
listener. SO_RCVBUF no longer overwrites a genuine TCP_WINDOW_CLAMP
either. When SOCK_RCVBUF_LOCK is set both functions still clamp
window_clamp to tcp_full_space(), and without a user clamp SO_RCVBUF
keeps updating window_clamp, so SO_RCVBUF on an established socket still
grows the advertised window as b025461303d8 intended.
Example: two netns joined by a veth pair, "ip route ... window 4096" on
the route to the peer, SO_RCVBUF set to 1 MiB before connect():
before: SYN win 65535, wscale 5
after: SYN win 2920, wscale 0
(the window scale option is still sent, with a shift of 0.)
Fixes: b025461303d8 ("tcp: update window_clamp when SO_RCVBUF is set")
Assisted-by: LLM
Signed-off-by: Shashank Mohan Jain <jain.sm@gmail.com>
---
include/linux/tcp.h | 3 ++-
net/ipv4/tcp.c | 12 ++++++++++++
net/ipv4/tcp_minisocks.c | 7 +++++--
net/ipv4/tcp_output.c | 2 +-
4 files changed, 20 insertions(+), 4 deletions(-)
diff --git a/include/linux/tcp.h b/include/linux/tcp.h
index e4d1720f4..2324841e5 100644
--- a/include/linux/tcp.h
+++ b/include/linux/tcp.h
@@ -301,7 +301,8 @@ struct tcp_sock {
* 0x5?10 << 16 + snd_wnd in net byte order
*/
u8 nonagle : 4,/* Disable Nagle algorithm? */
- rate_app_limited:1; /* rate_{delivered,interval_us} limited? */
+ rate_app_limited:1, /* rate_{delivered,interval_us} limited? */
+ window_clamp_user:1; /* TCP_WINDOW_CLAMP set by the user */
u8 received_ce_pending:4, /* Not yet transmit cnt of received_ce */
accecn_opt_sent_w_dsack:1,/* Sent ACCECN opt in previous ACK w/ D-SACK */
unused2:3;
diff --git a/net/ipv4/tcp.c b/net/ipv4/tcp.c
index f4c9101e5..5f364928b 100644
--- a/net/ipv4/tcp.c
+++ b/net/ipv4/tcp.c
@@ -1858,6 +1858,15 @@ int tcp_set_rcvlowat(struct sock *sk, int val)
void tcp_set_rcvbuf(struct sock *sk, int val)
{
+ /* A window clamp set explicitly via setsockopt(TCP_WINDOW_CLAMP)
+ * takes precedence: SO_RCVBUF must not overwrite it. Without such
+ * a user clamp, keep updating window_clamp from the buffer size so
+ * that SO_RCVBUF on an established socket still grows the advertised
+ * window (commit b025461303d8).
+ */
+ if (tcp_sk(sk)->window_clamp_user)
+ return;
+
tcp_set_window_clamp(sk, tcp_win_from_space(sk, val));
}
@@ -3427,6 +3436,7 @@ int tcp_disconnect(struct sock *sk, int flags)
tp->is_cwnd_limited = 0;
tp->max_packets_out = 0;
tp->window_clamp = 0;
+ tp->window_clamp_user = 0;
tp->delivered = 0;
tp->delivered_ce = 0;
tp->accecn_fail_mode = 0;
@@ -4056,6 +4066,8 @@ int do_tcp_setsockopt(struct sock *sk, int level, int optname,
case TCP_WINDOW_CLAMP:
err = tcp_set_window_clamp(sk, val);
+ if (!err)
+ tp->window_clamp_user = !!val;
break;
case TCP_QUICKACK:
diff --git a/net/ipv4/tcp_minisocks.c b/net/ipv4/tcp_minisocks.c
index 0ddfd5af6..c24dc975a 100644
--- a/net/ipv4/tcp_minisocks.c
+++ b/net/ipv4/tcp_minisocks.c
@@ -442,8 +442,11 @@ void tcp_openreq_init_rwin(struct request_sock *req,
mss = tcp_mss_clamp(tp, tcp_dst_advmss(dst));
window_clamp = READ_ONCE(tp->window_clamp);
- /* Set this up on the first call only */
- req->rsk_window_clamp = window_clamp ? : dst_metric(dst, RTAX_WINDOW);
+ /* Set this up on the first call only. Honour a window clamp the
+ * listener set via TCP_WINDOW_CLAMP; otherwise use the route metric.
+ */
+ req->rsk_window_clamp = tp->window_clamp_user ? window_clamp :
+ dst_metric(dst, RTAX_WINDOW);
/* limit the window selection if the user enforce a smaller rx buffer */
if (sk_listener->sk_userlocks & SOCK_RCVBUF_LOCK &&
diff --git a/net/ipv4/tcp_output.c b/net/ipv4/tcp_output.c
index c8865205c..21b4ae303 100644
--- a/net/ipv4/tcp_output.c
+++ b/net/ipv4/tcp_output.c
@@ -4137,7 +4137,7 @@ static void tcp_connect_init(struct sock *sk)
tcp_ca_dst_init(sk, dst);
- if (!tp->window_clamp)
+ if (!tp->window_clamp_user)
WRITE_ONCE(tp->window_clamp, dst_metric(dst, RTAX_WINDOW));
tp->advmss = tcp_mss_clamp(tp, tcp_dst_advmss(dst));
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH net-next v3 2/2] selftests/net/packetdrill: cover SO_RCVBUF vs the route window clamp
2026-10-12 13:26 [PATCH net-next v3 0/2] tcp: keep the route window metric when SO_RCVBUF is set before connect Shashank Mohan Jain
2026-10-12 13:26 ` [PATCH net-next v3 1/2] " Shashank Mohan Jain
@ 2026-10-12 13:26 ` Shashank Mohan Jain
2026-10-12 13:30 ` [PATCH net-next v3 0/2] tcp: keep the route window metric when SO_RCVBUF is set before connect netdev-bot+sinfo
2 siblings, 0 replies; 5+ messages in thread
From: Shashank Mohan Jain @ 2026-10-12 13:26 UTC (permalink / raw)
To: Eric Dumazet, Neal Cardwell, Jakub Kicinski, Paolo Abeni,
David S. Miller
Cc: Kuniyuki Iwashima, Simon Horman, netdev, linux-kernel
Add packetdrill tests for the interaction between SO_RCVBUF, a user
TCP_WINDOW_CLAMP and the route "window" metric (RTAX_WINDOW) on the SYN:
- tcp_rcvbuf_before_connect_window.pkt: SO_RCVBUF set before connect()
must not defeat the route metric; the SYN must advertise the
route-clamped window and window scale 0.
- tcp_window_clamp_vs_rcvbuf.pkt: a clamp set with TCP_WINDOW_CLAMP must
be honoured on the SYN and not be overwritten by a later SO_RCVBUF on
the still unconnected socket.
- tcp_fastopen_connect_rcvbuf_window.pkt: with TCP_FASTOPEN_CONNECT the
SYN is deferred to the first write() while the socket sits in
TCP_SYN_SENT; SO_RCVBUF applied in that gap must not defeat the route
metric on the deferred SYN either.
Without the first patch of this series the SYN carried win 65535,
wscale 5 in the first and third cases (the route metric was skipped) and
the SO_RCVBUF-derived window in the second (the user clamp was lost).
The tests are picked up automatically by the packetdrill selftest
Makefile (TEST_PROGS := $(wildcard *.pkt)).
Assisted-by: LLM
Signed-off-by: Shashank Mohan Jain <jain.sm@gmail.com>
---
.../tcp_fastopen_connect_rcvbuf_window.pkt | 22 +++++++++++++++
.../tcp_rcvbuf_before_connect_window.pkt | 27 +++++++++++++++++++
.../tcp_window_clamp_vs_rcvbuf.pkt | 24 +++++++++++++++++
3 files changed, 73 insertions(+)
create mode 100644 tools/testing/selftests/net/packetdrill/tcp_fastopen_connect_rcvbuf_window.pkt
create mode 100644 tools/testing/selftests/net/packetdrill/tcp_rcvbuf_before_connect_window.pkt
create mode 100644 tools/testing/selftests/net/packetdrill/tcp_window_clamp_vs_rcvbuf.pkt
diff --git a/tools/testing/selftests/net/packetdrill/tcp_fastopen_connect_rcvbuf_window.pkt b/tools/testing/selftests/net/packetdrill/tcp_fastopen_connect_rcvbuf_window.pkt
new file mode 100644
index 000000000..7892ac566
--- /dev/null
+++ b/tools/testing/selftests/net/packetdrill/tcp_fastopen_connect_rcvbuf_window.pkt
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0
+// TCP_FASTOPEN_CONNECT defers the SYN until the first write(); between
+// connect() and that write the socket is in TCP_SYN_SENT. SO_RCVBUF applied
+// in that gap must not defeat the route "window" metric on the deferred SYN.
+// optname 30 = TCP_FASTOPEN_CONNECT, 34 = TCP_FASTOPEN_NO_COOKIE on SOL_TCP.
+--ip_version=ipv4
+
+`./defaults.sh
+sysctl -q net.ipv4.tcp_fastopen=5
+ip route replace 192.0.2.1 via 192.168.0.1 dev tun0 window 4096`
+
+ 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+ +0 setsockopt(3, SOL_TCP, 30, [1], 4) = 0
+ +0 setsockopt(3, SOL_TCP, 34, [1], 4) = 0
+ +0 connect(3, ..., ...) = 0
+ +0 setsockopt(3, SOL_SOCKET, SO_RCVBUF, [1048576], 4) = 0
+ +0 fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0
+ +0 write(3, ..., 1) = 1
+
+// The deferred SYN (carrying 1 byte of data) must advertise the route
+// clamped window and window scale 0 (not win 65535, wscale 5).
+ +0 > S 0:1(1) win 2920 <mss 1460,sackOK,TS val 0 ecr 0,nop,wscale 0>
diff --git a/tools/testing/selftests/net/packetdrill/tcp_rcvbuf_before_connect_window.pkt b/tools/testing/selftests/net/packetdrill/tcp_rcvbuf_before_connect_window.pkt
new file mode 100644
index 000000000..7ec7f14d8
--- /dev/null
+++ b/tools/testing/selftests/net/packetdrill/tcp_rcvbuf_before_connect_window.pkt
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: GPL-2.0
+// Setting SO_RCVBUF before connect() must not defeat the route "window"
+// metric on the SYN.
+//
+// The route to the peer carries "window 4096" (RTAX_WINDOW). With SO_RCVBUF
+// also set before connect(), the SYN the kernel sends must still advertise
+// the small, route-clamped receive window and a window scale of 0 (the
+// window scale option is sent, with a shift of 0).
+//
+// Since commit b025461303d8 ("tcp: update window_clamp when SO_RCVBUF is
+// set"), setsockopt(SO_RCVBUF) wrote tp->window_clamp while the socket was
+// still in TCP_CLOSE. tcp_connect_init() then treated that as a user clamp
+// and skipped RTAX_WINDOW, so the SYN carried win 65535, wscale 5 instead.
+--ip_version=ipv4
+
+`./defaults.sh
+ip route replace 192.0.2.1 via 192.168.0.1 dev tun0 window 4096`
+
+ 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+ +0 setsockopt(3, SOL_SOCKET, SO_RCVBUF, [1048576], 4) = 0
+ +0 fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0
+ +0 connect(3, ..., ...) = -1 EINPROGRESS (Operation now in progress)
+
+// The SYN must advertise the route-clamped window and window scale 0.
+ +0 > S 0:0(0) win 2920 <mss 1460,sackOK,TS val 0 ecr 0,nop,wscale 0>
+ +.1 < S. 0:0(0) ack 1 win 65535 <mss 1460,nop,wscale 7>
+ +0 > . 1:1(0) ack 1
diff --git a/tools/testing/selftests/net/packetdrill/tcp_window_clamp_vs_rcvbuf.pkt b/tools/testing/selftests/net/packetdrill/tcp_window_clamp_vs_rcvbuf.pkt
new file mode 100644
index 000000000..3d73178df
--- /dev/null
+++ b/tools/testing/selftests/net/packetdrill/tcp_window_clamp_vs_rcvbuf.pkt
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0
+// A window clamp the user set with setsockopt(TCP_WINDOW_CLAMP) must be
+// honoured on the SYN even when SO_RCVBUF is set afterwards on the still
+// unconnected socket: SO_RCVBUF must not overwrite the user's clamp, and
+// the route "window" metric must not replace it either.
+//
+// TCP_WINDOW_CLAMP is optname 10 on SOL_TCP (this packetdrill build has no
+// symbol for it).
+--ip_version=ipv4
+
+`./defaults.sh
+ip route replace 192.0.2.1 via 192.168.0.1 dev tun0 window 4096`
+
+ 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+ +0 setsockopt(3, SOL_TCP, 10, [16384], 4) = 0
+ +0 setsockopt(3, SOL_SOCKET, SO_RCVBUF, [1048576], 4) = 0
+ +0 fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0
+ +0 connect(3, ..., ...) = -1 EINPROGRESS (Operation now in progress)
+
+// The SYN advertises the user's clamp (win 16060), not the route window
+// (2920) and not the SO_RCVBUF-derived full window.
+ +0 > S 0:0(0) win 16060 <mss 1460,sackOK,TS val 0 ecr 0,nop,wscale 0>
+ +.1 < S. 0:0(0) ack 1 win 65535 <mss 1460,nop,wscale 7>
+ +0 > . 1:1(0) ack 1
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread