О Фирме
Загрузка программ и дополнительных модулей
Документация на программы информационной системы
Прайс лист
Партнеры фирмы
Форум
Бесплано распространяемое программное обеспечение, учебные материалы
Рейтинг@Mail.ru

Универсальный есно-север под 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);
}

Вот и все. Универсальный эхо-сервер практически готов. Полный его код с подробными комментариями можно скачать здесь.

Все вопросы, а также замеченные ошибки и неточности пишите автору.