LXF121:Работать в сети

Материал из Linuxformat.

Перейти к: навигация, поиск
Ping Создадим собственный клон самой популярной диагностической утилиты

Содержание

Linux: Сетевой проект

Изобретать велосипед – задача неблагодарная, а вот разбирая готовый велосипед, можно узнать много нового и полезного. Артем Коротченко напишет для вас утилиту Ping.

Давайте вспомним самое начало истории нашей любимой операционной системы. В 1991 году один простой, но весьма смышленый финский студент опубликовал исходные коды первой версии ядра своей собственной ОС. Тогда она была еще неработоспособной и не особо полезной на практике, однако эксперимент по созданию первого свободного Unix заинтересовал определенные круги программистов.

Удивительно, что все началось не с закрытой разработки какой-нибудь коммерческой организации, а с сообщения в публичной телеконференции Usenet. Само по себе это уже является нонсенсом в индустрии программного обеспечения начала девяностых. Однако я назвал Linux сетевым проектом даже не потому, что он – дитя Интернета. К настоящему времени Linux прошел огромный путь, но до сих пор является плодом труда тысяч хакеров-энтузиастов. До сих пор его разработка не централизована (не имеет лидера), из чего следует второй смысл утверждения, вынесенного в заголовок: Linux – сетевой, в смысле децентрализованный, проект.

Итак, Сеть объединила самых талантливых людей со всего мира в работе над одним общим делом, с великой и благородной целью. Естественно, первоклассная поддержка Интернета и сетевого взаимодействия в такой системе должна была стать одной из важнейших ее особенностей. Понять, насколько верно это предположение, можно, оценив лишь тот факт, что многие (если не большинство) серверы в Интернете работают под управлением Linux.

Сетевая суть Linux отразилось и на писавшемся для него прикладном ПО. Даже те игровые серверы, клиентские части которых рассчитаны на Windows, в большинстве своем написаны для Linux. Нет сомнений в изобилии серверов Web, почты, FTP, DNS... Количество клиентских приложений и вовсе неисчислимо. Рожденный в Сети, Linux моментально обзавелся собственной реализацией интернет-протоколов. Освоить программирование в Linux – это значит научиться писать для него сетевое ПО.

Сетевое взаимодействие

Самая популярная сетевая программа – это, пожалуй, web-клиент. Поэтому я сначала думал написать маленький web-браузер. Но в таком случае программировать бы пришлось только HTTP-протокол, не спускаясь на нижние уровни.

Поэтому я остановился на всем известной утилите ping. Она легко реализуема, проста по сути и основана на протоколе низкого уровня (ICMP), что делает ее идеальной для начала разбирательства в сетевом программировании. Но что такое уровни протоколов? Что такое протокол? Глубоко ознакомиться с этой темой можно в Интернете (например, почитать документы RFC); я лишь затрону самое необходимое.

Обмен информацией между двумя хостами – сложный процесс. Международная организация по стандартизации применяет для его описания семь универсальных уровней OSI. Для нашего случая (TCP/IP) они объединены в четыре. Каждому уровню соответствуют протоколы – языки, благодаря которым узлы сети понимают друг друга.

В самом простом смысле взаимодействие двух систем представляет собой чередование электрических импульсов; для Wi-Fi — передачу модулированного радиосигнала. Но могут быть даже механические/акустические вибрации: во всех трех случаях речь идет об уровне доступа к сети (Network Interface Layer). О связанной с ним информации говорится как о наборе кадров; в качестве протоколов здесь фигурируют Ethernet, PPP.

Физические сигналы несут в себе некие потоки данных (электронное письмо, закачка файлов по FTP, онлайн-телевидение), суть которых второму хосту разобрать пока что нереально. А главное, неизвестно, кому они адресованы. Ведь очевидно, что между обменивающимися системами всегда будут единицы, десятки и сотни посредников; не будет их только в соединении напрямую.

Поэтому кадры сопровождаются адресами: исходным и получателя. Тогда говорится уже о втором, межсетевом уровне (Internet Layer), задача которого заключается в маршрутизации передаваемых данных до точки назначения. Об информационных потоках теперь говорится не как о последовательности битов, а как о датаграммах. Протокол, призванный распознавать адреса в любых датаграммах – IP (по правде сказать, есть еще MAC-адреса в Ethernet-заголовке, но сегодня они нас не интересуют). ICMP-протокол (диагностические сообщения, ошибки) также относится к межсетевому уровню.

Далее производится проверка. Если адрес стороны, получившей пакет, не совпадает с адресом назначения, система передает его дальше по сети. Таким образом, выше второго уровня чужие пакеты никогда не поднимаются. Если адреса совпадают, то можно уже думать, какому именно локальному сервису предназначается содержимое пакета. Для этого он поднимается на транспортный уровень (Host-to-Host Layer). По IP-заголовку вычисляется тип пакета (TCP или UDP), и соответствующая подсистема читает из этих данных свою ключевую информацию – порты отправляющей и принимающей стороны. TCP-заголовки содержат также флаги-параметры (запрос разрешения на установление сеанса, подтверждение сеанса, завершение и другие). В UDP-взаимодействии не происходит установления стабильного сеанса, поэтому этот протокол является более быстродействующим, но менее надежным (и от того менее популярным) аналогом TCP. В общем, связка TCP и IP – это основа функционирования Интернета.

Допустим, контрольные суммы совпали, никаких проблем не возникло. Теперь данными занимается подсистема прикладного уровня (Application Layer). Пакеты с 80‑м TCP-портом будут отправлены Web-серверу, с 21‑го их получит FTP, 25‑м займется SMTP. Конечно, это произойдет, если соответствующие демоны запущены, работают и не перенастроены на нестандартные порты. Иначе говорится, что порт закрыт, и тогда отправленные ему пакеты отбрасываются. (Это для сервера, клиентской стороне слушать порты обычно не нужно.)

Поскольку каждый уровень перед отправкой данных наверх убирает свой заголовок, ни одна из сетевых подсистем не знает предыстории пакетов. Прикладному уровню достаются чистые данные. Например, все, что увидит Web-сервер – это запрос на выдачу некой страницы, а Jabber-сервер получит XML-код вида:

<iq type='get' id='auth_некий md5-хэш'><query xmlns='jabber:iq:auth'>
<username>имя пользователя, под которым клиент собирается войти в систему</username>
</query></iq>

В соответствии с протоколом прикладного уровня Jabber (правильнее сказать, XMPP) он сгенерирует XML-ответ с просьбой ввести пароль. И теперь пойдет обратный процесс – формирование пакета. Это сообщение будет спускаться с верхнего уровня к нижнему, снабжаясь заголовками с IP-адресами, контрольными суммами и прочими атрибутами, а затем уйдет в Сеть.

Приступим к делу

Вся суть ping сводится к тому, чтобы посылать на адрес проверяемого хоста запросы ICMP Echo-Request и получать (или не получать) от него ответы ICMP Echo-Reply. Кроме того, утилита должна учитывать время задержки пакетов в сети.

Наша реализация будет не хуже оригинала, за тем лишь исключением, что вместо десятков входных параметров она сумеет понимать лишь самые важные: -- help, -- version, -c и, конечно, IP-адрес исследуемой системы. Аргумент «c» отвечаетза количество запросов, которые нужно отослать. Если он не будет задан, приложение будет работать до тех пор, пока его не завершат вручную. Обработка аргументов целиком реализуется с помощью функции getopt_long(), подробнее о которой вы можете прочитать в номере LXF112.

Наш клиент отправляет что-то серверу (четырехуровневая модель TCP/IP)

Помимо основных заголовков, нам нужны библиотеки сетевых функций и структур пакетов:

#include <netdb.h>
 #include <netinet/ip.h>
 #include <netinet/ip_icmp.h>

Теперь можно создать сокет:

int socket(int domain, int type, int protocol);

Первым аргументом этой функции обычно выбирается константа PF_INET, означающая, что мы желаем работать с протоколами IPv4.

Чтобы получить доступ к транспортному уровню, вторым аргументом указывается SOCK_STREAM для соединений TCP и SOCK_DGRAM для UDP. Третий аргумент всегда равен 0. Но чтобы формировать ICMP-пакеты, нам нужен межсетевой уровень, поэтому будем использовать так называемые «сырые» сокеты:

sp = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);

Можно идти дальше и задавать сокеты для доступа к заголовкам физического уровня (например, для работы с ARP-пакетами, выявляющими соответствие между IP- и Ethernet-адресами), но нам пока это не требуется. Разрешим широковещательные сообщения и увеличим размер приемного буфера:

int on = 1, size = 61440;
 …
 setsockopt(sp, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
 setsockopt(sp, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));

Заданный во входных параметрах IP (хост) представляет собой строку, массив переменных типа char. Но этого мало, и нужно использовать специальные адресные структуры servaddr, которые будут содержать более полную информацию об адресате.

struct hostent *hp;
 struct sockaddr_out servaddr;
 ...
 /* iparg – указатель на аргумент с адресом (arv[]) */
 hp = gethostbyname(iparg);
 ...
 /* Обнуляем структуру */
 bzero(&servaddr_out, sizeof(servaddr_out));
 servaddr_out.sin_family = AF_INET;
 servaddr_out.sin_addr = *((struct in_addr *) hp->h_addr);

Для получения пакетов нам потом понадобится еще одна такая структура – sockaddr_in.

В нашей программе будет два типа сигналов: информирующий о необходимости послать новый пакет (генерируется каждую секунду) и завершающий (например, по Ctrl+C). Назначим им функции-обработчики, зададим таймер:

struct itimerval tval;
 ...
 /* Обнуляем значение интервала */
 timerclear(&tval.it_interval);
 /* Устанавливаем значение интервала - 1 секунду */
 tval.it_interval.tv_sec = 1;
 /* Обнуляем значение времени срабатывания*/
 timerclear(&tval.it_value);
 /* Устанавливаем его в 1 микросекунду */
 tval.it_value.tv_usec = 1;
 /* Связываем сигнал таймера с функцией-обработчиком */
 (void) signal(SIGALRM, handler);
 /* Связываем сигнал завершения с функцией-обработчиком */
 (void) signal(SIGINT, handler);
 /* Запускаем таймер */
 (void) setitimer(ITIMER_REAL, &tval, NULL);

Прием и передача

Это были подготовительные действия. Теперь нужно отправлять и принимать пакеты. Первое возлагается на обработчик handler():

void handler(int signo)
 {
  int losscount;
  struct icmp *icmp;
 char sndbuf[BUFSIZE];
 if(signo == SIGINT) {
   /* … Вывести результаты - код опущен … */
 exit(0);
 }
 if(signo == SIGALRM) {
   icmp = (struct icmp *) sndbuf;
   icmp->icmp_type = ICMP_ECHO;
   icmp->icmp_code = 0;
   icmp->icmp_id = pid;
   icmp->icmp_seq = ++sntcount;
   gettimeofday((struct timeval *) icmp->icmp_data, NULL);
   icmp->icmp_cksum = in_cksum((unsigned short *) icmp, 64);
   if(sendto(sp,sndbuf,64,0,(struct sockaddr*)&servaddr_out, sizeof(servaddr_out))<0){
     fprintf(stderr, “%s: sendto failed: %s\n”, myname, strerror(errno));
   exit(1);
 }
 fflush(stdout);
 }
 }

Как видно, вторая часть функции занимается сигналами таймера. Каждую секунду некая область оперативной памяти компьютера объявляется буфером sndbuf, и в его бессмысленном наборе байтов происходит формирование пакета. С помощью указателя последовательно заполняются поля ICMP-заголовка: * Тип и код выбираются такими, чтобы полученный пакет представлял собой именно Echo Request.

  • Поле id выставляется по возвращаемому значению функции getpid() (это нужно, чтобы потом игнорировать сообщения Echo Reply, предназначенные другим процессам).
  • sequence – порядковый номер отправляемого пакета. Контрольная сумма рассчитывается по алгоритму из RFC с использованием популярной реализации in_cksum().

В данные пакета записывается текущее время. Поскольку в ответах они будут дублироваться, мы сможем определять время отклика хоста. Нам не нужен доступ к IP-заголовку, а потому его заполнение доверяется системе.

Теперь, когда имеется открытый сокет, заполненная адресная структура и подготовленный пакет, функция sendto() отправляет его в Сеть.

Прием сообщений производится в бесконечном цикле main():

int rcvlen;
 int servaddr_in_len = sizeof(servaddr_in);
 /* Длины заголовков */
 int ip_len, icmp_len;
 /* Буфер для полученного пакета */
 char rcvbuf[BUFSIZE];
 /* Время оборота пакета в миллисекундах */
 float time;
 /* Указатель на IP-заголовок */
 struct ip *ip;
 /* Указатель на ICMP-заголовок */
 struct icmp *icmp;
 /* Структура времени получения пакетов */
 struct timeval trcvbuf;
 /* Указатели на структуры времени получения и отправки пакетов */
 struct timeval *trcv, *tsnd;
 struct itimerval tval;
 ...
 trcv = &trcvbuf;
 /* Пакет начинается с IP-заголовка */
 ip = (struct ip *) rcvbuf;
 while(1) {
  /* Получаем пакет */
  rcvlen = recvfrom(sp, rcvbuf, sizeof(rcvbuf), 0, (struct sockaddr *)&servaddr_in, &servaddr_in_len);
  if(rcvlen < 0) {
   if(errno == EINTR)
    continue;
   fprintf(stderr, “%s: recvfrom() failed: %s\n”, myname, strerror(errno));
   return 1;
   }
  ip_len = ip->ip_hl << 2;
  icmp = (struct icmp *) (rcvbuf + ip_len); // ICMP-заголовок идет после IP-заголовка
  icmp_len = rcvlen – ip_len;
  /* Продолжаем, только если получили ICMP Echo Reply пакет, который предназначается
  именно нашей программе */
  if(icmp->icmp_type == ICMP_ECHOREPLY && icmp->icmp_id == pid) {
   rcvcount++;
   tsnd = (struct timeval *) icmp->icmp_data;
   /* Посчитаем время оборота пакета путем вычитания времен получения и отправки пакетов. Секунды и микросекунды можно перевести
   в миллисекунды и сложить. */
   gettimeofday(trcv, NULL);
   time = (trcv->tv_sec-tsnd->tv_sec)*1000+(trcv->tv_usec-tsnd->tv_usec)/(float)1000;
   /* ... Вывод результатов на экран – код опущен ... */
   /* Если обработано заданное количество пакетов, завершаем программу */
   if(sntcount == c && c)
    handler(SIGINT);
   }
  }

Тут перед нами стоит противоположная задача. В буфер rcvbuf записываются принимаемые из Сети данные, и нужно выделить в нем структуры сетевых заголовков. Буфер начинается с IP-заголовка, одно из полей которого характеризует его размер. Обратившись к этому полю, легко определить, где кончается часть IP и начинается заголовок ICMP. Длина последнего необходима для вывода строки результатов, определяется она еще проще – до конца буфера.

С определением начала ICMP-заголовка нам становятся доступны его поля, которые мы сразу же и проверяем. Если все в порядке – получен пакет Echo Reply, притом предназначающийся нашему процессу – значит, счетчик полученных ответов можно увеличить на единицу. Обновляем статистику, и затем выводим промежуточные результаты. Все! Полные исходные коды ищите на DVD.

А если проще?

Написание сетевых приложений можно упростить, воспользовавшись популярными библиотеками libpcap и libnet (доступными, кстати, не только в Linux и Unix). Первая предоставляет простой интерфейс для обработки входящего трафика, вторая предназначена для создания пакетов перед их последующей отправкой.

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

Что касается ping, то пользы от переписывания этой утилиты «простым путем» практически нет. libpcap в цикле main() обрабатывал бы входящие эхо-ответы. Ради интереса посмотрим, как libnet сформирует нам эхо-запрос. Добавим соответствующий заголовок:

#include <libnet.h>

Все последующие изменения касаются обработчика handler(), ведь именно он занимается отправлением пакетов. С помощью функции libnet_init() производим инициализацию сеанса:

libnet_t *l;
 l = libnet_init(LIBNET_RAW4, NULL, errbuf);
 if(l == NULL)
  {
   fprintf(stderr, “%s: libnet_init() failed: %s\n”, myname, errbuf);
   exit(1);
  }

Библиотека позволяет работать со всеми сетевыми уровнями и создавать любые заголовки. Мы просто задаем нужные в тэгах протоколов и заполняем их поля:

libnet_ptag_t ip, icmp;
 u_char data[BUFSIZE]; // Буфер поля данных char errbuf[LIBNET_ERRBUF_SIZE]; // Буфер ошибок
 gettimeofday((struct timeval *) &data, NULL); // Записываем время в данные
 icmp = libnet_build_icmpv4_echo(ICMP_ECHO, /* Тип */
                                   0, /* Код */
                                   0, /* Контрольная сумма */
                                 pid, /* ID */
                          ++sntcount, /* Номер очереди */
                                data, /* Указатель на данные */
                                  56, /* Длина данных */
                                   l, /* Указатель на libnet_t */
                                   icmp);
 if(icmp == -1)
  {
   fprintf(stderr, “%s: can't build ICMP header: %s\n”, myname, libnet_geterror(l));
   exit(1);
  }

Как видно, выгода по сравнению с предыдущим подходом все же есть: теперь нам не нужно считать контрольную сумму, libnet займется ею сам. Мы также доверим ему заполнение IP-заголовка, самостоятельно указав лишь IP хоста.

ip4 = libnet_autobuild_ipv4(LIBNET_IPV4_H + LIBNET_ICMPV4_ECHO_H + 56, /* Длина */
 IPPROTO_ICMP, /* Протокол */
 destaddr, /* IP назначения */
 l); /* Указатель на libnet_t */
 if(ip4 == -1)
  {
   fprintf(stderr, “%s: can't build IP header: %s\n”, myname, libnet_geterror(l));
   exit(1);
  }

Кстати, IP-адрес назначения тоже обрабатывается из аргумента немного легче, без заполнения структуры hostent (а потом еще и sockaddr_in – как это было раньше).

u_long destaddr;
 if((destaddr = libnet_name2addr4(l, iparg, LIBNET_RESOLVE)) == -1)
 {/* Выдать ошибку */}

Пакет готов, его можно отправить, а сеанс завершить.

if(libnet_write(l) == -1) {/* Выдать ошибку */}
 libnet_destroy(l);

Проверка боем

Штатная и разработанная нами утилиты ping: найдите одно отличие.

Скомпилируем утилиту. Никаких специальных ключей (если, конечно, вы не используете libpcap или libnet) для этого не потребуется. Просто наберите:

gcc -o minimalping minimalping.c

Прежде чем выполнить полученный файл, запомните: создавать raw-сокеты могут только приложения, запущенные привилегированным пользователем. Чтобы все остальные смогли пользоваться программой, необходимо выбрать ей владельца root, а также установить для нее SUID- или SGID-бит. Теперь можно сравнить результаты работы с оригинальным ping (см. рисунок).

Ну вот, а прошлая версия ping не отличалась совсем! Дело в том, что я перешел на Debian Lenny, и теперь для достижения абсолютной идентичности придется вновь править код: добавить выходные параметры mdev и time. Оставляю вам это в качестве домашнего задания.

Мы затронули лишь частичку удивительного мира. Программирование низких уровней стека TCP/IP открывает в нем безграничные возможности: черные ходы, сканеры, черви, снифферы, или, по другую сторону баррикады, honeypot-системы, брандмауэры и межсетевые экраны. C, особенно в связке с Linux, дает доступ к этим уровням.

Прослушивание в Сети

Можно ли посмотреть внутреннее устройство пакетов, «потрогать» их? Да, можно анализировать весь сетевой трафик, воспользовавшись программой-сниффером. Таким образом вы не только проверите все, о чем рассказывается в этой статье, но, возможно, к вам придет внезапное озарение вместе с глубоким пониманием основ Интернета.

Вероятно, в вашей системе уже есть tcpdump и netstat — классические утилиты Unix. Последняя, хоть и не является анализатором трафика, выдаст массу полезной информации обо всех входящих и исходящих соединениях. Если для вас важен GUI, следует обратить внимание на ettercap и особенно на Wireshark (ранее называвшийся Ethereal).

Давайте посмотрим на него поближе. Установить программу можно через менеджер пакетов вашего дистрибутива. Откройте ее, настройте на прослушивание нужного интерфейса (в моем случае это wlan0). Запустите наш ping – и, как ожидалось, в окне перехваченного трафика можно будет увидеть череду пакетов ICMP Echo-Request и ICMP Echo-Reply.

В нижнем окошке показывается некая последовательность байтов – это шестнадцатиричное представление отправленного эхо-запроса; в среднем – она разбирается по протоколам. Первым идет Ethernet-заголовок с его MAC-адресами и указанием на то, что это IP-пакет. Как вы помните, мы не трогали физический уровень. Далее идет IP-заголовок, для которого мы выбирали лишь IP-адрес назначения. Последующий ICMP-заголовок мы заполняли самостоятельно: тип, код, контрольная сумма, ID, Sequence. Знакомо? Если разобраться с последними 56 байтами собственно данных, увидим, что в них содержится системное время – мы определяли его функцией gettimeofday().

Более подробную информацию о Wireshark ищите на в статье этого номера Wireshark. LXF

Личные инструменты
  • Купить электронную версию
  • Подписаться на бумажную версию