Implementation of PING in Linux Programming

Keywords: Linux network socket less

The Chinese name of PING (Packet InterNet Groper) is Internet Packet Explorer, which is a tool used to check whether the network connection of another host system on the network is normal. The ping command works by sending an ICMP message to another host system on the network. If a message is received by the designated system, it will send the message back to the sender, which is a bit like the sound generator used in the submarine sonar system. So, we want to know if this host can communicate with another one. First of all, we need to confirm whether the network between our two hosts is connected or not, that is, whether what I said can be transmitted to you, which is the premise of communication between the two hosts. The methods and phenomena of using instruction Ping under Linux are as follows:

 

The implementation of PING does not seem complicated. I want to write my own code to implement this function. What kind of knowledge is needed? Let me briefly list:

  • Understanding of ICMP Protocol
  • RAW socket
  • Network Packeting and Unpacking Skills

The steps to build such a ping program are as follows:
  1. Encapsulation and Unencapsulation of ICMP Packets
  2. Create a thread for sending ICMP packages
  3. Create a thread for receiving ICMP packets
  4. Primitive socket programming
 
The PING process is as follows:
 
 
I. Encapsulation and Unencapsulation of ICMP Packets
(1) ICMP protocol understanding
To develop PING, we need to know that the implementation of PING is based on ICMP protocol. To encapsulate and unpack ICMP packages, we first need to understand the ICMP protocol. ICMP is located at the network layer, allowing the host or router to report errors and provide reports on exceptions. ICMP message is encapsulated in IP datagram as its data part. As the data of IP layer datagram, ICMP message, together with data header, composes IP datagram and sends it out. The ICMP message format is as follows:
There are two kinds of ICMP messages: ICMP error report message and ICMP inquiry message. The type of ICMP message used by PING program is ICMP inquiry message. Notice the "type" field in the ICMP message format mentioned above. When we group packages, we can fill in different values to calibrate the type of the ICMP message. The following are several commonly used ICMP message types.
The type of ICMP that our PING program needs to use is the echo request (8).
Because the format of ICMP message varies according to the type of ICMP message, the format of our ping package is as follows:

(2) ICMP package assembly
In contrast to the ping package format above, the code for encapsulating the ping package can be written as follows:
void icmp_pack(struct icmp* icmphdr, int seq, int length)
{
    int i = 0;

    icmphdr->icmp_type = ICMP_ECHO;  //Type Fill Back Request
    icmphdr->icmp_code = 0;   
    icmphdr->icmp_cksum = 0; //Note that it's important to fill in 0 first!
    icmphdr->icmp_seq = seq;  //Here's the serial number. Let's fill in 1.,2,3,4....
    icmphdr->icmp_id = pid & 0xffff;  //We use pid Act as icmp_id,icmp_id It's only 2 bytes, and pid There are 4 bytes.
    for(i=0;i<length;i++)
    {
        icmphdr->icmp_data[i] = i;  //Fill in data segments to make ICMP Messages greater than 64 B
    }

    icmphdr->icmp_cksum = cal_chksum((unsigned short*)icmphdr, length); //check sum calculator
}
Again and again, icmp_cksum must fill in 0 before performing checksum arithmetic calculation. Otherwise, when ping, the other host will discard the request package because of the error of checksum arithmetic, which will lead to the failure of ping. One of my colleagues has been searching for such a mistake for a long time. Please remember the lesson of blood.

Here's a brief introduction to checksum.

When communicating with computer network, in order to check whether the data is wrong in the process of data transmission, the data is usually transmitted together with the checksum. When receiving the data, the checksum will be calculated again. If the checksum is different from the original one, it will be regarded as an error. The data packet will be discarded and the icmp message will be returned.

 
The basic idea of the algorithm is as follows:

The checksum algorithms of IP/ICMP/IGMP/TCP/UDP and other protocols are the same, and the data stream is regarded as 16-bit integer stream for repeated overlay calculation. In order to calculate the checksum, the checksum and field are set to 0. Then, the binary inverse sum of each 16 bits in the valid data range is performed. The result exists in the test and field. If the length of the data is odd, a byte of 0 is added. When data is received, the sum of binary inverse codes for each 16 digits in the valid data range is also performed. Since the receiver includes the checksum of the sender's presence in the header in the calculation process, if there are no errors in the header in the transmission process, then the result of the receiver's calculation should be all 0 or all 1 (see the implementation in detail, the essence is the same). If the result is not all 0 or all 1, then the data is incorrect.

/*Checksum algorithm*/
unsigned short cal_chksum(unsigned short *addr,int len)
{       int nleft=len;
        int sum=0;
        unsigned short *w=addr;
        unsigned short answer=0;

        /*Add up ICMP header binary data in 2 bytes*/
        while(nleft>1)
        {       
            sum+=*w++;
            nleft-=2;
        }
        /*If the ICMP header is odd bytes, the last byte is left. Consider the last byte as a high byte of 2-byte data, the low byte of 2-byte data is 0, and continue to accumulate.*/
        if( nleft==1)
        {       
            *(unsigned char *)(&answer)=*(unsigned char *)w;
            sum+=answer;
        }
        sum=(sum>>16)+(sum&0xffff);
        sum+=(sum>>16);
        answer=~sum;
        return answer;
}
 
(3) unpacking of ICMP packets
Know how to package, then unpacking is not difficult. Note that when we receive an ICMP package, we do not think that the package is the ICMP reply package we sent out. We need to add a layer of code to determine whether the id and seq fields of the ICMP message conform to the settings of the ICMP message we sent to verify the correctness of the ICMP reply package.
int icmp_unpack(char* buf, int len)
{
    int iphdr_len;
    struct timeval begin_time, recv_time, offset_time;
    int rtt;  //round trip time

    struct ip* ip_hdr = (struct ip *)buf;
    iphdr_len = ip_hdr->ip_hl*4;
    struct icmp* icmp = (struct icmp*)(buf+iphdr_len); //Skip the pointer IP Head pointing ICMP head
    len-=iphdr_len;  //icmp Packet length
    if(len < 8)   //Determine whether the length is ICMP Packet length
    {
        fprintf(stderr, "Invalid icmp packet.Its length is less than 8\n");
        return -1;
    }

    //Judge that the package is ICMP Return the reply package and it was sent out by us.
    if((icmp->icmp_type == ICMP_ECHOREPLY) && (icmp->icmp_id == pid)) 
    {
        if((icmp->icmp_seq < 0) || (icmp->icmp_seq > PACKET_SEND_MAX_NUM))
        {
            fprintf(stderr, "icmp packet seq is out of range!\n");
            return -1;
        }

        ping_packet[icmp->icmp_seq].flag = 0;
        begin_time = ping_packet[icmp->icmp_seq].begin_time;  //Remove the time of delivery of the package
        gettimeofday(&recv_time, NULL);

        offset_time = cal_time_offset(begin_time, recv_time);
        rtt = offset_time.tv_sec*1000 + offset_time.tv_usec/1000; //In milliseconds

        printf("%d byte from %s: icmp_seq=%u ttl=%d rtt=%d ms\n",
            len, inet_ntoa(ip_hdr->ip_src), icmp->icmp_seq, ip_hdr->ip_ttl, rtt);        

    }
    else
    {
        fprintf(stderr, "Invalid ICMP packet! Its id is not matched!\n");
        return -1;
    }
    return 0;
}
 
 
2. Construction of Outsourcing Threads
According to the framework of PING program, we need to build a thread for sending Ping packages. My idea is this: using sendto for sending packages, we can keep the sending rate at 1.1 second. We need a global variable to record the time of the first Ping package. Besides, we need a global variable to record how many ping packages we send. The two variables are used to calculate the data after receiving a response from the Ping package.
void ping_send()
{
    char send_buf[128];
    memset(send_buf, 0, sizeof(send_buf));
    gettimeofday(&start_time, NULL); //Record the first ping Time of Pack Delivery
    while(alive)
    {
        int size = 0;
        gettimeofday(&(ping_packet[send_count].begin_time), NULL);
        ping_packet[send_count].flag = 1; //Mark this as set that the package has been sent

        icmp_pack((struct icmp*)send_buf, send_count, 64); //encapsulation icmp package
        size = sendto(rawsock, send_buf, 64, 0, (struct sockaddr*)&dest, sizeof(dest));
        send_count++; //Record issue ping Number of packages
        if(size < 0)
        {
            fprintf(stderr, "send icmp packet fail!\n");
            continue;
        }

        sleep(1);
    }
}

 

3. Construction of Packet Receiving Threads
We also set up a thread to receive the package. Here we use slect as a non-congestion function to receive the package, and set the timeout time for the slect function to 200 us. If the timeout occurs, the next loop will be carried out. Similarly, we need a global variable to record the number of ping reply packets received successfully.

void ping_recv()
{
    struct timeval tv;
    tv.tv_usec = 200;  //Set up select The timeout time of the function is 200 us
    tv.tv_sec = 0;
    fd_set read_fd;
    char recv_buf[512];
    memset(recv_buf, 0 ,sizeof(recv_buf));
    while(alive)
    {
        int ret = 0;
        FD_ZERO(&read_fd);
        FD_SET(rawsock, &read_fd);
        ret = select(rawsock+1, &read_fd, NULL, NULL, &tv);
        switch(ret)
        {
            case -1:
                fprintf(stderr,"fail to select!\n");
                break;
            case 0:
                break;
            default:
                {
                    int size = recv(rawsock, recv_buf, sizeof(recv_buf), 0);
                    if(size < 0)
                    {
                        fprintf(stderr,"recv data fail!\n");
                        continue;
                    }

                    ret = icmp_unpack(recv_buf, size); //Unencapsulation of received packets
                    if(ret == -1)  //Not your own icmp Pack, discarded and untreated
                    {
                        continue;
                    }
                    recv_count++; //Receiving Packet Counting
                }
                break;
        }

    }
}
 
 
IV. Interruption Processing
We specify that the maximum number of packets sent by a Ping is 64, and stop sending if it exceeds that value. As PING users, we usually only send several packages. If these packages return smoothly, we will interrupt Ping by crtl+c. The code here mainly writes an interrupt processing function for the interrupt signal, sets the global variable of alive to 0, and then stops the loop of sending Ping packet and ends the program.
void icmp_sigint(int signo)
{
    alive = 0;
    gettimeofday(&end_time, NULL);
    time_interval = cal_time_offset(start_time, end_time);
}

signal(SIGINT, icmp_sigint);
 
 
V. Overall Realization
All modules have been introduced, and now the complete code is posted.
  1 #include <stdio.h>
  2 #include <netinet/in.h>
  3 #include <netinet/ip.h>
  4 #include <netinet/ip_icmp.h>
  5 #include <unistd.h>
  6 #include <signal.h>
  7 #include <arpa/inet.h>
  8 #include <errno.h>
  9 #include <sys/time.h>
 10 #include <string.h>
 11 #include <netdb.h>
 12 #include <pthread.h>
 13 
 14 
 15 #define PACKET_SEND_MAX_NUM 64
 16 
 17 typedef struct ping_packet_status
 18 {
 19     struct timeval begin_time;
 20     struct timeval end_time;
 21     int flag;   //Send sign,1 Send for
 22     int seq;     //Sequence number of package
 23 }ping_packet_status;
 24 
 25 
 26 
 27 ping_packet_status ping_packet[PACKET_SEND_MAX_NUM];
 28 
 29 int alive;
 30 int rawsock;
 31 int send_count;
 32 int recv_count;
 33 pid_t pid;
 34 struct sockaddr_in dest;
 35 struct timeval start_time;
 36 struct timeval end_time;
 37 struct timeval time_interval;
 38 
 39 /*Checksum algorithm*/
 40 unsigned short cal_chksum(unsigned short *addr,int len)
 41 {       int nleft=len;
 42         int sum=0;
 43         unsigned short *w=addr;
 44         unsigned short answer=0;
 45 
 46         /*Add up ICMP header binary data in 2 bytes*/
 47         while(nleft>1)
 48         {       
 49             sum+=*w++;
 50             nleft-=2;
 51         }
 52         /*If the ICMP header is odd bytes, the last byte is left. Consider the last byte as a high byte of 2-byte data, the low byte of 2-byte data is 0, and continue to accumulate.*/
 53         if( nleft==1)
 54         {       
 55             *(unsigned char *)(&answer)=*(unsigned char *)w;
 56             sum+=answer;
 57         }
 58         sum=(sum>>16)+(sum&0xffff);
 59         sum+=(sum>>16);
 60         answer=~sum;
 61         return answer;
 62 }
 63 
 64 struct timeval cal_time_offset(struct timeval begin, struct timeval end)
 65 {
 66     struct timeval ans;
 67     ans.tv_sec = end.tv_sec - begin.tv_sec;
 68     ans.tv_usec = end.tv_usec - begin.tv_usec;
 69     if(ans.tv_usec < 0) //If receiving time usec Less than the sending time usec,Then to sec Domain borrow
 70     {
 71         ans.tv_sec--;
 72         ans.tv_usec+=1000000;
 73     }
 74     return ans;
 75 }
 76 
 77 void icmp_pack(struct icmp* icmphdr, int seq, int length)
 78 {
 79     int i = 0;
 80 
 81     icmphdr->icmp_type = ICMP_ECHO;
 82     icmphdr->icmp_code = 0;
 83     icmphdr->icmp_cksum = 0;
 84     icmphdr->icmp_seq = seq;
 85     icmphdr->icmp_id = pid & 0xffff;
 86     for(i=0;i<length;i++)
 87     {
 88         icmphdr->icmp_data[i] = i;
 89     }
 90 
 91     icmphdr->icmp_cksum = cal_chksum((unsigned short*)icmphdr, length);
 92 }
 93 
 94 int icmp_unpack(char* buf, int len)
 95 {
 96     int iphdr_len;
 97     struct timeval begin_time, recv_time, offset_time;
 98     int rtt;  //round trip time
 99 
100     struct ip* ip_hdr = (struct ip *)buf;
101     iphdr_len = ip_hdr->ip_hl*4;
102     struct icmp* icmp = (struct icmp*)(buf+iphdr_len);
103     len-=iphdr_len;  //icmp Packet length
104     if(len < 8)   //Determine whether the length is ICMP Packet length
105     {
106         fprintf(stderr, "Invalid icmp packet.Its length is less than 8\n");
107         return -1;
108     }
109 
110     //Judge that the package is ICMP Return the reply package and it was sent out by us.
111     if((icmp->icmp_type == ICMP_ECHOREPLY) && (icmp->icmp_id == pid)) 
112     {
113         if((icmp->icmp_seq < 0) || (icmp->icmp_seq > PACKET_SEND_MAX_NUM))
114         {
115             fprintf(stderr, "icmp packet seq is out of range!\n");
116             return -1;
117         }
118 
119         ping_packet[icmp->icmp_seq].flag = 0;
120         begin_time = ping_packet[icmp->icmp_seq].begin_time;
121         gettimeofday(&recv_time, NULL);
122 
123         offset_time = cal_time_offset(begin_time, recv_time);
124         rtt = offset_time.tv_sec*1000 + offset_time.tv_usec/1000; //In milliseconds
125 
126         printf("%d byte from %s: icmp_seq=%u ttl=%d rtt=%d ms\n",
127             len, inet_ntoa(ip_hdr->ip_src), icmp->icmp_seq, ip_hdr->ip_ttl, rtt);        
128 
129     }
130     else
131     {
132         fprintf(stderr, "Invalid ICMP packet! Its id is not matched!\n");
133         return -1;
134     }
135     return 0;
136 }
137 
138 void ping_send()
139 {
140     char send_buf[128];
141     memset(send_buf, 0, sizeof(send_buf));
142     gettimeofday(&start_time, NULL); //Record the first ping Time of Pack Delivery
143     while(alive)
144     {
145         int size = 0;
146         gettimeofday(&(ping_packet[send_count].begin_time), NULL);
147         ping_packet[send_count].flag = 1; //Mark this as set that the package has been sent
148 
149         icmp_pack((struct icmp*)send_buf, send_count, 64); //encapsulation icmp package
150         size = sendto(rawsock, send_buf, 64, 0, (struct sockaddr*)&dest, sizeof(dest));
151         send_count++; //Record issue ping Number of packages
152         if(size < 0)
153         {
154             fprintf(stderr, "send icmp packet fail!\n");
155             continue;
156         }
157 
158         sleep(1);
159     }
160 }
161 
162 void ping_recv()
163 {
164     struct timeval tv;
165     tv.tv_usec = 200;  //Set up select The timeout time of the function is 200 us
166     tv.tv_sec = 0;
167     fd_set read_fd;
168     char recv_buf[512];
169     memset(recv_buf, 0 ,sizeof(recv_buf));
170     while(alive)
171     {
172         int ret = 0;
173         FD_ZERO(&read_fd);
174         FD_SET(rawsock, &read_fd);
175         ret = select(rawsock+1, &read_fd, NULL, NULL, &tv);
176         switch(ret)
177         {
178             case -1:
179                 fprintf(stderr,"fail to select!\n");
180                 break;
181             case 0:
182                 break;
183             default:
184                 {
185                     int size = recv(rawsock, recv_buf, sizeof(recv_buf), 0);
186                     if(size < 0)
187                     {
188                         fprintf(stderr,"recv data fail!\n");
189                         continue;
190                     }
191 
192                     ret = icmp_unpack(recv_buf, size); //Unencapsulation of received packets
193                     if(ret == -1)  //Not your own icmp Pack, discarded and untreated
194                     {
195                         continue;
196                     }
197                     recv_count++; //Receiving Packet Counting
198                 }
199                 break;
200         }
201 
202     }
203 }
204 
205 void icmp_sigint(int signo)
206 {
207     alive = 0;
208     gettimeofday(&end_time, NULL);
209     time_interval = cal_time_offset(start_time, end_time);
210 }
211 
212 void ping_stats_show()
213 {
214     long time = time_interval.tv_sec*1000+time_interval.tv_usec/1000;
215     /*Note that the divisor cannot be zero, where send_count may be zero, so the runtime prompts for errors*/
216     printf("%d packets transmitted, %d recieved, %d%c packet loss, time %ldms\n",
217         send_count, recv_count, (send_count-recv_count)*100/send_count, '%', time);
218 }
219 
220 
221 int main(int argc, char* argv[])
222 {
223     int size = 128*1024;//128k
224     struct protoent* protocol = NULL;
225     char dest_addr_str[80];
226     memset(dest_addr_str, 0, 80);
227     unsigned int inaddr = 1;
228     struct hostent* host = NULL;
229 
230     pthread_t send_id,recv_id;
231 
232     if(argc < 2)
233     {
234         printf("Invalid IP ADDRESS!\n");
235         return -1;
236     }
237 
238     protocol = getprotobyname("icmp"); //Get the protocol type ICMP
239     if(protocol == NULL)
240     {
241         printf("Fail to getprotobyname!\n");
242         return -1;
243     }
244 
245     memcpy(dest_addr_str, argv[1], strlen(argv[1])+1);
246 
247     rawsock = socket(AF_INET,SOCK_RAW,protocol->p_proto);
248     if(rawsock < 0)
249     {
250         printf("Fail to create socket!\n");
251         return -1;
252     }
253 
254     pid = getpid();
255 
256     setsockopt(rawsock, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)); //Increase the receiving buffer to 128 K
257 
258     bzero(&dest,sizeof(dest));
259 
260     dest.sin_family = AF_INET;
261 
262     inaddr = inet_addr(argv[1]);
263     if(inaddr == INADDR_NONE)   //Determine whether user input is IP Address or Domain Name
264     {
265         //The domain name address is entered
266         host = gethostbyname(argv[1]);
267         if(host == NULL)
268         {
269             printf("Fail to gethostbyname!\n");
270             return -1;
271         }
272 
273         memcpy((char*)&dest.sin_addr, host->h_addr, host->h_length);
274     }
275     else
276     {
277         memcpy((char*)&dest.sin_addr, &inaddr, sizeof(inaddr));//The input is IP address
278     }
279     inaddr = dest.sin_addr.s_addr;
280     printf("PING %s, (%d.%d.%d.%d) 56(84) bytes of data.\n",dest_addr_str,
281         (inaddr&0x000000ff), (inaddr&0x0000ff00)>>8, 
282         (inaddr&0x00ff0000)>>16, (inaddr&0xff000000)>>24);
283 
284     alive = 1;  //control ping Sending and receiving
285 
286     signal(SIGINT, icmp_sigint);
287 
288     if(pthread_create(&send_id, NULL, (void*)ping_send, NULL))
289     {
290         printf("Fail to create ping send thread!\n");
291         return -1;
292     }
293 
294     if(pthread_create(&recv_id, NULL, (void*)ping_recv, NULL))
295     {
296         printf("Fail to create ping recv thread!\n");
297         return -1;
298     }
299 
300     pthread_join(send_id, NULL);//wait for send ping The process ends after the thread has finished
301     pthread_join(recv_id, NULL);//wait for recv ping The process ends after the thread has finished
302 
303     ping_stats_show();
304 
305     close(rawsock);
306     return 0;
307 
308 }
Compilation and experimental phenomena are as follows:
My experimental environment is two servers, the host of Ping is 172.0.5.183, and the host of Ping is 172.0.5.182. The following are my two experimental phenomena (ping IP and Ping domain name).
 
Special attention:

Only root users can use socket() function to generate the original socket. To enable ordinary users of Linux to execute the above programs, the following special operations are needed: log in with root and compile the above programs gcc-lpthread-o ping.c.

 

The experimental results show that PING is successful, indicating that the network between the two hosts is connected, and all Ping packets sent have received a response.
 
Below is the PING program that comes with Linux system. We can compare the PING program we designed with the PING program that comes with the system.

 

 

Posted by devork on Thu, 21 Mar 2019 17:24:52 -0700