*Первый пост, прошу отнестись лояльно, здравая критика приветствуется

Предыстория

Изучая сетевое программирование и имея в портфолио несколько проектиков на 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).

Комментарии (6)


  1. mihmig
    04.04.2023 06:34

    Всегда хотел научиться писать на C++
    Подскажите, есть ли какие материалы, где можно постепенно, по шагам изучить минимальный "стек". Вот какой путь
    1. Умею собирать и запускать std::cout << "Hello World!\n"; на Linux
    2. Умею собирать open-source- проекты через make install
    ...
    Я здесь :(
    ...
    N-1. Умею выполнять кросс-компиляцию для SoC Type: MediaTek MT7620A Linux version 4.14.215 gcc version 7.5.0 OpenWrt 19.07.6
    N. Собрал и запустил утилиту upload-a файлов из локальной папки на FTP-сервер



    1. CrashLogger
      04.04.2023 06:34
      +1

      Так вам надо не учиться писать на С++, а учиться собирать чужие проекты. Это намного проще. Достаточно прочитать документацию по autotools, cmake, ninja, whatever... Будут возникать вопросы - задавайте на StackOverflow или в телеграм каналах.


  1. Apoheliy
    04.04.2023 06:34
    +3

    Здоровая критика про пинг:

    • неплохо бы переменным sent, received присваивать начальные значения;

    • при разборе командной строки возможен крэш, если, например, указать ключ -с без числа. С остальными - аналогично;

    • если в командной строке указать несколько аргументов, но не указать destination, то может быть крэш по содержимому ip;

    • устанавливать опции сокета после отсылки каждого пакета? Может лучше один раз до всяких отсылок?

    • лучше добавить контроль результата при установке опций сокета - вдруг не установилось?

    • если при получении ответа пришло меньшее количество байт, то далее будет работа с мусорными (предыдущими) данными;

    • лучше не миксовать C подход (usleep, "\n") и C++ (chrono, endl);

    • в выводе help можно указать дефалтовые значения для параметров (10 пакетов и т.д.);

    • cout << "'####::'######::'##::::'##:'########::'##::::'##:'########:::'######::\n" - Э-э-э-м ... безымянная гордыня? Может лучше фамилия, имя, год создания?


  1. domix32
    04.04.2023 06:34

    if (len & 1) {

    Нечётный байт наголо добавляется в чексуму?


  1. scmnui
    04.04.2023 06:34
    +1

    Простите! А где здесь C++? Все написано в сишном стиле. Мне кажется эту статью надо было определить в C, а не в C++ раздел.