libpcap源碼分析_從pcap_open_live說起

libpcap是跨平臺網絡數據包捕獲函數庫,本文將基於Linux平臺對其源碼以及核心原理進行深入分析
備註: 以下分析都基於libpcap-1.8.1版本進行
             以下分析按照庫的核心API爲線索展開
            
以下分析源碼時只列出核心邏輯
1. 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);

        // 如果是名爲"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;

        if (!is_any_device) {
            // 獲取該接口的硬件類型,linux中專門用ARPHRD_*來標識設備接口類型
            arptype = iface_get_arptype(sock_fd, device, handle->errbuf);
            /* pcap中使用DLT_*來標識設備接口,所以這裏就是將ARPHRD_*映射成對應的DLT_*
             * 除此之外,還會根據接口類型修改offset,從而確保 offset + 2層頭長度 實現4字節對齊
             */
        }
    }


   
   相關數據結構:
   

    // 對一個接口執行捕獲操作的句柄結構
    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 + 2層頭長度 實現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 lo_ifindex;             // 記錄了環回接口序號

    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章