精品国产亚洲一区二区三区,男女作爱在线观看免费网站,欧美的又大又长做禁片A片,97国产精品人妻无码久久久

  • 相關軟件
    >Linux下Libpcap源碼分析和包過濾機制 創(chuàng)建者:webmaster 更新時間:2005-07-07 22:09

    libpcap是unix/linux平臺下的網(wǎng)絡數(shù)據(jù)包捕獲函數(shù)包,大多數(shù)網(wǎng)絡監(jiān)控軟件都以它為基礎。Libpcap可以在絕大多數(shù)類unix平臺下工作,本文分析了libpcap在linux 下的源代碼實現(xiàn),其中重點是linux的底層包捕獲機制和過濾器設置方式,同時也簡要的討論了 libpcap使用的包過濾機制 BPF。

    網(wǎng)絡監(jiān)控

    絕大多數(shù)的現(xiàn)代操作系統(tǒng)都提供了對底層網(wǎng)絡數(shù)據(jù)包捕獲的機制,在捕獲機制之上可以建立網(wǎng)絡監(jiān)控(Network Monitoring)應用軟件。網(wǎng)絡監(jiān)控也常簡稱為sniffer,其最初的目的在于對網(wǎng)絡通信情況進行監(jiān)控,以對網(wǎng)絡的一些異常情況進行調試處理。但隨著互連網(wǎng)的快速普及和網(wǎng)絡攻擊行為的頻繁出現(xiàn),保護網(wǎng)絡的運行安全也成為監(jiān)控軟件的另一個重要目的。例如,網(wǎng)絡監(jiān)控在路由器,防火墻、入侵檢查等方面使用也很廣泛。除此而外,它也是一種比較有效的黑客手段,例如,美國政府安全部門的"肉食動物"計劃。

    包捕獲機制

    從廣義的角度上看,一個包捕獲機制包含三個主要部分:最底層是針對特定操作系統(tǒng)的包捕獲機制,最高層是針對用戶程序的接口,第三部分是包過濾機制。

    不同的操作系統(tǒng)實現(xiàn)的底層包捕獲機制可能是不一樣的,但從形式上看大同小異。數(shù)據(jù)包常規(guī)的傳輸路徑依次為網(wǎng)卡、設備驅動層、數(shù)據(jù)鏈路層、IP層、傳輸層、最后到達應用程序。而包捕獲機制是在數(shù)據(jù)鏈路層增加一個旁路處理,對發(fā)送和接收到的數(shù)據(jù)包做過濾/緩沖等相關處理,最后直接傳遞到應用程序。值得注意的是,包捕獲機制并不影響操作系統(tǒng)對數(shù)據(jù)包的網(wǎng)絡棧處理。對用戶程序而言,包捕獲機制提供了一個統(tǒng)一的接口,使用戶程序只需要簡單的調用若干函數(shù)就能獲得所期望的數(shù)據(jù)包。這樣一來,針對特定操作系統(tǒng)的捕獲機制對用戶透明,使用戶程序有比較好的可移植性。包過濾機制是對所捕獲到的數(shù)據(jù)包根據(jù)用戶的要求進行篩選,最終只把滿足過濾條件的數(shù)據(jù)包傳遞給用戶程序。

    Libpcap應用程序框架

    Libpcap提供了系統(tǒng)獨立的用戶級別網(wǎng)絡數(shù)據(jù)包捕獲接口,并充分考慮到應用程序的可移植性。Libpcap可以在絕大多數(shù)類unix平臺下工作,參考資料 A 中是對基于 libpcap 的網(wǎng)絡應用程序的一個詳細列表。在windows平臺下,一個與libpcap 很類似的函數(shù)包 winpcap 提供捕獲功能,其官方網(wǎng)站是http://winpcap.polito.it/。

    Libpcap 軟件包可從 http://www.tcpdump.org/ 下載,然后依此執(zhí)行下列三條命令即可安裝,但如果希望libpcap能在linux上正常工作,則必須使內核支持"packet"協(xié)議,也即在編譯內核時打開配置選項 CONFIG_PACKET(選項缺省為打開)。






    ./configure
    ./make
    ./make install


    libpcap源代碼由20多個C文件構成,但在Linux系統(tǒng)下并不是所有文件都用到。可以通過查看命令make的輸出了解實際所用的文件。本文所針對的libpcap版本號為0.8.3,網(wǎng)絡類型為常規(guī)以太網(wǎng)。Libpcap應用程序從形式上看很簡單,下面是一個簡單的程序框架:






    char * device; /* 用來捕獲數(shù)據(jù)包的網(wǎng)絡接口的名稱 */
    pcap_t * p; /* 捕獲數(shù)據(jù)包句柄,最重要的數(shù)據(jù)結構 */
    struct bpf_program fcode; /* BPF 過濾代碼結構 */

    /* 第一步:查找可以捕獲數(shù)據(jù)包的設備 */
    device = pcap_lookupdev(errbuf);

    /* 第二步:創(chuàng)建捕獲句柄,準備進行捕獲 */
    p = pcap_open_live(device, 8000, 1, 500, errbuf);

    /* 第三步:如果用戶設置了過濾條件,則編譯和安裝過濾代碼 */
    pcap_compile(p, &fcode, filter_string, 0, netmask);
    pcap_setfilter(p, &fcode);

    /* 第四步:進入(死)循環(huán),反復捕獲數(shù)據(jù)包 */
    for( ; ; )
    {
    while((ptr = (char *)(pcap_next(p, &hdr))) == NULL);

    /* 第五步:對捕獲的數(shù)據(jù)進行類型轉換,轉化成以太數(shù)據(jù)包類型 */
    eth = (struct libnet_ethernet_hdr *)ptr;

    /* 第六步:對以太頭部進行分析,判斷所包含的數(shù)據(jù)包類型,做進一步的處理 */
    if(eth->ether_type == ntohs(ETHERTYPE_IP))
    …………
    if(eth->ether_type == ntohs(ETHERTYPE_ARP))
    …………
    }

    /* 最后一步:關閉捕獲句柄,一個簡單技巧是在程序初始化時增加信號處理函數(shù),
    以便在程序退出前執(zhí)行本條代碼 */
    pcap_close(p);


    檢查網(wǎng)絡設備

    libpcap 程序的第一步通常是在系統(tǒng)中找到合適的網(wǎng)絡接口設備。網(wǎng)絡接口在Linux網(wǎng)絡體系中是一個很重要的概念,它是對具體網(wǎng)絡硬件設備的一個抽象,在它的下面是具體的網(wǎng)卡驅動程序,而其上則是網(wǎng)絡協(xié)議層。Linux中最常見的接口設備名eth0和lo。Lo 稱為回路設備,是一種邏輯意義上的設備,其主要目的是為了調試網(wǎng)絡程序之間的通訊功能。eth0對應了實際的物理網(wǎng)卡,在真實網(wǎng)絡環(huán)境下,數(shù)據(jù)包的發(fā)送和接收都要通過 eht0。如果計算機有多個網(wǎng)卡,則還可以有更多的網(wǎng)絡接口,如eth1,eth2 等等。調用命令ifconfig可以列出當前所有活躍的接口及相關信息,注意對eth0的描述中既有物理網(wǎng)卡的MAC地址,也有網(wǎng)絡協(xié)議的IP地址。查看文件/proc/net/dev也可獲得接口信息。

    Libpcap中檢查網(wǎng)絡設備中主要使用到的函數(shù)關系如下圖:



    libpcap調用pcap_lookupdev()函數(shù)獲得可用網(wǎng)絡接口的設備名。首先利用函數(shù) getifaddrs() 獲得所有網(wǎng)絡接口的地址,以及對應的網(wǎng)絡掩碼、廣播地址、目標地址等相關信息,再利用 add_addr_to_iflist()、add_or_find_if()、get_instance() 把網(wǎng)絡接口的信息增加到結構鏈表 pcap_if 中,最后從鏈表中提取第一個接口作為捕獲設備。其中 get_instanced()的功能是從設備名開始,找第一個是數(shù)字的字符,做為接口的實例號。網(wǎng)絡接口的設備號越小,則排在鏈表的越前面,因此,通常函數(shù)最后返回的設備名為 eth0。雖然 libpcap 可以工作在回路接口上,但顯然 libpcap 開發(fā)者認為捕獲本機進程之間的數(shù)據(jù)包沒有多大意義。在檢查網(wǎng)絡設備操作中,主要用到的數(shù)據(jù)結構和代碼如下:






    /* libpcap 自定義的接口信息鏈表 [pcap.h] */
    struct pcap_if
    {
    struct pcap_if *next;
    char *name; /* 接口設備名 */
    char *description; /* 接口描述 */

    /*接口的 IP 地址, 地址掩碼, 廣播地址,目的地址 */
    struct pcap_addr addresses;
    bpf_u_int32 flags; /* 接口的參數(shù) */
    };

    char * pcap_lookupdev(register char * errbuf)
    {
    pcap_if_t *alldevs;
    ……
    pcap_findalldevs(&alldevs, errbuf);
    ……
    strlcpy(device, alldevs->name, sizeof(device));
    }
    打開網(wǎng)絡設備

    當設備找到后,下一步工作就是打開設備以準備捕獲數(shù)據(jù)包。Libpcap的包捕獲是建立在具體的操作系統(tǒng)所提供的捕獲機制上,而Linux系統(tǒng)隨著版本的不同,所支持的捕獲機制也有所不同。

    2.0 及以前的內核版本使用一個特殊的socket類型SOCK_PACKET,調用形式是socket(PF_INET, SOCK_PACKET, int protocol),但 Linux 內核開發(fā)者明確指出這種方式已過時。Linux 在 2.2及以后的版本中提供了一種新的協(xié)議簇 PF_PACKET 來實現(xiàn)捕獲機制。PF_PACKET 的調用形式為 socket(PF_PACKET, int socket_type, int protocol),其中socket類型可以是 SOCK_RAW和SOCK_DGRAM。SOCK_RAW 類型使得數(shù)據(jù)包從數(shù)據(jù)鏈路層取得后,不做任何修改直接傳遞給用戶程序,而 SOCK_DRRAM 則要對數(shù)據(jù)包進行加工(cooked),把數(shù)據(jù)包的數(shù)據(jù)鏈路層頭部去掉,而使用一個通用結構 sockaddr_ll 來保存鏈路信息。

    使用 2.0 版本內核捕獲數(shù)據(jù)包存在多個問題:首先,SOCK_PACKET 方式使用結構 sockaddr_pkt來保存數(shù)據(jù)鏈路層信息,但該結構缺乏包類型信息;其次,如果參數(shù) MSG_TRUNC 傳遞給讀包函數(shù) recvmsg()、recv()、recvfrom() 等,則函數(shù)返回的數(shù)據(jù)包長度是實際讀到的包數(shù)據(jù)長度,而不是數(shù)據(jù)包真正的長度。Libpcap 的開發(fā)者在源代碼中明確建議不使用 2.0 版本進行捕獲。

    相對2.0版本SOCK_PACKET方式,2.2版本的PF_PACKET方式則不存在上述兩個問題。在實際應用中,用戶程序顯然希望直接得到"原始"的數(shù)據(jù)包,因此使用 SOCK_RAW 類型最好。但在下面兩種情況下,libpcap 不得不使用SOCK_DGRAM類型,從而也必須為數(shù)據(jù)包合成一個"偽"鏈路層頭部(sockaddr_ll)。

    某些類型的設備數(shù)據(jù)鏈路層頭部不可用:例如 Linux 內核的 PPP 協(xié)議實現(xiàn)代碼對 PPP 數(shù)據(jù)包頭部的支持不可靠。

    在捕獲設備為"any"時:所有設備意味著libpcap對所有接口進行捕獲,為了使包過濾機制能在所有類型的數(shù)據(jù)包上正常工作,要求所有的數(shù)據(jù)包有相同的數(shù)據(jù)鏈路頭部。

    打開網(wǎng)絡設備的主函數(shù)是 pcap_open_live()[pcap-linux.c],其任務就是通過給定的接口設備名,獲得一個捕獲句柄:結構 pcap_t。pcap_t 是大多數(shù)libpcap函數(shù)都要用到的參數(shù),其中最重要的屬性則是上面討論到的三種 socket方式中的某一種。首先我們看看pcap_t的具體構成。






    struct pcap [pcap-int.h]
    {
    int fd; /* 文件描述字,實際就是 socket */

    /* 在 socket 上,可以使用 select() 和 poll() 等 I/O 復用類型函數(shù) */
    int selectable_fd;

    int snapshot; /* 用戶期望的捕獲數(shù)據(jù)包最大長度 */
    int linktype; /* 設備類型 */
    int tzoff; /* 時區(qū)位置,實際上沒有被使用 */
    int offset; /* 邊界對齊偏移量 */

    int break_loop; /* 強制從讀數(shù)據(jù)包循環(huán)中跳出的標志 */

    struct pcap_sf sf; /* 數(shù)據(jù)包保存到文件的相關配置數(shù)據(jù)結構 */
    struct pcap_md md; /* 具體描述如下 */

    int bufsize; /* 讀緩沖區(qū)的長度 */
    u_char buffer; /* 讀緩沖區(qū)指針 */
    u_char *bp;
    int cc;
    u_char *pkt;

    /* 相關抽象操作的函數(shù)指針,最終指向特定操作系統(tǒng)的處理函數(shù) */
    int (*read_op)(pcap_t *, int cnt, pcap_handler, u_char *);
    int (*setfilter_op)(pcap_t *, struct bpf_program *);
    int (*set_datalink_op)(pcap_t *, int);
    int (*getnonblock_op)(pcap_t *, char *);
    int (*setnonblock_op)(pcap_t *, int, char *);
    int (*stats_op)(pcap_t *, struct pcap_stat *);
    void (*close_op)(pcap_t *);

    /*如果 BPF 過濾代碼不能在內核中執(zhí)行,則將其保存并在用戶空間執(zhí)行 */
    struct bpf_program fcode;

    /* 函數(shù)調用出錯信息緩沖區(qū) */
    char errbuf[PCAP_ERRBUF_SIZE + 1];

    /* 當前設備支持的、可更改的數(shù)據(jù)鏈路類型的個數(shù) */
    int dlt_count;
    /* 可更改的數(shù)據(jù)鏈路類型號鏈表,在 linux 下沒有使用 */
    int *dlt_list;

    /* 數(shù)據(jù)包自定義頭部,對數(shù)據(jù)包捕獲時間、捕獲長度、真實長度進行描述 [pcap.h] */
    struct pcap_pkthdr pcap_header;
    };

    /* 包含了捕獲句柄的接口、狀態(tài)、過濾信息 [pcap-int.h] */
    struct pcap_md {
    /* 捕獲狀態(tài)結構 [pcap.h] */
    struct pcap_stat stat;

    int use_bpf; /* 如果為1,則代表使用內核過濾*/
    u_long TotPkts;
    u_long TotAccepted; /* 被接收數(shù)據(jù)包數(shù)目 */
    u_long TotDrops; /* 被丟棄數(shù)據(jù)包數(shù)目 */
    long TotMissed; /* 在過濾進行時被接口丟棄的數(shù)據(jù)包數(shù)目 */
    long OrigMissed; /*在過濾進行前被接口丟棄的數(shù)據(jù)包數(shù)目*/
    #ifdef linux
    int sock_packet; /* 如果為 1,則代表使用 2.0 內核的 SOCK_PACKET 模式 */
    int timeout; /* pcap_open_live() 函數(shù)超時返回時間*/
    int clear_promisc; /* 關閉時設置接口為非混雜模式 */
    int cooked; /* 使用 SOCK_DGRAM 類型 */
    int lo_ifindex; /* 回路設備索引號 */
    char *device; /* 接口設備名稱 */

    /* 以混雜模式打開 SOCK_PACKET 類型 socket 的 pcap_t 鏈表*/
    struct pcap *next;
    #endif
    };


    函數(shù)pcap_open_live()的調用形式是 pcap_t * pcap_open_live(const char *device, int snaplen, int promisc, int to_ms, char *ebuf),其中如果 device 為 NULL 或"any",則對所有接口捕獲,snaplen 代表用戶期望的捕獲數(shù)據(jù)包最大長度,promisc 代表設置接口為混雜模式(捕獲所有到達接口的數(shù)據(jù)包,但只有在設備給定的情況下有意義),to_ms 代表函數(shù)超時返回的時間。本函數(shù)的代碼比較簡單,其執(zhí)行步驟如下:

    * 為結構pcap_t分配空間并根據(jù)函數(shù)入?yún)ζ洳糠謱傩赃M行初試化。

    * 分別利用函數(shù) live_open_new() 或 live_open_old() 嘗試創(chuàng)建 PF_PACKET 方式或 SOCK_PACKET 方式的socket,注意函數(shù)名中一個為"new",另一個為"old"。 * 根據(jù) socket 的方式,設置捕獲句柄的讀緩沖區(qū)長度,并分配空間。 * 為捕獲句柄pcap_t設置linux系統(tǒng)下的特定函數(shù),其中最重要的是讀數(shù)據(jù)包函數(shù)和設置過濾器函數(shù)。(注意到這種從抽象模式到具體模式的設計思想在 linux 源代碼中也多次出現(xiàn),如VFS文件系統(tǒng)) handle->read_op = pcap_read_linux; handle->setfilter_op = pcap_setfilter_linux;下面我們依次分析 2.2 和 2.0 內核版本下的socket創(chuàng)建函數(shù)。






    static int
    live_open_new(pcap_t *handle, const char *device, int promisc,
      int to_ms, char *ebuf)
    {
    /* 如果設備給定,則打開一個 RAW 類型的套接字,否則,打開 DGRAM 類型的套接字 */
    sock_fd = device ?
    socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))
        : socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL));

    /* 取得回路設備接口的索引 */
    handle->md.lo_ifindex = iface_get_id(sock_fd, "lo", ebuf);

    /* 如果設備給定,但接口類型未知或是某些必須工作在加工模式下的特定類型,則使用加工模式 */
    if (device) {
    /* 取得接口的硬件類型 */
    arptype = iface_get_arptype(sock_fd, device, ebuf);

    /* linux 使用 ARPHRD_xxx 標識接口的硬件類型,而 libpcap 使用DLT_xxx
    來標識。本函數(shù)是對上述二者的做映射變換,設置句柄的鏈路層類型為
    DLT_xxx,并設置句柄的偏移量為合適的值,使其與鏈路層頭部之和為 4 的倍數(shù),目的是邊界對齊 */
    map_arphrd_to_dlt(handle, arptype, 1);

    /* 如果接口是前面談到的不支持鏈路層頭部的類型,則退而求其次,使用 SOCK_DGRAM 模式 */
    if (handle->linktype == xxx)
    {
    close(sock_fd);
    sock_fd = socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL));
    }

    /* 獲得給定的設備名的索引 */
    device_id = iface_get_id(sock_fd, device, ebuf);

    /* 把套接字和給定的設備綁定,意味著只從給定的設備上捕獲數(shù)據(jù)包 */
    iface_bind(sock_fd, device_id, ebuf);

    } else { /* 現(xiàn)在是加工模式 */
    handle->md.cooked = 1;
    /* 數(shù)據(jù)包鏈路層頭部為結構 sockaddr_ll, SLL 大概是結構名稱的簡寫形式 */
    handle->linktype = DLT_LINUX_SLL;
    device_id = -1;
    }

    /* 設置給定設備為混雜模式 */
    if (device && promisc)
    {
    memset(&mr, 0, sizeof(mr));
    mr.mr_ifindex = device_id;
    mr.mr_type = PACKET_MR_PROMISC;
    setsockopt(sock_fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
    &mr, sizeof(mr));
    }

    /* 最后把創(chuàng)建的 socket 保存在句柄 pcap_t 中 */
    handle->fd = sock_fd;
    }

    /* 2.0 內核下函數(shù)要簡單的多,因為只有唯一的一種 socket 方式 */
    static int
    live_open_old(pcap_t *handle, const char *device, int promisc,
        int to_ms, char *ebuf)
    {
    /* 首先創(chuàng)建一個SOCK_PACKET類型的 socket */
    handle->fd = socket(PF_INET, SOCK_PACKET, htons(ETH_P_ALL));

    /* 2.0 內核下,不支持捕獲所有接口,設備必須給定 */
    if (!device) {
    strncpy(ebuf,
        "pcap_open_live: The \"any\" device isn't
        supported on 2.0[.x]-kernel systems",
        PCAP_ERRBUF_SIZE);
    break;
    }

    /* 把 socket 和給定的設備綁定 */
    iface_bind_old(handle->fd, device, ebuf);

    /*以下的處理和 2.2 版本下的相似,有所區(qū)別的是如果接口鏈路層類型未知,則 libpcap 直接退出 */

    arptype = iface_get_arptype(handle->fd, device, ebuf);
    map_arphrd_to_dlt(handle, arptype, 0);
    if (handle->linktype == -1) {
    snprintf(ebuf, PCAP_ERRBUF_SIZE, "unknown arptype %d", arptype);
    break;
    }

    /* 設置給定設備為混雜模式 */
    if (promisc) {
    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, device, sizeof(ifr.ifr_name));
    ioctl(handle->fd, SIOCGIFFLAGS, &ifr);
    ifr.ifr_flags |= IFF_PROMISC;
    ioctl(handle->fd, SIOCSIFFLAGS, &ifr);
    }
    }


    比較上面兩個函數(shù)的代碼,還有兩個細節(jié)上的區(qū)別。首先是 socket 與接口綁定所使用的結構:老式的綁定使用了結構 sockaddr,而新式的則使用了 2.2 內核中定義的通用鏈路頭部層結構sockaddr_ll。






    iface_bind_old(int fd, const char *device, char *ebuf)
    {
    struct sockaddr saddr;
    memset(&saddr, 0, sizeof(saddr));
    strncpy(saddr.sa_data, device, sizeof(saddr.sa_data));
    bind(fd, &saddr, sizeof(saddr));
    }

    iface_bind(int fd, int ifindex, char *ebuf)
    {
    struct sockaddr_ll sll;
    memset(&sll, 0, sizeof(sll));
    sll.sll_family = AF_PACKET;
    sll.sll_ifindex = ifindex;
    sll.sll_protocol = htons(ETH_P_ALL);
    bind(fd, (struct sockaddr *) &sll, sizeof(sll);
    }

    第二個是在 2.2 版本中設置設備為混雜模式時,使用了函數(shù) setsockopt(),以及新的標志 PACKET_ADD_MEMBERSHIP 和結構 packet_mreq。我估計這種方式主要是希望提供一個統(tǒng)一的調用接口,以代替?zhèn)鹘y(tǒng)的(混亂的)ioctl 調用。







    struct packet_mreq
    {
    int         mr_ifindex;   /* 接口索引號 */
    unsigned short mr_type;     /* 要執(zhí)行的操作(號) */
    unsigned short mr_alen;     /* 地址長度 */
    unsigned char   mr_address[8]; /* 物理層地址 */
    };


    第二個是在 2.2 版本中設置設備為混雜模式時,使用了函數(shù) setsockopt(),以及新的標志 PACKET_ADD_MEMBERSHIP 和結構 packet_mreq。我估計這種方式主要是希望提供一個統(tǒng)一的調用接口,以代替?zhèn)鹘y(tǒng)的(混亂的)ioctl 調用。






    struct packet_mreq
    {
    int         mr_ifindex;   /* 接口索引號 */
    unsigned short mr_type;     /* 要執(zhí)行的操作(號) */
    unsigned short mr_alen;     /* 地址長度 */
    unsigned char   mr_address[8]; /* 物理層地址 */
    };
    用戶應用程序接口

    Libpcap 提供的用戶程序接口比較簡單,通過反復調用函數(shù)pcap_next()[pcap.c]則可獲得捕獲到的數(shù)據(jù)包。下面是一些使用到的數(shù)據(jù)結構:






    /* 單個數(shù)據(jù)包結構,包含數(shù)據(jù)包元信息和數(shù)據(jù)信息 */
    struct singleton [pcap.c]
    {
    struct pcap_pkthdr hdr; /* libpcap 自定義數(shù)據(jù)包頭部 */
    const u_char * pkt; /* 指向捕獲到的網(wǎng)絡數(shù)據(jù) */
    };

    /* 自定義頭部在把數(shù)據(jù)包保存到文件中也被使用 */
    struct pcap_pkthdr
    {
    struct timeval ts; /* 捕獲時間戳 */
    bpf_u_int32 caplen; /* 捕獲到數(shù)據(jù)包的長度 */
    bpf_u_int32 len; /* 數(shù)據(jù)包的真正長度 */
    }

    /* 函數(shù) pcap_next() 實際上是對函數(shù) pcap_dispatch()[pcap.c] 的一個包裝 */
    const u_char * pcap_next(pcap_t *p, struct pcap_pkthdr *h)
    {
    struct singleton s;
    s.hdr = h;

    /*入?yún)?1"代表收到1個數(shù)據(jù)包就返回;回調函數(shù) pcap_oneshot() 是對結構 singleton 的屬性賦值 */
    if (pcap_dispatch(p, 1, pcap_oneshot, (u_char*)&s) <= 0)
    return (0);
    return (s.pkt); /* 返回數(shù)據(jù)包緩沖區(qū)的指針 */
    }


    pcap_dispatch() 簡單的調用捕獲句柄 pcap_t 中定義的特定操作系統(tǒng)的讀數(shù)據(jù)函數(shù):return p->read_op(p, cnt, callback, user)。在 linux 系統(tǒng)下,對應的讀函數(shù)為 pcap_read_linux()(在創(chuàng)建捕獲句柄時已定義 [pcap-linux.c]),而pcap_read_linux() 則是直接調用 pcap_read_packet()([pcap-linux.c])。

    pcap_read_packet() 的中心任務是利用了 recvfrom() 從已創(chuàng)建的 socket 上讀數(shù)據(jù)包數(shù)據(jù),但是考慮到 socket 可能為前面討論到的三種方式中的某一種,因此對數(shù)據(jù)緩沖區(qū)的結構有相應的處理,主要表現(xiàn)在加工模式下對偽鏈路層頭部的合成。具體代碼分析如下:






    static int
    pcap_read_packet(pcap_t *handle, pcap_handler callback, u_char *userdata)
    {
    /* 數(shù)據(jù)包緩沖區(qū)指針 */
    u_char * bp;

    /* bp 與捕獲句柄 pcap_t 中 handle->buffer
    之間的偏移量,其目的是為在加工模式捕獲情況下,為合成的偽數(shù)據(jù)鏈路層頭部留出空間 */
    int offset;

    /* PACKET_SOCKET 方式下,recvfrom() 返回 scokaddr_ll 類型,而在SOCK_PACKET 方式下,
    返回 sockaddr 類型 */
    #ifdef HAVE_PF_PACKET_SOCKETS
    struct sockaddr_ll from;
    struct sll_header * hdrp;
    #else
    struct sockaddr from;
    #endif

    socklen_t fromlen;
    int packet_len, caplen;

    /* libpcap 自定義的頭部 */
    struct pcap_pkthdr pcap_header;

    #ifdef HAVE_PF_PACKET_SOCKETS
    /* 如果是加工模式,則為合成的鏈路層頭部留出空間 */
    if (handle->md.cooked)
    offset = SLL_HDR_LEN;

    /* 其它兩中方式下,鏈路層頭部不做修改的被返回,不需要留空間 */
    else
    offset = 0;
    #else
    offset = 0;
    #endif

    bp = handle->buffer + handle->offset;

    /* 從內核中接收一個數(shù)據(jù)包,注意函數(shù)入?yún)⒅袑?bp 的位置進行修正 */
    packet_len = recvfrom( handle->fd, bp + offset,
    handle->bufsize - offset, MSG_TRUNC,
    (struct sockaddr *) &from, &fromlen);

    #ifdef HAVE_PF_PACKET_SOCKETS

    /* 如果是回路設備,則只捕獲接收的數(shù)據(jù)包,而拒絕發(fā)送的數(shù)據(jù)包。顯然,我們只能在 PF_PACKET
    方式下這樣做,因為 SOCK_PACKET 方式下返回的鏈路層地址類型為
    sockaddr_pkt,缺少了判斷數(shù)據(jù)包類型的信息。*/
    if (!handle->md.sock_packet &&
    from.sll_ifindex == handle->md.lo_ifindex &&
    from.sll_pkttype == PACKET_OUTGOING)
    return 0;
    #endif

    #ifdef HAVE_PF_PACKET_SOCKETS
    /* 如果是加工模式,則合成偽鏈路層頭部 */
    if (handle->md.cooked) {
    /* 首先修正捕包數(shù)據(jù)的長度,加上鏈路層頭部的長度 */
    packet_len += SLL_HDR_LEN;
    hdrp = (struct sll_header *)bp;

    /* 以下的代碼分別對偽鏈路層頭部的數(shù)據(jù)賦值 */
    hdrp->sll_pkttype = xxx;
    hdrp->sll_hatype = htons(from.sll_hatype);
    hdrp->sll_halen = htons(from.sll_halen);
    memcpy(hdrp->sll_addr, from.sll_addr,
    (from.sll_halen > SLL_ADDRLEN) ?
    SLL_ADDRLEN : from.sll_halen);
    hdrp->sll_protocol = from.sll_protocol;
    }
    #endif

    /* 修正捕獲的數(shù)據(jù)包的長度,根據(jù)前面的討論,SOCK_PACKET 方式下長度可能是不準確的 */
    caplen = packet_len;
    if (caplen > handle->snapshot)
    caplen = handle->snapshot;

    /* 如果沒有使用內核級的包過濾,則在用戶空間進行過濾*/
    if (!handle->md.use_bpf && handle->fcode.bf_insns) {
    if (bpf_filter(handle->fcode.bf_insns, bp,
    packet_len, caplen) == 0)
    {
    /* 沒有通過過濾,數(shù)據(jù)包被丟棄 */
    return 0;
    }
    }

    /* 填充 libpcap 自定義數(shù)據(jù)包頭部數(shù)據(jù):捕獲時間,捕獲的長度,真實的長度 */
    ioctl(handle->fd, SIOCGSTAMP, &pcap_header.ts);
    pcap_header.caplen = caplen;
    pcap_header.len = packet_len;

    /* 累加捕獲數(shù)據(jù)包數(shù)目,注意到在不同內核/捕獲方式情況下數(shù)目可能不準確 */
    handle->md.stat.ps_recv++;

    /* 調用用戶定義的回調函數(shù) */
    callback(userdata, &pcap_header, bp);
    }

    數(shù)據(jù)包過濾機制

    大量的網(wǎng)絡監(jiān)控程序目的不同,期望的數(shù)據(jù)包類型也不同,但絕大多數(shù)情況都都只需要所有數(shù)據(jù)包的一(?。┎糠?。例如:對郵件系統(tǒng)進行監(jiān)控可能只需要端口號為 25(smtp)和 110(pop3) 的 TCP 數(shù)據(jù)包,對 DNS 系統(tǒng)進行監(jiān)控就只需要端口號為 53 的 UDP數(shù)據(jù)包。包過濾機制的引入就是為了解決上述問題,用戶程序只需簡單的設置一系列過濾條件,最終便能獲得滿足條件的數(shù)據(jù)包。包過濾操作可以在用戶空間執(zhí)行,也可以在內核空間執(zhí)行,但必須注意到數(shù)據(jù)包從內核空間拷貝到用戶空間的開銷很大,所以如果能在內核空間進行過濾,會極大的提高捕獲的效率。內核過濾的優(yōu)勢在低速網(wǎng)絡下表現(xiàn)不明顯,但在高速網(wǎng)絡下是非常突出的。在理論研究和實際應用中,包捕獲和包過濾從語意上并沒有嚴格的區(qū)分,關鍵在于認識到捕獲數(shù)據(jù)包必然有過濾操作?;旧峡梢哉J為,包過濾機制在包捕獲機制中占中心地位。

    包過濾機制實際上是針對數(shù)據(jù)包的布爾值操作函數(shù),如果函數(shù)最終返回true,則通過過濾,反之則被丟棄。形式上包過濾由一個或多個謂詞判斷的并操作(AND)和或操作(OR)構成,每一個謂詞判斷基本上對應了數(shù)據(jù)包的協(xié)議類型或某個特定值,例如:只需要 TCP 類型且端口為110的數(shù)據(jù)包或ARP類型的數(shù)據(jù)包。包過濾機制在具體的實現(xiàn)上與數(shù)據(jù)包的協(xié)議類型并無多少關系,它只是把數(shù)據(jù)包簡單的看成一個字節(jié)數(shù)組,而謂詞判斷會根據(jù)具體的協(xié)議映射到數(shù)組特定位置的值。如判斷ARP類型數(shù)據(jù)包,只需要判斷數(shù)組中第 13、14 個字節(jié)(以太頭中的數(shù)據(jù)包類型)是否為0X0806。從理論研究的意思上看,包過濾機制是一個數(shù)學問題,或者說是一個算法問題,其中心任務是如何使用最少的判斷操作、最少的時間完成過濾處理,提高過濾效率。

    BPF

    Libpcap 重點使用 BPF(BSD Packet Filter)包過濾機制,BPF 于 1992 年被設計出來,其設計目的主要是解決當時已存在的過濾機制效率低下的問題。BPF的工作步驟如下:當一個數(shù)據(jù)包到達網(wǎng)絡接口時,數(shù)據(jù)鏈路層的驅動會把它向系統(tǒng)的協(xié)議棧傳送。但如果 BPF 監(jiān)聽接口,驅動首先調用 BPF。BPF 首先進行過濾操作,然后把數(shù)據(jù)包存放在過濾器相關的緩沖區(qū)中,最后設備驅動再次獲得控制。注意到BPF是先對數(shù)據(jù)包過濾再緩沖,避免了類似sun的NIT過濾機制先緩沖每個數(shù)據(jù)包直到用戶讀數(shù)據(jù)時再過濾所造成的效率問題。參考資料D是關于BPF設計思想最重要的文獻。

    BPF 的設計思想和當時的計算機硬件的發(fā)展有很大聯(lián)系,相對老式的過濾方式CSPF(CMU/Stanford Packet Filter)它有兩大特點。1:基于寄存器的過濾機制,而不是早期內存堆棧過濾機制,2:直接使用獨立的、非共享的內存緩沖區(qū)。同時,BPF 在過濾算法是也有很大進步,它使用無環(huán)控制流圖(CFG control flow graph),而不是老式的布爾表達式樹(boolean expression tree)。布爾表達式樹理解上比較直觀,它的每一個葉子節(jié)點即是一個謂詞判斷,而非葉子節(jié)點則為 AND 操作或 OR操作。CSPF有三個主要的缺點。1:過濾操作使用的棧在內存中被模擬,維護棧指針需要使用若干的加/減等操作,而內存操作是現(xiàn)代計算機架構的主要瓶頸。2:布爾表達式樹造成了不需要的重復計算。3:不能分析數(shù)據(jù)包的變長頭部。BPF 使用的CFG 算法實際上是一種特殊的狀態(tài)機,每一節(jié)點代表了一個謂詞判斷,而左右邊分別對應了判斷失敗和成功后的跳轉,跳轉后又是謂詞判斷,這樣反復操作,直到到達成功或失敗的終點。CFG算法的優(yōu)點在于把對數(shù)據(jù)包的分析信息直接建立在圖中,從而不需要重復計算。直觀的看,CFG 是一種"快速的、一直向前"的算法。

    過濾代碼的編譯

    BPF 對 CFG 算法的代碼實現(xiàn)非常復雜,它使用偽機器方式。BPF 偽機器是一個輕量級的,高效的狀態(tài)機,對 BPF 過濾代碼進行解釋處理。BPF 過濾代碼形式為"opcode jt jfk",分別代表了操作碼和尋址方式、判斷正確的跳轉、判斷失敗的跳轉、操作使用的通用數(shù)據(jù)域。BPF 過濾代碼從邏輯上看很類似于匯編語言,但它實際上是機器語言,注意到上述 4 個域的數(shù)據(jù)類型都是 int 和 char 型。顯然,由用戶來寫過濾代碼太過復雜,因此 libpcap 允許用戶書寫高層的、容易理解的過濾字符串,然后將其編譯為BPF代碼。

    Libpcap使用了4個源程序gencode.c、optimize.c、grammar.c、scanner.c完成編譯操作,其中前兩個實現(xiàn)了對過濾字符串的編譯和優(yōu)化,后兩個主要是為編譯提供從協(xié)議相關過濾條件到協(xié)議無關(的字符數(shù)組)位置信息的映射,并且它們由詞匯分析器生成器 flex 和 bison 生成。參考資料 C 有對此兩個工具的講解。






    flex -Ppcap_ -t scanner.l > $$.scanner.c; mv $$.scanner.c scanner.c
    bison -y -p pcap_ -d grammar.y
    mv y.tab.c grammar.c
    mv y.tab.h tokdefs.h

    編譯過濾字符串調用了函數(shù) pcap_compile()[getcode.c],形式為:







    int pcap_compile(pcap_t *p, struct bpf_program *program,
      char *buf, int optimize, bpf_u_int32 mask)


    其中 buf 指向用戶過濾字符串,編譯后的 BPF 代碼存在在結構 bpf_program中,標志 optimize 指示是否對 BPF 代碼進行優(yōu)化。






    /* [pcap-bpf.h] */
    struct bpf_program {
    u_int bf_len; /* BPF 代碼中謂詞判斷指令的數(shù)目 */
    struct bpf_insn *bf_insns; /* 第一個謂詞判斷指令 */
    };

    /* 謂詞判斷指令結構,含意在前面已描述 [pcap-bpf.h] */
    struct bpf_insn {
    u_short code;
    u_char jt;
    u_char jf;
    bpf_int32 k;
    };
    過濾代碼的安裝

    前面我們曾經提到,在內核空間過濾數(shù)據(jù)包對整個捕獲機制的效率是至關重要的。早期使用 SOCK_PACKET 方式的 Linux 不支持內核過濾,因此過濾操作只能在用戶空間執(zhí)行(請參閱函數(shù) pcap_read_packet() 代碼),在《UNIX 網(wǎng)絡編程(第一卷)》(參考資料 B)的第 26 章中對此有明確的描述。不過現(xiàn)在看起來情況已經發(fā)生改變,linux 在 PF_PACKET 類型的 socket 上支持內核過濾。Linux 內核允許我們把一個名為 LPF(Linux Packet Filter) 的過濾器直接放到 PF_PACKET 類型 socket 的處理過程中,過濾器在網(wǎng)卡接收中斷執(zhí)行后立即執(zhí)行。LSF 基于BPF機制,但兩者在實現(xiàn)上有略微的不同。實際代碼如下:






    /* 在包捕獲設備上附加 BPF 代碼 [pcap-linux.c]*/
    static int
    pcap_setfilter_linux(pcap_t *handle, struct bpf_program *filter)
    {
    #ifdef SO_ATTACH_FILTER
    struct sock_fprog fcode;
    int can_filter_in_kernel;
    int err = 0;
    #endif

    /* 檢查句柄和過濾器結構的正確性 */
    if (!handle)
    return -1;
    if (!filter) {
    strncpy(handle->errbuf, "setfilter: No filter specified",
    sizeof(handle->errbuf));
    return -1;
    }

    /* 具體描述如下 */
    if (install_bpf_program(handle, filter) < 0)
    return -1;

    /* 缺省情況下在用戶空間運行過濾器,但如果
    在內核安裝成功,則值為 1 */
    handle->md.use_bpf = 0;


    /* 嘗試在內核安裝過濾器 */
    #ifdef SO_ATTACH_FILTER
    #ifdef USHRT_MAX
    if (handle->fcode.bf_len > USHRT_MAX) {
    /*過濾器代碼太長,內核不支持 */
    fprintf(stderr, "Warning: Filter too complex for kernel\n");
    fcode.filter = NULL;
    can_filter_in_kernel = 0;
    } else
    #endif /* USHRT_MAX */
    {
    /* linux 內核設置過濾器時使用的數(shù)據(jù)結構是 sock_fprog,
    而不是 BPF 的結構 bpf_program ,因此應做結構之間的轉換 */
    switch (fix_program(handle, &fcode)) {

    /* 嚴重錯誤,直接退出 */
    case -1:
    default:
    return -1;

    /* 通過檢查,但不能工作在內核中 */
    case 0:
    can_filter_in_kernel = 0;
    break;

    /* BPF 可以在內核中工作 */
    case 1:
    can_filter_in_kernel = 1;
    break;
    }
    }

    /* 如果可以在內核中過濾,則安裝過濾器到內核中 */
    if (can_filter_in_kernel) {
    if ((err = set_kernel_filter(handle, &fcode)) == 0)
    {
    /* 安裝成功 !!! */
    handle->md.use_bpf = 1;
    }
    else if (err == -1) /* 出現(xiàn)非致命性錯誤 */
    {
    if (errno != ENOPROTOOPT && errno != EOPNOTSUPP) {
    fprintf(stderr, "Warning: Kernel filter failed:
    %s\n",pcap_strerror(errno));
    }
    }
    }

    /* 如果不能在內核中使用過濾器,則去掉曾經可能在此 socket
    上安裝的內核過濾器。主要目的是為了避免存在的過濾器對數(shù)據(jù)包過濾的干擾 */
    if (!handle->md.use_bpf)
    reset_kernel_filter(handle);[pcap-linux.c]
    #endif
    }


    /* 把 BPF 代碼拷貝到 pcap_t 數(shù)據(jù)結構的 fcode 上 */
    int install_bpf_program(pcap_t *p, struct bpf_program *fp)
    {
    size_t prog_size;

    /* 首先釋放可能已存在的 BPF 代碼 */
    pcap_freecode(&p->fcode);

    /* 計算過濾代碼的長度,分配內存空間 */
    prog_size = sizeof(*fp->bf_insns) * fp->bf_len;
    p->fcode.bf_len = fp->bf_len;
    p->fcode.bf_insns = (struct bpf_insn *)malloc(prog_size);
    if (p->fcode.bf_insns == NULL) {
    snprintf(p->errbuf, sizeof(p->errbuf),
    "malloc: %s", pcap_strerror(errno));
    return (-1);
    }

    /* 把過濾代碼保存在捕獲句柄中 */
    memcpy(p->fcode.bf_insns, fp->bf_insns, prog_size);

    return (0);
    }

    /* 在內核中安裝過濾器 */
    static int set_kernel_filter(pcap_t *handle, struct sock_fprog *fcode)
    {
    int total_filter_on = 0;
    int save_mode;
    int ret;
    int save_errno;

    /*在設置過濾器前,socket 的數(shù)據(jù)包接收隊列中可能已存在若干數(shù)據(jù)包。當設置過濾器后,
    這些數(shù)據(jù)包極有可能不滿足過濾條件,但它們不被過濾器丟棄。
    這意味著,傳遞到用戶空間的頭幾個數(shù)據(jù)包不滿足過濾條件。
    注意到在用戶空間過濾這不是問題,因為用戶空間的過濾器是在包進入隊列后執(zhí)行的。
    Libpcap 解決這個問題的方法是在設置過濾器之前,
    首先讀完接收隊列中所有的數(shù)據(jù)包。具體步驟如下。*/

    /*為了避免無限循環(huán)的情況發(fā)生(反復的讀數(shù)據(jù)包并丟棄,
    但新的數(shù)據(jù)包不停的到達),首先設置一個過濾器,阻止所有的包進入 */

    setsockopt(handle->fd, SOL_SOCKET, SO_ATTACH_FILTER,
    &total_fcode, sizeof(total_fcode);

    /* 保存 socket 當前的屬性 */
    save_mode = fcntl(handle->fd, F_GETFL, 0);

    /* 設置 socket 它為非阻塞模式 */
    fcntl(handle->fd, F_SETFL, save_mode | O_NONBLOCK);

    /* 反復讀隊列中的數(shù)據(jù)包,直到沒有數(shù)據(jù)包可讀。這意味著接收隊列已被清空 */
    while (recv(handle->fd, &drain, sizeof drain, MSG_TRUNC) >= 0);

    /* 恢復曾保存的 socket 屬性 */
    fcntl(handle->fd, F_SETFL, save_mode);

    /* 現(xiàn)在安裝新的過濾器 */
    setsockopt(handle->fd, SOL_SOCKET, SO_ATTACH_FILTER,
    fcode, sizeof(*fcode));
    }

    /* 釋放 socket 上可能有的內核過濾器 */
    static int reset_kernel_filter(pcap_t *handle)
    {
    int dummy;
    return setsockopt(handle->fd, SOL_SOCKET, SO_DETACH_FILTER,
    &dummy, sizeof(dummy));
    }


    linux 在安裝和卸載過濾器時都使用了函數(shù) setsockopt(),其中標志SOL_SOCKET 代表了對 socket 進行設置,而 SO_ATTACH_FILTER 和 SO_DETACH_FILTER 則分別對應了安裝和卸載。下面是 linux 2.4.29 版本中的相關代碼:






    [net/core/sock.c]
    #ifdef CONFIG_FILTER
    case SO_ATTACH_FILTER:
    ……
    /* 把過濾條件結構從用戶空間拷貝到內核空間 */
    if (copy_from_user(&fprog, optval, sizeof(fprog)))
    break;
    /* 在 socket 上安裝過濾器 */
    ret = sk_attach_filter(&fprog, sk);
    ……

    case SO_DETACH_FILTER:
    /* 使用自旋鎖鎖住 socket */
    spin_lock_bh(&sk->lock.slock);

    filter = sk->filter;
    /* 如果在 socket 上有過濾器,則簡單設置為空,并釋放過濾器內存 */
    if (filter) {
    sk->filter = NULL;
    spin_unlock_bh(&sk->lock.slock);
    sk_filter_release(sk, filter);
    break;
    }
    spin_unlock_bh(&sk->lock.slock);
    ret = -ENONET;
    break;
    #endif


    上面出現(xiàn)的 sk_attach_filter() 定義在 net/core/filter.c,它把結構sock_fprog 轉換為結構 sk_filter, 最后把此結構設置為 socket 的過濾器:sk->filter = fp。

    其他代碼

    libpcap 還提供了其它若干函數(shù),但基本上是提供輔助或擴展功能,重要性相對弱一點。我個人認為,函數(shù) pcap_dump_open() 和 pcap_open_offline() 可能比較有用,使用它們能把在線的數(shù)據(jù)包寫入文件并事后進行分析處理。

    總結

    1994 年libpcap 的第一個版本被發(fā)布,到現(xiàn)在已有 11 年的歷史,如今libpcap 被廣泛的應用在各種網(wǎng)絡監(jiān)控軟件中。Libpcap 最主要的優(yōu)點在于平臺無關性,用戶程序幾乎不需做任何改動就可移植到其它 unix 平臺上;其次,libpcap也能適應各種過濾機制,特別對BPF的支持最好。分析它的源代碼,可以學習開發(fā)者優(yōu)秀的設計思想和實現(xiàn)技巧,也能了解到(linux)操作系統(tǒng)的網(wǎng)絡內核實現(xiàn),對個人能力的提高有很大幫助。
    相關文章
    本頁查看次數(shù):