How the ping utility works in Linux

If you decided to read this article, then you have probably worked in the Linux OS at some point. Today I will tell you how the fairly popular ping command works and show how its functionality is implemented in the C language.

The ping utility is installed by default in Linux. An example of its use (specify the domain or IP in the parameters, for example google.com or 173.194.73.138):

user@mycomputer:~$ ping google.com
PING google.com (173.194.73.138) 56(84) bytes of data.
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=1 ttl=56 time=23.8 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=2 ttl=56 time=23.2 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=3 ttl=56 time=24.3 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=4 ttl=56 time=23.5 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=5 ttl=56 time=23.3 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=6 ttl=56 time=23.1 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=7 ttl=56 time=24.3 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=8 ttl=56 time=25.4 ms
^C
--- google.com ping statistics ---
8 packets transmitted, 8 received, 0% packet loss, time 7013ms
rtt min/avg/max/mdev = 23.119/23.877/25.436/0.728 ms

But what does it mean that this command displayed on the screen? The ping command sends packets to the site we specified (google.com) and analyzes them. On the right we can see the time value, for example:

time = 23.8 ms

The time value shows how long it takes the data to reach google.com and return, in milliseconds. The example shows that the packet sent by the ping utility reached Google and returned back in 23.8 ms. I’ll tell you what other keywords mean later.

Take a look at the OSI network model:

Model

Level

Data type

Functions

Protocols

7. Applied

Data

Access to network services

HTTP, FTP

6. Performances

Data representation and encryption

ASCII

5. Session

Session management

RPC, PAP

4. Transport

Segments/Datagrams

Direct communication between endpoints and reliability

TCP, UDP, SCTP, Ports

3. Network

Packages

Route determination and logical addressing

IP, IPv6, ICMP

2.Channel

Bits/Frames

Physical addressing

Ethernet, ARP, Network card

1. Physical

Bits

Working with transmission media, signals and binary data

USB, RJ

The World Internet works precisely according to this model. The lowest level is physical. Your router sends bits over wires and fiber optic cables, but we won't cover that layer.

The second level is channel. According to its protocols (data transfer agreements), the computer already somehow sends data to the router. If you have ever used wired Internet, you have probably seen a cable for it that has a rather specific shape. At this level we can send frames through our programs, but it is very difficult to understand.

The third level (we will devote most of the article to it) allows you to forward packets. You probably know what an IP address is: a computer identifier on the Internet. We will also look at the ICMP protocol in this article.

Well, the fourth level (we’ll end there, I know you’re tired of reading about them) is the most, most common level. 80% of all programs on the Internet send data using the TCP protocol. Ports already appear here: packets are sent to them using protocols of all higher levels.

So, what protocol does the ping utility use to send its packets? Via ICMP. For ICMP packets you do not need to specify a port, just an IP address. In addition, almost all computers, when receiving an ICMP packet, send back a response to it.

Obviously, if we do not receive a response to an ICMP packet, it means the server is down.

Well, let's program! I wrote a small utility in C that works almost exactly like ping. It's called vping (very original).

user@mycomputer:~/osi/ping/vping$ sudo ./vping google.com
[sudo] password for user:
PING sending to 173.194.73.139
Data was sent... ttl = 104, seq = 1, time=19ms
Data was sent... ttl = 104, seq = 2, time=20ms
Data was sent... ttl = 104, seq = 3, time=20ms
Data was sent... ttl = 104, seq = 4, time=92ms
Data was sent... ttl = 104, seq = 5, time=20ms
Data was sent... ttl = 104, seq = 6, time=19ms
Data was sent... ttl = 104, seq = 7, time=20ms
Data was sent... ttl = 104, seq = 8, time=52ms
Data was sent... ttl = 104, seq = 9, time=24ms
Data was sent... ttl = 104, seq = 10, time=24ms
Data was sent... ttl = 104, seq = 11, time=24ms
Data was sent... ttl = 104, seq = 12, time=22ms

See, almost the same! First, let's connect the necessary headers:

#include <netinet/ip_icmp.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <netdb.h>

typedef uint64_t u64;
typedef uint32_t u32;
typedef uint16_t u16;
typedef uint8_t  u8;

typedef int64_t i64;
typedef int32_t i32;
typedef int16_t i16;
typedef int8_t  i8;

Yes, there are quite a few of them. I make typedefs for greater readability and shorter code.

Let's take a look at the main function:

int main(int argc, char *argv[])
{
  if (argc < 2) {
    fputs("Too few arguments!\n", stderr);
    return -1;
  }
  ...

I display an error if the user accidentally forgot to specify an IP or domain. Then I create the socket:

  int fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
  if (fd == -1) {
    puts("Run with sudo!");
    return -1;
  }

Please note that I am creating a socket with type SOCK_RAW. These are the so-called “raw” sockets. Using them, you can send not only a TCP or UDP packet, but any packet you want! They require root superuser rights, so the program must be launched via sudo. I, of course, specify the IPPROTO_ICMP protocol to send the ICMP packet.

Now I create a structure with type sockaddr_in that stores the IP address of the packet recipient (for example, Google):

  struct sockaddr_in dest;
  fill_sockaddr_in(&dest, argv[1]);

Let me remind you that argv[1] is the IP address or domain to which we send packets. Let's look at the contents of the fill_sockaddr_in function:

void fill_sockaddr_in(struct sockaddr_in *addr, char *ip) {
  reset(*addr);
  addr->sin_family = AF_INET;
  int inet_ptoned = inet_pton(AF_INET, ip, &addr->sin_addr.s_addr);
  if (!inet_ptoned) {
    char *new_ip = getipbydom(ip);
    inet_pton(AF_INET, new_ip, &addr->sin_addr.s_addr);
    printf("PING sending to %s\n", new_ip);
  } else if (inet_ptoned == -1) {
    puts("IP isn't correct!");
    exit(-1);
  } else {
    printf("PING sending to %s\n", ip);
  }
}

Okay, this is going to be difficult. First we reset the function using reset. Reset is a macro:

#define reset(buf) memset(&buf, 0, sizeof(buf))

On line 3 I specify the protocol family:

  addr->sin_family = AF_INET;

You can specify AF_INET or PF_INET, there is no difference. In the following lines we enter the recipient's IP:

  ... 
  int inet_ptoned = inet_pton(AF_INET, ip, &addr->sin_addr.s_addr);
  if (!inet_ptoned) {
    char *new_ip = getipbydom(ip);
    inet_pton(AF_INET, new_ip, &addr->sin_addr.s_addr);
    printf("PING sending to %s\n", new_ip);
  } else if (inet_ptoned == -1) {
    puts("IP isn't correct!");
    exit(-1);
  } else {
    printf("PING sending to %s\n", ip);
  }
}

Moreover, if the user specified a domain (google.com) instead of IP (173.194.73.139), then it still transforms it into an IP address and puts it in the sockaddr_in structure.

We won’t go into details about how the getipbydom function works; you can find the source code and read it yourself.

Hurray, we have filled out the sockaddr_in structure! It contains the recipient's IP. Well, now the most interesting part – filling out the ICMP header structure.

Let's look at the types of ICMP packets:

Type

Name

Who sends

0

Echo Reply

Server

3

Destination Unavailable

Server

4

Source Quench

Server

5

Redirect

Server

8

Echo

Client

eleven

Time Exceeded

Server

12

Parameter Problem

Server

13

Timestamp

Client

14

Timestamp Reply

Server

15

Information Request

Client

16

Information Reply

Server

As you can see, clients send only 3 types of packets to the server: Echo, Timestamp, Information Request. If successful, the server sends back Echo Reply, Timestamp Reply, Information Reply. Of course, some kind of error like Destination Unreachable may come back, but this is not particularly important to us. The main thing is that we get an answer.

We will send an Echo packet, this is the type they send most often. Of course, the headers look different for all of these types, but we'll look at the following for Echo:

1 byte

1 byte

1 byte

1 byte

Type

Code

Checksum

Identifier

Sequence Number

Data (for example, IP header)

We set the type to 8 (see previous table). We also reset the code (we don’t need it). We will calculate the checksum (checksum is the value for checking the integrity of the package). We set the identifier through the getpid function, and Sequance (hereinafter simply seq) is also set to random.

This macro function fills the ICMP header:

#define fill_icmphdr(icmph)                            \
  do {                                                 \
    icmph->type = 8;                                   \
    icmph->code = 0;                                   \
    icmph->checksum = 0;                               \
    icmph->un.echo.sequence = rand();                  \
    icmph->un.echo.id = getpid();                      \
    icmph->checksum = checksum(icmph, sizeof(*icmph)); \
  } while (0)

Please note that we first reset the checksum to zero, and then calculate it. Linux can just install its own if we don't reset it.

An experienced reader may argue that first seq should be set to one, and with each new package it should be increased by 1, but it is unlikely that anyone will reject a package simply because you did not set seq.

The checksum is calculated quite simply:

u16 checksum(void *b, i32 len)
{
    u16 *buf = b;
    u32 sum = 0;

    for (;len > 1; len -= 2)
        sum += *(buf++);
    if (len == 1)
        sum += *(u8*) buf;

    return ~((sum >> 16) + (sum & 0xffff) + \
    (((sum >> 16) + (sum & 0xffff)) >> 16));
}

Let's create an ICMP header and fill it in:

  char buf[64];
  reset(buf);

  struct icmphdr *icmph = (struct icmphdr*) buf;
  fill_icmphdr(icmph);

If you look at the output of the ping utility…

user@mycomputer:~$ ping google.com
PING google.com (173.194.73.138) 56(84) bytes of data.
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=1 ttl=56 time=23.8 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=2 ttl=56 time=23.2 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=3 ttl=56 time=24.3 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=4 ttl=56 time=23.5 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=5 ttl=56 time=23.3 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=6 ttl=56 time=23.1 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=7 ttl=56 time=24.3 ms
64 bytes from lq-in-f138.1e100.net (173.194.73.138): icmp_seq=8 ttl=56 time=25.4 ms
^C
--- google.com ping statistics ---
8 packets transmitted, 8 received, 0% packet loss, time 7013ms
rtt min/avg/max/mdev = 23.119/23.877/25.436/0.728 ms

…then we will see that the program also notifies us about seq (package number) and ttl. But what is TTL? This stands for Time To Life, and the IP header has this parameter. When sent, it is set to 64 (may vary), and each time a router forwarding the packet is encountered, it is decremented by 1. When the ttl becomes 0, it is destroyed. Why is this necessary? So that packets don’t wander around the Internet forever.

In this case, ttl = 56. This means that when reaching google.com, the packet still has ttl = 56.

How to calculate ttl? To do this, we receive an ICMP response packet, find the IP header in it and get the ttl field:

int get_ttl(int fd)
{
  unsigned char buf[1024];
  struct sockaddr_in reply;
  socklen_t reply_len = sizeof(reply);
  ssize_t read_bytes = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *) &reply, &reply_len);
  if (read_bytes <= 0) {
    return -1;
  }

  struct iphdr *iph = (struct iphdr*) buf;
  return iph->ttl;
}

All that remains is to send packets once per second and receive TTL from them, as well as the time spent on sending:

  ...  
  int seq = 1, ttl;
  struct timeval start, end;
  long mtime, seconds, useconds;

  for (;;) {
    gettimeofday(&start, 0); /* начало отсчета времени */

    send_pkt(fd, buf, icmph, dest);
    ttl = get_ttl(fd);

    gettimeofday(&end, 0); /* конец отсчета времени */

    seconds = end.tv_sec - start.tv_sec;
    useconds = end.tv_usec - start.tv_usec;
    mtime = (seconds * 100 + useconds / 1000.0) + 0.5; /* расчет времени */

    printf("ttl = %d, seq = %d, time=%ldms\n", ttl, seq++, mtime);
    usleep(1000000); /* 1 second */
  }

  close(fd);
  return 0;

Congratulations! Now you know how ping works, what ICMP is, how to send packets using it, and what TTL is.

If you want to support the author, you can put a star on github.

Ibid. You can also view the entire source code.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *