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(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).