mirror of
https://github.com/torvalds/linux.git
synced 2026-11-07 06:17:02 +01:00
bpf: tcp: Support selected sock_ops callbacks as struct_ops
In LSFMMBPF 2025, I have talked about moving the BPF_PROG_TYPE_SOCK_OPS
to a struct_ops interface [1].
The BPF_SOCK_OPS_*_CB enum interface has grown over time as new TCP
callback points were added. A BPF_PROG_TYPE_SOCK_OPS program now
commonly needs a large switch on sock_ops->op, and the shared
bpf_sock_ops_kern context has become harder to extend because different
callbacks have different locking, argument, skb, and helper
requirements. The existing 'union { u32 args[4]; u32 replylong[4]; }' is
also not reliable in passing args to bpf prog when there are multiple
progs attached to a cgroup.
The above has already been solved in struct_ops. Add a TCP-specific
struct_ops type, bpf_tcp_ops, and support attaching it to cgroups.
This allows each callback have its own func signature and allows
the verifier to select kfuncs/helpers based on the specific
struct_ops member being implemented.
This patch wires up the following existing sock_ops callbacks:
- BPF_SOCK_OPS_TIMEOUT_INIT
- BPF_SOCK_OPS_RWND_INIT
- BPF_SOCK_OPS_RTT_CB
- BPF_SOCK_OPS_STATE_CB
- BPF_SOCK_OPS_RETRANS_CB
- BPF_SOCK_OPS_TCP_CONNECT_CB
- BPF_SOCK_OPS_TCP_LISTEN_CB
- BPF_SOCK_OPS_RTO_CB
- BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB
- BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB
BASE_RTT is ignored as it is not particularly useful. NEEDS_ECN should
be done in bpf-tcp-cc instead. The tstamp ones should be a separate
struct_ops (e.g. "bpf_sock_ops") that can work in both TCP and UDP.
timeout_init and rwnd_init could have a request_sock pointer. This patch
tries a different API and directly passes the request_sock pointer as
an arg.
Two other approaches were considered before settling on having
bpf_get_retval() read the dispatcher's run_ctx via saved_run_ctx. The
first was to inherit the retval in the trampoline itself: add a helper
in the four __bpf_prog_enter*() paths that, for struct_ops programs,
copies the chained value from the caller's run_ctx (now saved_run_ctx)
into the program's own run_ctx. It works but puts a per-enter
program-type check on the generic trampoline fast path, taxing all
fentry/fexit/lsm callers for a cgroup-struct_ops-only feature. The
second was to do that same inherit only for the int-returning members
via a gen_prologue that emits a hidden kfunc at the start of
timeout_init/rwnd_init; this keeps the cost off the generic path and
scoped to bpf_tcp_ops, but needs a kfunc + BTF_ID + prologue-emission
machinery. The chosen approach avoids both: it touches neither the
trampoline nor the program, since saved_run_ctx already points at the
dispatcher's run_ctx that carries the value.
[1], page 13: https://drive.google.com/file/d/1wjKZth6T0llLJ_ONPAL_6Q_jbxbAjByp/view?usp=sharing
Signed-off-by: Martin KaFai Lau <martin.lau@kernel.org>
Signed-off-by: Amery Hung <ameryhung@gmail.com>
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
Reviewed-by: Emil Tsalapatis <emil@etsalapatis.com>
Link: https://patch.msgid.link/20260917200542.3689605-12-ameryhung@gmail.com
This commit is contained in:
parent
6d4ce90766
commit
5ac77ae329
|
|
@ -2634,6 +2634,7 @@ struct bpf_trace_run_ctx {
|
|||
struct bpf_tramp_run_ctx {
|
||||
struct bpf_run_ctx run_ctx;
|
||||
u64 bpf_cookie;
|
||||
int retval;
|
||||
struct bpf_run_ctx *saved_run_ctx;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2959,12 +2959,120 @@ static inline void tcp_clear_sock_ops_cb_flags(struct sock *sk)
|
|||
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_BPF_JIT) && defined(CONFIG_CGROUP_BPF)
|
||||
|
||||
struct bpf_tcp_ops {
|
||||
/* Should return the initial SYN (active open) or SYN-ACK (passive open)
|
||||
* retransmission timeout. Return the timeout in jiffies, or <= 0 for
|
||||
* the kernel default.
|
||||
*
|
||||
* @req: request_sock on the passive (synack) path; NULL otherwise.
|
||||
*/
|
||||
int (*timeout_init)(struct sock *sk, struct request_sock *req);
|
||||
|
||||
/* Should return the initial advertised receive window, in packets,
|
||||
* or < 0 for the kernel default. @req as in timeout_init().
|
||||
*/
|
||||
int (*rwnd_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).
|
||||
*/
|
||||
void (*active_established)(struct sock *sk, struct sk_buff *skb__nullable);
|
||||
|
||||
/* Called when a passive connection becomes established.
|
||||
* @skb is the ACK that completed the 3WHS.
|
||||
*/
|
||||
void (*passive_established)(struct sock *sk, struct sk_buff *skb);
|
||||
|
||||
/* Called when the retransmission timer fires. */
|
||||
void (*rto)(struct sock *sk);
|
||||
|
||||
/* Called on every RTT sample.
|
||||
* @mrtt: the measured RTT, in microseconds.
|
||||
* @srtt: the updated smoothed RTT.
|
||||
*/
|
||||
void (*rtt)(struct sock *sk, long mrtt, u32 srtt);
|
||||
|
||||
/* Called when the connection changes TCP state.
|
||||
* @state: the new state (one of the TCP_* states).
|
||||
*/
|
||||
void (*set_state)(struct sock *sk, int state);
|
||||
|
||||
/* Called when an skb is retransmitted.
|
||||
* @skb: the retransmitted skb.
|
||||
* @err: tcp_transmit_skb() return value (0 on success).
|
||||
*/
|
||||
void (*retrans)(struct sock *sk, struct sk_buff *skb, int err);
|
||||
|
||||
/* Called right before an active connection is initialized. */
|
||||
void (*connect)(struct sock *sk);
|
||||
|
||||
/* Called on listen(2), right after the socket enters TCP_LISTEN. */
|
||||
void (*listen)(struct sock *sk);
|
||||
};
|
||||
|
||||
#define bpf_tcp_ops_call(op, sk, ...) \
|
||||
do { \
|
||||
if (cgroup_bpf_enabled(CGROUP_TCP_SOCK_OPS)) { \
|
||||
const struct bpf_prog_array_item *item; \
|
||||
const struct bpf_tcp_ops *tcp_ops; \
|
||||
struct cgroup *cgrp; \
|
||||
\
|
||||
cgrp = sock_cgroup_ptr(&sk->sk_cgrp_data); \
|
||||
rcu_read_lock_dont_migrate(); \
|
||||
bpf_cgroup_struct_ops_foreach(tcp_ops, item, cgrp, \
|
||||
CGROUP_TCP_SOCK_OPS) { \
|
||||
if (tcp_ops->op) \
|
||||
tcp_ops->op(sk, ##__VA_ARGS__); \
|
||||
} \
|
||||
rcu_read_unlock_migrate(); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define bpf_tcp_ops_call_int(op, init_retval, sk, ...) \
|
||||
({ \
|
||||
int __retval = (init_retval); \
|
||||
if (cgroup_bpf_enabled(CGROUP_TCP_SOCK_OPS)) { \
|
||||
const struct bpf_prog_array_item *item; \
|
||||
const struct bpf_tcp_ops *tcp_ops; \
|
||||
struct bpf_tramp_run_ctx run_ctx; \
|
||||
struct bpf_run_ctx *old_run_ctx; \
|
||||
struct sock *__sk = sk_to_full_sk(sk); \
|
||||
struct request_sock *req = NULL; \
|
||||
struct cgroup *cgrp; \
|
||||
\
|
||||
if (__sk) { \
|
||||
run_ctx.retval = (init_retval); \
|
||||
cgrp = sock_cgroup_ptr(&__sk->sk_cgrp_data); \
|
||||
if (!sk_fullsock(sk)) \
|
||||
req = (struct request_sock *)sk; \
|
||||
rcu_read_lock_dont_migrate(); \
|
||||
old_run_ctx = bpf_set_run_ctx(&run_ctx.run_ctx);\
|
||||
bpf_cgroup_struct_ops_foreach(tcp_ops, item, cgrp, \
|
||||
CGROUP_TCP_SOCK_OPS) { \
|
||||
if (tcp_ops->op) \
|
||||
run_ctx.retval = tcp_ops->op(__sk, req, ##__VA_ARGS__); \
|
||||
} \
|
||||
bpf_reset_run_ctx(old_run_ctx); \
|
||||
rcu_read_unlock_migrate(); \
|
||||
__retval = run_ctx.retval; \
|
||||
} \
|
||||
} \
|
||||
__retval; \
|
||||
})
|
||||
#else
|
||||
#define bpf_tcp_ops_call(op, sk, ...) do { } while (0)
|
||||
#define bpf_tcp_ops_call_int(op, init_retval, sk, ...) (init_retval)
|
||||
#endif
|
||||
|
||||
static inline u32 tcp_timeout_init(struct sock *sk)
|
||||
{
|
||||
int timeout;
|
||||
|
||||
timeout = tcp_call_bpf(sk, BPF_SOCK_OPS_TIMEOUT_INIT, 0, NULL);
|
||||
|
||||
timeout = bpf_tcp_ops_call_int(timeout_init, timeout, sk);
|
||||
if (timeout <= 0)
|
||||
timeout = TCP_TIMEOUT_INIT;
|
||||
return min_t(int, timeout, TCP_RTO_MAX);
|
||||
|
|
@ -2975,7 +3083,7 @@ static inline u32 tcp_rwnd_init_bpf(struct sock *sk)
|
|||
int rwnd;
|
||||
|
||||
rwnd = tcp_call_bpf(sk, BPF_SOCK_OPS_RWND_INIT, 0, NULL);
|
||||
|
||||
rwnd = bpf_tcp_ops_call_int(rwnd_init, rwnd, sk);
|
||||
if (rwnd < 0)
|
||||
rwnd = 0;
|
||||
return rwnd;
|
||||
|
|
@ -2990,6 +3098,7 @@ static inline void tcp_bpf_rtt(struct sock *sk, long mrtt, u32 srtt)
|
|||
{
|
||||
if (BPF_SOCK_OPS_TEST_FLAG(tcp_sk(sk), BPF_SOCK_OPS_RTT_CB_FLAG))
|
||||
tcp_call_bpf_2arg(sk, BPF_SOCK_OPS_RTT_CB, mrtt, srtt);
|
||||
bpf_tcp_ops_call(rtt, sk, mrtt, srtt);
|
||||
}
|
||||
|
||||
#if IS_ENABLED(CONFIG_SMC)
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ obj-$(CONFIG_TCP_AO) += tcp_ao.o
|
|||
|
||||
ifeq ($(CONFIG_BPF_JIT),y)
|
||||
obj-$(CONFIG_BPF_SYSCALL) += bpf_tcp_ca.o
|
||||
obj-$(CONFIG_CGROUP_BPF) += bpf_tcp_ops.o
|
||||
endif
|
||||
|
||||
ifdef CONFIG_GCOV_PROFILE_NETFILTER
|
||||
|
|
|
|||
|
|
@ -227,6 +227,7 @@ int __inet_listen_sk(struct sock *sk, int backlog)
|
|||
return err;
|
||||
|
||||
tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_LISTEN_CB, 0, NULL);
|
||||
bpf_tcp_ops_call(listen, sk);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
188
net/ipv4/bpf_tcp_ops.c
Normal file
188
net/ipv4/bpf_tcp_ops.c
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
|
||||
|
||||
#include <linux/bpf.h>
|
||||
#include <linux/btf_ids.h>
|
||||
#include <linux/bpf_verifier.h>
|
||||
#include <net/bpf_sk_storage.h>
|
||||
#include <net/tcp.h>
|
||||
|
||||
static int timeout_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 int rwnd_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)
|
||||
{
|
||||
}
|
||||
|
||||
static void passive_established_stub(struct sock *sk, struct sk_buff *skb)
|
||||
{
|
||||
}
|
||||
|
||||
static void rto_stub(struct sock *sk)
|
||||
{
|
||||
}
|
||||
|
||||
static void rtt_stub(struct sock *sk, long mrtt, u32 srtt)
|
||||
{
|
||||
}
|
||||
|
||||
static void set_state_stub(struct sock *sk, int state)
|
||||
{
|
||||
}
|
||||
|
||||
static void retrans_stub(struct sock *sk, struct sk_buff *skb, int err)
|
||||
{
|
||||
}
|
||||
|
||||
static void connect_stub(struct sock *sk)
|
||||
{
|
||||
}
|
||||
|
||||
static void listen_stub(struct sock *sk)
|
||||
{
|
||||
}
|
||||
|
||||
static struct bpf_tcp_ops __bpf_tcp_ops = {
|
||||
.timeout_init = timeout_init_stub,
|
||||
.rwnd_init = rwnd_init_stub,
|
||||
.active_established = active_established_stub,
|
||||
.passive_established = passive_established_stub,
|
||||
.rto = rto_stub,
|
||||
.rtt = rtt_stub,
|
||||
.set_state = set_state_stub,
|
||||
.retrans = retrans_stub,
|
||||
.connect = connect_stub,
|
||||
.listen = listen_stub,
|
||||
};
|
||||
|
||||
BPF_CALL_0(bpf_tcp_ops_get_retval)
|
||||
{
|
||||
struct bpf_tramp_run_ctx *ctx =
|
||||
container_of(current->bpf_ctx, struct bpf_tramp_run_ctx, run_ctx);
|
||||
|
||||
/* bpf_get_retval() is only exposed to timeout_init/rwnd_init, which
|
||||
* always run via bpf_tcp_ops_call_int(). Its run_ctx carries the int
|
||||
* return value chained across the bpf_tcp_ops attached to the cgroup
|
||||
* and is this program's saved_run_ctx.
|
||||
*/
|
||||
if (WARN_ON_ONCE(!ctx->saved_run_ctx))
|
||||
return 0;
|
||||
|
||||
return container_of(ctx->saved_run_ctx, struct bpf_tramp_run_ctx,
|
||||
run_ctx)->retval;
|
||||
}
|
||||
|
||||
const struct bpf_func_proto bpf_tcp_ops_get_retval_proto = {
|
||||
.func = bpf_tcp_ops_get_retval,
|
||||
.gpl_only = false,
|
||||
.ret_type = RET_INTEGER,
|
||||
};
|
||||
|
||||
static const struct bpf_func_proto *
|
||||
get_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
|
||||
{
|
||||
u32 moff = prog->aux->attach_st_ops_member_off;
|
||||
|
||||
switch (func_id) {
|
||||
case BPF_FUNC_sk_storage_get:
|
||||
return &bpf_sk_storage_get_proto;
|
||||
case BPF_FUNC_sk_storage_delete:
|
||||
return &bpf_sk_storage_delete_proto;
|
||||
case BPF_FUNC_setsockopt:
|
||||
/* The listener is not locked. */
|
||||
if (moff == offsetof(struct bpf_tcp_ops, rwnd_init) ||
|
||||
moff == offsetof(struct bpf_tcp_ops, timeout_init))
|
||||
return NULL;
|
||||
return &bpf_sk_setsockopt_proto;
|
||||
case BPF_FUNC_getsockopt:
|
||||
if (moff == offsetof(struct bpf_tcp_ops, rwnd_init) ||
|
||||
moff == offsetof(struct bpf_tcp_ops, timeout_init))
|
||||
return NULL;
|
||||
return &bpf_sk_getsockopt_proto;
|
||||
case BPF_FUNC_get_retval:
|
||||
if (moff == offsetof(struct bpf_tcp_ops, timeout_init) ||
|
||||
moff == offsetof(struct bpf_tcp_ops, rwnd_init))
|
||||
return &bpf_tcp_ops_get_retval_proto;
|
||||
return NULL;
|
||||
default:
|
||||
return bpf_base_func_proto(func_id, prog);
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_valid_access(int off, int size, enum bpf_access_type type,
|
||||
const struct bpf_prog *prog, struct bpf_insn_access_aux *info)
|
||||
{
|
||||
if (!bpf_tracing_btf_ctx_access(off, size, type, prog, info))
|
||||
return false;
|
||||
|
||||
if (base_type(info->reg_type) == PTR_TO_BTF_ID &&
|
||||
!bpf_type_has_unsafe_modifiers(info->reg_type) &&
|
||||
info->btf_id == btf_sock_ids[BTF_SOCK_TYPE_SOCK])
|
||||
/* promote it to tcp_sock */
|
||||
info->btf_id = btf_sock_ids[BTF_SOCK_TYPE_TCP];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int bpf_tcp_ops_init_member(const struct btf_type *t,
|
||||
const struct btf_member *member,
|
||||
void *kdata, const void *udata)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bpf_tcp_ops_check_member(const struct btf_type *t,
|
||||
const struct btf_member *member,
|
||||
const struct bpf_prog *prog)
|
||||
{
|
||||
if (prog->sleepable)
|
||||
return -EINVAL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bpf_tcp_ops_init(struct btf *btf)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bpf_tcp_ops_validate(void *kdata)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct bpf_verifier_ops bpf_tcp_ops_verifier = {
|
||||
.get_func_proto = get_func_proto,
|
||||
.is_valid_access = is_valid_access,
|
||||
};
|
||||
|
||||
static struct bpf_struct_ops bpf_tcp_ops = {
|
||||
.verifier_ops = &bpf_tcp_ops_verifier,
|
||||
.init_member = bpf_tcp_ops_init_member,
|
||||
.check_member = bpf_tcp_ops_check_member,
|
||||
.init = bpf_tcp_ops_init,
|
||||
.validate = bpf_tcp_ops_validate,
|
||||
.name = "bpf_tcp_ops",
|
||||
.cgroup_atype = CGROUP_TCP_SOCK_OPS,
|
||||
.cfi_stubs = &__bpf_tcp_ops,
|
||||
.owner = THIS_MODULE,
|
||||
};
|
||||
|
||||
static int __init __bpf_tcp_ops_init(void)
|
||||
{
|
||||
return register_bpf_struct_ops(&bpf_tcp_ops, bpf_tcp_ops);
|
||||
}
|
||||
late_initcall(__bpf_tcp_ops_init);
|
||||
|
|
@ -2997,6 +2997,7 @@ void tcp_set_state(struct sock *sk, int state)
|
|||
|
||||
if (BPF_SOCK_OPS_TEST_FLAG(tcp_sk(sk), BPF_SOCK_OPS_STATE_CB_FLAG))
|
||||
tcp_call_bpf_2arg(sk, BPF_SOCK_OPS_STATE_CB, oldstate, state);
|
||||
bpf_tcp_ops_call(set_state, sk, state);
|
||||
|
||||
switch (state) {
|
||||
case TCP_ESTABLISHED:
|
||||
|
|
|
|||
|
|
@ -6724,6 +6724,10 @@ void tcp_init_transfer(struct sock *sk, int bpf_op, struct sk_buff *skb)
|
|||
tp->snd_cwnd_stamp = tcp_jiffies32;
|
||||
|
||||
bpf_skops_established(sk, bpf_op, skb);
|
||||
if (bpf_op == BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB)
|
||||
bpf_tcp_ops_call(active_established, sk, skb);
|
||||
else
|
||||
bpf_tcp_ops_call(passive_established, sk, skb);
|
||||
/* Initialize congestion control unless BPF initialized it already: */
|
||||
if (!icsk->icsk_ca_initialized)
|
||||
tcp_init_congestion_control(sk);
|
||||
|
|
|
|||
|
|
@ -3678,6 +3678,7 @@ int __tcp_retransmit_skb(struct sock *sk, struct sk_buff *skb, int segs)
|
|||
if (BPF_SOCK_OPS_TEST_FLAG(tp, BPF_SOCK_OPS_RETRANS_CB_FLAG))
|
||||
tcp_call_bpf_3arg(sk, BPF_SOCK_OPS_RETRANS_CB,
|
||||
TCP_SKB_CB(skb)->seq, segs, err);
|
||||
bpf_tcp_ops_call(retrans, sk, skb, err);
|
||||
|
||||
if (unlikely(err) && err != -EBUSY)
|
||||
NET_ADD_STATS(sock_net(sk), LINUX_MIB_TCPRETRANSFAIL, segs);
|
||||
|
|
@ -4302,6 +4303,7 @@ int tcp_connect(struct sock *sk)
|
|||
int err;
|
||||
|
||||
tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_CONNECT_CB, 0, NULL);
|
||||
bpf_tcp_ops_call(connect, sk);
|
||||
|
||||
#if defined(CONFIG_TCP_MD5SIG) && defined(CONFIG_TCP_AO)
|
||||
/* Has to be checked late, after setting daddr/saddr/ops.
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@ static int tcp_write_timeout(struct sock *sk)
|
|||
tcp_call_bpf_3arg(sk, BPF_SOCK_OPS_RTO_CB,
|
||||
icsk->icsk_retransmits,
|
||||
icsk->icsk_rto, (int)expired);
|
||||
bpf_tcp_ops_call(rto, sk);
|
||||
|
||||
if (expired) {
|
||||
/* Has it gone just too far? */
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user