近年来,eBPF 的使用量急剧上升。因为 eBPF 运行在内核态,且基于事件触发,所以代码的运行速度极快(不涉及上下文切换)更准确。eBPF 被广泛用于内核跟踪(kprobes/tracecing)的可观测性,此外还被用于一些基于 IP 地址的传统安全监控和访问控制不足的环境(例如,在基于容器的环境,如 Kubernetes )中。我们会在后续文章讨论如何使用 eBPF 进行 k8s 的 network policy 过滤,以及APP无侵入的可观测性。
在图 1 中,可以看到 Linux 内核中的各种钩子,其中可以钩住 eBPF 程序以执行。在linux-master\tools\include\uapi\linux\bpf.h 可以看到所有 sock_ops 相关的监听事件。
enum {
BPF_TCP_ESTABLISHED = 1,
BPF_TCP_SYN_SENT,
BPF_TCP_SYN_RECV,
BPF_TCP_FIN_WAIT1,
...、
(1) 内核空间组件,其中需要根据某些内核事件进行决策或数据收集,例如 NIC 上的数据包 Rx、生成 shell 的系统调用等。
(2) 用户空间组件,可以访问内核代码共享的数据结构(映射等)中写入的数据。
我们本次重点解释的代码是内核空间组件,我们使用 bpftool 命令行工具将代码加载到内核中,然后卸载它。
Linux 内核支持不同类型的 eBPF 程序,每个程序都可以附加到内核中可用的特定钩子(参见图 1)。当与这些钩子关联的事件被触发时,这些程序将执行,例如,进行诸如setsockopt()之类的系统调用,进入数据包缓冲区DMA之后的网络驱动程序钩子XDP等。
enum bpf_prog_type {
BPF_PROG_TYPE_UNSPEC,
BPF_PROG_TYPE_SOCKET_FILTER,
BPF_PROG_TYPE_KPROBE,
...
BPF_PROG_TYPE_XDP,
BPF_PROG_TYPE_SOCK_OPS,
BPF_PROG_TYPE_SK_MSG,
BPF_PROG_TYPE_SK_REUSEPORT,
...
所有类型都在 UAPI bpf.h 头文件中枚举,其中包含 eBPF 程序所需的面向用户的定义。
在这篇博文中,我们对 BPF_PROG_TYPE_SOCK_OPS 和 BPF_PROG_TYPE_SK_MSG 类型的 eBPF 程序感兴趣,它们允许我们将 BPF 程序连接到套接字操作。
当发生 TCP 连接事件时,sockops程序被执行,如上图红色框图;在套接字的 sendmsg 调用时,SK_MSG 将执行套接字数据重定向,如上图蓝色框图。
sk_msg 程序在用户态执行 sendmsg 时执行,但必须附加到套接字映射,特别是 BPF_MAP_TYPE_SOCKMAP 或 BPF_MAP_TYPE_SOCKHASH。这些映射是键值存储,其中主键是五元组信息(下文详述),值只能是套接字。一旦SK_MSG程序和MAP进行绑定,MAP中的所有套接字都将继承SK_MSG程序。本实例中,sk_msg的部分逻辑如下:
__section("sk_msg")
int bpf_tcpip_bypass(struct sk_msg_md *msg)
{
struct sock_key key = {};
sk_msg_extract4_key(msg, &key);
msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS);
return SK_PASS;
}
现在让我们把注意力转向如何填充sk_msg程序使用的 sockhash 映射。我们希望在发生 TCP 连接事件时填充此sockhash。现在,它是第二种 eBPF 程序类型 SOCK_OPS,它在 TCP 事件(如连接建立、TCP 重传等)时被调用,可以捕获套接字的详细信息。
在下面的代码片段中,我们创建了这个程序,该程序在套接字操作时触发,并处理主动(源套接字发送 SYN)和被动(目标套接字响应 SYN 的 ACK)TCP 连接的事件。
__section("sockops")
int bpf_sockops_v4(struct bpf_sock_ops *skops)
{
uint32_t family, op;
family = skops->family;
op = skops->op;
switch (op) {
case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB:
case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB:
if (family == 2) { //AF_INET
bpf_sock_ops_ipv4(skops);
}
break;
...
eBPF sockops 程序驻留在 ELF 部分 sockops 中,因此代码使用编译器部分属性放置在 sockops 部分中。当 TCP 连接事件发生时,调用 bpf_sock_ops_ipv4 方法来存储数据结构 sock_ops_map,其定义如下:
struct sock_key {
uint32_t sip4;
uint32_t dip4;
uint8_t family;
uint32_t sport;
uint32_t dport;
} __attribute__((packed));
struct bpf_map_def __section("maps") sock_ops_map = {
.type = BPF_MAP_TYPE_SOCKHASH,
.key_size = sizeof(struct sock_key),
.value_size = sizeof(int),
.max_entries = 65535,
.map_flags = 0,
};
static inline
void sk_msg_extract4_key(struct sk_msg_md *msg,
struct sock_key *key)
{
key->sip4 = msg->remote_ip4;
key->dip4 = msg->local_ip4;
key->family = 1;
key->dport = (bpf_htonl(msg->local_port) >> 16);
key->sport = FORCE_READ(msg->remote_port) >> 16;
}
(2) sk_msg 引用 sock_ops_map 查找对端 socket,进行转发。
为了快速验证 sockhash 映射被填充并被SK_MSG程序查找的数据路径,我们可以使用 SOCAT 启动 TCP 侦听器并使用 netcat 发送 TCP 连接请求。SOCK_OPS程序在获得TCP连接事件时将打印日志,可以从内核的跟踪文件中查找trace_pipe:
# start a TCP listener at port 1004, and echo back the received data
sudo socat TCP4-LISTEN:1004,fork exec:cat
# connect to the local TCP listener at port 1004
nc localhost 1004
sudo cat /sys/kernel/debug/tracing/trace_pipe
Output:
<<< ipv4 op = 4, port 57642 --> 1004 //客户端主动请求链接
<<< ipv4 op = 5, port 1004 --> 57642 //客户端收到服务端的链接信息
nc-212660 #1 bpf_tcpip_bypass before:src port:57642,dst port:1004
nc-212660 #2 bpf_tcpip_bypass transfer:src port:1004,dst port:57642
socat-212661 #1 bpf_tcpip_bypass before:src port:1004,dst port:57642
socat-212661 #2 bpf_tcpip_bypass transfer:src port:57642,dst port:1004
修改 bpf_tcpip_bypass 函数,假设增加对端口1000的拒绝访问。这里为了验证功能,没有指明ingress的label,实际k8s配置policy时,会指定ingress的label,match的入口请求才会drop。
int bpf_tcpip_bypass(struct sk_msg_md *msg)
{
struct sock_key key = {};
int dstport=bpf_ntohl((msg->remote_port) >> 16)>>16;
//增加对端口1000的拒绝访问
if ( dstport == 1000 )
{
printk("#bpf_tcpip_bypass sk_drop:src port:%d,dst port:%d\n",msg->local_port,dstport);
return SK_DROP;
}
printk("#1 bpf_tcpip_bypass before:src port:%u,dst port:%u\n",msg->local_port,bpf_ntohl((msg->remote_port) >> 16)>>16);
sk_msg_extract4_key(msg, &key);
int srcport=bpf_ntohl(key.sport)>>16;
printk("#2 bpf_tcpip_bypass transfer:src port:%u,dst port:%u\n",srcport,bpf_ntohl(key.dport)>>16);
msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS);
return SK_PASS;
}
nc localhost 1000
123
nc-219949 <<< ipv4 op = 4, port 40046 --> 1000
nc-219949 <<< ipv4 op = 5, port 1000 --> 40046
nc-219949 #bpf_tcpip_bypass sk_drop:src port:40046,dst port:1000
eBPF代码、加载和卸载eBPF程序、安装bpftool的方法可以参考https://github.com/grafanafans/club/tree/master/ebpf 执行。