UNIX網絡編程——原始套接字的魔力【下】

能夠接收鏈路層MAC幀的原始套接字linux

       前面咱們介紹過了經過原始套接字socket(AF_INET, SOCK_RAW, protocol)咱們能夠直接實現自行構造整個IP報文,而後對其收發。提醒一點,在用這種方式構造原始IP報文時,第三個參數protocol不能用IPPROTO_IP,這樣會讓系統疑惑,不知道該用什麼協議來伺候你了。網絡

       今天咱們介紹原始套接字的另外一種用法:直接從鏈路層收發數據幀,聽起來好像很神奇的樣子。在Linux系統中要從鏈路層(MAC)直接收發數幀,比較廣泛的作法就是用libpcap和libnet兩個動態庫來實現。但今天咱們就要用原始套接字來實現這個功能。多線程

  

       這裏的2字節幀類型用來指示該數據幀所承載的上層協議是IP、ARP或其餘。socket

       爲了實現直接從鏈路層收發數據幀,咱們要用到原始套接字的以下形式:函數

socket(PF_PACKET, type, protocol);

一、其中type字段可取SOCK_RAWSOCK_DGRAM。它們兩個都使用一種與設備無關的標準物理層地址結構struct sockaddr_ll{},但具體操做的報文格式不一樣:工具

  • SOCK_RAW:直接向網絡硬件驅動程序發送(或從網絡硬件驅動程序接收)沒有任何處理的完整數據報文(包括物理幀的幀頭),這就要求咱們必須瞭解對應設備的物理幀幀頭結構,才能正確地裝載和分析報文。也就是說咱們用這種套接字從網卡驅動上收上來的報文包含了MAC頭部,若是咱們要用這種形式的套接字直接向網卡發送數據幀,那麼咱們必須本身組裝咱們MAC頭部。這正符合咱們的需求。
  • SOCK_DGRAM:這種類型的套接字對於收到的數據報文的物理幀幀頭會被系統自動去掉,而後再將其往協議棧上層傳遞;一樣地,在發送時數據時,系統將會根據sockaddr_ll結構中的目的地址信息爲數據報文添加一個合適的MAC幀頭。

二、protocol字段,常見的,通常狀況下該字段取ETH_P_IP,ETH_P_ARP,ETH_P_RARP或ETH_P_ALL,固然鏈路層協議不少,確定不止咱們說的這幾個,但咱們通常只關心這幾個就夠咱們用了。這裏簡單提一下網絡數據收發的一點基礎。協議棧在組織數據收發流程時須要處理好兩個方面的問題:」從上到下」,即數據發送的任務;「從下到上」,即數據接收的任務。數據發送相對接收來講要容易些,由於對於數據接收而言,網卡驅動還要明確什麼樣的數據該接收、什麼樣的不應接收等問題。protocol字段可選的四個值及其意義以下:spa

protocol線程

code

做用對象

   ETH_P_IP                           

                0X0800

                               只接收發往目的MAC是本機的IP類型的數據幀

ETH_P_ARP

0X0806

                               只接收發往目的MAC是本機的ARP類型的數據幀

 ETH_P_RARP                            

0X8035

                               只接受發往目的MAC是本機的RARP類型的數據幀

ETH_P_ALL

0X0003

接收發往目的MAC是本機的全部類型(ip,arp,rarp)的數據幀,同時還能夠接收從本機發出去的全部數據幀。在混雜模式打開的狀況下,還會接收到發往目的MAC爲非本地硬件地址的數據幀。

       protocol字段可取的全部協議參見/usr/include/linux/if_ether.h頭文件裏的定義。

       最後,格外須要留心一點的就是,發送數據的時候須要本身組織整個以太網數據幀。和地址相關的結構體就不能再用前面的struct sockaddr_in{}了,而是struct sockaddr_ll{},以下:

struct sockaddr_ll{ 
    unsigned short sll_family; /* 老是 AF_PACKET */ 
    unsigned short sll_protocol; /* 物理層的協議 */ 
    int sll_ifindex; /* 接口號 */ 
    unsigned short sll_hatype; /* 報頭類型 */ 
    unsigned char sll_pkttype; /* 分組類型 */ 
    unsigned char sll_halen; /* 地址長度 */ 
    unsigned char sll_addr[8]; /* 物理層地址 */ 
};

       sll_protocoll:取值在linux/if_ether.h中,能夠指定咱們所感興趣的二層協議;

       sll_ifindex:置爲0表示處理全部接口,對於單網卡的機器就不存在「全部」的概念了。若是你有多網卡,該字段的值通常經過ioctl來搞定,模板代碼以下,若是咱們要獲取eth0接口的序號,可使用以下代碼來獲取:

struct  sockaddr_ll  sll;
struct ifreq ifr;

strcpy(ifr.ifr_name, "eth0");
ioctl(sockfd, SIOCGIFINDEX, &ifr);
sll.sll_ifindex = ifr.ifr_ifindex;

       sll_hatype:ARP硬件地址類型,定義在 linux/if_arp.h 中。 取ARPHRD_ETHER時表示爲以太網

       sll_pkttype:包含分組類型。目前,有效的分組類型有:目標地址是本地主機的分組用的 PACKET_HOST,物理層廣播分組用的PACKET_BROADCAST ,發送到一個物理層多路廣播地址的分組用的 PACKET_MULTICAST,在混雜(promiscuous)模式下的設備驅動器發向其餘主機的分組用的 PACKET_OTHERHOST,源於本地主機的分組被環回到分組套接口用的 PACKET_OUTGOING這些類型只對接收到的分組有意義

       sll_addrsll_halen指示物理層(如以太網,802.3,802.4或802.5等)地址及其長度,嚴格依賴於具體的硬件設備。相似於獲取接口索引sll_ifindex要獲取接口的物理地址,能夠採用以下代碼:

struct ifreq ifr;

strcpy(ifr.ifr_name, "eth0");
ioctl(sockfd, SIOCGIFHWADDR, &ifr);

       缺省狀況下,從任何接口收到的符合指定協議的全部數據報文都會被傳送到原始PACKET套接字口,而使用bind系統調用並以一個sochddr_ll結構體對象將PACKET套接字與某個網絡接口相綁定,就可以使咱們的PACKET原始套接字只接收指定接口的數據報文。 


       接下來咱們簡單介紹一下網卡是怎麼收報的,若是你對這部分已經很瞭解能夠跳過這部份內容。網卡從線路上收到信號流,網卡的驅動程序會去檢查數據幀開始的前6個字節,即目的主機的MAC地址,若是和本身的網卡地址一致它纔會接收這個幀,不符合的通常都是直接無視。而後該數據幀會被網絡驅動程序分解,IP報文將經過網絡協議棧,最後傳送到應用程序那裏。往上層傳遞的過程就是一個校驗和「剝頭」的過程,由協議棧各層去實現。


       接下來咱們來寫個簡單的抓包程序,將那些發給本機的IPv4報文全打印出來:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/if_ether.h>

int main(int argc, char **argv) {
   int sock, n;
   char buffer[2048];
   struct ethhdr *eth;
   struct iphdr *iph;

   if (0>(sock=socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP)))) {
     perror("socket");
     exit(1);
   }

   while (1) {
     printf("=====================================\n");
     //注意:在這以前我沒有調用bind函數,緣由是什麼呢?
     n = recvfrom(sock,buffer,2048,0,NULL,NULL);
     printf("%d bytes read\n",n);

     //接收到的數據幀頭6字節是目的MAC地址,緊接着6字節是源MAC地址。
     eth=(struct ethhdr*)buffer;
     printf("Dest MAC addr:%02x:%02x:%02x:%02x:%02x:%02x\n",eth->h_dest[0],eth->h_dest[1],eth->h_dest[2],eth->h_dest[3],eth->h_dest[4],eth->h_dest[5]);
     printf("Source MAC addr:%02x:%02x:%02x:%02x:%02x:%02x\n",eth->h_source[0],eth->h_source[1],eth->h_source[2],eth->h_source[3],eth->h_source[4],eth->h_source[5]);

     iph=(struct iphdr*)(buffer+sizeof(struct ethhdr));
     //咱們只對IPV4且沒有選項字段的IPv4報文感興趣
     if(iph->version ==4 && iph->ihl == 5){
             printf("Source host:%s\n",inet_ntoa(iph->saddr));
             printf("Dest host:%s\n",inet_ntoa(iph->daddr));
     }
   }
}
       編譯,而後運行,要以root身份才能夠運行該程序:

                       
                                

       正如咱們前面看到的,網卡丟棄全部不含有主機MAC地址00:0c:29:88:e0:1f的數據包,這是由於網卡處於非混雜模式,即每一個網卡只處理源地址是它本身的幀!


       這裏有三個例外的狀況:

一、若是一個幀的目的MAC地址是一個受限的廣播地址(255.255.255.255)那麼它將被全部的網卡接收。

二、若是一個幀的目的地址是組播地址,那麼它將被那些打開組播接收功能的網卡所接收。

三、網卡如被設置成混雜模式,那麼它將接收全部流經它的數據包。


       前面咱們恰好提到過網卡的 混雜模式,如今咱們就來火燒眉毛的實踐一哈看看混雜模式是否可讓咱們抓到全部數據包,只要在while循環前加上以下代碼就OK了:
struct ifreq ethreq;
… …
strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);
if(-1 == ioctl(sock,SIOCGIFFLAGS,&ethreq)){ //獲取接口標誌
     perror("ioctl");
     close(sock);
     exit(1);
}
ethreq.ifr_flags |=IFF_PROMISC; //IFF_PROMISC 這個標誌設置(由網絡代碼)來激活混雜操做
if(-1 == ioctl(sock,SIOCGIFFLAGS,&ethreq)){ //獲取接口標誌
     perror("ioctl");
     close(sock);
     exit(1);
}
while(1){
   … …
}


       至此,咱們一個網絡抓包工具的雛形就出現了。你們能夠基於此作更多的練習,加上多線程機制,對收到的不一樣類型的數據包作不一樣處理等等,反正由你發揮的空間是至關滴大,「狐狸未成精,只因太年輕」。把這塊吃透了,後面理解協議棧就會至關輕鬆。
相關文章
相關標籤/搜索