BPF (Berkeley Packet Filter) 相比于其他包过滤技术,最重要的突破就是实现了一种全新的内核态/用户态隔离下的内核数据过滤方案。自由定制的网络监控程序,总是作为用户级程序运行,为完成监控/过滤网络数据包的任务,必然地会涉及到内核空间/用户空间的拷贝。而众所周知的,内核空间/用户空间的拷贝代价极大,特别在大流量的情况下。BPF 的方案,通过部署一个安全的、沙箱化的内核代理直接实现在内核空间下的包过滤(Packet Filter),尽早地将非目标网络包剔除,只对真正有效的目标网络包实施拷贝。
BPF 最早于 1992 年被提出,1997 年起也被 Linux 内核吸收,定名 LSF (Linux Socket Filter, (aka) BPF:)。早期作用仅仅停留在过滤网络报文;在 2013 年由大牛 Alexei Starovoitov 彻底改造形成全新的 eBPF,并开始面向内核跟踪与事件监控、网络编程两大领域展示其强大的功能。
本篇只着眼于传统的 BPF 技术,探求 BPF 如何在内核埋入包过滤相关的钩子以实现其功能。
BPF 代理程序
本节所展示的源码均基于内核版本 2.6.24
既然说 BPF 的高效在于其将一定用户高度个性定制的包过滤代码埋入了内核,那么就先来看看代理程序是如何深入内核,并展开相应的工作。
与 BPF 相关的系统调用
socket(AF_PACKET, SOCK_RAW, ...)
bind(sockfd, iface)
setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, ...)
recv(sockfd, ...)
通过 setsockopt
系统调用,用户自定义的过滤程序将进入内核空间,并作为钩子对每个通过网络协议栈的网络包进行过滤,抓取并拷贝命中的网络包提交到监听 socket 的接收等待队列中。下列的示例程序展示了配置代理过滤程序及提取过滤结果的逻辑。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <netpacket/packet.h>
#include <linux/filter.h>
#define OP_LDH (BPF_LD | BPF_H | BPF_ABS)
#define OP_LDB (BPF_LD | BPF_B | BPF_ABS)
#define OP_JEQ (BPF_JMP | BPF_JEQ | BPF_K)
#define OP_RET (BPF_RET | BPF_K)
static struct sock_filter bpfcode[6] = {
{ OP_LDH, 0, 0, 12 }, // ldh [12]
{ OP_JEQ, 0, 2, ETH_P_IP }, // jeq #0x800, L2, L5
{ OP_LDB, 0, 0, 23 }, // ldb [23]
{ OP_JEQ, 0, 1, IPPROTO_TCP }, // jeq #0x6, L4, L5
{ OP_RET, 0, 0, 0 }, // ret #0x0
{ OP_RET, 0, 0, -1, }, // ret #0xffffffff
};
int main(int argc, char **argv)
{
int sock;
int n;
char buf[2000];
struct sockaddr_ll addr;
struct packet_mreq mreq;
struct iphdr *ip;
char saddr_str[INET_ADDRSTRLEN], daddr_str[INET_ADDRSTRLEN];
char *proto_str;
char *name;
struct sock_fprog bpf = { 6, bpfcode };
if (argc != 2) {
printf("Usage: %s ifname\n", argv[0]);
return 1;
}
name = argv[1];
sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock < 0) {
perror("socket");
return 1;
}
memset(&addr, 0, sizeof(addr));
addr.sll_ifindex = if_nametoindex(name);
addr.sll_family = AF_PACKET;
addr.sll_protocol = htons(ETH_P_ALL);
if (bind(sock, (struct sockaddr *) &addr, sizeof(addr))) {
perror("bind");
return 1;
}
if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf))) {
perror("setsockopt ATTACH_FILTER");
return 1;
}
memset(&mreq, 0, sizeof(mreq));
mreq.mr_type = PACKET_MR_PROMISC;
mreq.mr_ifindex = if_nametoindex(name);
if (setsockopt(sock, SOL_PACKET,
PACKET_ADD_MEMBERSHIP, (char *)&mreq, sizeof(mreq))) {
perror("setsockopt MR_PROMISC");
return 1;
}
for (;;) {
n = recv(sock, buf, sizeof(buf), 0);
if (n < 1) {
perror("recv");
return 0;
}
ip = (struct iphdr *)(buf + sizeof(struct ether_header));
inet_ntop(AF_INET, &ip->saddr, saddr_str, sizeof(saddr_str));
inet_ntop(AF_INET, &ip->daddr, daddr_str, sizeof(daddr_str));
switch (ip->protocol) {
#define PTOSTR(_p,_str) \
case _p: proto_str = _str; break
PTOSTR(IPPROTO_ICMP, "icmp");
PTOSTR(IPPROTO_TCP, "tcp");
PTOSTR(IPPROTO_UDP, "udp");
default:
proto_str = "";
break;
}
printf("IPv%d proto=%d(%s) src=%s dst=%s\n",
ip->version, ip->protocol, proto_str, saddr_str, daddr_str);
}
return 0;
}
setsockopt
作为代理程序进出内核的核心路径。提供 SO_ATTACH_FILTER
和 SO_DETACH_FILTER
两个操作枚举值
/* Copied from net/core/sock.c */
int sock_setsockopt(struct socket *sock, int level, int optname,
char __user *optval, int optlen)
{
struct sock *sk=sock->sk;
int ret = 0;
switch(optname) {
/* some code omitted ... */
case SO_ATTACH_FILTER:
ret = -EINVAL;
if (optlen == sizeof(struct sock_fprog)) {
struct sock_fprog fprog;
ret = -EFAULT;
/* 从用户空间向内核空间拷贝维护着代理程序的通用数据结构 */
if (copy_from_user(&fprog, optval, sizeof(fprog)))
break;
/* 给 sock 挂载上代理程序 */
ret = sk_attach_filter(&fprog, sk);
}
break;
case SO_DETACH_FILTER:
/* 解挂代理程序 */
ret = sk_detach_filter(sk);
break;
default:
ret = -ENOPROTOOPT;
break;
}
release_sock(sk);
return ret;
}
sk_attach_filter
对代理程序进行安全检测,并在确认不会导致内核 panic 的前提下,将程序挂载到钩子 sk_filter
上。
/* Copied from net/core/filter.c */
int sk_attach_filter(struct sock_fprog *fprog, struct sock *sk)
{
struct sk_filter *fp, *old_fp;
unsigned int fsize = sizeof(struct sock_filter) * fprog->len;
int err;
fp = sock_kmalloc(sk, fsize+sizeof(*fp), GFP_KERNEL);
/* 从用户空间向内核空间拷贝代理程序 */
if (copy_from_user(fp->insns, fprog->filter, fsize)) {
sock_kfree_s(sk, fp, fsize+sizeof(*fp));
return -EFAULT;
}
atomic_set(&fp->refcnt, 1);
fp->len = fprog->len;
/* 在沙箱中对代理程序做安全检测 */
err = sk_chk_filter(fp->insns, fp->len);
if (err) {
sk_filter_uncharge(sk, fp);
return err;
}
rcu_read_lock_bh();
old_fp = rcu_dereference(sk->sk_filter);
/* 在 sk_filter 绑上经过检测的代理程序 */
rcu_assign_pointer(sk->sk_filter, fp);
rcu_read_unlock_bh();
if (old_fp)
sk_filter_delayed_uncharge(sk, old_fp);
return 0;
}
核心的安全检测函数是 sk_chk_filter
。BPF 的沙箱化处理,就是实现里一套精简指令集,提供有限的几十个指令以及几个寄存器,在内核模拟了一个小的处理器来执行代理程序。这里的安全检测,其核心就是检测是否存在非预期的指令,相关指令的非法配合,是否存在读取非法地址的情况以及程序是否以 BPF_RET
指令作为结束。
了解了代理程序的挂载,再来看看这个代理程序在网络协议栈中由谁触发,并如何完成令人惊艳的过滤工作。
/* Copied from net/packet/af_packet.c */
/* socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) 内核创建 packet 族套接字的核心流程 */
static int packet_create(struct net *net, struct socket *sock, int protocol)
{
/* some code omitted ... */
po = pkt_sk(sk);
sk->sk_family = PF_PACKET;
po->num = proto;
spin_lock_init(&po->bind_lock);
/* 配置协议相关钩子函数 */
po->prot_hook.func = packet_rcv;
/* 根据套接字的类型,有两类不同的钩子 */
if (sock->type == SOCK_PACKET)
po->prot_hook.func = packet_rcv_spkt;
if (proto) {
po->prot_hook.type = proto;
/* 将钩子函数直接与网络设备挂钩 */
dev_add_pack(&po->prot_hook);
sock_hold(sk);
po->running = 1;
}
}
/* 网络包(接收/发送)达到数据链路层,都将尝试调用预置的钩子函数 */
static int packet_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
sk = pt->af_packet_priv;
snaplen = skb->len;
/* 核心代理函数的触发逻辑 */
res = run_filter(skb, sk, snaplen);
if (!res)
goto drop_n_restore;
/* 根据代理函数执行结果,如果 res 不为零,则将这个 sk_buff 提交到之前创建的 packet 族的套接字等待队列 */
__skb_queue_tail(&sk->sk_receive_queue, skb);
}
static inline unsigned int run_filter(struct sk_buff *skb, struct sock *sk,
unsigned int res)
{
struct sk_filter *filter;
rcu_read_lock_bh();
filter = rcu_dereference(sk->sk_filter);
/* 存在代理函数,则尝试执行 */
if (filter != NULL)
res = sk_run_filter(skb, filter->insns, filter->len);
rcu_read_unlock_bh();
return res;
}
总的来说,创建的套接字 socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))
就相当于在数据链路层挂上了一个窃听程序,负责将命中的包全部提取到这个窃听套接字的接收队列中。至于内核空间到用户空间的数据提取工作,就像普通的套接字一样。直接使用 recv
系统调用读取套接字的接收队列也就完成了。
最后来看一下代理程序是如何沙箱化执行的。
/* Copied from net/core/filter.c */
/*
* 核心的实现就是模拟了一套简单的处理器,由累加器、索引寄存器、小块内存、PC 组成,
* 一般负责对一段数据的某些特定字节的校验工作
*/
unsigned int sk_run_filter(struct sk_buff *skb, struct sock_filter *filter, int flen)
{
struct sock_filter *fentry; /* We walk down these */
void *ptr;
u32 A = 0; /* Accumulator */
u32 X = 0; /* Index Register */
u32 mem[BPF_MEMWORDS]; /* Scratch Memory Store */
u32 tmp;
int k;
int pc;
/*
* Process array of filter instructions.
*/
for (pc = 0; pc < flen; pc++) {
fentry = &filter[pc];
switch (fentry->code) {
case BPF_ALU|BPF_ADD|BPF_X:
A += X;
continue;
case BPF_ALU|BPF_ADD|BPF_K:
A += fentry->k;
continue;
case BPF_ALU|BPF_SUB|BPF_X:
A -= X;
continue;
case BPF_ALU|BPF_SUB|BPF_K:
A -= fentry->k;
continue;
case BPF_ALU|BPF_MUL|BPF_X:
A *= X;
continue;
case BPF_ALU|BPF_MUL|BPF_K:
A *= fentry->k;
continue;
case BPF_ALU|BPF_DIV|BPF_X:
if (X == 0)
return 0;
A /= X;
continue;
case BPF_ALU|BPF_DIV|BPF_K:
A /= fentry->k;
continue;
case BPF_ALU|BPF_AND|BPF_X:
A &= X;
continue;
case BPF_ALU|BPF_AND|BPF_K:
A &= fentry->k;
continue;
case BPF_ALU|BPF_OR|BPF_X:
A |= X;
continue;
case BPF_ALU|BPF_OR|BPF_K:
A |= fentry->k;
continue;
case BPF_ALU|BPF_LSH|BPF_X:
A <<= X;
continue;
case BPF_ALU|BPF_LSH|BPF_K:
A <<= fentry->k;
continue;
case BPF_ALU|BPF_RSH|BPF_X:
A >>= X;
continue;
case BPF_ALU|BPF_RSH|BPF_K:
A >>= fentry->k;
continue;
case BPF_ALU|BPF_NEG:
A = -A;
continue;
case BPF_JMP|BPF_JA:
pc += fentry->k;
continue;
case BPF_JMP|BPF_JGT|BPF_K:
pc += (A > fentry->k) ? fentry->jt : fentry->jf;
continue;
case BPF_JMP|BPF_JGE|BPF_K:
pc += (A >= fentry->k) ? fentry->jt : fentry->jf;
continue;
case BPF_JMP|BPF_JEQ|BPF_K:
pc += (A == fentry->k) ? fentry->jt : fentry->jf;
continue;
case BPF_JMP|BPF_JSET|BPF_K:
pc += (A & fentry->k) ? fentry->jt : fentry->jf;
continue;
case BPF_JMP|BPF_JGT|BPF_X:
pc += (A > X) ? fentry->jt : fentry->jf;
continue;
case BPF_JMP|BPF_JGE|BPF_X:
pc += (A >= X) ? fentry->jt : fentry->jf;
continue;
case BPF_JMP|BPF_JEQ|BPF_X:
pc += (A == X) ? fentry->jt : fentry->jf;
continue;
case BPF_JMP|BPF_JSET|BPF_X:
pc += (A & X) ? fentry->jt : fentry->jf;
continue;
case BPF_LD|BPF_W|BPF_ABS:
k = fentry->k;
load_w:
ptr = load_pointer(skb, k, 4, &tmp);
if (ptr != NULL) {
A = ntohl(get_unaligned((__be32 *)ptr));
continue;
}
break;
case BPF_LD|BPF_H|BPF_ABS:
k = fentry->k;
load_h:
ptr = load_pointer(skb, k, 2, &tmp);
if (ptr != NULL) {
A = ntohs(get_unaligned((__be16 *)ptr));
continue;
}
break;
case BPF_LD|BPF_B|BPF_ABS:
k = fentry->k;
load_b:
ptr = load_pointer(skb, k, 1, &tmp);
if (ptr != NULL) {
A = *(u8 *)ptr;
continue;
}
break;
case BPF_LD|BPF_W|BPF_LEN:
A = skb->len;
continue;
case BPF_LDX|BPF_W|BPF_LEN:
X = skb->len;
continue;
case BPF_LD|BPF_W|BPF_IND:
k = X + fentry->k;
goto load_w;
case BPF_LD|BPF_H|BPF_IND:
k = X + fentry->k;
goto load_h;
case BPF_LD|BPF_B|BPF_IND:
k = X + fentry->k;
goto load_b;
case BPF_LDX|BPF_B|BPF_MSH:
ptr = load_pointer(skb, fentry->k, 1, &tmp);
if (ptr != NULL) {
X = (*(u8 *)ptr & 0xf) << 2;
continue;
}
return 0;
case BPF_LD|BPF_IMM:
A = fentry->k;
continue;
case BPF_LDX|BPF_IMM:
X = fentry->k;
continue;
case BPF_LD|BPF_MEM:
A = mem[fentry->k];
continue;
case BPF_LDX|BPF_MEM:
X = mem[fentry->k];
continue;
case BPF_MISC|BPF_TAX:
X = A;
continue;
case BPF_MISC|BPF_TXA:
A = X;
continue;
case BPF_RET|BPF_K:
return fentry->k;
case BPF_RET|BPF_A:
return A;
case BPF_ST:
mem[fentry->k] = A;
continue;
case BPF_STX:
mem[fentry->k] = X;
continue;
default:
WARN_ON(1);
return 0;
}
/*
* Handle ancillary data, which are impossible
* (or very difficult) to get parsing packet contents.
*/
switch (k-SKF_AD_OFF) {
case SKF_AD_PROTOCOL:
A = ntohs(skb->protocol);
continue;
case SKF_AD_PKTTYPE:
A = skb->pkt_type;
continue;
case SKF_AD_IFINDEX:
A = skb->dev->ifindex;
continue;
default:
return 0;
}
}
return 0;
}
BPF JIT 技术
沙箱执行虽然保证了内核的安全,无需担心用户随意定制的程序导致内核 panic 。但大量的网络包都需要经过这个沙箱的检测,这部分逻辑的效率就变得至关重要了。因此,在 Linux 3.x 版本之后,BPF 开始引入 JIT 技术来提高这部分代码的执行效率。
下列源代码均来自于 Linux 3.10.1 版本
以 BPF 指令 BPF_S_ALU_ADD_X
来讲,本质上 JIT 就是做完成编译的工作,将高级语言向低级语言做一个翻译工作。
/* Copied from arch/x86/net/bpf_jit_comp.c */
case BPF_S_ALU_ADD_X: /* A += X; */
seen |= SEEN_XREG;
EMIT2(0x01, 0xd8); /* add %ebx,%eax */
break;
在 x86 架构下,翻译作机器码的 add %ebx,%eax
就是 0x01d8 啦
void bpf_jit_compile(struct sk_filter *fp)
{
u8 temp[64];
u8 *prog;
unsigned int proglen, oldproglen = 0;
int ilen, i;
int t_offset, f_offset;
u8 t_op, f_op, seen = 0, pass;
u8 *image = NULL;
u8 *func;
int pc_ret0 = -1; /* bpf index of first RET #0 instruction (if any) */
unsigned int cleanup_addr; /* epilogue code offset */
unsigned int *addrs;
const struct sock_filter *filter = fp->insns;
int flen = fp->len;
if (!bpf_jit_enable)
return;
addrs = kmalloc(flen * sizeof(*addrs), GFP_KERNEL);
if (addrs == NULL)
return;
/* Before first pass, make a rough estimation of addrs[]
* each bpf instruction is translated to less than 64 bytes
*/
for (proglen = 0, i = 0; i < flen; i++) {
proglen += 64;
addrs[i] = proglen;
}
cleanup_addr = proglen; /* epilogue address */
for (pass = 0; pass < 10; pass++) {
u8 seen_or_pass0 = (pass == 0) ? (SEEN_XREG | SEEN_DATAREF | SEEN_MEM) : seen;
/* no prologue/epilogue for trivial filters (RET something) */
proglen = 0;
prog = temp;
/* some code omitted ... */
/*
* JIT 全部基于 prog 指针实施,temp 和 prog 就分别作为 JIT 机器码的头尾指针。
* 所有翻译成的机器码全部存储在 temp 指向的内存区域
*/
ilen = prog - temp;
if (image) {
if (unlikely(proglen + ilen > oldproglen)) {
pr_err("bpb_jit_compile fatal error\n");
kfree(addrs);
module_free(NULL, image);
return;
}
/* 最终 JIT 之后的机器码会全部拷贝到 image 指向的内存区域 */
memcpy(image + proglen, temp, ilen);
}
proglen += ilen;
addrs[i] = proglen;
prog = temp;
if (image) {
if (proglen != oldproglen)
pr_err("bpb_jit_compile proglen=%u != oldproglen=%u\n", proglen, oldproglen);
break;
}
if (proglen == oldproglen) {
image = module_alloc(max_t(unsigned int,
proglen,
sizeof(struct work_struct)));
if (!image)
goto out;
}
oldproglen = proglen;
}
if (image) {
bpf_flush_icache(image, image + proglen);
/* image 指向的机器码就直接替代了非 JIT 下的 sk_run_filter 函数 */
fp->bpf_func = (void *)image;
}
out:
kfree(addrs);
return;
}
JIT 编译完成后的机器码,完全可以类比 C 语言在编译后的 ELF 文件中的机器码。是可以交由 CPU 直接执行的。而由 CPU 直接执行的效率,一般都是高于基于 CPU 模拟的一套处理器的。