ebpf 研究之原始套接字数据包过滤

7 篇文章 5 订阅

原始套接字数据包过滤

ebpf 支持原始套接字过滤功能,本文参考 《Linux 内核观测技术 BPF》第 6 章的示例进行描述,并深挖隐藏在 epbf 程序背后的一些技术细节。

ebpf 程序示例代码

bfp_program 源码如下:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/string.h>
#include <linux/tcp.h>
#include <linux/types.h>
#include <linux/udp.h>

#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t) & ((TYPE *)0)->MEMBER)
#endif

#define SEC(NAME) __attribute__((section(NAME), used)) 

struct bpf_map_def {
  unsigned int type;
  unsigned int key_size;
  unsigned int value_size;
  unsigned int max_entries;
  unsigned int map_flags;
};

static int (*bpf_map_update_elem)(struct bpf_map_def *map, void *key,
                                  void *value, __u64 flags) = (void *)
    BPF_FUNC_map_update_elem;
static void *(*bpf_map_lookup_elem)(struct bpf_map_def *map, void *key) =
    (void *)BPF_FUNC_map_lookup_elem;

unsigned long long load_byte(void *skb,
                             unsigned long long off) asm("llvm.bpf.load.byte");

struct bpf_map_def SEC("maps") countmap = {
    .type = BPF_MAP_TYPE_ARRAY,
    .key_size = sizeof(int),
    .value_size = sizeof(int),
    .max_entries = 256,
};

SEC("socket")
int socket_prog(struct __sk_buff *skb) {
  int proto = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
  int one = 1;
  int *el = bpf_map_lookup_elem(&countmap, &proto);
  if (el) {
    (*el)++;
  } else {
    el = &one;
  }
  bpf_map_update_elem(&countmap, &proto, el, BPF_ANY);
  return 0;
}

上述程序中,SEC(“socket”) 标识了原始套接字过滤类型,这一类型在加载 bpf 程序的时候会被使用以设定 prog_typeBPF_PROG_TYPE_SOCKET_FILTER 类型。

socket_prog 是 bpf 程序的核心逻辑,它首先加载报文 ip 头部中的协议字段,根据协议字段的值在 bpf map 中检索,已经存在时,就对旧值加 1 并更新,不存在则赋初值为 1,然后在 bpf_map_update_elem 中建立新的项目。

ebpf 程序自定义加载代码

使用如下程序加载上面的 bpf 程序并附加到一个网络接口中,根据我的网络接口情况,我修改了原书代码,修改后的代码如下:

#include <arpa/inet.h>
#include <assert.h>
#include <bpf/bpf.h>
#include <bpf/bpf_load.h>
#include <bpf/sock_example.h>
#include <errno.h>
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

char bpf_log_buf[BPF_LOG_BUF_SIZE];

int main(int argc, char **argv) {
  int sock = -1, i, key;
  int tcp_cnt, udp_cnt, icmp_cnt;

  char filename[256];
  snprintf(filename, sizeof(filename), "%s", argv[1]);

  if (load_bpf_file(filename)) {
    printf("%s", bpf_log_buf);
    return 1;
  }

  sock = open_raw_sock("wlp0s20f3");

  if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, prog_fd,
                 sizeof(prog_fd[0]))) {
    printf("setsockopt %s\n", strerror(errno));
    return 0;
  }

  for (i = 0; i < 1000; i++) {
    key = IPPROTO_TCP;
    assert(bpf_map_lookup_elem(map_fd[0], &key, &tcp_cnt) == 0);

    key = IPPROTO_UDP;
    assert(bpf_map_lookup_elem(map_fd[0], &key, &udp_cnt) == 0);

    key = IPPROTO_ICMP;
    assert(bpf_map_lookup_elem(map_fd[0], &key, &icmp_cnt) == 0);

    printf("TCP %d UDP %d ICMP %d packets\n", tcp_cnt, udp_cnt, icmp_cnt);
    sleep(1);
  }
}

上述程序的关键步骤如下:

  1. 使用 load_bpf_file 函数加载 ebpf 程序
  2. 创建一个指定接口的原始套接字
  3. 将加载后的 bpf 程序附着到原始套接字上
  4. 轮询获取 bpf map 中不同协议类型的统计值并打印

libbpf.so 编译问题

上面加载 ebpf 程序的代码需要使用 libbpf.so 中的接口,libbpf.so 源代码位于内核源码目录中,编译 libbpf.so 时遇到了如下报错:

$ make 

Auto-detecting system features:
...                        libelf: [ OFF ]
...                           bpf: [ OFF ]

参考 samples: bpf: fix build after move to full libbpf 在内核源码树的 tools 目录中先执行一次 clean,然后再编译 libbpf.so,能够解决这个问题。

操作记录如下:

$ cd tools/
$ make clean
$ cd lib/bpf/
$ make
$ make 

Auto-detecting system features:
...                        libelf: [ on  ]
...                           bpf: [ on  ]

  HOSTCC   fixdep.o
  HOSTLD   fixdep-in.o
  LINK     fixdep
  CC       libbpf.o
  CC       bpf.o
  CC       nlattr.o
  CC       btf.o
  CC       libbpf_errno.o
  CC       str_error.o
  CC       netlink.o
  CC       bpf_prog_linfo.o
  LD       libbpf-in.o
  LINK     libbpf.a
  LINK     libbpf.so
  LINK     test_libbpf

测试记录

执行 loader 程序加载 ebpf 程序后,测试得到如下输出信息:

TCP 1048 UDP 148 ICMP 34 packets
TCP 1048 UDP 152 ICMP 34 packets
TCP 1249 UDP 156 ICMP 34 packets
TCP 1271 UDP 156 ICMP 34 packets

icmp 报文的统计数据是准确的,符合预期。

loader 程序背后的系统调用

使用 strace 跟踪 ebpf 程序加载的过程,得到了如下关键信息:

bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_ARRAY, key_size=4, value_size=4, max_entries=256, map_flags=0, inner_map_fd=0, map_name="countmap", map_ifindex=0}, 112) = 4
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_SOCKET_FILTER, insn_cnt=25, insns=0xfd1d80, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(0, 0, 0), prog_flags=0, prog_name="", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 112) = 5
close(3)                                = 0
socket(AF_PACKET, SOCK_RAW|SOCK_CLOEXEC|SOCK_NONBLOCK, 
..........
bind(3, {sa_family=AF_PACKET, sll_protocol=htons(ETH_P_ALL), sll_ifindex=if_nametoindex("wlp0s20f3"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_BPF, [5], 4) = 0
bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=4, key=0x7ffc7298e314, value=0x7ffc7298e310}, 112) = 0
bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=4, key=0x7ffc7298e314, value=0x7ffc7298e30c}, 112) = 0
bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=4, key=0x7ffc7298e314, value=0x7ffc7298e308}, 112) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0
write(1, "TCP 0 UDP 0 ICMP 0 packets\n", 27TCP 0 UDP 0 ICMP 0 packets

loader 程序使用 bpf 系统调用与内核中的 ebpf 虚拟机进行交互,涉及 ebpf map 数据的创建与查询,ebpf 程序的加载等功能。

bpf_program ebpf 程序的加载由如下两次系统调用完成:

bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_ARRAY, key_size=4, value_size=4, max_entries=256, map_flags=0, inner_map_fd=0, map_name="countmap", map_ifindex=0}, 112) = 4
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_SOCKET_FILTER, insn_cnt=25, insns=0xfd1d80, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(0, 0, 0), prog_flags=0, prog_name="", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 112) = 5

从这个结果中可以看出,ebpf 程序中 map 的创建与 ebpf 程序代码的加载是区分开的,这一过程并不为用户可见,实际上这部分工作是在 samples/bpf/bpf_load.c 中悄悄完成的,其中涉及到对 elf 格式的解析,而 SEC(“maps”)、 SEC(“socket”)
正是这一机关中的重点内容,看看下面贴出的 SEC 定义内容不难明白其中的机关。

#define SEC(NAME) __attribute__((section(NAME), used))

maps、socket 放在两个不同的 section 中,根据 section 的名字就能够找到具体的内容,解析并加载之即可

另外一个非常重要的点就是上面两个 bpf 系统调用的返回值,创建 countmap 映射返回的值为 4,加载 socket ebpf 代码返回的值为 5。

这两个返回值是文件描述符,分别在下面这两个系统调用中被使用:

bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=4, key=0x7ffc7298e314, value=0x7ffc7298e310}, 112) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_BPF, [5], 4) = 0

bpf 系统调用按 key 查询 ebpf map 值map_fd=4 标识了使用的 ebpf map。setsocketopt 系统调用完成 ebpf 程序附着到套接字上的任务,它的第四个参数传递 socket ebpf 代码的文件描述符 5

在这时候,回到 loader 代码中,与上面两个系统调用相关的代码如下:

 if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, prog_fd,
                 sizeof(prog_fd[0])))
 ........
 bpf_map_lookup_elem(map_fd[0], &key, &tcp_cnt)

prog_fd 与 map_fd 都是数组,这两个数组的定义与赋值都隐藏在内核源码树中 samples/bpf/bpf_load.c 源文件中。

总结

本文使用一个示例描述了 ebpf 中原始套接字数据包过滤的功能,在一步步描述中,程序背后的一些机关浮上了水面。

通过研究将问题进一步推动到 bpf_load.c 源文件中,这个源文件正是 ebpf 程序加载过程中的一个黑盒,未来要进一步的研究。

写到这里我想到了下面几个问题:

  1. 内核中 ebpf 附着到 socket 的具体过程是怎样的?
  2. 网卡接收到报文后如何与 ebpf 程序建立关联?
  3. ebpf 程序如何作用到网卡接收到的每一个报文中?

希望伴随着我对 ebpf 技术的进一步深入,我能够给出这些问题的答案!

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值