25.1. 概述

原始套接口提供以下三种TCP及UDP套接口一般不提供的功能。

1. 使用原始套接口可以读写ICMPv4,IGMPv4,ICMPv6分组。例如:Ping程序,就使用原始套接口发送ICMP回射请求,并接受ICMP回射应答。用于多播路由的守护进程:mrouted,同样利用原始套接口来发送和接收IGMPv4分组。上述功能同样允许使用ICMP或IGMP构造的应用程序完成作为用户进程处理,而不必在增加过多的内核编码。例如,路由器发现守护进程(在Solaris 2.x下名为in.rdisc,TCPv1的附录F说明如何获得公开可得版本的源代码)即以这种方式构造。它处理内核完全不知道的两个ICMP消息(路由器通告和路由器征求)。

2. 使用原始套接口可以读写特殊的IPv4数据报,内核不处理这些数据报的IPv4协议字段。

3. 利用原始套接口,好似用IP_HDRINCL套接口选项可以构造自己的IPv4头部。

25.2. 原始套接口创建

创建一个原始套接口涉及以下几步:

1. 当第二个参数是SOCK_RAW时,调用socket函数创建一个原始套接口。第三个参数(协议)一般不应为0,例如,为了创建一个IPv4原始套接口,我们可以这样写:

int sockfd;
sockfd = socket(AF_INET, SOCK_RAW, protocol);

其中protocol参数值为形如IPPROTO_xxx的常值,由<netinet/in.h>头文件定义,如IPPROTO_IGMP。但要注意,头文件里定义了一个协议名,如IPPROTO_EGP,并不意味这内核肯定支持它。

为了防止普通用户向网络写自己的IP数据报,只有超级用户才有权创建原始套接口。

2. 可以设置IP_HDRINCL套接口选项:

const int on = 1;
if ( setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0 )

我们将在下一节介绍这个选项的效果。

3. 可以对原始套接口调用bind函数,但并不常用。该函数仅用来设置本地地址:对于一个原始套接口而言端口号没有什么意义。当进行输出的时候,bind设置在原始套接口上所发送的数据报中将用到的源IP地址(仅当IP_HDRINCL套接口选项未设置时),若不调用bind,则由内核将源IP地址设成外出接口的主IP地址。

4. 在原始套接口上可调用connect函数,但也不常用。connect函数仅设置目的地址,再重申一遍:端口号对原始套接口而言没有意义。对于输出而言,调用connect之后,由于目的地址已经指定,我们可以调用write或send,而不是sendto了。

25.3. 原始套接口输出

原始套接口的输出遵循以下规则:

1. 普通输出通过调用sendto或sendmsg并指定目的IP地址来完成。如果套接口已经连接,也可以调用write,writev或send

2. 如果IP_HDRINCL选项未设置,则内核写的数据起始地址指IP头部之后的第一个字节。因为这种情况下,内核将构造IP头部,并将它安在来自进程的数据之前。内核将IPv4头部的协议字段设置成用户在调用socket函数时所给的第三个参数。

3. 如果IP_HDRINCL选项已设置,则内核写的数据起始地址指IP头部的第一个字节。用户所提供的数据大小值必须包括头部的字节数,此时进程构造除了以下两项以外的整个IP头部:(a)IPv4标识字段可以设为0,要求内核设置该值。 (b)IPv4头部的校验和由内核来计算和存储。

4. 对于超出外出接口MTU的分组,内核将其分片。

IPv6的原始套接口与IPv4相比有以下几点不同:

1. IPv6原始套接口发送和接收的协议头部内,所有字段均使用网络字节序。

2. IPv6不存在类似于IPv4中IP_HDRINCL这样的套接口选项。使用IPv6原始套机口无法读写整个IPv6分组(包括扩展头部),不过通过套接口选项及辅助数据,我们几乎可得到IPv6头部的所有字段和所有扩展头部。但要读写整个IPv6数据报,仍必须使用数据链路访问。

3. IPv6原始套接口的校验和处理有差异

对于ICMPv6原始套接口而言,总是由内核来计算并在ICMPv6头部内存储其校验和。这一点与ICMPv4原始套接口有所不同,ICMPv4原始套接口由应用进程来计算并存储校验和。

25.4. 原始套接口输入

对于原始套接口输入,我们要回答的第一个问题是:接收到的哪些IP分组将传递给原始套接口。浙江遵循如下规则:

1. 接收到的TCP分组和UDP分组决不会传递给任何原始套接口,如果一个进程希望读取包括TCP或UDP分组的IP数据报,那么它们必须在数据链路层读入

2. 当内核处理完ICMP消息之后,绝大部分ICMP分组将传递给原始套接口。对源自Berkeley的实现而言,除了回射请求,时间戳请求和地址掩码请求将完全由内核处理以外,所有收到的ICMP分组都将传递给某个原始套接口。

3. 当内核处理完IGMP消息之后,所有IGMP分组都将传递给某个原始套接口。

4. 所有带有内核不能识别的协议字段的IP数据报都将传递给某个原始套机口。内核对这些分组唯一做的就是检验IP头部中的某些字段:IP版本,IPv4头部校验和,头部长度以及目的IP地址。

5. 如果数据报以片段形式到达,则该分组将在所有片段到达并重组后才传给原始套接口。

当内核准备好一个待传递的数据报之后,内核将对所有进程的原始套接口进行检查,以寻找所有匹配的套接口。每个匹配的套接口都将收到一个该IP数据报的拷贝。以下是对每个原始套接口所做的三个测试,只有当这三个测试都为真时,数据报才会递送给该套接口。

1. 如果在创建原始套接口时,所指定的protocol参数不为零(socket的第三个参数),则接收到的数据报的协议字段应与该值匹配。否则该数据报将不递送给该套接口。

2. 如果此原始套接口之上绑定了一个本地IP地址,那么接收到的数据报的目的IP地址应与该绑定地址相匹配,否则该数据报将不递送给该套接口。

3. 如果此原始套接口通过调用connect指定了一个对方IP地址,那么接收到的数据报的源IP地址应与该连接地址相匹配,否则该数据报将不递送给该套接口。

注意如果一个原始套机口以protocol参数为0的方式创建,并且为调用connect和bind,那么对于内核传递给原始套接口的每一个原始数据报,该套接口都会收到一份拷贝。此外当一个接收到的数据报传递给IPv4原始套机口时,整个数据报都将传递给进程。对于原始IPv6套接口,出扩展头部外的整个数据报内容都传递给套接口。

在传递给应用进程的IPv4头部内,ip_len,ip_off,ip_id是主机字节序的,其他字段是网络字节序的。在Linux下,所有字段均为网络字节序。

原始ICMPv6套接口接收绝大部分内核收到的ICMPv4消息,但是ICMPv6是ICMPv4的一个超集,包括ARP和IGMP的功能。这项需要过滤分组数量,过滤器用 struct icmp6_filter数据类型声明,该数据类型由<netinet/icmp6.h>定义。一个原始ICMPv6套接口的当前过滤器可用setsockopt和getsockopt来设置和获取,其中level参数为IPPROTO_CIMPV6,optname参数为ICMP6_FILTER。

以下是操作icmp6_filter结构的6个宏:

#include <netinet/icmp6.h>
void ICMP6_FILTER_SETPASSALL(struct icmp6_filter * filt);
void ICMP6_FILTER_SETBLOCKALL(struct icmp6_filter * filt);
void ICMP6_FILTER_SETPASS(int msgtype, struct icmp6_filter * filt);
void ICMP6_FILTER_SETBLOCK(int msgtype, struct icmp6_filter * filt);
int ICMP6_FILTER_WILLPASS(int msgtype, const struct icmp6_filter * filt);
int ICMP6_FILTER_WILLBLOCK(int msgtype, const struct icmp6_filter * filt);
/*返回值:如果过滤器传递(阻塞)相应消息类型为1,否则为0 */

以上这些宏调用中的filt参数是一个指向某个icmp6_filter变量的指针,前四个宏修改此icmp6_filter变量,后两个宏检查它。msgtype参数值在0~255之间,指定ICMP消息类型。

SETPASSALL宏设定所有消息类型均可传递给应用进程。SETBLOCKALL宏设定没有消息类型可传递。作为缺省,当一个ICMPv6原始套接口创建时,所有ICMPv6消息类型可传递给其应用进程。

SETPASS宏打开某个消息类型向该应用进程的传递,而SETBLOCK宏阻塞某个消息类型的传递。如果给定消息类型可以由过滤器传递时,WILLPASS宏返回1,否则返回0,如果给定消息类型被过滤器所阻塞时,WILLBLOCK宏返回1,否则返回0

作为例子,假想一个只接收ICMPv6路由器通过的应用程序:

struct icmp6_filter myfilt;
fd = Socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
ICMP6_FILTER_SETBLOCKALL(&myfilt);
ICMP6_FILTER_SETPASS(NO_ROUTER_ADVERT, &myfilt);
Setsockopt(fd, IPPROTO_IMCPV6, ICMP6_FILTER, &myfilt, sizeof(myfilt));

这个例子首先阻塞所有消息类型的传递,然后值传递路由器通告消息。

25.5. Ping程序

本章我们将开发一个同时支持IPv4和IPv6的Ping程序。此程序与公开可得版本有两点不同。首先,此程序忽略了公开可得Ping程序所支持的大量选项。其次,本程序同时支持IPv6和IPv4,而公开可得Ping程序只支持IPv4

Ping的操作非常简单,向某些IP地址发送一个ICMP回射请求,接着该节点返回一个ICMP回射应答。这两个ICMP消息在IPv4和IPv6下均得到支持。图25.1为ICMP消息的格式:

图25.2展示了程序运行的几个例子,第一个使用IPv4,第二个使用IPv6,注意一下#号提示符,它表示超级用户,因为只有超级用户才有权限创建原始套接口

图25.3为组成整个Ping程序的函数的概貌。

程序分两大部分:一部分读取一个原始套接口上收到的所有消息,并输出ICMP回射应答,另一部分每隔一秒发送一个回射请求。另一部分由SIGALRM信号每秒驱动一次。

图25.4给出所有程序都包括的头文件ping.h

/*包括IPv4和ICMPv4的基本头文件,定义部分全程变量以及函数原型 */
#include "unp.h"
#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.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 * );
/*我们使用proto结构来处理IPv4与IPv6的差异。此结构包含两个函数指针,两个套接口地址结果指针,这两个套接口地址结构的大小,以及ICMP的协议值。全程变量指针pr将指向为IPv4或IPv6初始化的某个proto结构 */
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;
/*我们包括两个定义IPv6和ICMPv6结构和常值的头文件 */
#ifdef IPV6
#include "ip6.h" /* should be <netinet/ip6.h> */
#include "icmp6.h" /* should be <netinet/icmp6.h> */
#endif

下面是main函数

#include "ping.h"
/*给IPv4和IPv6分别定义proto结构,由于不知道最终使用的是IPv4还是IPv6,套接口地址结构指针被初始化为NULL */
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_ICMP};
#endif
/*设置随同回射请求一起发送的可选数据长度为56个字节,这样,如产生IPv4数据报,则总长为84个字节(20字节IPv4头部,8字节ICMP头部);如果产生IPv6数据报,则总长104字节。回射请求中所包含的任何数据,都要由回射应答返回。在本例中,我们将在回射请求数据区的前8个字节内存储该回射请求发出的时间,以便在接收到回射应答后使用它来计算和输出RTT */
int datalen = 56;  /* data that goes with ICMP echo request */
int main(int argc, char * * argv)
{int c;struct addrinfo * ai;/*本程序唯一的命令行选项是 -v,它决定是否输出收到的大多数ICMP消息(当然,我们不输出属于运行中的另一个Ping进程的回射应答)。 */opterr = 0;  /* dont want getopt() writing to stderr */while( (c = getopt(argc, argv, "v") != -1 ){switch(c){case 'v':verbose++;break;case '?':err_quit("unrecongized option: %c", c);}}if( optind != argc -1 )err_quit("usage: ping[-v] <hostname>");host = argv[optind];pid = getpid();Signal(SIGALRM, sig_alrm);/*在命令行参数中必须有一个主机名或主机IP地址。这里调用host_serv来处理,返回的addrinfo结构含有地址族:或AF_INET或AF_INET6,接着全局pr初始化为正确的proto结构。我们还要通过IN6_IS_ADDR_V4MAPPED来确认一个IPv6地址不是一个IPv4映射的,这是因为即使有host_serv返回的地址是一个IPv6地址,发送给主机的仍然是IPv4地址。 */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);/* initialize 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} elseerr_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处理接收分组事宜 */readloop();exit(0);
}

下面是readloop函数

#include "ping.h"
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);/*将进程的有效用户ID设成进程的实际用户ID */setuid(getuid());  /* dont need special permissions any more *//*将套接口接收缓冲区设为61440字节,这要比缺省值大得多。这样做主要是为了减少接收缓冲区溢出的可能性 */size = 60 * 1024;  /* OK if setsockopt fails *//* */setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));/*调用信号处理程序发送一个分组,并调度下一次SIGALRM为一秒之后。 */sig_alrm(SIGALRM); /* send first packet *//*程序的主循环是一个无限循环,它读取返回给ICMP原始套接口的每个分组 */for ( ; ; ){len = pr->salen;n = recvfrom(sockfd, recvbuf, sizeof(recvbuf), 0, pr->sarecv, &len);if(n<0){if(errno == EINTR)continue;elseerr_sys("recvfrom error");}Gettimeofday(&tval, NULL);(*pr->fproc)(recvbuf, n, &tval);}
}

下面是tv_sub函数,用于将两个timeval结构相减,并将结果存入第一个timeval结构中。

#include "unp.h"
void tv_sub(struct timeval * out, struct timeval * in)
{if( (out->tv_usec -= in->tv_usec) < 0 )  /* out -= in */{--out->tv_sec;out->tv_usec += 1000000;}out->tv_sec -= in->tv_sec;
}

下面是proc_v4,用于处理所有收到的ICMPv4消息。图25.9标出了代码所用的各种头部,指针及长度。

#include "ping.h"
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;/*IPv4头部长度字段是以4字节为一个单位计数的,将它乘以4才是以字节为单位的头部长度。这使我们可以正确设置icmp以指向ICMP头部的起始地址 */  ip = (struct ip *) ptr; /* start of IP header */hlen1 = ip->ip_hi << 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);
  /*如果所收到的消息是一个ICMP回射应答,那么我们必须检查标识符字段,看看这个应答是不是对我们这个进程发出的请求的响应。如果在这个主机上进程被运行了多次,每个进程都将获得本主机所接收到的ICMP消息的一份拷贝 */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);/*通过将消息发出时间与当前时间相减,计算RTT */    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_notp_host(pr->sarecv, pr->salen), icmp->icmp_seq, ip->ip_ttl, rtt);} else if (verbose) /*如果用户指定了 -v 命令行选项,这段程序将输出所收到的所有其他ICMP消息的类型字段和代码字段 */{printf("%d bytes from %s: type = %d, code = %d\n", icmplen, Sock_ntop_host(pr->sarecv, pr->salen), icmp->icmp_type, icmp->icmp_code);}
}

ICMPv6消息由函数proc_v6处理,图25.11为ICMPv6应答结构:头部,指针及长度

#include "ping.h"
void proc_v6(char * ptr, ssize_t len, struct timeval * tvrecv)
{
#ifdef IPV6int hlen1, icmp6len;double rtt;struct ip6_hdr * ip6;struct icmp6_hdr * icmp6;struct itmeval * tvsend;/*IPv6头部长度固定为40字节,接下来是ICMPv6头部 */ip6 = (struct ip6_hdr *) ptr;  /* start of IPv6 header */hlen1 = sizeof(struct ip6_hdr);if(ip6->ip6_nxt != IPPROTO_ICMPV6)err_quit("next header not IPPROTO_ICMPV6");icmp6 = (struct icmp6_hdr *)(ptr + hlen1);if( (icmp6len = len - hlen1) < 8)err_quit("icmp6len (%d) < 8", icmp6len);/*如果ICMP消息类型为回射应答,则检查标识符字段,看是不是给我们的应答。如果一切正常,则计算RTT,并和序列号及IPv6跳限一起输出 */if(icmp6->icmp6_type == ICMP6_ECHO_REPLY){if(icmp6->icmp6_id != pid)return; /* not a response to our ECHO_REQUEST */if(icmp6len < 16)err_quit("icmp6len (%d) < 16", icmp6len);tvsend = (struct timeval *) (icmp6 + 1);tv_sub(tvrecv, tvsend);rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0;printf("%d bytes from %s: seq=%u, hlim=%d, rtt=%.3f ms\n", icmp6len, Sock_ntop_host(pr->sarecv, pr->salen), icmp6->icmp6_seq, ip6->ip6_hlim, rtt);} else  if(verbose) /*如果用户指定了 -v 选项,则输出接收到的所有其他ICMP消息的类型和代码字段 */{printf("%d bytes from %s: type = %d, code = %d\n", icmp6len, Sock_ntop_host(pr->sarecv, pr->salen), icmp6->icmp6_type, icmp->icmp6_code);}
#endif /* IPV6 */
}

我们的SIGALRM信号处理函数是sig_alrm函数

#include "ping.h"
void sig_alrm(int signo)
{(*pr->fsend)();alarm(1);return; /* probably interrupts recvfrom() */
}

函数send_v4构造一个ICMPv4回射请求消息并写入原始套接口

#include "ping.h"
void send_v4(void)
{int  len;struct  icmp * icmp;/*构造ICMPv4消息,用进程ID设置标识符字段,用全程变量nsent设置序列号,并为下一个分组将nsent增1,当前时间存入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);/*为了计算ICMP校验和,我们先设置校验和字段为0,然后调用函数in_cksum来计算校验和,将结果存入校验和字段。 */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);/*ICMP消息通过原始套接口发送。由于我们没有使用IP_HDRINCL,内核将为我们构造IPv4分组的头部并将其安在我们的缓冲区前 */}

函数in_cksum的一个实现,第一个while循环计算所有16位值只和,如果长度为奇数,则最后一个字节加入和中。这个实现对Ping程序而言还不错,但对内核使用的大数据量的校验和计算不大适合。

unsigned short
in_cksum(unsigned short * addr, int len)
{int  nleft = len;int  sum = 0;unsigned short * w = addr;unsigned short answer = 0;/* * Our algorithm is simple, using a 32 bit accumulator (sum), * we add sequential 16 bit words to it, and at the end, * fold back all the carry bits from the top 16 bits into the lower 16 bits*/while(nleft > 1){sum += *w++;nleft -= 2;}if(nleft == 1){*(unsigned char *)(&answer) = *(unsigned char *)w;sum += answer;}/* add back carry outs from top 16 bits to low 16 bits */sum = (sum >>16) + (sum & 0xffff);  /* add hi 16 to low 16 */sum += (sum >> 16); /* add carry */answer = ~sum; /* truncate to 16 bits */return(answer);
}

函数send_v6如下,它构造并发送ICMPv6回射请求。

#include "ping.h"
void send_v6()
{
#ifdef IPV6int len;struct icmp6_hdr * icmp6;icmp6 = (struct icmp6_hdr *)sendbuf;icmp6->icmp6_type = ICMP6_ECHO_REQUEST;icmp6->icmp6_code = 0;icmp6->icmp6_id = pid;icmp6->icmp6_seq = nsent++;Gettimeofday((struct timeval *)(icmp6+1), NULL);len = 8 + datalen; /* 8-byte ICMPv6 header */Sendto(sockfd, sendbuf, len, 0, pr->sasend, pr->salen);/* kernel calculates and stores checksum for us */
#endif /* IPV6 */
}

25.6. Traceroute程序

本节我们将开发一个自己的Traceroute程序,同时支持IPv4和IPv6

Traceroute的用途是确定从我们的主机到目的地之间IP数据报行进的路径。Traceroute使用IPv4的TTL字段或IPv6的跳限字段以及两个ICMP消息。一开始,它向目的主机发送一个TTL(或跳限)为1的UDP数据报。这个数据报导致头一跳的路由器返回一个“time exceeded in transmit(传输中超时)”错。接着每次对TTL加1,并分别发出UDP数据报,这样可以一次一次确定下一个路由器。当UDP数据报终于到达目的主机时,希望目的主机能返回一个“port unreachable(端口不可达)”错,这通过向一个期望未被使用的随机端口发送UDP数据报来实现。

下面是程序要使用的trace.h头文件

/*我们包括标准IPv4头文件,该头文件定义了IPv4,ICMPv4和UDP的结构及常值。rec结构用于定义我们将要发送的UDP数据报的数据部分。不过我们将发现,其实我们不需要检查这些数据,这是用于调试目的 */
#include "unp.h"
#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <netinet/udp.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 *);
/*定义一个proto结构来处理IPv4和IPv6两协议之间的不同,该结构含有函数指针、指向套接口地址结构的指针,及其他在两个IP版本之间有区别的常值。当main函数处理完目的地址以后,全程变量pr指针指向某个初始化为IPv4或IPv6类型的proto结构 */
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 */
} * ptr;
/*包括定义了IPv6及ICMPv6结构及常值的头文件 */
#ifdef IPV6
#include "ip6.h"  /* should be <netinet/ip.h> */
#include "icmp6.h"  /* should be <netinet/icmp6.h> */
#endif

下面为main函数,它处理命令行参数,给IPv4或IPv6初始化pr指针,并调用traceloop函数

#include "trace.h"
/*我们给IPv4和IPv6分别定义一个proto结构,不过直到函数尾,才为套接口地址结构的指针分配地址 */
struct proto  proto_v4 = {icmpcode_v4, recv_v4, NULL, NULL, NULL, NULL, 0, IPPROTO_ICMP, IPPROTO_IP, IP_TTL};
#ifdef  IPV6
struct proto  proto_v6 = {icmpcode_v6, recv_v6, NULL, NULL, NULL, NULL, 0, IPPROTO_ICMPV6, IPPROTO_IPV6, IPV6_UNICAST_HOPS};
#endif
/*最大TTL或跳限缺省设为30,不过 -m 参数可以让用户修改此值。对于每一个TTL值,我们将发三个控测分组。目的端口缺省设为32768+666,且每发一个UDP数据报就增1. */
int datalen = sizeof(struct rec);  /* defaults */
int max_ttl = 30;
int nprobes = 3;
u_short  dport = 32768 + 666;
int main(int argc, char * * argv)
{int  c;struct addrinfo * ai;/* -v 参数是程序输出收到的大多数ICMP消息 */opterr = 0;  /* dont want getopt() writing to stderr */while( ( c = getopt(argc, argv, "m:v" ) != -1){switch(c){case 'm':if( (max_ttl = atoi(optarg) ) <= 1)err_quit("invalid -m value");break;case 'v':verbose++;break;case '?':err_quit("unrecognized option: %c", c);}}if (optind != argc-1)err_quit("usage: traceroute [ -m <maxttl> -v] <hostname>");host = argv[optind];pid = getpid();Signal(SIGALRM, sig_alrm);/*使用host_serv函数处理目的的主机名或IP地址。该函数返回一个指向addrinfo结构的指针。根据返回地址类型的不同(IPv4或IPv6),初始化proto结构。将该结构指针存入全程变量pr */ai = Host_serv(host, NULL, 0, 0);printf("traceroute to %s (%s): %d hops max, %d data bytes\n", ai->ai_canonname, Sock_ntop_host(ai->ai_addr, ai->ai_addrlen), max_ttl, datalen);/* initialize 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 traceroute IPv4-mapped IPv6 address");
#endif} elseerr_quit("unknown address family %d", ai->ai_family);pr->sasend = ai->ai_addr;  /* contains destination address */pr->sarecv = Calloc(1, ai->ai_addrlen);pr->salast = Calloc(1, ai->ai_addrlen);pr->sabind = Calloc(1, ai->ai_addrlen);pr->salen = ai->ai_addrlen;traceloop(); /* */exit(0);
}

traceloop函数发送数据报并读取返回的ICMP消息,这是程序的主循环。

#include "trace.h"
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());  /* dont 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);elseprintf(" %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");}
}

h

25.8. 小结

转载于:https://www.cnblogs.com/s7vens/archive/2012/04/16/2451635.html

UNP Chapter 25 - 原始套接口相关推荐

  1. 路由套接口 unp17

    在路由器接口中支持三种类型的操作 1. 进程能通过写路由套接口向内核发消息. 2. 进程能在路由套接口上从内核读消息,这是核心通知进程已收到一个ICMP重定向消息并进行了处理的方式. 3. 进程可以用 ...

  2. 网络编程(原始套接字)

    原始套接字提供如下功能: 1.读写ICMPv4,IGMPv4及ICMPv6分组.如ping程序,就是使用原始套接口中发送ICMP回显请求,并接受ICMP回显应答 2.读写特殊的IPv4数据报.大多数内 ...

  3. 网络编程学习笔记(ICMPv6和IPv6套接口选项)

    ICMPv6套接口选项级别为IPPROTO_ICMPV6 ICMP6_FILTER: 获取和设置一个icmp6_filter结构,这指明256个可能的ICMPv6消息类型中哪一个传递给在原始套接口上的 ...

  4. 网络编程学习笔记(IPv4套接口选项)

    这些选项级别为IPPROTO_IP IP_HDRINCL: 如果此选项给一个原始IP套接口,必须为所有发判定以此原始套接口上的数据报构造自己的IP头部.一般情况下,内核为发送到原始套接口上的数据报构造 ...

  5. 套接口学习(一)实现

    套接口这个概念最先由4.2BSD(1983)引入.如今已经成为一个通用的网络应用程序编程接口.受到全部操作系统的支持.套接口层位于应用程序和 协议栈之间,相应用程序屏蔽了与协议相关实现的详细细节. 通 ...

  6. 网络编程——原始套接字实现原理

    目录 1. 基础知识 1.1.概述 1.2.链路层原始套接字 1.3.网络层原始套接字 2.原始套接字的实现 2.1  原始套接字报文收发流程 2.2链路层原始套接字的实现 2.2.1  套接字创建 ...

  7. 原始套接字-SOCK_RAW

    原始套接字 简介 套接口最常用的两种类型:SOCK_STREAM和SOCK_DGRAM. SOCK_STREAM: 流式套接口,传输的是字节流,每次传输的数据没有边界,它是面向连接的,底层使用TCP协 ...

  8. SOCK_DGRAM(数据报套接字)与SOCK_STREAM(流套接口)的区别

    一.SOCK_DGRAM (数据报套接字)特性: 1.分组在发送后,可能无序地到达接收端 2.分组可能丢失.如果发生丢失,不会采取任何补救的措施,而且接受端也不必知道有分租丢失. 3.数据报分组有尺寸 ...

  9. 原始套接字学习笔记(1)

    一般来说,我们会用到如下三种套接字: TCP:SOCK_STREAM套接字 UDP:SOCK_DGRAM套接字 原始套接字:SOCK_RAW套接字 对于TCP和UDP两种套接字,相对来说只要配置好IP ...

最新文章

  1. Lambda 表达式基础理论与示例
  2. 将python3.7降为3.5_python3.7降至3.5【python cookbook】python访问子字符串
  3. 俄罗斯议会下院通过“老大哥”反恐法
  4. 合并数组:双针模型,原地实现
  5. python自加1_python中有自增
  6. 软件测试 给视频添加字幕功能,巧用百度OCR文字识别技术,实现视频字幕识别...
  7. Tree Restoration Gym - 101755F (并查集)
  8. 《AngularJS实战》——3.2 过滤器的应用
  9. 【ffmpeg】不带透明通道的视频overlay
  10. 使用通达信获取股票历史数据
  11. PDCA过程模式在信息安全管理体系的应用
  12. 精准目标群体,精确博客选择——谈feedsky经典博客Market力作
  13. java连接云服务Hadoop伪分布式错误:Call From LAPTOP-14BPR3NI/192.168.1.2 to node1:9000 failed on connection
  14. 第九十五章 SQL函数 MINUTE
  15. Zookeeper集群启动异常: Cannot open channel to x at election address xx/xxx.xxx.xxx.xxx:3888
  16. 微擎mysql和redis_微擎如何开启redis,redis开启方法详解
  17. Linux下使用md5sum计算和检验MD5码
  18. 算术的逻辑运算与指令详解
  19. Charles抓手机包
  20. 项目部署uwsgi +Nginx

热门文章

  1. Android Retrofit下载文件进度
  2. 从事前端开发,日常工作中必备的工具有哪些?
  3. (免费领取Java面试题)Java面试中经常被问到的问题
  4. Linux设备模型 (1)
  5. order调用mdp
  6. Caused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Incorrect datetime value:
  7. 两种方法清空memcache
  8. Centos 下Nginx 自启动脚本
  9. systemverilog编译介绍
  10. 通过v$sqlarea,v$sql查询最占用资源的查询