XDP入门--通过用户态程序自动加载与卸载eBPF程序字节码到网卡

通过文章XDP入门–之hello world 我们知道,可以通过iproute2的ip工具向网卡去加载和卸载eBPF程序的字节码。但这个使用起来不太方便。而且在需要网卡恢复正常工作时,还需要输入相应的命令去手动卸载eBPF程序的字节码。
更为通用和可行的做法是创建一个常规的用户态程序做为让用户运行使用的管理控制程序。

  • 当用户态程序启动时,自动去加载eBPF程序的字节码到网卡
  • 当用户态程序退出时,自动去卸载eBPF程序的字节码,让相应网卡恢复正常工作模式
  • 当用户态程序运行时,用于对eBPF程序的字节码的运行做出配置与控制

所以本文章就介绍如上所述的自动加载和卸载的代码实现。

1、测试环境

硬件:基于树莓派Zero w + 带二个以太网卡的扩展底板----图中的RPi
网络:如下图所示

                                                     +- RPi -------+          +- old pc1----+
                                                     |         Eth0+----------+ Eth0        |    
                 +- Router ----+                     |  DHCP server|          | 10.0.0.10   |
                 | Firewall    |                     |   10.0.0.1  |          |             |
(Internet)---WAN-+ DHCP server +-WLAN AP-+-)))   (((-+ WLAN        |          +-------------+
                 | 192.168.3.1 |                     |             |          
                 +-------------+                     |             |          +- old pc2----+
                                                     |         Eth1+----------+ Eth0        |   
                                                     |             |          | 10.0.0.4    |                                                       
                                                     +-------------+          |             |
                                                                              +-------------+

在这里插入图片描述
在这里插入图片描述

1、eBPF字节码的源代码实现

这个字节码实现的功能:每进来一个报文就检测是不是IPV4的报文,如果是,则打印本报文的源IP地址和目标IP地址

#include <stdio.h>
#include <linux/bpf.h>
#include <net/ethernet.h>
#include <linux/if_vlan.h>
#include <netinet/in.h>
#include <linux/ip.h>
#include <bpf/bpf_helpers.h>

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

__section("prog")
int xdp_ip_filter(struct xdp_md *ctx)
{
    void *end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    int ip_src;
    int ip_dst;
    long int offset;
    short int eth_type;

    char info_fmt1[] = "Dst Addr: %pi4";
    char info_fmt2[] = "Src Addr: %pi4";
    char info_fmt3[] ="-----------------";
   
    struct ethhdr *eth = data;
    offset = sizeof(*eth);

    if (data + offset > end) {
    return XDP_ABORTED;
    }
    eth_type = eth->h_proto;

    /* 只处理 IPv4 地址*/
    if (eth_type == ntohs(ETH_P_IPV6)) {
        return XDP_PASS;
    }

    struct iphdr *iph = data + offset;
    offset += sizeof(struct iphdr);
    /*  在读取之前,确保你要读取的子节在数据包的长度范围内  */
    if (iph + 1 > end) {
        return XDP_ABORTED;
    }
    ip_src = iph->saddr;
    ip_dst = iph->daddr;

    bpf_trace_printk(info_fmt3, sizeof(info_fmt3));
    bpf_trace_printk(info_fmt2, sizeof(info_fmt2), &ip_src);
    bpf_trace_printk(info_fmt1, sizeof(info_fmt1), &ip_dst);

    return XDP_PASS;
}

char __license[] __section("license") = "GPL";

3、用户态应用程度实现简介

注意,

  1. 因为实验测试用的树梅派只有二个以太网卡,所以代码里就写死二个网卡了。
  2. 这个用户态程序不会自动退出,只能以ctrl+c的方式强制退出
  3. 编程运行环境为32位linux,所以如果你的环境是64位的,char, short, int, long, long long的长度需要进行调整

#include <stdio.h>
#include <signal.h>
#include <sys/socket.h>
#include <net/if.h>
#include <bpf/bpf.h>
#include <linux/bpf.h>
#include <linux/rtnetlink.h>
#include "/usr/src/linux-6.1/tools/testing/selftests/bpf/bpf_util.h"

int flags = XDP_FLAGS_UPDATE_IF_NOEXIST;
static int *ifindex_list;

// 退出时自动卸载eBPF字节码的函数
static void uninstall_exit(int sig)
{
        int i = 0;
        for (i = 0; i < 2; i++) {
                bpf_set_link_xdp_fd(ifindex_list[i], -1, 0);
        }
        exit(0);
}
// 以下是用户态程序入口
int main(int argc, char *argv[])
{
        int i;
        char filename[64];
        struct bpf_object *obj;
        struct bpf_prog_load_attr prog_load_attr = {
                .prog_type      = BPF_PROG_TYPE_XDP,
        };
        int prog_fd;

        // 以下bridge.o依赖于你编译出来的.o文件名,做修改,.o要和当前代码编译出来的可执行程序放在同一个目录下
        snprintf(filename, sizeof(filename), "bridge.o");
        prog_load_attr.file = filename;

        // 从文件中载入eBPF字节码
        if (bpf_prog_load_xattr(&prog_load_attr, &obj, &prog_fd)) {
                return 1;
        }

        ifindex_list = (int *)calloc(2, sizeof(int *));

        //注意,运行时,需要输入二个网卡的ifname, 就是eth0 eth1这种,依赖于系统不一样,可能名字会不同,下面的代码会把ifname转换在ifindex。
        ifindex_list[0] = if_nametoindex(argv[1]);
        ifindex_list[1] = if_nametoindex(argv[2]);

        for (i = 0; i < 2; i++) {
                // 将eBPF字节码安装到指定的网卡
                if (bpf_set_link_xdp_fd(ifindex_list[i], prog_fd, flags) < 0) {
                        printf("install xdp fd failed\n");
                        return 1;
                }
        }
        //设置程序退出时,自动卸载eBPF字节码的函数
        signal(SIGINT, uninstall_exit);

        // 进入运行循环,什么都不做,只打印一个working...
        while(1){
            i++;
            sleep(1);
            printf("working...%d\r\n", i);
        }

}


4、编译与运行

将用户态程序存成文件main.c, 将eBPF程序存成bridge.c, 分别用以下命令进行编译

gcc main.c -lbpf
sudo clang -O2 -Wall -target bpf -c bridge.c -o bridge.o

会在当前目录下生成a.out和bridge.o二个文件

然后用以下命令运行:

sudo ./a.out eth0 eth1

5、运行状态验证

  1. 用户态程序已经正常运行(这里有个告警打印,不影响功能,忽略则可)

meihualing@raspberrypi:~/userloadprint $ sudo ./a.out eth0 eth1
libbpf: elf: skipping unrecognized data section(4) .rodata.str1.1
working...3
working...4
working...5
working...6
working...7
working...8
working...9
working...10
working...11
working...12
working...13
working...14
working...15
working...16
working...17
working...18
working...19
working...20
working...21
working...22
working...23
working...24
working...25
working...26
working...27

2,然后eBPF字节码运行的输出如下(如需要详细的原理,可参见XDP入门–BPF程序如何打印log, printf log,打印日志)

因为eth0,eth1接着的电脑后台程序在和互联网交互,所以不需要任何东西就可以看到字节码的打印输出,你的linux特别干净,没有后台运行的东西,则可以从10.0.0.4 ping 10.0.0.10后,得到类似下方的输出。

sudo cat /sys/kernel/debug/tracing/trace_pipe

  kworker/u3:0-60      [000] d.s..  7613.615253: bpf_trace_printk: -----------------
    kworker/u3:0-60      [000] d.s..  7613.615287: bpf_trace_printk: Src Addr: 010.000.000.004
    kworker/u3:0-60      [000] d.s..  7613.615299: bpf_trace_printk: Dst Addr: 115.223.009.115
    kworker/u3:0-60      [000] d.s..  7613.615756: bpf_trace_printk: -----------------
    kworker/u3:0-60      [000] d.s..  7613.615784: bpf_trace_printk: Src Addr: 010.000.000.004
    kworker/u3:0-60      [000] d.s..  7613.615795: bpf_trace_printk: Dst Addr: 115.223.009.115
    kworker/u3:0-60      [000] d.s..  7613.617634: bpf_trace_printk: -----------------
    kworker/u3:0-60      [000] d.s..  7613.617668: bpf_trace_printk: Src Addr: 010.000.000.004
    kworker/u3:0-60      [000] d.s..  7613.617679: bpf_trace_printk: Dst Addr: 115.223.009.115
            sshd-1291    [000] d.s..  7613.620971: bpf_trace_printk: -----------------
            sshd-1291    [000] d.s..  7613.621009: bpf_trace_printk: Src Addr: 010.000.000.004
            sshd-1291    [000] d.s..  7613.621022: bpf_trace_printk: Dst Addr: 192.168.003.190
          <idle>-0       [000] d.s..  7613.628747: bpf_trace_printk: -----------------
          <idle>-0       [000] d.s..  7613.628785: bpf_trace_printk: Src Addr: 010.000.000.004
          <idle>-0       [000] d.s..  7613.628797: bpf_trace_printk: Dst Addr: 192.168.003.190
          <idle>-0       [000] d.s..  7613.839358: bpf_trace_printk: -----------------
          <idle>-0       [000] d.s..  7613.839397: bpf_trace_printk: Src Addr: 010.000.000.004
          <idle>-0       [000] d.s..  7613.839409: bpf_trace_printk: Dst Addr: 192.168.003.190
          <idle>-0       [000] d.s..  7613.849433: bpf_trace_printk: -----------------
          <idle>-0       [000] d.s..  7613.849473: bpf_trace_printk: Src Addr: 010.000.000.004
          <idle>-0       [000] d.s..  7613.849484: bpf_trace_printk: Dst Addr: 192.168.003.190

<think>我们正在讨论如何使用Python加载和执行eBPF字节码。根据引用[1],使用原生CO-RE libbpf需要编写用户态加载代码来帮助eBPF程序加载和从内核获取信息。而引用[2]提到eBPF的Maps机制用于用户程序和内核程序之间的交互。引用[3]提到了BCC工具,但指出在分发时可能需要编译环境。 在Python中,我们可以使用bcc库(BPF Compiler Collection)来加载和运行eBPF程序。但是,用户要求的是加载eBPF字节码(即已经编译好的eBPF程序),而不是源代码。bcc库通常是从源代码编译,但也可以加载已编译的字节码。 另外,还有一个库叫做libbpf,它提供了更底层的操作,并且支持CO-RE(Compile Once Run Everywhere)。在Python中,我们可以使用libbpf的Python绑定,或者使用bcc库。 步骤: 1. 确保系统支持eBPF,并且已经安装了必要的工具和库(如bcc、libbpf等)。 2. 编写eBPF程序并编译为字节码(例如,使用clang编译为ELF文件,其中包含eBPF字节码)。 3. 使用Python库(如bcc)加载该ELF文件,并附加到相应的事件上。 这里我们使用bcc库,因为它提供了方便的Python接口。虽然bcc通常用于编译eBPF源代码,但它也支持加载已编译的程序。 示例: 假设我们已经有一个编译好的eBPF程序(例如`program.bpf.o`),我们可以使用以下Python代码加载并运行它: ```python from bcc import BPF # 加载已编译的eBPF字节码文件 b = BPF(src_file="program.bpf.o") # 附加eBPF程序到事件(例如kprobe) b.attach_kprobe(event="some_kernel_function", fn_name="my_bpf_program") # 读取eBPF map或perf事件等 while True: # 例如,从map中读取数据 print(b["my_map"].items()) ``` 注意:`BPF`类的`src_file`参数也可以接受已编译的ELF文件(通常是.o文件)。这样,我们就避免了每次运行时的编译。 但是,如果用户希望直接提供字节码(而不是文件),则可以使用`BPF`类的`text`参数,但是`text`参数通常用于eBPF的C源代码。对于字节码,我们可以使用`BPF`类的`__init__`方法中的`bytecode`参数(注意:bcc的某些版本支持)。不过,更常见的是从文件加载。 另外,如果使用libbpf,我们可以考虑使用libbpf的Python绑定(如pybpf或libbpf-ctypes等),但bcc更为常用。 因此,推荐的方法是: 1. 将eBPF程序编译为ELF对象文件(包含eBPF字节码)。 2. 使用bcc的Python库加载该文件。 编译eBPF程序字节码文件(假设源代码为`program.bpf.c`): ```bash clang -O2 -target bpf -c program.bpf.c -o program.bpf.o ``` 在Python中加载并运行。 如果用户没有提供编译好的文件,而是希望直接嵌入字节码,那么可能需要将字节码转换为C数组或直接读取字节码内容,然后使用bcc的`BPF`类的`bytecode`参数(如果支持)。但是,bcc的文档中并没有明确说明`bytecode`参数的使用,所以更推荐使用文件方式。 另外,引用[1]中提到,使用原生的CO-RE libbpf需要编写用户态加载代码。在bcc中,这些加载代码已经被封装,因此使用起来更方便。 相关问题: 1. 如何将eBPF程序编译为字节码? 2. 除了kprobe,还有哪些事件可以附加eBPF程序? 3. 如何从eBPF的map中读取数据并处理? 4. 使用Python加载eBPF字节码时,如何管理eBPF程序的生命周期? 5. 在哪些场景下使用eBPF字节码直接加载比使用BCC实时编译更有优势? 根据上述讨论,我们给出一个完整的示例。</think>### 如何使用 Python 加载和执行 eBPF 字节码 加载和执行 eBPF 字节码需要以下关键步骤: #### 1. **准备工作** - 确保内核支持 eBPF(Linux 4.4+) - 安装依赖: ```bash sudo apt install libbpf-dev python3-bpfcc bpfcc-tools ``` - 编译 eBPF 程序字节码(`.o` 文件): ```bash clang -O2 -target bpf -c ebpf_program.c -o ebpf_program.o ``` #### 2. **Python 实现步骤** 使用 `bcc` 库加载字节码并管理 eBPF 程序: ```python from bcc import BPF # 加载 eBPF 字节码文件 bpf = BPF(src_file="ebpf_program.o") # 附加到内核事件(示例:kprobe) bpf.attach_kprobe(event="tcp_sendmsg", fn_name="trace_tcp_send") # 读取 eBPF Map 数据(用户态内核态交互) counter_map = bpf.get_table("counter") while True: for k, v in counter_map.items(): print(f"PID {k.value}: {v.value} packets") counter_map.clear() time.sleep(1) ``` #### 3. **关键组件说明** - **eBPF Map**:用户态和内核态通过 Map 交互数据(引用[^2]) - **程序附加**:支持 `kprobe`/`tracepoint`/`XDP` 等多种挂载点 - **字节码验证**:加载时内核会自动验证字节码安全性 #### 4. **优化建议** 1. **使用 CO-RE(Compile Once Run Everywhere)** 通过 `BTF` 解决内核版本兼容性问题(引用[^1]): ```bash clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I/usr/include -c ebpf.c -o ebpf.o ``` 2. **减少依赖** 静态链接 libbpf 避免 LLVM 依赖(引用[^1]) #### 5. **Android 平台注意事项** - 需内核开启 `CONFIG_DEBUG_INFO_BTF=y`(引用[^3]) - 使用 `libbpf` 替代 BCC 以降低资源消耗(引用[^3]) > **提示**:完整示例见 [BCC 官方文档](https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md) --- ### 相关问题 1. **如何验证 eBPF 字节码在内核中的安全性?** [涉及字节码验证器工作原理和安全限制] 2. **eBPF Map 有哪些类型?如何选择适合的 Map 类型?** [对比 Hash/Array/PerfRB/StackTrace 等 Map 类型] 3. **在 Android 上部署 eBPF 程序有哪些特殊限制?** [分析 Android 内核配置和 SELinux 策略影响] 4. **如何实现 eBPF 程序的热更新?** [讨论原子替换和 Map 数据保留技术] 5. **eBPF 传统内核模块的性能对比有何差异?** [对比加载效率、安全性和执行开销] [^1]: 开发、构建和分发 eBPF 一直以来都是一个高门槛的工作...对于 eBPF 程序的分发、管理也没有很好地解决方案 [^2]: 内核程序如何用户的程序进行程序交互呢?其实eBPF专门有一套Maps机制... [^3]: android平台eBPF初探...BCC在android系统上也可以运行,但是要对系统进行一定程度的修改...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值