BPF初探 - Android中BPF运用实例

本文主要介绍在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中查看,这里也不赘述。

你可能感兴趣的:(Linux)