Универсальный есно-север под Linux
В данной статье мы рассмотрим разработку универсального эхо-сервера. Однако вначале стоит сказать, чем же наш эхо-сервер, будет отличаться от обычного эхо сервера. Как известно, обыкновенный эхо-сервер работает по следующему алгоритму: прослушивает определенный TCP или UDP порт и как только на этот порт приходят какие либо данные, он сразу пересылает их обратно отправителю. Таким образом эхо-сервер работает исключительно с данными. Однако иногда возникает задача перенаправлять отправителю не только данные но также и служебную информацию, т.е. все полученные пакеты, причем пакеты пришедшие не на определенный порт а на любой. Такая задача может возникнуть, например, при тестировании сетеобразующего оборудования (интеллектуальных маршрутизаторов ) или файрволов на правильность прохождения пакетов ( файрволинга ).
Сначала мы рассмотрим написание простого эхо-сервера, а затем переделаем его в универсальный, который перенаправляет все пакеты.
Писать наш эхо-сервер будем на языке ANSI C под ОС Linux . При разработке использовалась использовалась ОС Linux Mandrake 10.0. Программа не требует установки никаких дополнительных библиотек, ткак как написана на стандартных типах сокетов в ОС Linux . При разработки были использованы следующие типы сокетов :
- SOCK _ PACKET – пакетный сокет . Сокет работающий только в режиме чтения, т.е. только для приема. Осуществляет прием и обработку пакетов на канальном уровне (кадров Ethernet). Аналогичного типа сокетов в ОС Windows нет.
- SOCK _ RAW – низкоуровневый сокет . Сокет предназначен для работы на сетевом уровне (с протоколом IP ). С его помощью возможно как чтение служебных полей протокола сетевого уровня, так и генерация пакетов (до сетевого уровня включительно). В ОС Windows аналог данного сокета присутствует.
Ниже приведен код простейшего эхо-сервера:
#include <stdio.h> #include <sys/socket.h> #include <resolv.h> #include <arpa/inet.h> #include <errno.h> #define MAXBUF 1024 int main(int Count, char **Strings) { int sockfd; struct sockaddr_in self; char buffer[MAXBUF]; if (Count==1) { printf("Не верный формат вызова!!!\n"); printf("echo-server [port]\n"); exit(1); } /*---Создание сокета TCP---*/ if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0 ) { perror("Ошибка создания сокета"); exit(errno); } /*---Задание адреса и порта серверу---*/ bzero(&self, sizeof(self)); self.sin_family = AF_INET; self.sin_port = htons((int) Strings[1]); self.sin_addr.s_addr = INADDR_ANY; /*---Связь сокета с портом---*/ if ( bind(sockfd, (struct sockaddr*)&self, sizeof(self)) != 0 ) { perror("ошибка связи сокета"); exit(errno); } /*---включение прослушивания---*/ if ( listen(sockfd, 20) != 0 ) { perror("ошибка включения прослушивания"); exit(errno); } printf("Выход из программы по нажатию клавиши ENTER "); /*---Запускаем цикл до нажатия клавиши ENTER... ---*/ while (1) { char i; int clientfd; struct sockaddr_in client_addr; int addrlen=sizeof(client_addr); printf("---\n"); /*---проверка соединения (создание канала данных)---*/ clientfd = accept(sockfd, (struct sockaddr*)&client_addr,&addrlen); printf("%s:%d connected\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); /*---отправка любого сообщения назад---*/ send(clientfd, buffer, recv(clientfd, buffer, MAXBUF, 0), 0); /*---завершение соединения---*/ close(clientfd); //необходимо сделать??? scanf("%c",i); if (i=='\r') { printf("Выход\n"); break; } } printf("Сервер остановлен!!!\n"); /*---Закрытие сокета---*/ close(sockfd); return 0; }
Кратко поясним принцип работы приведенного кода. В самом начале программа проверяет задан ли в качестве параметра номер порта, который необходимо прослушивать. Если порт не задан в качестве входного параметра, то выводится сообщение об ошибке и происходит выход из программы. Если порт задан, то создается TCP-сокет , затем осуществляется связь данного сокета с портом, после этого данный сокет переводится в режим прослушивания указанного порта. Далее запускается цикл, на каждой итерации которого проверяется, если получены какие либо данные, то они пересылаются назад отправителю. Выход из цикла осуществляется по нажатию клавиши ENTER .
Теперь перейдем к разработке универсального эхо-сервера, который перенаправляет все пакеты, который будет функционировать по следующему алгоритму:
- после запуска на выполнение эхо-сервер определяет параметры сетевого интерфейса eth0, такие как IP-адрес, MAC-адрес и переводит интерфейс в неразборчивый режим ( promiscuous mode ). В этом режиме интерфейс принимает все пакеты, циркулирующие в сети, даже если они не адресованы данному хосту;
- создается пакетный сокет и выполняется его привязка к выбранному сетевому интерфейсу (eth0). Далее анализатор в бесконечном цикле выполняет прием сетевых пакетов и отображает данные об этом пакете - MAC-адреса и IP-адреса отправителя и получателя, размер пакета, размер IP заголовка, тип транспортного протокола (TCP/UDP), порт отправителя и получателя. Выход из цикла осуществляется по приходу сигнала SIGINT (генерируется комбинацией клавиш Ctrl-C );
- получив сигнал SIGINT, анализатор прерывает цикл приема пакетов, снимает флаг неразборчивого режима с сетевого интерфейса и завершает выполнение.
Определять параметры сетевого интерфейса и переключать его режимы будет функция getifconf (). Прототип данной функции выглядит следующим образом:
int getifconf (__u8 *, struct ifparam *, int )
Функция принимает три параметра:
- указатель на строку, содержащую символьное имя сетевого интерфейса;
- указатель на структуру, в которой будут сохранены параметры сетевого интерфейса. Определение этой структуры будет рассмотрено ниже;
- флаг, определяющий режим работы интерфейса
Создавать пакетный сокет будет функция getsock_recv ():
int getsock_recv ( int )
Параметром функции является индекс сетевого интерфейса, к которому будет привязан сокет. Далее переводим сетевой интерфейс в режим прослушивания. Для этого получаем значения флагов текущего режима:
if(ioctl(fd, SIOCGIFFLAGS, &ifr) < 0) { perror("ioctl SIOCGIFFLAGS"); close(fd); return -1; }
В зависимости от значения третьего параметра функции, устанавливаем или снимаем флаг неразборчивого режима:
if(mode) ifr.ifr_flags |= IFF_PROMISC; else ifr.ifr_flags &= ~(IFF_PROMISC);
Устанавливаем новое значение флагов интерфейса:
if(ioctl(fd, SIOCSIFFLAGS, &ifr) < 0) { perror("ioctl SIOCSIFFLAGS"); close(fd); return (-1); }
Далее нам необходимо создать пакетный сокет, который будет принимать пакеты из сети на канальном уровне (кадры Ethernet), так как нам необходимо обрабатывать не только IP-адреса, но и MAC-адреса (подробнее об этом будет рассказано ниже). При работе с пакетными сокетами для хранения адресной информации сетевого интерфейса вместо структуры sockaddr_in используется структура sockaddr_ll, которая обьявлена в файле linux/if_packet.h
struct sockaddr_ll s_ll; /*создаем пакетный сокет*/ sd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if(sd < 0) return -1; memset((void *)&s_ll, 0, sizeof(struct sockaddr_ll));
Также необходимо заполнить поля адресной структуры:
s_ll.sll_family = PF_PACKET; // тип сокета s_ll.sll_protocol = htons(ETH_P_ALL); // тип принимаемого протокола s_ll.sll_ifindex = index; // индекс сетевого интерфейса
Если у вас несколько сетевых интерфейсов, то вам необходимо привязать сокет к одному из них, однако если вы хотите перехватывать и перенаправлять пакеты со всех сетевых интерфейсов, то вам привязку сокета осуществлять не нужно. Привязка сокета к интерфейсу осуществляется функцией:
bind(sd,(struct sockaddr *)&s_ll,sizeof(struct sockaddr_ll))
Все подготовительные этапы пройдены и теперь нам необходимо написать цикл приема пакетов и перенаправления их. Однако если мы сейчас напишем данный цикл, то у нас возникнет следующая проблема. При приеме любого пакета наш эхо-сервер меняет IP-адреса отправителя и получателя и отправляет пакет обратно в сеть. Все как и должно быть, однако здесь не все так просто… Тот пакет, который наш эхо-сервер отправил в сеть (с измененными IP-адресами), будет снова перехвачен самим эхо-сервером и опять произойдет смена IP-адресов о отправка пакета в сеть и т.д. до бесконечности, т.е. происходит зацикливание эхо-сервера, и генерируется лавинообразный поток пакетов как отправителю так и получателю первого принятого пакета. Для того чтобы отличать пакеты отправленные эхо-сервером и не перенаправлять их снова нам необходимо воспользоваться MAC-адресами в пакете, именно поэтому мы создали пакетный сокет, а не обычный RAW - сокет . Таким образом при захвате пакета нам необходимо проверять MAC-адрес отправителя пакета и если это собственный MAC-адрес (так как MAC-адреса в пакетах формируются автоматически), то этот пакет просто игнорировать и не отправлять назад в сеть.
for(;;) { memset(buff, 0, ETH_FRAME_LEN); rec = recvfrom(eth0_if, (char*)buff, ifp.mtu + 18, 0, NULL, NULL); if(rec < 0 || rec >ETH_FRAME_LEN) { perror("recvfrom: "); return -1; }; //выделяем память под IP пакет (без заголовка Ethernet) memset(buff_send,0,rec-14); memcpy((void *)&buff_send,buff+14,rec-14); //выделение заголовков протоколов memcpy((void *)ð, buff, ETH_HLEN); memcpy((void *)&ip, buff + ETH_HLEN, sizeof(struct iphdr)); memcpy((void *)&ip_send, buff_send, sizeof(struct iphdr)); if((ip.version) != 4) continue; memcpy((void *)&tcp, buff + ETH_HLEN+ ip.ihl * 4, sizeof(struct tcphdr)); memcpy((void *)&tcp_send, buff_send + ip_send.ihl * 4, sizeof(struct tcphdr)); /* *Проверяем MAC-адрес отправителя если это собственный MAC-адрес, * то пакет заново не перенаправлям */ if ((eth.h_source[0]==my_mac[0]) && (eth.h_source[1]==my_mac[1]) && (eth.h_source[2]==my_mac[2]) && (eth.h_source[3]==my_mac[3]) && (eth.h_source[4]==my_mac[4]) && (eth.h_source[5]==my_mac[5])) { continue; } /* *Меняем местами IP-адреса получателя и отправителя */ ip_send.saddr=ip.daddr; ip_send.daddr=ip.saddr; //себе пакеты не перенаправляем if (ip_send.daddr==ifp.ip) { //printf("Себе пакеты не перенаправляем\n"); continue; }; //отчет о принятом пакете if (flag) fprintf(fp,"Принят пакет: \t%d.%d.%d.%d\t%d.%d.%d.%d\n ",buff_send[12], buff_send[13],buff_send[14],buff_send[15],buff_send[16], buff_send[17],buff_send[18],buff_send[19]); else printf("Принят пакет: \t%d.%d.%d.%d\t%d.%d.%d.%d\n ",buff_send[12], buff_send[13],buff_send[14],buff_send[15], buff_send[16],buff_send[17],buff_send[18],buff_send[19]); //выделение и изменеие IP адреса в принятом пакете ip1 = buff_send[12]; ip2 = buff_send[13]; ip3 = buff_send[14]; ip4 = buff_send[15]; buff_send[12] = buff_send[16]; buff_send[13] = buff_send[17]; buff_send[14] = buff_send[18]; buff_send[15] = buff_send[19]; buff_send[16] = ip1; buff_send[17] = ip2; buff_send[18] = ip3; buff_send[19] = ip4; //задание адреса назначения пакета inet_aton(inet_ntoa(ip_send.daddr),&addr.sin_addr); addr.sin_family=AF_INET; //создание сокета send_socket = socket(AF_INET,SOCK_RAW,IPPROTO_RAW); //отправка echo-пакета if ((rec<0) &&(rec>ETH_FRAME_LEN)) { res=sendto(send_socket,buff_send,rec-14,0, (struct sockaddr *)&addr,sizeof(addr)); if (res==0) { if (!flag) perror("Ошибка отправки пакета:\n "); } else { if (flag) fprintf(fp,"Отправлен пакет: \t%d.%d.%d.%d\t%d.%d.%d.%d \n", buff_send[12],buff_send[13],buff_send[14], buff_send[15],buff_send[16], buff_send[17],buff_send[18],buff_send[19]); else printf("Отправлен пакет: \t%d.%d.%d.%d\t%d.%d.%d.%d\n ",buff_send[12], buff_send[13],buff_send[14],buff_send[15],buff_send[16], buff_send[17],buff_send[18],buff_send[19]); } } ; //очистка сокета close(send_socket); }
Вот и все. Универсальный эхо-сервер практически готов. Полный его код с подробными комментариями можно скачать здесь.
Все вопросы, а также замеченные ошибки и неточности пишите автору.