Re: [PATCH net-next v7] tcp: add BPF kfuncs to set per-socket ECN mode and AccECN option
From: Alexei Starovoitov
Date: Fri Oct 16 2026 - 14:12:03 EST
On Fri, Oct 16, 2026 at 03:40 PM Irlanki Sandeep <irlanki.s@xxxxxxxxxxx> wrote:
> To provide granular control, introduce two new BPF kfuncs that can be
> called from sockops programs:
sockops is legacy. New tcp hooks go into bpf_tcp_ops. See
commit 5ac77ae32940 ("bpf: tcp: Support selected sock_ops callbacks as struct_ops")
kfuncs that take bpf_sock_ops_kern cannot be called from there.
It's in bpf-next only, so pls respin on top of bpf-next and
cc Amery, Kuniyuki.
> The ECN mode kfunc is restricted to the CONNECT_CB and LISTEN_CB
> callbacks since the mode is only consulted during connection
> establishment, while the AccECN option kfunc may be called from any
[...]
> inherited by the accepted child sockets. Note that a listener is already
> hashed before the LISTEN_CB callback runs, so connections accepted in that
> window follow the sysctl defaults: for the AccECN option this is transient,
The mode is read once per handshake. No need to store it in tcp_sock.
Ask the prog at the time the sysctl is read, like timeout_init() and
rwnd_init() do. On the passive side the prog gets the listener and
the req, so it can decide per peer or per incoming interface.
One byte in the listener cannot do that.
Then no need for tp->ecn_mode, TCP_ECN_MODE_UNSPEC, tcp_ecn_mode_eff(),
bpf_sock_ops_set_ecn_mode() with its op check, and the LISTEN_CB window
is gone.
With syncookies rsk_listener is NULL and the sysctl is used.
rwnd_init() has the same limitation.
Something like this (compile tested only):
diff --git a/include/net/tcp.h b/include/net/tcp.h
--- a/include/net/tcp.h
+++ b/include/net/tcp.h
@@ -2971,6 +2971,12 @@ struct bpf_tcp_ops {
*/
int (*rwnd_init)(struct sock *sk, struct request_sock *req);
+ /* Should return the ECN variant to negotiate in this handshake:
+ * one of net.ipv4.tcp_ecn values. bpf_get_retval() has the sysctl.
+ * @req as in timeout_init().
+ */
+ int (*ecn_init)(struct sock *sk, struct request_sock *req);
+
/* Called when an active connection becomes established.
* @skb is the SYNACK that completed the 3WHS, or NULL for a
* TCP_REPAIR socket (tcp_finish_connect() with no skb).
diff --git a/include/net/tcp_ecn.h b/include/net/tcp_ecn.h
--- a/include/net/tcp_ecn.h
+++ b/include/net/tcp_ecn.h
@@ -32,6 +32,17 @@ enum tcp_accecn_option {
TCP_ACCECN_OPTION_PERSIST = 3,
};
+static inline u8 tcp_ecn_init_bpf(struct sock *sk)
+{
+ u8 tcp_ecn = READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_ecn);
+ int ret;
+
+ ret = bpf_tcp_ops_call_int(ecn_init, tcp_ecn, sk);
+ if (ret < 0 || ret > TCP_ECN_IN_ACCECN_OUT_NOECN)
+ return tcp_ecn;
+ return ret;
+}
+
/* Apply either ECT(0) or ECT(1) based on TCP_CONG_ECT_1_NEGOTIATION flag */
static inline void INET_ECN_xmit_ect_1_negotiation(struct sock *sk)
{
@@ -599,7 +610,7 @@ static inline void tcp_ecn_send_syn(struct sock *sk, struct sk_buff *skb)
struct tcp_sock *tp = tcp_sk(sk);
bool bpf_needs_ecn = tcp_bpf_ca_needs_ecn(sk);
bool use_ecn, use_accecn;
- u8 tcp_ecn = READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_ecn);
+ u8 tcp_ecn = tcp_ecn_init_bpf(sk);
use_accecn = tcp_ecn == TCP_ECN_IN_ACCECN_OUT_ACCECN ||
tcp_ca_needs_accecn(sk);
diff --git a/net/ipv4/bpf_tcp_ops.c b/net/ipv4/bpf_tcp_ops.c
--- a/net/ipv4/bpf_tcp_ops.c
+++ b/net/ipv4/bpf_tcp_ops.c
@@ -24,6 +24,14 @@ static int rwnd_init_stub(struct sock *sk, struct request_sock *req__nullable)
return ctx->retval;
}
+static int ecn_init_stub(struct sock *sk, struct request_sock *req__nullable)
+{
+ struct bpf_tramp_run_ctx *ctx =
+ container_of(current->bpf_ctx, struct bpf_tramp_run_ctx, run_ctx);
+
+ return ctx->retval;
+}
+
static void active_established_stub(struct sock *sk, struct sk_buff *skb__nullable)
{
}
@@ -87,6 +95,7 @@ static void dequeue_rcvq_stub(struct sock *sk)
static struct bpf_tcp_ops __bpf_tcp_ops = {
.timeout_init = timeout_init_stub,
.rwnd_init = rwnd_init_stub,
+ .ecn_init = ecn_init_stub,
.active_established = active_established_stub,
.passive_established = passive_established_stub,
.rto = rto_stub,
@@ -258,7 +267,8 @@ get_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
return NULL;
case BPF_FUNC_get_retval:
if (moff == offsetof(struct bpf_tcp_ops, timeout_init) ||
- moff == offsetof(struct bpf_tcp_ops, rwnd_init))
+ moff == offsetof(struct bpf_tcp_ops, rwnd_init) ||
+ moff == offsetof(struct bpf_tcp_ops, ecn_init))
return &bpf_tcp_ops_get_retval_proto;
return NULL;
case BPF_FUNC_reserve_hdr_opt:
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -7500,13 +7500,13 @@ static void tcp_ecn_create_request(struct request_sock *req,
const struct dst_entry *dst)
{
const struct tcphdr *th = tcp_hdr(skb);
- const struct net *net = sock_net(listen_sk);
+ u8 tcp_ecn = tcp_ecn_init_bpf((struct sock *)req);
bool th_ecn = th->ece && th->cwr;
bool ect, ecn_ok;
u32 ecn_ok_dst;
if (tcp_accecn_syn_requested(th) &&
- (READ_ONCE(net->ipv4.sysctl_tcp_ecn) >= 3 ||
+ (tcp_ecn >= 3 ||
tcp_ca_needs_accecn(listen_sk))) {
inet_rsk(req)->ecn_ok = 1;
tcp_rsk(req)->accecn_ok = 1;
@@ -7520,7 +7520,7 @@ static void tcp_ecn_create_request(struct request_sock *req,
ect = !INET_ECN_is_not_ect(TCP_SKB_CB(skb)->ip_dsfield);
ecn_ok_dst = dst_feature(dst, DST_FEATURE_ECN_MASK);
- ecn_ok = READ_ONCE(net->ipv4.sysctl_tcp_ecn) || ecn_ok_dst;
+ ecn_ok = tcp_ecn || ecn_ok_dst;
if (((!ect || th->res1 || th->ae) && ecn_ok) ||
tcp_ca_needs_ecn(listen_sk) ||
[...]
The commit log says that the problem is in the path, and in v4 Jakub
said "This is a routing property".
RFC3168 ECN has a per-route knob for that: RTAX_FEATURE_ECN.
usec TS got RTAX_FEATURE_TCP_USEC_TS.
Your reply to Eric in that thread had a per-app reason too: AccECN for
latency sensitive apps and not for bulk traffic.
Make it clear in the commit log if this is must-have part of design.
I'm not saying that this bpf ops should be the way to go.
If it's only the path, one more route feature bit
might be all that Android needs.
pw-bot: cr