libpcap源码分析_从pcap_open_live说起

libpcap是跨平台网络数据包捕获函数库,本文将基于Linux平台对其源码以及核心原理进行深入分析
备注: 以下分析都基于libpcap-1.8.1版本进行
       以下分析按照库的核心API为线索展开
       以下分析源码时只列出核心逻辑

API: pcap_open_live
   描述: 针对指定的网络接口创建一个捕获句柄,用于后续捕获数据
   实现逻辑分析:
    @device  - 指定网络接口名,比如"eth0"。如果传入NULL或"any",则意味着对所有接口进行捕获
    @snaplen - 设置每个数据包的捕捉长度,上限MAXIMUM_SNAPLEN
    @promisc - 是否打开混杂模式
    @to_ms   - 设置获取数据包时的超时时间(ms)
    备注:to_ms值会影响3个捕获函数(pcap_next、pcap_loop、pcap_dispatch)的行为

    pcap_t *pcap_open_live(const char *device, int snaplen, int promisc, int to_ms, char *errbuf)
    {
        pcap_t *p;
        // 基于指定的设备接口创建一个pcap句柄
        p = pcap_create(device, errbuf);
        // 设置最大捕获包的长度
        status = pcap_set_snaplen(p, snaplen);
        // 设置数据包的捕获模式
        status = pcap_set_promisc(p, promisc);
        // 设置执行捕获操作的持续时间
        status = pcap_set_timeout(p, to_ms);
        // 使指定pcap句柄进入活动状态,这里实际包含了创建捕获套接字的动作
        status = pcap_activate(p);
        return p;
    }
    
    pcap_t *pcap_create(const char *device, char *errbuf)
    {
        pcap_t *p;
        char *device_str;

        // 转储传入的设备名,如果传入NULL,则设置为"any"
        if (device == NULL)
            device_str = strdup("any");
        else
            device_str = strdup(device);

        // 创建一个普通网络接口类型的pcap句柄
        p = pcap_create_interface(device_str, errbuf);
        p->opt.device = device_str;
        return p;
    }

    pcap_t *pcap_create_interface(const char *device, char *ebuf)
    {
        pcap_t *handle;
        // 创建并初始化一个包含私有空间struct pcap_linux的pcap句柄
        handle = pcap_create_common(ebuf, sizeof (struct pcap_linux));

        // 在刚创建了该pcap句柄后,这里首先覆盖了2个回调函数
        handle->activate_op = pcap_activate_linux;
        handle->can_set_rfmon_op = pcap_can_set_rfmon_linux;
    }

    pcap_t *pcap_create_common(char *ebuf, size_t size)
    {
        pcap_t *p;
        // 申请一片连续内存,用作包含size长度私有空间的pcap句柄,其中私有空间紧跟在该pcap结构后
        p = pcap_alloc_pcap_t(ebuf, size);

        // 为新建的pcap句柄注册一系列缺省的回调函数,这些缺省的回调函数大部分会在后面覆盖为linux下对应回调函数
        p->can_set_rfmon_op = pcap_cant_set_rfmon;
        initialize_ops(p);

        return p;
    }

    int pcap_set_snaplen(pcap_t *p, int snaplen)
    {
        // 设置该pcap句柄捕获包的最大长度前,需要确保当前并未处于活动状态
        if (pcap_check_activated(p))
            return (PCAP_ERROR_ACTIVATED);

        // 如果传入了无效的最大包长,则会设置为缺省值
        if (snaplen <= 0 || snaplen > MAXIMUM_SNAPLEN)
            snaplen = MAXIMUM_SNAPLEN;

        p->snapshot = snaplen;
    }
    
    int pcap_set_promisc(pcap_t *p, int promisc)
    {
        // 设置该pcap句柄关联接口的数据包捕获模式前,需要确保当前并未处于活动状态
        if (pcap_check_activated(p))
            return (PCAP_ERROR_ACTIVATED);

        p->opt.promisc = promisc;
    }

    int pcap_set_timeout(pcap_t *p, int timeout_ms)
    {
        // 设置该pcap句柄执行捕获操作的持续时间前,需要确保当前并未处于活动状态
        if (pcap_check_activated(p))
            return (PCAP_ERROR_ACTIVATED);

        p->opt.timeout = timeout_ms;
    }

    int pcap_activate(pcap_t *p)
    {
        int status;
        // 确保没有进行重复激活
        if (pcap_check_activated(p))
            return (PCAP_ERROR_ACTIVATED);
        
        // 调用事先注册的activate_op方法,完成对该pcap句柄的激活,这个过程中会创建用于捕获的套接字,以及尝试开启PACKET_MMAP机制
        status = p->activate_op(p);
        return status;
    }

    int pcap_activate_linux(pcap_t *handle)
    {
        int status;
        // 获取该pcap句柄的私有空间
        struct pcap_linux *handlep = handle->priv;

        device = handle->opt.device;

        // 为该pcap句柄注册linux平台相关的一系列回调函数(linux平台的回调函数又分为2组,这里缺省注册了一组不使用PACKET_MMAP机制的回调)
        handle->inject_op = pcap_inject_linux;
        handle->setfilter_op = pcap_setfilter_linux;
        handle->setdirection_op = pcap_setdirection_linux;
        handle->set_datalink_op = pcap_set_datalink_linux;
        handle->getnonblock_op = pcap_getnonblock_fd;
        handle->setnonblock_op = pcap_setnonblock_fd;
        handle->cleanup_op = pcap_cleanup_linux;
        handle->read_op = pcap_read_linux;
        handle->stats_op = pcap_stats_linux;

        // "any"设备不支持混杂模式
        if (strcmp(device, "any") == 0) {
            if (handle->opt.promisc)
                handle->opt.promisc = 0;
        }

        handlep->device = strdup(device);
        handlep->timeout = handle->opt.timeout;

        // 开启混杂模式的接口,需要先从/proc/net/dev中获取该接口当前"drop"报文数量
        if (handle->opt.promisc)
            handlep->proc_dropped = linux_if_drops(handlep->device);

        /* 创建用于捕获接口收到的原始报文的套接字
         * 备注:旧版本的kernel使用SOCK_PACKET类型套接字来实现对原始报文的捕获,这种过时的方式不再展开分析
         *       较新的kernel使用PF_PACKET来实现该功能
         */
        ret = activate_new(handle);
        // 这里只分析成功使用PF_PACKET创建套接字的情况
        if (ret == 1) {
            /* 尝试对新创建的套接字开启PACKET_MMAP功能
             * 备注:旧版本的kernel不支持开启PACKET_MMAP功能,所以只能作为普通的原始套接字使用
             *       较新版本的kernel逐渐开始支持v1、v2、v3版本的PACKET_MMAP,这里将会尝试开启当前系统支持的最高版本PACKET_MMAP
             */
            switch (activate_mmap(handle, &status)) {
                case 1: // 返回1意味着成功开启PACKET_MMAP功能,这里是为poll选择一个合适的超时时间
                    set_poll_timeout(handlep);
                    return status;
                case 0: // 返回0意味着kernel不支持PACKET_MMAP
                    break;
            }
        }

        /* 程序运行到这里只有2种可能:
         *      通过新式的PF_PACKET创建了套接字,但不支持PACKET_MMAP特性时
         *      通过老式的SOCKET_PACKET创建了套接字之后
         */

        // 如果配置了套接字接收缓冲区长度,就在这里进行设置
        if (handle->opt.buffer_size != 0) {
            setsockopt(handle->fd, SOL_SOCKET, SO_RCVBUF,&handle->opt.buffer_size,sizeof(handle->opt.buffer_size));
        }

        // 不开启PACKET_MMAP的情况下,就在这里分配用户空间接收缓冲区
        handle->buffer   = malloc(handle->bufsize + handle->offset);
        handle->selectable_fd = handle->fd;
    }

    int activate_new(pcap_t *handle)
    {
        struct pcap_linux *handlep = handle->priv;
        const char      *device = handle->opt.device;
        int         is_any_device = (strcmp(device, "any") == 0);
        struct packet_mreq  mr;

        // 如果是名为"any"的接口,则创建SOCK_DGRAM类型的套接字;通常情况下都是创建SOCK_RAW类型的套接字
        sock_fd = is_any_device ?
            socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)) :
            socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

        // 记录下环回接口的序号
        handlep->lo_ifindex = iface_get_id(sock_fd, "lo", handle->errbuf);

        handle->offset   = 0;
        // 对于接口名不是"any"的接口,如果其接口类型未定义或者属于一种不支持工作在raw模式下的接口,这些接口仍旧要回退到cooked模式
        if (!is_any_device) {
            // 获取该接口的硬件类型,linux中专门用ARPHRD_*来标识设备接口类型
            arptype = iface_get_arptype(sock_fd, device, handle->errbuf);
            /* pcap中使用DLT_*来标识设备接口,所以这里就是将ARPHRD_*映射成对应的DLT_*
             * 除此之外,还会根据接口类型修改offset,从而确保 offset + 2层头长度 实现4字节对齐
             */
            map_arphrd_to_dlt(handle, sock_fd, arptype, device, 1);
            // 符合以下情况的都需要回退到cooked模式
            if (handle->linktype == -1 || handle->linktype == DLT_LINUX_SLL || handle->linktype == DLT_LINUX_IRDA ||
                handle->linktype == DLT_LINUX_LAPD || handle->linktype == DLT_NETLINK ||
                (handle->linktype == DLT_EN10MB && (strncmp("isdn", device, 4) == 0 || strncmp("isdY", device, 4) == 0))) {
                close(sock_fd);
                sock_fd = socket(PF_PACKET, SOCK_DGRAM,htons(ETH_P_ALL));
                handlep->cooked = 1;
            }
            
            // 获取该接口的序号
            handlep->ifindex = iface_get_id(sock_fd, device,handle->errbuf);
            // 将创建的套接字绑定到该设备接口上
            iface_bind(sock_fd, handlep->ifindex,handle->errbuf);
        } else {
            // 对于接口名为"any"的接口,直接将其设置为cooked模式
            handlep->cooked = 1;
            handle->linktype = DLT_LINUX_SLL;
            // 接口名为"any"的接口只是一个泛指,实际不存在这个接口,所以也不进行绑定
            handlep->ifindex = -1;
        }

        // 在非"any"设备接口上开启混杂模式
        if (!is_any_device && handle->opt.promisc) {
            memset(&mr, 0, sizeof(mr));
            mr.mr_ifindex = handlep->ifindex;
            mr.mr_type    = PACKET_MR_PROMISC;
            setsockopt(sock_fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,&mr, sizeof(mr);
        }

        /* 使能对辅助数据的支持,辅助数据主要就是报文的vlan头信息,同时将offset增加vlan标签字段长
         * 备注:后续如果启用了PACKET_MMAP V3机制,就不需要在这里启用该功能了
         */
        int val = 1;
        setsockopt(sock_fd, SOL_PACKET, PACKET_AUXDATA, &val,sizeof(val));
        handle->offset += VLAN_TAG_LEN;

        // 加工模式下必须确保最大包的长度不小于 SLL_HDR_LEN + 1
        if (handlep->cooked)
            if (handle->snapshot < SLL_HDR_LEN + 1)
            handle->snapshot = SLL_HDR_LEN + 1;

        handle->bufsize = handle->snapshot;

        // 设置vlan标签的偏移量
        switch (handle->linktype) {
        case DLT_EN10MB:    // 普通以太网设备VLAN标签位于目的mac和源mac之后位置
            handlep->vlan_offset = 2 * ETH_ALEN;
            break;
        case DLT_LINUX_SLL: // cooked模式下的设备VLAN标签记录在sll_header->sll_protocol字段
            handlep->vlan_offset = SLL_HDR_LEN - 2;
            break;
        }

        // 配置该pcap句柄的时间戳精度
        if (handle->opt.tstamp_precision == PCAP_TSTAMP_PRECISION_NANO) {
            int nsec_tstamps = 1;
            setsockopt(sock_fd, SOL_SOCKET, SO_TIMESTAMPNS, &nsec_tstamps, sizeof(nsec_tstamps));
        }

        handle->fd = sock_fd;
        return 1;
    }


   
   相关数据结构:

    // 对一个接口执行捕获操作的句柄结构
    struct pcap {
        read_op_t read_op;      // 在该接口上进行读操作的回调函数(linux上就是 pcap_read_linux / pcap_read_linux_mmap_v3)
        int fd;                 // 该接口关联的套接字
        int selectable_fd;      // 通常就是fd
        u_int bufsize;          // 接收缓冲区的有效大小,该值初始时来自用户配置的snapshot,当开启PACKET_MMAP时,跟配置的接收环形缓冲区tp_frame_size值同步
        void *buffer;           /* 当开启PACKET_MMAP时,指向一个成员为union thdr结构的数组,记录了接收环形缓冲区中每个帧的帧头;
                                 * 当不支持PACKET_MMAP时,指向用户空间的接收缓冲区,其大小为 bufsize + offset
                                 */
        int cc;                 // 跟配置的接收环形缓冲区tp_frame_nr值同步(由于pcap中内存块数量和帧数量相等,所以本字段也就是内存块数量)
        int break_loop;         // 标识是否强制退出循环捕获
        void *priv;             // 指向该pcap句柄的私有空间(紧跟在本pcap结构后),linux下就是struct pcap_linux
        struct pcap *next;      // 这张链表记录了所有已经打开的pcap句柄,目的是可以被用于关闭操作
        int snapshot;           // 该pcap句柄支持的最大捕获包的长度,对于普通的以太网接口可以设置为1518,对于环回口可以设置为65549,其他情况下可以设置为MAXIMUM_SNAPLEN
        int linktype;           // 接口的链路类型,对于以太网设备/环回设备,通常就是DLT_EN10MB
        int offset;             // 该值跟接口链路类型相关,目的是确保 offset + L2层头长度 实现4字节对齐
        int activated;          // 标识该pcap句柄是否处于运作状态,处于运作状态的pcap句柄将不允许进行修改
        struct pcap_opt opt;    // 该句柄包含的一个子结构
        pcap_direction_t direction;     // 捕包方向
        struct bpf_program fcode;       // BPF过滤模块
        int dlt_count;                  // 该设备对应的dlt_list中元素数量,通常为2
        u_int *dlt_list;                // 指向该设备对应的DLT_*列表

        activate_op_t activate_op;              // 对应回调函数:pcap_activate_linux
        can_set_rfmon_op_t can_set_rfmon_op;    // 对应回调函数:pcap_can_set_rfmon_linux
        inject_op_t inject_op;                  // 对应回调函数:pcap_inject_linux
        setfilter_op_t setfilter_op;            // 对应回调函数:pcap_setfilter_linux       / pcap_setfilter_linux_mmap
        setdirection_op_t setdirection_op;      // 对应回调函数:pcap_setdirection_linux
        set_datalink_op_t set_datalink_op;      // 对应回调函数:pcap_set_datalink_linux
        getnonblock_op_t getnonblock_op;        // 对应回调函数:pcap_getnonblock_fd        / pcap_getnonblock_mmap
        setnonblock_op_t setnonblock_op;        // 对应回调函数:pcap_setnonblock_fd        / pcap_setnonblock_mmap
        stats_op_t stats_op;                    // 对应回调函数:pcap_stats_linux
        pcap_handler oneshot_callback;          // 对应回调函数:pcap_oneshot_mmap
        cleanup_op_t cleanup_op;                // 对应回调函数:pcap_cleanup_linux_mmap
    }

    // pcap句柄包含的一个子结构
    struct pcap_opt {
        char *device;       // 接口名,比如"eth0"
        int timeout;        // 该pcap句柄进行捕获操作的持续时间(ms),0意味着不超时
        u_int buffer_size;  // 接收缓冲区长度,缺省就是2M. 当PACKET_MMAP开启时,该值用来配置接收环形缓冲区;当不支持PACKET_MMAP时,该值用来配置套接字的接收缓冲区
        int promisc;        // 标识该pcap句柄是否开启混杂模式,需要注意的是,"any"设备不允许开启混杂模式
        int rfmon;          // 表示该pcap句柄是否开启监听模式,该模式只用于无线网卡
        int immediate;      // 标识收到报文时是否立即传递给用户
        int tstamp_type;        // 该pcap句柄使用的时间戳类型
        int tstamp_precision;   // 该pcap句柄使用的时间戳精度
    }

    // 跟pcap句柄关联的linux平台私有空间
    struct pcap_linux {
        u_int   packets_read;       // 统计捕获到的包数量
        long    proc_dropped;       // 统计丢弃的包数量

        char    *device;            // 接口名,同步自pcap->opt.device
        int filter_in_userland;     // 标识用户空间是否需要过滤包
        int timeout;                // 进行捕获操作的持续时间,同步自pcap->opt.timeout
        int sock_packet;            // 0意味着使用了PF_PACKET方式创建的套接字
        int cooked;                 // 1意味着使用了SOCK_DGRAM类型套接字,0意味着使用了SOCK_RAW类型套接字
        int ifindex;                // 对于普通的以太网接口,这里记录了其接口序号
        int lo_ifindex;             // 记录了环回接口序号
        u_char  *mmapbuf;           // 接收环形缓冲区在用户进程中的映射地址
        size_t  mmapbuflen;         // mmap实际映射的接收环形缓冲区长度
        int vlan_offset;            // VLAN标签距离报文头部的偏移量,普通以太网链路上该值为12;cooked模式下为sll_header->sll_protocol字段
        u_int   tp_version;         // 环形缓冲区的版本号
        u_int   tp_hdrlen;          // 环形缓冲区的帧头长(跟环形缓冲区版本有关)
        int poll_timeout;           // poll系统调用传入的超时参数,默认来自上面的timeout,但TPACKET_V3在3.19版本之前不允许不超时
        unsigned char *current_packet;  // (仅用于TPACKET_V3)指向当前待处理的帧
        int packets_left;               // (仅用于TPACKET_V3)待处理的帧数量
    }

小结:以上一系列分析都是针对pcap_open_live进行展开,该API的主要功能就是针对指定的接口创建一个pcap句柄,后续对该接口的所有操作都是基于该句柄展开。
      该API的实质就是创建一个用于捕获的原始套接字,而较新版本内核中(支持PACKET_MMAP),通过mmap机制实现了对捕获效率的大大提升,
      对于该机制的深入分析,将以activate_mmap为线索,在下一篇笔记中独立展开。

 

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: `pcap_open_live()` 函数是 libpcap 库中用于打开网络接口并开始捕获数据包的函数。它的原型如下: ```c pcap_t *pcap_open_live(const char *device, int snaplen, int promisc, int to_ms, char *errbuf); ``` 其中,参数含义如下: - `device`:指定要打开的网络接口的名称,比如 `eth0` 或 `wlan0`。 - `snaplen`:指定捕获数据包的最大长度。如果数据包的长度超过了该值,则只会捕获前 `snaplen` 个字节的数据。 - `promisc`:指定是否开启混杂模式,即是否接收所有经过该网络接口的数据包,而不仅仅是目标地址为本机的数据包。 - `to_ms`:指定超时时间,单位为毫秒。如果在该时间内没有捕获到数据包,则 `pcap_next()` 函数会返回 NULL。 - `errbuf`:指定一个缓冲区,用于存储错误信息。如果函数执行失败,则会在该缓冲区中存储错误信息。 该函数返回一个 `pcap_t` 类型的指针,该指针可以传递给其他 libpcap 函数使用,比如 `pcap_compile()` 和 `pcap_loop()`。 以下是一个简单的使用示例: ```c #include <pcap.h> #include <stdio.h> int main(int argc, char *argv[]) { char *device = "eth0"; int snaplen = 65535; int promisc = 1; int to_ms = 1000; char errbuf[PCAP_ERRBUF_SIZE]; // 打开网络接口 pcap_t *handle = pcap_open_live(device, snaplen, promisc, to_ms, errbuf); if (handle == NULL) { fprintf(stderr, "pcap_open_live() failed: %s\n", errbuf); return 1; } // 进行数据包捕获和处理 // ... // 关闭网络接口 pcap_close(handle); return 0; } ``` 在以上示例中,我们使用 `pcap_open_live()` 函数打开了一个名为 `eth0` 的网络接口,并设置了捕获数据包的最大长度为 65535,开启了混杂模式,设置了超时时间为 1000 毫秒。如果函数执行失败,则使用 `pcap_errbuf` 缓冲区存储错误信息。在接下来的代码中,可以使用 `handle` 指针进行数据包捕获和处理。最后,我们使用 `pcap_close()` 函数关闭网络接口。 ### 回答2: pcap_open_live是一个用于打开网络接口的函数,它是libpcap库中的一个函数。 使用pcap_open_live函数可以打开指定的网络接口,以便于后续的数据包捕获和分析。该函数的参数包括网络接口名称、捕获数据包的最大长度、是否设置为混杂模式、等待时间等。 其中,网络接口名称是一个字符串,指定要打开的网络接口的名称。可以使用pcap_findalldevs函数获取系统上所有可用的网络接口列表,并选择合适的接口名称。捕获数据包的最大长度是一个整数,表示每个数据包的最大字节数。可以根据实际需求设置合适的数值,一般建议设置为足够大的值。 是否设置混杂模式是一个布尔值,用于指定是否开启混杂模式。在混杂模式下,可以捕获网络上的所有数据包,而不仅仅是目的地址是本机的数据包。因此,如果需要对整个网络上的数据进行捕获和分析,可以将该参数设置为真。 等待时间是指在调用pcap_next或pcap_loop等函数时,如果没有数据包到达,允许等待的最长时间。可以根据实际需求设置合适的数值,较长的等待时间可以保证数据包的完整性,但也会增加程序的响应时间。 总之,pcap_open_live函数是一个用于打开网络接口的函数,使用它可以实现对特定网络接口上的数据包进行捕获和分析。 ### 回答3: pcap_open_live是一个用于打开网络接口的库函数,用于捕获网络数据包的函数。它是libpcap库的一部分,常用于网络数据包分析和网络安全领域。 pcap_open_live函数可以接受三个参数,分别是网络接口名称、最大捕获数据长度和是否开启混杂模式。网络接口名称可以是设备名称,如“eth0”或“en0”,也可以是特殊的字符串“any”,表示打开所有的网络接口。最大捕获数据长度表示捕获的数据包的最大长度,一般建议设置为65535字节,以确保捕获整个数据包。而混杂模式则是表示是否开启网络接口的混杂模式,即接收所有通过该接口的数据包,而不仅仅是目标地址是本机的数据包。 使用pcap_open_live函数,可以方便地打开网络接口,开始捕获数据包。在捕获数据包时,我们可以使用其他libpcap函数如pcap_next_ex函数来获取每个捕获的数据包。通过分析捕获的数据包,我们可以获得网络协议、源和目标IP地址、源和目标端口等网络信息,进而进行网络分析和安全检测。 总之,pcap_open_live函数是一个功能强大的函数,用于打开网络接口并捕获网络数据包。它在网络分析和网络安全领域有着广泛的应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值