[eBPF] sockops类型运行梳理
BPF_PROG_TYPE_SOCK_OPS程序类型
内核中socket事件(本质是各函数)中的埋点,能够在socket的生命周期每个节点执行BPF程序。
- 进入Linux内核的commit
- session定义:
SEC_DEF("sockops", SOCK_OPS, BPF_CGROUP_SOCK_OPS, SEC_ATTACHABLE_OPT)
- 参数,由
BPF_PROG_TYPE(BPF_PROG_TYPE_SOCK_OPS, sock_ops, struct bpf_sock_ops, struct bpf_sock_ops_kern)
可知:bpf程序的入参是bpf_sock_ops,对应内核中的上下文是bpf_sock_ops_kern。 - 使用场景:参考博客
示例代码
__section("sockops") // 加载到 ELF 中的 `sockops` 区域,有 socket operations 时触发执行
int bpf_sockmap(struct bpf_sock_ops *skops)
{
switch (skops->op) {
case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB: // 被动建连
case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB: // 主动建连
if (skops->family == 2) { // AF_INET
bpf_sock_ops_ipv4(skops); // 将 socket 信息记录到到 sockmap
}
break;
default:
break;
}
return 0;
}
Hook位置梳理
- tcp_output.c,中核心函数
int tcp_connect(struct sock *sk)
一开始就埋了点:
// https://elixir.bootlin.com/linux/v6.6.4/source/net/ipv4/tcp_output.c#L3946
/* Build a SYN and send it off. */
int tcp_connect(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *buff;
int err;
tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_CONNECT_CB, 0, NULL);//此处埋点,去执行挂载在BPF_SOCK_OPS_TCP_CONNECT_CB下的bpf程序。
if (inet_csk(sk)->icsk_af_ops->rebuild_header(sk))
return -EHOSTUNREACH; /* Routing failure or similar. */
...
-
BPF_SOCK_OPS_*
宏:
在源码中位置
由源码中注释可知,这里是“已知 BPF sock_ops 运算符的列表”。
在socket的阶段宏表示,可以在
上面的BPF_SOCK_OPS_TCP_CONNECT_CB
是“在初始化活动连接之前调用 BPF 程序”,符合直觉。
由示例bpf程序可知,这个宏被包装在了上下文结构体中,op字段。在内核态ebpf程序中可以获取到。 -
tcp_call_bpf(struct sock *sk, int op, u32 nargs, u32 *args)
函数
用于在内核socket处理链路中埋点。
// https://elixir.bootlin.com/linux/v6.6.4/source/include/net/tcp.h#L2409
static inline int tcp_call_bpf(struct sock *sk, int op, u32 nargs, u32 *args)
{
struct bpf_sock_ops_kern sock_ops;
int ret;
memset(&sock_ops, 0, offsetof(struct bpf_sock_ops_kern, temp));
if (sk_fullsock(sk)) {
sock_ops.is_fullsock = 1;
sock_owned_by_me(sk);
}
sock_ops.sk = sk;
sock_ops.op = op;
if (nargs > 0)
memcpy(sock_ops.args, args, nargs * sizeof(*args));
ret = BPF_CGROUP_RUN_PROG_SOCK_OPS(&sock_ops);
if (ret == 0)
ret = sock_ops.reply;
else
ret = -1;
return ret;
}
其核心逻辑是将上下文封装成bpf_sock_ops_kern
结构体,再调用BPF_CGROUP_RUN_PROG_SOCK_OPS
宏(顾名思义,运行挂载的SOCK_OPS程序)。
后续链路:
// https://elixir.bootlin.com/linux/v6.6.4/source/include/linux/bpf-cgroup.h#L328
#define BPF_CGROUP_RUN_PROG_SOCK_OPS(sock_ops) \
({ \
int __ret = 0; \
if (cgroup_bpf_enabled(CGROUP_SOCK_OPS) && (sock_ops)->sk) { \
typeof(sk) __sk = sk_to_full_sk((sock_ops)->sk); \
if (__sk && sk_fullsock(__sk)) \
__ret = __cgroup_bpf_run_filter_sock_ops(__sk, \
sock_ops, \
CGROUP_SOCK_OPS); \
} \
__ret; \
})
// https://elixir.bootlin.com/linux/v6.6.4/source/kernel/bpf/cgroup.c#L1510
int __cgroup_bpf_run_filter_sock_ops(struct sock *sk,
struct bpf_sock_ops_kern *sock_ops,
enum cgroup_bpf_attach_type atype)
{
struct cgroup *cgrp = sock_cgroup_ptr(&sk->sk_cgrp_data);
return bpf_prog_run_array_cg(&cgrp->bpf, atype, sock_ops, bpf_prog_run,
0, NULL);
}
// https://elixir.bootlin.com/linux/v6.6.4/source/kernel/bpf/cgroup.c#L31
static __always_inline int
bpf_prog_run_array_cg(const struct cgroup_bpf *cgrp,
enum cgroup_bpf_attach_type atype,
const void *ctx, bpf_prog_run_fn run_prog,
int retval, u32 *ret_flags)
{
const struct bpf_prog_array_item *item;
const struct bpf_prog *prog;
const struct bpf_prog_array *array;
struct bpf_run_ctx *old_run_ctx;
struct bpf_cg_run_ctx run_ctx;
u32 func_ret;
run_ctx.retval = retval;
migrate_disable();
rcu_read_lock();
array = rcu_dereference(cgrp->effective[atype]);
item = &array->items[0];
old_run_ctx = bpf_set_run_ctx(&run_ctx.run_ctx);
while ((prog = READ_ONCE(item->prog))) {
run_ctx.prog_item = item;
func_ret = run_prog(prog, ctx);//!!!执行挂载在此处的ebpf程序
if (ret_flags) {
*(ret_flags) |= (func_ret >> 1);
func_ret &= 1;
}
if (!func_ret && !IS_ERR_VALUE((long)run_ctx.retval))
run_ctx.retval = -EPERM;
item++;
}
bpf_reset_run_ctx(old_run_ctx);
rcu_read_unlock();
migrate_enable();
return run_ctx.retval;
}
总结
BPF_PROG_TYPE_SOCK_OPS程序类型能够在socket处理流程中的关键路径上执行ebpf程序。
- 基础:在内核源码中静态预埋点,在socket处理函数中调用
tcp_call_bpf
函数,最终执行挂载的bpf逻辑。 - 内核对外透露出的信息(bpf程序的权限):
tcp_call_bpf
函数将上下文信息(sock结构体)封装成bpf_sock_ops_kern
,最终透传出去,ebpf程序能够访问bpf_sock_ops
中的字段。 - 应用:可以重定向socket,绕过TCP/IP协议栈;修改socket参数(通过辅助函数)…