能夠接收鏈路層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_RAW或SOCK_DGRAM。它們兩個都使用一種與設備無關的標準物理層地址結構struct sockaddr_ll{},但具體操做的報文格式不一樣:工具
二、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_addr和sll_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報文將經過網絡協議棧,最後傳送到應用程序那裏。往上層傳遞的過程就是一個校驗和「剝頭」的過程,由協議棧各層去實現。
#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)那麼它將被全部的網卡接收。
二、若是一個幀的目的地址是組播地址,那麼它將被那些打開組播接收功能的網卡所接收。
三、網卡如被設置成混雜模式,那麼它將接收全部流經它的數據包。
struct ifreq ethreq; … … strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ); if(-1 == ioctl(sock,SIOCGIFFLAGS,ðreq)){ //獲取接口標誌 perror("ioctl"); close(sock); exit(1); } ethreq.ifr_flags |=IFF_PROMISC; //IFF_PROMISC 這個標誌設置(由網絡代碼)來激活混雜操做 if(-1 == ioctl(sock,SIOCGIFFLAGS,ðreq)){ //獲取接口標誌 perror("ioctl"); close(sock); exit(1); } while(1){ … … }