#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 */
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.3为组成整个Ping程序的函数的概貌。
ping.h文件
/*包括IPv4和ICMPv4的基本头文件,定义部分全程变量以及函数原型 */ #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;
#include <stdio.h> #include <errno.h> #include <signal.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> #include <stdlib.h> #include <stdarg.h> #include "ping.h" #undef IPV6 // printf("%d bytes from %s: type = %d, code = %d\n", icmplen, Sock_ntop_host(pr->sarecv, pr->salen), icmp->icmp_type, icmp->icmp_code); #define Gettimeofday gettimeofday #define Sock_ntop_host(X,Y) inet_ntop(AF_INET,&(((struct sockaddr_in*)(X))->sin_addr),strip,Y) char strip[128] ; err_doit(int errnoflag, const char *fmt, va_list ap) { int errno_save; char buf[255]; errno_save = errno; /* value caller might want printed */ vsprintf(buf, fmt, ap); if (errnoflag) sprintf(buf+strlen(buf), ": %s", strerror(errno_save)); strcat(buf, "\n"); fflush(stdout); /* in case stdout and stderr are the same */ fputs(buf, stderr); fflush(stderr); /* SunOS 4.1.* doesn't grok NULL argument */ return; } err_sys(const char *fmt, ...) { va_list ap; va_start(ap, fmt); err_doit(1, fmt, ap); va_end(ap); exit(1); } err_quit(const char *fmt, ...) { va_list ap; va_start(ap, fmt); err_doit(0, fmt, ap); va_end(ap); exit(1); } 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_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); /*如果所收到的消息是一个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_ntop_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); } } /*给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 */ 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; } 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); } 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分组的头部并将其安在我们的缓冲区前 */ } void sig_alrm(int signo) { (*pr->fsend)(); alarm(1); return; /* probably interrupts recvfrom() */ } 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); if(sockfd<0){ perror("socket:"); exit(-1); } /*将进程的有效用户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; else err_sys("recvfrom error"); } Gettimeofday(&tval, NULL); (*pr->fproc)(recvbuf, n, &tval); } } struct addrinfo * Host_serv(const char *host, const char *serv, int family, int socktype) { int n; struct addrinfo hints, *res; bzero(&hints, sizeof(struct addrinfo)); hints.ai_flags = AI_CANONNAME; /* always return canonical name */ hints.ai_family = family; /* 0, AF_INET, AF_INET6, etc. */ hints.ai_socktype = socktype; /* 0, SOCK_STREAM, SOCK_DGRAM, etc. */ if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) err_quit("host_serv error for %s, %s: %s", (host == NULL) ? "(no hostname)" : host, (serv == NULL) ? "(no service name)" : serv, gai_strerror(n)); return(res); /* return pointer to first on linked list */ } 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 } 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处理接收分组事宜 */ readloop(); exit(0); }
[root@localhost raw_sock]# ./a.out -v 10.33.28.254
PING 10.33.28.254 (10.33.28.254): 56 data bytes
64 bytes from 10.33.28.254: seq=0, ttl=255, rtt=1.733 ms
64 bytes from 10.33.28.254: seq=1, ttl=255, rtt=1.668 ms
64 bytes from 10.33.28.254: seq=2, ttl=255, rtt=0.863 ms
64 bytes from 10.33.28.254: seq=3, ttl=255, rtt=0.990 ms
64 bytes from 10.33.28.254: seq=4, ttl=255, rtt=1.239 ms
64 bytes from 10.33.28.254: seq=5, ttl=255, rtt=4.184 ms
参考:http://www.cnblogs.com/s7vens/archive/2012/04/16/2451635.html