Принцип работы утилиты ping в Linux

c64c82944a42c58a8406a1f33c0c5c8e

Если вы решили прочесть эту статью, то наверняка вы когда-нибудь работали в ОС Linux. Сегодня я вам расскажу, как работает довольно популярная команда ping и покажу, как реализован ее функционал на языке Си.

Утилита ping стоит в Linux по умолчанию. Пример ее использования (указываем домен или айпи в параметры, например google.com или 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

Но что же значит то, что эта команда вывела на экран? Команда ping отсылает пакеты на сайт, который мы указали (google.com), и анализирует. Справа мы можем увидеть значение time, например:

time = 23.8 ms

Значение time показывает, за сколько времени данные доходят до google.com и возвращаются обратно, в миллисекундах. На примере видно, что пакет, который отправила утилита ping, дошел до гугла и вернулся обратно за 23.8 мс. А о том, что значат другие ключевые слова, расскажу позже.

Взгляните на сетевую модель OSI:

Модель

Уровень

Тип данных

Функции

Протоколы

7. Прикладной

Данные

Доступ к сетевым службам

HTTP, FTP

6. Представления

Представление и шифрование данных

ASCII

5. Сеансовый

Управление сеансом связи

RPC, PAP

4. Транспортный

Сегменты / Датаграммы

Прямая связь между конечными пунктами и надёжность

TCP, UDP, SCTP, Порты

3. Сетевой

Пакеты

Определение маршрута и логическая адресация

IP, IPv6, ICMP

2 .Канальный

Биты / Кадры

Физическая адресация

Ethernet, ARP, Сетевая карта

1. Физический

Биты

Работа со средой передачи, сигналами и двоичными данными

USB, RJ

Всемирный интернет работает именно по этой модели. Самый нижний уровень — физический. Ваш роутер пересылает биты по проводам и оптоволоконным кабелям, однако мы этот уровень рассматривать не будем.

Второй уровень — канальный. По его протоколам (договоренностям по передаче данных) компьютер уже хоть как-то пересылает данные на роутер. Если вы когда-нибудь пользовались проводным интернетом, то наверняка видели для него кабель, имеющий довольно специфичную форму. На этом уровне мы можем пересылать кадры через свои программы, однако он весьма сложен для понимания.

Третий же уровень (мы ему посвятим большую часть статьи) позволяет пересылать пакеты. Вы наверняка знаете, что такое IP-адрес: идентификатор компьютера в интернете. Также мы рассмотрим ICMP протокол в этой статье.

Ну, а четвертый уровень (на нем и закончим, я знаю, что вам надоело про них читать) — самый-самый распространенный уровень. 80% всех программ, входящий в интернет, пересылают данные по TCP протоколу. Тут уже появляются порты: на них присылаются пакеты по протоколам всех более высоких уровней.

Итак, по какому протоколу утилита ping отсылает свои пакеты? По ICMP. Для ICMP пакетов не нужно указывать порт, лишь IP-адрес. К тому же практически все компьютеры при получении ICMP пакета отсылают обратно ответ на него.

Очевидно, что если мы не получаем ответ на ICMP пакет — значит, сервер выключен.

Ну что ж, давайте программировать! Я написал небольшую утилиту на языке Си, которая работает почти так же, как и ping. Она называется vping (очень оригинально).

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

Видите, почти то же самое! Сперва подключим нужные заголовки:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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;

Да, их не мало. Typedef’ы я делаю для большей читабельности и более короткого кода.

Давайте заглянем в функцию main:

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

Я вывожу ошибку, если пользователь случайно забыл указать айпи или домен. Затем я создаю сокет:

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

Обратите внимание, что я создаю сокет с типом SOCK_RAW. Это так называемые «сырые» сокеты. Используя их, можно послать не только TCP или UDP пакет, но и вообще любой, какой только захочется! Именно они требуют прав суперпользователя root, потому программу нужно запускать через sudo. Я, конечно же, указываю протокол IPPROTO_ICMP для отправки ICMP пакета.

Теперь я создаю структуру с типом sockaddr_in, хранящую IP-адрес получателя пакетов (например, гугла):

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

Напомню, что argv[1] — это IP-адрес или домен, на который мы отсылаем пакеты. Давайте посмотрим содержимое функции fill_sockaddr_in:

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);
  }
}

Ладно, тут уже придется непросто. Сперва мы обнуляем функцию через reset. Reset — это макрос:

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

В строке 3 я указываю семейство протоколов:

  addr->sin_family = AF_INET;

Можно указать AF_INET или PF_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);
  }
}

Причем если пользователь указал вместо айпи (173.194.73.139) домен (google.com), то она его все равно трансформирует в IP-адрес и кладет в структуру sockaddr_in.

В подробности работы функции getipbydom мы не будем, вы и сами сможете найти исходный код и почитать его.

Ура, мы заполнили структуру sockaddr_in! Именно она содержит айпи получателя. Ну, а теперь самое интересное — заполнение структуры ICMP заголовка.

Давайте рассмотрим типы ICMP пакетов:

Тип

Название

Кто отправляет

0

Echo Reply

Сервер

3

Destination Unreachable

Сервер

4

Source Quench

Сервер

5

Redirect

Сервер

8

Echo

Клиент

11

Time Exceeded

Сервер

12

Parameter Problem

Сервер

13

Timestamp

Клиент

14

Timestamp Reply

Сервер

15

Information Request

Клиент

16

Information Reply

Сервер

Как вы можете видеть, клиенты отсылают на сервер только 3 типа пакетов: Echo, Timestamp, Information Request. В удачном случае сервер отправляет назад Echo Reply, Timestamp Reply, Information Reply. Конечно, может прийти назад какая-то ошибка по типу Destination Unreachable, но нам это не особо важно. Главное — чтобы мы получили ответ.

Мы будем слать Echo пакет, такие шлют чаще всего. Конечно, заголовки для всех этих типов выглядят по-разному, однако мы рассмотрим для Echo:

1 байт

1 байт

1 байт

1 байт

Type

Code

Checksum

Identifier

Sequence Number

Data (например, IP заголовок)

Тип ставим 8 (см. предыдущую таблицу). Код тоже обнуляем (нам он не нужен). Чексумму мы рассчитаем (чексумма — значение для проверки целостности пакета). Идентификатор мы ставим через функцию getpid, ну, а Sequance (далее просто seq) ставим тоже случайным.

Вот эта макрос-функция заполняет ICMP заголовок:

#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)

Обратите внимание, что мы сперва чексумму обнуляем, а затем уже ее рассчитываем. Линукс может просто поставить свою, если мы не обнулим.

Опытный читатель может возразить, что сперва seq нужно поставить в единицу, а при каждом новом пакете увеличивать его на 1, однако вряд ли кто-то отклонит пакет, просто потому что вы seq не поставили.

Чексумма рассчитывается весьма просто:

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));
}

Давайте же создадим ICMP заголовок и заполним его:

  char buf[64];
  reset(buf);

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

Если посмотреть на вывод утилиты ping…

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

…то мы увидим, что программа оповещает нас еще и о seq (номер пакета), и о ttl. Но что же такое ttl? Это расшифровывается как Time To Life, и этот параметр есть у IP заголовка. При отправке он ставится в значение 64 (может отличаться), а при каждой встрече маршрутизатора, перенаправляющего пакет, уменьшается на 1. Когда ttl становится равным 0, он уничтожается. Зачем это надо? Чтобы пакеты по интернету не блуждали вечно.

В данном случае ttl = 56. Это значит, что, доходя до google.com, у пакета остается ttl = 56.

Как же посчитать ttl? Для этого мы получаем ответный ICMP пакет, находим в нем IP заголовок и получаем поле ttl:

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;
}

Осталось лишь отправлять пакеты раз в секунду и получать с них ttl, а также затраченное на отправку время:

  ...  
  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;

Поздравляю! Теперь вы знаете, как работает ping, что такое ICMP, как отослать по нему пакеты и что такое ttl.

Если хотите поддержать автора, можете поставить звездочку на гитхабе.

Там же можете и посмотреть весь исходный код.

© Habrahabr.ru