*Первый пост, прошу отнестись лояльно, здравая критика приветствуется
Предыстория
Изучая сетевое программирование и имея в портфолио несколько проектиков на 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)
Apoheliy
04.04.2023 06:34+3Здоровая критика про пинг:
неплохо бы переменным sent, received присваивать начальные значения;
при разборе командной строки возможен крэш, если, например, указать ключ -с без числа. С остальными - аналогично;
если в командной строке указать несколько аргументов, но не указать destination, то может быть крэш по содержимому ip;
устанавливать опции сокета после отсылки каждого пакета? Может лучше один раз до всяких отсылок?
лучше добавить контроль результата при установке опций сокета - вдруг не установилось?
если при получении ответа пришло меньшее количество байт, то далее будет работа с мусорными (предыдущими) данными;
лучше не миксовать C подход (usleep, "\n") и C++ (chrono, endl);
в выводе help можно указать дефалтовые значения для параметров (10 пакетов и т.д.);
cout << "'####::'######::'##::::'##:'########::'##::::'##:'########:::'######::\n" - Э-э-э-м ... безымянная гордыня? Может лучше фамилия, имя, год создания?
scmnui
04.04.2023 06:34+1Простите! А где здесь C++? Все написано в сишном стиле. Мне кажется эту статью надо было определить в C, а не в C++ раздел.
mihmig
Всегда хотел научиться писать на 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-сервер
neon1ks
https://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list
https://ru.stackoverflow.com/questions/454263/Книги-и-учебные-ресурсы-по-С
https://sijinjoseph.com/programmer-competency-matrix/
https://github.com/omreps/programmer-competency-matrix/blob/master/README.md
CrashLogger
Так вам надо не учиться писать на С++, а учиться собирать чужие проекты. Это намного проще. Достаточно прочитать документацию по autotools, cmake, ninja, whatever... Будут возникать вопросы - задавайте на StackOverflow или в телеграм каналах.