【转载】linux环境下tcpdump源代码分析

linux环境下tcpdump源代码分析
 原文时间 2013-10-11 13:13:02  
 原文链接  
 主题 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;        /* www.qixoo.qixoo.com/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 为以十六进制打印报文,如使用-xx, xflag数值为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 这两个重要的函数源代码分析,后续介绍。

时间: 2024-08-02 11:04:27

【转载】linux环境下tcpdump源代码分析的相关文章

[转载]Linux 环境下编译 0.11版本内核 kernel

最近在看<linux内 核0.11完全注释>一书,由于书中涉及汇编语言的地方众多,本人在大学时汇编语言学得一塌糊涂,所以实在看不下去了,头都大了只好匆匆看了个头尾(前面 几章和最后一章).看来即使有<九阴真经>这样的武功秘籍,内功不够也是修炼不出来神马来的.于是索性下了个0.11版本的kernel下来尝试编译一 把. linux-0.11.tar.gz 下载地址: 下面开始工作: 1. tar xvfz linux-0.11.tar.gz 2. cd linux-0.11 3. m

Linux环境下段错误的产生原因及调试方法小结(转载)

转载自http://www.cnblogs.com/panfeng412/archive/2011/11/06/2237857.html 最近在Linux环境下做C语言项目,由于是在一个原有项目基础之上进行二次开发,而且项目工程庞大复杂,出现了不少问题,其中遇到最多.花费时间 最长的问题就是著名的“段错误”(Segmentation Fault).借此机会系统学习了一下,这里对Linux环境下的段错误做个小结,方便以后同类问题的排查与解决. 1. 段错误是什么 一句话来说,段错误是指访问的内存超

深度分析LINUX环境下如何配置multi-path

首先介绍一下什么是多路径(multi-path)?先说说多路径功能产生的背景,在多路径功能出现之前,主机上的硬盘是直接挂接到一个总线(PCI)上,路径是一对一的关系,也就是一条路径指向一个硬盘或是存储设备,这样的一对一关系对于操作系统而言,处理相对简单,但是缺少了可靠性.当出现了光纤通道网络(Fibre Channle)也就是通常所说的SAN网络时,或者由iSCSI组成的IPSAN环境时,由于主机和存储之间通过光纤通道交换机或者多块网卡及IP来连接时,构成了多对多关系的IO通道,也就是说一台主机

在Linux环境下使用OpenSSL对消息和文件进行加密(转载)

转自:http://netsecurity.51cto.com/art/201301/378513.htm 1.简介 OpenSSL是一款功能强大的加密工具包.我们当中许多人已经在使用OpenSSL,用于创建RSA私匙或证书签名请求(CSR).不过,你可曾 知道可以使用OpenSSL来测试计算机速度?或者还可以用它来对文件或消息进行加密?本文将介绍几个简单易学的技巧,教你如何使用OpenSSL对消息 和文件进行加密. [相关推荐]:网络安全工具百宝箱 2.对消息进行加密和解密 首先,我们不妨对简

mosquitto在Linux环境下的部署/安装/使用/测试

mosquitto在Linux环境下的部署 看了有三四天的的源码,(当然没怎么好好看了),突然发现对mosquitto的源码有了一点点感觉,于是在第五天决定在Linux环境下部署mosquitto. 使用传统源码安装步骤: 步骤1:http://mosquitto.org/files/source/官网下载源码,放到Linux环境中.解压后,找到主要配置文件config.mk,其中包含mosquitto的安装选项,需要注意的是,默认情况下mosquitto的安装需要OpenSSL(一个强大的安全

【原创】Linux环境的图形系统和AMD显卡驱动编程(1)——Linux环境下的图形系统简介

Linux/Unix环境下最早的图形系统是Xorg图形系统,Xorg图形系统通过扩展的方式以适应显卡和桌面图形发展的需要,然而随着软硬件的发展,特别是嵌入式系统的发展,Xorg显得庞大而落后.开源社区开发开发了一些新的图形系统,比如Wayland图形系统. 由于图形系统.3D图形本身的复杂以及历史原因,Linux下的图形系统相关的源码庞大而且复杂,而且缺少学习的资料(所有源代码分析或者驱动编程的书籍都很少介绍显卡驱动).在后续一系列文章中,笔者将从对AMD硬件编程的角度出发对部分问题做一个简单的

在linux环境下编译运行OpenCV程序的两种方法

原来以为在Ubuntu下安装好了OpenCV之后,自己写个简单的程序应该很容易吧,但是呢,就是为了编译一个简单的显示图片的程序我都快被弄崩溃了. 在谷歌和上StackOverFlow查看相关问题解答之后,我下面就介绍Command Line和CMake两种方式. 首先我先粘上我测试的代码吧,文件名为Test.c 1 #include <highgui.h> 2 3 int main(int argc,char ** argv) { 4 5 IplImage* img = cvLoadImage

Linux环境下的图形系统和AMD R600显卡编程(1)——Linux环境下的图形系统简介

转:https://www.cnblogs.com/shoemaker/p/linux_graphics01.html Linux/Unix环境下最早的图形系统是Xorg图形系统,Xorg图形系统通过扩展的方式以适应显卡和桌面图形发展的需要,然而随着软硬件的发展,特别是嵌入式系统的发展,Xorg显得庞大而落后.开源社区开发开发了一些新的图形系统,比如Wayland图形系统. 由于图形系统.3D图形本身的复杂以及历史原因,Linux下的图形系统相关的源码庞大而且复杂,而且缺少学习的资料(所有源代码

QT在linux环境下读取和设置系统时间(通过system来直接调用Linux命令,注意权限问题)

QT在Linux环境下读取和设置系统时间 本文博客链接:http://blog.csdn.NET/jdh99,作者:jdh,转载请注明. 环境: 主机:Fedora12 开发软件:QT 读取系统时间 [cpp] view plain copy void moreidDialog::refresh_time() { QDateTime time; txt_time->setText(time.currentDateTime().toString("yyyy") + ".&