5

BPF 进阶笔记(一):BPF 程序类型详解:使用场景、函数签名、执行位置及程序示例

 3 years ago
source link: http://arthurchiao.art/blog/bpf-advanced-notes-1-zh/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

BPF 进阶笔记(一):BPF 程序类型详解:使用场景、函数签名、执行位置及程序示例

Published at 2021-07-04 | Last Update 2021-07-04

内核目前支持 30 来种 BPF 程序类型。对于主要的程序类型,本文将介绍其:

  1. 使用场景:适合用来做什么?
  2. Hook 位置:在何处(where)、何时(when)会触发执行?例如在内核协议栈的哪个位置,或是什么事件触发执行。
  3. 程序签名(程序 入口函数 签名)
    1. 传入参数:调用到 BPF 程序时,传给它的上下文(context,也就是函数参数)是什么?
    2. 返回值:返回值类型、含义、合法列表等。
  4. 加载方式:如何将程序附着(attach)到执行点?
  5. 程序示例:一些实际例子。
  6. 延伸阅读:其他高级主题,例如相关的内核设计与实现。

本文主要参考:

  1. BPF: A Tour of Program Types,内容略老,基于内核 4.14

关于 “BPF 进阶笔记” 系列

平时学习使用 BPF 时所整理。由于是笔记而非教程,因此内容不会追求连贯,有基础的 同学可作查漏补缺之用。

文中涉及的代码,如无特殊说明,均基于内核 5.8/5.10 版本。


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

  1. err != 0:直接 return err,返回到调用方,不再继续原来正常的内核处理逻辑 __sock_queue_rcv_skb();所以效果就是:将这个包过滤了出来(符合过滤条件);
  2. err == 0:接下来继续执行正常的内核处理,也就是这个包不符合过滤条件

所以至此大概就知道要实现过滤和截断功能,程序应该返回什么了。要精确搞清楚,需要 看 sk_filter() 一直调用到 BPF 程序的代码,看中间是否对 BPF 程序的返回值做了封 装和转换。

这里给出结论:BPF 程序的返回值

  • nn < pkt_size):返回一个 截断的包(副本),只保留前面 n 个字节。
  • 0忽略这个包;

需要说明:

  1. 这里所谓的截断并不是截断原始包,而只是复制一份包的元数据,修改其中的包长字段;
  2. 程序本身不会截断或丢弃原始流量,也就是说,对原始流量是只读的(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 的优化,提升性能。例如,

  1. 监听到被动建连(passive establishment of a connection)事件时,如果 对端和本机不在同一个网段,就动态修改这个 socket 的 MTU。 这样能避免包因为太大而被中间路由器分片(fragmenting)。
  2. 替换目的 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 分为两种类型:

  1. 通过 BPF 程序的返回值来动态修改配置,类型包括

    1. BPF_SOCK_OPS_TIMEOUT_INIT
    2. BPF_SOCK_OPS_RWND_INIT
    3. BPF_SOCK_OPS_NEEDS_ECN
    4. BPF_SOCK_OPS_BASE_RTT
  2. 在 socket/tcp 状态发生变化时,回调(callback)BPF 程序,类型包括

    1. BPF_SOCK_OPS_TCP_CONNECT_CB
    2. BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB
    3. BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB
    4. BPF_SOCK_OPS_RTO_CB
    5. BPF_SOCK_OPS_RETRANS_CB
    6. BPF_SOCK_OPS_STATE_CB
    7. 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 类型的程序呢?

  1. BPF_PROG_TYPE_CGROUP_SOCK 类型的 BPF 程序:在一个连接(connection)的生命周期中只执行一次
  2. 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)

  1. bpf: Adding support for sock_ops

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 类型

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 等)。

加载方式:attach 到某个 sockmap(可使用 bpftool 等工具)

这种程序需要指定 BPF_SK_SKB_STREAM_* 类型,将 BPF 程序 attach 到 sockmap:

  • 重定向程序:指定 BPF_SK_SKB_STREAM_VERDICT 加载到某个 sockmap。
  • strparser 程序:指定 BPF_SK_SKB_STREAM_PARSER 加载到某个 sockmap。

1. (译) 利用 ebpf sockmap/redirection 提升 socket 性能(2020)

2. strparser 框架:解析消息流

  1. 内核 patch 文档:BPF: sockmap and sk redirect support

————————————————————————

TC 子系统相关类型

————————————————————————

将 BPF 程序用作 tc 分类器(classifiers)和执行器(actions)。

  • BPF_PROG_TYPE_SCHED_CLS:tc classifier,分类器
  • BPF_PROG_TYPE_SCHED_ACT:tc action,动作

TC 是 Linux 的 QoS 子系统。帮助信息(非常有用):

  • tc(8) manpage for a general introduction
  • tc-bpf(8) for BPF specifics

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)

  1. 为网络设备添加分类器(classifier/qdisc):创建一个 “clsact” qdisc
  2. 为网络设备添加过滤器(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

2 BPF_PROG_TYPE_SCHED_ACT

使用方式与 BPF_PROG_TYPE_SCHED_CLS 类似,但用作 TC action。

场景一:tc action

Hook 位置

加载方式:tc 命令

————————————————————————

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:

  • 首先创建一个 netlink 类型的 socket:socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)
  • 然后发送一个 NLA_F_NESTED | 43 类型的 netlink 消息,表示这是 XDP message。消息中包含 BPF fd, the interface index (ifindex) 等信息。

tc attach BPF 程序,其实背后使用的也是 netlink socket。

1. samples/bpf/bpf_load.c

————————————————————————

CGroups 相关的类型

————————————————————————

CGroups 用于对一组进程(a group of processes)进行控制,

  • 处理资源分配,例如 CPU、网络带宽等。
  • 系统资源权限控制(allowing or denying)。

CGroups 最典型的使用场景是容器(containers)。

  • 命名空间(namespace):控制资源视图,即能看到什么,不能看到什么
  • CGroups:控制的能使用多少

具体到 eBPF 方面,可以用 CGroups 来控制访问权限(allow or deny),程序的返回结果只有两种:

  1. 禁止(导致随后包被丢弃)

例如,很多 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 文件描述符

————————————————————————

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);
    ...
}

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()

  1. perf_event_open() ,带一些采样配置信息;
  2. ioctl(fd, PERF_EVENT_IOC_SET_BPF) 设置 BPF程序,
  3. 然后用 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>

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>

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK