在剖析ping之前我们先补充一点知识。。。
(1)结构体addinfo
头文件:#include<netdb.h>
struct addrinfo
{
int ai_flags;
int ai_family; //AF_INET,AF_INET6,UNIX etc
int ai_socktype; //STREAW,DATAGRAM,RAW
int ai_protocol; //IPPROTO_IP,IPPROTO_IPV4,IPPROTO_IPV6 etc
size_t ai_addrlen; //length of ai_addr
char* ai_canonname; //full hostname
struct sockaddr* ai_addr; //addr of host
struct addrinfo* ai_next;
}
参数 |
取值 |
值 |
说明 |
ai_family |
AF_INET |
2 |
IPv4 |
AF_INET6 |
23 |
IPv6 |
|
AF_UNSPEC |
0 |
协议无关 |
|
ai_protocol |
IPPROTO_IP |
0 |
IP协议 |
IPPROTO_IPV4 |
4 |
IPv4 |
|
IPPROTO_IPV6 |
41 |
IPv6 |
|
IPPROTO_UDP |
17 |
UDP |
|
IPPROTO_TCP |
6 |
TCP |
|
ai_socktype |
SOCK_STREAM |
1 |
流 |
SOCK_DGRAM |
2 |
数据报 |
|
ai_flags |
AI_PASSIVE |
1 |
被动的,用于bind,通常用于server socket |
AI_CANONNAME |
2 |
用于返回主机的规范名称 | |
AI_NUMERICHOST |
4 |
地址为数字串 |
(2)getopt函数(分析命令行参数):
头文件:#include<unistd.h>
函数原型: int getopt (int argc,char* const argv[ ],const char*optstring);
extern char* optarg;
extern int optind,opterr,optopt;
getopt()所设置的全局变量包括:
optarg-----指向当前选项参数(如果有)的指针。optind------再次调用getopt()时的下一个指针的索引。
optopt-----最后一个未知选项。
补充说明:
optstring中的指定的内容的意义(例如getopt(argc, argv, "ab:c:de::");)
1.单个字符,表示选项,(如上例中的abcde各为一个选项)
2.单个字符后接一个冒号:表示该选项后必须跟一个参数。参数紧跟在选项后或者以空格隔开。该参数的指针赋给optarg。(如上例中的b:c:)
3. 单个字符后跟两个冒号,表示该选项后可以跟一个参数,也可以不跟。如果跟一个参数,参数必须紧跟在选项后不能以空格隔开。该参数的指针赋给optarg。(如上例中的e::,如果没有跟参数,则optarg = NULL).
(3)getaddrinfo函数
gethostbyname和gethostbyaddr这两个函数仅仅支持IPv4,getaddrinfo函数能够处理名字到地址以及服务到端口这两种转换,返回的是一个sockaddr结构的链表而不是一个地址清单。
头文件:#include<netdb.h>------>linux下
#include<ws2tcpip.h>------->windows下
函数原型:
int getaddrinfo(const char *hostname,const char* service,const struct addrinfo *hints,
struct addrinfo**result)
参数说明:
hostname:一个主机名或者地址串(IPv4的点分十进制串或者IPv6的16进制串)
service:服务名可以是十进制的端口号,也可以是已定义的服务名称,如ftp、http等
hints:可以是一个空指针,也可以是一个指向某个addrinfo结构体的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。举例来说:指定的服务既可支持TCP也可支持UDP,所以调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM使得返回的仅仅是适用于数据报套接口的信息。
result:本函数通过result指针参数返回一个指向addrinfo结构体链表的指针。
返回值:0——成功,非0——出错
(4)setuid函数:
setuid函数设置实际用户ID和有效用户ID。Linux的setuid函数和Unix中的setuid函数的行为是不同的。在Linux中, setuid(uid)函数的执行步骤为:(1)如果由普通用户调用,将当前进程的有效ID设置为uid. (2)如果由有效用户ID符为0的进程调用,则将真实,有效和已保存用户ID都设置为uid.
在Unix中.setuid(uid)函数的行为为: (1)如果进程没有超级用户特权,且uid等于实际用户ID或已保存用户ID,则将有效的用户ID设置为uid.否则返回错误.(2)如果进程是有超级用户特权,则将真实、有效和已保存用户表示符都设置为uid.如果两个条件都不满足,则设置errno为EPERM。
函数在执行成功的时候返回0,在出错的时候返回-1
(5)结构体struct msghdr,用于接收数据包,其定义如下:
struct msghdr{
void *msg_name;//保存数据包的目的地址
int msg_namelen;//地址长度
struct iovec *msg_iov;
__kernal_size_t msg_iovlen;
void *msg_control;
__kernal_size_t msg_controllen;
unsigned msg_flags;
};
该结构体可以分为四组:
第一组是msg_name和msg_namelen,记录这个消息的名字,其实就是数据包的目的地址。msg_name是指向一个结构体struct sockaddr的指针。
第二组msg_iov和msg_iovlen,记录这个消息的内容。msg_iov是一个指向结构体struct iovec的指针,实际 上,确切的说,应该是一个结构体struct iovec的数组。下面是该结构体的定义:
struct iovec{
void__user *iov_base;
__kernal_size_t iov_len;
}
iov_base指向数据包缓冲区,即参数buff,iov_len是buff的长度。msghdr中允许一次传递多个buff,以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度(即有多少个buff)。在我们的ping程序的实例中:
msg.msg_iov = { struct iovec = { iov_base = { icmp头+填充字符‘E‘ }, iov_len = 40 } }
msg.msg_len = 1
第三组是msg_control和msg_controllen,它们可被用于发送任何的控制信息,在我们的例子中,没有控制信息要发送。暂时略过。
第四组是msg_flags。其值即为传入的参数flags。raw协议不支持MSG_OOB向标志,即带外数据
(6)下来我们分别来看ip的报文格式和icmp报文格式:
在其中我们可以看到icmp报文是作为ip数据报的数据,所以icmp的报头起始位置等于ip报头加上其报文的长度。。。。
下面给出部分的源码(主要是针对IPv4进行注释):
ping.h:
#include "unp.h" #include <netinet/in_systm.h> #include <netinet/ip.h> #include <netinet/ip_icmp.h> #define BUFSIZE 1500 /* globals */ 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 init_v6(void); void proc_v4(char *, ssize_t, struct msghdr *, struct timeval *); void proc_v6(char *, ssize_t, struct msghdr *, 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 msghdr *, struct timeval *); void (*fsend)(void); void (*finit)(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; #ifdef IPV6 #include <netinet/ip6.h> #include <netinet/icmp6.h> #endif
main.c
#include "ping.h" struct proto proto_v4 = { proc_v4, send_v4, NULL, NULL, NULL, 0, IPPROTO_ICMP }; int datalen = 56; /* data that goes with ICMP echo request */ int main(int argc, char **argv) { int c; struct addrinfo *ai; char *h; opterr = 0; /* don't want getopt() writing to stderr */ //处理命令行参数 //例如:./ping 127.0.0.1 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保存ip地址 host = argv[optind]; //获取当前的进程号 pid = getpid() & 0xffff; /* ICMP ID field is 16 bits */ //发送信号函数 Signal(SIGALRM, sig_alrm); //在命令行参数中必须有一个主机名或ip地址,调用Host_serv函数进行处理 //返回addrinfo结构 ai = Host_serv(host, NULL, 0, 0); h = Sock_ntop_host(ai->ai_addr, ai->ai_addrlen); printf("PING %s (%s): %d data bytes\n", ai->ai_canonname ? ai->ai_canonname : h, h, datalen); /* 4initialize according to protocol */ //初始化协议结构体pr if (ai->ai_family == AF_INET) { pr = &proto_v4; } 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); }
Host_serv.c:
/* include host_serv */ #include "unp.h" 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 */ //其值为0代表:协议无关 hints.ai_family = family; /* AF_UNSPEC, AF_INET, AF_INET6, etc. */ hints.ai_socktype = socktype; /* 0, SOCK_STREAM, SOCK_DGRAM, etc. */ if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) return(NULL); return(res); /* return pointer to first on linked list */ } /* end host_serv */ /* * There is no easy way to pass back the integer return code from * getaddrinfo() in the function above, short of adding another argument * that is a pointer, so the easiest way to provide the wrapper function * is just to duplicate the simple function as we do here. */ 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 */ }
sock_ntop.c:
#include "unp.h" #ifdef HAVE_SOCKADDR_DL_STRUCT #include <net/if_dl.h> #endif /* include sock_ntop */ char * sock_ntop(const struct sockaddr *sa, socklen_t salen) { char portstr[8]; static char str[128]; /* Unix domain is largest */ switch (sa->sa_family) { //当是IPv4协议时 case AF_INET: { struct sockaddr_in *sin = (struct sockaddr_in *) sa; //点分十进制与二进制的转化 if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL) return(NULL); //将端口的网络字节序转换为主机字节序 if (ntohs(sin->sin_port) != 0) { snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port)); strcat(str, portstr); } return(str); } /* end sock_ntop */ #ifdef IPV6 case AF_INET6: { struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa; str[0] = '['; if (inet_ntop(AF_INET6, &sin6->sin6_addr, str + 1, sizeof(str) - 1) == NULL) return(NULL); if (ntohs(sin6->sin6_port) != 0) { snprintf(portstr, sizeof(portstr), "]:%d", ntohs(sin6->sin6_port)); strcat(str, portstr); return(str); } return (str + 1); } #endif #ifdef AF_UNIX case AF_UNIX: { struct sockaddr_un *unp = (struct sockaddr_un *) sa; /* OK to have no pathname bound to the socket: happens on every connect() unless client calls bind() first. */ if (unp->sun_path[0] == 0) strcpy(str, "(no pathname bound)"); else snprintf(str, sizeof(str), "%s", unp->sun_path); return(str); } #endif #ifdef HAVE_SOCKADDR_DL_STRUCT case AF_LINK: { struct sockaddr_dl *sdl = (struct sockaddr_dl *) sa; if (sdl->sdl_nlen > 0) snprintf(str, sizeof(str), "%*s (index %d)", sdl->sdl_nlen, &sdl->sdl_data[0], sdl->sdl_index); else snprintf(str, sizeof(str), "AF_LINK, index=%d", sdl->sdl_index); return(str); } #endif default: snprintf(str, sizeof(str), "sock_ntop: unknown AF_xxx: %d, len %d", sa->sa_family, salen); return(str); } return (NULL); } char * Sock_ntop(const struct sockaddr *sa, socklen_t salen) { char *ptr; if ( (ptr = sock_ntop(sa, salen)) == NULL) err_sys("sock_ntop error"); /* inet_ntop() sets errno */ return(ptr); }
proc_v4.c:
#include "ping.h" void proc_v4(char *ptr, ssize_t len, struct msghdr *msg, 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 */ //获取ip首部长度 hlen1 = ip->ip_hl << 2; /* length of IP header */ //如果不是ICMP协议,直接返回 if (ip->ip_p != IPPROTO_ICMP) return; /* not ICMP */ //icmp的头部等于ip+ip长度 icmp = (struct icmp *) (ptr + hlen1); /* start of ICMP header */ if ( (icmplen = len - hlen1) < 8) return; /* malformed packet */ //设置类型为回显 if (icmp->icmp_type == ICMP_ECHOREPLY) { if (icmp->icmp_id != pid) return; /* not a response to our ECHO_REQUEST */ if (icmplen < 16) return; /* not enough data to use */ //计算往返时间 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); } }
sig_alarm.c
#include "ping.h" void sig_alrm(int signo) { (*pr->fsend)(); alarm(1); return; }
send_v4.c:
#include "ping.h" 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++; memset(icmp->icmp_data, 0xa5, datalen); /* fill with pattern */ 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); }
程序的执行结果:
分别使用自己的ping命令和系统的ping命令。。。该ping程序目前没有系统ping的功能那么完整,至少能实现基本的检测。。。。。
上图就是我们整个程序的大致流程。。。。