Linux下一代防火墙bpfilter是什么?让我演示给你看

昨晚为了解决公司的一个bug熬了个夜,等待期间花了半小时撸了一个bpfilter的简易POC,今早发了个朋友圈:

且看这个链接:https://lwn.net/Articles/747504

这个POC还是复杂了,而且标准发行版里根本就不可用,ko模块不可执行,依然是标准模块。
原理就这么简单,想证明可行性大可不必折腾大场面搞什么umh的(作者的意图显然是站技术实现的立场的,而我从业务角度考虑如何快速部署),昨晚花了半小时几条命令,四十来行代码就能说明问题。

xdp的ebpf只能作为nf的cache存在,无状态,功能有限,全靠map关联状态,且关联到interface,创建完备的有状态流表还是需要慢速路径,完全替代nf当前是不可能的。其实Cisco从早期开始就用这种方式支持ACL了,都二十多了。

我的POC也仅仅支持无状态五元组匹配。

我想关于Linux bpfilter的中文描述,我又是第一人了。


bpfilter是什么?几乎没有什么资料介绍,这里有必要简述一番。

我们知道,Linux防火墙框架经历了ip firewall,ipchains,iptanles,nftables,越发完美,但是性能一直都是问题,毕竟不管任何框架,防火墙规则都是以某种 列表 的形式存在的,内核并没有对列表进行任何预处理,在内核协议栈的处理经路上顺序callback一堆list回调函数,这是问题的根源。

更加可恶的是,iptables规则不支持增量配置,因此无论是规则的增删改查,其时间复杂度均为 O ( n ) O(n) ,看到这个 O ( n ) O(n) ,程序员就怒了。

虽然nftables让规则list更加紧凑,但是执行逻辑依然繁琐,这个时候,ebpf来了。

我之所以对ebpf情有独钟,并不是因为我精通这个,我喜欢ebpf完全是因为早在2004年的时候,我就听说 “可以将Cisco/H3C的ACL规则‘编译’到接口” ,Cisco路由器交换机对ACL规则进行一定的预处理优化,编译加载到具体的网卡,这个思路很先进了。然后我发现ebpf也可以将ACL规则编译到XDP,这完全是情怀使然,与技术无关。

如今,Linux上iproute2套装也可以完成eBPF程序载入网卡了,这个iproute2套装我一用就是8年!我非常喜欢它的与时俱进!

ebpf程序会被编译成中间字节码,然后被JIT编译成智能网卡可以执行的指令序列(就像被JIT编译成x86_64指令一般)并且载入智能网卡的ram,成为其XDP的可执行程序,这就是其智能之所在。

大致的原理就是这样,但是原理说再多没个鸟用,问题的关键在我们该如何做才能玩起来。


链接 https://lwn.net/Articles/747504 所示的POC依赖一种叫做umh的机制,说白了就是个trick,该trick旨在解决以下的矛盾:

  • iptables规则到JIT码的转换确实内核来负责。
  • JIT编译过程非常复杂且策略化,不适合在内核完成。

于是就出现了umh。然而直到Ubuntu 19.10(内核版本5.3.0-23-generic),bpfilter依然处于 不可用 的状态,并且构建链接所述的POC执行环境非常繁琐。

即然如此,为什么不自己DIY一个呢?

让我们开始。


我的POC很简单,它完成下面的功能:

  • 将用户配置的iptales规则实时翻译成ebpf字节码注入到某网卡的XDP(通用的,或者硬件offload的)。

为了完成这个,必须有一种机制可以监控iptables规则的变化,我找到了xtables-monitor程序,它事实上和iptables一样,也是xtables-multi的一个链接:

root@zhaoya-VirtualBox:/home/zhaoya# ls /usr/local/sbin/xtables-monitor -l
lrwxrwxrwx 1 root root 17 11月 27 11:31 /usr/local/sbin/xtables-monitor -> xtables-nft-multi

它可以监控规则的变化。不过这里有个问题,xtables-monitor只对iptables-nft兼容命令有效,无法监控传统iptables命令修改的规则,而bpfilter的目标之一就是兼容传统iptables工具套件的二进制ABI。不过这些对于做一个POC而言,问题不大。如果真的要做的话,就要对Linux内核本身做fix了,无非也就是添加几行代码的事儿。

接下来的任务是,我们要把xtables-monitor监控到的变化注入到一个程序中,该程序将这个变化反应到对应网卡的XDP eBPF程序上去。

这并不难,下面我用执果溯因的思路,一步步将代码贴出来,请看我们需要的结果:

root@zhaoya-VirtualBox:/samples/bpf# xtables-monitor -e |xargs -i ./bpf_filter {} /sys/fs/bpf/xdp/globals/action_map

命令无需解释,唯一要说的就是iptables套件对stdout有需求,所以必须将iptables源码树中所有的打印规则的函数printf换成dprintf到stdout。

我们现在需要看看bpf_filter程序的代码:

// bpf_filter_user.c
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <bpf/bpf.h>
#include <linux/bpf.h>
#include "bpf_util.h"

#define SIZE	32

static int action_map_fd;

int main(int argc, char **argv)
{
	int o, i = 0, opt = 0, action;
	in_addr_t addr, *paddr;
	char buff[64], *token;
	char *optstring[32];
   	const char s[2] = " ";
	char *mapfile;

	// 传入的正是xtables-monitor捕获的规则集的变化
	optstring[i++] = calloc(1, SIZE);
	strcpy(optstring[0], "EVENT:");
	sprintf(buff, "%s\n", strstr(argv[1], optstring[0]));

	mapfile = argv[2];

	token = strtok(buff, s);
	token = strtok(NULL, s);

	while (token != NULL) {
		optstring[i] = malloc(SIZE);
		strcpy(optstring[i++], token);
		token = strtok(NULL, s);
	}
	optstring[i] = NULL;

	while((o = getopt(i, optstring, "4t:A:D:s:j:")) != EOF) {
		switch (o) {
		case 'A':
			 opt = 1; break;
		case 'D':
			 opt = 0; break;
		case 's': {
			 char *raw_addr = strtok(optarg, "/");
			 addr = inet_addr(raw_addr);
			 paddr = &addr;
			 break;
			  }
		case 'j':
			 if (!strncmp(optarg, "DROP", 4)) {
			 	action = 1;
			 } else if (!strncmp(optarg, "ACCEPT", 6)) {
			 	action = 0;
			 }
			 break;
		default: break;
		}
	}

	// 这是一个PIN住的全局map,我们打开它。
	action_map_fd = bpf_obj_get(mapfile);
	// 增加或者删除规则,只要体现在XDP的map上
	if (opt == 0) {
		bpf_map_delete_elem(action_map_fd, (const void *)paddr);
		printf("delete source IP:%x\n", addr);
	} else if (opt == 1) {
		bpf_map_update_elem(action_map_fd, (const void *)&addr, (const unsigned int *)&action, 0);
		printf("add/update source IP:%x\n", addr);
	}
	return 0;
}

还差一个代码,即eBPF代码本身了,现在就给出:

// bpf_filter_kern.c
#include <uapi/linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <uapi/linux/bpf.h>
#include "bpf_helpers.h"

#define PIN_GLOBAL_NS		2
struct bpf_elf_map {
	__u32 type;
	__u32 size_key;
	__u32 size_value;
	__u32 max_elem;
	__u32 flags;
	__u32 id;
	__u32 pinning;
};

// 系统范围内的全局map
struct bpf_elf_map SEC("maps") action_map = {
	.type = BPF_MAP_TYPE_HASH,
	.size_key = sizeof(int),
	.size_value = sizeof(int),
	.pinning        = PIN_GLOBAL_NS,
	.max_elem = 100,
};

static inline int parse_ipv4(void *data, u64 nh_off, void *data_end,
			     __be32 *src, __be32 *dest)
{
	struct iphdr *iph = data + nh_off;

	if (iph + 1 > data_end)
		return 0;
	*src = iph->saddr;
	*dest = iph->daddr;
	return iph->protocol;
}

// 默认策略为ACCEPT的处理逻辑本身
SEC("xdp_action") // 注意iproute2的section字段
int xdp_drop_prog(struct xdp_md *ctx)
{
	void *data_end = (void *)(long)ctx->data_end;
	void *data = (void *)(long)ctx->data;
	int *action_entry = NULL;
	int in_index = ctx->ingress_ifindex, *out_index;
	__be32 src_ip = 0, dest_ip = 0;
	struct ethhdr *eth = (struct ethhdr *)data;
	u16 h_proto;
	u64 nh_off;
	u32 ipproto;

	nh_off = sizeof(*eth);
	if (data + nh_off > data_end) {
		return XDP_DROP;
	}

	h_proto = eth->h_proto;
	if (h_proto != htons(ETH_P_IP))
		return XDP_PASS;
	ipproto = parse_ipv4(data, nh_off, data_end, &src_ip, &dest_ip);
	action_entry = bpf_map_lookup_elem(&action_map, &src_ip);
	if (action_entry) {
		if (*action_entry == 0)
			return XDP_PASS;
		else if (*action_entry == 1)
			return XDP_DROP;
	}
	// Default policy PASS
	return  XDP_PASS;
}

char _license[] SEC("license") = "GPL";

把上面的eBPF程序编译成bpf_filter_kern.o的时候,代码工作就全部结束了。

是时候用一下它了。

我使用iproute2来将这个eBPF的字节码注入网卡enp0s8的XDP上,这已经非常类似Cisco处理ACL的做法了:

root@zhaoya-VirtualBox:~# ip --force link set dev enp0s8 xdp obj bpf_filter_kern.o sec xdp_action
# 上述命令如果需要将规则处理offload到硬件(前提是硬件要支持,比如Netronome NFP SmartNIC),就要用xdpoffload选项(而不是xdp)了

当以上命令执行完毕之后,效果就是在enp0s8网卡上多了XDP处理:

root@zhaoya-VirtualBox:~# ip link ls dev enp0s8
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:ce:25:7e brd ff:ff:ff:ff:ff:ff
    prog/xdp id 13 tag e177e2c4d4c4914d jited

我们可以通过bpftool一窥究竟:

root@zhaoya-VirtualBox:~# bpftool p|tail -3
13: xdp  tag e177e2c4d4c4914d  gpl
	loaded_at 2019-11-27T21:20:53+0800  uid 0
	xlated 264B  jited 166B  memlock 4096B  map_ids 12
root@zhaoya-VirtualBox:~# bpftool map dump id 12
Found 0 elements

为了确认全局的PIN map已经生成,我们确认下面的文件已经建立:

root@zhaoya-VirtualBox:~# ll  /sys/fs/bpf/xdp/globals/action_map
-rw------- 1 root root 0 11月 27 18:39 /sys/fs/bpf/xdp/globals/action_map

是时候测试一下效果了。

搭建以下的拓扑:
在这里插入图片描述
确认56.110能ping通被测试机器56.101。

此时在56.101上执行下面的命令:

xtables-monitor -e |xargs -i ./bpf_filter {} /sys/fs/bpf/xdp/globals/action_map

我们添加一条iptables规则:

iptables-nft -A INPUT -s 192.168.56.110 -j DROP

可以发现,ping不通了,此时为了确认确实是XDP DROP生效了而不是iptales规则本身生效了,我们可以使用tcpdump确认没有抓到任何数据包,同时bpftool显示,XDP的map确实有表项生成:

root@zhaoya-VirtualBox:~# bpftool map dump id 12
key: c0 a8 38 6e  value: 01 00 00 00
Found 1 element

这就是被禁止的192.168.56.110的DROP策略,同时,我们可以看到注入到enp0s8网卡的eBPF代码:

root@zhaoya-VirtualBox:~# bpftool p |tail -3
13: xdp  tag e177e2c4d4c4914d  gpl
	loaded_at 2019-11-27T21:20:53+0800  uid 0
	xlated 264B  jited 166B  memlock 4096B  map_ids 12
root@zhaoya-VirtualBox:~# bpftool p d x i 13
   0: (79) r2 = *(u64 *)(r1 +8)
   1: (79) r1 = *(u64 *)(r1 +0)
   2: (b7) r3 = 0
   3: (63) *(u32 *)(r10 -4) = r3
   4: (b7) r6 = 1
   5: (bf) r3 = r1
   6: (07) r3 += 14
   7: (2d) if r3 > r2 goto pc+23
   8: (71) r3 = *(u8 *)(r1 +12)
   9: (71) r4 = *(u8 *)(r1 +13)
  10: (67) r4 <<= 8
  11: (4f) r4 |= r3
  12: (b7) r6 = 2
  13: (55) if r4 != 0x8 goto pc+17
  14: (bf) r3 = r1
  15: (07) r3 += 34
  16: (2d) if r3 > r2 goto pc+2
  17: (61) r1 = *(u32 *)(r1 +26)
  18: (63) *(u32 *)(r10 -4) = r1
  19: (bf) r2 = r10
  20: (07) r2 += -4
  21: (18) r1 = map[id:12]
  23: (85) call __htab_map_lookup_elem#104208
  24: (15) if r0 == 0x0 goto pc+1
  25: (07) r0 += 56
  26: (15) if r0 == 0x0 goto pc+4
  27: (61) r1 = *(u32 *)(r0 +0)
  28: (b7) r6 = 1
  29: (15) if r1 == 0x1 goto pc+1
  30: (b7) r6 = 2
  31: (bf) r0 = r6
  32: (95) exit

接下来,让我们删除iptables规则:

iptables-nft -D INPUT -s 192.168.56.110 -j DROP

map重新归为空:

root@zhaoya-VirtualBox:~# bpftool map dump id 12
Found 0 elements

两台机器继续相互可以ping通。所以说,XDP的eBPF规则完美生效。时隔很多年,Linux支持了XDP/eBPF之后,也终于可以像Cisco/H3C那般自我了。

这就是我的POC了。其实我在代码中已经留下了 “接口” ,比如getopt那块,我已经按照规范指引了正轨的代码应该怎么写,幸运的是,iptables规则本身就是argv格式的,确实不错。


浙江温州皮鞋湿,下雨进水不会胖。

发布了1545 篇原创文章 · 获赞 4728 · 访问量 1055万+

猜你喜欢

转载自blog.csdn.net/dog250/article/details/103283981