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 以太网帧简介 。

浙公网安备 33010602011771号