本文主要介绍在Android中对BPF的使用及其解析,参考Android 7.1源码
注:阅读本文需要一定的网络协议基础
参考文章:https://www.freebsd.org/cgi/man.cgi?query=bpf&sektion=4&manpath=FreeBSD+4.7-RELEASE
什么是BPF
伯克利包过滤器(Berkeley Packet Filter,简称BPF),以协议无关的方式提供指向数据链路层的原始接口,网络上的所有数据包都可以通过这种机制进行访问。伯克利包过滤器以一种字符设备存在,如"/dev/bpf0"
,"/dev/bpf1"
等,这些字符设备的文件描述符都通过BIOCSETIF
ioctl与特定的网络接口进行绑定,每一个网络接口都可以和多个绑定的监听者共享,因此每个文件描述符下的过滤器可以看到被过滤出的包。
BPF在Android中的运用
使用BPF过滤报文非常高效,且规则自定义非常强大,tcpdump中就是使用了BPF进行包过滤。在Android中,很早就使用了BPF,典型的一个例子就是DhcpClient,作为dhcp过程的请求方,在发出dhcp request之后,就是通过BPF过滤对应特征的报文来确定是否dhcp成功,同时获取dhcp server分配的IP地址。
/** frameworks/base/services/net/java/android/net/dhcp/DhcpClient.java*/
// - We use a packet socket to receive, because servers send us packets bound for IP addresses
// which we have not yet configured, and the kernel protocol stack drops these.
// - We use a UDP socket to send, so the kernel handles ARP and routing for us (DHCP servers can
// be off-link as well as on-link).
private FileDescriptor mPacketSock;
为什么要使用BPF来实现dhcp这一项功能?上面的代码注释已经说明,在dhcp成功前,STA还没有成功获取IP,因此dhcp server发送过来的带有IP的包会被kernel丢弃,这样,dhcp client当然也无法获取dhcp request的回复包了。下面看看如何创建这样一个过滤器:
/** frameworks/base/services/net/java/android/net/dhcp/DhcpClient.java*/
// 初始化packet socket
private boolean initPacketSocket() {
try {
// 创建AF_PACKET类型的原始socket
mPacketSock = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IP);
PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IP, mIface.getIndex());
Os.bind(mPacketSock, addr);
// 重点在这,创建完socket之后,将filter与socket进行绑定
NetworkUtils.attachDhcpFilter(mPacketSock);
} catch(SocketException|ErrnoException e) {
Log.e(TAG, "Error creating packet socket", e);
return false;
}
return true;
}
跳过NetworkUtils.java,这里面只是个空壳工具类,所有实现都在其jni中,下面实现了如何将过滤规则填充到filter中:
/** frameworks/base/core/jni/android_net_NetUtils.cpp*/
// dhcp请求方的端口:68
static const uint16_t kDhcpClientPort = 68;
// 创建BPF过滤规则,并将过滤规则与DhcpClient.java中创建的socket进行绑定
static void android_net_utils_attachDhcpFilter(JNIEnv *env, jobject clazz, jobject javaFd)
{
// 到ip头的偏移,也就是到ip头开始前的长度,即以太头的长度,
// 由于以太头的长度定长,始终是14字节,所以ip头的偏移固定
uint32_t ip_offset = sizeof(ether_header);
// 协议包含在ip头中,协议的偏移为ether_header长度和protocol在iphdr中的偏移
// iphdr的定义可以参考ip.h
uint32_t proto_offset = ip_offset + offsetof(iphdr, protocol);
// 同proto_offset
uint32_t flags_offset = ip_offset + offsetof(iphdr, frag_off);
// udp中dest port的偏移
// 注意,这里不是dest port的准确,还缺少了ip头的长度
// 准确偏移应该是: ether头长 + ip头长 + (dest在udphdr中的偏移)
uint32_t dport_indirect_offset = ip_offset + offsetof(udphdr, dest);
struct sock_filter filter_code[] = {
// Check the protocol is UDP.
// 指令含义:BPF_LD+BPF_B+BPF_ABS A <- P[k:1],将proto_offset处这个字节(即protocol)存入到累加器中
BPF_STMT(BPF_LD | BPF_B | BPF_ABS, proto_offset),
// 将protocol与IPPROTO_UDP进行比较,即过滤协议是否是udp
// 指令含义:pc += (A== k) ? jt : jf,条件跳跃指令,成功继续,
// 失败则调到第6步,就是"BPF_STMT(BPF_RET | BPF_K, 0)" reject
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_UDP, 0, 6),
// Check this is not a fragment.
// 与上面protocol过滤相似,这里不做解析
BPF_STMT(BPF_LD | BPF_H | BPF_ABS, flags_offset),
BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, 0x1fff, 4, 0),
// Get the IP header length.
// 获取iphdr的长度,由于ip头非定长,因此需要计算
// ip头长度在iphdr第一个字节中有定义,一共4bit,由于linux中是BIG_ENDIAN,因此其后4位为ip头长度
// 指令含义:X<- 4*(P[k:1]&0xf),将计算出的ip头长放入X寄存器
BPF_STMT(BPF_LDX | BPF_B | BPF_MSH, ip_offset),
// Check the destination port.
// 计算到udphdr中dest的偏移,并存入A累加器
// 指令含义:A <- P[X+k:2]:
// X寄存器中变量(ip头长) + dport_indirect_offset(ether头长 + udphdr中dest的偏移) = dest port的准确偏移
// dest占两字节,因此偏移量向后两字节
BPF_STMT(BPF_LD | BPF_H | BPF_IND, dport_indirect_offset),
// 与protocol中BPF_JMP相同,过滤dest port是否是68
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, kDhcpClientPort, 0, 1),
// Accept or reject.
BPF_STMT(BPF_RET | BPF_K, 0xffff),
BPF_STMT(BPF_RET | BPF_K, 0)
};
// 将过滤器code包装成sock_fprog类型
struct sock_fprog filter = {
sizeof(filter_code) / sizeof(filter_code[0]),
filter_code,
};
// 通过jni获取DhcpClient中创建的packet socket fd.
int fd = jniGetFDFromFileDescriptor(env, javaFd);
// 将filter和socket进行绑定
if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
jniThrowExceptionFmt(env, "java/net/SocketException",
"setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
}
}
上面分析了如何构建BPF规则并与DhcpClient中创建的packet socket进行绑定,这一部分是这篇文章要介绍的核心内容,也是BPF运用中的难点,学习时可以参考本文最后的参考文章。
在配置完BPF规则后,DhcpClient通过socket获取dhcp回复后,将按照协议依次解析etherhdr,ipdhr等,最终获取dhcp server分配的IP地址,这些细节都可以在DhcpClient中查看,这里也不赘述。