使用 BPF 统计网络流量

2021-03-21 22:34:11 浏览数 (1)

本文介绍使用 BPF 统计网络流量。网络流量是云产品的重要计费指标,服务器每秒可以处理上百万的数据包,这也要求有高效的方法来统计流量,而 BPF 最初作为网络包处理的技术,被设计和构造成可以支持这个速率的流量处理。

使用 libpcap

BPF 之前的时代,我们可以使用 libpcap 实现与 tcpdump 类似的方式,捕获网络流量并拷贝到用户程序中进行统计。

下面是一个简单的示例:

代码语言:txt复制
#define ETHERNET_HEADER_LEN 14
#define MIN_IP_HEADER_LEN 20
#define IP_HL(ip) (((ip)->ihl) & 0x0f)

const char* dev = "br-7e20abc6df31";
const char* filter_expr = "src net 172.20.0.0/16";

int64_t traffic = 0;

// Reference:
//      https://www.tcpdump.org/manpages/
//      https://tools.ietf.org/html/rfc791
void traffic_stat() {
    // find the IPv4 network number and netmask for a device
    bpf_u_int32 net = 0;
    bpf_u_int32 mask = 0;
    char errbuf[PCAP_ERRBUF_SIZE] = {0};
    int ret = pcap_lookupnet(dev, &net, &mask, errbuf);
    if (ret == PCAP_ERROR) {
        fprintf(stderr, "pcap_lookupnet failed: %sn", errbuf);
        exit(EXIT_FAILURE);
    }

    // open a device for capturing
    int promisc = 1;
    int timeout = 1000;   // in milliseconds
    const int SNAP_LEN = 64;
    auto handle = pcap_open_live(dev, SNAP_LEN, promisc, timeout, errbuf);
    if (!handle) {
        fprintf(stderr, "pcap_open_live failed: %sn", errbuf);
        exit(EXIT_FAILURE);
    }

    // compile a filter expression
    struct bpf_program fp;
    int optimize = 1;
    ret = pcap_compile(handle, &fp, filter_expr, optimize, net);
    if (ret == PCAP_ERROR) {
        fprintf(stderr, "pcap_compile failed: %sn", pcap_geterr(handle));
        exit(EXIT_FAILURE);
    }

    // set the filter
    ret = pcap_setfilter(handle, &fp);
    if (ret == PCAP_ERROR) {
        fprintf(stderr, "pcap_setfilter failed: %sn", pcap_geterr(handle));
        exit(EXIT_FAILURE);
    }

    // process packets from a live capture
    int packet_count = -1;  // -1 means infinity
    pcap_loop(handle, packet_count, [](u_char* args, const struct pcap_pkthdr* header, const u_char* bytes) {

        auto ip_header = reinterpret_cast<iphdr*>(const_cast<u_char*>(bytes)   ETHERNET_HEADER_LEN);
        const int ip_header_len = IP_HL(ip_header) * 4;
        if (ip_header_len < MIN_IP_HEADER_LEN) {
            return;
        }

        auto len = ntohs(ip_header->tot_len);
        if (len <= 0) {
            return;
        }

        auto traffic = reinterpret_cast<int64_t*>(args);
        *traffic  = len;

    }, reinterpret_cast<u_char*>(&traffic));

    // free a BPF program
    pcap_freecode(&fp);
    // close the capture device
    pcap_close(handle);
}

br-7e20abc6df31 是使用 Docker 创建的网桥,IP 是 172.20.0.1,使用 src net 172.20.0.0/16 表达式过滤出站流量。

这个程序可以做正确的事情,即统计流量。问题在于,它需要把所有流经网卡的流量都拷贝到用户程序,然后在进行统计,而这些拷贝随后就被丢弃,浪费了大量系统资源。

大杀器 BPF

BPF 显然是这个问题的完美解决方案。我们需要的只是累计出站流量,如果能放到内核态执行,意味着我们不需要拷贝网络数据包。

下面是一个使用 libbpf 编写的程序,它的作用与上述 libpcap 的作用一致:

代码语言:txt复制
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

int ifindex = 0;
__u64 traffic = 0;

SEC("tp_btf/netif_receive_skb")
int BPF_PROG(netif_receive_skb, struct sk_buff *skb)
{
    if (skb->dev->ifindex == ifindex) {
        traffic  = skb->data_len;
    }
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

这里,ifindex 即上述网络设备 br-7e20abc6df31 的接口索引。

在这个程序中,我们把它挂载在了 netif_receive_skb 这个 tracepoint 上,网卡接收到数据包后,我们判断是否是目的设备,若是,则累加流量。没有数据拷贝,没有上下文切换,简单高效。

实战

使用 dd 创建一个 512M 大小的文件:

代码语言:txt复制
$ dd if=/dev/zero of=/tmp/testfile bs=4096 count=131072

创建一个新的网络设备:

代码语言:txt复制
$ sudo docker network create my-tc-net

使用上面创建的网络运行一个 nginx 服务器,提供文件下载:

代码语言:txt复制
$ sudo docker run -d --rm 
        -p 10086:80 
        -v /tmp/testfile:/home/data/testfile 
        -v $(PWD)/default.conf:/etc/nginx/conf.d/default.conf 
        --name my-nginx 
        --network my-tc-net 
        nginx:alpine

这是我们的服务器配置:

代码语言:txt复制
server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    location /downloads/ {
        alias               /home/data/;
    }
}

运行上述的流量统计程序:

代码语言:txt复制
$ sudo ./trafficstat

下载文件:

代码语言:txt复制
$ curl http://localhost:10086/downloads/testfile --output testfile

查看流量统计输出:

代码语言:txt复制
$ sudo ./trafficstat
...
traffic in bytes: 536878816
...

符合预期,大功告成。本文源代码可在这里找到。

结论

本文通过实例演示了使用 libbpf 编写 BPF 程序,实现在内核态高效统计网络流量的方案。

0 人点赞