Универсальный есно-север под 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);
}
Вот и все. Универсальный эхо-сервер практически готов. Полный его код с подробными комментариями можно скачать здесь.
Все вопросы, а также замеченные ошибки и неточности пишите автору.



