原文地址:Linux 原始套接字--myping的实现 作者:草根老师
一、套接字的类型A.流套接字(SOCK_STREAM)
用于提供面向连接、可靠的数据传输服务,其使用传输层的TCP协议
B.数据报套接字(SOCK_DGRAM)
用于提供一个无连接、不可靠的服务,其使用传输层上的UDP协议
C.原始套接字(SOCK_RAM)
原始套接字是相对表中套接字(即前面两种套接字)而言的。它与标准套接字的区别是原始套接字可以读写内核没有处理的IP数据包,流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。
所以要访问其他协议的数据必须使用原始套接字。
二、ping命令
ping命令是用来查看网络上另一个主机系统的网络连接是否正常的一个工具。ping命令的工作原理:向网络上的另一个主机系统发送ICMP报文,如果指定系统得到了报文,它将把报文一模一样地传回给发送者。
来看看在linux下使用ping命令的效果:
从上面可以看到,ping命令执行后显示出被系统主机名(或域名)和相应 IP地址、返回给当前主机的ICMP报文顺序号、ttl生存时间和往返时间rtt(单位是豪秒,即千分之一秒)。这些信息对我们后面要实现的myping有提示作用。
三、ICMP的介绍
ICMP(Internet Control Message,网际控制报文协议)是为网关和目标主机而提供的一种差错控制机制,使它们在遇到差错时能把错误报告发给报文源发方。ICMP协议是IP层的一个协议,但是由于差错报告在发送给报文源发方时可能也要经过若干子网,因此牵涉到路由选择等问题,所以ICMP报文需通过IP协议来发送。
ICMP数据报的数据发送前需要两级封装:首先添加ICMP报头形成ICMP报文,在添加IP头形成IP数据报。
注意:IP头不需要我们实现,由内核协议栈自动添加,我们只需要实现ICMP报文。
A.在Linux环境下,IP头定义如下:
我们要实现ping,需要关注一下数据:
<1>IP报头长度IHL(Internet Header Length)
其以4字节为一个单位来记录IP报头的长度,由上述IP数据结构的ip_hl变量。所以实际IP报头的长度是ip_hl << 2。
<2>生存时间TTL(Time To Live),是以秒为单位,指出IP数据报能在网络上停留的最长时间,其值由发送方设定,并在经过路由的每一个节点减一,当该值为0时,数据报将被丢弃,是上述IP数据结构的ip_ttl变量。
B. ICMP报文
IPCMP报文分为两种:一是错误报告报文,二是查询报文。
注意:每个ICMP报头均包含类型、编码、校验和这三项内容,长度为:8位、8位、16位。其余选项则随ICMP的功能不同而不同。
ping命令只使用众多ICMP报文中的两种:"请求(ICMP_ECHO)"和"回应(ICMP_ECHOREPLY)"。在linux中定义如下:
这两种报文格式如下:
通过wirshark抓包格式如下:
ICMP报头在linux定义如下:
struct icmp{ u_int8_t icmp_type;/* type of message, see below */ u_int8_t icmp_code;/* type sub code */ u_int16_t icmp_cksum;/* ones complement checksum of struct */ union { u_char ih_pptr;/* ICMP_PARAMPROB */ struct in_addr ih_gwaddr;/* gateway address */ struct ih_idseq/* echo datagram */ { u_int16_t icd_id; u_int16_t icd_seq; } ih_idseq; u_int32_t ih_void;
/* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */ struct ih_pmtu { u_int16_t ipm_void; u_int16_t ipm_nextmtu; } ih_pmtu;
struct ih_rtradv { u_int8_t irt_num_addrs; u_int8_t irt_wpa; u_int16_t irt_lifetime; } ih_rtradv; } icmp_hun;#defineicmp_pptr icmp_hun.ih_pptr#defineicmp_gwaddr icmp_hun.ih_gwaddr#defineicmp_id icmp_hun.ih_idseq.icd_id(标识一个ICMP报文,一般我们用PID标识)#defineicmp_seq icmp_hun.ih_idseq.icd_seq(发送报文的序号)#defineicmp_void icmp_hun.ih_void#defineicmp_pmvoid icmp_hun.ih_pmtu.ipm_void#defineicmp_nextmtu icmp_hun.ih_pmtu.ipm_nextmtu#defineicmp_num_addrs icmp_hun.ih_rtradv.irt_num_addrs#defineicmp_wpa icmp_hun.ih_rtradv.irt_wpa#defineicmp_lifetime icmp_hun.ih_rtradv.irt_lifetime union { struct { u_int32_t its_otime; u_int32_t its_rtime; u_int32_t its_ttime; } id_ts; struct { struct ip idi_ip; /* options and then 64 bits of data */ } id_ip; struct icmp_ra_addr id_radv; u_int32_t id_mask; u_int8_t id_data[1]; } icmp_dun;#defineicmp_otime icmp_dun.id_ts.its_otime#defineicmp_rtime icmp_dun.id_ts.its_rtime#defineicmp_ttime icmp_dun.id_ts.its_ttime#defineicmp_ip icmp_dun.id_ip.idi_ip#defineicmp_radv icmp_dun.id_radv#defineicmp_mask icmp_dun.id_mask#defineicmp_data icmp_dun.id_data(可以看到id_data是含有一个元素的数组名,为什么这样干呀?思考...)};
以上红色部分使我们实现ping需要填充的部分。
规定:ICMP报头为8字节
<1>协议头校验和算法
unsigned short chksum(addr,len) unsigned short *addr; // 校验数据开始地址(注意是以2字节为单位) int len; // 校验数据的长度大小,以字节为单位{ int sum = 0; // 校验和 int nleft = len; // 未累加的数据长度 unsigned short *p; // 走动的临时指针,2字节为单位 unsigned short tmp = 0; // 奇数字节长度时用到
while( nleft > 1) { sum += *p++; // 累加 nleft -= 2; }
// 奇数字节长度
if(nleft == 1)
{
// 将最后字节压如2字节的高位
*(unsigned char *)&tmp = *(unsigned char *)p;
sum += tmp; }
//高位低位相加 sum = (sum >> 16) + (sum & 0xffff);
// 上一步溢出时(十六进制相加进位),将溢出位也加到sum中
sum += sum >> 16;
// 注意类型转换,现在的校验和为16位 tmp = ~sum;
return tmp;}
网际校验和算法,把被校验的数据16位进行累加,然后取反码,若数据字节长度为奇数,则数据尾部补一个字节的0以凑成偶数。此算法适用于IPv4、ICMPv4、IGMPV4、ICMPv6、UDP和TCP校验和,更详细的信息请参考RFC1071。
<2>rtt往返时间
为了实现这一功能,可利用ICMP数据报携带一个时间戳。使用以下函数生成时间戳:
获取系统时间,放在struct timeval的变量中,第二个参数tzp指针表示时区,一般都是NULL,大多数代码都是这样,我也没关注过。
其中tv_sec为秒数,tv_usec微秒数。在发送和接收报文时由gettimeofday分别生成两个timeval结构,两者之差即为往返时间,即ICMP报文发送与接收的时间差。
<3>数据统计
系统自带的ping命令当它发送完所有ICMP报文后,会对所有发送和所有接收的ICMP报文进行统计,从而计算ICMP报文丢失的比率。
注意:为达到此目标,我们在编写代码时,定义两个全局变量:接收计数器和发送计数器,用于记录ICMP报文接收和发送数目。丢失数目 = 发送总数 - 接收总数,丢失比率 = 丢失数目 / 发送总数。
四、myping的实现
<1>补充知识
判断一个字符串是否是 string :"192.168.1.45" 这样的字符串
if( inet_addr(string) == INADDR_NONE ){ .........}
<2>补充知识
通过协议名如"icmp"获取对应的协议编号
解释如下:
注意:
创建原始套接字的时候,就需要指定其协议编号.
struct protoent *protocol;int sockfd_ram;
if((protocol = getprotobyname("icmp")) == NULL){ perror("Fail to getprotobyname"); exit(EXIT_FAILURE);}
//我们一般在创建,流套接字和数据包套接字时指定的是0,代表让系统自己自动去识别if((sockfd_ram = socket(AF_INET,SOCK_RAM,protocol->p_proto)) < 0){ perror("Fail to socket"); exit(EXIT_FAILURE); }
<3>补充知识
通过主机名或域名获取其对应的ip地址
这个函数的传入值是域名或者主机名,例如"www.google.cn"等等。传出值,是一个hostent的结构。如果函数调用失败,将返回NULL。
hostent->h_name
表示的是主机的规范名。例如www.google.com的规范名其实是www.l.google.com。
hostent->h_aliases
表示的是主机的别名.www.google.com就是google他自己的别名。有的时候,有的主机可能有好几个别名,这些,其实都是为了易于用户记忆而为自己的网站多取的名字。
hostent->h_addrtype
表示的是主机ip地址的类型,到底是ipv4(AF_INET),还是pv6(AF_INET6)
hostent->h_length
表示的是主机ip地址的长度
hostent->h_addr_lisst
表示的是主机的ip地址,注意,这个是以网络字节序存储的。千万不要直接用printf带%s参数来打这个东西,会有问题的哇。所以到真正需要打印出这个IP的话,需要调用inet_ntop()。
这个函数,是将类型为af的网络地址结构src,转换成主机序的字符串形式,存放在长度为cnt的字符串中。返回指向dst的一个指针。如果函数调用错误,返回值是NULL。
#include <netdb.h>
#include <sys/socket.h>
#include <stdio.h>
int main(int argc, char **argv)
{
char *ptr, **pptr;
struct hostent *hptr;
char str[32];
ptr = argv[1];
if((hptr = gethostbyname(ptr)) == NULL)
{
printf(" gethostbyname error for host:%s\n", ptr);
return 0;
}
printf("official hostname:%s\n",hptr->h_name);
for(pptr = hptr->h_aliases; *pptr != NULL; pptr++)
printf(" alias:%s\n",*pptr);
switch(hptr->h_addrtype)
{
case AF_INET:
case AF_INET6:
pptr=hptr->h_addr_list;
for(; *pptr!=NULL; pptr++)
printf(" address:%s\n",
inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
printf(" first address: %s\n",
inet_ntop(hptr->h_addrtype, hptr->h_addr, str, sizeof(str)));
break;
default:
printf("unknown address type\n");
break;
}
return 0;
}
编译运行
-----------------------------
# gcc test.c
# ./a.out www.baidu.com
official hostname:www.a.shifen.com
alias:www.baidu.com
address:121.14.88.11
address:121.14.89.11
first address: 121.14.88.11
<4>myping源码
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <errno.h>
- #include <sys/socket.h>
- #include <sys/types.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <netdb.h>
- #include <sys/time.h>
- #include <netinet/ip_icmp.h>
- #include <unistd.h>
- #include <signal.h>
- #define MAX_SIZE 1024
- char send_buf[MAX_SIZE];
- char recv_buf[MAX_SIZE];
-
int nsend = 0,nrecv = 0;
-
int datalen = 56;
-
//统计结果
- void statistics(int signum)
-
{
- printf("\n----------------PING statistics---------------\n");
- printf("%d packets transmitted,%d recevid,%%%d lost\n",nsend,nrecv,(nsend - nrecv)/nsend * 100);
- exit(EXIT_SUCCESS);
-
}
-
//校验和算法
-
int calc_chsum(unsigned short *addr,int len)
-
{
- int sum = 0,n = len;
- unsigned short answer = 0;
- unsigned short *p = addr;
-
- //每两个字节相加
- while(n > 1)
- {
- sum += *p ++;
- n -= 2;
- }
-
- //处理数据大小是奇数,在最后一个字节后面补0
- if(n == 1)
- {
- *((unsigned char *)&answer) = *(unsigned char *)p;
- sum += answer;
- }
-
- //将得到的sum值的高2字节和低2字节相加
- sum = (sum >> 16) + (sum & 0xffff);
-
- //处理溢出的情况
- sum += sum >> 16;
- answer = ~sum;
- return answer;
-
}
-
int pack(int pack_num)
-
{
- int packsize;
- struct icmp *icmp;
- struct timeval *tv;
- icmp = (struct icmp *)send_buf;
- icmp->icmp_type = ICMP_ECHO;
- icmp->icmp_code = 0;
- icmp->icmp_cksum = 0;
- icmp->icmp_id = htons(getpid());
- icmp->icmp_seq = htons(pack_num);
- tv = (struct timeval *)icmp->icmp_data;
- //记录发送时间
- if(gettimeofday(tv,NULL) < 0)
- {
- perror("Fail to gettimeofday");
- return -1;
- }
-
- packsize = 8 + datalen;
- icmp->icmp_cksum = calc_chsum((unsigned short *)icmp,packsize);
-
- return packsize;
-
}
-
int send_packet(int sockfd,struct sockaddr *paddr)
-
{
- int packsize;
-
- //将send_buf填上a
- memset(send_buf,'a',sizeof(send_buf));
- nsend ++;
- //打icmp包
- packsize = pack(nsend);
- if(sendto(sockfd,send_buf,packsize,0,paddr,sizeof(struct sockaddr)) < 0)
- {
- perror("Fail to sendto");
- return -1;
- }
- return 0;
-
}
- struct timeval time_sub(struct timeval *tv_send,struct timeval *tv_recv)
-
{
- struct timeval ts;
- if(tv_recv->tv_usec - tv_send->tv_usec < 0)
- {
- tv_recv->tv_sec --;
- tv_recv->tv_usec += 1000000;
- }
- ts.tv_sec = tv_recv->tv_sec - tv_send->tv_sec;
- ts.tv_usec = tv_recv->tv_usec - tv_send->tv_usec;
- return ts;
-
}
-
int unpack(int len,struct timeval *tv_recv,struct sockaddr *paddr,char *ipname)
-
{
- struct ip *ip;
- struct icmp *icmp;
- struct timeval *tv_send,ts;
- int ip_head_len;
- float rtt;
- ip = (struct ip *)recv_buf;
- ip_head_len = ip->ip_hl << 2;
- icmp = (struct icmp *)(recv_buf + ip_head_len);
-
- len -= ip_head_len;
- if(len < 8)
- {
- printf("ICMP packets\'s is less than 8.\n");
- return -1;
- }
-
- if(ntohs(icmp->icmp_id) == getpid() && icmp->icmp_type == ICMP_ECHOREPLY)
- {
- nrecv ++;
- tv_send = (struct timeval *)icmp->icmp_data;
- ts = time_sub(tv_send,tv_recv);
- rtt = ts.tv_sec * 1000 + (float)ts.tv_usec/1000;//以毫秒为单位
- printf("%d bytes from %s (%s):icmp_req = %d ttl=%d time=%.3fms.\n",
- len,ipname,inet_ntoa(((struct sockaddr_in *)paddr)->sin_addr),ntohs(icmp->icmp_seq),ip->ip_ttl,rtt);
- }
-
- return 0;
-
}
-
int recv_packet(int sockfd,char *ipname)
-
{
- int addr_len ,n;
- struct timeval tv;
- struct sockaddr from_addr;
-
- addr_len = sizeof(struct sockaddr);
- if((n = recvfrom(sockfd,recv_buf,sizeof(recv_buf),0,&from_addr,&addr_len)) < 0)
- {
- perror("Fail to recvfrom");
- return -1;
- }
- if(gettimeofday(&tv,NULL) < 0)
- {
- perror("Fail to gettimeofday");
- return -1;
- }
- unpack(n,&tv,&from_addr,ipname);
- return 0;
-
}
-
int main(int argc,char *argv[])
-
{
- int size = 50 * 1024;
- int sockfd,netaddr;
- struct protoent *protocol;
- struct hostent *host;
- struct sockaddr_in peer_addr;
-
- if(argc < 2)
- {
- fprintf(stderr,"usage : %s ip.\n",argv[0]);
- exit(EXIT_FAILURE);
- }
-
- //获取icmp的信息
- if((protocol = getprotobyname("icmp")) == NULL)
- {
- perror("Fail to getprotobyname");
- exit(EXIT_FAILURE);
- }
-
- //创建原始套接字
- if((sockfd = socket(AF_INET,SOCK_RAW,protocol->p_proto)) < 0)
- {
- perror("Fail to socket");
- exit(EXIT_FAILURE);
- }
- //回收root权限,设置当前用户权限
- setuid(getuid());
- /*
- 扩大套接子接收缓冲区到50k,这样做主要为了减少接收缓冲区溢出的可能性
- 若无影中ping一个广播地址或多播地址,将会引来大量应答
- */
- if(setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&size,sizeof(size)) < 0)
- {
- perror("Fail to setsockopt");
- exit(EXIT_FAILURE);
- }
- //填充对方的地址
- bzero(&peer_addr,sizeof(peer_addr));
- peer_addr.sin_family = AF_INET;
- //判断是主机名(域名)还是ip
- if((netaddr = inet_addr(argv[1])) == INADDR_NONE)
- {
- //是主机名(域名)
- if((host = gethostbyname(argv[1])) == NULL)
- {
- fprintf(stderr,"%s unknown host : %s.\n",argv[0],argv[1]);
- exit(EXIT_FAILURE);
- }
- memcpy((char *)&peer_addr.sin_addr,host->h_addr,host->h_length);
-
- }else{//ip地址
- peer_addr.sin_addr.s_addr = netaddr;
- }
-
- //注册信号处理函数
- signal(SIGALRM,statistics);
- signal(SIGINT,statistics);
- alarm(5);
- //开始信息
- printf("PING %s(%s) %d bytes of data.\n",argv[1],inet_ntoa(peer_addr.sin_addr),datalen);
- //发送包文和接收报文
- while(1)
- {
- send_packet(sockfd,(struct sockaddr *)&peer_addr);
- recv_packet(sockfd,argv[1]);
- alarm(5);
- sleep(1);
- }
- exit(EXIT_SUCCESS);
- }
注意:由于原始套接字的创建只能是拥有超级权限的进程创建,所以我们需要将我们编译好的可执行文件,把其文件所有者改为root,再将其set-uid-bit位进行设置。操作如下: