2017年5月17日 星期三

[C] 用 SOCK_RAW 實現簡單的 ping 功能




<解釋原理>
我們要自行建立一個 "ICMP Header" 並送到對方主機
倘若對方主機活著且存在,他將會 REPLY "IP Header" 和 "ICMP Header" 回來

所以我們要使用
socket(PF_INET, SOCK_RAW, IPPROTO_ICMP) 來建立我們的 socket





註:這只是個簡單的範例,其中還有許多BUG需要深入探討的哦,但在這裡先不多談

<程式碼>
/* raw_ping.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>

/* 做 checksum 運算
 * 可以驗證資料在傳輸過程中有無損壞
 */
unsigned short checksum(unsigned short *buf, int bufsz){
 unsigned long sum = 0xffff;
 
 while(bufsz > 1){
  sum += *buf;
  buf++;
  bufsz -= 2;
 }

 /* 因為傳進來的時候是 unsigned short
  * 所以這裡記得轉成 unsigned char
  */
 if(bufsz == 1)
  sum += *(unsigned char*)buf;

 sum = (sum & 0xffff) + (sum >> 16);
 sum = (sum & 0xffff) + (sum >> 16);

 return ~sum;
}

int main(int argc, char *argv[]){
 int sockfd;

 /* 此結構已經定義在 netinet/ip_icmp.h 裡面
  * 想知道完整的結構體可以去 google
  */
 struct icmphdr hdr;

 struct sockaddr_in addr;
 int n;

 char buf[2000];
 struct icmphdr *icmphdrptr;
 struct iphdr *iphdrptr;

 if(argc != 2){
  printf("usage : %s IPADDR\n", argv[0]);
  return 1;
 }

 addr.sin_family = PF_INET;  //IPv4
 //inet_pton() 可以把 IP 位址轉成 network order 的形式
 n = inet_pton(PF_INET, argv[1], &addr.sin_addr);
 if(n<0){
  perror("inet_pton");
  return -1;
 }

 /*建立 RAW socket*/
 sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);
 if(sockfd<0){
  perror("socket");
  return -1;
 }

 //先清空 hdr (但這裡不清也無所謂)
 memset(&hdr, 0, sizeof(hdr));

 /*先設定 ICMP Header*/
 hdr.type = ICMP_ECHO;
 hdr.code = 0;
 hdr.checksum = 0;
 hdr.un.echo.id = 0;
 hdr.un.echo.sequence = 0;

 /*計算 ICMP Header 的 checksum*/
 hdr.checksum = checksum((unsigned short *)&hdr, sizeof(hdr));

 /*傳送只有 ICMP Header 的 ICMP 封包*/
 n = sendto(sockfd, (char*)&hdr, sizeof(hdr), 0, (struct sockaddr *)&addr, sizeof(addr));
 if(n<1){
  perror("sendto");
  return -1;
 }

 /* 送完 ICMP 封包後
  * 接著當然要來接收 ICMP ECHO REPLY 囉
  */

 /*清空 buffer (其實這邊不清也無所謂)*/
 memset(buf, 0, sizeof(buf));

 /*接收來自對方主機的 ICMP ECHO REPLY*/
 n = recv(sockfd, buf, sizeof(buf), 0);
 if(n<1){
  perror("recv");
  return -1;
 }

 /*從接收到的資料中取出 IP Header 的部分*/
 iphdrptr = (struct iphdr*)buf;

 /*從接收到的資料中取出 ICMP Header 的部分*/
 icmphdrptr = (struct icmphdr*)(buf + (iphdrptr->ihl)*4);

 /*選出 ICMP Header 的類型*/
 if(icmphdrptr->type != 0){
  //printf("checksum = %d\n", hdr.checksum);

  //如果 type 是3的話就多印出他的 code ,以便知道發生甚麼問題
  if(icmphdrptr->type == 3){
   printf("received ICMP %d, and the code is %d\n", icmphdrptr->type, icmphdrptr->code);
  }
  else{
   printf("received ICMP %d\n", icmphdrptr->type);
   printf("The host %s is alive\n", argv[1]);
  }
 }

 close(sockfd);
 return 0;
}




註:開啟 RAW socket 需要 root 權限,記得用 sudo 執行程式


<執行圖>
sudo ./raw_ping (+你想ping的IP)







由上圖可看到 ping 自己時收到 ICMP type 8,8號的意思是"ICMP ECHO"
表示你 ping 的主機是活著且存在

想知道更多的 ICMP Type 可以去查閱 維基百科-控制消息協議

沒有留言:

張貼留言