2016年11月3日 星期四

libpcap - List devices(2)


libpcap可以列出所有網卡deivce、地址資訊。

先看一下自己主機上有哪些device:
~ % ifconfig -l
lo0 gif0 stf0 en0 en1 en2 p2p0 awdl0 bridge0
目前可用的網卡有這些,不同系統上有不同的名稱,以Ethernet來說:Mac OS X是en*,Linux是eth*,而Windows是\Device\NPF_{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}



那來寫程式吧。


include的header
#include <stdio.h>
#include <stdlib.h>
#include <pcap.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#ifndef __linux
#include <net/if.h>
#include <netinet/in.h>
#include <net/if_dl.h>
#include <net/ethernet.h>
#else /* if BSD */
#include <linux/if_ether.h>
#include <netpacket/packet.h>
#include <linux/if_link.h>
#include <netinet/ether.h>
#endif /* if linux */

#ifndef __linux ... #else ... #endif是判斷核心是BSD還是linux,Mac OS X核心算是BSD來的,CentOS核心算是linux的,當為linux會定義__linux,所以只要用一些macro就可以判斷了。

有些比較底層的部分會因為核心不同實作上也會不同,所以偶爾需要靠macro的方法判斷;這程式會需要解析link層的地址,像這個就需要用這技巧了。


pcap_if_t *devices = NULL;
char errbuf[PCAP_ERRBUF_SIZE];

宣告一下變數,pcap_if_t *是主要儲存所有device資訊的結構體,本身就是一個link list的結構;char errbuf[]在一些libpcap初始化時候會需要的儲存錯誤訊息字串。

結構pcap_if_t宣告:
typedef struct pcap_if pcap_if_t;
/*
 * Item in a list of interfaces.
 */
struct pcap_if {
    struct pcap_if *next;
    char *name;        /* name to hand to "pcap_open_live()" */
    char *description;    /* textual description of interface, or NULL */
    struct pcap_addr *addresses;
    bpf_u_int32 flags;    /* PCAP_IF_ interface flags */
};


函數pcap_findalldevs()能夠取得所有可用的device。
//get all devices
if(-1 == pcap_findalldevs(&devices, errbuf)) {
    fprintf(stderr, "pcap_findalldevs(): %s\n", errbuf);
    exit(1);
}//end if

函數pcap_findalldevs()原型:
int pcap_findalldevs(pcap_if_t **alldevsp, char *errbuf);
  • 返回值:成功傳回0,失敗傳回-1,錯誤訊息在errbuf。
  • 參數:alldevsp介面的鏈結指標的指標。errbuf錯誤訊息。
  • 功能:用來取得目前可用的device。


成功取得後就用迴圈來走訪link list吧。
//list all device
for(pcap_if_t *d = devices ; d ; d = d->next) {
    printf("Device: %s\n", d->name);
    if(d->description) {
        printf("    Description: %s\n", d->description);
    }//end if
    printf("    Loopback: %s\n",(d->flags & PCAP_IF_LOOPBACK) ? "yes" : "no");
#ifdef PCAP_IF_UP
    printf("    Up: %s\n",(d->flags & PCAP_IF_UP) ? "yes" : "no");
#endif /* if PCAP_IF_UP */
#ifdef PCAP_IF_RUNNING
    printf("    Running: %s\n",(d->flags & PCAP_IF_RUNNING) ? "yes" : "no");
#endif /* if PCAP_IF_RUNNING */

pcap_if_t *的name成員是device的名稱,description是描述,成員flags可以用來判斷是不是loopback,在一些系統上還有PCAP_IF_UPPCAP_IF_RUNNING可以使用。


成員addresses也是一個link list結構,所以一樣用迴圈走訪。
    //list all address
    for(struct pcap_addr *a = d->addresses ; a ; a = a->next) {

結構struct pcap_addr宣告:
/*
 * Representation of an interface address.
 */
struct pcap_addr {
    struct pcap_addr *next;
    struct sockaddr *addr;        /* address */
    struct sockaddr *netmask;    /* netmask for that address */
    struct sockaddr *broadaddr;    /* broadcast address for that address */
    struct sockaddr *dstaddr;    /* P2P destination address for that address */
};


宣告一下會用到變數。
        sa_family_t family = a->addr->sa_family;
        char ntop_buf[256];

sa_family_t是地址類型家族,常見的有AF_INET(PF_INET)AF_INET6(PF_INET6)AF_LINKAF_PACKET(PF_PACKET)以及AF_UNIX(PF_UNIX),先用變數family來記錄目前地址的類型。


        if(family == AF_INET || family == AF_INET6) {
            if(a->addr) {
                printf("        Address: %s\n",
                       inet_ntop(family, &((struct sockaddr_in *)a->addr)->sin_addr, ntop_buf, sizeof(ntop_buf)));
            }//end if
            if(a->netmask) {
                printf("        Netmask: %s\n",
                       inet_ntop(family, &((struct sockaddr_in *)a->netmask)->sin_addr, ntop_buf, sizeof(ntop_buf)));
            }//end if
            if(a->broadaddr) {
                printf("        Broadcast Address: %s\n",
                       inet_ntop(family, &((struct sockaddr_in *)a->broadaddr)->sin_addr, ntop_buf, sizeof(ntop_buf)));
            }//end if
            if(a->dstaddr) {
                printf("        Destination Address: %s\n",
                       inet_ntop(family, &((struct sockaddr_in *)a->dstaddr)->sin_addr, ntop_buf, sizeof(ntop_buf)));
            }//end if
        }//end else


當family為AF_INET(表示IPv4)或AF_INET6(表示IPv6)時候用函數inet_ntop()來將網路地址格式(一個無號的32bit整數)轉成字串格式。

函數inet_ntop()原型:
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • 返回值:成功傳回dst參數,失敗傳回NULL。
  • 參數:af只有AF_INET或AF_INET6表示不是IPv4就是IPv6。src網路順序地址。dst結果字串。size結果字串陣列長度。
  • 功能:將網路順序的地址轉成字串,dst陣列大小如果是IPv4可以用INET_ADDRSTRLEN,IPv6則是INET6_ADDRSTRLEN這兩個macros。


#ifdef AF_LINK
        else if(family == AF_LINK && a->addr) {
            struct sockaddr_dl *sdl = (struct sockaddr_dl *)a->addr;
            if (sdl->sdl_family == AF_LINK && sdl->sdl_alen == ETHER_ADDR_LEN) {
                printf("        Link address: %s\n", ether_ntoa((const struct ether_addr *)(sdl->sdl_data + sdl->sdl_nlen)));
                //printf("        Link address: %s\n", ether_ntoa((const struct ether_addr *)LLADDR(sdl)));
            }//end if
        }//end if
#elif AF_PACKET
        else if(family == AF_PACKET && a->addr) {
            struct sockaddr_ll *sll = (struct sockaddr_ll *)a->addr;
            if (sll->sll_family == AF_PACKET && sll->sll_halen == ETHER_ADDR_LEN) {
                printf("        Link address: %s\n", ether_ntoa((const struct ether_addr *)sll->sll_addr));
            }//end if
        }//end if
#endif

函數pcap_findalldevs()可以取得link層的地址,但是解析方法BSD和linux不太相同,BSD定義了AF_LINK而linux定義了AF_PACKET表示link層的類型,所以這邊用這兩個marcos來判斷屬於哪種類型。

當是AF_LINK類型時,宣告struct sockaddr_dl *表示link層地址,當成員sdl_family為AF_LINK且成員sdl_alen長度為6(ETHER_ADDR_LEN),表示可能為Ethernet地址,所以使用函數ether_ntoa()來將十六進位格式(Binary)轉成字串格式。但是成員sdl_data包含了device name,所以必須加上成員sdl_nlen(device name長度)才會指向地址開始的位置(或是可以使用註解起來的macros LLADDR)。

函數ether_ntoa()原型:
char *ether_ntoa(const struct ether_addr *n);
  • 返回值:MAC address字串,失敗傳回NULL。
  • 參數:n要轉換的網路封包用的地址結構指標。
  • 功能:將網路封包用的地址結構指標轉成字串。

巨集LLADDR定義:
#define LLADDR(s) ((caddr_t)((s)->sdl_data + (s)->sdl_nlen))

如果是AF_PACKET情況,則宣告struct sockaddr_ll *,當成員sll_family為AF_PACKET且成員ssl_halen長度為6(ETHER_ADDR_LEN),就以Ethernet地址方式解析,成員sll_addr就已經是地址位置了。


最後要釋放取得的device link list。
//free
pcap_freealldevs(devices);

函數pcap_freealldevs()原型:
void pcap_freealldevs(pcap_if_t *alldevsp);
  • 參數:alldevsp介面的鏈結指標。
  • 功能:用來釋放從pcap_findalldevs()取得的device link list指標。


編譯方法用gcc編譯:
libpcap % gcc -I/usr/local/opt/libpcap/include -Wall -std=gnu99 -L/usr/local/opt/libpcap/lib -lpcap list-devices.c -o list-devices

  • -I:include header搜尋路徑;homebrew的libpcap預設安裝在/usr/local/opt/libpcap
  • -L:library搜尋路徑。
  • -l:要link的lib,libpcap的lib名稱為libpcap.a(或libpcap.so),所以指定-lpcap。
  • -o:輸出檔名。
  • -W:警告訊息,-Wall顯示所有警告。
  • -std:使用的規範。
其中參數-std請使用gnu99別用c99,不然在CentOS上libpcap會無法編譯。


執行結果(Mac OS X):
libpcap % ./list-devices 
Device: en0
    Loopback: no
    Up: yes
    Running: yes
        Link address: 6c:40:8:bc:ae:98

        Address: 0:0:fe80::6e40:8ff
        Netmask: ::ffff:ffff:ffff:ffff:0:0

        Address: 192.168.1.50
        Netmask: 255.255.255.0
        Broadcast Address: 192.168.1.255
...

執行結果(CentOS):
[root@tutu libpcap]# ./list-devices 
Device: eth0
    Loopback: no
        Link address: 0:c:29:b0:81:f7

        Address: 192.168.1.67
        Netmask: 255.255.255.0
        Broadcast Address: 192.168.1.255

        Address: 0:0:fe80::20c:29ff
        Netmask: ::ffff:ffff:ffff:ffff:0:0
...

在CentOS底下記得以root權限跑程式,因為要存取的link層需要有root權限(其實在Mac OS X上也需要,不過可以透過修改/dev/bpf*的權限讓一般使用者也可以透過libpcap存取到link層)。


Source code on Github

1 則留言: