Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
iakovlev.org

Глава 25 : Raw socket

Код данной главы лежит тут

Символьные , или не структурированные сокеты (raw socket) , дают 3 возможности , которых нет в TCP :

  
 	1. Позволяют читать-писать пакеты icmp (ping)
 	2. Можно читать-писать ip-датаграммы   
 	3. Можно написать свой собственный протокол
 
При создании символьных сокетов нужно выполнить следующие шаги :
  
 	1. Вызвать функцию socket со вторым аргументом SOCK_RAW .
 		Например : 	int sockfd = socket(AF_INET , SOCK_RAW , protocol)
 		protocol - одна из констант IPPROTO_xxx , например IPPROTO_ICMP
 	2. Параметр сокета IP_HDRINCL можно установить так :
 		int on = 1;
 		if (setsockopt(sockfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on)) < 0 ) error ...
 	3. Использование bind необязательно
 	4. использование connect необязательно , т.к. в raw socket нет понятия порта  
 
Вывод на символьных сокетах регулируется следующими правилами :
  
 	1. Стандартный вывод делается с помощью sendto или sendmsg 
 	2. Если параметр IP_HDRINCL не установлен,начальный адрес данных начинается сразу после ip-заголовка
 	3. Если параметр IP_HDRINCL установлен ,  начальный адрес данных начинается с первого байта ip-заголовка 
 	4. Ядро фрагментирует пакеты , превышающие MTU .
 
Ввод регулируется следующими правилами :
  
 	1. Пакеты UDP и TCP никогда не передаются на символьный сокет
 	2. Все ICMP-пакеты передаются на символьный сокет
 	3. Все пакеты с непонятным протоколом передаются на символьный сокет
 
Ядро выбирает из нескольких символьных сокетов нужный по следующему принципу :
  
 	1. Протокол пакета должен совпасть с 3-м аргументом функции socket
 	2. Если сокет создан с помощью bind , ip-адрес в пакете должет совпасть с локальным ip
 	3. Для функции connect аналогично 
 
Реализуем общеизвестную программу ping , которая отличается от общеизвестной наличием всего одного параметра и поддержкой IPV6. Принцип работы ping следующий : посылается эхо-запрос icmp и на него приходит эхо-ответ. Формат ICMP-сообщений :
Здесь код=0 , идентификатор - id процесса ping , порядковый номер - это номер отправляемого пакета , в дополнительных данных - время .

Обзор функций программы ping :
Программа состоит из 2-х частей : одна читает , другая отсылает : Приведем текст хидера ping.h :

  
 //ping/ping.h
 
 #define	BUFSIZE		1500
 
 			/* globals */
 char	 recvbuf[BUFSIZE];
 char	 sendbuf[BUFSIZE];
 
 int		 datalen;			/* #bytes of data, following ICMP header */
 char	*host;
 int		 nsent;				/* add 1 for each sendto() */
 pid_t	 pid;				/* our PID */
 int		 sockfd;
 int		 verbose;
 
 			/* function prototypes */
 void	 proc_v4(char *, ssize_t, struct timeval *);
 void	 proc_v6(char *, ssize_t, struct timeval *);
 void	 send_v4(void);
 void	 send_v6(void);
 void	 readloop(void);
 void	 sig_alrm(int);
 void	 tv_sub(struct timeval *, struct timeval *);
 
 struct proto {
   void	 (*fproc)(char *, ssize_t, struct timeval *);
   void	 (*fsend)(void);
   struct sockaddr  *sasend;	/* sockaddr{} for send, from getaddrinfo */
   struct sockaddr  *sarecv;	/* sockaddr{} for receiving */
   socklen_t	    salen;		/* length of sockaddr{}s */
   int	   	    icmpproto;	/* IPPROTO_xxx value for ICMP */
 } *pr;
 
Структура proto служит для обработки различий между IPV4 и IPV6.

Функция main :

  
 struct proto	proto_v4 = { proc_v4, send_v4, NULL, NULL, 0, IPPROTO_ICMP };
 
 #ifdef	IPV6
 struct proto	proto_v6 = { proc_v6, send_v6, NULL, NULL, 0, IPPROTO_ICMPV6 };
 #endif
 
 int	datalen = 56;		/* data that goes with ICMP echo request */
 
 int main(int argc, char **argv)
 {
 	int				c;
 	struct addrinfo	*ai;
 
 	opterr = 0;		/* don't want getopt() writing to stderr */
 	while ( (c = getopt(argc, argv, "v")) != -1) {
 		switch (c) {
 		case 'v':
 			verbose++;
 			break;
 
 		case '?':
 			err_quit("unrecognized option: %c", c);
 		}
 	}
 
 	if (optind != argc-1)
 		err_quit("usage: ping [ -v ] < hostname>");
 	host = argv[optind];
 
 	pid = getpid();
 	Signal(SIGALRM, sig_alrm);
 
 	ai = Host_serv(host, NULL, 0, 0);
 
 	printf("PING %s (%s): %d data bytes\n", ai->ai_canonname,
 		   Sock_ntop_host(ai->ai_addr, ai->ai_addrlen), datalen);
 
 		/* 4initialize according to protocol */
 	if (ai->ai_family == AF_INET) {
 		pr = &proto_v4;
 #ifdef	IPV6
 	} else if (ai->ai_family == AF_INET6) {
 		pr = &proto_v6;
 		if (IN6_IS_ADDR_V4MAPPED(&(((struct sockaddr_in6 *)
 								 ai->ai_addr)->sin6_addr)))
 			err_quit("cannot ping IPv4-mapped IPv6 address");
 #endif
 	} else
 		err_quit("unknown address family %d", ai->ai_family);
 
 	pr->sasend = ai->ai_addr;
 	pr->sarecv = Calloc(1, ai->ai_addrlen);
 	pr->salen = ai->ai_addrlen;
 
 	readloop();
 
 	exit(0);
 }
 
Устанавливаем количество дополнительных данных - 56 байт . IP4-датаграмма будет длиной 84 байта : 56 байт данных , 20 байт ip-заголовка , 8 байт icmp-заголовка. IP6-датаграмма будет длиной 104 байта . Время отправки будет храниться в первых 8 байтах данных .

Символьный сокет создается в функции readloop

  
 void readloop(void)
 {
 	int				size;
 	char			recvbuf[BUFSIZE];
 	socklen_t		len;
 	ssize_t			n;
 	struct timeval	tval;
 
 	sockfd = Socket(pr->sasend->sa_family, SOCK_RAW, pr->icmpproto);
 	setuid(getuid());		/* don't need special permissions any more */
 
 	size = 60 * 1024;		/* OK if setsockopt fails */
 	setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
 
 	sig_alrm(SIGALRM);		/* send first packet */
 
 	for ( ; ; ) {
 		len = pr->salen;
 		n = recvfrom(sockfd, recvbuf, sizeof(recvbuf), 0, pr->sarecv, &len);
 		if (n < 0) {
 			if (errno == EINTR)
 				continue;
 			else
 				err_sys("recvfrom error");
 		}
 
 		Gettimeofday(&tval, NULL);
 		(*pr->fproc)(recvbuf, n, &tval);
 	}
 }
 
Пытаемся установить размер приемного буфера сокета 60 * 1024 , заведомо большего , чем по умолчанию - тем самым мы уменьшаем вероятность его переполнения . Запускаем обработчик сигнала SIGALRM раз в секунду

Основной цикл программы - бесконечный цикл , считывающий все пакеты , возвращаемые на наш сокет. На следующем рисунке приведены различные заголовки , указатели и длины , используемые в коде.

Функция обработки ICMP4 :

  
 void
 proc_v4(char *ptr, ssize_t len, struct timeval *tvrecv)
 {
 	int				hlen1, icmplen;
 	double			rtt;
 	struct ip		*ip;
 	struct icmp		*icmp;
 	struct timeval	*tvsend;
 
 	ip = (struct ip *) ptr;		/* start of IP header */
 	hlen1 = ip->ip_hl << 2;		/* length of IP header */
 
 	icmp = (struct icmp *) (ptr + hlen1);	/* start of ICMP header */
 	if ( (icmplen = len - hlen1) < 8)
 		err_quit("icmplen (%d) < 8", icmplen);
 
 	if (icmp->icmp_type == ICMP_ECHOREPLY) {
 		if (icmp->icmp_id != pid)
 			return;			/* not a response to our ECHO_REQUEST */
 		if (icmplen < 16)
 			err_quit("icmplen (%d) < 16", icmplen);
 
 		tvsend = (struct timeval *) icmp->icmp_data;
 		tv_sub(tvrecv, tvsend);
 		rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0;
 
 		printf("%d bytes from %s: seq=%u, ttl=%d, rtt=%.3f ms\n",
 				icmplen, Sock_ntop_host(pr->sarecv, pr->salen),
 				icmp->icmp_seq, ip->ip_ttl, rtt);
 
 	} else if (verbose) {
 		printf("  %d bytes from %s: type = %d, code = %d\n",
 				icmplen, Sock_ntop_host(pr->sarecv, pr->salen),
 				icmp->icmp_type, icmp->icmp_code);
 	}
 }
 
Чтобы убедиться , что ответ предназначен для нас , проверяем идентификатор , вычисляем RTT .

Обработчик сигнала SIGALRM :

  
 void
 sig_alrm(int signo)
 {
 	(*pr->fsend)();
 
 	alarm(1);
 	return;		/* probably interrupts recvfrom() */
 }
 
При отсылке запроса вычисляется контрольная сумма ICMP по заголовку и всем следующим за ним данным :

  
 void
 send_v4(void)
 {
 	int			len;
 	struct icmp	*icmp;
 
 	icmp = (struct icmp *) sendbuf;
 	icmp->icmp_type = ICMP_ECHO;
 	icmp->icmp_code = 0;
 	icmp->icmp_id = pid;
 	icmp->icmp_seq = nsent++;
 	Gettimeofday((struct timeval *) icmp->icmp_data, NULL);
 
 	len = 8 + datalen;		/* checksum ICMP header and data */
 	icmp->icmp_cksum = 0;
 	icmp->icmp_cksum = in_cksum((u_short *) icmp, len);
 
 	Sendto(sockfd, sendbuf, len, 0, pr->sasend, pr->salen);
 }
 
Перед отправкой нужно посчитать контрольную сумму для Интернета , которая является суммой обратных кодов (см. in_cksum).

Теперь напишем свою облегченную версию программы traceroute . Она у меня например выдает такой результат :

  
 # ./traceroute iakovlev.org
 	traceroute to iakovlev.org (62.213.78.144): 30 hops max, 12 data bytes
 	...     
  3   ae0-201.RT.M9.MSK.RU.retn.net (87.245.255.53)  1.782 ms  1.115 ms  1.221 ms
  4   ReTN-Caravan.ge1-4.m9-3.caravan.ru (87.245.255.146)  1.123 ms  1.134 ms  1.100 ms
  5   vlan65.ge1-3.office-1.caravan.ru (217.23.131.150)  1.494 ms  1.519 ms  1.644 ms
  6   iakovlev.org (62.213.78.144)  1.569 ms  1.585 ms  2.215 ms
 	
 Результат кстати совпадает один в один со стандартной утилитой 
 
Она начинает свою работу с отправки udp-датаграммы , которая вынуждает первый же маршрутизатор вернуть icmp-сообщение об ошибке Time exceeded . Затем полю TTL (ограничение пересылок) увеличивается значение до 1 , и посылается следующая udp-датаграмма , которая достигает второго маршрутизатора . Когда udp достигает получателя , необходимо его заставить вернуть icmp-ошибку Port unreachable .

Заголовочный файл trace.h

  
 //traceroute/trace.h
 
 #define	BUFSIZE		1500
 
 struct rec {					/* format of outgoing UDP data */
   u_short	rec_seq;			/* sequence number */
   u_short	rec_ttl;			/* TTL packet left with */
   struct timeval	rec_tv;		/* time packet left */
 };
 
 			/* globals */
 char	 recvbuf[BUFSIZE];
 char	 sendbuf[BUFSIZE];
 
 int		 datalen;			/* #bytes of data, following ICMP header */
 char	*host;
 u_short	 sport, dport;
 int		 nsent;				/* add 1 for each sendto() */
 pid_t	 pid;				/* our PID */
 int		 probe, nprobes;
 int		 sendfd, recvfd;	/* send on UDP sock, read on raw ICMP sock */
 int		 ttl, max_ttl;
 int		 verbose;
 
 			/* function prototypes */
 char	*icmpcode_v4(int);
 char	*icmpcode_v6(int);
 int		 recv_v4(int, struct timeval *);
 int		 recv_v6(int, struct timeval *);
 void	 sig_alrm(int);
 void	 traceloop(void);
 void	 tv_sub(struct timeval *, struct timeval *);
 
 struct proto {
   char	*(*icmpcode)(int);
   int	 (*recv)(int, struct timeval *);
   struct sockaddr  *sasend;	/* sockaddr{} for send, from getaddrinfo */
   struct sockaddr  *sarecv;	/* sockaddr{} for receiving */
   struct sockaddr  *salast;	/* last sockaddr{} for receiving */
   struct sockaddr  *sabind;	/* sockaddr{} for binding source port */
   socklen_t   		salen;	/* length of sockaddr{}s */
   int			icmpproto;	/* IPPROTO_xxx value for ICMP */
   int	   ttllevel;		/* setsockopt() level to set TTL */
   int	   ttloptname;		/* setsockopt() name to set TTL */
 } *pr;
 
Функция traceloop - основной цикл обработки:
  
 void
 traceloop(void)
 {
 	int					seq, code, done;
 	double				rtt;
 	struct rec			*rec;
 	struct timeval		tvrecv;
 
 	recvfd = Socket(pr->sasend->sa_family, SOCK_RAW, pr->icmpproto);
 	setuid(getuid());		/* don't need special permissions any more */
 
 	sendfd = Socket(pr->sasend->sa_family, SOCK_DGRAM, 0);
 
 	pr->sabind->sa_family = pr->sasend->sa_family;
 	sport = (getpid() & 0xffff) | 0x8000;	/* our source UDP port# */
 	sock_set_port(pr->sabind, pr->salen, htons(sport));
 	Bind(sendfd, pr->sabind, pr->salen);
 
 	sig_alrm(SIGALRM);
 
 	seq = 0;
 	done = 0;
 	for (ttl = 1; ttl <= max_ttl && done == 0; ttl++) {
 		Setsockopt(sendfd, pr->ttllevel, pr->ttloptname, &ttl, sizeof(int));
 		bzero(pr->salast, pr->salen);
 
 		printf("%2d  ", ttl);
 		fflush(stdout);
 
 		for (probe = 0; probe < nprobes; probe++) {
 			rec = (struct rec *) sendbuf;
 			rec->rec_seq = ++seq;
 			rec->rec_ttl = ttl;
 			Gettimeofday(&rec->rec_tv, NULL);
 
 			sock_set_port(pr->sasend, pr->salen, htons(dport + seq));
 			Sendto(sendfd, sendbuf, datalen, 0, pr->sasend, pr->salen);
 
 			if ( (code = (*pr->recv)(seq, &tvrecv)) == -3)
 				printf(" *");		/* timeout, no reply */
 			else {
 				char	str[NI_MAXHOST];
 
 				if (sock_cmp_addr(pr->sarecv, pr->salast, pr->salen) != 0) {
 					if (getnameinfo(pr->sarecv, pr->salen, str, sizeof(str),
 									NULL, 0, 0) == 0)
 						printf(" %s (%s)", str,
 								Sock_ntop_host(pr->sarecv, pr->salen));
 					else
 						printf(" %s",
 								Sock_ntop_host(pr->sarecv, pr->salen));
 					memcpy(pr->salast, pr->sarecv, pr->salen);
 				}
 				tv_sub(&tvrecv, &rec->rec_tv);
 				rtt = tvrecv.tv_sec * 1000.0 + tvrecv.tv_usec / 1000.0;
 				printf("  %.3f ms", rtt);
 
 				if (code == -1)		/* port unreachable; at destination */
 					done++;
 				else if (code >= 0)
 					printf(" (ICMP %s)", (*pr->icmpcode)(code));
 			}
 			fflush(stdout);
 		}
 		printf("\n");
 	}
 }
 
Нам нужны 2 сокета : символьный , на который мы читаем все вернувшиеся icmp-сообщения , и udp , с которого мы посылаем пробные пакеты .

Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье