Глава 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 , с которого мы посылаем пробные пакеты .
|