Netdev List
 help / color / mirror / Atom feed
From: Willem Ferguson <wferguson@cloudflare.com>
To: netdev@vger.kernel.org
Cc: davem@davemloft.net, edumazet@kernel.org, kuba@kernel.org,
	pabeni@redhat.com, ncardwell@google.com, bpf@vger.kernel.org,
	ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org,
	eddyz87@gmail.com, memxor@gmail.com, linux-doc@vger.kernel.org,
	corbet@lwn.net, linux-kselftest@vger.kernel.org,
	shuah@kernel.org, linux-kernel@vger.kernel.org,
	kernel-team@cloudflare.com
Subject: [RFC PATCH net-next 1/4] tcp: add plumbing to carry an identifier in TSval low bits
Date: Tue, 13 Oct 2026 10:39:03 +0000	[thread overview]
Message-ID: <20261013103906.2660384-2-wferguson@cloudflare.com> (raw)
In-Reply-To: <20261013103906.2660384-1-wferguson@cloudflare.com>

Add per-connection state that lets the low bits of locally generated
TSvals carry a small identifier, and teach the receive side to strip it
from echoed TSecr values:

 - tcp_request_sock, tcp_sock and tcp_timewait_sock gain ts_id and
   ts_id_bits. They are copied from the request socket to the child and
   from the child to the timewait socket, and cleared on every path that
   creates a request socket without going through tcp_openreq_init()
   (SYN cookies and bpf_sk_assign_tcp_reqsk()).

 - tcp_tsval_encode() shifts the timestamp clock left by ts_id_bits and
   ORs in ts_id. It is used for SYN-ACK, established, request socket and
   timewait TSvals. The SYN-ACK path keeps the identifier when it
   replaces a zero TSval with ~0U.

 - tcp_tsecr_decode() undoes the encoding. The shift drops ts_id_bits of
   clock range, so the decoded echo is re-centred on the current clock
   with sign_extend32() before RTT estimation and PAWS consume it.

A BUILD_BUG_ON() checks that, at the widest identifier allowed, an
encoded TSval cannot advance by 2^31 within TCP_PAWS_WRAP, the idle
period after which Linux stops enforcing PAWS against ts_recent.

Nothing sets ts_id_bits yet, so this patch does not change behaviour.
With ts_id_bits == 0, tcp_tsval_encode() returns its input and the
established receive path adds a single likely() branch before the
existing tsoffset subtraction.

Signed-off-by: Willem Ferguson <wferguson@cloudflare.com>
---
 .../networking/net_cachelines/tcp_sock.rst    |  2 +
 include/linux/tcp.h                           |  8 +++
 include/net/tcp.h                             | 50 ++++++++++++++++++-
 net/core/filter.c                             |  2 +
 net/ipv4/syncookies.c                         |  2 +
 net/ipv4/tcp.c                                | 13 +++++
 net/ipv4/tcp_input.c                          | 11 ++--
 net/ipv4/tcp_minisocks.c                      | 18 ++++++-
 net/ipv4/tcp_output.c                         | 15 ++++--
 9 files changed, 110 insertions(+), 11 deletions(-)

diff --git a/Documentation/networking/net_cachelines/tcp_sock.rst b/Documentation/networking/net_cachelines/tcp_sock.rst
index 103328dc409e..91a00194ad3a 100644
--- a/Documentation/networking/net_cachelines/tcp_sock.rst
+++ b/Documentation/networking/net_cachelines/tcp_sock.rst
@@ -32,6 +32,8 @@ u32                           lsndtime                read_write
 u32                           last_oow_ack_time
 u32                           compressed_ack_rcv_nxt
 u32                           tsoffset                read_mostly         read_mostly         tcp_established_options(tx);tcp_fast_parse_options(rx)
+u8                            ts_id                   read_mostly         read_mostly         tcp_established_options(tx);tcp_fast_parse_options(rx)
+u8                            ts_id_bits              read_mostly         read_mostly         tcp_established_options(tx);tcp_fast_parse_options(rx)
 struct list_head              tsq_node
 struct list_head              tsorted_sent_queue      read_write                              tcp_update_skb_after_send
 u32                           snd_wl1                                     read_mostly         tcp_may_update_window
diff --git a/include/linux/tcp.h b/include/linux/tcp.h
index e4d1720f49f0..6de7ace4b016 100644
--- a/include/linux/tcp.h
+++ b/include/linux/tcp.h
@@ -145,6 +145,8 @@ static inline void tcp_clear_options(struct tcp_options_received *rx_opt)
  * only four options will fit in a standard TCP header */
 #define TCP_NUM_SACKS 4
 
+#define TCP_TS_ID_BITS_MAX	8
+
 struct tcp_request_sock_ops;
 
 struct tcp_request_sock {
@@ -154,6 +156,8 @@ struct tcp_request_sock {
 	bool				tfo_listener;
 	bool				is_mptcp;
 	bool				req_usec_ts;
+	u8				ts_id;
+	u8				ts_id_bits;
 #if IS_ENABLED(CONFIG_MPTCP)
 	bool				drop_req;
 #endif
@@ -220,6 +224,8 @@ struct tcp_sock {
 	/* TXRX read-mostly hotpath cache lines */
 	__cacheline_group_begin(tcp_sock_read_txrx);
 	u32	tsoffset;	/* timestamp offset */
+	u8	ts_id;		/* identifier encoded in TSval low bits */
+	u8	ts_id_bits;	/* number of identifier bits in TSval */
 	u32	snd_wnd;	/* The window we expect to receive	*/
 	u32	mss_cache;	/* Cached effective mss, not including SACKS */
 	u32	snd_cwnd;	/* Sending congestion window		*/
@@ -575,6 +581,8 @@ struct tcp_timewait_sock {
 	u32			  tw_rcv_wnd;
 	u32			  tw_ts_offset;
 	u32			  tw_ts_recent;
+	u8			  tw_ts_id;
+	u8			  tw_ts_id_bits;
 
 	/* The time we sent the last out-of-window ACK: */
 	u32			  tw_last_oow_ack_time;
diff --git a/include/net/tcp.h b/include/net/tcp.h
index 0265c27a5fd7..85d3038f41af 100644
--- a/include/net/tcp.h
+++ b/include/net/tcp.h
@@ -1074,14 +1074,60 @@ static inline u32 tcp_skb_timestamp_ts(bool usec_ts, const struct sk_buff *skb)
 	return div_u64(skb->skb_mstamp_ns, NSEC_PER_MSEC);
 }
 
+static inline u32 tcp_ts_id_mask(u8 ts_id_bits)
+{
+	return BIT(ts_id_bits) - 1;
+}
+
+static inline u32 tcp_ts_clock_mask(u8 ts_id_bits)
+{
+	return U32_MAX >> ts_id_bits;
+}
+
+static inline u32 tcp_tsval_encode(u32 tsval, u8 ts_id, u8 ts_id_bits)
+{
+	if (!ts_id_bits)
+		return tsval;
+
+	return ((tsval & tcp_ts_clock_mask(ts_id_bits)) << ts_id_bits) |
+	       ts_id;
+}
+
+static inline u32 tcp_tsecr_decode(u32 tsecr, u32 tsoffset, u8 ts_id_bits,
+				   u32 now)
+{
+	if (!ts_id_bits)
+		return tsecr - tsoffset;
+
+	tsecr = ((tsecr >> ts_id_bits) - tsoffset) &
+		tcp_ts_clock_mask(ts_id_bits);
+
+	return now + sign_extend32(tsecr - now, 31 - ts_id_bits);
+}
+
+/* Keeps the timestamp clock read behind the branch: it is only needed to
+ * re-center an echo that was truncated to make room for an identifier.
+ */
+static inline u32 tcp_tsecr_decode_tp(u32 tsecr, const struct tcp_sock *tp)
+{
+	if (likely(!tp->ts_id_bits))
+		return tsecr - tp->tsoffset;
+
+	return tcp_tsecr_decode(tsecr, tp->tsoffset, tp->ts_id_bits,
+				tcp_time_stamp_ts(tp));
+}
+
 static inline u32 tcp_tw_tsval(const struct tcp_timewait_sock *tcptw)
 {
-	return tcp_clock_ts(tcptw->tw_sk.tw_usec_ts) + tcptw->tw_ts_offset;
+	return tcp_tsval_encode(tcp_clock_ts(tcptw->tw_sk.tw_usec_ts) +
+				tcptw->tw_ts_offset, tcptw->tw_ts_id,
+				tcptw->tw_ts_id_bits);
 }
 
 static inline u32 tcp_rsk_tsval(const struct tcp_request_sock *treq)
 {
-	return tcp_clock_ts(treq->req_usec_ts) + treq->ts_off;
+	return tcp_tsval_encode(tcp_clock_ts(treq->req_usec_ts) + treq->ts_off,
+				treq->ts_id, treq->ts_id_bits);
 }
 
 #define tcp_flag_byte(th) (((u_int8_t *)th)[13])
diff --git a/net/core/filter.c b/net/core/filter.c
index 70dc621672f2..a0a74c7e2e3d 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -12579,6 +12579,8 @@ __bpf_kfunc int bpf_sk_assign_tcp_reqsk(struct __sk_buff *s, struct sock *sk,
 
 	treq->req_usec_ts = !!attrs->usec_ts_ok;
 	treq->ts_off = tsoff;
+	treq->ts_id = 0;
+	treq->ts_id_bits = 0;
 
 	skb_orphan(skb);
 	skb->sk = req_to_sk(req);
diff --git a/net/ipv4/syncookies.c b/net/ipv4/syncookies.c
index 73e129768184..f0d164846a5d 100644
--- a/net/ipv4/syncookies.c
+++ b/net/ipv4/syncookies.c
@@ -280,6 +280,8 @@ static int cookie_tcp_reqsk_init(struct sock *sk, struct sk_buff *skb,
 	treq->snt_synack = 0;
 	treq->snt_tsval_first = 0;
 	treq->tfo_listener = false;
+	treq->ts_id = 0;
+	treq->ts_id_bits = 0;
 	treq->rcv_isn = ntohl(th->seq) - 1;
 	treq->snt_isn = ntohl(th->ack_seq) - 1;
 	/* The request socket was freed after the SYN-ACK; use the cookie
diff --git a/net/ipv4/tcp.c b/net/ipv4/tcp.c
index f4c9101e5996..af994edde7f7 100644
--- a/net/ipv4/tcp.c
+++ b/net/ipv4/tcp.c
@@ -462,6 +462,8 @@ void tcp_init_sock(struct sock *sk)
 	tcp_assign_congestion_control(sk);
 
 	tp->tsoffset = 0;
+	tp->ts_id = 0;
+	tp->ts_id_bits = 0;
 	tp->rack.reo_wnd_steps = 1;
 
 	sk->sk_write_space = sk_stream_write_space;
@@ -3429,6 +3431,8 @@ int tcp_disconnect(struct sock *sk, int flags)
 	tp->window_clamp = 0;
 	tp->delivered = 0;
 	tp->delivered_ce = 0;
+	tp->ts_id = 0;
+	tp->ts_id_bits = 0;
 	tp->accecn_fail_mode = 0;
 	tp->saw_accecn_opt = TCP_ACCECN_OPT_NOT_SEEN;
 	tcp_accecn_init_counters(tp);
@@ -5212,6 +5216,8 @@ static void __init tcp_struct_check(void)
 
 	/* TXRX read-mostly hotpath cache lines */
 	CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_read_txrx, tsoffset);
+	CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_read_txrx, ts_id);
+	CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_read_txrx, ts_id_bits);
 	CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_read_txrx, snd_wnd);
 	CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_read_txrx, mss_cache);
 	CACHELINE_ASSERT_GROUP_MEMBER(struct tcp_sock, tcp_sock_read_txrx, snd_cwnd);
@@ -5302,6 +5308,13 @@ void __init tcp_init(void)
 	BUILD_BUG_ON(sizeof(struct tcp_skb_cb) >
 		     sizeof_field(struct sk_buff, cb));
 
+	/* An encoded TSval must not advance by 2^31 within the window that
+	 * Linux PAWS keeps ts_recent valid; peers using RFC 7323's 24 day
+	 * window are not covered (see tcp_ts_id_bits in ip-sysctl.rst).
+	 */
+	BUILD_BUG_ON(((u64)TCP_PAWS_WRAP * MSEC_PER_SEC) <<
+		     TCP_TS_ID_BITS_MAX >= (1ULL << 31));
+
 	tcp_struct_check();
 
 	percpu_counter_init(&tcp_sockets_allocated, 0, GFP_KERNEL);
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
index 1ddce9c4b7e8..68af0283742e 100644
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -4717,7 +4717,8 @@ static bool tcp_parse_aligned_timestamp(struct tcp_sock *tp, const struct tcphdr
 		tp->rx_opt.rcv_tsval = ntohl(*ptr);
 		++ptr;
 		if (*ptr)
-			tp->rx_opt.rcv_tsecr = ntohl(*ptr) - tp->tsoffset;
+			tp->rx_opt.rcv_tsecr = tcp_tsecr_decode_tp(ntohl(*ptr),
+								   tp);
 		else
 			tp->rx_opt.rcv_tsecr = 0;
 		return true;
@@ -4749,7 +4750,8 @@ static bool tcp_fast_parse_options(const struct net *net,
 
 	tcp_parse_options(net, skb, &tp->rx_opt, 1, NULL);
 	if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr)
-		tp->rx_opt.rcv_tsecr -= tp->tsoffset;
+		tp->rx_opt.rcv_tsecr =
+			tcp_tsecr_decode_tp(tp->rx_opt.rcv_tsecr, tp);
 
 	return true;
 }
@@ -6881,7 +6883,8 @@ static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
 
 	tcp_parse_options(sock_net(sk), skb, &tp->rx_opt, 0, &foc);
 	if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr)
-		tp->rx_opt.rcv_tsecr -= tp->tsoffset;
+		tp->rx_opt.rcv_tsecr =
+			tcp_tsecr_decode_tp(tp->rx_opt.rcv_tsecr, tp);
 
 	if (th->ack) {
 		/* rfc793:
@@ -7512,6 +7515,8 @@ static void tcp_openreq_init(struct request_sock *req,
 	tcp_rsk(req)->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
 	tcp_rsk(req)->snt_synack = 0;
 	tcp_rsk(req)->snt_tsval_first = 0;
+	tcp_rsk(req)->ts_id = 0;
+	tcp_rsk(req)->ts_id_bits = 0;
 	tcp_rsk(req)->last_oow_ack_time = 0;
 	tcp_rsk(req)->accecn_ok = 0;
 	tcp_rsk(req)->saw_accecn_opt = TCP_ACCECN_OPT_NOT_SEEN;
diff --git a/net/ipv4/tcp_minisocks.c b/net/ipv4/tcp_minisocks.c
index 0ddfd5af6e58..43580b395bff 100644
--- a/net/ipv4/tcp_minisocks.c
+++ b/net/ipv4/tcp_minisocks.c
@@ -122,7 +122,11 @@ tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
 
 		if (tmp_opt.saw_tstamp) {
 			if (tmp_opt.rcv_tsecr)
-				tmp_opt.rcv_tsecr -= tcptw->tw_ts_offset;
+				tmp_opt.rcv_tsecr =
+					tcp_tsecr_decode(tmp_opt.rcv_tsecr,
+							 tcptw->tw_ts_offset,
+							 tcptw->tw_ts_id_bits,
+							 tcp_clock_ts(tw->tw_usec_ts));
 			tmp_opt.ts_recent	= READ_ONCE(tcptw->tw_ts_recent);
 			tmp_opt.ts_recent_stamp	= ts_recent_stamp;
 			paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
@@ -347,6 +351,8 @@ void tcp_time_wait(struct sock *sk, int state, int timeo)
 		tcptw->tw_ts_recent	= tp->rx_opt.ts_recent;
 		tcptw->tw_ts_recent_stamp = tp->rx_opt.ts_recent_stamp;
 		tcptw->tw_ts_offset	= tp->tsoffset;
+		tcptw->tw_ts_id		= tp->ts_id;
+		tcptw->tw_ts_id_bits	= tp->ts_id_bits;
 		tw->tw_usec_ts		= tp->tcp_usec_ts;
 		tcptw->tw_last_oow_ack_time = 0;
 		tcptw->tw_tx_delay	= tp->tcp_tx_delay;
@@ -639,6 +645,8 @@ struct sock *tcp_create_openreq_child(const struct sock *sk,
 		newtp->total_rto_recoveries = 1;
 	}
 	newtp->tsoffset = treq->ts_off;
+	newtp->ts_id = treq->ts_id;
+	newtp->ts_id_bits = treq->ts_id_bits;
 #ifdef CONFIG_TCP_MD5SIG
 	newtp->md5sig_info = NULL;	/*XXX*/
 #endif
@@ -705,11 +713,17 @@ struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
 		if (tmp_opt.saw_tstamp) {
 			tmp_opt.ts_recent = req->ts_recent;
 			if (tmp_opt.rcv_tsecr) {
+				u32 now = tcp_clock_ts(tcp_rsk(req)->req_usec_ts);
+
 				if (inet_rsk(req)->tstamp_ok && !fastopen)
 					tsecr_reject = !between(tmp_opt.rcv_tsecr,
 							tcp_rsk(req)->snt_tsval_first,
 							READ_ONCE(tcp_rsk(req)->snt_tsval_last));
-				tmp_opt.rcv_tsecr -= tcp_rsk(req)->ts_off;
+				tmp_opt.rcv_tsecr =
+					tcp_tsecr_decode(tmp_opt.rcv_tsecr,
+							 tcp_rsk(req)->ts_off,
+							 tcp_rsk(req)->ts_id_bits,
+							 now);
 			}
 			/* We do not store true stamp, but it is not required,
 			 * it can be estimated (approximately)
diff --git a/net/ipv4/tcp_output.c b/net/ipv4/tcp_output.c
index c8865205cdc8..966adebc9154 100644
--- a/net/ipv4/tcp_output.c
+++ b/net/ipv4/tcp_output.c
@@ -1101,11 +1101,15 @@ static unsigned int tcp_synack_options(const struct sock *sk,
 		opts->options |= OPTION_TS;
 		opts->tsval = tcp_skb_timestamp_ts(tcp_rsk(req)->req_usec_ts, skb) +
 			      tcp_rsk(req)->ts_off;
-		if (!tcp_rsk(req)->snt_tsval_first) {
-			if (!opts->tsval)
-				opts->tsval = ~0U;
+		opts->tsval = tcp_tsval_encode(opts->tsval,
+					       tcp_rsk(req)->ts_id,
+					       tcp_rsk(req)->ts_id_bits);
+		if (!tcp_rsk(req)->snt_tsval_first && !opts->tsval)
+			opts->tsval = tcp_tsval_encode(~0U,
+						       tcp_rsk(req)->ts_id,
+						       tcp_rsk(req)->ts_id_bits);
+		if (!tcp_rsk(req)->snt_tsval_first)
 			tcp_rsk(req)->snt_tsval_first = opts->tsval;
-		}
 		WRITE_ONCE(tcp_rsk(req)->snt_tsval_last, opts->tsval);
 		opts->tsecr = req->ts_recent;
 		remaining -= TCPOLEN_TSTAMP_ALIGNED;
@@ -1171,6 +1175,9 @@ static unsigned int tcp_established_options(struct sock *sk, struct sk_buff *skb
 		opts->options |= OPTION_TS;
 		opts->tsval = skb ? tcp_skb_timestamp_ts(tp->tcp_usec_ts, skb) +
 				tp->tsoffset : 0;
+		if (skb)
+			opts->tsval = tcp_tsval_encode(opts->tsval, tp->ts_id,
+						       tp->ts_id_bits);
 		opts->tsecr = tp->rx_opt.ts_recent;
 		size += TCPOLEN_TSTAMP_ALIGNED;
 	}
-- 
2.47.3


  reply	other threads:[~2026-10-13 10:39 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-13 10:39 [RFC PATCH net-next 0/4] tcp: carry a BPF-chosen identifier in TSval low bits Willem Ferguson
2026-10-13 10:39 ` Willem Ferguson [this message]
2026-10-13 10:39 ` [RFC PATCH net-next 2/4] tcp: add BPF_SOCK_OPS_TCP_TS_ID_CB to assign a TSval identifier Willem Ferguson
2026-10-13 10:39 ` [RFC PATCH net-next 3/4] tcp: add TCP_TS_ID socket option Willem Ferguson
2026-10-13 10:39 ` [RFC PATCH net-next 4/4] selftests/bpf: add tcp_ts_id test Willem Ferguson

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261013103906.2660384-2-wferguson@cloudflare.com \
    --to=wferguson@cloudflare.com \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=corbet@lwn.net \
    --cc=daniel@iogearbox.net \
    --cc=davem@davemloft.net \
    --cc=eddyz87@gmail.com \
    --cc=edumazet@kernel.org \
    --cc=kernel-team@cloudflare.com \
    --cc=kuba@kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=memxor@gmail.com \
    --cc=ncardwell@google.com \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=shuah@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox