Linux原始套接字----ping命令的實現

轉載自:http://blog.chinaunix.net/uid-26833883-id-3237924.html

 

Linux  原始套接字--myping的實現

 

一、套接字的類型

A.流套接字(SOCK_STREAM)

用於提供面向連接、可靠的數據傳輸服務,其使用傳輸層的TCP協議

B.數據報套接字(SOCK_DGRAM)

用於提供一個無連接、不可靠的服務,其使用傳輸層上的UDP協議

C.原始套接字(SOCK_RAM)

原始套接字是相對錶中套接字(即前面兩種套接字)而言的。它與標準套接字的區別是原始套接字可以讀寫內核沒有處理的IP數據包,流套接字只能讀取TCP協議的數據,數據報套接字只能讀取UDP協議的數據。

所以要訪問其他協議的數據必須使用原始套接字。


二、ping命令

ping命令是用來查看網絡上另一個主機系統的網絡連接是否正常的一個工具。
ping命令的工作原理:向網絡上的另一個主機系統發送ICMP報文,如果指定系統得到了報文,它將把報文一模一樣地傳回給發送者。

來看看在linux下使用ping命令的效果:



從上面可以看到,ping命令執行後顯示出被系統主機名(或域名)和相應 IP地址、返回給當前主機的ICMP報文順序號、ttl生存時間和往返時間rtt(單位是豪秒,即千分之一秒)。這些信息對我們後面要實現的myping有提示作用。

三、ICMP的介紹

ICMP(Internet Control Message,網際控制報文協議)是爲網關和目標主機而提供的一種差錯控制機制,使它們在遇到差錯時能把錯誤報告發給報文源發方。ICMP協議是IP層的一個協議,但是由於差錯報告在發送給報文源發方時可能也要經過若干子網,因此牽涉到路由選擇等問題,所以ICMP報文需通過IP協議來發送。



ICMP數據報的數據發送前需要兩級封裝:首先添加ICMP報頭形成ICMP報文,在添加IP頭形成IP數據報。

注意:IP頭不需要我們實現,由內核協議棧自動添加,我們只需要實現ICMP報文。

A.在Linux環境下,IP頭定義如下:



我們要實現ping,需要關注一下數據:

<1>IP報頭長度IHL(Internet Header Length)

其以4字節爲一個單位來記錄IP報頭的長度,由上述IP數據結構的ip_hl變量。所以實際IP報頭的長度是ip_hl << 2。

<2>生存時間TTL(Time To Live),是以秒爲單位,指出IP數據報能在網絡上停留的最長時間,其值由發送方設定,並在經過路由的每一個節點減一,當該值爲0時,數據報將被丟棄,是上述IP數據結構的ip_ttl變量。

B. ICMP報文

IPCMP報文分爲兩種:一是錯誤報告報文,二是查詢報文。

注意:每個ICMP報頭均包含類型、編碼、校驗和這三項內容,長度爲:8位、8位、16位。其餘選項則隨ICMP的功能不同而不同。

ping命令只使用衆多ICMP報文中的兩種:"請求(ICMP_ECHO)"和"迴應(ICMP_ECHOREPLY)"。在linux中定義如下:




這兩種報文格式如下:



通過wirshark抓包格式如下:



ICMP報頭在linux定義如下:

struct icmp
{
u_int8_t  icmp_type; /* type of message, see below */
  u_int8_t  icmp_code; /* type sub code */
  u_int16_t icmp_cksum; /* ones complement checksum of struct */
  union
  {
    u_char ih_pptr; /* ICMP_PARAMPROB */
    struct in_addr ih_gwaddr; /* gateway address */
    struct ih_idseq /* echo datagram */
    {
      u_int16_t icd_id;
      u_int16_t icd_seq;
    } ih_idseq;
    u_int32_t ih_void;

    /* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */
    struct ih_pmtu
    {
      u_int16_t ipm_void;
      u_int16_t ipm_nextmtu;
    } ih_pmtu;

    struct ih_rtradv
    {
      u_int8_t irt_num_addrs;
      u_int8_t irt_wpa;
      u_int16_t irt_lifetime;
    } ih_rtradv;
  } icmp_hun;
#define icmp_pptr icmp_hun.ih_pptr
#define icmp_gwaddr icmp_hun.ih_gwaddr
#define icmp_id icmp_hun.ih_idseq.icd_id(標識一個ICMP報文,一般我們用PID標識)
#define icmp_seq icmp_hun.ih_idseq.icd_seq(發送報文的序號)
#define icmp_void icmp_hun.ih_void
#define icmp_pmvoid icmp_hun.ih_pmtu.ipm_void
#define icmp_nextmtu icmp_hun.ih_pmtu.ipm_nextmtu
#define icmp_num_addrs icmp_hun.ih_rtradv.irt_num_addrs
#define icmp_wpa icmp_hun.ih_rtradv.irt_wpa
#define icmp_lifetime icmp_hun.ih_rtradv.irt_lifetime
  union
  {
    struct
    {
      u_int32_t its_otime;
      u_int32_t its_rtime;
      u_int32_t its_ttime;
    } id_ts;
    struct
    {
      struct ip idi_ip;
      /* options and then 64 bits of data */
    } id_ip;
    struct icmp_ra_addr id_radv;
    u_int32_t   id_mask;
    u_int8_t    id_data[1];
  } icmp_dun;
#define icmp_otime icmp_dun.id_ts.its_otime
#define icmp_rtime icmp_dun.id_ts.its_rtime
#define icmp_ttime icmp_dun.id_ts.its_ttime
#define icmp_ip icmp_dun.id_ip.idi_ip
#define icmp_radv icmp_dun.id_radv
#define icmp_mask icmp_dun.id_mask
#define icmp_data icmp_dun.id_data(可以看到id_data是含有一個元素的數組名,爲什麼這樣幹呀?思考...)
};

以上紅色部分使我們實現ping需要填充的部分。

規定:ICMP報頭爲8字節

<1>協議頭校驗和算法

unsigned short chksum(addr,len)  
    unsigned short *addr;  // 校驗數據開始地址(注意是以2字節爲單位)   
    int len;                // 校驗數據的長度大小,以字節爲單位
    int sum = 0;        // 校驗和
    int nleft = len;    // 未累加的數據長度   
    unsigned short *p;  // 走動的臨時指針,2字節爲單位    
    unsigned short tmp = 0; // 奇數字節長度時用到    
  
   while( nleft > 1)  
   {      
       sum += *p++;    // 累加        
       nleft -= 2;  
    }  

  
// 奇數字節長度    
   if(nleft == 1)   
   {      
      
// 將最後字節壓如2字節的高位        
       *(unsigned char *)&tmp = *(unsigned char *)p;
       sum += tmp;  
   } 
    
//高位低位相加  
    sum = (sum >> 16) + (sum & 0xffff);    
  
   // 上一步溢出時(十六進制相加進位),將溢出位也加到sum中      
   sum += sum >> 16;
  

   // 注意類型轉換,現在的校驗和爲16位
    tmp = ~sum;         
  
   return tmp;
}

網際校驗和算法,把被校驗的數據16位進行累加,然後取反碼,若數據字節長度爲奇數,則數據尾部補一個字節的0以湊成偶數。此算法適用於IPv4、ICMPv4、IGMPV4、ICMPv6、UDP和TCP校驗和,更詳細的信息請參考RFC1071。

<2>rtt往返時間

爲了實現這一功能,可利用ICMP數據報攜帶一個時間戳。使用以下函數生成時間戳:



獲取系統時間,放在struct timeval的變量中,第二個參數tzp指針表示時區,一般都是NULL,大多數代碼都是這樣,我也沒關注過。



其中tv_sec爲秒數,tv_usec微秒數。在發送和接收報文時由gettimeofday分別生成兩個timeval結構,兩者之差即爲往返時間,即ICMP報文發送與接收的時間差。

<3>數據統計

系統自帶的ping命令當它發送完所有ICMP報文後,會對所有發送和所有接收的ICMP報文進行統計,從而計算ICMP報文丟失的比率。

注意:爲達到此目標,我們在編寫代碼時,定義兩個全局變量:接收計數器和發送計數器,用於記錄ICMP報文接收和發送數目。丟失數目 =  發送總數 - 接收總數,丟失比率 = 丟失數目 / 發送總數。


四、myping的實現

<1>補充知識

判斷一個字符串是否是  string :"192.168.1.45" 這樣的字符串



if(   inet_addr(string) == INADDR_NONE  )
{
        .........
}

<2>補充知識

通過協議名如"icmp"獲取對應的協議編號



解釋如下:




注意:

創建原始套接字的時候,就需要指定其協議編號.

struct  protoent  *protocol;
int sockfd_ram;

if((protocol = getprotobyname("icmp")) == NULL)
{
    perror("Fail to getprotobyname");
    exit(EXIT_FAILURE);
}

//我們一般在創建,流套接字和數據包套接字時指定的是0,代表讓系統自己自動去識別
if((sockfd_ram = socket(AF_INET,SOCK_RAM,protocol->p_proto)) < 0)
{
    perror("Fail to socket");
    exit(EXIT_FAILURE);   
}

<3>補充知識

通過主機名或域名獲取其對應的ip地址



這個函數的傳入值是域名或者主機名,例如"www.google.cn"等等。傳出值,是一個hostent的結構。如果函數調用失敗,將返回NULL。



  hostent->h_name
    表示的是主機的規範名。例如www.google.com的規範名其實是www.l.google.com。
    hostent->h_aliases
    表示的是主機的別名.www.google.com就是google他自己的別名。有的時候,有的主機可能有好幾個別名,這些,其實都是爲了易於用戶記憶而爲自己的網站多取的名字。
    hostent->h_addrtype   
    表示的是主機ip地址的類型,到底是ipv4(AF_INET),還是pv6(AF_INET6)
    hostent->h_length     
    表示的是主機ip地址的長度
    hostent->h_addr_lisst
    表示的是主機的ip地址,注意,這個是以網絡字節序存儲的。千萬不要直接用printf帶%s參數來打這個東西,會有問題的哇。所以到真正需要打印出這個IP的話,需要調用inet_ntop()。



  這個函數,是將類型爲af的網絡地址結構src,轉換成主機序的字符串形式,存放在長度爲cnt的字符串中。返回指向dst的一個指針。如果函數調用錯誤,返回值是NULL。


#include
#include
#include

int main(int argc, char **argv)
{
    char   *ptr, **pptr;
    struct hostent *hptr;
    char   str[32];
    ptr = argv[1];

    if((hptr = gethostbyname(ptr)) == NULL)
    {
        printf(" gethostbyname error for host:%s\n", ptr);
        return 0;
    }

    printf("official hostname:%s\n",hptr->h_name);
    for(pptr = hptr->h_aliases; *pptr != NULL; pptr++)
        printf(" alias:%s\n",*pptr);

    switch(hptr->h_addrtype)
    {
        case AF_INET:
        case AF_INET6:
            pptr=hptr->h_addr_list;
            for(; *pptr!=NULL; pptr++)
                printf(" address:%s\n",
                       inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
            printf(" first address: %s\n",
                       inet_ntop(hptr->h_addrtype, hptr->h_addr, str, sizeof(str)));
        break;
        default:
            printf("unknown address type\n");
        break;
    }

    return 0;
}


編譯運行
-----------------------------
# gcc test.c
# ./a.out www.baidu.com
official hostname:www.a.shifen.com
alias:www.baidu.com
address:121.14.88.11
address:121.14.89.11
first address: 121.14.88.11

<4>myping源碼

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <errno.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <netinet/in.h>
  8. #include <arpa/inet.h>
  9. #include <netdb.h>
  10. #include <sys/time.h>
  11. #include <netinet/ip_icmp.h>
  12. #include <unistd.h>
  13. #include <signal.h>

  14. #define MAX_SIZE 1024

  15. char send_buf[MAX_SIZE];
  16. char recv_buf[MAX_SIZE];
  17. int nsend = 0,nrecv = 0;
  18. int datalen = 56;

  19. //統計結果
  20. void statistics(int signum)
  21. {
  22.     printf("\n----------------PING statistics---------------\n");
  23.     printf("%d packets transmitted,%d recevid,%%%d lost\n",nsend,nrecv,(nsend - nrecv)/nsend * 100);
  24.     exit(EXIT_SUCCESS);
  25. }

  26. //校驗和算法
  27. int calc_chsum(unsigned short *addr,int len)
  28. {
  29.     int sum = 0,n = len;
  30.     unsigned short answer = 0;
  31.     unsigned short *p = addr;
  32.    
  33.     //每兩個字節相加
  34.     while(n > 1)
  35.     {
  36.         sum += *p ++;
  37.         n -= 2;
  38.     }
  39.    
  40.     //處理數據大小是奇數,在最後一個字節後面補0
  41.     if(n == 1)
  42.     {
  43.         *((unsigned char *)&answer) = *(unsigned char *)p;
  44.         sum += answer;
  45.     }
  46.    
  47.     //將得到的sum值的高2字節和低2字節相加
  48.     sum = (sum >> 16) + (sum & 0xffff);
  49.    
  50.     //處理溢出的情況
  51.     sum += sum >> 16;
  52.     answer = ~sum;

  53.     return answer;
  54. }

  55. int pack(int pack_num)
  56. {
  57.     int packsize;
  58.     struct icmp *icmp;
  59.     struct timeval *tv;

  60.     icmp = (struct icmp *)send_buf;
  61.     icmp->icmp_type = ICMP_ECHO;
  62.     icmp->icmp_code = 0;
  63.     icmp->icmp_cksum = 0;
  64.     icmp->icmp_id = htons(getpid());
  65.     icmp->icmp_seq = htons(pack_num);
  66.     tv = (struct timeval *)icmp->icmp_data;

  67.     //記錄發送時間
  68.     if(gettimeofday(tv,NULL) < 0)
  69.     {
  70.         perror("Fail to gettimeofday");
  71.         return -1;
  72.     }
  73.    
  74.     packsize = 8 + datalen;
  75.     icmp->icmp_cksum = calc_chsum((unsigned short *)icmp,packsize);
  76.    
  77.     return packsize;
  78. }

  79. int send_packet(int sockfd,struct sockaddr *paddr)
  80. {
  81.     int packsize;
  82.    
  83.     //將send_buf填上a
  84.     memset(send_buf,'a',sizeof(send_buf));

  85.     nsend ++;
  86.     //打icmp包
  87.     packsize = pack(nsend);

  88.     if(sendto(sockfd,send_buf,packsize,0,paddr,sizeof(struct sockaddr)) < 0)
  89.     {
  90.         perror("Fail to sendto");
  91.         return -1;
  92.     }

  93.     return 0;
  94. }

  95. struct timeval  time_sub(struct timeval *tv_send,struct timeval *tv_recv)
  96. {
  97.     struct timeval ts;

  98.     if(tv_recv->tv_usec - tv_send->tv_usec < 0)
  99.     {
  100.         tv_recv->tv_sec --;
  101.         tv_recv->tv_usec += 1000000;
  102.     }

  103.     ts.tv_sec = tv_recv->tv_sec - tv_send->tv_sec;
  104.     ts.tv_usec = tv_recv->tv_usec - tv_send->tv_usec;

  105.     return ts;
  106. }

  107. int unpack(int len,struct timeval *tv_recv,struct sockaddr *paddr,char *ipname)
  108. {
  109.     struct ip *ip;
  110.     struct icmp *icmp;
  111.     struct timeval *tv_send,ts;
  112.     int ip_head_len;
  113.     float rtt;

  114.     ip = (struct ip *)recv_buf;
  115.     ip_head_len = ip->ip_hl << 2;
  116.     icmp = (struct icmp *)(recv_buf + ip_head_len);
  117.    
  118.     len -= ip_head_len;
  119.     if(len < 8)
  120.     {
  121.         printf("ICMP packets\'s is less than 8.\n");
  122.         return -1;
  123.     }
  124.        
  125.     if(ntohs(icmp->icmp_id) == getpid() && icmp->icmp_type == ICMP_ECHOREPLY)
  126.     {
  127.         nrecv ++;
  128.         tv_send = (struct timeval *)icmp->icmp_data;
  129.         ts = time_sub(tv_send,tv_recv);
  130.         rtt = ts.tv_sec * 1000 + (float)ts.tv_usec/1000;//以毫秒爲單位
  131.         printf("%d bytes from %s (%s):icmp_req = %d ttl=%d time=%.3fms.\n",
  132.             len,ipname,inet_ntoa(((struct sockaddr_in *)paddr)->sin_addr),ntohs(icmp->icmp_seq),ip->ip_ttl,rtt);
  133.     }
  134.    
  135.     return 0;
  136. }

  137. int recv_packet(int sockfd,char *ipname)
  138. {
  139.     int addr_len ,n;
  140.     struct timeval tv;
  141.     struct sockaddr from_addr;
  142.    
  143.     addr_len = sizeof(struct sockaddr);
  144.     if((n = recvfrom(sockfd,recv_buf,sizeof(recv_buf),0,&from_addr,&addr_len)) < 0)
  145.     {
  146.         perror("Fail to recvfrom");
  147.         return -1;
  148.     }

  149.     if(gettimeofday(&tv,NULL) < 0)
  150.     {
  151.         perror("Fail to gettimeofday");
  152.         return -1;
  153.     }

  154.     unpack(n,&tv,&from_addr,ipname);

  155.     return 0;
  156. }


  157. int main(int argc,char *argv[])
  158. {
  159.     int size = 50 * 1024;
  160.     int sockfd,netaddr;
  161.     struct protoent *protocol;
  162.     struct hostent *host;
  163.     struct sockaddr_in peer_addr;
  164.    
  165.     if(argc < 2)
  166.     {
  167.         fprintf(stderr,"usage : %s ip.\n",argv[0]);
  168.         exit(EXIT_FAILURE);
  169.     }
  170.    
  171.     //獲取icmp的信息
  172.     if((protocol = getprotobyname("icmp")) ==NULL)
  173.     {
  174.         perror("Fail to getprotobyname");
  175.         exit(EXIT_FAILURE);
  176.     }
  177.    
  178.     //創建原始套接字
  179.     if((sockfd = socket(AF_INET,SOCK_RAW,protocol->p_proto)) < 0)
  180.     {
  181.         perror("Fail to socket");
  182.         exit(EXIT_FAILURE);
  183.     }

  184.     //回收root權限,設置當前用戶權限
  185.     setuid(getuid());

  186.     /*
  187.      擴大套接子接收緩衝區到50k,這樣做主要爲了減少接收緩衝區溢出的可能性
  188.      若無影中ping一個廣播地址或多播地址,將會引來大量應答
  189.      */
  190.     if(setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&size,sizeof(size)) < 0)
  191.     {
  192.         perror("Fail to setsockopt");
  193.         exit(EXIT_FAILURE);
  194.     }

  195.     //填充對方的地址
  196.     bzero(&peer_addr,sizeof(peer_addr));
  197.     peer_addr.sin_family = AF_INET;
  198.     //判斷是主機名(域名)還是ip
  199.     if((netaddr = inet_addr(argv[1])) == INADDR_NONE)
  200.     {   
  201.         //是主機名(域名)
  202.         if((host = gethostbyname(argv[1])) == NULL)
  203.         {
  204.             fprintf(stderr,"%s unknown host : %s.\n",argv[0],argv[1]);
  205.             exit(EXIT_FAILURE);
  206.         }

  207.         memcpy((char *)&peer_addr.sin_addr,host->h_addr,host->h_length);
  208.        
  209.     }else{//ip地址
  210.         peer_addr.sin_addr.s_addr = netaddr;
  211.     }
  212.    
  213.     //註冊信號處理函數
  214.     signal(SIGALRM,statistics);
  215.     signal(SIGINT,statistics);
  216.     alarm(5);

  217.     //開始信息
  218.     printf("PING %s(%s) %d bytes of data.\n",argv[1],inet_ntoa(peer_addr.sin_addr),datalen);

  219.     //發送包文和接收報文
  220.     while(1)
  221.     {
  222.         send_packet(sockfd,(struct sockaddr *)&peer_addr);
  223.         recv_packet(sockfd,argv[1]);
  224.         alarm(5);
  225.         sleep(1);
  226.     }

  227.     exit(EXIT_SUCCESS);
  228. }

注意:由於原始套接字的創建只能是擁有超級權限的進程創建,所以我們需要將我們編譯好的可執行文件,把其文件所有者改爲root,再將其set-uid-bit位進行設置。操作如下:


 

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