当前位置:   article > 正文

【Linux编程】Linux环境下tcpdump源代码分析_tcpdump源码

tcpdump源码

Linux环境下tcpdump源代码分析

  1. tcpdump.c 是tcpdump 工具的main.c, 本文旨对tcpdump的框架有简单了解,只展示linux平台使用的一部分核心代码。
  2. Tcpdump 的使用目的就是打印出指定条件的报文,即使有再多的正则表达式作为过滤条件。所以只要懂得tcpdump -nXXi eth0 的实现原理即可。
  3. 进入main之前,先看一些头文件
  4. netdissect.h里定义了一个数据结构struct netdissect_options来描述tcdpump支持的所有参数动作,每一个参数有对应的flag, 在tcpdump 的main 里面, 会根据用户的传入的参数来增加相应flag数值, 最后根据这些flag数值来实现特定动作。各个参数含义请参考源代码注释。
  5. struct netdissect_options {
  6.   int ndo_aflag;        /* translate network and broadcast addresses */
  7.   //打印出以太网头部
  8.   int ndo_eflag;        /* print ethernet header */
  9.   int ndo_fflag;        /* don't translate "foreign" IP address */
  10.   int ndo_Kflag;        /* don't check TCP checksums */
  11.   //不将地址转换为名字
  12.   int ndo_nflag;        /* leave addresses as numbers */
  13.   int ndo_Nflag;        /* remove domains from printed host names */
  14.   int ndo_qflag;        /* quick (shorter) output */
  15.   int ndo_Rflag;        /* print sequence # field in AH/ESP*/
  16.   int ndo_sflag;        /* use the libsmi to translate OIDs */
  17.   int ndo_Sflag;        /* print raw TCP sequence numbers */
  18.   // 报文到达时间
  19.   int ndo_tflag;        /* print packet arrival time */
  20.   int ndo_Uflag;        /* "unbuffered" output of dump files */
  21.   int ndo_uflag;        /* Print undecoded NFS handles */
  22.   //详细信息
  23.   int ndo_vflag;        /* verbose */
  24.   // 十六进制打印报文
  25.   int ndo_xflag;        /* print packet in hex */
  26.   // 十六进制和ASCII码打印报文
  27.   int ndo_Xflag;        /* print packet in hex/ascii */
  28.   //以ASCII码显示打印报文
  29.   int ndo_Aflag;        /* print packet only in ascii observing TAB,
  30.                  * LF, CR and SPACE as graphical chars
  31.                  */
  32. ...
  33.    //默认的打印函数
  34.   void (*ndo_default_print)(netdissect_options *,
  35.               register const u_char *bp, register u_int length);
  36.   void (*ndo_info)(netdissect_options *, int verbose);
  37. ...
  38. }
  39. interface.h   接口头文件,定义了一堆宏就为了方便调用struct netdissect_options里的成员。
  40. #ifndef NETDISSECT_REWORKED
  41. extern netdissect_options *gndo;
  42. ...
  43. #define nflag gndo->ndo_nflag 
  44. ...
  45. #define tflag gndo->ndo_tflag 
  46. ...
  47. #define vflag gndo->ndo_vflag 
  48. #define xflag gndo->ndo_xflag 
  49. #define Xflag gndo->ndo_Xflag 
  50. ...
  51.       
  52. #endif                                         
  53. tcpdump.c 
  54. int
  55. main(int argc, char **argv)
  56. register char *cp, *infile, *cmdbuf, *device, *RFileName, *WFileName;
  57.     pcap_handler callback;
  58.     int type;          
  59.     struct bpf_program fcode;
  60.                
  61.     struct print_info printinfo;
  62. ...
  63.     //对netdissect_options中一些参数初始化
  64.     gndo->ndo_Oflag=1;
  65.     gndo->ndo_Rflag=1;
  66.     gndo->ndo_dlt=-1;
  67.     gndo->ndo_default_print=ndo_default_print;
  68.     gndo->ndo_printf=tcpdump_printf;
  69.     gndo->ndo_error=ndo_error;
  70.     gndo->ndo_warning=ndo_warning;
  71.     gndo->ndo_snaplen = DEFAULT_SNAPLEN;
  72. ...                 
  73.     opterr = 0;     
  74.     while (  
  75. /*经典的getopt框架。 字符数组为tcpdump 支持的全部参数。可以看到, 参数x, X,t这些参数后面没有:或::, 这说明这些参数会产生叠加的效果。
  76. */      
  77.         (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)
  78.         switch (op) {
  79. ...
  80. //case 里面的处理大多相似,以下仅用-i,-X,-x做例。
  81.         //-i 参数用来指定网口
  82.         case 'i':  
  83.             if (optarg[0] == '0' && optarg[1] == 0)
  84.                 error("Invalid adapter index");
  85.    
  86.             device = optarg;
  87.             break;
  88.          
  89. //-x 为以十六进制打印报文,如使用-xx, xflag数值为2,后面根据xflag>1来打印出链路层头部 
  90.         case 'x':
  91.             ++xflag;
  92.             ++suppress_default_print;
  93.             break;
  94.                
  95.         case 'X':
  96.             ++Xflag;                                                                 
  97.             ++suppress_default_print;
  98.             break;
  99.      
  100.         
  101. //case 'n', case 'A'等操作类似如上                 
  102. ...
  103. }
  104. ...
  105. /*展开核心代码前处理信号,信号处理函数cleanup会调用info()来打印当用户按ctrl+c等发送中止信号时tcpdump显示已处理报文的统计信息。
  106. 3 packets captured
  107. 3 packets received by filter
  108. 0 packets dropped by kernel
  109. */
  110.     (void)setsignal(SIGPIPE, cleanup);
  111.     (void)setsignal(SIGTERM, cleanup);
  112.     (void)setsignal(SIGINT, cleanup);                                                               
  113.     (void)setsignal(SIGCHLD, child_cleanup);
  114. ...
  115. //从 -r 参数读取指定文件, 在此忽略
  116. if (RFileName != NULL) {
  117. ...
  118.     } else {
  119.       //如果没有-i 参数来指定网络接口, 那么调用 pcap_lookupdev()来寻找可用的网络接口
  120.         if (device == NULL) {
  121.             device = pcap_lookupdev(ebuf);
  122.             if (device == NULL)
  123.                 error("%s", ebuf);
  124.         }
  125. /*pcap_open_live() 定义为:
  126. pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf) 
  127. device为要打开的指定设备
  128. snaplen为最大报文长度, 由-s 指定. 默认为65536. 
  129. Promise 为是否要将网口配置为混杂模式, 由-p 指定,!Pflag:默认为是。  
  130. to_ms 为超时时间。 *ebuf 为传递错误信息使用。 
  131. 函数返回捕获报文的句柄。
  132. */
  133.         *ebuf = '\0';
  134.         pd = pcap_open_live(device, snaplen, !pflag, 1000, ebuf);                                                                  
  135.         if (pd == NULL)
  136.             error("%s", ebuf);
  137.         else if (*ebuf)
  138.             warning("%s", ebuf);
  139. // -w 参数 加结果写入一个文件, 在此忽略
  140.  if (WFileName) {
  141. ...
  142.     } else {
  143.         //返回数据链路层的枚举值
  144.         type = pcap_datalink(pd);
  145.     
  146.     printinfo.printer = lookup_printer(type);
  147.  
  148. /*lookup_printer() 作用如下:根据该数据链路层类型返回相应的打印函数指针。定义如下:
  149. static if_printer
  150.  lookup_printer(int type)
  151. {              
  152.     struct printer *p;
  153.  
  154.     for (p = printers; p->f; ++p)                                                                
  155.         if (type == p->type)
  156.             return p->f;
  157.       
  158.     return NULL;
  159. }              
  160.  
  161. 其中struct printer定义为 一个打印函数指针, 一个类型数值
  162. typedef u_int (*if_printer)(const struct pcap_pkthdr *, const u_char *);
  163. struct printer {  
  164.     if_printer f; 
  165.     int type;     
  166. };
  167. printers 为一个struct printer数组, 定义如下: 
  168. static struct printer printers[] ={
  169.     { arcnet_if_print,  DLT_ARCNET },
  170.     { ether_if_print,   DLT_EN10MB },                                                                  
  171.     { token_if_print,   DLT_IEEE802 },
  172. ...
  173. 由上可以看到, 当为以太网环境(DLT_EN10MB)时,实现函数为ether_if_print,
  174. 当为IEEE802令牌环网环境时, 实现函数为 token_if_print。
  175. 等等。 不同数据链路层环境有不同的调用函数来实现打印特定格式的报文。 
  176. for (p = printers; p->f; ++p)  : 从数组首个元素开始,循环条件是元素存在f指针,依次遍历全部数组成员。 
  177. 所以当数据链路层的类型为DLT_EN10MB时, 对应的打印函数为ether_if_print。
  178. 我本人觉得 lookup_printer() 这个函数写得甚是巧妙。 非常值得借鉴。 每一种类型定义一个数据结构struct printer, 包含一个函数指针和一个类型值。 将全部的类型放入一个数组中,遍历数组时根据类型值返回对应的函数指针, 再有新类型时,仅将其添加到数组中即可。 
  179. */
  180.        if (printinfo.printer == NULL) {
  181.             gndo->ndo_dltname = pcap_datalink_val_to_name(type);
  182.             if (gndo->ndo_dltname != NULL)
  183.                 error("unsupported data link type %s",
  184.                       gndo->ndo_dltname);
  185.             else 
  186.                 error("unsupported data link type %d", type);
  187.         }
  188. //函数指针callback指向print_packet   
  189.         callback = print_packet;
  190. //将printinfo作为unsigned char * 赋值给pcap_usrdata, 在后面作为pcap_loop()的参数
  191.        pcap_userdata = (u_char *)&printinfo;
  192.     }  
  193.     if (RFileName == NULL) { 
  194.         int dlt;             
  195.         const char *dlt_name;
  196.     
  197. ...
  198.         /*pcap_datalink() 返回数据链路层类型枚举值,这里返回DLT_EN10MB */
  199.         dlt = pcap_datalink(pd);
  200.         //根据该枚举返回数据链路类型char *name: “EN10MB”
  201.         dlt_name = pcap_datalink_val_to_name(dlt);
  202.         if (dlt_name == NULL) {
  203.             (void)fprintf(stderr, "listening on %s, link-type %u, capture size %u bytes\n",
  204.                 device, dlt, snaplen);
  205.         } else {             
  206.             (void)fprintf(stderr, "listening on %s, link-type %s (%s), capture size %u bytes\n",
  207.                 device, dlt_name,
  208.                //获取该数据链路层类型的字符串描述
  209.                 pcap_datalink_val_to_description(dlt), snaplen);
  210.         }                    
  211. /*
  212. 使用tcpdump -nXXi eth0
  213.  后,打印信息:
  214. listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
  215. 即来源于此。 
  216. */
  217.     /*调用 pcap_loop(), 循环捕获报文并将报文交给callback处理,直到遇到错误或退出信号。 
  218. Cnt 为 -c 参数指定,默认0. Usrdata 作为callback 的参数。 
  219. pcap_loop() 是libpcap 提供的API,它完成了与底层驱动的通信,首先创建了一个socket,将句柄封装后交给底层驱动,驱动收到数据包后将其写入socket, 从内核层发往用户层, 用户层的pcap_loop()持续poll 这个socket , 发现其有数据后就将数据交给callback函数处理,进行打印。 这样做的优点是可直接使用linux的既有socket IPC架构, 缺点是要承受在高速的以太网环境里,从内核层到用户层的拷贝动作产生的开销代价。
  220.      */
  221.     status = pcap_loop(pd, cnt, callback, pcap_userdata);
  222.     ...
  223.     pcap_close(pd);
  224. /*
  225. 由上面看到, callback 的实现函数为print_packet(), pcap_loop()调用callpack 时传给print_packet()三个参数,第一个为含有特定链路层打印函数的结构体pcap_userdata, 第二个为包含报文信息的 struct pcap_pkthdr 常量指针, 第三个为数据包内容的字符串常量指针。 
  226. 其中struct pcap_pkthdr 定义为:   
  227.    
  228. struct pcap_pkthdr{
  229.       struct timeval ts;   //时间戳数据结构
  230.       bpf_u_int32 caplen;  //报文捕获长度
  231.       bpf_u_int32 len;     //报文实际长度
  232. }
  233. 注: 如一个报文实际长度100B, 但tcpdump捕获80B时停止, 那么caplen 为80, len 为 100。 
  234. static void
  235. print_packet(u_char *user, const struct pcap_pkthdr *h, const u_char *sp)
  236. {   
  237.     struct print_info *print_info;
  238.     u_int hdrlen;
  239.     
  240.     ++packets_captured;
  241.     
  242.     ++infodelay;
  243.     ts_print(&h->ts);
  244.     
  245.    /*取得参数user 的数据结构, 后面(*print_info->printer)即调用user提供的打印函数,
  246.      这里为ether_if_print()
  247.    */
  248.     print_info = (struct print_info *)user;
  249.     snapend = sp + h->caplen;
  250.     
  251.     //调用ether_if_print()
  252.     hdrlen = (*print_info->printer)(h, sp);
  253.     if (Xflag) {
  254. //当tcpdump 有多个X参数时, 如 tcpdump -XX 时, 以十六进制和ASCII码打印出链路层头部信息
  255.         if (Xflag > 1) {
  256.             hex_and_ascii_print("\n\t", sp, h->caplen);
  257.         } else {
  258.         //只有一个X参数,即tcpdump -X 时,不打印链路层头部
  259.             if (h->caplen > hdrlen)
  260.                 hex_and_ascii_print("\n\t", sp + hdrlen,
  261. h->caplen - hdrlen);
  262.         }
  263.     } else if (xflag) {
  264. //同-X, 当存在多个-x 参数,如tcpdump -xx 时, 打印链路层头部, 但只以十六进制打印
  265.         if (xflag > 1) {
  266.             hex_print("\n\t", sp, h->caplen);
  267.         } else {
  268.             if (h->caplen > hdrlen)
  269.                 hex_print("\n\t", sp + hdrlen,
  270. h->caplen - hdrlen);
  271.         }
  272.     } else if (Aflag) {
  273. //-A 参数, 以ASCII码打印报文信息
  274.         if (Aflag > 1) {
  275.                               
  276.             ascii_print(sp, h->caplen);
  277.         } else {
  278.             if (h->caplen > hdrlen)
  279.                 ascii_print(sp + hdrlen, h->caplen - hdrlen);
  280.         }
  281.     }
  282.     
  283.     putchar('\n');
  284.     
  285.     --infodelay;
  286.     if (infoprint)
  287.         info(0);
  288. }
  289. */
  290. /*
  291. 在print-ether.c里, 有ether_if_print 的定义, 同样的, 在print-token.c 里有token_if_print的定义, print-arcnet.c里有arcnet_if_print的定义。Tcpdump 目录里大量的 “print-” 开头的文件均是特定的打印函数。
  292. print-ether.c
  293. u_int
  294. ether_if_print(const struct pcap_pkthdr *h, const u_char *p)
  295. {   
  296.     //将报文内容, 报文捕获长度, 报文实际长度传给 ether_print
  297.     ether_print(p, h->len, h->caplen); 
  298. }
  299. ether_print定义:
  300. void
  301. ether_print(const u_char *p, u_int length, u_int caplen)
  302. {   
  303.     struct ether_header *ep;
  304. /*
  305. 以太网头部定义
  306. #define ETHER_HDRLEN        14                 //头部长14字节
  307. #define ETHER_ADDR_LEN      6
  308. struct  ether_header {                                                                 
  309.     u_int8_t    ether_dhost[ETHER_ADDR_LEN];
  310.         //DMAC, 6字节
  311.     u_int8_t    ether_shost[ETHER_ADDR_LEN];
  312.         //SMAC, 6字节
  313.     u_int16_t   ether_type;
  314.                     //type, 2字节
  315. }; 
  316. */
  317.     u_short ether_type;
  318.     u_short extracted_ether_type;
  319.     
  320.     if (caplen < ETHER_HDRLEN) {
  321.         printf("[|ether]");
  322.         return;
  323.     }
  324.     
  325.     /*如果有 -e参数,打印链路层头部,调用 ether_hdr_print() ,定义见下方。
  326.    */
  327.    if (eflag)
  328.         ether_hdr_print(p, length);
  329.     
  330.     length -= ETHER_HDRLEN;
  331.     caplen -= ETHER_HDRLEN;
  332.     ep = (struct ether_header *)p;
  333.     p += ETHER_HDRLEN;
  334.     
  335.     ether_type = ntohs(ep->ether_type);
  336. //具体的打印细节不做研究了
  337.     if (ether_type <= ETHERMTU) {
  338.         /* Try to print the LLC-layer header & higher layers */
  339.         if (llc_print(p, length, caplen, ESRC(ep), EDST(ep),
  340.             &extracted_ether_type) == 0) {
  341.             if (!eflag)
  342.                 ether_hdr_print((u_char *)ep, length + ETHER_HDRLEN);
  343.             if (!suppress_default_print)
  344.                 default_print(p, caplen);
  345.         }
  346.     } else if (ether_encap_print(ether_type, p, length, caplen,
  347.         &extracted_ether_type) == 0) {  
  348.         if (!eflag)
  349.             ether_hdr_print((u_char *)ep, length + ETHER_HDRLEN);
  350.    
  351.         if (!suppress_default_print)
  352.             default_print(p, caplen);
  353.     } 
  354. */
  355. /*
  356. 使用 tcpdump -nei eth0 会有如下显示: 
  357. 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
  358. ether_hdr_print 定义:
  359. static inline void
  360. ether_hdr_print(register const u_char *bp, u_int length)
  361. {
  362.     register const struct ether_header *ep;
  363.     ep = (const struct ether_header *)bp;
  364.    
  365.     //打印出 原MAC > 目的MAC, 比如上面的  d0:df:9a:53:f0:07 > 01:00:5e:7f:ff:fa
  366.     (void)printf("%s > %s",
  367.              etheraddr_string(ESRC(ep)),
  368.              etheraddr_string(EDST(ep)));
  369.    
  370.     //如果没有-q 参数, 
  371.     if (!qflag) {
  372.             if (ntohs(ep->ether_type) <= ETHERMTU)
  373.                   (void)printf(", 802.3");
  374.                 else 
  375. //打印出协议类型, 如上面的ethertype IPv4 (0x0800)
  376.                   (void)printf(", ethertype %s (0x%04x)",
  377.                        tok2str(ethertype_values,"Unknown", ntohs(ep->ether_type)),
  378. ntohs(ep->ether_type));        
  379.         } else {
  380.                 if (ntohs(ep->ether_type) <= ETHERMTU)
  381.                           (void)printf(", 802.3");
  382.                 else 
  383.                           (void)printf(", %s", tok2str(ethertype_values,"Unknown Ethertype (0x%04x)", ntohs(ep->ether_type)));  
  384.         }   
  385.     //打印出报文长度, 如上面的length 175   
  386.     (void)printf(", length %u: ", length);
  387. }  
  388. */
  389. 总结:
  390.   
  391.  概括地看, tcpdump.c 可分三个部分:
  392.  第一部分是用struct netdissect_options数据结构作为一个参数集合, 并用getopt框架来处理argv的参数逻辑。 
  393.  第二部分是使用libpcap库函数来搭建与底层IPC通道。 其中最重要的API有三个, 第一个是pcap_lookupdev(), 查找可用网口,第二个是pcap_open_live(),打开指定设备并将其配置为混杂模式返回句柄, 第三个是使用pcap_loop()持续获取报文数据,调用回调函数进行打印处理。
  394.  第三部分是实现callback 函数,tcpdump.c里的callback函数只做了一个封装,最终调用的是参数pcap_userdata里提供的特定数据链路层的打印函数, 这个函数指针的查找是由lookup_printer()实现的。
  395. ————————————————
  396. 版权声明:本文为CSDN博主「韩大卫」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
  397. 原文链接:https://blog.csdn.net/han_dawei/article/details/12615199
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/559992
推荐阅读
相关标签
  

闽ICP备14008679号