tcpreplay 流量拆分算法研究

1.1  算法目的

现在网络架构一般是Client-Server架构,所以网络流量一般是分 C-S 和 S-C 两个方向。tcpdump等抓包工具获取的pcap包,两个流向的数据没有被区分。流量方向的区分有什么好处?这种拆分至少有两个好处,一是在抓包基础上定制数据包,可以支持单独修改一个流向的IP,MAC等字段。二是实际测试被测设备的时候,可以将两个流向的流量通过不同的端口发送出来。Tcpprep支持了这种拆分(早先版本这部分功能混合在tcpreplay中,后来独立拆分成为tcpprep工具)

Tcpprep3.4.4 支持了以下流量拆分的参数

-a, --auto=str       Auto-split mode   自动模式

-c, --cidr=str        CIDR-split mode   子网匹配模式

-r, --regex=str      Regex-split mode  正则匹配模式

-p, --port           Port-split mode  端口匹配模式

-e, --mac=str        Source MAC split mode  MAC匹配模式

-reverse            Matches to be client instead of server

其中,auto 模式支持5种子模式

Auto = bridge|router|client|server|first

另外,下面两个参数也是流量拆分相关的,当 auto=router 时,用户可选下面的参数配合,

-m, --minmask=num          Minimum network mask length in auto mode

-M, --maxmask=num          Maximum network mask length in auto mode

1.2  算法思想

考虑一个问题,如何判断一个packets 是 C->S 还是 S->C ?

一种思路是用户指定,比如用户指定某个IP或MAC是C->S,或者更进一步,IP匹配某个正则表达式就是C->S,这种情况下,实际上是用户‘手动’判断,不需要特别的算法。上面各种模式中,除了 auto 模式,其余模式都属于这类‘手动’方法,前提是用户必须对这些pcap包非常熟悉。

那么,如何实现‘自动’识别C->S还是S->C?

答案是,利用网络包的协议特征。因为一般的网络流量都基于TCP或UDP,以TCP为例,SYN,ACK等标志位就可以支持流量方向的判断了。Tcpprep3.4.4 使用到的协议特征

包括如下:

Tcpprep3.4.4 使用到的协议特征

C->S 客户端特征:

Sending a TCP Syn packet to another host

Making a DNS request

Recieving an ICMP port unreachable

S->C 服务端特征:

Sending a TCP Syn/Ack packet to another host

Sending a DNS Reply

Sending an ICMP port unreachable

       所以,自动流量拆分算法的基本思路是:解析pcap的每一个packet,判断其特征位的值,对比客户端特征和服务端特征,得到一个比较结果,通过比较结果来判断流量方向。

       考虑上述思路,是逐个packet单独计算,但是,一个pcap的许多packet,其IP,MAC,PORT等都是相同的,换句话说,是属于一个方向。因此,特征匹配运算的基本单位,不应该是一个packet,而应该是以IP为单位。

Tcpprep在实现的时候根据IP大小建立了一颗红黑树,相同IP的packet不再单独生成节点,而是将特征匹配的运算结果累计在相同IP的节点上,最后某个IP属于哪个方向是由红黑树的对应节点的累计结果得到的。该算法需要大量查找和插入操作,用红黑树比较合适。

       上面两段落的描述即是 auto=bridge 的算法思路,也是自动拆分流量的基本算法。其他4种自动拆分方向算法都建立在bridge之上,它们之间的关系是,都先使用 bridge 模式运算得到红黑树,对于树上还无法归入 C->S 或 S->C 的节点,再使用对应模式的策略。在auto=server ,就是剩下的节点全部视为 S->C,在 auto=client ,就是剩下的节点全部视为

C->S,在 auto=first ,就是剩下的节点全部视为与第一个packet 方向一致。

auto=router 的策略比较复杂,使用到了 CIDR 这种数据结构,CIDR其实是一个链表,每个链表节点存放一个 cidr 地址,它的思路是,如果这个无法判断的节点的IP刚好落在一个其余IP都是S->C的cidr里,那么它就是 S->C,相反,如果改节点的IP刚好落在某个cidr,而树上其他在该cidr 里的节点都是 C->S,那么该节点也是 C->S。

 

 

1.3  算法流程

Tcpprep 主流程

Process(pcap) 流程

1.1  算法实现

1.1.1  数据结构

/*tcpprep 控制结构*/

struct tcpprep_opt_s {

    pcap_t *pcap;  /*pcap包控制句柄*/

    int verbose;

    char *tcpdump_args;

 

    tcpr_cache_t *cachedata; /*缓存数据子控制结构,具体见缓存算法相关描述*/

    tcpr_cidr_t *cidrdata;/*cidir 链表控制结构*/

    char *maclist; /*mac 地址列表,适用于mode = MAC*/

    tcpr_xX_t xX; /*exclude ip 列表*/

    tcpr_bpf_t bpf;

    tcpr_services_t services;

    char *comment; /* cache file comment */

    int nocomment; /* don't include the cli in the comment */

    int mode;      /* mode */

    int automode;  /* our auto mode */

    int min_mask; /*这两个适用于 auto=router */

    int max_mask;

    double ratio; /*server 和 client 的比率*/

    regex_t preg; /*适用于 mode = grex */

    int nonip;

};

typedef struct tcpprep_opt_s tcpprep_opt_t;

 

 

 

/*红黑树节点控制结构*/

typedef struct tcpr_tree_s {

    RB_ENTRY(tcpr_tree_s) node; /*在 redblack.h 中定义*/

    int family;

    union {

        unsigned long ip;           /* ip/network address in network byte order */

        struct tcpr_in6_addr ip6;

    } u;

    u_char mac[ETHER_ADDR_LEN]; /* mac address of system */

    int masklen;                /* CIDR network mask length */

/*下面这两个变量就是用来累计客户端和服务端特征的变量*/

    int server_cnt;             /* count # of times this entry was flagged server */

    int client_cnt;             /* flagged client */

/*运算结果是什么方向存放在下面这个type里*/

    int type;                   /* 1 = server, 0 = client, -1 = undefined */

} tcpr_tree_t;

 

/* *根节点*/

typedef struct tcpr_data_tree_s {

    tcpr_tree_t *rbh_root;

} tcpr_data_tree_t;

 

1.1.2  主要函数实现

/**

 * 使用 libpcap library 解析 packets

* 根据流量拆分算法运算结果生成cache file,去掉了部分无关代码

 */

static COUNTER

process_raw_packets(pcap_t * pcap)

{

    ipv4_hdr_t *ip_hdr = NULL; /*ipv4 头结构,定义在 libpcap library*/

    ipv6_hdr_t *ip6_hdr = NULL; /*ipv6 头结构*/

    eth_hdr_t *eth_hdr = NULL; /*以太帧头结构*/

    struct pcap_pkthdr pkthdr; /*pcap头控制结构,定义在 libpcap library*/

    const u_char *pktdata = NULL;

    COUNTER packetnum = 0;

    int l2len, cache_result = 0;

    u_char ipbuff[MAXPACKET], *buffptr;

    tcpr_dir_t direction; /*流量方向*/

/*下面是主循环*/

    while ((pktdata = pcap_next(pcap, &pkthdr)) != NULL) {

        packetnum++;

/*下面检查exclude list,如果匹配,缓存写入 DON’T_SEND,continue处理下一个*/

        /* look for include or exclude LIST match */

        if (options.xX.list != NULL) {

            if (options.xX.mode < xXExclude) {

                if (!check_list(options.xX.list, packetnum)) {

add_cache(&(options.cachedata), DONT_SEND, 0);

                    continue;

                }

            }

            else if (check_list(options.xX.list, packetnum)) {

                add_cache(&(options.cachedata), DONT_SEND, 0);

                continue;

            }

        }

/*获取ip头,如果获取不到,除非用户设定了MAC模式,在MAC模式下,还可以通过mac值判定方向,否则直接将 type=NONIP写入缓存,continue在下一个packet*/

        eth_hdr = (eth_hdr_t *)pktdata;

        if (options.mode != MAC_MODE) {

            buffptr = ipbuff;

            /* 获取IPv4 */

            if ((ip_hdr = (ipv4_hdr_t *)get_ipv4(pktdata, pkthdr.caplen,

                    pcap_datalink(pcap), &buffptr))) {

                dbg(2, "Packet is IPv4");

            }

            /* 获取IPv6 */

            else if ((ip6_hdr = (ipv6_hdr_t *)get_ipv6(pktdata, pkthdr.caplen,

                    pcap_datalink(pcap), &buffptr))) {

                dbg(2, "Packet is IPv6");

            }

            /* we're something else... */

            else { /*都获取不到,写对应packet缓存为 nonip*/

                if (options.mode != AUTO_MODE) {

                    dbg(3, "Adding to cache using options for Non-IP packets");

                    add_cache(&options.cachedata, SEND, options.nonip);

                }

                /* go to next packet */

                continue;

            }

/*下面判定 exclude ip 列表,如果匹配,则缓存写入 DON’T_SEND,continue处理下一个packet*/

            l2len = get_l2len(pktdata, pkthdr.caplen, pcap_datalink(pcap));

            /* look for include or exclude CIDR match */

            if (options.xX.cidr != NULL) {

                if (ip_hdr) {

                    if (!process_xX_by_cidr_ipv4(options.xX.mode, options.xX.cidr, ip_hdr)) {

                        add_cache(&options.cachedata, DONT_SEND, 0);

                        continue;

                    }

                } else if (ip6_hdr) {

                    if (!process_xX_by_cidr_ipv6(options.xX.mode, options.xX.cidr, ip6_hdr)) {

                        add_cache(&options.cachedata, DONT_SEND, 0);

                        continue;

                    }

                }

            }

        }

 

/*下面分别处理各种拆分模式*/

  switch (options.mode) {

        case REGEX_MODE: /*正则表达式模式*/

            if (ip_hdr) {/*拿源IP跟用户设定的regex匹配得到结果,写入缓存*/

                direction = check_ipv4_regex(ip_hdr->ip_src.s_addr);

            } else if (ip6_hdr) {

                direction = check_ipv6_regex(&ip6_hdr->ip_src);

            }

cache_result = add_cache(&options.cachedata, SEND, direction);

            break;

 

case CIDR_MODE: /*cidr列表模式*/

         if (ip_hdr) {/*拿源IP跟用户设定的cidr列表匹配得到结果,写入缓存*/

                direction = check_ip_cidr(options.cidrdata, ip_hdr->ip_src.s_addr) ? TCPR_DIR_C2S : TCPR_DIR_S2C;

            } else if (ip6_hdr) {

                direction = check_ip6_cidr(options.cidrdata, &ip6_hdr->ip_src) ? TCPR_DIR_C2S : TCPR_DIR_S2C;

            }

            cache_result = add_cache(&options.cachedata, SEND, direction);

            break;

 

        case MAC_MODE: /*MAC模式*/

            direction = macinstring(options.maclist, (u_char *)eth_hdr->ether_shost);

            cache_result = add_cache(&options.cachedata, SEND, direction);

            break;

 

        case AUTO_MODE: /*auto模式

比如 auto=bridge,会分成两次运行,第一次检测 auto_mode,创建红黑树,第二次检测 bridge_mode,对树做运算并将结果写入缓存,router等模式也是一样的处理*/

 /* first run through in auto mode: create tree */

            if (options.automode != FIRST_MODE) {

                if (ip_hdr) {

                    add_tree_ipv4(ip_hdr->ip_src.s_addr, pktdata);

                } else if (ip6_hdr) {

                    add_tree_ipv6(&ip6_hdr->ip_src, pktdata);

                }

            } else {

                if (ip_hdr) {

                    add_tree_first_ipv4(pktdata);

                } else if (ip6_hdr) {

                    add_tree_first_ipv6(pktdata);

                }

            }

            break;

case ROUTER_MODE:

            /* 具体到router,第二次运行,根据树的结果生成cache

             */

            if (ip_hdr) {

                cache_result = add_cache(&options.cachedata, SEND,

                    check_ip_tree(options.nonip, ip_hdr->ip_src.s_addr));

            } else {

                cache_result = add_cache(&options.cachedata, SEND,

                    check_ip6_tree(options.nonip, &ip6_hdr->ip_src));

            }

            break;

 

  case BRIDGE_MODE:

            /* 具体到bridge,第二次运行,根据树的结果生成cache

             */

            if (ip_hdr) {

                cache_result = add_cache(&options.cachedata, SEND,

                    check_ip_tree(DIR_UNKNOWN, ip_hdr->ip_src.s_addr));

            } else {

                cache_result = add_cache(&options.cachedata, SEND,

                    check_ip6_tree(DIR_UNKNOWN, &ip6_hdr->ip_src));

            }

            break;

case SERVER_MODE:

            /* 具体到server,第二次运行,根据树的结果生成cache

             */

            if (ip_hdr) {

                cache_result = add_cache(&options.cachedata, SEND,

                    check_ip_tree(DIR_SERVER, ip_hdr->ip_src.s_addr));

            } else {

                cache_result = add_cache(&options.cachedata, SEND,

                    check_ip6_tree(DIR_SERVER, &ip6_hdr->ip_src));

            }

            break;

     case CLIENT_MODE:

          /* 具体到client,第二次运行,根据树的结果生成cache

             */

            if (ip_hdr) {

                cache_result = add_cache(&options.cachedata, SEND,

                    check_ip_tree(DIR_CLIENT, ip_hdr->ip_src.s_addr));

            } else {

                cache_result = add_cache(&options.cachedata, SEND,

                    check_ip6_tree(DIR_CLIENT, &ip6_hdr->ip_src));

            }

            break;

     case PORT_MODE:

            /*port模式,根据目的端口得到方向

             */

            cache_result = add_cache(&options.cachedata, SEND,

                check_dst_port(ip_hdr, ip6_hdr, (pkthdr.caplen - l2len)));

            break;

case FIRST_MODE:

           /* 具体到first,第二次运行,根据树的结果生成cache

             */

            if (ip_hdr) {

                cache_result = add_cache(&options.cachedata, SEND,

                    check_ip_tree(DIR_UNKNOWN, ip_hdr->ip_src.s_addr));

            } else {

                cache_result = add_cache(&options.cachedata, SEND,

                    check_ip6_tree(DIR_UNKNOWN, &ip6_hdr->ip_src));

            }

            break;

        default:

            errx(-1, "Whops!  What mode are we in anyways? %d", options.mode);

        }

    return packetnum;

}

/*

*从上面代码的实现可以看到,auto模式的(包括bridge,router,client,server,first)都需要两次处理,第一次根据pcap生成一颗树,第二次根据这棵树生成缓存。非auto模式的只需要执行一次,逐个解析pcap的每个packet,得到结果后马上写入缓存

*/

Auto-bridge算法最终归于对红黑树的操作,包括addtree,processtree,calcutree,checkiptree等操作,auto-router涉及tree2cidr,checkipcidr操作

 

/*

*下面函数实现了将一个packet解析后变成红黑树一个node的方法

*/

tcpr_tree_t *

packet2tree(const u_char * data)

{

    tcpr_tree_t *node = NULL;

    eth_hdr_t *eth_hdr = NULL;

    ipv4_hdr_t ip_hdr;

    ipv6_hdr_t ip6_hdr;

    tcp_hdr_t tcp_hdr;

    udp_hdr_t udp_hdr;

    icmpv4_hdr_t icmp_hdr;

    dnsv4_hdr_t dnsv4_hdr;

    u_int16_t ether_type;

    u_char proto = 0;

    int hl = 0;

 

    node = new_tree();

    eth_hdr = (eth_hdr_t *) (data);/*将data存放在eth_hdr结构体中*/

    /* prevent issues with byte alignment, must memcpy */

    memcpy(&ether_type, (u_char*)eth_hdr + 12, 2);/*取出 ether_type*/

 

/*下面判断ether_type的类型,做不同操作*/

    /* drop VLAN info if it exists before the IP info */

    if (ether_type == htons(ETHERTYPE_VLAN)) {

       dbg(4,"Processing as VLAN traffic...");

       /* prevent issues with byte alignment, must memcpy */

       memcpy(&ether_type, (u_char*)eth_hdr + 16, 2);

       hl += 4;

    }

    if (ether_type == htons(ETHERTYPE_IP)) {

        memcpy(&ip_hdr, (data + TCPR_ETH_H + hl), TCPR_IPV4_H);/*取IP头*/

        node->family = AF_INET;

        node->u.ip = ip_hdr.ip_src.s_addr;/*node存放的IP是源IP*/

        proto = ip_hdr.ip_p;/*proto 存放连接层的协议,是下面判断流向的基础*/

        hl += ip_hdr.ip_hl * 4;

    } else if (ether_type == htons(ETHERTYPE_IP6)) {

        memcpy(&ip6_hdr, (data + TCPR_ETH_H + hl), TCPR_IPV6_H);

        node->family = AF_INET6;

        node->u.ip6 = ip6_hdr.ip_src;

        proto = ip6_hdr.ip_nh; /*proto 存放连接层的协议,是下面判断流向的基础*/

        hl += TCPR_IPV6_H;

    } else {

       dbgx(2,"Unrecognized ether_type (%x)", ether_type);

    }

    /* copy over the source mac */

    strncpy((char *)node->mac, (char *)eth_hdr->ether_shost, 6);

/*下面处理 TCP 的情况*/

    if (proto == IPPROTO_TCP) {

        /* memcpy it over to prevent alignment issues */

        memcpy(&tcp_hdr, (data + TCPR_ETH_H + hl), TCPR_TCP_H);

        /* ftp-data is going to skew our results so we ignore it */

        if (tcp_hdr.th_sport == 20)

            return (node);

 

        /* set TREE->type based on TCP flags */

        if (tcp_hdr.th_flags == TH_SYN) {

            node->type = DIR_CLIENT;

          }

else if (tcp_hdr.th_flags == (TH_SYN | TH_ACK)) {

            node->type = DIR_SERVER;

          }

        else {

            dbg(3, "is an unknown");

        }

}

/*下面处理 UDP 的情况*/

    else if (proto == IPPROTO_UDP) {

        /* memcpy over to prevent alignment issues */

        memcpy(&udp_hdr, (data + TCPR_ETH_H + hl), TCPR_UDP_H);

 

        switch (ntohs(udp_hdr.uh_dport)) {/*由目的端口判断是dns协议的情况*/

        case 0x0035:           /* dns */

            /* prevent memory alignment issues */

            memcpy(&dnsv4_hdr,

                   (data + TCPR_ETH_H + hl + TCPR_UDP_H), TCPR_DNS_H);

 

            if (dnsv4_hdr.flags & DNS_QUERY_FLAG) {

                /* bit set, response */

                node->type = DIR_SERVER;

            }

            else {

                /* bit not set, query */

                node->type = DIR_CLIENT;

            }

            return (node);

            break;

        default:

            break;

        }

switch (ntohs(udp_hdr.uh_sport)) {/*由源端口判断是dns协议的情况*/

        case 0x0035:           /* dns */

            /* prevent memory alignment issues */

            memcpy(&dnsv4_hdr,

                   (data + TCPR_ETH_H + hl + TCPR_UDP_H),

                   TCPR_DNS_H);

              /*通过检查特定标志位的值,判断是哪个流向*/

            if ((dnsv4_hdr.flags & 0x7FFFF) ^ DNS_QUERY_FLAG) {

                node->type = DIR_SERVER;

            }

            else {

                node->type = DIR_CLIENT;

             }

            return (node);

            break;

        default:

 

            dbgx(3, "unknown UDP protocol: %hu->%hu", udp_hdr.uh_sport,

                udp_hdr.uh_dport);

            break;

        }

    }

/*下面处理 ICMP的情况*/

    else if (proto == IPPROTO_ICMP) {

        /* prevent alignment issues */

        memcpy(&icmp_hdr, (data + TCPR_ETH_H + hl), TCPR_ICMPV4_H);

        /* if port unreachable, then source == server, dst == client  */

        if ((icmp_hdr.icmp_type == ICMP_UNREACH) &&

            (icmp_hdr.icmp_code == ICMP_UNREACH_PORT)) {

            node->type = DIR_SERVER;

            dbg(3, "is a server with a closed port");

}

    }

    return (node);

}

/*

*从函数实现可以看出,基本上是根据协议规范解析packet,通过检测特定协议的特定标志位的值来判断该packet的流向

*/

 

1.1.1  实验结果

1.1.1.1  实验1

1.1.1.1  实验2

下面是使用 auto=router的实验情况:

1.1.1.1  实验3

 

本文使用了wireshark在局域网中随机抓取了一个包,使用auto=router拆分成功,准确率一般。结果如下:

1.1.1  发现该算法的一个问题

算法简单回顾:整个pcap包含的packet的源IP都被整合进入一颗红黑树,然后遍历整棵红黑树,算每个节点的比例,得出结果是C还是S。使用的时候,通过取packet的IP,看它是在红黑树的哪个节点,拿那个节点的值。

问题:

假设有一种情况,一个IP同时作为客户端和服务器(在本机上架设一个webserver,然后用本机的浏览器请求页面),这种情况下,本机IP事实上同时是C和S,但根据tcpprep的红黑树算法,它的最终结果要么是C要么是S。

 

原文地址:https://www.cnblogs.com/jiayy/p/tcpreplay.html