C++, ping и traceroute

от автора

Предыстория

Изучая сетевое программирование и имея в портфолио несколько проектиков на C++, связанных с сетевым программированием, я решил написать что-нибудь, что будет иметь реальное практическое применение.

Первое, что мне пришло в голову — утилита ping.

Ping — утилита для проверки целостности и качества соединений в сетях на основе TCP/IP, а также обиходное наименование самого запроса

Я подумал, что почитав доки: https://www.rfc-editor.org/rfc/rfc792, смогу написать собственную имплементацию.

Ping

В принципе, алгоритм прост и понятен: отправляешь пакет и засекаешь время до ответа.

Спустя несколько дней был готов приемлемый вариант ping-а, который есть на Github.

Для понимания работы traceroute необходимо иметь представление о работе ping-а, так что разбор некоторых строк кода не повредит.

pid_t ppid = getppid();

В этой строчке мы получаем идентификатор потока нашего ping-a, которому выделено поле в протоколе ICMP.

Далее создается структура ICMP заголовка:

struct icmpHeader {     uint8_t type;     uint8_t code;     uint16_t checksum;      union {         struct {             uint16_t identifier;             uint16_t sequence;             uint64_t payload;         } echo;          struct ICMP_PACKET_POINTER_HEADER {             uint8_t pointer;         } pointer;          struct ICMP_PACKET_REDIRECT_HEADER {             uint32_t gatewayAddress;         } redirect;     } meta; };

Поля type, code, checksum — обязательные. В пинге мы использовали только echo, структура из 7 — 11 строк, но другие структуры — осколки имплементации ICMP, которые в принципе можно было бы убрать. В дальнейшем те же структуры будут использоваться и в traceroute.

Далее идет функция генерации интернет-чексуммы:

uint16_t checksum(const void *data, size_t len) {     auto p = reinterpret_cast<const uint16_t *>(data);      uint32_t sum = 0;      if (len & 1) {         sum = reinterpret_cast<const uint8_t *>(p)[len - 1];     }      len /= 2;      while (len--) {         sum += *p++;         if (sum & 0xffff0000) {             sum = (sum >> 16) + (sum & 0xffff);         }     }      return static_cast<uint16_t>(~sum); }

После отправки пакета, нам нужно засечь время, которое пакет шел от нас к цели и обратно:

 long int send_flag = sendto(sock, &icmpPacket, sizeof(icmpPacket), 0,                               (struct sockaddr *) &in_addr,                              socklen_t(sizeof(in_addr)));          sent++;          uint64_t ms_before = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();           if (send_flag < 0) {             perror("send error");             return;         }          char buf[1024];          auto *icmpResponseHeader = (struct icmpHeader *) buf;          struct timeval tv;         tv.tv_sec = response_timeout;         tv.tv_usec = 0;          setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));         setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));           int data_length_byte = recv(sock, icmpResponseHeader, sizeof(buf), 0);          if(data_length_byte == -1) {             cout << "\033[1;31m" << "Host unreachable or response timeout." << "\033[0m" << "   ";             cout << "Sequence: " << "\033[1;35m" << i << "\033[0m" << "    ";             cout << "Process id: " << "\033[1;35m" << ppid << "\033[0m" << endl;             continue;         }          uint64_t ms_after = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();

Строки создания пакета и присваивания его полям значений опущены, так как не содержат критически важной информации. Стоит отметить только, что ICMP выделяет поле для бинарных данных, которые в коде заполнены набором единиц и нулей.

Traceroute

Из Википедии:

Traceroute — это служебная компьютерная программа, предназначенная для определения маршрутов следования данных в сетях TCP/IP.

Для начала, я проанализировал пакеты оригинального линуксового traceroute Wireshark-ом.

Поскольку отправка «сырых» пакетов требует рут-привилений, traceroute использует UDP, отправляя пакеты с увеличивающися TTL на рандомный порт цели и ждет получения ответа о закрытости порта.

Все же я подумал, что писать на ICMP будет проще, хоть и для запуска нужен будет рут.

Traceroute отправляет эхо-пакеты с увеличивающимся TTL. TTL — Time To Live, время жизни пакета и по дефолту он равен 30. Время жизни пакета уменьшается после прохождения им каждого узла в сети. Допустим, мы хотим найти маршрут до 1.1.1.1:

Если отправить пакет с TTL 1, то он упрется в первый узел (обычно в ваш роутер 192.168.0.1 или в подсеть) и тот вернет ответ: TTL exceeded, что значит «время жизни истекло». Из его ответа можно вытащить ip_src и таким образом узнать IP-адрес первого узла. Взяв TTL 2, можно узнать IP-адрес второго узла и т.д

Наглядно это можно увидеть на сайте https://www.ip-lookup.org/visual/traceroute

Traceroute также есть на Github

Меняется тип пакета (8) и каждую итерацию цикла увеличивается TTL.

icmp_packet.type = 8; icmp_packet.code = 0; icmp_packet.checksum = 0; icmp_packet.meta.echo.identifier = ppid; icmp_packet.meta.echo.sequence = i; icmp_packet.meta.echo.payload = 0b101101010110100101; // random binary data icmp_packet.checksum = checksum(&icmp_packet, sizeof(icmp_packet));
int ttl = i + 1;  setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl));

В следующем коде мы проверяем, от кого пришел ответ — от цели или нет. Если от цели, то прерываем цикл и показываем результаты.

if (strcmp(inet_ntoa(src_addr.sin_addr), ip) == 0) {     cout << endl << "\033[1;35m" << ttl << "\033[0m" << " hops between you and " << ip << endl;     break; }

Я считаю, что traceroute и ping — утилиты, которые улучшат портфолио и помогут глубже разобраться с сетевым программированием. В любом случае для общего развития рекомендую прочитать https://www.rfc-editor.org/rfc/rfc791 (про протокол IP).


ссылка на оригинал статьи https://habr.com/ru/articles/726714/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *