Loading

ebpf运行流程以及Demo编写

发展历程

  • linux 2.1.75 -- 初次引入bpf, 只能进行网络包过滤
  • linux 3.0 -- 更换bpf解释器, 提升效率, 依旧只能进行网络包过滤
  • linux 4.x -- 变成ebpf, 一个通用的虚拟机, 可以做更多事情, 比如内核态函数, 用户态函数, 跟踪点, 性能事件, 安全控制等.

适用场景

运行方式

何时运行

ebpf代码需要事件触发才能执行, 比如系统调用, 内核函数调用, 退出, 网络事件等. ebpf通过强大的内核态插桩和用户态插桩, 来达到几乎可以在任意位置进行插桩的效果.

运行流程

通常的, 我们需要借助LLVM来将我们编写的eBPF程序转化为bpf字节码, 然后通过bpf系统调用提交给内核执行, 而内核在接受BPF字节码之前会使用验证器对字节码进行校验, 只有通过校验的字节码才会提交给编译器执行. 因此, 你不必担心eBPF程序会将你的系统变得不稳定.
如果BPF字节码中有不安全的操作, 则会拒绝执行, 验证器有自己的判断逻辑, 比如:

  • 只有特权进程才可以执行bpf系统调用
  • BPF程序不能有无限循环
  • BPF程序不能导致内核崩溃
  • BPF程序必须在有限时间内完成
    当通过校验后, 会将ebpf代码编译成机器码, 然后挂载执行.
    这整个流程类似一个虚拟机, 有自己的校验系统和存储系统.

数据读取

BPF程序可以利用BPF映射(map)进行存储数据, 同时, 用户的程序也需要通过map来与运行在内核中的BPF程序进行交互.来达到读取和传输数据的目的.

限制

eBPF并不是万能的, 他也有一些限制, 比如:

  • eBPF程序必须被校验器校验通过后才可以执行, 并且不能包含无法到达的执行
  • eBPF程序不能随意的调用内核函数, 只能调用在API中定义的辅助函数
  • eBPF程序的栈空间只有512字节, 如果需要更大的存储, 必须借助map存储
  • 在内核5.2前, eBPF最多只支持4096条指令, 5.2之后是500万条.
  • 因为内核的快速变化, 在不同版本的内核中运行同一个eBPF程序可能不兼容, 需要调整源码并重新编译.
  • 要想稳定运行eBPF, linux内核至少需要4.9或以上, 最好是5.X或者更新.

交互方式

完整的ebpf程序分为两个部分, 用户态代码和内核态代码, 内核态代码是直接load进内核的代码, 而用户态部分控制内核态代码的load, 和与ebpf生成的map进行交互以便获取数据.

系统交互(用户态)

ebpf的操作, 必须依靠和系统进行交互才可以, 向系统发出操作指令, 才可以进行下去, 操作指令随着linux内核升级, 也越来越丰富, 例如, 5.13版本的操作指令已经有36个 bpf.h - include/uapi/linux/bpf.h - Linux source code (v5.13) - Bootlin
其中, 分为几大类:

  • 创建map映射
  • 对map映射进行操作:cru
  • 验证并加载BPF程序
  • 将BPF程序在内核事件上 挂载/卸载
  • 将BPF程序储存在 /sys/fs/bpf 中做持久化
  • 从 /sys/fs/bpf 中查找BPF程序
  • 验证和加载BTF信息

辅助函数(内核态)

为了安全, ebpf程序并不能随意的调用内核函数, 内核定义了一系列的辅助函数, 我们只能使用辅助函数来让ebpf程序与内核的其他模块进行交互
从内核5.13开始, ebpf也逐步支持某些系统函数直接调用, 但是有严格的要求, 这里不展开说.
并且, 不同类型的ebpf程序支持的辅助函数也是不同的,这个后面会讲.
辅助函数分为以下几大类:

  • 写入调试信息
  • 对map映射进行操作:ru
  • 从内存(用户空间和内核空间)指针中读取数据
  • 从内存(用户空间和内核空间)指针中读取字符串
  • 获取系统启动到现在的时长
  • 获取当前线程信息(ID/名称/数据结构)
  • 写入数据
  • 获取堆栈信息
    辅助函数不能对map进行创建, 只有用户态可以创建

操作map映射

BPF映射提供了大块的键值存储, 可以被用户态访问, 从而获取ebpf程序的运行状态和数据, ebpf程序最多可以访问64个不同的映射, 并且多个ebpf程序可以通过同一个map映射来共享状态和信息.
map映射可以通过用户态进行创建, 但是无法删除, map映射会在对应的ebpf退出时自动删除, 如果想要持久化, 可以通过用户态指令将数据保存

BTF

在编写ebpf代码的时候, 需要引入一些内核的数据类型(头文件), 还需要手动安装头文件, 这会导致一些问题, 比如:

  • 不同内核版本的路径和数据结构不同
  • 需要引入一堆头文件
  • 生产环境的机器可能不允许安装头文件(安全考虑)
    从linux内核5.2开始, 在编译内核时, 就会把内核的数据结构自动的镶嵌到 vmlinux 中, 还可以借助命令, 将这个定义导出到一个头文件中
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

这样, 我们只需要引入这一个头文件即可.
另外, 为了解决多个linux内核的数据结构不同的问题, eBPF有着CO-RE项目, 会针对不同版本进行适配.
需要注意的是, BTF技术只在linux内核5.2才出现.

事件触发

epbf程序类型

eBPF 程序类型决定了一个 eBPF 程序可以挂载的事件类型和事件参数,这也就意味着,内核中不同事件会触发不同类型的 eBPF 程序.Linux 内核 v5.13 已经支持 30 种不同类型的 eBPF 程序
bpf.h - include/uapi/linux/bpf.h - Linux source code (v5.13) - Bootlin
ebpf程序按照功能可以大致分为三种:

  • 跟踪: 从内核和程序的运行状态中提取跟踪信息, 来了解当前系统正在发生什么.
  • 网络: 对网络数据包进行过滤和处理, 监控和控制网络数据包收发.
  • 其他: 安全控制/BPF扩展等

demo

这里是一个demo, 作用是打印执行 tcp connect 的数据包

main.go

// This program demonstrates attaching a fentry eBPF program to
// tcp_connect. It prints the command/IPs/ports information
// once the host sent a TCP SYN packet to a destination.
// It supports IPv4 at this example.
//
// Sample output:
//
// examples# go run -exec sudo ./fentry
// 2021/11/06 17:51:15 Comm   Src addr      Port   -> Dest addr        Port
// 2021/11/06 17:51:25 wget   10.0.2.15     49850  -> 142.250.72.228   443
// 2021/11/06 17:51:46 ssh    10.0.2.15     58854  -> 10.0.2.1         22
// 2021/11/06 18:13:15 curl   10.0.2.15     54268  -> 104.21.1.217     80


package main

import (
    "bytes"
    "encoding/binary"
    "errors"
    "log"
    "net"
    "os"
    "os/signal"
    "syscall"

    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/ringbuf"
    "github.com/cilium/ebpf/rlimit"
)

// $BPF_CLANG and $BPF_CFLAGS are set by the Makefile.
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -type event bpf demo.c -- -I../headers

func main() {
    stopper := make(chan os.Signal, 1)
    signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)

    // Allow the current process to lock memory for eBPF resources.
    // 解除内存锁, 防止老版本内核有内存限制
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    // Load pre-compiled programs and maps into the kernel.
    // 将bpf内核态代码加载到内核
    objs := bpfObjects{}
    if err := loadBpfObjects(&objs, nil); err != nil {
        log.Fatalf("loading objects: %v", err)
    }
    defer objs.Close()

    // 设置BTF类型的钩子
    link, err := link.AttachTracing(link.TracingOptions{
        Program: objs.bpfPrograms.TcpConnect,
    })
    if err != nil {
        log.Fatal(err)
    }
    defer link.Close()

    // 获取map数据读取游标
    rd, err := ringbuf.NewReader(objs.bpfMaps.Events)
    if err != nil {
        log.Fatalf("opening ringbuf reader: %s", err)
    }
    defer rd.Close()

    go func() {
        <-stopper

        if err := rd.Close(); err != nil {
            log.Fatalf("closing ringbuf reader: %s", err)
        }
    }()

    log.Printf("%-16s %-15s %-6s -> %-15s %-6s",
        "Comm",
        "Src addr",
        "Port",
        "Dest addr",
        "Port",
    )

    // bpfEvent is generated by bpf2go.
    var event bpfEvent
    for {
        // 读取数据
        record, err := rd.Read()
        if err != nil {
            if errors.Is(err, ringbuf.ErrClosed) {
                log.Println("received signal, exiting..")
                return
            }
            log.Printf("reading from reader: %s", err)
            continue
        }

        // Parse the ringbuf event entry into a bpfEvent structure.
        if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.BigEndian, &event); err != nil {
            log.Printf("parsing ringbuf event: %s", err)
            continue
        }

        log.Printf("%-16s %-15s %-6d -> %-15s %-6d",
            event.Comm,
            intToIP(event.Saddr),
            event.Sport,
            intToIP(event.Daddr),
            event.Dport,
        )
    }
}

// intToIP converts IPv4 number to net.IP
func intToIP(ipNum uint32) net.IP {
    ip := make(net.IP, 4)
    binary.BigEndian.PutUint32(ip, ipNum)
    return ip
}

demo.c

//go:build ignore
// 标识 go build 时忽略本文件


#include "common.h"

#include "bpf_endian.h"
#include "bpf_tracing.h"

#define AF_INET 2
#define TASK_COMM_LEN 16

char __license[] SEC("license") = "GPL";  // 此ebpf开源协议是GPL

/**
 * This example copies parts of struct sock_common and struct sock from
 * the Linux kernel, but doesn't cause any CO-RE information to be emitted
 * into the ELF object. This requires the struct layout (up until the fields
 * that are being accessed) to match the kernel's, and the example will break
 * or misbehave when this is no longer the case.
 *
 * Also note that BTF-enabled programs like fentry, fexit, fmod_ret, tp_btf,
 * lsm, etc. declared using the BPF_PROG macro can read kernel memory without
 * needing to call bpf_probe_read*().
 */

/**
 * struct sock_common reflects the start of the kernel's struct sock_common.
 * It only contains the fields up until skc_family that are accessed in the
 * program, with padding to match the kernel's declaration.
 */

// 网络层的最小表示 sock_common 结构
struct sock_common {
    union {
        struct {
            __be32 skc_daddr;
            __be32 skc_rcv_saddr;
        };
    };
    union {
        // Padding out union skc_hash.
        __u32 _;
    };
    union {
        struct {
            __be16 skc_dport;
            __u16 skc_num;
        };
    };
    short unsigned int skc_family;
};

/**
 * struct sock reflects the start of the kernel's struct sock.
 */

// socket结构
struct sock {
    struct sock_common __sk_common;
};

// map内的数据结构
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF); 
    __uint(max_entries, 1 << 24);
} events SEC(".maps");

/**
 * The sample submitted to userspace over a ring buffer.
 * Emit struct event's type info into the ELF's BTF so bpf2go
 * can generate a Go type from it.
 */

// 环形数据
struct event {
    u8 comm[16];
    __u16 sport;
    __be16 dport;
    __be32 saddr;
    __be32 daddr;
};
struct event *unused __attribute__((unused));

// hook到tcp_connect事件
SEC("fentry/tcp_connect")
int BPF_PROG(tcp_connect, struct sock *sk) {
    // AF_INET代表是IPV4
    // AP_INET6是IPV6
    // 不是IPV4则退出
    if (sk->__sk_common.skc_family != AF_INET) {
        return 0;
    }

    // 将数据存储到tcp_info
    struct event *tcp_info;
    // bpf_ringbuf_reserve 将数据直接读取, 防止复制导致内存资源损耗
    tcp_info = bpf_ringbuf_reserve(&events, sizeof(struct event), 0);
    if (!tcp_info) {
        return 0;
    }

    // 数据转换
    tcp_info->saddr = sk->__sk_common.skc_rcv_saddr;
    tcp_info->daddr = sk->__sk_common.skc_daddr;
    tcp_info->dport = sk->__sk_common.skc_dport;
    tcp_info->sport = bpf_htons(sk->__sk_common.skc_num);

    // 获取当前调用的进程
    bpf_get_current_comm(&tcp_info->comm, TASK_COMM_LEN);

    bpf_ringbuf_submit(tcp_info, 0);

    return 0;
}

执行

确保上一级同级目录下有headers文件 https://github.com/cilium/ebpf/tree/master/examples

export BPF_CLANG=clang
go generate
go run .

需要一个main.go和一个c文件
export BPF_CLANG=clang
go generate
生产代码
go run .

这个错误提示是因为没有挂载 debugfs 或 tracefs 文件系统。这两个文件系统是内核提供的用于调试的文件系统,其中 debugfs 用于内核调试,tracefs 用于跟踪。如果你想要使用 tracepoint,需要先挂载 tracefs 文件系统。你可以使用以下命令挂载 tracefs 文件系统:sudo mount -t tracefs nodev /sys/kernel/debug/tracing¹。

eBPF 和 Go 入门 |网络操作 (networkop.co.uk)

posted @ 2023-04-03 19:38  ChnMig  阅读(956)  评论(0编辑  收藏  举报