BPF 进阶笔记(一):BPF 程序类型详解:使用场景、函数签名、执行位置及程序示例
关于本文
内核目前支持 30 来种 BPF 程序类型。对于主要的程序类型,本文将介绍其:
- 使用场景 :适合用来做什么?
- Hook 位置 :在 何处(where)、何时(when) 会触发执行?例如在内核协议栈的哪个位置,或是什么事件触发执行。
-
程序签名
(程序
入口函数
签名)
- 传入参数:调用到 BPF 程序时,传给它的上下文(context,也就是函数参数)是什么?
- 返回值:返回值类型、含义、合法列表等。
- 加载方式 :如何将程序附着(attach)到执行点?
- 程序示例 :一些实际例子。
- 延伸阅读 :其他高级主题,例如相关的内核设计与实现。
本文主要参考:
-
BPF: A Tour of Program Types
,内容略老,基于内核
4.14
关于 “BPF 进阶笔记” 系列
平时学习使用 BPF 时所整理。由于是笔记而非教程,因此内容不会追求连贯,有基础的 同学可作查漏补缺之用。
文中涉及的代码,如无特殊说明,均基于内核 5.8/5.10 版本。
- 关于 “BPF 进阶笔记” 系列
-
- BPF 程序类型:完整列表
- BPF attach 类型:完整列表
- ————————————————————————
- ————————————————————————
-
1
BPF_PROG_TYPE_SOCKET_FILTER
-
- 场景一:流量过滤/复制(只读,相当于抓包)
- 场景二:可观测性:流量统计
-
Hook 位置:
sock_queue_rcv_skb()
-
-
传入参数:
struct __sk_buff *
-
传入参数:
-
加载方式:
setsockopt()
-
-
1. 可观测性:内核自带
samples/bpf/sockex1
~samples/bpf/sockex3
- 2. 流量复制:每个包只保留前 N 个字节
-
1. 可观测性:内核自带
-
-
2
BPF_PROG_TYPE_SOCK_OPS
-
使用场景:动态跟踪/修改 socket 操作
- 场景一:动态跟踪:监听 socket 事件
- 场景二:动态修改 socket(例如 tcp 建连)操作
-
场景三:socket redirection(需要
BPF_PROG_TYPE_SK_SKB
程序配合)
-
-
传入参数:
struct bpf_sock_ops *
-
传入参数:
- 加载方式:attach 到某个 cgroup(可使用 bpftool 等工具)
-
- 1. Customize TCP initial RTO (retransmission timeout) with BPF
- 2. Cracking kubernetes node proxy (aka kube-proxy),其中的第五种实现方式
- 3. (译) 利用 ebpf sockmap/redirection 提升 socket 性能(2020)
-
使用场景:动态跟踪/修改 socket 操作
-
3
BPF_PROG_TYPE_SK_SKB
-
- 场景一:修改 skb/socket 信息,socket 重定向
- 场景二:动态解析消息流(stream parsing)
-
- socket redirection 类型
-
strparser 类型:
smap_parse_func_strparser()
/smap_verdict_func()
-
-
传入参数:
struct __sk_buff *
-
传入参数:
- 加载方式:attach 到某个 sockmap(可使用 bpftool 等工具)
-
- 1. (译) 利用 ebpf sockmap/redirection 提升 socket 性能(2020)
-
2.
strparser
框架:解析消息流
-
- ————————————————————————
- ————————————————————————
-
1
BPF_PROG_TYPE_SCHED_CLS
-
Hook 位置:
sch_handle_ingress()
/sch_handle_egress()
-
-
传入参数:
struct __sk_buff *
-
传入参数:
-
加载方式:
tc
命令(背后使用 netlink)
-
Hook 位置:
-
2
BPF_PROG_TYPE_SCHED_ACT
- ————————————————————————
- XDP(eXpress Data Path)程序
- ————————————————————————
-
-
- 场景一:防火墙、四层负载均衡等
-
-
传入参数:
struct xdp_md *
-
返回值:
enum xdp_action
-
传入参数:
- 加载方式:netlink socket
-
- 1. samples/bpf/bpf_load.c
-
- ————————————————————————
- ————————————————————————
-
1
BPF_PROG_TYPE_CGROUP_SKB
-
- 场景一:在 CGroup 级别:放行/丢弃数据包
-
Hook 位置:
sk_filter_trim_cap()
-
-
传入参数:
struct sk_buff *skb
-
传入参数:
- 加载方式:attach 到 cgroup 文件描述符
-
-
2
BPF_PROG_TYPE_CGROUP_SOCK
-
- 场景一:在 CGroup 级别:触发 socket 操作时拒绝/放行网络访问
-
-
传入参数:
struct sk_buff *skb
-
传入参数:
-
触发执行:
inet_create()
- 加载方式:attach 到 cgroup 文件描述符
-
- ————————————————————————
- kprobes、tracepoints、perf events
- ————————————————————————
-
1
BPF_PROG_TYPE_KPROBE
-
- 场景一:观测内核函数(kprobe)和用户空间函数(uprobe)
-
Hook 位置:
k[ret]probe_perf_func()
/u[ret]probe_perf_func()
-
-
传入参数:
struct pt_regs *ctx
-
传入参数:
-
加载方式:
/sys/fs/debug/tracing/
目录下的配置文件
-
-
2
BPF_PROG_TYPE_TRACEPOINT
-
- 场景一:Instrument 内核代码中的 tracepoints
-
Hook 位置:
perf_trace_<event_class>()
-
- 传入参数:因 tracepoint 而异
-
加载方式:
/sys/fs/debug/tracing/
目录下的配置文件
-
-
3
BPF_PROG_TYPE_PERF_EVENT
-
- 场景一:Instrument 软件/硬件 perf 事件
-
-
传入参数:
struct bpf_perf_event_data *
-
传入参数:
- 触发执行:每个采样间隔执行一次
-
- ————————————————————————
- ————————————————————————
-
1
BPF_PROG_TYPE_LWT_IN
-
- 场景一:检查入向流量是否需要做解封装(decap)
-
Hook 位置:
lwtunnel_input()
-
-
传入参数:
struct sk_buff *
-
传入参数:
-
加载方式:
ip route add
-
-
2
BPF_PROG_TYPE_LWT_OUT
- 场景一:对出向流量做封装(encap)
-
加载方式:
ip route add
-
-
传入参数:
struct __sk_buff *
-
传入参数:
-
触发执行:
lwtunnel_output()
-
3
BPF_PROG_TYPE_LWT_XMIT
-
- 场景一:实现轻量级隧道发送端的 encap/redir 方法
-
Hook 位置:
lwtunnel_xmit()
-
-
传入参数:
struct __sk_buff *
-
传入参数:
-
加载方式:
ip route add
-
基础
BPF 程序类型:完整列表
Kernel 5.8 支持的 BPF 程序类型 :
// include/uapi/linux/bpf.h enum bpf_prog_type { BPF_PROG_TYPE_UNSPEC, BPF_PROG_TYPE_SOCKET_FILTER, BPF_PROG_TYPE_KPROBE, BPF_PROG_TYPE_SCHED_CLS, // CLS: tc classifier,分类器 BPF_PROG_TYPE_SCHED_ACT, // ACT: tc action,动作 BPF_PROG_TYPE_TRACEPOINT, BPF_PROG_TYPE_XDP, BPF_PROG_TYPE_PERF_EVENT, BPF_PROG_TYPE_CGROUP_SKB, BPF_PROG_TYPE_CGROUP_SOCK, BPF_PROG_TYPE_LWT_IN, BPF_PROG_TYPE_LWT_OUT, BPF_PROG_TYPE_LWT_XMIT, BPF_PROG_TYPE_SOCK_OPS, BPF_PROG_TYPE_SK_SKB, BPF_PROG_TYPE_CGROUP_DEVICE, BPF_PROG_TYPE_SK_MSG, BPF_PROG_TYPE_RAW_TRACEPOINT, BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_PROG_TYPE_LWT_SEG6LOCAL, BPF_PROG_TYPE_LIRC_MODE2, BPF_PROG_TYPE_SK_REUSEPORT, BPF_PROG_TYPE_FLOW_DISSECTOR, BPF_PROG_TYPE_CGROUP_SYSCTL, BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE, BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_PROG_TYPE_TRACING, BPF_PROG_TYPE_STRUCT_OPS, BPF_PROG_TYPE_EXT, BPF_PROG_TYPE_LSM, };
BPF attach 类型:完整列表
通过 socket()
系统调用将 BPF 程序 attach 到 hook 点时用到, 定义
:
// include/uapi/linux/bpf.h enum bpf_attach_type { BPF_CGROUP_INET_INGRESS, BPF_CGROUP_INET_EGRESS, BPF_CGROUP_INET_SOCK_CREATE, BPF_CGROUP_SOCK_OPS, BPF_SK_SKB_STREAM_PARSER, BPF_SK_SKB_STREAM_VERDICT, BPF_CGROUP_DEVICE, BPF_SK_MSG_VERDICT, BPF_CGROUP_INET4_BIND, BPF_CGROUP_INET6_BIND, BPF_CGROUP_INET4_CONNECT, BPF_CGROUP_INET6_CONNECT, BPF_CGROUP_INET4_POST_BIND, BPF_CGROUP_INET6_POST_BIND, BPF_CGROUP_UDP4_SENDMSG, BPF_CGROUP_UDP6_SENDMSG, BPF_LIRC_MODE2, BPF_FLOW_DISSECTOR, BPF_CGROUP_SYSCTL, BPF_CGROUP_UDP4_RECVMSG, BPF_CGROUP_UDP6_RECVMSG, BPF_CGROUP_GETSOCKOPT, BPF_CGROUP_SETSOCKOPT, BPF_TRACE_RAW_TP, BPF_TRACE_FENTRY, BPF_TRACE_FEXIT, BPF_MODIFY_RETURN, BPF_LSM_MAC, BPF_TRACE_ITER, BPF_CGROUP_INET4_GETPEERNAME, BPF_CGROUP_INET6_GETPEERNAME, BPF_CGROUP_INET4_GETSOCKNAME, BPF_CGROUP_INET6_GETSOCKNAME, BPF_XDP_DEVMAP, __MAX_BPF_ATTACH_TYPE };
————————————————————————
Socket 相关类型
————————————————————————
用于 过滤和重定向 socket 数据,或者监听 socket 事件 。类型包括:
BPF_PROG_TYPE_SOCKET_FILTER BPF_PROG_TYPE_SOCK_OPS BPF_PROG_TYPE_SK_SKB BPF_PROG_TYPE_SK_MSG BPF_PROG_TYPE_SK_REUSEPORT
1 BPF_PROG_TYPE_SOCKET_FILTER
使用场景
场景一:流量过滤/复制(只读,相当于抓包)
从名字 SOCKET_FILTER 可以看出,这种类型的 BPF 程序能对流量进行 过滤(filtering)。
场景二:可观测性:流量统计
仍然是对流量进行过滤,但只统计流量信息,不要包本身。
Hook 位置: sock_queue_rcv_skb()
在
sock_queue_rcv_skb()
中触发执行:
// net/core/sock.c // 处理 socket 入向流量,TCP/UDP/ICMP/raw-socket 等协议类型都会执行到这里 int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb) { err = sk_filter(sk, skb); // 执行 BPF 代码,这里返回的 err 表示对这个包保留前多少字节(trim) if (err) // 如果字节数大于 0 return err; // 跳过接下来的处理逻辑,直接返回到更上层 // 如果字节数等于 0,继续执行内核正常的 socket receive 逻辑 return __sock_queue_rcv_skb(sk, skb); }
程序签名
传入参数: struct __sk_buff *
上面可以看到,hook 入口 sk_filter(sk, skb)
传的是 struct sk_buff *skb
,
但经过层层传递,最终传递给 BPF 程序的其实是 struct __sk_buff *
。
这个结构体的
定义
见 include/uapi/linux/bpf.h
,
// include/uapi/linux/bpf.h // user accessible mirror of in-kernel sk_buff. struct __sk_buff { ... }
-
如注释所说,它是对
struct sk_buff
的 用户可访问字段 的镜像。 -
BPF 程序中对
struct __sk_buff
字段的访问,将会被 BPF 校验器转换成对相应的 struct sk_buff 字段的访问 。 - 为什么要多引入这一层封装 ,见 bpf: allow extended BPF programs access skb fields 。
返回值
再来看一下 hook 前后的逻辑:
// net/core/sock.c // 处理 socket 入向流量,TCP/UDP/ICMP/raw-socket 等协议类型都会执行到这里 int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb) { err = sk_filter(sk, skb); // 执行 BPF 代码,这里返回的 err 表示对这个包保留前多少字节(trim) if (err) // 如果字节数大于 0 return err; // 跳过接下来的处理逻辑,直接返回到更上层 // 如果字节数等于 0,继续执行内核正常的 socket receive 逻辑 return __sock_queue_rcv_skb(sk, skb); }
如果 sk_filter()
的返回值 err
-
err != 0
:直接return err
,返回到调用方, 不再继续原来正常的内核处理逻辑__sock_queue_rcv_skb()
;所以效果就是: 将这个包过滤了出来 (符合过滤条件); -
err == 0
:接下来继续执行正常的内核处理,也就是 这个包不符合过滤条件 ;
所以至此大概就知道要实现过滤和截断功能,程序应该返回什么了。要精确搞清楚,需要
看 sk_filter()
一直调用到 BPF 程序的代码,看中间是否对 BPF 程序的返回值做了封
装和转换。
这里给出结论:BPF 程序的 返回值 ,
-
n
(n < pkt_size
):返回一个 截断的包 (副本),只保留前面n
个字节。 -
0
: 忽略 这个包;
需要说明:
- 这里所谓的截断并不是截断原始包,而只是复制一份包的元数据,修改其中的包长字段;
- 程序本身不会截断或丢弃原始流量,也就是说,对 原始流量是只读的 (read only);
加载方式: setsockopt()
通过 setsockopt(fd, SO_ATTACH_BPF, ...)
系统调用,其中 fd 是
BPF 程序的文件描述符
。
程序示例
1. 可观测性:内核自带 samples/bpf/sockex1
~ samples/bpf/sockex3
这三个例子都是用 BPF 程序 过滤网络设备设备上的包 , 根据协议类型、IP、端口等信息统计流量。
源码 samples/bpf/ 。
$ cd samples/bpf/ $ make $ ./sockex1
2. 流量复制:每个包只保留前 N 个字节
下面的例子根据包的协议类型对包进行截断。简单起见,不解析 IPv4 option 字段。
#include <uapi/linux/bpf.h> #include <uapi/linux/in.h> #include <uapi/linux/types.h> #include <uapi/linux/string.h> #include <uapi/linux/if_ether.h> #include <uapi/linux/if_packet.h> #include <uapi/linux/ip.h> #include <uapi/linux/tcp.h> #include <uapi/linux/udp.h> #include <bpf/bpf_helpers.h> #ifndef offsetof #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) #endif // We are only interested in TCP/UDP headers, so drop every other protocol // and trim packets after the TCP/UDP header by returning eth_len + ipv4_hdr + TCP/UDP header __section("socket") int bpf_trim_skb(struct __sk_buff *skb) { int proto = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol)); int size = ETH_HLEN + sizeof(struct iphdr); switch (proto) { case IPPROTO_TCP: size += sizeof(struct tcphdr); break; case IPPROTO_UDP: size += sizeof(struct udphdr); break; default: size = 0; break; // drop this packet } return size; } char _license[] __section("license") = "GPL";
编译及测试:比较简单的方法是将源文件放到内核源码树中 samples/bpf/
目录下。
参考其中的 sockex1
来编译、加载和测试。
延伸阅读
相关实现见
sk_filter_trim_cap()
,
它会进一步调用
bpf_prog_run_save_cb()
。
2 BPF_PROG_TYPE_SOCK_OPS
使用场景:动态跟踪/修改 socket 操作
这里所说的 socket 事件包括建连(connection establishment)、重传(retransmit)、超时(timeout)等等。
场景一:动态跟踪:监听 socket 事件
这种场景只会拦截和解析系统事件,不会修改任何东西。
场景二:动态修改 socket(例如 tcp 建连)操作
拦截到事件后,通过 bpf_setsockopt()
动态修改 socket 配置,
能够实现 per-connection 的优化,提升性能。例如,
- 监听到被动建连(passive establishment of a connection)事件时,如果 对端和本机不在同一个网段 ,就 动态修改这个 socket 的 MTU 。 这样能避免包因为太大而被中间路由器分片(fragmenting)。
- 替换目的 IP 地址 ,实现高性能的 透明代理及负载均衡 。 Cilium 对 K8s 的 Service 就是这样实现的,详见 [1]。
场景三:socket redirection(需要 BPF_PROG_TYPE_SK_SKB
程序配合)
这个其实算是“动态修改”的特例。与 BPF_PROG_TYPE_SK_SKB
程序配合,通过
sockmap+redirection 实现 socket 重定向。这种情况下分为两段 BPF 程序,
-
第一段是
BPF_PROG_TYPE_SOCK_OPS
程序,拦截 socket 事件,并从struct bpf_sock_ops
中提取 socket 信息存储到 sockmap; -
第二段是
BPF_PROG_TYPE_SK_SKB
类型程序,从拦截到的 socket message 提取 socket 信息,然后去 sockmap 查找对端 socket,然后通过bpf_sk_redirect_map()
直接重定向过去。
Hook 位置:多个地方
其他类型的 BPF 程序都是在某个特定的代码出执行的,但 SOCK_OPS 程序不同,它们
在多个地方执行,op 字段表示触发执行的地方
。 op
字段是枚举类型, 完整列表
:
// include/uapi/linux/bpf.h /* List of known BPF sock_ops operators. */ enum { BPF_SOCK_OPS_VOID, BPF_SOCK_OPS_TIMEOUT_INIT, // 初始化 TCP RTO 时调用 BPF 程序 // 程序应当返回希望使用的 SYN-RTO 值;-1 表示使用默认值 BPF_SOCK_OPS_RWND_INIT, // BPF 程序应当返回 initial advertized window (in packets);-1 表示使用默认值 BPF_SOCK_OPS_TCP_CONNECT_CB, // 主动建连 初始化之前 回调 BPF 程序 BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB, // 主动建连 成功之后 回调 BPF 程序 BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB,// 被动建连 成功之后 回调 BPF 程序 BPF_SOCK_OPS_NEEDS_ECN, // If connection's congestion control needs ECN */ BPF_SOCK_OPS_BASE_RTT, // 获取 base RTT。The correct value is based on the path,可能还与拥塞控制 // 算法相关。In general it indicates // a congestion threshold. RTTs above this indicate congestion BPF_SOCK_OPS_RTO_CB, // 触发 RTO(超时重传)时回调 BPF 程序,三个参数: // Arg1: value of icsk_retransmits // Arg2: value of icsk_rto // Arg3: whether RTO has expired BPF_SOCK_OPS_RETRANS_CB, // skb 发生重传之后,回调 BPF 程序,三个参数: // Arg1: sequence number of 1st byte // Arg2: # segments // Arg3: return value of tcp_transmit_skb (0 => success) BPF_SOCK_OPS_STATE_CB, // TCP 状态发生变化时,回调 BPF 程序。参数: // Arg1: old_state // Arg2: new_state BPF_SOCK_OPS_TCP_LISTEN_CB, // 执行 listen(2) 系统调用,socket 进入 LISTEN 状态之后,回调 BPF 程序 };
从以上注释可以看到,这些 OPS 分为两种类型:
-
通过 BPF 程序的返回值来动态修改配置 ,类型包括
BPF_SOCK_OPS_TIMEOUT_INIT BPF_SOCK_OPS_RWND_INIT BPF_SOCK_OPS_NEEDS_ECN BPF_SOCK_OPS_BASE_RTT
-
在 socket/tcp 状态发生变化时, 回调(callback)BPF 程序 ,类型包括
BPF_SOCK_OPS_TCP_CONNECT_CB BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB BPF_SOCK_OPS_RTO_CB BPF_SOCK_OPS_RETRANS_CB BPF_SOCK_OPS_STATE_CB BPF_SOCK_OPS_TCP_LISTEN_CB
引入该功能的内核 patch 见 bpf: Adding support for sock_ops ;
SOCK_OPS 类型的 BPF 程序 都是从 tcp_call_bpf() 调用过来的 ,这个文件中多个地方都会调用到该函数。
程序签名
传入参数: struct bpf_sock_ops *
结构体 定义 ,
// include/uapi/linux/bpf.h struct bpf_sock_ops { __u32 op; // socket 事件类型,就是上面的 BPF_SOCK_OPS_* union { __u32 args[4]; // Optionally passed to bpf program __u32 reply; // BPF 程序的返回值。例如,op==BPF_SOCK_OPS_TIMEOUT_INIT 时, // BPF 程序的返回值就表示希望为这个 TCP 连接设置的 RTO 值 __u32 replylong[4]; // Optionally returned by bpf prog }; __u32 family; __u32 remote_ip4; /* Stored in network byte order */ __u32 local_ip4; /* Stored in network byte order */ __u32 remote_ip6[4]; /* Stored in network byte order */ __u32 local_ip6[4]; /* Stored in network byte order */ __u32 remote_port; /* Stored in network byte order */ __u32 local_port; /* stored in host byte order */ ... };
返回值
如前面所述,ops 类型不同,返回值也不同。
加载方式:attach 到某个 cgroup(可使用 bpftool 等工具)
指定以 BPF_CGROUP_SOCK_OPS
类型,将 BPF 程序 attach 到某个 cgroup 文件描述符。
依赖 cgroupv2。
内核已经有了 BPF_PROG_TYPE_CGROUP_SOCK
类型的 BPF 程序,这里为什么又要引入一个 BPF_PROG_TYPE_SOCK_OPS
类型的程序呢?
-
BPF_PROG_TYPE_CGROUP_SOCK
类型的 BPF 程序:在一个连接(connection)的生命周期中 只执行一次 , -
BPF_PROG_TYPE_SOCK_OPS
类型的 BPF 程序:在一个连接的生命周期中,在 不同地方被多次调用 。
程序示例
1. Customize TCP initial RTO (retransmission timeout) with BPF
2. Cracking kubernetes node proxy (aka kube-proxy) ,其中的第五种实现方式
3. (译) 利用 ebpf sockmap/redirection 提升 socket 性能(2020)
延伸阅读
3 BPF_PROG_TYPE_SK_SKB
使用场景
场景一:修改 skb/socket 信息,socket 重定向
这个功能依赖 sockmap,后者是一种特殊类型的 BPF map,其中存储的是 socket 引用(references)。
典型流程:
- 创建 sockmap
- 拦截 socket 操作,将 socket 信息存入 sockmap
- 拦截 socket sendmsg/recvmsg 等系统调用,从 msg 中提取信息(IP、port等),然后 在 sockmap 中查找对端 socket,然后重定向过去。
根据提取到的 socket 信息判断接下来应该做什么的过程称为verdict(判决)。 verdict 类型可以是:
__SK_DROP __SK_PASS __SK_REDIRECT
场景二:动态解析消息流(stream parsing)
这种程序的一个应用是 strparser framework 。
它与上层应用配合, 在内核中提供应用层消息解析的支持 (provide kernel support for application layer messages)。两个使用了 strparser 框架的例子:TLS 和 KCM( Kernel Connection Multiplexor)。
Hook 位置
socket redirection 类型
TODO
strparser 类型: smap_parse_func_strparser()
/ smap_verdict_func()
Socket receive 过程执行到
smap_parse_func_strparser()
时,触发 STREAM_PARSER BPF 程序执行。
执行到 smap_verdict_func()
时,触发 VERDICT BPF 程序执行。
程序签名
传入参数: struct __sk_buff *
见的介绍。
从中可以提取出 socket 信息(IP、port 等)。
返回值
TODO
加载方式:attach 到某个 sockmap(可使用 bpftool 等工具)
这种程序需要指定 BPF_SK_SKB_STREAM_*
类型,将 BPF 程序 attach 到 sockmap:
BPF_SK_SKB_STREAM_VERDICT BPF_SK_SKB_STREAM_PARSER
程序示例
1. (译) 利用 ebpf sockmap/redirection 提升 socket 性能(2020)
2. strparser
框架:解析消息流
延伸阅读
- 内核 patch 文档: BPF: sockmap and sk redirect support
————————————————————————
TC 子系统相关类型
————————————————————————
将 BPF 程序用作 tc 分类器(classifiers)和执行器(actions)。
BPF_PROG_TYPE_SCHED_CLS BPF_PROG_TYPE_SCHED_ACT
TC 是 Linux 的 QoS 子系统。帮助信息(非常有用):
tc(8) tc-bpf(8)
1 BPF_PROG_TYPE_SCHED_CLS
使用场景
场景一:tc 分类器
tc(8)
命令支持 eBPF,因此能直接将 BPF 程序作为
classifiers 和 actions 加载到 ingress/egress hook 点。
如何使用 tc BPF 提供的能力,参考 man8: tc-bpf 。
Hook 位置: sch_handle_ingress()
/ sch_handle_egress()
sch_handle_ingress()/egress()
会调用到 tcf_classify()
,
- 对于 ingress,通过网络设备的 receive 方法做 流量分类 ,这个处 理位置在 网卡驱动处理之后,在内核协议栈(IP 层)处理之前 。
- 对于 egress,将包交给设备队列(device queue)发送之前,执行 BPF 程序。
程序签名
传入参数: struct __sk_buff *
见的介绍。
返回值
返回 TC verdict 结果。
加载方式: tc
命令(背后使用 netlink)
步骤:
- 为网络设备添加分类器(classifier/qdisc):创建一个 “clsact” qdisc
- 为网络设备添加过滤器(filter):需要指定方向(egress/ingress)、目标文件、ELF section 等选项
例如,
$ tc qdisc add dev eth0 clsact $ tc filter add dev eth0 egress bpf da obj toy-proxy-bpf.o sec egress
加载过程分为 tc 前端和内核 bpf 后端两部分, 中间通过 netlink socket 通信,源码分析见 Firewalling with BPF/XDP: Examples and Deep Dive
程序示例
延伸阅读
- cls_bpf.c 实现 tc classifier 模块
2 BPF_PROG_TYPE_SCHED_ACT
使用方式与 BPF_PROG_TYPE_SCHED_CLS
类似,但用作 TC action。
使用场景
场景一:tc action
Hook 位置
程序签名
加载方式: tc
命令
延伸阅读
- act_bpf.c 实现 tc action 模块
————————————————————————
XDP(eXpress Data Path)程序
————————————————————————
XDP 位于设备驱动中(在创建 skb 之前),因此能最大化网络处理性能, 而且可编程、通用(很多厂商的设备都支持)。
1 BPF_PROG_TYPE_XDP
使用场景
场景一:防火墙、四层负载均衡等
由于 XDP 程序执行时 skb 都还没创建,开销非常低,因此效率非常高。适用于 DDoS 防御、四层负载均衡等场景。
XDP 就是通过 BPF hook 对内核进行 运行时编程 (run-time programming),但 基于内核而不是绕过(bypass)内核 。
Hook 位置:网络驱动
XDP 是在 网络驱动 中实现的, 有专门的 TX/RX queue (native 方式)。
对于没有实现 XDP 的驱动,内核中实现了一个称为 “generic XDP” 的 fallback 实现, 见 net/core/dev.c 。
- Native XDP:处理的阶段非常早,在 skb 创建之前,因此性能非常高;
- Generic XDP: 在 skb 创建之后 ,因此性能比前者差,但功能是一样的。
程序签名
传入参数: struct xdp_md *
定义 ,非常轻量级:
// include/uapi/linux/bpf.h /* user accessible metadata for XDP packet hook */ struct xdp_md { __u32 data; __u32 data_end; __u32 data_meta; /* Below access go through struct xdp_rxq_info */ __u32 ingress_ifindex; /* rxq->dev->ifindex */ __u32 rx_queue_index; /* rxq->queue_index */ __u32 egress_ifindex; /* txq->dev->ifindex */ };
返回值: enum xdp_action
// include/uapi/linux/bpf.h enum xdp_action { XDP_ABORTED = 0, XDP_DROP, XDP_PASS, XDP_TX, XDP_REDIRECT, };
加载方式:netlink socket
通过 netlink socket 消息 attach:
socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE) NLA_F_NESTED | 43
用 tc
attach BPF 程序,其实背后使用的也是 netlink socket。
程序示例
1. samples/bpf/bpf_load.c
延伸阅读
TODO
————————————————————————
CGroups 相关的类型
————————————————————————
CGroups 用于 对一组进程 (a group of processes)进行控制,
- 处理资源分配,例如 CPU、网络带宽等。
- 系统资源权限控制(allowing or denying)。
CGroups 最典型的使用场景是容器 (containers)。
- 命名空间(namespace):控制资源视图,即 能看到什么,不能看到什么 ,
- CGroups:控制的 能使用多少 ?
具体到 eBPF 方面,可以 用 CGroups 来控制访问权限 (allow or deny),程序的返回结果只有两种:
- 放行
- 禁止(导致随后包被丢弃)
例如,很多 hook 会执行到宏 __cgroup_bpf_run_filter_skb()
,它负责执行 cgroup BPF 程序:
// include/linux/bpf-cgroup.h #define BPF_CGROUP_RUN_PROG_INET_INGRESS(sk, skb) \ ({ \ int __ret = 0; \ if (cgroup_bpf_enabled) \ __ret = __cgroup_bpf_run_filter_skb(sk, skb, \ BPF_CGROUP_INET_INGRESS); \ \ __ret; \ }) #define BPF_CGROUP_RUN_SK_PROG(sk, type) \ ({ \ int __ret = 0; \ if (cgroup_bpf_enabled) { \ __ret = __cgroup_bpf_run_filter_sk(sk, type); \ } \ __ret; \ })
完整的 cgroups hook 列表见 enum bpf_attach_type
列表,其中的 BPF_CGROUP_*
。
1 BPF_PROG_TYPE_CGROUP_SKB
使用场景
场景一:在 CGroup 级别:放行/丢弃数据包
在 IP egress/ingress 层禁止或允许网络访问。
Hook 位置: sk_filter_trim_cap()
对于 inet ingress, sk_filter_trim_cap()
会调用 BPF_CGROUP_RUN_PROG_INET_INGRESS(sk, skb)
;如果返回值非零,错误信息会传递给调
用方(例如, __sk_receive_skb()
),随后包会被丢弃并释放(discarded and freed)
。
egress 是类似的,但在 ip[6]_finish_output()
中。
程序签名
传入参数: struct sk_buff *skb
返回值
-
1
:放行; -
其他任何值:会使
__cgroup_bpf_run_filter_skb()
返回-EPERM
,这会进一步返 回给调用方,告诉它们应该丢弃该包。
加载方式:attach 到 cgroup 文件描述符
根据 BPF attach 的 hook 位置,选择合适的 attach 类型:
BPF_CGROUP_INET_INGRESS BPF_CGROUP_INET_EGRESS
2 BPF_PROG_TYPE_CGROUP_SOCK
使用场景
场景一:在 CGroup 级别:触发 socket 操作时拒绝/放行网络访问
这里的 socket 相关事件包括 BPF_CGROUP_INET_SOCK_CREATE、BPF_CGROUP_SOCK_OPS。
程序签名
传入参数: struct sk_buff *skb
返回值
跟前面一样,程序返回 1 表示允许访问。
返回其他值会导致 __cgroup_bpf_run_filter_sk()
返回 -EPERM
,调用方收到这个返回值会将包丢弃。
触发执行: inet_create()
Socket 创建时会执行 inet_create()
,里面会调用 BPF_CGROUP_RUN_PROG_INET_SOCK()
,如果该函数执行失败,socket 就会被释放。
加载方式:attach 到 cgroup 文件描述符
TODO
————————————————————————
kprobes、tracepoints、perf events
————————————————————————
三者都用于 kernel instrumentation。简单对比:
数据源 | Type | Kernel/User space | |
---|---|---|---|
kprobes | Dynamic | Kernel | 观测内核函数的运行时(进入和离开函数)参数值等信息 |
uprobes | Dynamic | Userspace | 同上,但观测的是用户态函数 |
tracepoints | Static | Kernel | 将自定义 handler 编译并加载到某些内核 hook,能拿到更多观测信息 |
USDT | Static | Userspace |
更具体区别可参考 Linux tracing systems & how they fit together 。
-
kprobes :对 特定函数 进行 instrumentation。
-
进入
函数时触发
kprobe
-
离开
函数时触发
kretprobe
启用后,会 将 probe 位置的一段空代码替换为一个断点指令 。 当程序执行到这个断点时,会 触发一条 trap 指令 ,然后保存寄存器状态, 跳转到指定的处理函数 (instrumentation handler)。
- kprobes 由 kprobe_dispatcher() 处理, 其中会获取 kprobe 的地址和寄存器上下文信息。
- kretprobes 是通过 kprobes 实现的。
-
进入
函数时触发
-
Tracepoints :内核中的轻量级 hook。
Tracepoints 与 kprobes 类似,但 前者是动态插入代码来完成的,后者 显式地(静态地)写在代码中的 。 启用之后,会 从这些地方收集 debug 信息 。
同一个 tracepoints 可能会在多个地方声明;例如,
trace_drv_return_int()
在 net/mac80211/driver-ops.c 中的多个地方被调用。查看可用的 tracepoints 列表:
ls /sys/kernel/debug/tracing/events
。 -
Perf events :是这里提到的几种 eBPF 程序的基础。
BPF 基于已有的基础设施来完成事件采样(event sampling),允许 attach 程序到 感兴趣的 perf 事件,包括kprobes, uprobes, tracepoints 以及软件和硬件事件。
这些 instrumentation points 使 BPF 成为了一个通用的跟踪工具 , 超越了最初的网络范畴。
1 BPF_PROG_TYPE_KPROBE
使用场景
场景一:观测内核函数(kprobe)和用户空间函数(uprobe)
通过 kprobe
/ kretprobe
观测内核函数。 k[ret]probe_perf_func()
会执行加载到 probe 点的 BPF 程序。
另外,这种程序也能 attach 到 u[ret]probes
,详情见 uprobetracer.txt
。
Hook 位置: k[ret]probe_perf_func()
/ u[ret]probe_perf_func()
启用某个 probe 并执行到断点时, k[ret]probe_perf_func()
会通过 trace_call_bpf()
执行 attach 在这个 probe 位置的 BPF 程序。
u[ret]probe_perf_func()
也是类似的。
程序签名
传入参数: struct pt_regs *ctx
可以通过这个指针访问寄存器。
这个变量内的很多字段是平台相关的,但也有一些通用函数,例如 regs_return_value(regs)
,返回的是存储程序返回值的寄存器内的值(x86 上对应的是 ax
寄存器)。
返回值
加载方式: /sys/fs/debug/tracing/
目录下的配置文件
/sys/kernel/debug/tracing/events/[uk]probe/<probename>/id /sys/kernel/debug/tracing/events/[uk]retprobe/<probename>/id
程序示例
Documentation/trace/kprobetrace.txt 有详细的例子。例如,
# 创建一个名为 `myprobe` 的程序,attach 到进入函数 `tcp_retransmit_skb()` 的地方 $ echo 'p:myprobe tcp_retransmit_skb' > /sys/kernel/debug/tracing/kprobe_events # 获取 probe id $ cat /sys/kernel/debug/tracing/events/kprobes/myprobe/id 2266
用以上 id 打开一个 perf event,启用它,然后将这个 perf event 的 BPF 程序指定为我们的程序。 过程可参考 load_and_attach() :
// samples/bpf/bpf_load.c static int load_and_attach(const char *event, struct bpf_insn *prog, int size) { struct perf_event_attr attr; /* Load BPF program and assign programfd to it; and get probeid of probe from sysfs */ attr.type = PERF_TYPE_TRACEPOINT; attr.sample_type = PERF_SAMPLE_RAW; attr.sample_period = 1; attr.wakeup_events = 1; attr.config = probeid; // /sys/kernel/debug/tracing/events/kprobes/<probe>/id eventfd = sys_perf_event_open(&attr, -1, 0, programfd, 0); ioctl(eventfd, PERF_EVENT_IOC_ENABLE, 0); ioctl(eventfd, PERF_EVENT_IOC_SET_BPF, programfd); ... }
延伸阅读
TODO
2 BPF_PROG_TYPE_TRACEPOINT
使用场景
场景一:Instrument 内核代码中的 tracepoints
启用方式和上面的 kprobe 类似:
$ echo 1 > /sys/kernel/xxx/enable
可跟踪的事件都在 /sys/kernel/debug/tracing/events 目录下面。
Hook 位置: perf_trace_<event_class>()
相应的 tracepoint 启用并执行到之后, perf_trace_<event_class>()
(定义见 include/trace/perf.h)
调用 perf_trace_run_bpf_submit()
,后者通过 trace_call_bpf()
触发 BPF 程序执行。
程序签名
传入参数:因 tracepoint 而异
传入的参数和类型因 tracepoint 而异,见其定义。
见 /sys/kernel/debug/tracing/events/<tracepoint>/format
。例如,
$ sudo cat /sys/kernel/debug/tracing/events/net/netif_rx/format name: netif_rx ID: 1457 format: field:unsigned short common_type; offset:0; size:2; signed:0; field:unsigned char common_flags; offset:2; size:1; signed:0; field:unsigned char common_preempt_count; offset:3; size:1; signed:0; field:int common_pid; offset:4; size:4; signed:1; field:void * skbaddr; offset:8; size:8; signed:0; field:unsigned int len; offset:16; size:4; signed:0; field:__data_loc char[] name; offset:20; size:4; signed:1; print fmt: "dev=%s skbaddr=%p len=%u", __get_str(name), REC->skbaddr, REC->len
顺便看一下这个 tracepoint 在内核中的 实现 ,
// net/core/dev.c static int netif_rx_internal(struct sk_buff *skb) { net_timestamp_check(netdev_tstamp_prequeue, skb); trace_netif_rx(skb); ... }
返回值
加载方式: /sys/fs/debug/tracing/
目录下的配置文件
例如,
# 启用 `net/net_dev_xmit` tracepoint as "myprobe2" $ echo 'p:myprobe2 trace:net/net_dev_xmit' > /sys/kernel/debug/tracing/kprobe_events # 获取 probe ID $ cat /sys/kernel/debug/tracing/events/kprobes/myprobe2/id 2270
过程加载代码可参考 load_and_attach() 。
3 BPF_PROG_TYPE_PERF_EVENT
使用场景
场景一:Instrument 软件/硬件 perf 事件
包括系统调用事件、定时器超时事件、硬件采样事件等。硬件事件包括 PMU(processor monitoring unit)事件,它告诉我们已经执行了多少条指令之类的信息。
Perf 事件监控能具体到某个进程、组、处理器,也可以指定采样频率。
加载方式: ioctl()
- perf_event_open() ,带一些采样配置信息;
-
ioctl(fd, PERF_EVENT_IOC_SET_BPF)
设置 BPF程序, - 然后用 ioctl(fd, PERF_EVENT_IOC_ENABLE) 启用事件,
程序签名
传入参数: struct bpf_perf_event_data *
定义 :
// include/uapi/linux/bpf_perf_event.h struct bpf_perf_event_data { bpf_user_pt_regs_t regs; __u64 sample_period; __u64 addr; };
触发执行:每个采样间隔执行一次
取决于 perf event firing 和选择的采样频率。
————————————————————————
轻量级隧道类型
————————————————————————
Lightweight tunnels 提供了 对内核路 由子系统的编程能力 ,据此可以实现轻量级隧道。
举个例子,下面是没有 BPF 编程能力时,如何(为不同协议)添加路由:
# VXLAN: $ ip route add 40.1.1.1/32 encap vxlan id 10 dst 50.1.1.2 dev vxlan0 $ MPLS: $ ip route add 10.1.1.0/30 encap mpls 200 via inet 10.1.1.1 dev swp1
有了 BPF 可编程性之后,能为出向流量(入向是只读的)做封装。 详见 BPF for lightweight tunnel encapsulation 。
与 tc
类似, ip route
支持直接将 BPF 程序 attach 到网络设备:
$ ip route add 192.168.253.2/32 \ encap bpf out obj lwt_len_hist_kern.o section len_hist \ dev veth0
1 BPF_PROG_TYPE_LWT_IN
使用场景
场景一:检查入向流量是否需要做解封装(decap)
Examine inbound packets for lightweight tunnel de-encapsulation.
Hook 位置: lwtunnel_input()
该函数支持多种封装类型。 The BPF case runs bpf_input in net/core/lwt_bpf.c with redirection disallowed.
程序签名
传入参数: struct sk_buff *
返回值
加载方式: ip route add
$ ip route add <route+prefix> encap bpf in obj <bpf obj file.o> section <ELF section> dev <device>
延伸阅读
TODO
2 BPF_PROG_TYPE_LWT_OUT
使用场景
场景一:对出向流量做封装(encap)
加载方式: ip route add
$ ip route add <route+prefix> encap bpf out obj <bpf object file.o> section <ELF section> dev <device>
程序签名
传入参数: struct __sk_buff *
触发执行: lwtunnel_output()
3 BPF_PROG_TYPE_LWT_XMIT
使用场景
场景一:实现轻量级隧道发送端的 encap/redir 方法
Hook 位置: lwtunnel_xmit()
程序签名
传入参数: struct __sk_buff *
定义见。
加载方式: ip route add
$ ip route add <route+prefix> encap bpf xmit obj <bpf obj file.o> section <ELF section> dev <device>
- [译] 写给工程师:关于证书(certificate)和公钥基础设施(PKI)的一切(SmallStep, 2018)
- [译] 基于角色的访问控制(RBAC):演进历史、设计理念及简洁实现(Tailscale, 2021)
- [译] Control Group v2(cgroupv2 权威指南)(KernelDoc, 2021)
- [译] Linux Socket Filtering (LSF, aka BPF)(KernelDoc,2021)
- [译] LLVM eBPF 汇编编程(2020)
- [译] Cilium:BPF 和 XDP 参考指南(2021)
- BPF 进阶笔记(三):BPF Map 内核实现
- BPF 进阶笔记(二):BPF Map 类型详解:使用场景、程序示例
- BPF 进阶笔记(一):BPF 程序类型详解:使用场景、函数签名、执行位置及程序示例
- 源码解析:K8s 创建 pod 时,背后发生了什么(四)(2021)
- 源码解析:K8s 创建 pod 时,背后发生了什么(三)(2021)
- [译] 迈向完全可编程 tc 分类器(NetdevConf,2016)
- [译] 云原生世界中的数据包标记(packet mark)(LPC, 2020)
- [译] 利用 eBPF 支撑大规模 K8s Service (LPC, 2019)
- 计算规模驱动下的网络方案演进
- 迈入 Cilium BGP 的云原生网络时代
- [译] BeyondProd:云原生安全的一种新方法(Google, 2019)