C++, ping и traceroute

6043481afb0a3c29e3fd442af90d7cfc

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

Предыстория

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

    uint32_t sum = 0;

    if (len & 1) {
        sum = reinterpret_cast(p)[len - 1];
    }

    len /= 2;

    while (len--) {
        sum += *p++;
        if (sum & 0xffff0000) {
            sum = (sum >> 16) + (sum & 0xffff);
        }
    }

    return static_cast(~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(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(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).

© Habrahabr.ru