计算机系统应用教程网站

网站首页 > 技术文章 正文

AF-PACKET的V3版本 afk安装包

btikc 2024-10-12 11:08:49 技术文章 18 ℃ 0 评论

一 前言

上一篇介绍了通过AF-PACKET的V1 版本进行网络包的捕获,比较新的Linux内核是支持V3版本的,相对于前两个版本(V2和V1比较相似,V2版本的时间精度从微秒提升到纳秒。)V3版本,具有以下的提升:

CPU使用率降低约15-20%数据包捕获率提高约20%数据包的密度提升2倍(不知道什么意思, 如 ~2x increase in packet density)端口聚合分析非静态数据帧大小,可以保存整个数据包。

所以这次就学习V3版本的用法,和其他能提示AF-PACKET抓包性能的均衡策略和方法。

二 实战

V3的版本结构每次遍历和以前的不同是按照block遍历,当然下一层再按照frame遍历。
V3的时间戳精确度到纳秒。

struct tpacket_req3 {
    unsigned int    tp_block_size;      // 每个连续内存块的最小尺寸(必须是 PAGE_SIZE * 2^n )
    unsigned int    tp_block_nr;        // 内存块数量
    unsigned int    tp_frame_size;      // 每个帧的大小(虽然V3中的帧长是可变的,但创建时还是会传入一个最大的允许值)
    unsigned int    tp_frame_nr;        // 帧的总个数(必须等于 每个内存块中的帧数量*内存块数量)
    unsigned int    tp_retire_blk_tov;  // 内存块的寿命(ms),超时后即使内存块没有被数据填入也会被内核停用,0意味着不设超时
    unsigned int    tp_sizeof_priv;     // 每个内存块中私有空间大小,0意味着不设私有空间
    unsigned int    tp_feature_req_word;// 标志位集合(目前就支持1个标志 TP_FT_REQ_FILL_RXHASH)
}

// TPACKET_V3环形缓冲区每个帧的头部结构
struct tpacket3_hdr {
    __u32       tp_next_offset; // 指向同一个内存块中的下一个帧
    __u32       tp_sec;         // 时间戳(s)
    __u32       tp_nsec;        // 时间戳(ns)
    __u32       tp_snaplen;     // 捕获到的帧实际长度
    __u32       tp_len;         // 帧的理论长度
    __u32       tp_status;      // 帧的状态
    __u16       tp_mac;         // 以太网MAC字段距离帧头的偏移量
    __u16       tp_net;
    union {
        struct tpacket_hdr_variant1 hv1;    // 包含vlan信息的子结构
    };
    __u8        tp_padding[8];
}

下面是内核文档中的收包例子,代码如下:


/* Written from scratch, but kernel-to-user space API usage
 * dissected from lolpcap:
 *  Copyright 2011, Chetan Loke <loke.chetan@gmail.com>
 *  License: GPL, version 2.0
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <poll.h>
#include <unistd.h>
#include <signal.h>
#include <inttypes.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h>
#include <linux/ip.h>

#ifndef likely
# define likely(x)      __builtin_expect(!!(x), 1)
#endif
#ifndef unlikely
# define unlikely(x)        __builtin_expect(!!(x), 0)
#endif

struct block_desc {
    uint32_t version;
    uint32_t offset_to_priv;
    struct tpacket_hdr_v1 h1;
};

struct ring {
    struct iovec *rd;
    uint8_t *map;
    struct tpacket_req3 req;
};

static unsigned long packets_total = 0, bytes_total = 0;
static sig_atomic_t sigint = 0;

static void sighandler(int num)
{
    sigint = 1;
}

static int setup_socket(struct ring *ring, char *netdev)
{
    int err, i, fd, v = TPACKET_V3;
    struct sockaddr_ll ll;
    unsigned int blocksiz = 1 << 22, framesiz = 1 << 11;
    unsigned int blocknum = 64;

    fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (fd < 0) {
        perror("socket");
        exit(1);
    }

    err = setsockopt(fd, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
    if (err < 0) {
        perror("setsockopt");
        exit(1);
    }

    memset(&ring->req, 0, sizeof(ring->req));
    ring->req.tp_block_size = blocksiz;
    ring->req.tp_frame_size = framesiz;
    ring->req.tp_block_nr = blocknum;
    ring->req.tp_frame_nr = (blocksiz * blocknum) / framesiz;
    ring->req.tp_retire_blk_tov = 60;
    ring->req.tp_feature_req_word = TP_FT_REQ_FILL_RXHASH;

    err = setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &ring->req,
                     sizeof(ring->req));
    if (err < 0) {
        perror("setsockopt");
        exit(1);
    }

    ring->map = mmap(NULL, ring->req.tp_block_size * ring->req.tp_block_nr,
                     PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, 0);
    if (ring->map == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }

    ring->rd = malloc(ring->req.tp_block_nr * sizeof(*ring->rd));
    assert(ring->rd);
    for (i = 0; i < ring->req.tp_block_nr; ++i) {
        ring->rd[i].iov_base = ring->map + (i * ring->req.tp_block_size);
        ring->rd[i].iov_len = ring->req.tp_block_size;
    }

    memset(&ll, 0, sizeof(ll));
    ll.sll_family = PF_PACKET;
    ll.sll_protocol = htons(ETH_P_ALL);
    ll.sll_ifindex = if_nametoindex(netdev);
    ll.sll_hatype = 0;
    ll.sll_pkttype = 0;
    ll.sll_halen = 0;

    err = bind(fd, (struct sockaddr *) &ll, sizeof(ll));
    if (err < 0) {
        perror("bind");
        exit(1);
    }

    return fd;
}

static void display(struct tpacket3_hdr *ppd)
{
    struct ethhdr *eth = (struct ethhdr *) ((uint8_t *) ppd + ppd->tp_mac);
    struct iphdr *ip = (struct iphdr *) ((uint8_t *) eth + ETH_HLEN);

    if (eth->h_proto == htons(ETH_P_IP)) {
        struct sockaddr_in ss, sd;
        char sbuff[NI_MAXHOST], dbuff[NI_MAXHOST];

        memset(&ss, 0, sizeof(ss));
        ss.sin_family = PF_INET;
        ss.sin_addr.s_addr = ip->saddr;
        getnameinfo((struct sockaddr *) &ss, sizeof(ss),
                    sbuff, sizeof(sbuff), NULL, 0, NI_NUMERICHOST);

        memset(&sd, 0, sizeof(sd));
        sd.sin_family = PF_INET;
        sd.sin_addr.s_addr = ip->daddr;
        getnameinfo((struct sockaddr *) &sd, sizeof(sd),
                    dbuff, sizeof(dbuff), NULL, 0, NI_NUMERICHOST);

        printf("%s -> %s, ", sbuff, dbuff);
    }

    printf("rxhash: 0x%x\n", ppd->hv1.tp_rxhash);
}

static void walk_block(struct block_desc *pbd, const int block_num)
{
    int num_pkts = pbd->h1.num_pkts, i;
    unsigned long bytes = 0;
    struct tpacket3_hdr *ppd;

    ppd = (struct tpacket3_hdr *) ((uint8_t *) pbd +
                                   pbd->h1.offset_to_first_pkt);
    for (i = 0; i < num_pkts; ++i) {
        bytes += ppd->tp_snaplen;
        display(ppd);

        ppd = (struct tpacket3_hdr *) ((uint8_t *) ppd +
                                       ppd->tp_next_offset);
    }

    packets_total += num_pkts;
    bytes_total += bytes;
}

static void flush_block(struct block_desc *pbd)
{
    pbd->h1.block_status = TP_STATUS_KERNEL;
}

static void teardown_socket(struct ring *ring, int fd)
{
    munmap(ring->map, ring->req.tp_block_size * ring->req.tp_block_nr);
    free(ring->rd);
    close(fd);
}

int main(int argc, char **argp)
{
    int fd, err;
    socklen_t len;
    struct ring ring;
    struct pollfd pfd;
    unsigned int block_num = 0, blocks = 64;
    struct block_desc *pbd;
    struct tpacket_stats_v3 stats;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s INTERFACE\n", argp[0]);
        return EXIT_FAILURE;
    }

    signal(SIGINT, sighandler);

    memset(&ring, 0, sizeof(ring));
    fd = setup_socket(&ring, argp[argc - 1]);
    assert(fd > 0);

    memset(&pfd, 0, sizeof(pfd));
    pfd.fd = fd;
    pfd.events = POLLIN | POLLERR;
    pfd.revents = 0;

    while (likely(!sigint)) {
        pbd = (struct block_desc *) ring.rd[block_num].iov_base;

        if ((pbd->h1.block_status & TP_STATUS_USER) == 0) {
            poll(&pfd, 1, -1);
            continue;
        }

        walk_block(pbd, block_num);
        flush_block(pbd);
        block_num = (block_num + 1) % blocks;
    }

    len = sizeof(stats);
    err = getsockopt(fd, SOL_PACKET, PACKET_STATISTICS, &stats, &len);
    if (err < 0) {
        perror("getsockopt");
        exit(1);
    }

    fflush(stdout);
    printf("\nReceived %u packets, %lu bytes, %u dropped, freeze_q_cnt: %u\n",
           stats.tp_packets, bytes_total, stats.tp_drops,
           stats.tp_freeze_q_cnt);

    teardown_socket(&ring, fd);
    return 0;
}

代码说明:

  1. 使用的tpacket_req3 时候会有两个以前没有的变量赋值如下:
    ring->req.tp_retire_blk_tov = 60;
    ring->req.tp_feature_req_word = TP_FT_REQ_FILL_RXHASH;

tp_retrie_blk_tov 即超时值,单位是毫秒。当这个超时被触发时,内核会将该块的状态从 TP_STATUS_USER 改为 TP_STATUS_KERNEL,即超时触发后,会将缓存区释放给内核用来装数据了。
tp_feature_req_word 即功能值,tp_feature_req_word 被设置为 TP_FT_REQ_FILL_RXHASH。这个标志请求内核在每个数据包的头部填充接收哈希(RX hash)值。接收哈希通常用于负载均衡和流量分类等场景,它可以提供一种快速的方式来决定数据包应该如何被处理或路由。

  1. v3版本来说,数据包帧的最小大小,不能低于这个大小。
  2. mmap申请内核和用户空间共享的内存,注意设置MAP_LOCKED标识的意思,这个内存是不可以交换到磁盘上的,会被锁在物理内存中。
  3. 如果需要让内核设置包的时间,可以通过下面代码设置:
int req = SOF_TIMESTAMPING_RAW_HARDWARE;
setsockopt(fd, SOL_PACKET, PACKET_TIMESTAMP, (void *) &req, sizeof(req));
  1. 转换ip代码:
struct sockaddr_in ss, sd;
char sbuff[NI_MAXHOST], dbuff[NI_MAXHOST];

memset(&ss, 0, sizeof(ss));
ss.sin_family = PF_INET;
ss.sin_addr.s_addr = ip->saddr;
getnameinfo((struct sockaddr *) &ss, sizeof(ss),sbuff, sizeof(sbuff), NULL, 0, NI_NUMERICHOST);

这段代码是将获取的包的ip地址,其中:NI_NUMERICHOST: 直接返回主机的IP地址,而不是尝试查找主机名。

  1. 主循环:
    while (likely(!sigint)) {
        pbd = (struct block_desc *) ring.rd[block_num].iov_base;

        if ((pbd->h1.block_status & TP_STATUS_USER) == 0) {
            poll(&pfd, 1, -1);
            continue;
        }

        walk_block(pbd, block_num);
        flush_block(pbd);
        block_num = (block_num + 1) % blocks;
    }

在没有中断的情况下,获取block头部信息,根据头的block状态,如果不是TP_STATUS_USER情况下,继续进入poll等待。如果已经有数据了,则走walk循环,然后更改block状态,block指针后移,做循环队列,这个和原来v1版本的frame做循环。
代码如下:

 // 调整block执行第一个packet包、下一个包是通过tp_next_offset 来指向的。   
 ppd = (struct tpacket3_hdr *) ((uint8_t *) pbd +
                                   pbd->h1.offset_to_first_pkt);
    for (i = 0; i < num_pkts; ++i) {
        bytes += ppd->tp_snaplen;
        display(ppd);

        ppd = (struct tpacket3_hdr *) ((uint8_t *) ppd +
                                       ppd->tp_next_offset);
    }

代码里面数据结构示意图如下:

代码编译运行:

gcc -g seven.c -o seven
./seven em1
192.168.3.128 -> 218.39.92.236, rxhash: 0xfac12039
192.168.3.128 -> 218.39.92.236, rxhash: 0xc2bf9e0f
192.168.3.128 -> 218.39.92.236, rxhash: 0xfac12039
192.168.3.128 -> 218.39.92.236, rxhash: 0xc2bf9e0f
192.168.3.128 -> 218.39.92.236, rxhash: 0xfac12039
192.168.3.128 -> 218.39.92.236, rxhash: 0xfac12039
192.168.3.128 -> 218.39.92.236, rxhash: 0xfac12039
218.39.92.236 -> 192.168.3.128, rxhash: 0xc2bf9e0f
218.39.92.236 -> 192.168.3.128, rxhash: 0xfac12039
218.39.92.236 -> 192.168.3.128, rxhash: 0xfac12039
192.168.3.128 -> 218.39.92.236, rxhash: 0xc2bf9e0f
10.0.15.20 -> 10.0.15.100, rxhash: 0x2cb2fffa
10.0.15.20 -> 10.0.15.100, rxhash: 0x2cb2fffa

三 参考

https://www.kernel.org/doc/Documentation/networking/packet_mmap.txt

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表