Linux 网络:cBPF 简介

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 什么是 cBPF?

BPF, 是 Berkeley Packet Filter 的缩写。早期的 BPF,是用来过滤网络包BPF 虚拟机指令程序。BPF 发展到今天,其功能和灵活性已经被大大扩展增强,包括了 cBPF(classic Berkeley Packet Filter)eBPF(extended Berkeley Packet Filter)。本文讲述传统的 cBPF

2.1 cBPF 工作原理

cBPF 的工作原理很简单,编写一段 BPF 指令,用来判断给定的网络数据包是否符合过滤条件:如果符合过滤条件,则接收不接收该数据包。换句话说,就是给 BPF 指令输入一个网络数据包,该段指令返回 0(表示拒绝数据包) 或 非 0 值(表示接收数据包)。当然,过滤指令BPF 虚拟机类型的,所以还要有一个 BPF 指令解释器将 BPF 指令翻译成本地指令(如 ARM,x86)来执行。来看一下 cBPF 工作过程的逻辑框图:

图中的 filter 就是 BPF 过滤指令片段

2.2 cBPF 的使用

用来过滤的 BPF 指令段每 socket 独立的,通过接口 setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, ...) 设置。如下面的代码片段,表示 socket 只接收来自 IP 地址 192.168.0.18 的数据包:

/*
 * tcpdump src 192.168.0.18 -d
 * (000) ldh      [12]
 * (001) jeq      #0x800           jt 2    jf 4
 * (002) ld       [26]
 * (003) jeq      #0xc0a80012      jt 8    jf 9
 * (004) jeq      #0x806           jt 6    jf 5
 * (005) jeq      #0x8035          jt 6    jf 9
 * (006) ld       [28]
 * (007) jeq      #0xc0a80012      jt 8    jf 9
 * (008) ret      #262144
 * (009) ret      #0
 */
/* tcpdump src 192.168.0.18 -dd */
static struct sock_filter sk_filter[] = { // socket 只接收来自 IP 地址 192.168.0.18 的数据包
	{ 0x28, 0, 0, 0x0000000c },
	{ 0x15, 0, 2, 0x00000800 },
	{ 0x20, 0, 0, 0x0000001a },
	{ 0x15, 4, 5, 0xc0a80012 },
	{ 0x15, 1, 0, 0x00000806 },
	{ 0x15, 0, 3, 0x00008035 },
	{ 0x20, 0, 0, 0x0000001c },
	{ 0x15, 0, 1, 0xc0a80012 },
	{ 0x6, 0, 0, 0x00040000 },
	{ 0x6, 0, 0, 0x00000000 },
};
struct sock_fprog sk_prg;

sk_prg.len = sizeof(sk_filter) / sizeof(sk_filter[0]);
sk_prg.filter = sk_filter;
setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &sk_prg, sizeof(sk_prg));

我们通常不需要手工编写 BPF 指令片段,可以借助 tcpdump -dd 命令来帮助我们编写。BPF 指令集的含义,可参考本文末尾参考链接1.[1]指向文章中的相关内容。

2.3 实现细节

2.3.1 挂接 BPF 指令到 sokcet

先从 setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &sk_prg, sizeof(sk_prg)); 开始:

sys_setsockopt()
	sock_setsockopt()
		sk_attach_filter()
/* net/core/filter.c */

int sk_attach_filter(struct sock_fprog *fprog, struct sock *sk)
{
	struct bpf_prog *prog = __get_filter(fprog, sk); /* 为 filter 创建 bpf 程序,包括指令 */
	...
	err = __sk_attach_prog(prog, sk); /* 将 filter bpf 程序挂接到 socket */
	...
}

/* 为 filter 创建 bpf 程序,包括指令 */
static
struct bpf_prog *__get_filter(struct sock_fprog *fprog, struct sock *sk)
{
	unsigned int fsize = bpf_classic_proglen(fprog); /* 过滤程序的长度:字节数 */
	struct bpf_prog *prog;
	...
	/* 分配 bpf 程序对象空间,包括指令空间 */
	prog = bpf_prog_alloc(bpf_prog_size(fprog->len), 0);
	...
	/* 拷贝过滤程序指令 */
	if (copy_from_user(prog->insns, fprog->filter, fsize)) {
		__bpf_prog_free(prog);
		return ERR_PTR(-EFAULT);
	}

	prog->len = fprog->len; /* 设定程序长度 */
	...
}

static int __sk_attach_prog(struct bpf_prog *prog, struct sock *sk)
{
	struct sk_filter *fp, *old_fp;
	...
	fp = kmalloc(sizeof(*fp), GFP_KERNEL);
	...
	fp->prog = prog; /* 设定 sock 过滤用的 bpf_prog */
	...
	rcu_assign_pointer(sk->sk_filter, fp); // sk->sk_filter = fp;
	...
}

2.3.2 执行 BPF 指令过滤数据包

以下面的代码片段为例进行分析:

fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
...
setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &sk_prg, sizeof(sk_prg));
...

收到网络数据时,产生中断(假定为 ARM + GIC 硬件平台):

gic_handle_irq()
	__do_softirq()
		net_rx_action()
			fec_enet_rx_napi() // 假定为 FEC 的 MAC
				napi_gro_receive()
					netif_receive_skb_internal()
						...
						packet_rcv() // PF_PACKET, SOCK_RAW
static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
		      struct packet_type *pt, struct net_device *orig_dev)
{
	...
	res = run_filter(skb, sk, snaplen); /* 运行 BPF 过滤程序 */
	if (!res) /* 如果 BPF 过滤拒绝该数据包, */
		goto drop_n_restore; /* 则丢弃该数据包 */
	...
	
	/* 接收数据包 */
	spin_lock(&sk->sk_receive_queue.lock);
	po->stats.stats1.tp_packets++;
	sock_skb_set_dropcount(sk, skb);
	__skb_queue_tail(&sk->sk_receive_queue, skb); /* 将数据包加入 socket 的 skb 接收队列 */
	spin_unlock(&sk->sk_receive_queue.lock);
	sk->sk_data_ready(sk); /* 唤醒等待 socket 数据包的进程 */
	return 0;
	...
drop_n_restore:
	...	
}

/* 运行 BPF 过滤程序 */
static unsigned int run_filter(struct sk_buff *skb,
			       const struct sock *sk,
			       unsigned int res)
{
	struct sk_filter *filter;

	rcu_read_lock();
	filter = rcu_dereference(sk->sk_filter);
	if (filter != NULL)
		res = bpf_prog_run_clear_cb(filter->prog, skb);
	rcu_read_unlock();

	return res;
}

/* include/linux/filter.h */
/*
 * 执行 BPF 程序。
 * __bpf_prog_run##stack_size() = ___bpf_prog_run32(), ...
 * 		___bpf_prog_run() 
 */
#define BPF_PROG_RUN(filter, ctx)  (*filter->bpf_func)(ctx, filter->insnsi)

static inline u32 bpf_prog_run_clear_cb(const struct bpf_prog *prog,
					struct sk_buff *skb)
{
	u8 *cb_data = bpf_skb_cb(skb);

	if (unlikely(prog->cb_access))
		memset(cb_data, 0, BPF_SKB_CB_LEN);

	return BPF_PROG_RUN(prog, skb); /* 执行 BPF 程序: ___bpf_prog_run() */
}

这里是以 PF_PACKET 为例进行分析,最终调用了 ___bpf_prog_run() 来执行过滤程序。而其它类型的过滤程序执行过程,最终也都会都会调用 ___bpf_prog_run() 来执行过滤程序。

3. cBPF 示例:过滤 EDSA 协议下的 PTP 以太网帧

我们想从下图场景中的 eth0 过滤 IEEE 1588 PTP 帧:

发送端的用户空间代码的逻辑核心如下:

/*
 * 发送端。
 * 通过 L2 层发送数据 IEEE 1588 PTP 协议事件数据包。
 */
int fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

// 加入 IEEE 1588 PTP 事件协议包多播组:
// MAC 地址 01:1B:19:00:00:0x00 表示 PTP 事件类型协议帧的组播 MAC。
int option = 1;
unsigned char ptp_dst_mac[MAC_LEN] = {0x01, 0x1B, 0x19, 0x00, 0x00, 0x00};

memset(&mreq, 0, sizeof(mreq));
mreq.mr_ifindex = index;
mreq.mr_type = PACKET_MR_MULTICAST;
mreq.mr_alen = MAC_LEN;
memcpy(mreq.mr_address, addr1, MAC_LEN);
setsockopt(fd, SOL_PACKET, option, &mreq, sizeof(mreq)); 

// 启用 MAC 的 IEEE 1588 时间戳:在发送时,会对 IEEE 1588 协议包打上时间戳
struct ifreq ifreq;
struct hwtstamp_config cfg;

memset(&ifreq, 0, sizeof(ifreq));
strncpy(ifreq.ifr_name, "eth0", sizeof(ifreq.ifr_name) - 1);
ifreq->ifr_data = (void *) &cfg;
cfg.tx_type   = HWTSTAMP_TX_ON;
cfg.rx_filter = HWTSTAMP_FILTER_PTP_V2_EVENT;
ioctl(fd, SIOCSHWTSTAMP, &ifreq);

int flags = SOF_TIMESTAMPING_TX_HARDWARE |
			SOF_TIMESTAMPING_RX_HARDWARE |
			SOF_TIMESTAMPING_RAW_HARDWARE;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags ));

// 封装 IEEE 1588 协议包,然后发送
sendmsg()

接收端用户空间的核心代码,和发送端的基本相同,只不过加入了 cBPF(Classic Berkeley Packet Filter) 过滤钩子。过滤钩子考虑了两种情形:一是带 802.1 Q VLAN 标记的 PTP 以太网帧;另一种是不带 VLAN 标记PTP 以太网帧。

/*
 * tcpdump -d '(ether[8:2] == 0xDADA and ether[16:2] == 0x88F7 and ether[18:1] & 0x8 != 0x8)'
 * (000) ldh      [8]
 * (001) jeq      #0xdada          jt 2    jf 8
 * (002) ldh      [16]
 * (003) jeq      #0x88f7          jt 4    jf 8
 * (004) ldb      [18]
 * (005) and      #0x8
 * (006) jeq      #0x8             jt 8    jf 7
 * (007) ret      #262144
 * (008) ret      #0
 */
/* tcpdump -dd '(ether[8:2] == 0xDADA and ether[16:2] == 0x88F7 and ether[18:1] & 0x8 != 0x8)' */
static struct sock_filter raw_filter_ptp_event[] = { // 过滤 EDSA 协议下的 PTP 协议事件类型包
	{ 0x28, 0, 0, 0x00000008 },
	{ 0x15, 0, 6, 0x0000dada },
	{ 0x28, 0, 0, 0x00000010 },
	{ 0x15, 0, 4, 0x000088f7 },
	{ 0x30, 0, 0, 0x00000012 },
	{ 0x54, 0, 0, 0x00000008 },
	{ 0x15, 1, 0, 0x00000008 },
	{ 0x6, 0, 0, 0x00040000 },
	{ 0x6, 0, 0, 0x00000000 },
};
struct sock_fprog prg;

prg.len = ARRAY_SIZE(raw_filter_ptp_event);
prg.filter = raw_filter_ptp_event;
setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &prg, sizeof(prg));

EDSA 协议头的 PTP 抓包数据如下图:

更多关于带 EDSA 协议头的 PTP 数据包的细节,可参考博文 Linux 网络:交换芯片 EDSA 以太网帧简介

4. 参考资料


  1. The BSD Packet Filter: A New Architecture for User-level Packet Capture ↩︎

posted @ 2025-04-07 10:30  JiMoKuangXiangQu  阅读(34)  评论(0)    收藏  举报