linux环境下tcpdump源代码分析

Linux 环境下tcpdump 源代码分析





韩大卫@吉林师范大学





tcpdump.c tcpdump 工具的main.c, 本文旨对tcpdump的框架有简单了解,只展示linux平台使用的一部分核心代码。



Tcpdump 的使用目的就是打印出指定条件的报文,即使有再多的正则表达式作为过滤条件。所以只要懂得tcpdump -nXXi eth0 的实现原理即可。



进入main之前,先看一些头文件



netdissect.h里定义了一个数据结构struct netdissect_options来描述tcdpump支持的所有参数动作,每一个参数有对应的flag, tcpdump main 里面, 会根据用户的传入的参数来增加相应flag数值, 最后根据这些flag数值来实现特定动作。各个参数含义请参考源代码注释。



struct netdissect_options {

  int ndo_aflag;        /* translate network and broadcast addresses */

  //打印出以太网头部

  int ndo_eflag;        /* print ethernet header */

  int ndo_fflag;        /* don't translate "foreign" IP address */

  int ndo_Kflag;        /* don't check TCP checksums */

  //不将地址转换为名字

  int ndo_nflag;        /* leave addresses as numbers */

  int ndo_Nflag;        /* remove domains from printed host names */

  int ndo_qflag;        /* quick (shorter) output */

  int ndo_Rflag;        /* print sequence # field in AH/ESP*/

  int ndo_sflag;        /* use the libsmi to translate OIDs */

  int ndo_Sflag;        /* print raw TCP sequence numbers */

  // 报文到达时间

  int ndo_tflag;        /* print packet arrival time */

  int ndo_Uflag;        /* "unbuffered" output of dump files */

  int ndo_uflag;        /* Print undecoded NFS handles */

  //详细信息

  int ndo_vflag;        /* verbose */

  // 十六进制打印报文

  int ndo_xflag;        /* print packet in hex */

  // 十六进制和ASCII码打印报文

  int ndo_Xflag;        /* print packet in hex/ascii */

  //ASCII码显示打印报文

  int ndo_Aflag;        /* print packet only in ascii observing TAB,

                 * LF, CR and SPACE as graphical chars

                 */

...

   //默认的打印函数

  void (*ndo_default_print)(netdissect_options *,

              register const u_char *bp, register u_int length);

  void (*ndo_info)(netdissect_options *, int verbose);

...

}



interface.h   接口头文件,定义了一堆宏就为了方便调用struct netdissect_options里的成员。



#ifndef NETDISSECT_REWORKED

extern netdissect_options *gndo;

...

#define nflag gndo->ndo_nflag 

...

#define tflag gndo->ndo_tflag 

...

#define vflag gndo->ndo_vflag 

#define xflag gndo->ndo_xflag 

#define Xflag gndo->ndo_Xflag 

...

      

#endif                                         





tcpdump.c 





int

main(int argc, char **argv)

{ 

register char *cp, *infile, *cmdbuf, *device, *RFileName, *WFileName;

    pcap_handler callback;

    int type;          

    struct bpf_program fcode;

               

    struct print_info printinfo;

...

    //netdissect_options中一些参数初始化

    gndo->ndo_Oflag=1;

    gndo->ndo_Rflag=1;

    gndo->ndo_dlt=-1;

    gndo->ndo_default_print=ndo_default_print;

    gndo->ndo_printf=tcpdump_printf;

    gndo->ndo_error=ndo_error;

    gndo->ndo_warning=ndo_warning;

    gndo->ndo_snaplen = DEFAULT_SNAPLEN;

...                 

    opterr = 0;     

    while (  

/*经典的getopt框架。 字符数组为tcpdump 支持的全部参数。可以看到, 参数x, X,t这些参数后面没有:或::, 这说明这些参数会产生叠加的效果。

*/      

        (op = getopt(argc, argv, "aA" B_FLAG "c:C:d" D_FLAG "eE:fF:G:i:" I_FLAG "KlLm:M:nNOpqr:Rs:StT:u" U_FLAG "vw:W:xXy:Yz:Z:")) != -1)

        switch (op) {

...

//case 里面的处理大多相似,以下仅用-i,-X,-x做例。

        //-i 参数用来指定网口

        case 'i':  

            if (optarg[0] == '0' && optarg[1] == 0)

                error("Invalid adapter index");

   



            device = optarg;



            break;

         

…

//-x 为以十六进制打印报文,如使用-xxxflag数值为2,后面根据xflag>1来打印出链路层头部 

        case 'x':

            ++xflag;

            ++suppress_default_print;

            break;

               

        case 'X':

            ++Xflag;                                                                 

            ++suppress_default_print;

            break;

 	

        

//case 'n', case 'A'等操作类似如上                 

...



}

...

/*展开核心代码前处理信号,信号处理函数cleanup会调用info()来打印当用户按ctrl+c等发送中止信号时tcpdump显示已处理报文的统计信息。

3 packets captured

3 packets received by filter

0 packets dropped by kernel

*/

    (void)setsignal(SIGPIPE, cleanup);

    (void)setsignal(SIGTERM, cleanup);

    (void)setsignal(SIGINT, cleanup);                                                               

    (void)setsignal(SIGCHLD, child_cleanup);

...



//-r 参数读取指定文件, 在此忽略

if (RFileName != NULL) {

...

    } else {

      //如果没有-i 参数来指定网络接口, 那么调用 pcap_lookupdev()来寻找可用的网络接口

        if (device == NULL) {

            device = pcap_lookupdev(ebuf);

            if (device == NULL)

                error("%s", ebuf);

        }



/pcap_open_live() 定义为:

pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf) 

device为要打开的指定设备

snaplen为最大报文长度, 由-s 指定. 默认为65536. 

Promise 为是否要将网口配置为混杂模式, 由-p 指定,!Pflag:默认为是。  

to_ms 为超时时间。 *ebuf 为传递错误信息使用。 

函数返回捕获报文的句柄。

*/

        *ebuf = '\0';

        pd = pcap_open_live(device, snaplen, !pflag, 1000, ebuf);                                                                  

        if (pd == NULL)

            error("%s", ebuf);

        else if (*ebuf)

            warning("%s", ebuf);





// -w 参数 加结果写入一个文件, 在此忽略

 if (WFileName) {

...

    } else {

        //返回数据链路层的枚举值

        type = pcap_datalink(pd);

    

printinfo.printer = lookup_printer(type);

 

/*lookup_printer() 作用如下:根据该数据链路层类型返回相应的打印函数指针。定义如下:



static if_printer

 lookup_printer(int type)

{              

    struct printer *p;

 

    for (p = printers; p->f; ++p)                                                                

        if (type == p->type)

            return p->f;

      

    return NULL;

}              

 

其中struct printer定义为 一个打印函数指针, 一个类型数值

typedef u_int (*if_printer)(const struct pcap_pkthdr *, const u_char *);

struct printer {  

    if_printer f; 

    int type;     

};

printers 为一个struct printer数组, 定义如下: 

static struct printer printers[] =

    { arcnet_if_print,  DLT_ARCNET },

    { ether_if_print,   DLT_EN10MB },                                                                  

    { token_if_print,   DLT_IEEE802 },

...



由上可以看到, 当为以太网环境(DLT_EN10MB)时,实现函数为ether_if_print,

当为IEEE802令牌环网环境时, 实现函数为 token_if_print

等等。 不同数据链路层环境有不同的调用函数来实现打印特定格式的报文。 



for (p = printers; p->f; ++p)  : 从数组首个元素开始,循环条件是元素存在f指针,依次遍历全部数组成员。 

所以当数据链路层的类型为DLT_EN10MB时, 对应的打印函数为ether_if_print



我本人觉得 lookup_printer() 这个函数写得甚是巧妙。 非常值得借鉴。 每一种类型定义一个数据结构struct printer, 包含一个函数指针和一个类型值。 将全部的类型放入一个数组中,遍历数组时根据类型值返回对应的函数指针, 再有新类型时,仅将其添加到数组中即可。 

*/

       if (printinfo.printer == NULL) {

            gndo->ndo_dltname = pcap_datalink_val_to_name(type);

            if (gndo->ndo_dltname != NULL)

                error("unsupported data link type %s",

                      gndo->ndo_dltname);

            else 

                error("unsupported data link type %d", type);

        }

//函数指针callback指向print_packet   

        callback = print_packet;



//printinfo作为unsigned char * 赋值给pcap_usrdata, 在后面作为pcap_loop()的参数

       pcap_userdata = (u_char *)&printinfo;

    }  



    if (RFileName == NULL) { 

        int dlt;             

        const char *dlt_name;

    

...

        /*pcap_datalink() 返回数据链路层类型枚举值,这里返回DLT_EN10MB */

        dlt = pcap_datalink(pd);

        //根据该枚举返回数据链路类型char *name: “EN10MB”

        dlt_name = pcap_datalink_val_to_name(dlt);

        if (dlt_name == NULL) {

            (void)fprintf(stderr, "listening on %s, link-type %u, capture size %u bytes\n",

                device, dlt, snaplen);

        } else {             

            (void)fprintf(stderr, "listening on %s, link-type %s (%s), capture size %u bytes\n",



                device, dlt_name,

               //获取该数据链路层类型的字符串描述

                pcap_datalink_val_to_description(dlt), snaplen);

        }                    



/*

使用tcpdump -nXXi eth0

 后,打印信息:

listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes

即来源于此。 

*/

    /*调用 pcap_loop(), 循环捕获报文并将报文交给callback处理,直到遇到错误或退出信号。 

Cnt -c 参数指定,默认0. Usrdata 作为callback 的参数。 

pcap_loop() libpcap 提供的API,它完成了与底层驱动的通信,首先创建了一个socket,将句柄封装后交给底层驱动,驱动收到数据包后将其写入socket, 从内核层发往用户层, 用户层的pcap_loop()持续poll 这个socket , 发现其有数据后就将数据交给callback函数处理,进行打印。 这样做的优点是可直接使用linux的既有socket IPC架构, 缺点是要承受在高速的以太网环境里,从内核层到用户层的拷贝动作产生的开销代价。

     */

    status = pcap_loop(pd, cnt, callback, pcap_userdata);

	...

	pcap_close(pd);

/*

由上面看到, callback 的实现函数为print_packet()pcap_loop()调用callpack 时传给print_packet()三个参数,第一个为含有特定链路层打印函数的结构体pcap_userdata, 第二个为包含报文信息的 struct pcap_pkthdr 常量指针, 第三个为数据包内容的字符串常量指针。 



其中struct pcap_pkthdr 定义为:   

   

struct pcap_pkthdr{

      struct timeval ts;   //时间戳数据结构

      bpf_u_int32 caplen;  //报文捕获长度

      bpf_u_int32 len;     //报文实际长度

}

注: 如一个报文实际长度100B, 但tcpdump捕获80B时停止, 那么caplen 80, len 100



static void

print_packet(u_char *user, const struct pcap_pkthdr *h, const u_char *sp)

{   

    struct print_info *print_info;

    u_int hdrlen;

    

    ++packets_captured;

    

    ++infodelay;

    ts_print(&h->ts);

    

   /*取得参数user 的数据结构, 后面(*print_info->printer)即调用user提供的打印函数,

     这里为ether_if_print()

   */

    print_info = (struct print_info *)user;





    snapend = sp + h->caplen;

    

    //调用ether_if_print()

    hdrlen = (*print_info->printer)(h, sp);



    if (Xflag) {

//tcpdump 有多个X参数时, 如 tcpdump -XX 时, 以十六进制和ASCII码打印出链路层头部信息

        if (Xflag > 1) {

            hex_and_ascii_print("\n\t", sp, h->caplen);



        } else {

        //只有一个X参数,即tcpdump -X 时,不打印链路层头部

            if (h->caplen > hdrlen)

                hex_and_ascii_print("\n\t", sp + hdrlen,

h->caplen - hdrlen);



        }

    } else if (xflag) {

//-X, 当存在多个-x 参数,如tcpdump -xx 时, 打印链路层头部, 但只以十六进制打印

        if (xflag > 1) {

            hex_print("\n\t", sp, h->caplen);



        } else {

            if (h->caplen > hdrlen)

                hex_print("\n\t", sp + hdrlen,

h->caplen - hdrlen);



        }

    } else if (Aflag) {

//-A 参数, 以ASCII码打印报文信息

        if (Aflag > 1) {

                              

            ascii_print(sp, h->caplen);

        } else {

            if (h->caplen > hdrlen)

                ascii_print(sp + hdrlen, h->caplen - hdrlen);

        }

    }

    

    putchar('\n');

    

    --infodelay;

    if (infoprint)

        info(0);

}

*/



/*

print-ether.c里, 有ether_if_print 的定义, 同样的, 在print-token.c 里有token_if_print的定义, print-arcnet.c里有arcnet_if_print的定义。Tcpdump 目录里大量的 “print-” 开头的文件均是特定的打印函数。



print-ether.c



u_int

ether_if_print(const struct pcap_pkthdr *h, const u_char *p)

{   

    //将报文内容, 报文捕获长度, 报文实际长度传给 ether_print

    ether_print(p, h->len, h->caplen); 

}



ether_print定义:



void

ether_print(const u_char *p, u_int length, u_int caplen)

{   

    struct ether_header *ep;



/*

以太网头部定义

#define ETHER_HDRLEN        14 				//头部长14字节

#define ETHER_ADDR_LEN      6

struct  ether_header {                                                                 

    u_int8_t    ether_dhost[ETHER_ADDR_LEN];

		//DMAC, 6字节

    u_int8_t    ether_shost[ETHER_ADDR_LEN];

		//SMAC, 6字节

    u_int16_t   ether_type;

					//type, 2字节

}; 

*/



    u_short ether_type;

    u_short extracted_ether_type;

    

    if (caplen < ETHER_HDRLEN) {

        printf("[|ether]");

        return;

    }

    

    /*如果有 -e参数,打印链路层头部,调用 ether_hdr_print() ,定义见下方。

   */

   if (eflag)

        ether_hdr_print(p, length);

    

    length -= ETHER_HDRLEN;

    caplen -= ETHER_HDRLEN;

    ep = (struct ether_header *)p;

    p += ETHER_HDRLEN;

    

    ether_type = ntohs(ep->ether_type);

//具体的打印细节不做研究了

    if (ether_type <= ETHERMTU) {

        /* Try to print the LLC-layer header & higher layers */

        if (llc_print(p, length, caplen, ESRC(ep), EDST(ep),

            &extracted_ether_type) == 0) {

            if (!eflag)

                ether_hdr_print((u_char *)ep, length + ETHER_HDRLEN);

            if (!suppress_default_print)

                default_print(p, caplen);

        }

    } else if (ether_encap_print(ether_type, p, length, caplen,

        &extracted_ether_type) == 0) {  

        if (!eflag)

            ether_hdr_print((u_char *)ep, length + ETHER_HDRLEN);

   

        if (!suppress_default_print)

            default_print(p, caplen);

    } 

} 



*/





/*

使用 tcpdump -nei eth0 会有如下显示: 



12:53:12.189132 d0:df:9a:53:f0:07 > 01:00:5e:7f:ff:fa, ethertype IPv4 (0x0800), length 175: 10.10.168.94.60395 > 239.255.255.250.1900: UDP, length 133



ether_hdr_print 定义:


static inline void

ether_hdr_print(register const u_char *bp, u_int length)

{

    register const struct ether_header *ep;

    ep = (const struct ether_header *)bp;

   

    //打印出 原MAC > 目的MAC, 比如上面的  d0:df:9a:53:f0:07 > 01:00:5e:7f:ff:fa

    (void)printf("%s > %s",

             etheraddr_string(ESRC(ep)),

             etheraddr_string(EDST(ep)));

   

    //如果没有-q 参数, 

    if (!qflag) {

            if (ntohs(ep->ether_type) <= ETHERMTU)

                  (void)printf(", 802.3");

                else 

//打印出协议类型, 如上面的ethertype IPv4 (0x0800)

                  (void)printf(", ethertype %s (0x%04x)",

                       tok2str(ethertype_values,"Unknown", ntohs(ep->ether_type)),

ntohs(ep->ether_type));        

        } else {

                if (ntohs(ep->ether_type) <= ETHERMTU)

                          (void)printf(", 802.3");

                else 

                          (void)printf(", %s", tok2str(ethertype_values,"Unknown Ethertype (0x%04x)", ntohs(ep->ether_type)));  

        }   



    //打印出报文长度, 如上面的length 175   

    (void)printf(", length %u: ", length);

}  

*/





总结:

  

 概括地看, tcpdump.c 可分三个部分:

 第一部分是用struct netdissect_options数据结构作为一个参数集合, 并用getopt框架来处理argv的参数逻辑。 



 第二部分是使用libpcap库函数来搭建与底层IPC通道。 其中最重要的API有三个, 第一个是pcap_lookupdev(), 查找可用网口,第二个是pcap_open_live(),打开指定设备并将其配置为混杂模式返回句柄, 第三个是使用pcap_loop()持续获取报文数据,调用回调函数进行打印处理。



 第三部分是实现callback 函数,tcpdump.c里的callback函数只做了一个封装,最终调用的是参数pcap_userdata里提供的特定数据链路层的打印函数, 这个函数指针的查找是由lookup_printer()实现的。





关于pcap_open_live pcap_loop 这两个重要的函数源代码分析,后续介绍。





你可能感兴趣的:(tcpdump)